Skip to content

Commit f9fdf50

Browse files
authored
Merge pull request #19 from tomeichlersmith/18-check-exit-status
add ability to check exit status of command that was run
2 parents b3f4248 + 9ba271d commit f9fdf50

File tree

8 files changed

+219
-8
lines changed

8 files changed

+219
-8
lines changed

README.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,39 @@ At the moment of writing, there are examples using:
127127
- Node
128128
- Rust
129129
130+
Often, it is helpful to ensure that the commands being run successfully complete
131+
or at least return the expected exit code. This check is supported through an
132+
optional flag after `cmdrun` but before your command.
133+
Any errors encountered by cmdrun are reported in the rendered mdbook.
134+
For example, the following source
135+
136+
````markdown
137+
<!-- cmdrun -0 echo hello world -->
138+
```diff
139+
<!-- cmdrun -0 diff a.rs b.rs -->
140+
```
141+
```diff
142+
<!-- cmdrun -1 diff a.rs b.rs -->
143+
```
144+
````
145+
gets rendered as
146+
````markdown
147+
hello world
148+
```diff
149+
**cmdrun error**: 'diff a.rs b.rs' returned exit code 1 instead of 0.
150+
```
151+
```diff
152+
2c2
153+
< println!("I'm from `a.rs`");
154+
---
155+
> println!("I'm from `b.rs`");
156+
```
157+
````
158+
The available flags for specifying the exit code are
159+
- `-N` where `N` is the integer exit code that the command should return
160+
- `--strict` requires the command to return 0
161+
- `--expect-return-code N` requires the command to return code `N`
162+
130163
131164
## Contributors
132165

src/cmdrun.rs

Lines changed: 108 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -157,22 +157,122 @@ impl CmdRun {
157157
}
158158
}
159159

160+
fn cmdrun_error_message(message: &str, command: &str) -> String {
161+
format!("**cmdrun error**: {} in 'cmdrun {}'", message, command)
162+
}
163+
160164
// This method is public for unit tests
161165
pub fn run_cmdrun(command: String, working_dir: &str, inline: bool) -> Result<String> {
166+
// unfortunately, we need to manually parse the command string for cmdrun's
167+
// exit status checking flags.
168+
// Some experimentation using clap was done; however, splitting and then re-escaping
169+
// the shellwords was found to be a large barrier to using this other tool.
170+
let (command, correct_exit_code): (String, Option<i32>) =
171+
if let Some(first_word) = command.split_whitespace().next() {
172+
if first_word.starts_with('-') {
173+
if first_word.starts_with("--") {
174+
// double-tick long form
175+
match first_word {
176+
"--strict" => (
177+
command
178+
.split_whitespace()
179+
.skip(1)
180+
.collect::<Vec<&str>>()
181+
.join(" "),
182+
Some(0),
183+
),
184+
"--expect-return-code" => {
185+
if let Some(second_word) = command.split_whitespace().nth(1) {
186+
match second_word.parse::<i32>() {
187+
Ok(return_code) => (
188+
command
189+
.split_whitespace()
190+
.skip(2)
191+
.collect::<Vec<&str>>()
192+
.join(" "),
193+
Some(return_code),
194+
),
195+
Err(_) => {
196+
return Ok(Self::cmdrun_error_message(
197+
"No return code after '--expect-return-code'",
198+
&command,
199+
));
200+
}
201+
}
202+
} else {
203+
// no second word after return code, print error
204+
return Ok(Self::cmdrun_error_message(
205+
"No return code after '--expect-return-code'",
206+
&command,
207+
));
208+
}
209+
}
210+
some_other_word => {
211+
// unrecognized flag, print error
212+
return Ok(Self::cmdrun_error_message(
213+
&format!("Unrecognized cmdrun flag {}", some_other_word),
214+
&command,
215+
));
216+
}
217+
}
218+
} else {
219+
// single-tick short form
220+
let (_, exit_code) = first_word.rsplit_once('-').unwrap_or(("", "0"));
221+
match exit_code.parse::<i32>() {
222+
Ok(return_code) => (
223+
command
224+
.split_whitespace()
225+
.skip(1)
226+
.collect::<Vec<&str>>()
227+
.join(" "),
228+
Some(return_code),
229+
),
230+
Err(_) => {
231+
return Ok(Self::cmdrun_error_message(
232+
&format!(
233+
"Unable to interpret short-form exit code {} as a number",
234+
first_word
235+
),
236+
&command,
237+
));
238+
}
239+
}
240+
}
241+
} else {
242+
(command, None)
243+
}
244+
} else {
245+
(command, None)
246+
};
247+
162248
let output = Command::new(LAUNCH_SHELL_COMMAND)
163249
.args([LAUNCH_SHELL_FLAG, &command])
164250
.current_dir(working_dir)
165251
.output()
166252
.with_context(|| "Fail to run shell")?;
167253

168254
let stdout = Self::format_whitespace(String::from_utf8_lossy(&output.stdout), inline);
169-
170-
// let stderr = String::from_utf8_lossy(&output.stderr).to_string();
171-
172-
// eprintln!("command: {}", command);
173-
// eprintln!("stdout: {:?}", stdout);
174-
// eprintln!("stderr: {:?}", stderr);
175-
176-
Ok(stdout)
255+
match (output.status.code(), correct_exit_code) {
256+
(None, _) => Ok(Self::cmdrun_error_message(
257+
"Command was ended before completing",
258+
&command,
259+
)),
260+
(Some(code), Some(correct_code)) => {
261+
if code != correct_code {
262+
Ok(format!(
263+
"**cmdrun error**: '{command}' returned exit code {code} instead of {correct_code}.\n{0}\n{1}",
264+
String::from_utf8_lossy(&output.stdout),
265+
String::from_utf8_lossy(&output.stderr)))
266+
} else {
267+
Ok(stdout)
268+
}
269+
}
270+
(Some(_code), None) => {
271+
// no correct code specified, program exited with some code _code
272+
// could put default check requiring code to be zero here but
273+
// that would break current behavior
274+
Ok(stdout)
275+
}
276+
}
177277
}
178278
}

