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.

feat(command): port mkdir from just-bash

Create directories with optional parent creation and verbose
output. Mirrors GNU mkdir semantics as implemented by just-bash:
the -p flag suppresses errors for existing targets and creates
intermediate directories, while plain mkdir errors with "File
exists" or "No such file or directory" to match coreutils
diagnostics. Verbose output is written to stdout, errors to
stderr, and a partial-failure batch returns exit 1 after
processing the remaining operands.

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

Xe Iaso d7824549 bc85b066

+374
+122
command/internal/mkdir/mkdir.go
··· 1 + package mkdir 2 + 3 + import ( 4 + "context" 5 + "errors" 6 + "fmt" 7 + "io" 8 + "path" 9 + "strings" 10 + 11 + "github.com/pborman/getopt/v2" 12 + "mvdan.cc/sh/v3/interp" 13 + "tangled.org/xeiaso.net/kefka/command" 14 + ) 15 + 16 + type Impl struct{} 17 + 18 + func (Impl) Exec(ctx context.Context, ec *command.ExecContext, args []string) error { 19 + if ec == nil { 20 + return errors.New("mkdir: nil ExecContext") 21 + } 22 + if ec.FS == nil { 23 + return errors.New("mkdir: ExecContext has no filesystem") 24 + } 25 + 26 + stdout := ec.Stdout 27 + if stdout == nil { 28 + stdout = io.Discard 29 + } 30 + stderr := ec.Stderr 31 + if stderr == nil { 32 + stderr = io.Discard 33 + } 34 + 35 + set := getopt.New() 36 + set.SetProgram("mkdir") 37 + set.SetParameters("DIRECTORY...") 38 + 39 + usage := func() { 40 + fmt.Fprint(stderr, "Usage: mkdir [OPTION]... DIRECTORY...\n") 41 + fmt.Fprint(stderr, "Create the DIRECTORY(ies), if they do not already exist.\n\n") 42 + fmt.Fprint(stderr, " -p, --parents no error if existing, make parent directories as needed\n") 43 + fmt.Fprint(stderr, " -v, --verbose print a message for each created directory\n") 44 + fmt.Fprint(stderr, " --help display this help and exit\n") 45 + } 46 + set.SetUsage(usage) 47 + 48 + parents := set.BoolLong("parents", 'p', "no error if existing, make parent directories as needed") 49 + verbose := set.BoolLong("verbose", 'v', "print a message for each created directory") 50 + help := set.BoolLong("help", 0, "display this help and exit") 51 + 52 + if err := set.Getopt(append([]string{"mkdir"}, args...), nil); err != nil { 53 + fmt.Fprintf(stderr, "mkdir: %s\n", err) 54 + usage() 55 + return interp.ExitStatus(1) 56 + } 57 + if *help { 58 + usage() 59 + return nil 60 + } 61 + 62 + dirs := set.Args() 63 + if len(dirs) == 0 { 64 + fmt.Fprint(stderr, "mkdir: missing operand\n") 65 + return interp.ExitStatus(1) 66 + } 67 + 68 + exitCode := 0 69 + for _, dir := range dirs { 70 + full := resolvePath(ec, dir) 71 + 72 + if !*parents { 73 + if _, err := ec.FS.Stat(full); err == nil { 74 + fmt.Fprintf(stderr, "mkdir: cannot create directory '%s': File exists\n", dir) 75 + exitCode = 1 76 + continue 77 + } 78 + parent := path.Dir(full) 79 + if parent != "." && parent != "/" && parent != "" { 80 + if _, err := ec.FS.Stat(parent); err != nil { 81 + fmt.Fprintf(stderr, "mkdir: cannot create directory '%s': No such file or directory\n", dir) 82 + exitCode = 1 83 + continue 84 + } 85 + } 86 + } 87 + 88 + if err := ec.FS.MkdirAll(full, 0o755); err != nil { 89 + fmt.Fprintf(stderr, "mkdir: cannot create directory '%s': %s\n", dir, err) 90 + exitCode = 1 91 + continue 92 + } 93 + 94 + if *verbose { 95 + fmt.Fprintf(stdout, "mkdir: created directory '%s'\n", dir) 96 + } 97 + } 98 + 99 + if exitCode != 0 { 100 + return interp.ExitStatus(uint8(exitCode)) 101 + } 102 + return nil 103 + } 104 + 105 + func resolvePath(ec *command.ExecContext, p string) string { 106 + dir := ec.Dir 107 + if dir == "" { 108 + dir = "." 109 + } 110 + if path.IsAbs(p) { 111 + p = strings.TrimPrefix(p, "/") 112 + if p == "" { 113 + return "." 114 + } 115 + return path.Clean(p) 116 + } 117 + joined := path.Join(dir, p) 118 + if joined == "" { 119 + return "." 120 + } 121 + return joined 122 + }
+250
command/internal/mkdir/mkdir_test.go
··· 1 + package mkdir 2 + 3 + import ( 4 + "bytes" 5 + "context" 6 + "os" 7 + "strings" 8 + "testing" 9 + 10 + "github.com/go-git/go-billy/v5" 11 + "github.com/go-git/go-billy/v5/memfs" 12 + "tangled.org/xeiaso.net/kefka/command" 13 + ) 14 + 15 + func newFS(t *testing.T) billy.Filesystem { 16 + t.Helper() 17 + fs := memfs.New() 18 + if err := fs.MkdirAll("existing", 0o755); err != nil { 19 + t.Fatal(err) 20 + } 21 + if err := fs.MkdirAll("parent", 0o755); err != nil { 22 + t.Fatal(err) 23 + } 24 + f, err := fs.OpenFile("file.txt", os.O_CREATE|os.O_WRONLY, 0o644) 25 + if err != nil { 26 + t.Fatal(err) 27 + } 28 + f.Write([]byte("hello\n")) 29 + f.Close() 30 + return fs 31 + } 32 + 33 + func run(t *testing.T, args []string, fs billy.Filesystem) (string, string, error) { 34 + t.Helper() 35 + var stdout, stderr bytes.Buffer 36 + ec := &command.ExecContext{ 37 + Stdout: &stdout, 38 + Stderr: &stderr, 39 + Dir: ".", 40 + FS: fs, 41 + } 42 + err := Impl{}.Exec(context.Background(), ec, args) 43 + return stdout.String(), stderr.String(), err 44 + } 45 + 46 + func isDir(t *testing.T, fs billy.Filesystem, name string) bool { 47 + t.Helper() 48 + info, err := fs.Stat(name) 49 + if err != nil { 50 + return false 51 + } 52 + return info.IsDir() 53 + } 54 + 55 + func TestMkdir(t *testing.T) { 56 + tests := []struct { 57 + name string 58 + args []string 59 + wantStdout string 60 + wantErrSub string 61 + wantErr bool 62 + check func(t *testing.T, fs billy.Filesystem) 63 + }{ 64 + { 65 + name: "create single directory", 66 + args: []string{"newdir"}, 67 + check: func(t *testing.T, fs billy.Filesystem) { 68 + if !isDir(t, fs, "newdir") { 69 + t.Errorf("newdir was not created") 70 + } 71 + }, 72 + }, 73 + { 74 + name: "create multiple directories", 75 + args: []string{"a", "b", "c"}, 76 + check: func(t *testing.T, fs billy.Filesystem) { 77 + for _, d := range []string{"a", "b", "c"} { 78 + if !isDir(t, fs, d) { 79 + t.Errorf("%s was not created", d) 80 + } 81 + } 82 + }, 83 + }, 84 + { 85 + name: "create inside existing parent", 86 + args: []string{"parent/child"}, 87 + check: func(t *testing.T, fs billy.Filesystem) { 88 + if !isDir(t, fs, "parent/child") { 89 + t.Errorf("parent/child was not created") 90 + } 91 + }, 92 + }, 93 + { 94 + name: "missing operand", 95 + args: []string{}, 96 + wantErrSub: "missing operand", 97 + wantErr: true, 98 + }, 99 + { 100 + name: "fails when directory exists", 101 + args: []string{"existing"}, 102 + wantErrSub: "File exists", 103 + wantErr: true, 104 + }, 105 + { 106 + name: "fails when parent does not exist", 107 + args: []string{"nope/child"}, 108 + wantErrSub: "No such file or directory", 109 + wantErr: true, 110 + }, 111 + { 112 + name: "parents flag silent on existing", 113 + args: []string{"-p", "existing"}, 114 + check: func(t *testing.T, fs billy.Filesystem) { 115 + if !isDir(t, fs, "existing") { 116 + t.Errorf("existing was removed") 117 + } 118 + }, 119 + }, 120 + { 121 + name: "parents flag creates intermediate directories", 122 + args: []string{"-p", "a/b/c"}, 123 + check: func(t *testing.T, fs billy.Filesystem) { 124 + for _, d := range []string{"a", "a/b", "a/b/c"} { 125 + if !isDir(t, fs, d) { 126 + t.Errorf("%s was not created", d) 127 + } 128 + } 129 + }, 130 + }, 131 + { 132 + name: "long parents flag", 133 + args: []string{"--parents", "deep/path/here"}, 134 + check: func(t *testing.T, fs billy.Filesystem) { 135 + if !isDir(t, fs, "deep/path/here") { 136 + t.Errorf("deep/path/here was not created") 137 + } 138 + }, 139 + }, 140 + { 141 + name: "verbose prints to stdout", 142 + args: []string{"-v", "newdir"}, 143 + wantStdout: "mkdir: created directory 'newdir'\n", 144 + check: func(t *testing.T, fs billy.Filesystem) { 145 + if !isDir(t, fs, "newdir") { 146 + t.Errorf("newdir was not created") 147 + } 148 + }, 149 + }, 150 + { 151 + name: "long verbose flag", 152 + args: []string{"--verbose", "newdir"}, 153 + wantStdout: "mkdir: created directory 'newdir'\n", 154 + }, 155 + { 156 + name: "verbose with multiple", 157 + args: []string{"-pv", "a/b", "c"}, 158 + wantStdout: "mkdir: created directory 'a/b'\nmkdir: created directory 'c'\n", 159 + check: func(t *testing.T, fs billy.Filesystem) { 160 + if !isDir(t, fs, "a/b") { 161 + t.Errorf("a/b was not created") 162 + } 163 + if !isDir(t, fs, "c") { 164 + t.Errorf("c was not created") 165 + } 166 + }, 167 + }, 168 + { 169 + name: "partial success returns error and continues", 170 + args: []string{"existing", "newdir"}, 171 + wantErrSub: "File exists", 172 + wantErr: true, 173 + check: func(t *testing.T, fs billy.Filesystem) { 174 + if !isDir(t, fs, "newdir") { 175 + t.Errorf("newdir was not created after earlier failure") 176 + } 177 + }, 178 + }, 179 + { 180 + name: "unknown flag", 181 + args: []string{"--no-such-flag", "foo"}, 182 + wantErr: true, 183 + }, 184 + { 185 + name: "absolute path resolves under fs root", 186 + args: []string{"/abs"}, 187 + check: func(t *testing.T, fs billy.Filesystem) { 188 + if !isDir(t, fs, "abs") { 189 + t.Errorf("abs was not created") 190 + } 191 + }, 192 + }, 193 + } 194 + 195 + for _, tt := range tests { 196 + t.Run(tt.name, func(t *testing.T) { 197 + fs := newFS(t) 198 + stdout, stderr, err := run(t, tt.args, fs) 199 + if tt.wantErr { 200 + if err == nil { 201 + t.Fatalf("expected error, got nil; stdout=%q stderr=%q", stdout, stderr) 202 + } 203 + } else if err != nil { 204 + t.Fatalf("unexpected error: %v; stderr=%q", err, stderr) 205 + } 206 + if tt.wantStdout != "" && stdout != tt.wantStdout { 207 + t.Errorf("stdout = %q, want %q", stdout, tt.wantStdout) 208 + } 209 + if tt.wantErrSub != "" && !strings.Contains(stderr, tt.wantErrSub) { 210 + t.Errorf("stderr = %q, want substring %q", stderr, tt.wantErrSub) 211 + } 212 + if tt.check != nil { 213 + tt.check(t, fs) 214 + } 215 + }) 216 + } 217 + } 218 + 219 + func TestHelp(t *testing.T) { 220 + fs := newFS(t) 221 + stdout, stderr, err := run(t, []string{"--help"}, fs) 222 + if err != nil { 223 + t.Fatalf("unexpected error: %v", err) 224 + } 225 + if stdout != "" { 226 + t.Errorf("expected empty stdout, got %q", stdout) 227 + } 228 + if !strings.Contains(stderr, "Usage: mkdir [OPTION]... DIRECTORY...") { 229 + t.Errorf("usage line missing from stderr: %q", stderr) 230 + } 231 + if !strings.Contains(stderr, "--parents") { 232 + t.Errorf("--parents flag missing from help output: %q", stderr) 233 + } 234 + if !strings.Contains(stderr, "--verbose") { 235 + t.Errorf("--verbose flag missing from help output: %q", stderr) 236 + } 237 + } 238 + 239 + func TestNoFilesystem(t *testing.T) { 240 + var stdout, stderr bytes.Buffer 241 + ec := &command.ExecContext{ 242 + Stdout: &stdout, 243 + Stderr: &stderr, 244 + Dir: ".", 245 + } 246 + err := Impl{}.Exec(context.Background(), ec, []string{"foo"}) 247 + if err == nil { 248 + t.Fatal("expected error when ExecContext.FS is nil") 249 + } 250 + }
+2
command/registry/coreutils/coreutils.go
··· 22 22 "tangled.org/xeiaso.net/kefka/command/internal/hostname" 23 23 "tangled.org/xeiaso.net/kefka/command/internal/join" 24 24 "tangled.org/xeiaso.net/kefka/command/internal/ls" 25 + "tangled.org/xeiaso.net/kefka/command/internal/mkdir" 25 26 "tangled.org/xeiaso.net/kefka/command/internal/truecmd" 26 27 "tangled.org/xeiaso.net/kefka/command/internal/unexpand" 27 28 "tangled.org/xeiaso.net/kefka/command/internal/zcat" ··· 50 51 reg.Register("hostname", hostname.Impl{}) 51 52 reg.Register("join", join.Impl{}) 52 53 reg.Register("ls", ls.Impl{}) 54 + reg.Register("mkdir", mkdir.Impl{}) 53 55 reg.Register("true", truecmd.Impl{}) 54 56 reg.Register("unexpand", unexpand.Impl{}) 55 57 reg.Register("zcat", zcat.Impl{})