Skip to content

Commit 1c8acf8

Browse files
committed
chore: add --stdin-paths and -stdin-paths0
1 parent 7f65ff4 commit 1c8acf8

File tree

15 files changed

+140
-4
lines changed

15 files changed

+140
-4
lines changed

crates/typos-cli/src/bin/typos-cli/args.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,17 @@ impl Default for Format {
4444
#[command(group = clap::ArgGroup::new("mode").multiple(false))]
4545
pub(crate) struct Args {
4646
/// Paths to check with `-` for stdin
47-
#[arg(default_value = ".")]
47+
#[arg(default_value = ".", group = "source")]
4848
pub(crate) path: Vec<std::path::PathBuf>,
4949

50+
/// Read the list of newline separated paths from stdin
51+
#[arg(long, group = "source")]
52+
pub(crate) stdin_paths: bool,
53+
54+
/// Read the list of '\0' separated paths from stdin
55+
#[arg(long, group = "source")]
56+
pub(crate) stdin_paths0: bool,
57+
5058
/// Custom config file
5159
#[arg(short = 'c', long = "config")]
5260
pub(crate) custom_config: Option<std::path::PathBuf>,
@@ -104,6 +112,12 @@ pub(crate) struct Args {
104112
pub(crate) verbose: clap_verbosity_flag::Verbosity,
105113
}
106114

115+
impl Args {
116+
pub fn is_using_stdin_paths(&self) -> bool {
117+
self.stdin_paths || self.stdin_paths0
118+
}
119+
}
120+
107121
#[derive(Debug, Clone, clap::Args)]
108122
#[command(rename_all = "kebab-case")]
109123
pub(crate) struct FileArgs {

crates/typos-cli/src/bin/typos-cli/main.rs

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
use std::io::Write;
1+
use std::io::{Read as _, Write as _};
2+
use std::path::PathBuf;
23

34
use clap::Parser;
45

@@ -156,8 +157,29 @@ fn run_checks(args: &args::Args) -> proc_exit::ExitResult {
156157

157158
let mut typos_found = false;
158159
let mut errors_found = false;
159-
for path in args.path.iter() {
160+
161+
let stdin_paths = if args.stdin_paths {
162+
Some(
163+
std::io::stdin()
164+
.lines()
165+
.map(|res| res.map(PathBuf::from))
166+
.collect::<Result<_, _>>()
167+
.with_code(proc_exit::sysexits::IO_ERR)?,
168+
)
169+
} else if args.stdin_paths0 {
170+
Some(unix_read_paths_from_stdin()?)
171+
} else {
172+
None
173+
};
174+
175+
// Note: stdin_paths and args.path are mutually exclusive, enforced by clap
176+
for path in stdin_paths.as_ref().unwrap_or(&args.path) {
177+
// Note paths are passed through stdin, `-` is treated like a normal path
160178
let cwd = if path == std::path::Path::new("-") {
179+
if args.is_using_stdin_paths() {
180+
return proc_exit::sysexits::USAGE_ERR
181+
.with_message("Can't use `-` (stdin) while using stdin provided paths");
182+
};
161183
global_cwd.clone()
162184
} else if path.is_file() {
163185
let mut cwd = path
@@ -269,6 +291,39 @@ fn run_checks(args: &args::Args) -> proc_exit::ExitResult {
269291
}
270292
}
271293

294+
#[cfg(target_family = "unix")]
295+
fn unix_read_paths_from_stdin() -> Result<Vec<PathBuf>, proc_exit::Exit> {
296+
use std::ffi::OsString;
297+
use std::os::unix::ffi::OsStringExt;
298+
299+
let mut buf = Vec::new();
300+
std::io::stdin()
301+
.read_to_end(&mut buf)
302+
.with_code(proc_exit::sysexits::IO_ERR)?;
303+
304+
let (mut paths, rest) = buf.into_iter().fold(
305+
(vec![], vec![]),
306+
|(mut paths, mut cur_path): (Vec<PathBuf>, Vec<u8>), byte| {
307+
if byte == 0 {
308+
paths.push(PathBuf::from(OsString::from_vec(cur_path)));
309+
(paths, vec![])
310+
} else {
311+
cur_path.push(byte);
312+
(paths, cur_path)
313+
}
314+
},
315+
);
316+
if !rest.is_empty() {
317+
paths.push(PathBuf::from(OsString::from_vec(rest)));
318+
}
319+
Ok(paths)
320+
}
321+
322+
#[cfg(not(target_family = "unix"))]
323+
fn unix_read_paths_from_stdin() -> Result<Vec<PathBuf>, proc_exit::Exit> {
324+
proc_exit::sysexits::CONFIG_ERR.with_message("Reading zero-separated paths is not supported on Windows. PRs are welcome.")
325+
}
326+
272327
fn init_logging(level: Option<log::Level>) {
273328
if let Some(level) = level {
274329
let mut builder = env_logger::Builder::new();

crates/typos-cli/tests/cli_tests.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
#[test]
22
#[cfg(feature = "dict")]
33
fn cli_tests() {
4-
trycmd::TestCases::new().case("tests/cmd/*.toml");
4+
trycmd::TestCases::new()
5+
.case("tests/cmd/*.toml")
6+
.case("tests/cmd/*.trycmd");
57
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
[files]
2+
extend-exclude = ["_typos.toml"]
3+
4+
[default.extend-identifiers]
5+
hello = "goodbye"
6+
7+
[type.fail]
8+
extend-glob = ["*.fail"]
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
hello
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
hello
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
hello
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
hello
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
bin.name = "typos"
2+
args = "--stdin-paths"
3+
stdin = """
4+
b.fail
5+
d.fail
6+
"""
7+
stdout = """
8+
error: `hello` should be `goodbye`
9+
--> b.fail:1:1
10+
|
11+
1 | hello
12+
| ^^^^^
13+
|
14+
error: `hello` should be `goodbye`
15+
--> d.fail:1:1
16+
|
17+
1 | hello
18+
| ^^^^^
19+
|
20+
"""
21+
stderr = ""
22+
status.code = 2
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
[files]
2+
extend-exclude = ["_typos.toml"]
3+
4+
[default.extend-identifiers]
5+
hello = "goodbye"
6+
7+
[type.fail]
8+
extend-glob = ["*.fail"]

0 commit comments

Comments
 (0)