Initial release

This commit is contained in:
Tomek Wójcik 2023-12-05 20:13:26 +00:00
parent 804082112a
commit d1eb53b952
19 changed files with 385 additions and 1 deletions

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
build/
dist/
nimcache/
*.stamp
*.bottle.tar.gz

4
.gitmodules vendored Normal file
View File

@ -0,0 +1,4 @@
[submodule "submodules/commandant"]
path = submodules/commandant
url = https://github.com/tomekwojcik/commandant.git
branch = tomekwojcik_0_15_1

12
CHANGELOG.md Normal file
View File

@ -0,0 +1,12 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.0.2] - 2023-12-05
### Added
- Initial release.

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
pathsd by BTHLabs <contact@bthlabs.pl> (https://bthlabs.pl)
Copyright (c) 2023-present BTHLabs. All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

72
Makefile Normal file
View File

@ -0,0 +1,72 @@
VERSION = 1.0.1
PREFIX ?= /usr/local
OS != uname -o
SOURCES := src/pathsd.nim
VENDOR_SOURCES := submodules/commandant/commandant.nim
ALL_SOURCES := $(SOURCES) $(VENDOR_SOURCES)
ALL_STAMPS := ${ALL_SOURCES:.nim=.stamp}
OUT := build/pathsd
.POSIX:
.SUFFIXES:
.SUFFIXES: .stamp .nim
.PHONY: all test install-license install-license-freebsd install clean sdist bdist-freebsd
all: $(OUT)
build:
mkdir build
dist:
mkdir dist
# This is such a hack. I love it ;).
.nim.stamp:
touch $@
$(OUT): build $(ALL_STAMPS)
nimble build -d:release
test:
nimble test
install-license:
install LICENSE "$(DESTDIR)$(PREFIX)/share/doc/pathsd/"
install-license-freebsd:
install -d "$(DESTDIR)$(PREFIX)/share/licenses/pathsd-$(VERSION)/"
install freebsd/LICENSE "$(DESTDIR)$(PREFIX)/share/licenses/pathsd-$(VERSION)/"
install freebsd/catalog.mk "$(DESTDIR)$(PREFIX)/share/licenses/pathsd-$(VERSION)/" # Hmm?
install LICENSE "$(DESTDIR)$(PREFIX)/share/licenses/pathsd-$(VERSION)/MIT"
install: $(OUT) dist
install -d "$(DESTDIR)$(PREFIX)/bin"
install -d "$(DESTDIR)$(PREFIX)/share/doc/pathsd"
install -s build/pathsd "$(DESTDIR)$(PREFIX)/bin/"
install README.md "$(DESTDIR)$(PREFIX)/share/doc/pathsd/"
install CHANGELOG.md "$(DESTDIR)$(PREFIX)/share/doc/pathsd/"
install NOTICE.txt "$(DESTDIR)$(PREFIX)/share/doc/pathsd/"
if [ "$(OS)" = "FreeBSD" ];then make install-license-freebsd; else make install-license; fi
clean:
nimble clean
rm -f $(ALL_STAMPS)
rm -rf build/ dist/
sdist: build dist
rm -rf build/sdistroot/pathsd-${VERSION}
mkdir -p build/sdistroot/pathsd-${VERSION}
git ls-files --recurse-submodules | cpio -pd build/sdistroot/pathsd-${VERSION}
(cd build/sdistroot; tar cvf ../../dist/pathsd-${VERSION}.tar.gz pathsd-${VERSION}/)
bdist-freebsd: $(OUT) dist
make install DESTDIR=build/pkgroot
cat freebsd/manifest.in | sed -e 's|%%VERSION%%|${VERSION}|' | sed -e 's|%%PREFIX%%|${PREFIX}|' | sed -e 's|%%FLATSIZE%%|${:! du -c build/pkgroot | tail -n 1 | cut -f 1!:}|' > build/manifest
echo ${:! find build/pkgroot -type f !:C/^build\/pkgroot//} | tr ' ' '\n' > build/pkg-plist
pkg create -M build/manifest -p build/pkg-plist -r build/pkgroot/ -o dist/ -f tgz

18
NOTICE.txt Normal file
View File

@ -0,0 +1,18 @@
pathsd by BTHLabs <contact@bthlabs.pl> (https://bthlabs.pl)
Copyright (c) 2023-present BTHLabs. All rights reserved.
Licensed under terms of the MIT License
---
pathsd by BTHLabs includes the following third party software
commandant
Copyright (c) 2021 Casey McMahon
Copyright (c) 2013 Guillaume Viger
Licensed under terms of the MIT License
Nim -- a Compiler for Nim. https://nim-lang.org/
Copyright (C) 2006-2023 Andreas Rumpf. All rights reserved.
Licensed under terms of the MIT License

View File

@ -1,3 +1,58 @@
# pathsd
This repository contains the *pathsd* project.
*pathsd* is a small CLI utility to manage the `PATH` enironment variable.
## Building and installing
*pathsd* is written in Nim programming language. To build it, you need to
install the compiler. Consult
[nim documentation](https://nim-lang.org/install.html) for instructions on
installing nim on your OS. The required version is 2.0.0 or newer.
To build *pathsd*, issue the following command:
```
$ make
```
The built binary will be placed in `build/pathsd`. To install, copy it
somewhere.
## Usage
*pathsd* requires at least one directory of _parts_ to operate on, e.g.
```
paths.d/
├── 01-bilbo
└── 02-homebrew
```
Each of the files should have one or more lines, each line being a single
entry in the rendered `PATH` variable.
Running the program with such a directory would yield the following result:
```
$ pathsd paths.d/
export PATH="/Users/bilbo/opt/bin:/opt/homebrew/bin:/opt/homebrew/sbin":$PATH
```
You can specify multiple directories. They'll be processed one by one in the
order specified on the command line.
The program is best used in your shell's startup file, e.g.
```bash
eval $(pathsd paths.d/)
```
At the time of writing, only `bash` is supported.
## Author
*pathsd* is developed by [Tomek Wójcik](https://www.bthlabs.pl/)
## License
*pathsd* is licensed under the MIT License.

1
freebsd/LICENSE Normal file
View File

@ -0,0 +1 @@
This package has a single license: MIT (MIT license / X11 license).

5
freebsd/catalog.mk Normal file
View File

@ -0,0 +1,5 @@
_LICENSE=MIT
_LICENSE_NAME=MIT license / X11 license
_LICENSE_PERMS=dist-mirror dist-sell pkg-mirror pkg-sell auto-accept
_LICENSE_GROUPS=COPYFREE FSF GPL OSI
_LICENSE_DISTFILES=pathsd-1.0.0.tar.gz

13
freebsd/manifest.in Normal file
View File

@ -0,0 +1,13 @@
name: pathsd
version: %%VERSION%%
origin: bthlabs/infra
comment: BTHLabs pathsd
www: https://www.bthlabs.pl/
maintainer: contact@bthlabs.pl
prefix: %%PREFIX%%
flatsize: %%FLATSIZE%%
desc: <<EOD
BTHLabs pathsd
EOD
deps: {
}

1
nim.cfg Normal file
View File

@ -0,0 +1 @@
path = "submodules/commandant"

5
nimble.lock Normal file
View File

@ -0,0 +1,5 @@
{
"version": 2,
"packages": {},
"tasks": {}
}

16
pathsd.nimble Normal file
View File

@ -0,0 +1,16 @@
# Package
version = "1.0.1"
author = "Tomek Wójcik <contact@bthlabs.pl>"
description = "pathsd by BTHLabs"
license = "MIT"
binDir = "build"
srcDir = "src"
bin = @["pathsd"]
# Dependencies
requires "nim >= 2.0.0"
# requires "commandant 0.15.1"

120
src/pathsd.nim Normal file
View File

@ -0,0 +1,120 @@
import std/[algorithm, dirs, logging, options, paths, strutils, syncio]
import strformat
import commandant
type
ErrorCode = typeof(QuitSuccess)
MainResult* = object
output*: seq[string]
errorCode*: ErrorCode
const version = "1.0.1"
const usage_string = """usage: pathsd [--version | --help] [-v | -q] [-s shell] search_path ..."""
const version_string = fmt"""pathsd {version}"""
const help_string = fmt"""pathsd {version} by BTHLabs
Developed by Tomek Wójcik <contact@btlabs.pl> (https://bthlabs.pl/)
(c) 2023-present by BTHLabs | MIT License
Usage:
pathsd [options] search_path ...
Options:
-s --shell target shell (defaults to bash)
-v --verbose turn debugging messages on
-q --quiet supress all logging messages
--version show the version
--help show this help"""
const QuitInvalidArgs: ErrorCode = 64
var logger*: ConsoleLogger = newConsoleLogger(
fmtStr = "$datetime $appname $levelname: ",
levelThreshold = lvlInfo,
useStderr = true,
)
proc handleCommandantError(reason: ExitReason,
msg: string = "",
token: Option[CmdToken] = none(CmdToken),
) =
case reason
of ExitReason.missingArgumentValue, ExitReason.missingOptionValue:
quit(usage_string, QuitInvalidArgs)
of ExitReason.exception:
logger.log(lvlError, msg)
quit(QuitFailure)
proc readPart(path: Path): seq[string] =
logger.log(lvlDebug, fmt"Processing part: {path.string}")
var pathFile: File = open(path.string)
result = @[]
try:
var line: string = readLine(pathFile)
while line != "":
result.add(line)
line = readLine(pathFile)
except EOFError as exception:
discard
finally:
close(pathFile)
return result
proc main*(searchPaths: seq[string]): MainResult =
var parts: seq[string] = @[]
logger.log(lvlDebug, fmt"Processing search paths: {searchPaths}")
for searchPath in searchPaths:
var searchPathPath: Path = Path(searchPath)
if not dirExists(searchPathPath):
logger.log(lvlNotice, fmt"Skipping {searchPath}: does not exist?")
continue
var partPaths: seq[Path] = @[]
for pathComponent, path in walkDir(searchPathPath, true, false, true):
if pathComponent in [PathComponent.pcFile, PathComponent.pcLinkToFile]:
partPaths.add(searchPathPath / path)
partPaths.sort do (x: Path, y: Path) -> int:
result = cmp(x.string, y.string)
for partPath in partPaths:
parts = parts & readPart(partPath)
return MainResult(output: parts, errorCode: QuitSuccess)
when isMainModule:
commandline:
arguments(searchPaths, string, true)
commandant.option(shell, string, "shell", "s", "bash")
flag(verbose, "verbose", "v")
flag(quiet, "quiet", "q")
exitoption("help", "h", help_string, QuitFailure)
exitoption("version", "", version_string, QuitFailure)
errorproc(handleCommandantError)
var loggerLevel: Level = lvlInfo
if quiet:
loggerLevel = lvlNone
elif verbose:
loggerLevel = lvlDebug
logger.levelThreshold = loggerLevel
var mainResult = main(searchPaths)
if mainResult.output != @[] and mainResult.errorCode == QuitSuccess:
var output: string = ""
case shell
of "bash":
let pathComponents = join(mainResult.output, ":")
output = fmt"""export PATH="{pathComponents}":$PATH"""
if output != "":
echo(output)
quit(mainResult.errorCode)

1
submodules/commandant Submodule

@ -0,0 +1 @@
Subproject commit 0a2094c7b93d1e2385856e2f499eebc75c2f4af6

1
tests/fixtures/paths.d/01-bilbo vendored Normal file
View File

@ -0,0 +1 @@
/Users/bilbo/opt/bin

2
tests/fixtures/paths.d/02-homebrew vendored Normal file
View File

@ -0,0 +1,2 @@
/opt/homebrew/bin
/opt/homebrew/sbin

View File

@ -0,0 +1,30 @@
import std/[logging, paths]
import unittest
import ../../src/pathsd
suite "Test main() proc":
setup:
const fixturesPath = Path(currentSourcePath) / Path("..") / Path("..") / Path("fixtures")
const pathsdFixturePath = fixturesPath / Path("paths.d")
const expectedOutput = @["/Users/bilbo/opt/bin", "/opt/homebrew/bin", "/opt/homebrew/sbin"]
pathsd.logger.levelThreshold = lvlNone
test "Test happy path":
# When
var mainResult = pathsd.main(@[pathsdFixturePath.string])
# Then
check mainResult.output == expectedOutput
check mainResult.errorCode == QuitSuccess
test "Test skipping search paths that don't exist":
# When
var mainResult = pathsd.main(@[
pathsdFixturePath.string,
(fixturesPath / Path("idontexist")).string,
])
# Then
check mainResult.output == expectedOutput

1
tests/tester.nim Normal file
View File

@ -0,0 +1 @@
import pathsd/test_main