Skip to content

Commit 3c72306

Browse files
yangshunclaude
andcommitted
Match Linux tree file size options: -s for raw bytes, -h for human-readable (1024), --si for SI (1000)
- Change -s to display raw bytes in bracketed format like Linux tree - Add -h/--human-readable for binary human-readable sizes (KiB, MiB) - Add --si for SI human-readable sizes (kB, MB) - Remove -h as help shortcut (--help only) to free it for sizes - Both -h and --si imply -s Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 0640d63 commit 3c72306

File tree

9 files changed

+152
-95
lines changed

9 files changed

+152
-95
lines changed

README.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ tree [options] [path/to/dir]
5353
The following options are available:
5454

5555
```txt
56-
$ tree -h
56+
$ tree --help
5757
5858
Usage:
5959
$ tree <command> [options]
@@ -75,6 +75,10 @@ File options:
7575
-Q, --quote Quote filenames in double quotes.
7676
-p, --permissions Show file type and permissions.
7777
-s, --sizes Print the size of each file in bytes along with the name.
78+
-h, --human-readable Print file sizes in human-readable format (powers
79+
of 1024). Implies -s.
80+
--si Print file sizes using SI units (powers of 1000).
81+
Implies -s.
7882
--du For each directory report its size as the accumulation
7983
of sizes of all its files and sub-directories. Implies -s.
8084
-D, --date Show last modification time for each entry.
@@ -100,7 +104,7 @@ Output options:
100104
-J, --json Output the tree as a JSON structure.
101105
102106
Misc options:
103-
-h, --help Display this message
107+
--help Display this message
104108
--version Display version number
105109
```
106110

@@ -170,6 +174,8 @@ console.log(result);
170174
| `quote` | `false` | `boolean` | Quote filenames in double quotes. |
171175
| `permissions` | `false` | `boolean` | Show file type and permissions (e.g. `[drwxr-xr-x]`). |
172176
| `sizes` | `false` | `boolean` | Print the size of each file in bytes along with the name. |
177+
| `humanReadable` | `false` | `boolean` | Print file sizes in human-readable format (powers of 1024). Implies `sizes`. |
178+
| `si` | `false` | `boolean` | Print file sizes using SI units (powers of 1000). Implies `sizes`. |
173179
| `du` | `false` | `boolean` | For each directory, report its size as the accumulation of sizes of all its files and sub-directories. Implies `sizes`. |
174180
| `date` | `false` | `boolean` | Show last modification time for each entry. |
175181
| `trailingSlash` | `false` | `boolean` | Append a `/` for directories. |

src/__snapshots__/index.test.ts.snap

