From b7ee0701bbd14fe09e17c10da64a7cd358087148 Mon Sep 17 00:00:00 2001 From: Alessio Treglia Date: Fri, 8 Dec 2023 13:36:51 +0100 Subject: [PATCH] new pathctl command --- Makefile | 28 +++++-- cmd/pathctl/main.go | 146 +++++++++++++++++++++++++++++++++++++ go.mod | 10 ++- go.sum | 10 +++ internal/path/dirlist.go | 10 +++ internal/path/list.go | 79 ++++++++++++++++++++ internal/path/list_test.go | 54 ++++++++++++++ internal/path/path.go | 5 -- 8 files changed, 329 insertions(+), 13 deletions(-) create mode 100644 cmd/pathctl/main.go create mode 100644 internal/path/dirlist.go create mode 100644 internal/path/list.go create mode 100644 internal/path/list_test.go diff --git a/Makefile b/Makefile index 7436dfa..dfecf83 100644 --- a/Makefile +++ b/Makefile @@ -31,13 +31,14 @@ ifdef verbose VERBOSE = -v endif -all: build check +all: generate build check BUILD_TARGETS := build install +build: generate build: BUILD_ARGS=-o $(BUILDDIR)/ -$(BUILD_TARGETS): generate $(BUILDDIR)/ +$(BUILD_TARGETS): $(BUILDDIR)/ go $@ $(VERBOSE) -mod=readonly $(BUILD_FLAGS) $(BUILD_ARGS) ./... $(BUILDDIR)/: @@ -67,15 +68,28 @@ clean: rm -rf $(BUILDDIR) rm -f \ $(COVERAGE_REPORT_FILENAME) \ - generate-stamp + generate-stamp version-stamp + +version-stamp: generate + cp internal/version/version.txt $@ list: @echo $(BINS) | tr ' ' '\n' macos-codesign: build - codesign --verbose -s $(CODESIGN_IDENTITY) --options=runtime ./build/* - -unixtools.dmg: macos-codesign - create-dmg --volname unixtools --codesign $(CODESIGN_IDENTITY) --sandbox-safe $@ ./build + codesign --verbose -s $(CODESIGN_IDENTITY) --options=runtime $(BUILDDIR)/* + +unixtools.pkg: version-stamp macos-codesign + pkgbuild --identifier io.asscrypto.unixtools \ + --install-location ./Library/ --root $(BUILDDIR) $@ + +unixtools.dmg: version-stamp macos-codesign + VERSION=$(shell cat version-stamp); \ + mkdir -p dist/unixtools-$${VERSION}/bin ; \ + cp -a $(BUILDDIR)/* dist/unixtools-$${VERSION}/bin/ ; \ + chmod 0755 dist/unixtools-$${VERSION}/bin/* ; \ + create-dmg --volname unixtools --codesign $(CODESIGN_IDENTITY) \ + --sandbox-safe --no-internet-enable \ + $@ dist/unixtools-$${VERSION} .PHONY: all clean check distclean build list macos-codesign generate diff --git a/cmd/pathctl/main.go b/cmd/pathctl/main.go new file mode 100644 index 0000000..c0487e4 --- /dev/null +++ b/cmd/pathctl/main.go @@ -0,0 +1,146 @@ +package main + +import ( + "flag" + "fmt" + "log" + "os" + "path/filepath" + + "github.com/alessio/unixtools/internal/path" + "github.com/alessio/unixtools/internal/version" +) + +const ( + progName = "pathctl" +) + +var ( + helpMode bool + versionMode bool + pathListSep string +) + +var ( + envVar string + paths path.List +) + +func init() { + flag.BoolVar(&helpMode, "help", false, "display this help and exit.") + flag.BoolVar(&versionMode, "version", false, "output version information and exit.") + flag.StringVar(&pathListSep, "s", string(os.PathListSeparator), "path list separator.") + flag.StringVar(&envVar, "e", "PATH", "input environment variable") + flag.Usage = usage + flag.CommandLine.SetOutput(os.Stderr) +} + +func main() { + log.SetFlags(0) + log.SetPrefix(fmt.Sprintf("%s: ", progName)) + log.SetOutput(os.Stderr) + flag.Parse() + + handleHelpAndVersionModes() + + paths = path.NewPathList(envVar) + + if flag.NArg() < 1 { + list() + os.Exit(0) + } + + if flag.NArg() == 1 { + switch flag.Arg(0) { + case "list", "l": + list() + case "appendPathctlDir", "apd": + appendPath(exePath()) + case "prependPathctlDir", "ppd": + prepend(exePath()) + default: + log.Fatalf("unrecognized command: %s", flag.Arg(0)) + } + } else { + switch flag.Arg(0) { + case "prepend", "p": + prepend(flag.Arg(1)) + case "drop", "d": + drop(flag.Arg(1)) + case "append", "a": + appendPath(flag.Arg(1)) + } + } + + fmt.Println(paths.String()) +} + +func list() { + for _, p := range paths.StringSlice() { + fmt.Println(p) + } +} + +func prepend(p string) { + //oldPath := pathEnvvar + if ok := paths.Prepend(p); !ok { + log.Println("the path already exists") + } +} + +func drop(p string) { + if ok := paths.Drop(p); !ok { + log.Println("the path already exists") + } +} + +func appendPath(p string) { + if ok := paths.Append(p); !ok { + log.Println("the path already exists") + } +} + +func handleHelpAndVersionModes() { + if helpMode { + usage() + os.Exit(0) + } + + if versionMode { + version.PrintWithCopyright() + os.Exit(0) + } +} + +func usage() { + s := fmt.Sprintf(`Usage: %s COMMAND [PATH] +Make the management of the PATH environment variable +simple, fast, and predictable. + +Commands: + + append, a append a path to the end + drop, d drop a path + list, l list the paths + prepend, p prepend a path to the list + +Options: +`, progName) + _, _ = fmt.Fprintln(os.Stderr, s) + + flag.PrintDefaults() + + _, _ = fmt.Fprintln(os.Stderr, ` +If COMMAND is not provided, it prints the contents of the PATH +environment variable; the default output format is one path per +line.`) +} + +func exePath() string { + exePath, err := os.Executable() + if err != nil { + log.Fatal(err) + } + + return filepath.Dir(exePath) +} diff --git a/go.mod b/go.mod index 3b2b61d..ce0bdb4 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,11 @@ module github.com/alessio/unixtools -go 1.17 +go 1.21 + +require github.com/stretchr/testify v1.8.4 + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum index e69de29..fa4b6e6 100644 --- a/go.sum +++ b/go.sum @@ -0,0 +1,10 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/path/dirlist.go b/internal/path/dirlist.go new file mode 100644 index 0000000..7621372 --- /dev/null +++ b/internal/path/dirlist.go @@ -0,0 +1,10 @@ +package path + +// List handles a list of directories in a predictable way. +type List interface { + String() interface{} // + StringSlice() []string + Prepend(path string) bool + Append(path string) bool + Drop(path string) bool +} diff --git a/internal/path/list.go b/internal/path/list.go new file mode 100644 index 0000000..7d769bc --- /dev/null +++ b/internal/path/list.go @@ -0,0 +1,79 @@ +package path + +import ( + "os" + "path/filepath" + "slices" + "strings" +) + +var ListSeparator = string(os.PathListSeparator) + +type dirList struct { + lst []string +} + +func newDirList(lst []string) *dirList { + return &dirList{lst: lst} +} + +func NewPathList(v string) List { + return newDirList(makePathList(os.Getenv(v))) +} + +func (p *dirList) String() interface{} { + return strings.Join(p.lst, ListSeparator) +} + +func (p *dirList) StringSlice() []string { + return p.lst +} + +func (p *dirList) Prepend(path string) bool { + cleanPath := normalizePath(path) + if idx := slices.Index(p.lst, cleanPath); idx == -1 { + p.lst = append([]string{cleanPath}, p.lst...) + return true + } + + return false +} + +func (p *dirList) Append(path string) bool { + cleanPath := normalizePath(path) + if idx := slices.Index(p.lst, cleanPath); idx == -1 { + p.lst = append(p.lst, cleanPath) + return true + } + + return false +} + +func (p *dirList) Drop(path string) bool { + cleanPath := normalizePath(path) + if idx := slices.Index(p.lst, cleanPath); idx != -1 { + p.lst = slices.Delete(p.lst, idx, idx+1) + return true + } + + return false +} + +func makePathList(pathStr string) []string { + if pathStr == "" { + return nil + } + + rawList := strings.Split(pathStr, ListSeparator) + cleanList := make([]string, len(rawList)) + + for i, s := range rawList { + cleanList[i] = normalizePath(s) + } + + return cleanList +} + +func normalizePath(s string) string { + return filepath.Clean(s) +} diff --git a/internal/path/list_test.go b/internal/path/list_test.go new file mode 100644 index 0000000..196b5c1 --- /dev/null +++ b/internal/path/list_test.go @@ -0,0 +1,54 @@ +package path_test + +import ( + "fmt" + "github.com/alessio/unixtools/internal/path" + "github.com/stretchr/testify/require" + "testing" +) + +func TestPathList_Prepend(t *testing.T) { + envVarName := fmt.Sprintf("TEST_%s", t.Name()) + t.Setenv(envVarName, "/var/:/root/config:/Programs///") + lst := path.NewPathList(envVarName) + + require.Equal(t, lst.String(), "/var:/root/config:/Programs") + + require.True(t, lst.Prepend("/usr/local/go/bin")) + require.False(t, lst.Prepend("/usr/local/go/bin")) + require.False(t, lst.Prepend("/usr///local///go/bin/")) + + require.Equal(t, "/usr/local/go/bin:/var:/root/config:/Programs", lst.String()) + require.Equal(t, []string{"/usr/local/go/bin", "/var", "/root/config", "/Programs"}, lst.StringSlice()) +} + +func TestPathList_Append(t *testing.T) { + envVarName := fmt.Sprintf("TEST_%s", t.Name()) + t.Setenv(envVarName, "/var/:/root/config:/Programs///") + lst := path.NewPathList(envVarName) + + require.Equal(t, "/var:/root/config:/Programs", lst.String()) + + require.True(t, lst.Append("/usr/local/go/bin")) + require.False(t, lst.Append("/usr/local/go/bin")) + require.False(t, lst.Append("/usr///local///go/bin/")) + + require.Equal(t, "/var:/root/config:/Programs:/usr/local/go/bin", lst.String()) + require.Equal(t, []string{"/var", "/root/config", "/Programs", "/usr/local/go/bin"}, lst.StringSlice()) +} + +func TestPathList_Drop(t *testing.T) { + envVarName := fmt.Sprintf("TEST_%s", t.Name()) + t.Setenv(envVarName, + "/usr/local/bin:/home/user/.local/bin/:/usr/local/sbin:/var:/root") + lst := path.NewPathList(envVarName) + + require.Equal(t, "/usr/local/bin:/home/user/.local/bin:/usr/local/sbin:/var:/root", lst.String()) + require.False(t, lst.Drop("/etc")) // non existing + require.True(t, lst.Drop("/home/user/.local/bin")) + require.False(t, lst.Drop("/home/user/.local/bin")) + require.True(t, lst.Drop("/root/./")) + + require.Equal(t, "/usr/local/bin:/usr/local/sbin:/var", lst.String()) + require.Equal(t, []string{"/usr/local/bin", "/usr/local/sbin", "/var"}, lst.StringSlice()) +} diff --git a/internal/path/path.go b/internal/path/path.go index 936eed5..1cdacd6 100644 --- a/internal/path/path.go +++ b/internal/path/path.go @@ -1,7 +1,6 @@ package path import ( - "path" "strings" ) @@ -46,7 +45,3 @@ func RemoveDir(path string, s string) string { return strings.Join(newPath, ":") } - -func normalizePath(s string) string { - return path.Clean(strings.TrimRight(s, "/")) -}