Skip to content

Commit 2510be9

Browse files
elifarleyclaude
andcommitted
Add flexible CLI output flags: -T/--tree-only, short aliases, and column control
New flags: - -T/--tree-only: Show only path column (minimum output) - -t: Short alias for --tree - -n: Short alias for --numbered-indent - --show-word-count: Override --skip-word-count to show word count column - -S/--short: Alias for --tree-only --show-word-count Features: - Tree-only mode can add date column back with --date-format [seconds|day] - Date appears before path as requested when re-added - Short flags work in combinations (-nT, -Tn, etc.) - Conflict detection for mutually exclusive flags - All existing functionality preserved Implementation: - Refactored column formatting with DisplayConfig class - Centralized format_data_row() function eliminates duplication - Updated both manual and Click CLI parsers - Enhanced flat and tree renderers - Updated documentation with examples 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 89694cc commit 2510be9

File tree

5 files changed

+346
-147
lines changed

5 files changed

+346
-147
lines changed

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,18 @@ cedarmapper ls --skip-date
5656
# Skip word counting for faster analysis
5757
cedarmapper ls --skip-word-count
5858

59+
# Tree-only output (paths only)
60+
cedarmapper ls --tree-only
61+
62+
# Short output with word counts and paths
63+
cedarmapper ls --short
64+
65+
# Tree-only with date re-added
66+
cedarmapper ls --tree-only --date-format day
67+
68+
# Use short aliases
69+
cedarmapper ls -t -n # equivalent to --tree --numbered-indent
70+
5971
# Follow symbolic links during traversal
6072
cedarmapper ls --follow-symlinks
6173

@@ -67,8 +79,13 @@ cedarmapper ls src/ --tree --max-depth 3 --sort -s
6779

6880
- `--max-depth, -d` - Display max depth: 0=root only, 1=root+children, etc.
6981
- `--tree` - Show tree-like nested output (human-friendly)
82+
- `--tree-only, -T` - Show only path column (tree-only mode)
83+
- `-t` - Alias for `--tree`
84+
- `-n` - Alias for `--numbered-indent`
7085
- `--follow-symlinks` - Follow symbolic links during traversal
7186
- `--skip-word-count` - Skip word counting for speed
87+
- `--show-word-count` - Show word count column (overrides `--skip-word-count`)
88+
- `--short, -S` - Alias for `--tree-only --show-word-count`
7289
- `--date-format` - Date format: 'seconds' (default) or 'day'
7390
- `--skip-date` - Skip date display entirely
7491
- `--numbered-indent` - Show depth as number prefix (tree only)

src/cedarmapper/cli/main.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,15 @@ def run_cli(argv: list[str] | None = None) -> tuple[int, str]:
2828
follow_symlinks = False
2929
skip_word_count = False
3030
date_format = "seconds" # seconds | day
31+
explicit_date_format = False # Whether user explicitly set --date-format
3132
skip_date = False
3233
numbered_indent = False
3334
sort_spec = None
3435
skip_header = False
3536
skip_totals = False
37+
tree_only = False
38+
show_word_count = False
39+
short_flag = False
3640

3741
it = iter(argv)
3842
for token in it:
@@ -52,12 +56,23 @@ def run_cli(argv: list[str] | None = None) -> tuple[int, str]:
5256
elif token == "--date-format":
5357
try:
5458
date_format = next(it)
59+
explicit_date_format = True # User explicitly set date format
5560
if date_format not in ("seconds", "day"):
5661
return 2, "Error: --date-format must be one of seconds|day"
5762
except StopIteration:
5863
return 2, "Error: --date-format expects a value"
5964
elif token == "--numbered-indent":
6065
numbered_indent = True
66+
elif token in ("--tree-only", "-T"):
67+
tree_only = True
68+
elif token == "-t":
69+
tree = True # alias for --tree
70+
elif token == "-n":
71+
numbered_indent = True # alias for --numbered-indent
72+
elif token == "--show-word-count":
73+
show_word_count = True
74+
elif token in ("--short", "-S"):
75+
short_flag = True
6176
elif token == "--sort":
6277
try:
6378
sort_spec = next(it)
@@ -74,6 +89,19 @@ def run_cli(argv: list[str] | None = None) -> tuple[int, str]:
7489
# positional path (first one)
7590
path = token
7691

