Skip to content

Commit

Permalink
new pathctl command (#18)
Browse files Browse the repository at this point in the history
Co-authored-by: Alessio Treglia <alessio@jur.io>
  • Loading branch information
alessio and Alessio Treglia authored Dec 12, 2023
1 parent 58b5732 commit 5cceb45
Show file tree
Hide file tree
Showing 8 changed files with 329 additions and 13 deletions.
28 changes: 21 additions & 7 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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)/:
Expand Down Expand Up @@ -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
146 changes: 146 additions & 0 deletions cmd/pathctl/main.go
Original file line number Diff line number Diff line change
@@ -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)
}
10 changes: 9 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -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
)
10 changes: 10 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -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=
10 changes: 10 additions & 0 deletions internal/path/dirlist.go
Original file line number Diff line number Diff line change
@@ -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
}
79 changes: 79 additions & 0 deletions internal/path/list.go
Original file line number Diff line number Diff line change
@@ -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)
}
54 changes: 54 additions & 0 deletions internal/path/list_test.go
Original file line number Diff line number Diff line change
@@ -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())
}
5 changes: 0 additions & 5 deletions internal/path/path.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package path

import (
"path"
"strings"
)

Expand Down Expand Up @@ -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, "/"))
}

0 comments on commit 5cceb45

Please sign in to comment.