diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6973764..33941ec 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,9 +15,9 @@ jobs: with: fetch-depth: 0 fetch-tags: true - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: - go-version: '1.21' + go-version: '1.23' - run: go version - run: go mod tidy - run: go generate ./... diff --git a/.gitignore b/.gitignore index 1a98199..5a51788 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,5 @@ generate-stamp .envrc version.txt + +.vscode diff --git a/cmd/pathctl/main.go b/cmd/pathctl/main.go index 6ca8c15..0c25a39 100644 --- a/cmd/pathctl/main.go +++ b/cmd/pathctl/main.go @@ -7,19 +7,19 @@ import ( "os" "strings" + "al.essio.dev/pkg/tools/dirlist" "al.essio.dev/pkg/tools/internal/version" - "al.essio.dev/pkg/tools/pathlist" ) const ( - programme = "pathctl" + program = "pathctl" ) var ( helpMode bool versionMode bool listMode bool - noprefixMode bool + noPrefixMode bool dropMode bool ) @@ -27,55 +27,41 @@ var ( envVar string ) -var cmdHandlers map[string]func(d pathlist.List) +var cmdHandlers map[string]func(d dirlist.List) func init() { flag.BoolVar(&helpMode, "help", false, "display this help and exit.") flag.BoolVar(&versionMode, "version", false, "output version information and exit.") flag.BoolVar(&dropMode, "D", false, "drop the path before adding it again to the list.") - flag.BoolVar(&noprefixMode, "noprefix", false, "output the variable contents only.") + flag.BoolVar(&noPrefixMode, "noprefix", false, "output the variable contents only.") flag.BoolVar(&listMode, "L", false, "use a newline character as path list separator.") flag.StringVar(&envVar, "E", "PATH", "input environment variable.") flag.Usage = usage flag.CommandLine.SetOutput(os.Stderr) - cmdHandlers = func() map[string]func(pathlist.List) { - hAppend := func(d pathlist.List) { - if dropMode { - d.Drop(flag.Arg(1)) - } - d.Append(flag.Arg(1)) - } - hDrop := func(d pathlist.List) { d.Drop(flag.Arg(1)) } - hPrepend := func(d pathlist.List) { - if dropMode { - d.Drop(flag.Arg(1)) - } - d.Prepend(flag.Arg(1)) - } - - return map[string]func(pathlist.List){ - "append": hAppend, - "drop": hDrop, - "prepend": hPrepend, + cmdHandlers = func() map[string]func(dirlist.List) { + return map[string]func(dirlist.List){ + "append": cmdHandlerAppend, + "drop": cmdHandlerDrop, + "prepend": cmdHandlerPrepend, // aliases - "a": hAppend, - "d": hDrop, - "p": hPrepend, + "a": cmdHandlerAppend, + "d": cmdHandlerDrop, + "p": cmdHandlerPrepend, } }() } func main() { log.SetFlags(0) - log.SetPrefix(fmt.Sprintf("%s: ", programme)) + log.SetPrefix(fmt.Sprintf("%s: ", program)) log.SetOutput(os.Stderr) flag.Parse() handleHelpAndVersionModes() - dirs := pathlist.New() + dirs := dirlist.New() dirs.LoadEnv(envVar) if flag.NArg() < 1 { @@ -91,11 +77,11 @@ func main() { } } -func printPathList(d pathlist.List) { +func printPathList(d dirlist.List) { var sb = strings.Builder{} sb.Reset() - printPrefix := !noprefixMode + printPrefix := !noPrefixMode switch { case listMode: @@ -138,7 +124,7 @@ Commands: prepend, p prepend a path to the list. Options: -`, programme) +`, program) _, _ = fmt.Fprintln(os.Stderr, s) flag.PrintDefaults() @@ -152,3 +138,21 @@ element of the path list. If COMMAND is not provided, it prints the contents of the PATH environment variable.`) } + +func cmdHandlerAppend(d dirlist.List) { + if dropMode { + d.Drop(flag.Arg(1)) + } + d.Append(flag.Arg(1)) +} + +func cmdHandlerDrop(d dirlist.List) { + d.Drop(flag.Arg(1)) +} + +func cmdHandlerPrepend(d dirlist.List) { + if dropMode { + d.Drop(flag.Arg(1)) + } + d.Prepend(flag.Arg(1)) +} diff --git a/pathlist/list.go b/dirlist/list.go similarity index 68% rename from pathlist/list.go rename to dirlist/list.go index 0a5eeb4..cb47469 100644 --- a/pathlist/list.go +++ b/dirlist/list.go @@ -1,8 +1,9 @@ -// Package pathlist implements functions to manipulate PATH-like +// Package dirlist implements functions to manipulate PATH-like // environment variables. -package pathlist +package dirlist import ( + "fmt" "os" "path/filepath" "slices" @@ -19,9 +20,6 @@ type List interface { // Contains returns true if the list contains the path. Contains(string) bool - // Nil returns true if the list is emppty. - Nil() bool - // Load reads the list of directories from a string. Load(string) @@ -60,17 +58,13 @@ func New() List { } func (d *dirList) Contains(p string) bool { - return slices.Contains(d.lst, p) + return slices.Contains(d.lst, filepath.Clean(p)) } func (d *dirList) Reset() { d.init() } -func (d *dirList) Nil() bool { - return d.lst == nil || len(d.lst) == 0 -} - func (d *dirList) Load(s string) { d.src = s d.load() @@ -80,26 +74,25 @@ func (d *dirList) LoadEnv(s string) { d.Load(os.Getenv(s)) } -func (d *dirList) Slice() []string { - if d.Nil() { - return []string{} +func (d *dirList) Slice() (dst []string) { + if len(d.lst) == 0 { + return } - dst := make([]string, len(d.lst)) - n := copy(dst, d.lst) - if n != len(d.lst) { - panic("couldn't copy the list") + dst = make([]string, len(d.lst)) + if n := copy(dst, d.lst); n == len(d.lst) { + return dst } - return dst + panic("couldn't copy the list") } func (d *dirList) String() string { - if !d.Nil() { - return strings.Join(d.lst, string(filepath.ListSeparator)) + if len(d.lst) == 0 { + return "" } - return "" + return strings.Join(d.lst, string(filepath.ListSeparator)) } func (d *dirList) load() { @@ -108,7 +101,7 @@ func (d *dirList) load() { func (d *dirList) Append(path string) { p := filepath.Clean(path) - if d.Nil() { + if len(d.lst) == 0 { d.lst = []string{p} return } @@ -119,9 +112,10 @@ func (d *dirList) Append(path string) { } func (d *dirList) Drop(path string) { - if d.Nil() { + if len(d.lst) == 0 { return } + p := filepath.Clean(path) if idx := slices.Index(d.lst, p); idx != -1 { @@ -131,7 +125,7 @@ func (d *dirList) Drop(path string) { func (d *dirList) Prepend(path string) { p := filepath.Clean(path) - if d.Nil() { + if len(d.lst) == 0 { d.lst = []string{p} return } @@ -147,12 +141,16 @@ func (d *dirList) init() { } func (d *dirList) cleanPathVar() []string { - if d.src == "" { + return cleanPathVar(d.src) +} + +func cleanPathVar(src string) []string { + if src == "" { return nil } - pthSlice := filepath.SplitList(d.src) - if pthSlice == nil { + pthSlice := filepath.SplitList(src) + if len(pthSlice) == 0 { return nil } @@ -161,15 +159,20 @@ func (d *dirList) cleanPathVar() []string { func (d *dirList) clone(o *dirList) *dirList { o.src = d.src - o.lst = make([]string, len(d.lst)) - copy(o.lst, d.lst) + + n := len(d.lst) + o.lst = make([]string, n) + + if m := copy(o.lst, d.lst); n != m { + panic(fmt.Sprintf("copy: expected %d items, got %d", n, m)) + } return o } -func removeDups[T comparable](col []T, applyFn func(T) (T, bool)) []T { - var uniq = make([]T, 0) - ks := make(map[T]interface{}) +func removeDups(col []string, applyFn func(string) (string, bool)) []string { + var uniq = make([]string, 0) + ks := make(map[string]interface{}) for _, el := range col { vv, ok := applyFn(el) @@ -177,6 +180,7 @@ func removeDups[T comparable](col []T, applyFn func(T) (T, bool)) []T { continue } + vv = filepath.Join(filepath.Split(vv)) if _, ok := ks[vv]; !ok { uniq = append(uniq, vv) ks[vv] = struct{}{} @@ -187,10 +191,11 @@ func removeDups[T comparable](col []T, applyFn func(T) (T, bool)) []T { } var filterEmptyStrings = func(s string) (string, bool) { - clean := filepath.Clean(s) - if clean != "" { - return clean, true + if strings.TrimSpace(s) == "" { + return s, false } - return clean, false + // It'd pointless to check filepath.Clean()'s return + // value's nil-ness as it would never be "". + return filepath.Clean(s), true } diff --git a/dirlist/list_internal_test.go b/dirlist/list_internal_test.go new file mode 100644 index 0000000..603d406 --- /dev/null +++ b/dirlist/list_internal_test.go @@ -0,0 +1,36 @@ +package dirlist + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_newDirList(t *testing.T) { + d := new(dirList) + d.Reset() + require.NotNil(t, d) + require.Equal(t, "", d.String()) + + d.Append("/sbin") + require.Equal(t, "/sbin", d.String()) + + d1 := new(dirList) + d1.Reset() + d1 = d.clone(d1) + d.Append("/var") + require.NotEqual(t, &d, &d1) + d1.Prepend("/usr/bin") + d1.Append("/usr/local/bin") + require.Equal(t, "/usr/bin:/sbin:/usr/local/bin", d1.String()) +} + +func Test_removeDups(t *testing.T) { + require.Equal(t, + []string{"alpha", "bravo", "charlie", "."}, + removeDups( + []string{"alpha", "bravo", "charlie", "bravo", " ", "."}, + filterEmptyStrings, + ), + ) +} diff --git a/dirlist/list_test.go b/dirlist/list_test.go new file mode 100644 index 0000000..5bff6d8 --- /dev/null +++ b/dirlist/list_test.go @@ -0,0 +1,113 @@ +package dirlist_test + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" + + "al.essio.dev/pkg/tools/dirlist" +) + +func TestList_Append(t *testing.T) { + d := dirlist.New() + require.Equal(t, "", d.String()) + for _, p := range []string{"/var", "/var", "/bin", "/bin/", "/bin///"} { + d.Append(p) + } + + require.Equal(t, "/var:/bin", d.String()) + d.Prepend("/bin///") + require.Equal(t, "/var:/bin", d.String()) +} + +func TestList_Prepend(t *testing.T) { + d := dirlist.New() + dirs := []string{ + "/var", "/var", "/bin", "/bin/", + } + + for _, dir := range dirs { + d.Prepend(dir) + } + + d.Prepend("/sbin") + d.Prepend("/var") + d.Prepend("/usr/local/bin") + d.Prepend("/opt/local/bin") + require.Equal(t, "/opt/local/bin:/usr/local/bin:/sbin:/bin:/var", d.String()) +} + +func TestList_Drop(t *testing.T) { + d := dirlist.New() + d.Load("/opt/local/bin:/usr/local/bin:/sbin:/bin:/var:/bin") + d.Drop("/opt/local/bin") + d.Drop("/opt/local/bin") + d.Drop("/opt/local/bin") + d.Drop("/usr/local/bin") + d.Drop("/var") + require.NotEqual(t, "", d.String()) + d.Drop("/sbin") + d.Drop("/bin") + require.Equal(t, "", d.String()) + + require.NotPanics(t, func() { dirlist.New().Drop("") }) + + d1 := dirlist.New() + d1.Load(`/Library/Application Support:/Library/Application Support/`) + d1.Drop("/Library/Application Support") + require.False(t, d1.Contains("/Library/Application Support")) +} + +func TestList_Reset(t *testing.T) { + d1 := dirlist.New() + d1.Reset() + + d2 := dirlist.New() + d2.Load("/opt/local/bin:/usr/local/bin:/sbin:/bin:/var:/bin") + d2.Reset() + require.Equal(t, 0, len(d2.Slice())) + require.Equal(t, "", d2.String()) +} + +func TestList_Contains(t *testing.T) { + d := dirlist.New() + d.Load("/opt/local/bin:/usr/local/bin:/sbin:/bin:/var:/bin") + require.False(t, d.Contains("/ur/local/sbin")) + require.False(t, d.Contains("/ur/local////sbin/")) + require.True(t, d.Contains("/sbin")) + require.True(t, d.Contains("///sbin//")) +} + +func TestList_LoadEnv(t *testing.T) { + tests := []struct { + name string + val string + want string + }{ + {"empty", "", ""}, + } + for i, tt := range tests { + tt2 := tt + t.Run(tt2.name, func(t *testing.T) { + envvar := fmt.Sprintf("%s_%d_VAR", t.Name(), i) + d := dirlist.New() + t.Setenv(envvar, tt.val) + d.LoadEnv(envvar) + require.Equal(t, tt2.want, d.String()) + }) + } +} + +func TestList_Slice(t *testing.T) { + d := dirlist.New() + require.Equal(t, 0, len(d.Slice())) + d.Load("/usr/bin:/bin") + require.Equal(t, []string{"/usr/bin", "/bin"}, d.Slice()) +} + +func TestList_String(t *testing.T) { + d := dirlist.New() + d.Load("/usr/bin:/bin") + require.Equal(t, "/usr/bin:/bin", d.String()) +} diff --git a/go.mod b/go.mod index 43622bb..225a191 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,7 @@ module al.essio.dev/pkg/tools go 1.21 -require github.com/stretchr/testify v1.8.4 +require github.com/stretchr/testify v1.9.0 require ( github.com/davecgh/go-spew v1.1.1 // indirect diff --git a/go.sum b/go.sum index fa4b6e6..60ce688 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,8 @@ 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= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 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= diff --git a/internal/version/revision.txt b/internal/version/revision.txt index e084248..4bfef03 100644 --- a/internal/version/revision.txt +++ b/internal/version/revision.txt @@ -1 +1 @@ -main-304c719 \ No newline at end of file +main-325d644 \ No newline at end of file diff --git a/internal/version/version.txt b/internal/version/version.txt index 9bdb566..87ff52e 100644 --- a/internal/version/version.txt +++ b/internal/version/version.txt @@ -1 +1 @@ -v1.4.1 \ No newline at end of file +v1.4.2 \ No newline at end of file diff --git a/pathlist/list_test.go b/pathlist/list_test.go deleted file mode 100644 index f2c8576..0000000 --- a/pathlist/list_test.go +++ /dev/null @@ -1,77 +0,0 @@ -package pathlist - -import ( - "testing" - - "github.com/stretchr/testify/require" -) - -func Test_newDirList(t *testing.T) { - d := new(dirList) - d.Reset() - require.NotNil(t, d) - require.True(t, d.Nil()) - require.Equal(t, "", d.String()) - - d.Append("/ciao") - require.Equal(t, "/ciao", d.String()) - - d1 := new(dirList) - d1.Reset() - d1 = d.clone(d1) - d.Append("/var") - require.NotEqual(t, &d, &d1) -} - -func Test_DirList_Append(t *testing.T) { - d := New() - require.True(t, d.Nil()) - for _, p := range []string{"/var", "/var", "/bin", "/bin/", "/bin///"} { - d.Append(p) - } - - require.Equal(t, "/var:/bin", d.String()) - d.Prepend("/bin///") - require.Equal(t, "/var:/bin", d.String()) - - // require.Equal(t, 2, d.Append("/var"), ("/usr/local/bin", "/opt/local/bin")) - // require.Equal(t, "/var:/bin:/usr/local/bin:/opt/local/bin", d.String()) -} - -func Test_DirList_Prepend(t *testing.T) { - d := New() - dirs := []string{ - "/var", "/var", "/bin", "/bin/", - } - - for _, dir := range dirs { - d.Prepend(dir) - } - - d.Append("/bin/") - - require.Equal(t, "/bin:/var", d.String()) - d.Prepend("/sbin") - require.Equal(t, d.Slice(), []string{"/sbin", "/bin", "/var"}) - require.Equal(t, d.String(), "/sbin:/bin:/var") - d.Prepend("/var") - d.Prepend("/usr/local/bin") - d.Prepend("/opt/local/bin") - require.Equal(t, d.String(), "/opt/local/bin:/usr/local/bin:/sbin:/bin:/var") -} - -func Test_DirList_Drop(t *testing.T) { - d := New() - d.Load("/opt/local/bin:/usr/local/bin:/sbin:/bin:/var:/bin") - require.Equal(t, d.Slice(), []string{"/opt/local/bin", "/usr/local/bin", "/sbin", "/bin", "/var"}) - d.Drop("/opt/local/bin") - d.Drop("/opt/local/bin") - d.Drop("/opt/local/bin") - d.Drop("/usr/local/bin") - d.Drop("/var") - require.False(t, d.Nil()) - d.Drop("/sbin") - d.Drop("/bin") - require.Equal(t, "", d.String()) - require.True(t, d.Nil()) -}