92+
# Apply aliases and resolve conflicts
93+
if short_flag:
94+
tree_only = True
95+
show_word_count = True
96+
97+
# tree_only implies tree mode for consistency
98+
if tree_only and not tree:
99+
tree = True
100+
101+
# Validate conflicting flags
102+
if show_word_count and skip_word_count:
103+
return 2, "Error: --show-word-count and --skip-word-count are mutually exclusive"
104+
77105
start = Path(path)
78106
if not start.exists():
79107
return 2, f"Error: Path not found: {start}"
@@ -93,6 +121,9 @@ def run_cli(argv: list[str] | None = None) -> tuple[int, str]:
93121
skip_header=skip_header,
94122
skip_totals=skip_totals,
95123
skip_word_count=skip_word_count,
124+
tree_only=tree_only,
125+
show_word_count=show_word_count,
126+
explicit_date_format=explicit_date_format,
96127
)
97128
else:
98129
out = render_flat(
@@ -104,6 +135,9 @@ def run_cli(argv: list[str] | None = None) -> tuple[int, str]:
104135
skip_header=skip_header,
105136
skip_totals=skip_totals,
106137
skip_word_count=skip_word_count,
138+
tree_only=tree_only,
139+
show_word_count=show_word_count,
140+
explicit_date_format=explicit_date_format,
107141
)
108142

