diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4a8b8e8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +build/ +dist/ +nimcache/ + +*.stamp +*.bottle.tar.gz diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..1da089e --- /dev/null +++ b/.gitmodules @@ -0,0 +1,4 @@ +[submodule "submodules/commandant"] + path = submodules/commandant + url = https://github.com/tomekwojcik/commandant.git + branch = tomekwojcik_0_15_1 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..17d6241 --- /dev/null +++ b/CHANGELOG.md @@ -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. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c5bac90 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +pathsd by BTHLabs (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. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..980fdd1 --- /dev/null +++ b/Makefile @@ -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 diff --git a/NOTICE.txt b/NOTICE.txt new file mode 100644 index 0000000..47911a8 --- /dev/null +++ b/NOTICE.txt @@ -0,0 +1,18 @@ +pathsd by BTHLabs (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 diff --git a/README.md b/README.md index 795b24a..b249e92 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/freebsd/LICENSE b/freebsd/LICENSE new file mode 100644 index 0000000..6657117 --- /dev/null +++ b/freebsd/LICENSE @@ -0,0 +1 @@ +This package has a single license: MIT (MIT license / X11 license). diff --git a/freebsd/catalog.mk b/freebsd/catalog.mk new file mode 100644 index 0000000..103c66a --- /dev/null +++ b/freebsd/catalog.mk @@ -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 diff --git a/freebsd/manifest.in b/freebsd/manifest.in new file mode 100644 index 0000000..cb4a9ba --- /dev/null +++ b/freebsd/manifest.in @@ -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: <= 2.0.0" +# requires "commandant 0.15.1" diff --git a/src/pathsd.nim b/src/pathsd.nim new file mode 100644 index 0000000..454be96 --- /dev/null +++ b/src/pathsd.nim @@ -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 (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) diff --git a/submodules/commandant b/submodules/commandant new file mode 160000 index 0000000..0a2094c --- /dev/null +++ b/submodules/commandant @@ -0,0 +1 @@ +Subproject commit 0a2094c7b93d1e2385856e2f499eebc75c2f4af6 diff --git a/tests/fixtures/paths.d/01-bilbo b/tests/fixtures/paths.d/01-bilbo new file mode 100644 index 0000000..b097f03 --- /dev/null +++ b/tests/fixtures/paths.d/01-bilbo @@ -0,0 +1 @@ +/Users/bilbo/opt/bin diff --git a/tests/fixtures/paths.d/02-homebrew b/tests/fixtures/paths.d/02-homebrew new file mode 100644 index 0000000..5857719 --- /dev/null +++ b/tests/fixtures/paths.d/02-homebrew @@ -0,0 +1,2 @@ +/opt/homebrew/bin +/opt/homebrew/sbin diff --git a/tests/pathsd/test_main.nim b/tests/pathsd/test_main.nim new file mode 100644 index 0000000..225b440 --- /dev/null +++ b/tests/pathsd/test_main.nim @@ -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 diff --git a/tests/tester.nim b/tests/tester.nim new file mode 100644 index 0000000..11cbfdf --- /dev/null +++ b/tests/tester.nim @@ -0,0 +1 @@ +import pathsd/test_main