Skip to content

Commit 2998d51

Browse files
feat(cli): add recursive traversal (#98)
1 parent c42bef4 commit 2998d51

File tree

10 files changed

+68
-28
lines changed

10 files changed

+68
-28
lines changed

.github/workflows/workflow.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,8 @@ jobs:
138138
- name: Run KeepSorted
139139
shell: bash
140140
run: |
141-
# keepsorted works only with file paths; filter tracked files first.
141+
# keepsorted can walk directories with --recursive, but we filter tracked files
142+
# explicitly to avoid processing examples or tests during validation.
142143
git ls-files -z \
143144
| grep -vzE '^tests/|^e2e-tests/|^README.md$' \
144145
| xargs -0 -n1 ./target/release/keepsorted \

CHANGELOG.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ format and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html)
1111
- `--diff-command` option to delegate diff generation to an external tool
1212
- Buildifier-style exit codes with `4` indicating failed check mode
1313

14-
### Changed
15-
- Removed `--recursive` flag; use shell tools to process directories
14+
### Added
15+
- `--recursive` (`-r`) flag to process directories recursively
1616

1717
### Fixed
1818
- Show a usage error when an unknown feature is specified

README.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,14 @@ cargo install keepsorted
2525
- `keepsorted --check <path>` verifies sorting without modifying files.
2626
- `keepsorted --diff <path>` shows a diff of required changes.
2727
- `keepsorted --fix <path>` updates files in place.
28-
- keepsorted only processes a single file at a time. Directory traversal and filtering are left to shell tools like `git ls-files` so that you can easily include or exclude paths. See [Architecture](docs/architecture.md) for details.
28+
- By default `keepsorted` processes a single file. Use `--recursive` (`-r`) to walk a directory and format every supported file. For complex include/exclude rules, combine `keepsorted` with shell tools like `git ls-files`. See [Architecture](docs/architecture.md) for details.
2929

3030
Run `keepsorted --check` in CI after filtering tracked files with
3131
`git ls-files` to prevent unsorted changes.
3232

3333
### Pre-commit hook
3434

35-
keepsorted only accepts explicit file paths. To scan all tracked files except
36-
the test directories, save this script as `.git/hooks/pre-commit`:
35+
keepsorted requires explicit paths unless you enable `--recursive`. To scan all tracked files except the test directories, save this script as `.git/hooks/pre-commit`:
3736

