Skip to content

Commit 61b2390

Browse files
committed
Show --help when invoked without arguments on a TTY
- Detect if stdin is a TTY and show help instead of waiting for input - Add '-' argument to explicitly read from stdin (useful for interactive input) - Update help text to document the '-' option with example
1 parent f6d5360 commit 61b2390

1 file changed

Lines changed: 75 additions & 20 deletions

File tree

src/main.rs

Lines changed: 75 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
use bash_ast::{init, parse_to_json, schema_json};
66
use std::env;
77
use std::fs;
8-
use std::io::{self, BufRead, Write};
8+
use std::io::{self, BufRead, IsTerminal, Write};
99
use std::process::ExitCode;
1010

1111
const VERSION: &str = env!("CARGO_PKG_VERSION");
@@ -22,7 +22,7 @@ DESCRIPTION:
2222
compatibility with bash syntax.
2323
2424
ARGUMENTS:
25-
[FILE] Bash script file to parse. If omitted, reads from stdin.
25+
[FILE] Bash script file to parse. Use '-' to read from stdin explicitly.
2626
2727
OPTIONS:
2828
-h, --help Print this help message and exit
@@ -34,12 +34,15 @@ EXAMPLES:
3434
# Parse a script file
3535
bash-ast script.sh
3636
37-
# Parse from stdin
37+
# Parse from stdin (piped)
3838
echo 'echo hello' | bash-ast
3939
4040
# Parse inline with here-string
4141
bash-ast <<< 'for i in a b c; do echo $i; done'
4242
43+
# Read from stdin interactively (use '-' to wait for input)
44+
bash-ast -
45+
4346
# Compact output for piping
4447
bash-ast -c script.sh | jq '.commands[]'
4548
@@ -82,7 +85,9 @@ MORE INFO:
8285

8386
fn main() -> ExitCode {
8487
let args: Vec<String> = env::args().collect();
85-
run(&args[1..], io::stdin().lock(), io::stdout(), io::stderr())
88+
let stdin = io::stdin();
89+
let is_tty = stdin.is_terminal();
90+
run(&args[1..], stdin.lock(), io::stdout(), io::stderr(), is_tty)
8691
}
8792