Lines changed: 68 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -416,105 +416,105 @@ exports[`tree > dirsOnly 11`] = `
416416
exports[`tree > dirsOnly 12`] = `"varied-sizes"`;
417417

418418
exports[`tree > du 1`] = `
419-
"24B chain
420-
└── 24B alpha
421-
└── 24B alpha
422-
└── 24B alpha
423-
└── 24B alpha
424-
└── 24B alpha
425-
└── 24B alpha
426-
└── 24B alpha
427-
└── 24B alpha
428-
└── 24B alpha.txt"
419+
"[ 24] chain
420+
└── [ 24] alpha
421+
└── [ 24] alpha
422+
└── [ 24] alpha
423+
└── [ 24] alpha
424+
└── [ 24] alpha
425+
└── [ 24] alpha
426+
└── [ 24] alpha
427+
└── [ 24] alpha
428+
└── [ 24] alpha.txt"
429429
`;
430430

431431
exports[`tree > du 2`] = `
432-
"29B double-level
433-
├── 9B alpha.txt
434-
└── 20B beta
435-
└── 20B alpha.txt"
432+
"[ 29] double-level
433+
├── [ 9] alpha.txt
434+
└── [ 20] beta
435+
└── [ 20] alpha.txt"
436436
`;
437437

438-
exports[`tree > du 3`] = `"0B empty-dir"`;
438+
exports[`tree > du 3`] = `"[ 0] empty-dir"`;
439439

440440
exports[`tree > du 4`] = `
441-
"30B hidden-files
442-
├── 8B another.txt
443-
└── 8B visible.txt"
441+
"[ 30] hidden-files
442+
├── [ 8] another.txt
443+
└── [ 8] visible.txt"
444444
`;
445445

446446
exports[`tree > du 5`] = `
447-
"5B mixed-content
448-
├── 1B alpha.txt
449-
├── 1B bravo-dir
450-
│ └── 1B inner.txt
451-
├── 1B charlie.txt
452-
├── 1B delta-dir
453-
│ └── 1B inner.txt
454-
└── 1B echo.txt"
447+
"[ 5] mixed-content
448+
├── [ 1] alpha.txt
449+
├── [ 1] bravo-dir
450+
│ └── [ 1] inner.txt
451+
├── [ 1] charlie.txt
452+
├── [ 1] delta-dir
453+
│ └── [ 1] inner.txt
454+
└── [ 1] echo.txt"
455455
`;
456456

457457
exports[`tree > du 6`] = `
458-
"68B multi-files
459-
├── 5B alpha.txt
460-
├── 17B beta.txt
461-
└── 46B charlie.txt"
458+
"[ 68] multi-files
459+
├── [ 5] alpha.txt
460+
├── [ 17] beta.txt
461+
└── [ 46] charlie.txt"
462462
`;
463463

464464
exports[`tree > du 7`] = `
465-
"127B multi-level
466-
├── 10B alpha.txt
467-
├── 48B beta
468-
│ ├── 27B alpha
469-
│ │ ├── 16B alpha.txt
470-
│ │ └── 11B beta
471-
│ │ └── 11B alpha.txt
472-
│ ├── 9B beta.txt
473-
│ └── 12B charlie
474-
│ └── 12B alpha.txt
475-
├── 54B charlie
476-
│ ├── 13B alpha.txt
477-
│ ├── 18B beta
478-
│ │ └── 18B alpha.txt
479-
│ └── 23B charlie.txt
480-
└── 15B delta.txt"
465+
"[ 127] multi-level
466+
├── [ 10] alpha.txt
467+
├── [ 48] beta
468+
│ ├── [ 27] alpha
469+
│ │ ├── [ 16] alpha.txt
470+
│ │ └── [ 11] beta
471+
│ │ └── [ 11] alpha.txt
472+
│ ├── [ 9] beta.txt
473+
│ └── [ 12] charlie
474+
│ └── [ 12] alpha.txt
475+
├── [ 54] charlie
476+
│ ├── [ 13] alpha.txt
477+
│ ├── [ 18] beta
478+
│ │ └── [ 18] alpha.txt
479+
│ └── [ 23] charlie.txt
480+
└── [ 15] delta.txt"
481481
`;
482482

483483
exports[`tree > du 8`] = `
484-
"0B numbered-files
485-
├── 0B file1.txt
486-
├── 0B file10.txt
487-
├── 0B file2.txt
488-
├── 0B file20.txt
489-
└── 0B file3.txt"
484+
"[ 0] numbered-files
485+
├── [ 0] file1.txt
486+
├── [ 0] file10.txt
487+
├── [ 0] file2.txt
488+
├── [ 0] file20.txt
489+
└── [ 0] file3.txt"
490490
`;
491491

492492
exports[`tree > du 9`] = `
493-
"5B single-file
494-
└── 5B alpha.txt"
493+
"[ 5] single-file
494+
└── [ 5] alpha.txt"
495495
`;
496496

497497
exports[`tree > du 10`] = `
498-
"13B single-level
499-
├── 13B alpha.txt
500-
└── 0B beta"
498+
"[ 13] single-level
499+
├── [ 13] alpha.txt
500+
└── [ 0] beta"
501501
`;
502502

503503
exports[`tree > du 11`] = `
504-
"4B unicode-names
505-
├── 1B café.txt
506-
├── 1B emoji-🎉.txt
507-
├── 1B über.txt
508-
└── 1B 日本語
509-
└── 1B ファイル.txt"
504+
"[ 4] unicode-names
505+
├── [ 1] café.txt
506+
├── [ 1] emoji-🎉.txt
507+
├── [ 1] über.txt
508+
└── [ 1] 日本語
509+
└── [ 1] ファイル.txt"
510510
`;
511511

512512
exports[`tree > du 12`] = `
513-
"11.1kB varied-sizes
514-
├── 10kB large.txt
515-
├── 1kB medium.txt
516-
├── 100B small.txt
517-
└── 1B tiny.txt"
513+
"[ 11101] varied-sizes
514+
├── [ 10000] large.txt
515+
├── [ 1000] medium.txt
516+
├── [ 100] small.txt
517+
└── [ 1] tiny.txt"
518518
`;
519519

520520
exports[`tree > exclude 1`] = `