109143
return 0, out
@@ -171,6 +205,40 @@ def cli() -> None:
171205
default=False,
172206
help="Skip totals footer display.",
173207
)
208+
@click.option(
209+
"--tree-only",
210+
"-T",
211+
is_flag=True,
212+
default=False,
213+
help="Show only path column (tree-only mode).",
214+
)
215+
@click.option(
216+
"-t",
217+
"tree_alias",
218+
is_flag=True,
219+
default=False,
220+
help="Alias for --tree.",
221+
)
222+
@click.option(
223+
"-n",
224+
"numbered_indent_alias",
225+
is_flag=True,
226+
default=False,
227+
help="Alias for --numbered-indent.",
228+
)
229+
@click.option(
230+
"--show-word-count",
231+
is_flag=True,
232+
default=False,
233+
help="Show word count column (overrides --skip-word-count).",
234+
)
235+
@click.option(
236+
"--short",
237+
"-S",
238+
is_flag=True,
239+
default=False,
240+
help="Alias for --tree-only --show-word-count.",
241+
)
174242
def ls(
175243
path: str,
176244
max_depth: int,
@@ -183,8 +251,30 @@ def ls(
183251
sort_spec: str | None,
184252
skip_header: bool,
185253
skip_totals: bool,
254+
tree_only: bool,
255+
tree_alias: bool,
256+
numbered_indent_alias: bool,
257+
show_word_count: bool,
258+
short: bool,
186259
) -> None:
187260
"""List repository contents with analysis for LLM consumption."""
261+
# Apply aliases
262+
tree = tree or tree_alias
263+
numbered_indent = numbered_indent or numbered_indent_alias
264+
265+
# Apply short flag alias
266+
if short:
267+
tree_only = True
268+
show_word_count = True
269+
270+
# Validate conflicting flags
271+
if show_word_count and skip_word_count:
272+
raise click.BadParameter("--show-word-count and --skip-word-count are mutually exclusive")
273+
274+
# tree_only implies tree mode for consistency
275+
if tree_only and not tree:
276+
tree = True
277+
188278
argv = [path]
189279
if max_depth:
190280
argv.extend(["--max-depth", str(max_depth)])
@@ -206,6 +296,12 @@ def ls(
206296
argv.append("--skip-header")
207297
if skip_totals:
208298
argv.append("--skip-totals")
299+
if tree_only:
300+
argv.append("--tree-only")
301+
if show_word_count:
302+
argv.append("--show-word-count")
303+
if short:
304+
argv.append("--short")
209305

210306
code, out = run_cli(argv)
211307

src/cedarmapper/render/flat.py

Lines changed: 44 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -8,25 +8,15 @@
88
from cedarmapper.core.model import DirInfo, FileInfo
99
from cedarmapper.render.sort import make_comparator, parse_sort_spec
1010
from cedarmapper.render.utils import (
11+
DisplayConfig,
12+
format_data_row,
1113
format_header_row,
1214
format_separator_row,
1315
format_totals_row,
1416
get_column_specs,
1517
)
1618

1719

18-
def _fmt_count(c: int | None) -> str:
19-
return "-" if c is None else str(c)
20-
21-
22-
def _fmt_mtime(dt: datetime, mode: str) -> str:
23-
if mode == "none":
24-
return ""
25-
if mode == "day":
26-
return dt.date().isoformat()
27-
return dt.replace(microsecond=0).isoformat()
28-
29-
3020
class _DisplayRow:
3121
"""Container for a displayable row; used for sorting."""
3222

@@ -64,6 +54,9 @@ def render_flat(
6454
skip_header: bool = False,
6555
skip_totals: bool = False,
6656
skip_word_count: bool = False,
57+
tree_only: bool = False,
58+
show_word_count: bool = False,
59+
explicit_date_format: bool = False,
6760
) -> str:
6861
"""
6962
Render all nodes (dirs and files) that are at display depth <= max_depth.
@@ -126,36 +119,55 @@ def _render_node(node: DirInfo | FileInfo, depth: int, parent_path: str) -> None
126119
comp = make_comparator(specs)
127120
rows.sort(key=cmp_to_key(comp))
128121

122+
# Create display configuration
123+
config = DisplayConfig(
124+
tree_only=tree_only,
125+
show_word_count=show_word_count,
126+
skip_word_count=skip_word_count,
127+
skip_date=skip_date,
128+
date_format=date_format,
129+
explicit_date_format=explicit_date_format,
130+
)
131+
129132
# Build lines
130133
lines: list[str] = []
131134

132135
# Add header if not skipped
133136
if not skip_header:
134-
column_specs = get_column_specs(date_format, skip_word_count, skip_date)
135-
lines.append(format_header_row(column_specs))
137+
# For backward compatibility with existing tests, use original column specs
138+
# but adjusted for new tree_only logic
139+
effective_skip_word_count = skip_word_count and not show_word_count
140+
effective_skip_date = skip_date or (tree_only and date_format == "none")
141+
142+
column_specs = get_column_specs(date_format, effective_skip_word_count, effective_skip_date)
143+
144+
# Special case: tree_only with date re-added
145+
if tree_only and not skip_date and date_format != "none":
146+
column_specs = [spec for spec in column_specs if spec.name != "Size"]
147+
148+
# Show path column unless tree_only with no other columns
149+
show_path = not (tree_only and not column_specs and date_format == "none")
150+
lines.append(format_header_row(column_specs, show_path=show_path))
136151

137152
# Add data rows
138153
for r in rows:
139-
date_str = _fmt_mtime(r.mtime, date_format)
140-
if skip_word_count and skip_date:
141-
# Size + Path
142-
lines.append(f"{r.size_bytes:>12} {r.path}")
143-
elif skip_word_count and not skip_date:
144-
# Size + Modified + Path
145-
lines.append(f"{r.size_bytes:>12} {date_str:>19} {r.path}")
146-
elif not skip_word_count and skip_date:
147-
# Words + Size + Path
148-
lines.append(f"{_fmt_count(r.word_count):>7} {r.size_bytes:>12} {r.path}")
149-
else:
150-
# Words + Size + Modified + Path
151-
lines.append(
152-
f"{_fmt_count(r.word_count):>7} {r.size_bytes:>12} {date_str:>19} {r.path}"
153-
)
154+
line = format_data_row(config, r.obj, r.path)
155+
lines.append(line)
154156

155157
# Add totals footer if not skipped and we have multiple data rows
156158
if not skip_totals and len(rows) >= 2:
157-
column_specs = get_column_specs(date_format, skip_word_count, skip_date)
158-
lines.append(format_separator_row(column_specs))
159-
lines.append(format_totals_row(column_specs, root))
159+
effective_skip_word_count = skip_word_count and not show_word_count
160+
effective_skip_date = skip_date or (tree_only and date_format == "none")
161+
162+
column_specs = get_column_specs(date_format, effective_skip_word_count, effective_skip_date)
163+
164+
# Special case: tree_only with date re-added
165+
if tree_only and not skip_date and date_format != "none":
166+
column_specs = [spec for spec in column_specs if spec.name != "Size"]
167+
168+
# Show path column unless tree_only with no other columns
169+
show_path = not (tree_only and not column_specs and date_format == "none")
170+
lines.append(format_separator_row(column_specs, show_path=show_path))
171+
lines.append(format_totals_row(column_specs, root, show_path=show_path))
160172

161173
return "\n".join(lines)

0 commit comments

Comments
 (0)