8893
#[derive(Debug, Default)]
@@ -105,6 +110,7 @@ fn parse_args(args: &[String]) -> Result<Config, String> {
105110
"-V" | "--version" => config.version = true,
106111
"-c" | "--compact" => config.compact = true,
107112
"-s" | "--schema" => config.schema = true,
113+
"-" => positional.push(arg.clone()), // `-` means read from stdin
108114
s if s.starts_with('-') => {
109115
return Err(format!(
110116
"Unknown option: {s}\nTry 'bash-ast --help' for usage."
@@ -126,7 +132,13 @@ fn parse_args(args: &[String]) -> Result<Config, String> {
126132
}
127133

128134
/// Run the CLI with the given arguments and input/output streams
129-
fn run<R, W, E>(args: &[String], mut input: R, mut output: W, mut error: E) -> ExitCode
135+
fn run<R, W, E>(
136+
args: &[String],
137+
mut input: R,
138+
mut output: W,
139+
mut error: E,
140+
stdin_is_tty: bool,
141+
) -> ExitCode
130142
where
131143
R: BufRead,
132144
W: Write,
@@ -141,8 +153,10 @@ where
141153
}
142154
};
143155

144-
// Handle --help
145-
if config.help {
156+
// Handle --help, or show help if no file/stdin and stdin is a TTY (no piped input)
157+
// Users can use `-` to explicitly read from stdin even in a TTY
158+
let reading_stdin = config.file.as_deref() == Some("-");
159+
if config.help || (config.file.is_none() && !config.schema && stdin_is_tty && !reading_stdin) {
146160
let _ = write!(output, "{HELP}");
147161
return ExitCode::SUCCESS;
148162
}
@@ -160,22 +174,23 @@ where
160174
return ExitCode::SUCCESS;
161175
}
162176

163-
// Read script from file or stdin
164-
let script = if let Some(ref path) = config.file {
165-
match fs::read_to_string(path) {
177+
// Read script from file or stdin (use "-" to explicitly read from stdin)
178+
let script = match config.file.as_deref() {
179+
Some("-") | None => {
180+
let mut content = String::new();
181+
if let Err(e) = input.read_to_string(&mut content) {
182+
let _ = writeln!(error, "Error reading stdin: {e}");
183+
return ExitCode::from(1);
184+
}
185+
content
186+
}
187+
Some(path) => match fs::read_to_string(path) {
166188
Ok(content) => content,
167189
Err(e) => {
168190
let _ = writeln!(error, "Error reading '{path}': {e}");
169191
return ExitCode::from(1);
170192
}
171-
}
172-
} else {
173-
let mut content = String::new();
174-
if let Err(e) = input.read_to_string(&mut content) {
175-
let _ = writeln!(error, "Error reading stdin: {e}");
176-
return ExitCode::from(1);
177-
}
178-
content
193+
},
179194
};
180195

181196
// Initialize bash parser
@@ -208,14 +223,19 @@ mod tests {
208223
}
209224

210225
impl TestRun {
211-
/// Run CLI with given args and stdin content
226+
/// Run CLI with given args and stdin content (simulates piped input)
212227
fn new(cli_args: &[&str], stdin: &str) -> Self {
228+
Self::with_tty(cli_args, stdin, false)
229+
}
230+
231+
/// Run CLI with given args, stdin content, and TTY flag
232+
fn with_tty(cli_args: &[&str], stdin: &str, stdin_is_tty: bool) -> Self {
213233
let input = Cursor::new(stdin.to_string());
214234
let mut output = Vec::new();
215235
let mut error = Vec::new();
216236

217237
let args: Vec<String> = cli_args.iter().map(|&s| s.to_string()).collect();
218-
let exit_code = run(&args, input, &mut output, &mut error);
238+
let exit_code = run(&args, input, &mut output, &mut error, stdin_is_tty);
219239

220240
Self {
221241
exit_code,
@@ -346,4 +366,39 @@ mod tests {
346366
assert!(t.stdout.contains("\"type\": \"for\""));
347367
assert!(t.stderr.is_empty());
348368
}
369+
370+
#[test]
371+
fn test_no_args_tty_shows_help() {
372+
// When stdin is a TTY and no args given, show help
373+
let t = TestRun::with_tty(&[], "", true);
374+
assert!(t.success());
375+
assert!(t.stdout.contains("USAGE:"));
376+
assert!(t.stdout.contains("bash-ast"));
377+
assert!(t.stderr.is_empty());
378+
}
379+
380+
#[test]
381+
fn test_no_args_piped_empty_is_error() {
382+
// When stdin is piped (not TTY) but empty, it's an error
383+
let t = TestRun::with_tty(&[], "", false);
384+
assert_eq!(t.exit_code, ExitCode::from(1));
385+
assert!(t.stderr.contains("Error"));
386+
}
387+
388+
#[test]
389+
fn test_dash_reads_stdin_even_on_tty() {
390+
// Using `-` explicitly reads from stdin, even if it's a TTY
391+
let t = TestRun::with_tty(&["-"], "echo hello", true);
392+
assert!(t.success());
393+
assert!(t.stdout.contains("\"type\": \"simple\""));
394+
assert!(t.stdout.contains("echo"));
395+
}
396+
397+
#[test]
398+
fn test_dash_reads_stdin_piped() {
399+
// Using `-` works the same as no arg when piped
400+
let t = TestRun::new(&["-"], "echo hello");
401+
assert!(t.success());
402+
assert!(t.stdout.contains("\"type\": \"simple\""));
403+
}
349404
}

0 commit comments

Comments
 (0)