src/cli.test.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ describe('cli', () => {
2929
expect(exitCode).toBe(0);
3030
});
3131

32-
test('displays help with -h', async () => {
33-
const { stdout, exitCode } = await run(['-h']);
32+
test('displays help with --help', async () => {
33+
const { stdout, exitCode } = await run(['--help']);
3434
expect(stdout).toContain('--all-files');
3535
expect(stdout).toContain('--exclude');
3636
expect(exitCode).toBe(0);
@@ -233,11 +233,11 @@ describe('cli', () => {
233233
]);
234234
// Without sizes, just the filename
235235
expect(withoutSizes).toContain('alpha.txt');
236-
expect(withoutSizes).not.toMatch(/\dB/);
237-
// With sizes, each line should have a size prefix like "5B" or "4.1kB"
236+
expect(withoutSizes).not.toMatch(/\[\s*\d+\]/);
237+
// With sizes, each line should have a bracketed size prefix like [ 1024]
238238
const lines = withSizes.split('\n');
239239
for (const line of lines) {
240-
expect(line).toMatch(/\d+(\.\d+)?(B|kB|MB)/);
240+
expect(line).toMatch(/\[\s*\d+\]/);
241241
}
242242
});
243243

@@ -249,7 +249,7 @@ describe('cli', () => {
249249
// --du implies -s, so sizes should be shown
250250
const lines = stdout.split('\n');
251251
for (const line of lines) {
252-
expect(line).toMatch(/\dB/);
252+
expect(line).toMatch(/\[\s*\d+\]/);
253253
}
254254
});
255255

@@ -363,7 +363,7 @@ describe('cli', () => {
363363
});
364364

365365
test('help includes --gitignore option', async () => {
366-
const { stdout } = await run(['-h']);
366+
const { stdout } = await run(['--help']);
367367
expect(stdout).toContain('--gitignore');
368368
});
369369

src/cli.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,14 @@ cli
3535
'-s, --sizes',
3636
'Print the size of each file in bytes along with the name.',
3737
)
38+
.option(
39+
'-h, --human-readable',
40+
'Print file sizes in human-readable format (powers of 1024). Implies -s.',
41+
)
42+
.option(
43+
'--si',
44+
'Print file sizes using SI units (powers of 1000). Implies -s.',
45+
)
3846
.option(
3947
'--du',
4048
'For each directory report its size as the accumulation of sizes of all its files and sub-directories. Implies -s.',
@@ -65,12 +73,18 @@ cli
6573
// JSON options
6674
.option('-J, --json', 'Output the tree as a JSON structure.');
6775

68-
cli.help();
76+
// Register --help without -h shortcut so -h can be reclaimed for other use.
77+
cli.option('--help', 'Display this message');
6978
cli.version(version, '--version');
7079

7180
const parsed = cli.parse();
7281

73-
if (parsed.options.help || parsed.options.version) {
82+
if (parsed.options.help) {
83+
cli.outputHelp();
84+
process.exit(0);
85+
}
86+
87+
if (parsed.options.version) {
7488
process.exit(0);
7589
}
7690

@@ -104,6 +118,10 @@ const options: Options = {
104118
...(opts.prune != null && { prune: opts.prune as boolean }),
105119
...(opts.quote != null && { quote: opts.quote as boolean }),
106120
...(opts.sizes != null && { sizes: opts.sizes as boolean }),
121+
...(opts.humanReadable != null && {
122+
humanReadable: opts.humanReadable as boolean,
123+
}),
124+
...(opts.si != null && { si: opts.si as boolean }),
107125
...(opts.du != null && { du: opts.du as boolean }),
108126
...(opts.exclude != null && {
109127
exclude: (opts.exclude as string)

src/formats/json.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,14 @@ const DEFAULT_OPTIONS: RequiredOptions = {
1111
du: false,
1212
filesFirst: false,
1313
fullPath: false,
14+
gitignore: true,
15+
humanReadable: false,
1416
noIndent: false,
1517
permissions: false,
1618
prune: false,
1719
quote: false,
1820
sizes: false,
21+
si: false,
1922
exclude: [],
2023
maxDepth: Number.POSITIVE_INFINITY,
2124
reverse: false,

src/formats/text.test.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,15 @@ const DEFAULT_OPTIONS: RequiredOptions = {
1010
dirsOnly: false,
1111
du: false,
1212
filesFirst: false,
13+
humanReadable: false,
1314
fullPath: false,
15+
gitignore: true,
1416
noIndent: false,
1517
permissions: false,
1618
prune: false,
1719
quote: false,
1820
sizes: false,
21+
si: false,
1922
exclude: [],
2023
maxDepth: Number.POSITIVE_INFINITY,
2124
reverse: false,
@@ -145,12 +148,28 @@ describe('formatAsText', () => {
145148
expect(lines[1]).toBe('└── [-rw-r--r--] a.ts');
146149
});
147150

148-
test('sizes shows file size', () => {
151+
test('sizes shows file size in raw bytes', () => {
149152
const node = makeFile('a.ts', { size: 2048 });
150153
const lines = formatAsText(node, opts({ sizes: true }), '.');
154+
expect(lines[0]).toBe('[ 2048] a.ts');
155+
});
156+
157+
test('sizes with si shows human-readable size (powers of 1000)', () => {
158+
const node = makeFile('a.ts', { size: 2048 });
159+
const lines = formatAsText(node, opts({ sizes: true, si: true }), '.');
151160
expect(lines[0]).toContain('2.05kB');
152161
});
153162

163+
test('sizes with humanReadable shows binary size (powers of 1024)', () => {
164+
const node = makeFile('a.ts', { size: 2048 });
165+
const lines = formatAsText(
166+
node,
167+
opts({ sizes: true, humanReadable: true }),
168+
'.',
169+
);
170+
expect(lines[0]).toContain('2KiB');
171+
});
172+
154173
test('fullPath shows relative path', () => {
155174
const node = makeDir('src', [makeFile('a.ts', { path: 'src/a.ts' })]);
156175
const lines = formatAsText(node, opts({ fullPath: true }), 'src');

src/formats/text.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,13 @@ export function formatAsText(
4949
}
5050

5151
if (options.sizes) {
52-
const prettifiedFilesize = prettyBytes(node.size);
53-
line.push(prettifiedFilesize.replace(' ', ''));
52+
if (options.si) {
53+
line.push(prettyBytes(node.size).replace(' ', ''));
54+
} else if (options.humanReadable) {
55+
line.push(prettyBytes(node.size, { binary: true }).replace(' ', ''));
56+
} else {
57+
line.push(`[${String(node.size).padStart(10)}]`);
58+
}
5459
line.push(' ');
5560
}
5661

0 commit comments

Comments
 (0)