tests/command.rs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,3 +90,63 @@ cfg_if! {
9090

9191
}
9292
}
93+
94+
// the inline flag only affects the output and here I'm just checking exit codes
95+
// so I only test without the inline flag
96+
// I am also not testing more complicated input/output strings and so it is
97+
// the same between Windoze and Unix
98+
add_test!(pass_without_exit_code_spec, "exit 1", "", false);
99+
add_test!(short_match_fail_exit_code, "-1 exit 1", "", false);
100+
add_test!(short_match_pass_exit_code, "-0 exit 0", "", false);
101+
add_test!(
102+
short_exit_code_mismatch,
103+
"-0 exit 1",
104+
"**cmdrun error**: 'exit 1' returned exit code 1 instead of 0.\n\n",
105+
false
106+
);
107+
add_test!(
108+
long_match_fail_exit_code,
109+
"--expect-return-code 1 exit 1",
110+
"",
111+
false
112+
);
113+
add_test!(
114+
long_match_pass_exit_code1,
115+
"--expect-return-code 0 exit 0",
116+
"",
117+
false
118+
);
119+
add_test!(long_match_pass_exit_code2, "--strict exit 0", "", false);
120+
add_test!(
121+
long_exit_code_mismatch1,
122+
"--expect-return-code 0 exit 1",
123+
"**cmdrun error**: 'exit 1' returned exit code 1 instead of 0.\n\n",
124+
false
125+
);
126+
add_test!(
127+
long_exit_code_mismatch2,
128+
"--strict exit 1",
129+
"**cmdrun error**: 'exit 1' returned exit code 1 instead of 0.\n\n",
130+
false
131+
);
132+
add_test!(
133+
not_a_cmdrun_flag,
134+
"--flag-dne echo hello world",
135+
"**cmdrun error**: Unrecognized cmdrun flag --flag-dne in 'cmdrun --flag-dne echo hello world'",
136+
false
137+
);
138+
add_test!(
139+
shortform_typo,
140+
"--0 echo hello world",
141+
"**cmdrun error**: Unrecognized cmdrun flag --0 in 'cmdrun --0 echo hello world'",
142+
false
143+
);
144+
add_test!(missing_arg_no_cmd, "--expect-return-code",
145+
"**cmdrun error**: No return code after '--expect-return-code' in 'cmdrun --expect-return-code'",
146+
false);
147+
add_test!(missing_arg_no_code, "--expect-return-code echo hello world",
148+
"**cmdrun error**: No return code after '--expect-return-code' in 'cmdrun --expect-return-code echo hello world'",
149+
false);
150+
add_test!(bad_short_form_exit_code, "-NaN echo hello world",
151+
"**cmdrun error**: Unable to interpret short-form exit code -NaN as a number in 'cmdrun -NaN echo hello world'",
152+
false);

tests/regression.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ fn check_all_regressions_dirs() {
7373
entries,
7474
vec![
7575
"bash_call",
76+
"err_messages",
7677
"inline_call",
7778
"py_factorial",
7879
"py_fibonacci",
@@ -90,3 +91,4 @@ add_dir!(py_factorial);
9091
add_dir!(py_fibonacci);
9192
add_dir!(rust_call);
9293
add_dir!(shell);
94+
add_dir!(err_messages);
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Error Messages
2+
3+
<!-- cmdrun --0 echo simple typo -->
4+
5+
<!-- cmdrun eco simple typo that will inject empty line -->
6+
7+
<!-- cmdrun -0 eco simple typo that will now inject error message -->
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
input.md
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Error Messages
2+
3+
**cmdrun error**: Unrecognized cmdrun flag --0 in 'cmdrun --0 echo simple typo '
4+
5+
**cmdrun error**: 'eco simple typo that will now inject error message' returned exit code 127 instead of 0.
6+
7+
sh: 1: eco: not found
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
output.md

0 commit comments

Comments
 (0)