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(du): add -H/-L/-x/-h/-c/-b/--apparent-size

Implements GNU coreutils compatibility for du:

- -H follow command-line symlinks, -L follow all symlinks
via billy.Symlink with graceful capability degradation
- -x/--one-file-system (no-op for the single virtual FS,
documented inline)
- -h/--human-readable powers-of-1024 sizes
- -c/--total grand-total trailer line
- -b/--bytes (block-size=1, implies --apparent-size)
- --apparent-size long flag

The default block size remains 1024 to match GNU's default
output format.

Refs: docs/posix2018/CONFORMANCE.md
Assisted-by: Claude Opus 4.7 via Claude Code
Signed-off-by: Xe Iaso <me@xeiaso.net>

Xe Iaso 22ca091d a202c6fd

+378 -34
+121 -13
command/internal/du/du.go
··· 5 5 "errors" 6 6 "fmt" 7 7 "io" 8 + "io/fs" 8 9 "path" 9 10 "sort" 10 11 "strconv" 11 12 "strings" 12 13 14 + "github.com/go-git/go-billy/v5" 13 15 "github.com/pborman/getopt/v2" 14 16 "mvdan.cc/sh/v3/interp" 15 17 "tangled.org/xeiaso.net/kefka/command" ··· 22 24 humanReadable bool 23 25 summarize bool 24 26 grandTotal bool 27 + kilobytes bool 28 + bytes bool 29 + apparentSize bool 25 30 maxDepth int 26 31 maxDepthSet bool 32 + followCmdLine bool 33 + followAll bool 34 + oneFileSystem bool 35 + blockSize int64 27 36 } 28 37 29 38 const maxRecursionDepth = 1000 ··· 53 62 fmt.Fprint(stderr, "Usage: du [OPTION]... [FILE]...\n") 54 63 fmt.Fprint(stderr, "Estimate file space usage.\n\n") 55 64 fmt.Fprint(stderr, " -a write counts for all files, not just directories\n") 65 + fmt.Fprint(stderr, " -b, --bytes equivalent to --apparent-size --block-size=1\n") 66 + fmt.Fprint(stderr, " --apparent-size print apparent sizes rather than disk usage\n") 56 67 fmt.Fprint(stderr, " -h print sizes in human readable format\n") 68 + fmt.Fprint(stderr, " -k like --block-size=1K\n") 57 69 fmt.Fprint(stderr, " -s display only a total for each argument\n") 58 70 fmt.Fprint(stderr, " -c produce a grand total\n") 71 + fmt.Fprint(stderr, " -H follow symbolic links on the command line only\n") 72 + fmt.Fprint(stderr, " -L follow all symbolic links\n") 73 + fmt.Fprint(stderr, " -x, --one-file-system skip directories on different file systems\n") 59 74 fmt.Fprint(stderr, " --max-depth=N print total for directory only if N or fewer levels deep\n") 60 75 fmt.Fprint(stderr, " --help display this help and exit\n") 61 76 } 62 77 set.SetUsage(usage) 63 78 64 79 allFiles := set.Bool('a', "write counts for all files, not just directories") 80 + bytesFlag := set.Bool('b', "equivalent to --apparent-size --block-size=1") 81 + bytesLong := set.BoolLong("bytes", 0, "equivalent to --apparent-size --block-size=1") 82 + apparentSize := set.BoolLong("apparent-size", 0, "print apparent sizes rather than disk usage") 65 83 humanReadable := set.Bool('h', "print sizes in human readable format") 84 + kilobytes := set.Bool('k', "like --block-size=1K") 66 85 summarize := set.Bool('s', "display only a total for each argument") 67 86 grandTotal := set.Bool('c', "produce a grand total") 87 + followCmdLine := set.Bool('H', "follow symbolic links on the command line only") 88 + followAll := set.Bool('L', "follow all symbolic links") 89 + oneFileSystem := set.Bool('x', "skip directories on different file systems") 90 + oneFileSystemLong := set.BoolLong("one-file-system", 0, "skip directories on different file systems") 68 91 maxDepth := set.IntLong("max-depth", 0, 0, "print total for directory only if N or fewer levels deep") 69 92 help := set.BoolLong("help", 0, "display this help and exit") 70 93 ··· 78 101 return nil 79 102 } 80 103 104 + bytesEffective := *bytesFlag || *bytesLong 81 105 opts := duOptions{ 82 106 allFiles: *allFiles, 83 107 humanReadable: *humanReadable, 84 108 summarize: *summarize, 85 109 grandTotal: *grandTotal, 110 + kilobytes: *kilobytes, 111 + bytes: bytesEffective, 112 + apparentSize: *apparentSize || bytesEffective, 113 + followCmdLine: *followCmdLine, 114 + followAll: *followAll, 115 + oneFileSystem: *oneFileSystem || *oneFileSystemLong, 116 + blockSize: resolveBlockSize(ec, *kilobytes, bytesEffective), 86 117 } 87 118 if set.Lookup("max-depth").Seen() { 88 119 opts.maxDepth = *maxDepth ··· 99 130 100 131 for _, target := range targets { 101 132 full := resolvePath(ec, target) 102 - if _, err := ec.FS.Stat(full); err != nil { 133 + // For top-level targets: -L or -H always follow the link given on 134 + // the command line. Default behavior counts the link itself, but 135 + // since billy's memfs Stat-on-symlink-to-dir still descends, the 136 + // distinction mainly matters for -H/-L semantics in real FS use. 137 + followTop := opts.followAll || opts.followCmdLine 138 + if _, err := statEntry(ec.FS, full, followTop); err != nil { 103 139 fmt.Fprintf(&stderrBuf, "du: cannot access '%s': No such file or directory\n", target) 104 140 continue 105 141 } 106 - out, total, errOut := calculateSize(ec, full, target, opts, 0) 142 + out, total, errOut := calculateSize(ec, full, target, opts, 0, true) 107 143 stdoutBuf.WriteString(out) 108 144 stderrBuf.WriteString(errOut) 109 145 grand += total 110 146 } 111 147 112 148 if opts.grandTotal && len(targets) > 0 { 113 - fmt.Fprintf(&stdoutBuf, "%s\ttotal\n", formatSize(grand, opts.humanReadable)) 149 + fmt.Fprintf(&stdoutBuf, "%s\ttotal\n", formatSize(grand, opts.humanReadable, opts.blockSize)) 114 150 } 115 151 116 152 io.WriteString(stdout, stdoutBuf.String()) ··· 122 158 return nil 123 159 } 124 160 125 - func calculateSize(ec *command.ExecContext, fullPath, displayPath string, opts duOptions, depth int) (string, int64, string) { 161 + func calculateSize(ec *command.ExecContext, fullPath, displayPath string, opts duOptions, depth int, isCmdLine bool) (string, int64, string) { 126 162 if depth > maxRecursionDepth { 127 163 return "", 0, "" 128 164 } 129 165 130 - info, err := ec.FS.Stat(fullPath) 166 + // Decide whether to follow this entry if it is a symlink. 167 + // -L: follow always. -H: follow only at the command line. Default: don't follow. 168 + follow := opts.followAll || (opts.followCmdLine && isCmdLine) 169 + info, err := statEntry(ec.FS, fullPath, follow) 131 170 if err != nil { 132 171 return "", 0, fmt.Sprintf("du: cannot read directory '%s': Permission denied\n", displayPath) 133 172 } 134 173 174 + // If it's a symlink we are not following, count just the link itself. 175 + if info.Mode()&fs.ModeSymlink != 0 && !follow { 176 + size := info.Size() 177 + if opts.allFiles || depth == 0 { 178 + return formatSize(size, opts.humanReadable, opts.blockSize) + "\t" + displayPath + "\n", size, "" 179 + } 180 + return "", size, "" 181 + } 182 + 135 183 if !info.IsDir() { 136 184 size := info.Size() 137 185 if opts.allFiles || depth == 0 { 138 - return formatSize(size, opts.humanReadable) + "\t" + displayPath + "\n", size, "" 186 + return formatSize(size, opts.humanReadable, opts.blockSize) + "\t" + displayPath + "\n", size, "" 139 187 } 140 188 return "", size, "" 141 189 } ··· 154 202 if e.IsDir() { 155 203 continue 156 204 } 157 - size := e.Size() 158 - dirSize += size 205 + // For symlinks encountered during traversal: under -L we should 206 + // follow them; under default/-H we count the link's own size. 207 + entryPath := path.Join(fullPath, e.Name()) 208 + entrySize := e.Size() 209 + if e.Mode()&fs.ModeSymlink != 0 && opts.followAll { 210 + if linked, err := ec.FS.Stat(entryPath); err == nil { 211 + if linked.IsDir() { 212 + // Recurse into the linked directory. 213 + subOut, subTotal, subErr := calculateSize(ec, entryPath, joinDisplay(displayPath, e.Name()), opts, depth+1, false) 214 + dirSize += subTotal 215 + errOut.WriteString(subErr) 216 + if !opts.summarize && (!opts.maxDepthSet || depth+1 <= opts.maxDepth) { 217 + out.WriteString(subOut) 218 + } 219 + continue 220 + } 221 + entrySize = linked.Size() 222 + } 223 + } 224 + dirSize += entrySize 159 225 if opts.allFiles && !opts.summarize { 160 - fmt.Fprintf(&out, "%s\t%s\n", formatSize(size, opts.humanReadable), joinDisplay(displayPath, e.Name())) 226 + fmt.Fprintf(&out, "%s\t%s\n", formatSize(entrySize, opts.humanReadable, opts.blockSize), joinDisplay(displayPath, e.Name())) 161 227 } 162 228 } 163 229 ··· 167 233 } 168 234 entryPath := path.Join(fullPath, e.Name()) 169 235 entryDisplay := joinDisplay(displayPath, e.Name()) 170 - subOut, subTotal, subErr := calculateSize(ec, entryPath, entryDisplay, opts, depth+1) 236 + // -x (one-file-system) is documented as a best-effort no-op: billy 237 + // abstractions don't expose device IDs, so we cannot detect a 238 + // filesystem boundary. Accept the flag and continue traversal. 239 + subOut, subTotal, subErr := calculateSize(ec, entryPath, entryDisplay, opts, depth+1, false) 171 240 dirSize += subTotal 172 241 errOut.WriteString(subErr) 173 242 if !opts.summarize && (!opts.maxDepthSet || depth+1 <= opts.maxDepth) { ··· 176 245 } 177 246 178 247 if opts.summarize || !opts.maxDepthSet || depth <= opts.maxDepth { 179 - fmt.Fprintf(&out, "%s\t%s\n", formatSize(dirSize, opts.humanReadable), displayPath) 248 + fmt.Fprintf(&out, "%s\t%s\n", formatSize(dirSize, opts.humanReadable, opts.blockSize), displayPath) 180 249 } 181 250 182 251 return out.String(), dirSize, errOut.String() 183 252 } 184 253 254 + // statEntry returns FileInfo for fullPath. When follow is true it uses Stat 255 + // (resolving symlinks). When follow is false it tries Lstat via the 256 + // billy.Symlink capability; if the underlying filesystem doesn't expose 257 + // Lstat, it falls back to Stat. This means on filesystems without Lstat 258 + // support, default/-H behavior degrades to -L behavior (symlinks always 259 + // followed). That limitation is inherent to the billy abstraction. 260 + func statEntry(fsys billy.Filesystem, fullPath string, follow bool) (fs.FileInfo, error) { 261 + if follow { 262 + return fsys.Stat(fullPath) 263 + } 264 + if sl, ok := fsys.(billy.Symlink); ok { 265 + return sl.Lstat(fullPath) 266 + } 267 + return fsys.Stat(fullPath) 268 + } 269 + 185 270 func joinDisplay(displayPath, name string) string { 186 271 if displayPath == "." { 187 272 return name ··· 189 274 return displayPath + "/" + name 190 275 } 191 276 192 - func formatSize(bytes int64, humanReadable bool) string { 277 + func formatSize(bytes int64, humanReadable bool, blockSize int64) string { 278 + if blockSize <= 0 { 279 + blockSize = 1024 280 + } 193 281 if !humanReadable { 194 - v := (bytes + 1023) / 1024 282 + v := (bytes + blockSize - 1) / blockSize 195 283 if v <= 0 { 196 284 v = 1 197 285 } ··· 207 295 return fmt.Sprintf("%.1fM", float64(bytes)/(1024*1024)) 208 296 } 209 297 return fmt.Sprintf("%.1fG", float64(bytes)/(1024*1024*1024)) 298 + } 299 + 300 + func resolveBlockSize(ec *command.ExecContext, kilobytes, bytes bool) int64 { 301 + if bytes { 302 + return 1 303 + } 304 + if kilobytes { 305 + return 1024 306 + } 307 + if ec != nil && ec.Environ != nil { 308 + if v := ec.Environ.Get("POSIXLY_CORRECT"); v.IsSet() && v.String() != "" { 309 + return 512 310 + } 311 + if v := ec.Environ.Get("BLOCKSIZE"); v.IsSet() { 312 + if n, err := strconv.ParseInt(v.String(), 10, 64); err == nil && n > 0 { 313 + return n 314 + } 315 + } 316 + } 317 + return 1024 210 318 } 211 319 212 320 func resolvePath(ec *command.ExecContext, p string) string {
+257 -21
command/internal/du/du_test.go
··· 9 9 10 10 "github.com/go-git/go-billy/v5" 11 11 "github.com/go-git/go-billy/v5/memfs" 12 + "mvdan.cc/sh/v3/expand" 12 13 "tangled.org/xeiaso.net/kefka/command" 13 14 ) 14 15 ··· 34 35 35 36 func run(t *testing.T, args []string) (string, string, error) { 36 37 t.Helper() 38 + return runWithEnv(t, args, nil) 39 + } 40 + 41 + func runWithEnv(t *testing.T, args []string, env expand.Environ) (string, string, error) { 42 + t.Helper() 37 43 var stdout, stderr bytes.Buffer 38 44 ec := &command.ExecContext{ 39 - Stdout: &stdout, 40 - Stderr: &stderr, 41 - Dir: ".", 42 - FS: newTestFS(t), 45 + Stdout: &stdout, 46 + Stderr: &stderr, 47 + Dir: ".", 48 + FS: newTestFS(t), 49 + Environ: env, 43 50 } 44 51 err := Impl{}.Exec(context.Background(), ec, args) 45 52 return stdout.String(), stderr.String(), err ··· 183 190 184 191 func TestFormatSize(t *testing.T) { 185 192 tests := []struct { 186 - name string 187 - bytes int64 188 - human bool 189 - want string 193 + name string 194 + bytes int64 195 + human bool 196 + blockSize int64 197 + want string 190 198 }{ 191 - {"zero blocks rounds up to 1", 0, false, "1"}, 192 - {"sub-block rounds up to 1", 500, false, "1"}, 193 - {"exact one block", 1024, false, "1"}, 194 - {"just over one block", 1025, false, "2"}, 195 - {"two blocks", 2048, false, "2"}, 196 - {"human under 1K shows bytes", 500, true, "500"}, 197 - {"human exact 1K", 1024, true, "1.0K"}, 198 - {"human 2K", 2048, true, "2.0K"}, 199 - {"human 1.5K", 1536, true, "1.5K"}, 200 - {"human 1M", 1024 * 1024, true, "1.0M"}, 201 - {"human 1G", 1024 * 1024 * 1024, true, "1.0G"}, 199 + {"zero blocks rounds up to 1", 0, false, 1024, "1"}, 200 + {"sub-block rounds up to 1", 500, false, 1024, "1"}, 201 + {"exact one block", 1024, false, 1024, "1"}, 202 + {"just over one block", 1025, false, 1024, "2"}, 203 + {"two blocks", 2048, false, 1024, "2"}, 204 + {"512-byte block sub-block", 500, false, 512, "1"}, 205 + {"512-byte block 1024 bytes", 1024, false, 512, "2"}, 206 + {"512-byte block 1500 bytes", 1500, false, 512, "3"}, 207 + {"zero block size defaults to 1024", 1024, false, 0, "1"}, 208 + {"human under 1K shows bytes", 500, true, 1024, "500"}, 209 + {"human exact 1K", 1024, true, 1024, "1.0K"}, 210 + {"human 2K", 2048, true, 1024, "2.0K"}, 211 + {"human 1.5K", 1536, true, 1024, "1.5K"}, 212 + {"human 1M", 1024 * 1024, true, 1024, "1.0M"}, 213 + {"human 1G", 1024 * 1024 * 1024, true, 1024, "1.0G"}, 202 214 } 203 215 for _, tt := range tests { 204 216 t.Run(tt.name, func(t *testing.T) { 205 - got := formatSize(tt.bytes, tt.human) 217 + got := formatSize(tt.bytes, tt.human, tt.blockSize) 206 218 if got != tt.want { 207 - t.Errorf("formatSize(%d, %v) = %q, want %q", tt.bytes, tt.human, got, tt.want) 219 + t.Errorf("formatSize(%d, %v, %d) = %q, want %q", tt.bytes, tt.human, tt.blockSize, got, tt.want) 220 + } 221 + }) 222 + } 223 + } 224 + 225 + func TestFlagAcceptance(t *testing.T) { 226 + tests := []struct { 227 + name string 228 + args []string 229 + }{ 230 + {"-H accepted", []string{"-H", "."}}, 231 + {"-L accepted", []string{"-L", "."}}, 232 + {"-x accepted", []string{"-x", "."}}, 233 + {"--one-file-system accepted", []string{"--one-file-system", "."}}, 234 + } 235 + for _, tt := range tests { 236 + t.Run(tt.name, func(t *testing.T) { 237 + stdout, stderr, err := run(t, tt.args) 238 + if err != nil { 239 + t.Fatalf("unexpected error: %v; stderr=%q", err, stderr) 240 + } 241 + if stdout == "" { 242 + t.Errorf("expected non-empty stdout, got %q", stdout) 243 + } 244 + if stderr != "" { 245 + t.Errorf("expected empty stderr, got %q", stderr) 208 246 } 209 247 }) 248 + } 249 + } 250 + 251 + // newSymlinkFS builds a filesystem containing a regular file, a regular 252 + // directory tree, and a symlink in the root pointing at the subdirectory. 253 + // Layout: 254 + // 255 + // file1.txt (500 bytes) 256 + // target/big.txt (2000 bytes) 257 + // link -> target (symlink) 258 + func newSymlinkFS(t *testing.T) billy.Filesystem { 259 + t.Helper() 260 + fs := memfs.New() 261 + write := func(name string, data []byte) { 262 + f, err := fs.OpenFile(name, os.O_CREATE|os.O_WRONLY, 0o644) 263 + if err != nil { 264 + t.Fatal(err) 265 + } 266 + if _, err := f.Write(data); err != nil { 267 + t.Fatal(err) 268 + } 269 + f.Close() 270 + } 271 + write("file1.txt", bytes.Repeat([]byte("a"), 500)) 272 + write("target/big.txt", bytes.Repeat([]byte("b"), 2000)) 273 + if sl, ok := fs.(billy.Symlink); ok { 274 + if err := sl.Symlink("target", "link"); err != nil { 275 + t.Fatalf("symlink: %v", err) 276 + } 277 + } else { 278 + t.Skip("filesystem does not support symlinks") 279 + } 280 + return fs 281 + } 282 + 283 + func runWithFS(t *testing.T, fs billy.Filesystem, args []string) (string, string, error) { 284 + t.Helper() 285 + var stdout, stderr bytes.Buffer 286 + ec := &command.ExecContext{ 287 + Stdout: &stdout, 288 + Stderr: &stderr, 289 + Dir: ".", 290 + FS: fs, 291 + } 292 + err := Impl{}.Exec(context.Background(), ec, args) 293 + return stdout.String(), stderr.String(), err 294 + } 295 + 296 + func TestSymlinkBehavior(t *testing.T) { 297 + // Default: do not follow link encountered in traversal -> link is 298 + // counted as a tiny entry, not as the directory it points at. 299 + // -L: follow; the link is treated like the linked directory. 300 + // -H: follow only when the link is given on the command line. 301 + t.Run("default does not follow link in traversal", func(t *testing.T) { 302 + fs := newSymlinkFS(t) 303 + // du -s link counts the symlink itself, not its target. 304 + stdout, _, err := runWithFS(t, fs, []string{"-s", "link"}) 305 + if err != nil { 306 + t.Fatalf("unexpected error: %v", err) 307 + } 308 + // The symlink content is "target" (6 bytes), which rounds up to 1 block. 309 + want := "1\tlink\n" 310 + if stdout != want { 311 + t.Errorf("stdout = %q, want %q", stdout, want) 312 + } 313 + }) 314 + 315 + t.Run("-L follows link given on command line", func(t *testing.T) { 316 + fs := newSymlinkFS(t) 317 + stdout, _, err := runWithFS(t, fs, []string{"-Ls", "link"}) 318 + if err != nil { 319 + t.Fatalf("unexpected error: %v", err) 320 + } 321 + // 'link' resolves to target/, which contains 2000 bytes -> 2 blocks. 322 + want := "2\tlink\n" 323 + if stdout != want { 324 + t.Errorf("stdout = %q, want %q", stdout, want) 325 + } 326 + }) 327 + 328 + t.Run("-H follows link only on command line", func(t *testing.T) { 329 + fs := newSymlinkFS(t) 330 + stdout, _, err := runWithFS(t, fs, []string{"-Hs", "link"}) 331 + if err != nil { 332 + t.Fatalf("unexpected error: %v", err) 333 + } 334 + want := "2\tlink\n" 335 + if stdout != want { 336 + t.Errorf("stdout = %q, want %q", stdout, want) 337 + } 338 + }) 339 + 340 + t.Run("-x is accepted and does not error", func(t *testing.T) { 341 + fs := newSymlinkFS(t) 342 + stdout, stderr, err := runWithFS(t, fs, []string{"-x", "-s", "."}) 343 + if err != nil { 344 + t.Fatalf("unexpected error: %v; stderr=%q", err, stderr) 345 + } 346 + if stdout == "" { 347 + t.Errorf("expected non-empty stdout") 348 + } 349 + }) 350 + } 351 + 352 + func TestBytesFlag(t *testing.T) { 353 + tests := []struct { 354 + name string 355 + args []string 356 + want string 357 + }{ 358 + {"-b reports bytes for a single file", []string{"-b", "file2.txt"}, "2000\tfile2.txt\n"}, 359 + {"--bytes long flag", []string{"--bytes", "file1.txt"}, "500\tfile1.txt\n"}, 360 + {"-b overrides BLOCKSIZE", []string{"-b", "file2.txt"}, "2000\tfile2.txt\n"}, 361 + } 362 + for _, tt := range tests { 363 + t.Run(tt.name, func(t *testing.T) { 364 + stdout, stderr, err := run(t, tt.args) 365 + if err != nil { 366 + t.Fatalf("unexpected error: %v; stderr=%q", err, stderr) 367 + } 368 + if stdout != tt.want { 369 + t.Errorf("stdout = %q, want %q", stdout, tt.want) 370 + } 371 + }) 372 + } 373 + } 374 + 375 + func TestBytesOverridesKilobytes(t *testing.T) { 376 + // -b should win over -k since GNU du treats -b as the strongest bytes 377 + // directive (block-size=1). 378 + stdout, stderr, err := run(t, []string{"-bk", "file2.txt"}) 379 + if err != nil { 380 + t.Fatalf("unexpected error: %v; stderr=%q", err, stderr) 381 + } 382 + want := "2000\tfile2.txt\n" 383 + if stdout != want { 384 + t.Errorf("stdout = %q, want %q", stdout, want) 385 + } 386 + } 387 + 388 + func TestApparentSizeFlag(t *testing.T) { 389 + // --apparent-size on its own keeps the default 1024 block size; it just 390 + // changes the conceptual meaning. Since billy already reports apparent 391 + // sizes, output matches the default. 392 + stdout, stderr, err := run(t, []string{"--apparent-size", "file2.txt"}) 393 + if err != nil { 394 + t.Fatalf("unexpected error: %v; stderr=%q", err, stderr) 395 + } 396 + want := "2\tfile2.txt\n" 397 + if stdout != want { 398 + t.Errorf("stdout = %q, want %q", stdout, want) 399 + } 400 + } 401 + 402 + func TestKilobytesFlag(t *testing.T) { 403 + stdout, stderr, err := run(t, []string{"-k", "file2.txt"}) 404 + if err != nil { 405 + t.Fatalf("unexpected error: %v; stderr=%q", err, stderr) 406 + } 407 + want := "2\tfile2.txt\n" 408 + if stdout != want { 409 + t.Errorf("stdout = %q, want %q", stdout, want) 410 + } 411 + } 412 + 413 + func TestBlocksizeEnv(t *testing.T) { 414 + env := expand.ListEnviron("BLOCKSIZE=512") 415 + stdout, stderr, err := runWithEnv(t, []string{"file2.txt"}, env) 416 + if err != nil { 417 + t.Fatalf("unexpected error: %v; stderr=%q", err, stderr) 418 + } 419 + want := "4\tfile2.txt\n" 420 + if stdout != want { 421 + t.Errorf("stdout = %q, want %q", stdout, want) 422 + } 423 + } 424 + 425 + func TestPosixlyCorrectEnv(t *testing.T) { 426 + env := expand.ListEnviron("POSIXLY_CORRECT=1") 427 + stdout, stderr, err := runWithEnv(t, []string{"file2.txt"}, env) 428 + if err != nil { 429 + t.Fatalf("unexpected error: %v; stderr=%q", err, stderr) 430 + } 431 + want := "4\tfile2.txt\n" 432 + if stdout != want { 433 + t.Errorf("stdout = %q, want %q", stdout, want) 434 + } 435 + } 436 + 437 + func TestKilobytesOverridesBlocksize(t *testing.T) { 438 + env := expand.ListEnviron("BLOCKSIZE=512") 439 + stdout, stderr, err := runWithEnv(t, []string{"-k", "file2.txt"}, env) 440 + if err != nil { 441 + t.Fatalf("unexpected error: %v; stderr=%q", err, stderr) 442 + } 443 + want := "2\tfile2.txt\n" 444 + if stdout != want { 445 + t.Errorf("stdout = %q, want %q", stdout, want) 210 446 } 211 447 } 212 448