3837
```shell
3938
#!/bin/sh

docs/architecture.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ Each file under `src/strategies/` provides a `process` function that sorts lines
6060

6161
### CLI (`src/main.rs`)
6262

63-
`main.rs` implements the command-line interface using `clap`. It parses arguments, selects the formatting mode (check, diff or fix) and passes a single file to `handle_file`. Directory traversal is intentionally left to external scripts so that the binary stays simple and composable. There is deliberately no `-r` or `--recursive` option; use tools like `git ls-files` if you need to process multiple files. The helper `handle_file` runs the crate API on each file and applies the chosen mode.
63+
`main.rs` implements the command-line interface using `clap`. It parses arguments, selects the formatting mode (check, diff or fix) and processes files via `handle_file`. The CLI now supports a basic `-r`/`--recursive` flag to walk the provided directory and run `keepsorted` on every supported file it finds. This is convenient for simple use cases where users want to sort all files under the current directory. For more advanced setups that require ignoring particular paths, callers can still generate the file list themselves with tools like `find` or `git ls-files` and pass each path explicitly. The helper `handle_file` runs the crate API on each file and applies the chosen mode.
6464

6565
When `--mode diff` is used, the CLI can delegate diff generation to an external
6666
program by passing `--diff-command <command>`. The option is parsed and executed
@@ -96,7 +96,7 @@ Rust unit tests inside `tests/` verify the behaviour of individual strategies fo
9696

9797
To stay focused and composable, `keepsorted` does **not** aim to:
9898

99-
- Perform recursive directory traversal (use external tools like `find`, `git ls-files`, or CI filters)
99+
- Handle advanced directory traversal or ignore rules automatically
100100
- Act as a full-fledged parser for every supported file type
101101
- Handle ignore files or exclude paths automatically
102102
- Automatically detect project structure or configuration files

e2e-tests/directory_error.bats

Lines changed: 0 additions & 12 deletions
This file was deleted.

e2e-tests/files/help.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ Arguments:
77

88
Options:
99
-p, --path <PATH> File to process (conflicts with positional path)
10+
-r, --recursive Process directories recursively
1011
-f, --features <FEATURE> Enable experimental features [possible values: gitignore, codeowners, rust_derive_alphabetical, rust_derive_canonical]
1112
--check alias for '--mode check'
1213
--diff alias for '--mode diff'

e2e-tests/files/help_long.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ Options:
1919
-p, --path <PATH>
2020
File to process (conflicts with positional path)
2121

22+
-r, --recursive
23+
Process directories recursively
24+
2225
-f, --features <FEATURE>
2326
Enable experimental features
2427

e2e-tests/recursive.bats

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
#!/usr/bin/env bats
2+
3+
load './helpers.bash'
4+
5+
@test "process directory recursively" {
6+
mkdir "$TEST_TMPDIR/dir"
7+
cp "$FILES_DIR/generic/1_in.txt" "$TEST_TMPDIR/dir/file.txt"
8+
9+
run_keepsorted --mode fix --recursive "$TEST_TMPDIR/dir"
10+
[ "$status" -eq "$EXIT_SUCCESS" ]
11+
diff "$FILES_DIR/generic/1_out.txt" "$TEST_TMPDIR/dir/file.txt"
12+
}
13+

run-all.sh

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ clippy_status=$?
2525
cargo fmt --all -- --check
2626
fmt_status=$?
2727

28-
# keepsorted accepts only explicit file paths, so filter tracked files first.
28+
# keepsorted can walk directories with --recursive, but we filter tracked files
29+
# explicitly to avoid processing examples or tests during validation.
2930
git ls-files -z \
3031
| grep -vzE '^tests/|^e2e-tests/|^README.md$' \
3132
| xargs -0 -n1 ./target/release/keepsorted \

src/main.rs

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use clap::{arg, command, Parser, ValueEnum};
22
use keepsorted::process_file;
33
use std::io;
4-
use std::path::Path;
4+
use std::path::{Path, PathBuf};
55
use std::process::{self, Command};
66

77
/// Exit code used when the command is invoked incorrectly.
@@ -110,6 +110,10 @@ struct Args {
110110
)]
111111
positional_path: Option<String>,
112112

113+
/// Recursively traverse directories for files
114+
#[arg(short = 'r', long, help = "Process directories recursively")]
115+
recursive: bool,
116+
113117
#[arg(
114118
short = 'f',
115119
long,
@@ -190,12 +194,29 @@ fn main() {
190194
let mut exit_code = 0;
191195

192196
if path.is_dir() {
193-
eprintln!(
194-
"{}: read {}: is a directory",
195-
env!("CARGO_PKG_NAME"),
196-
path.display()
197-
);
198-
process::exit(EXIT_USAGE_ERROR);
197+
if !args.recursive {
198+
eprintln!(
199+
"{}: read {}: is a directory",
200+
env!("CARGO_PKG_NAME"),
201+
path.display()
202+
);
203+
process::exit(EXIT_USAGE_ERROR);
204+
}
205+
let mut files = Vec::new();
206+
if let Err(e) = collect_files(path, &mut files) {
207+
eprintln!(
208+
"{}: failed to read directory {}: {}",
209+
env!("CARGO_PKG_NAME"),
210+
path.display(),
211+
e
212+
);
213+
process::exit(EXIT_RUNTIME_ERROR);
214+
}
215+
for file in files {
216+
if !handle_file(&file, &features, mode, args.diff_command.as_deref()) {
217+
exit_code = EXIT_CHECK_FAILED;
218+
}
219+
}
199220
} else if !handle_file(path, &features, mode, args.diff_command.as_deref()) {
200221
exit_code = EXIT_CHECK_FAILED;
201222
}
@@ -335,3 +356,16 @@ fn handle_file(path: &Path, features: &[Feature], mode: Mode, diff_command: Opti
335356
}
336357
}
337358
}
359+
360+
fn collect_files(dir: &Path, out: &mut Vec<PathBuf>) -> io::Result<()> {
361+
for entry in std::fs::read_dir(dir)? {
362+
let entry = entry?;
363+
let path = entry.path();
364+
if path.is_dir() {
365+
collect_files(&path, out)?;
366+
} else if path.is_file() {
367+
out.push(path);
368+
}
369+
}
370+
Ok(())
371+
}

0 commit comments

Comments
 (0)