A virtual jailed shell environment for Go apps backed by an io/fs#FS.
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

refactor(command): port ls to getopt/v2 and match upstream help

Re-port ls flag parsing from spf13/pflag onto pborman/getopt/v2 to
match the convention established by the newer just-bash ports
(base64, cat, basename, etc.). Output of --help now mirrors the
lsHelp block from vercel-labs/just-bash verbatim, including the
short-only -l/-S/-t/-1 flags and the trailing --help line. Parse
errors print "ls: <err>" plus the usage block to stderr and exit 2,
matching coreutils usage-error semantics.

Assisted-by: Claude Opus 4.7 via Claude Code
Signed-off-by: Xe Iaso <me@xeiaso.net>

Xe Iaso 66c3a408 2ab1dfab

+61 -37
+52 -34
command/internal/ls/ls.go
··· 16 16 17 17 "github.com/go-git/go-billy/v5" 18 18 "github.com/go-git/go-billy/v5/util" 19 - "github.com/spf13/pflag" 19 + "github.com/pborman/getopt/v2" 20 20 "mvdan.cc/sh/v3/interp" 21 21 "tangled.org/xeiaso.net/kefka/command" 22 22 ) ··· 31 31 return errors.New("ls: ExecContext has no filesystem") 32 32 } 33 33 34 - var ( 35 - showAll bool 36 - showAlmostAll bool 37 - longFormat bool 38 - humanReadable bool 39 - recursive bool 40 - reverse bool 41 - sortBySize bool 42 - classifyFiles bool 43 - directoryOnly bool 44 - sortByTime bool 45 - onePerLine bool 46 - ) 34 + stderr := ec.Stderr 35 + if stderr == nil { 36 + stderr = io.Discard 37 + } 47 38 48 - flagSet := pflag.NewFlagSet("ls", pflag.ContinueOnError) 49 - flagSet.SetOutput(ec.Stderr) 50 - flagSet.BoolVarP(&showAll, "all", "a", false, "do not ignore entries starting with .") 51 - flagSet.BoolVarP(&showAlmostAll, "almost-all", "A", false, "do not list . and ..") 52 - flagSet.BoolVarP(&longFormat, "long", "l", false, "use a long listing format") 53 - flagSet.BoolVarP(&humanReadable, "human-readable", "h", false, "with -l, print sizes like 1K 234M 2G etc.") 54 - flagSet.BoolVarP(&recursive, "recursive", "R", false, "list subdirectories recursively") 55 - flagSet.BoolVarP(&reverse, "reverse", "r", false, "reverse order while sorting") 56 - flagSet.BoolVarP(&sortBySize, "sort-size", "S", false, "sort by file size, largest first") 57 - flagSet.BoolVarP(&classifyFiles, "classify", "F", false, "append indicator (one of */=>@) to entries") 58 - flagSet.BoolVarP(&directoryOnly, "directory", "d", false, "list directories themselves, not their contents") 59 - flagSet.BoolVarP(&sortByTime, "sort-time", "t", false, "sort by time, newest first") 60 - flagSet.BoolVarP(&onePerLine, "one-per-line", "1", false, "list one file per line") 39 + set := getopt.New() 40 + set.SetProgram("ls") 41 + set.SetParameters("[FILE]...") 61 42 62 - if err := flagSet.Parse(args); err != nil { 63 - return errors.Join(err, interp.ExitStatus(2)) 43 + usage := func() { 44 + fmt.Fprint(stderr, "Usage: ls [OPTION]... [FILE]...\n") 45 + fmt.Fprint(stderr, "list directory contents\n\n") 46 + fmt.Fprint(stderr, " -a, --all do not ignore entries starting with .\n") 47 + fmt.Fprint(stderr, " -A, --almost-all do not list . and ..\n") 48 + fmt.Fprint(stderr, " -d, --directory list directories themselves, not their contents\n") 49 + fmt.Fprint(stderr, " -F, --classify append indicator (one of */=>@) to entries\n") 50 + fmt.Fprint(stderr, " -h, --human-readable with -l, print sizes like 1K 234M 2G etc.\n") 51 + fmt.Fprint(stderr, " -l use a long listing format\n") 52 + fmt.Fprint(stderr, " -r, --reverse reverse order while sorting\n") 53 + fmt.Fprint(stderr, " -R, --recursive list subdirectories recursively\n") 54 + fmt.Fprint(stderr, " -S sort by file size, largest first\n") 55 + fmt.Fprint(stderr, " -t sort by time, newest first\n") 56 + fmt.Fprint(stderr, " -1 list one file per line\n") 57 + fmt.Fprint(stderr, " --help display this help and exit\n") 58 + } 59 + set.SetUsage(usage) 60 + 61 + showAll := set.BoolLong("all", 'a', "do not ignore entries starting with .") 62 + showAlmostAll := set.BoolLong("almost-all", 'A', "do not list . and ..") 63 + directoryOnly := set.BoolLong("directory", 'd', "list directories themselves, not their contents") 64 + classifyFiles := set.BoolLong("classify", 'F', "append indicator (one of */=>@) to entries") 65 + humanReadable := set.BoolLong("human-readable", 'h', "with -l, print sizes like 1K 234M 2G etc.") 66 + longFormat := set.Bool('l', "use a long listing format") 67 + reverse := set.BoolLong("reverse", 'r', "reverse order while sorting") 68 + recursive := set.BoolLong("recursive", 'R', "list subdirectories recursively") 69 + sortBySize := set.Bool('S', "sort by file size, largest first") 70 + sortByTime := set.Bool('t', "sort by time, newest first") 71 + onePerLine := set.Bool('1', "list one file per line") 72 + help := set.BoolLong("help", 0, "display this help and exit") 73 + 74 + if err := set.Getopt(append([]string{"ls"}, args...), nil); err != nil { 75 + fmt.Fprintf(stderr, "ls: %s\n", err) 76 + usage() 77 + return interp.ExitStatus(2) 78 + } 79 + if *help { 80 + usage() 81 + return nil 64 82 } 65 83 _ = sortByTime 66 84 _ = onePerLine 67 85 68 - paths := flagSet.Args() 86 + paths := set.Args() 69 87 if len(paths) == 0 { 70 88 paths = []string{"."} 71 89 } ··· 86 104 ) 87 105 88 106 switch { 89 - case directoryOnly: 90 - out, errS, code = listDirectoryEntry(ec, p, longFormat, humanReadable, classifyFiles) 107 + case *directoryOnly: 108 + out, errS, code = listDirectoryEntry(ec, p, *longFormat, *humanReadable, *classifyFiles) 91 109 case strings.ContainsAny(p, "*?["): 92 - out, errS, code = listGlob(ec, p, showAll, showAlmostAll, longFormat, reverse, humanReadable, sortBySize, classifyFiles) 110 + out, errS, code = listGlob(ec, p, *showAll, *showAlmostAll, *longFormat, *reverse, *humanReadable, *sortBySize, *classifyFiles) 93 111 default: 94 - out, errS, code = listPath(ctx, ec, p, showAll, showAlmostAll, longFormat, recursive, showHeader, reverse, humanReadable, sortBySize, classifyFiles) 112 + out, errS, code = listPath(ctx, ec, p, *showAll, *showAlmostAll, *longFormat, *recursive, showHeader, *reverse, *humanReadable, *sortBySize, *classifyFiles) 95 113 } 96 114 97 115 stdoutBuf.WriteString(out)
+9 -3
command/internal/ls/ls_test.go
··· 184 184 wantStdout: "deeper\ninner.txt\n", 185 185 }, 186 186 { 187 - name: "unknown flag returns error", 188 - args: []string{"--no-such-flag"}, 189 - wantErr: true, 187 + name: "unknown flag returns error", 188 + args: []string{"--no-such-flag"}, 189 + wantStderr: "ls: unknown option: --no-such-flag\nUsage: ls [OPTION]... [FILE]...\nlist directory contents\n\n -a, --all do not ignore entries starting with .\n -A, --almost-all do not list . and ..\n -d, --directory list directories themselves, not their contents\n -F, --classify append indicator (one of */=>@) to entries\n -h, --human-readable with -l, print sizes like 1K 234M 2G etc.\n -l use a long listing format\n -r, --reverse reverse order while sorting\n -R, --recursive list subdirectories recursively\n -S sort by file size, largest first\n -t sort by time, newest first\n -1 list one file per line\n --help display this help and exit\n", 190 + wantErr: true, 191 + }, 192 + { 193 + name: "help prints usage to stderr", 194 + args: []string{"--help"}, 195 + wantStderr: "Usage: ls [OPTION]... [FILE]...\nlist directory contents\n\n -a, --all do not ignore entries starting with .\n -A, --almost-all do not list . and ..\n -d, --directory list directories themselves, not their contents\n -F, --classify append indicator (one of */=>@) to entries\n -h, --human-readable with -l, print sizes like 1K 234M 2G etc.\n -l use a long listing format\n -r, --reverse reverse order while sorting\n -R, --recursive list subdirectories recursively\n -S sort by file size, largest first\n -t sort by time, newest first\n -1 list one file per line\n --help display this help and exit\n", 190 196 }, 191 197 } 192 198