Skip to content

Commit 1c227ab

Browse files
committed
Print executed commands if run in verbose mode
It is currently difficult to debug what went wrong in cases such as OS exec errors, because verbose output prints the file but does not say how it is run. Update spawning to print executed commands with loquacious and above verbosity. The result when run with `-v` is something like the following: + cd "/home/user/workspace" && "/run/user/1000/just/just-kynaKD/configure" The `+` syntax is meant to match shell scripts run with `-x`. This gets coloring to differentiate it. Environment variables are not printed unless `-vv` is set: + cd "/home/user/workspace" && RUST_BACKTRACE="1" CARGO_HOME="/cargo-home" "/run/user/1000/just/just-r2rLtg/configure"
1 parent c594c8e commit 1c227ab

File tree

9 files changed

+144
-20
lines changed

9 files changed

+144
-20
lines changed

src/command_ext.rs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@ pub(crate) trait CommandExt {
1111

1212
fn export_scope(&mut self, settings: &Settings, scope: &Scope, unexports: &HashSet<String>);
1313

14-
fn output_guard(self) -> (io::Result<process::Output>, Option<Signal>);
14+
fn output_guard(self, config: &Config) -> (io::Result<process::Output>, Option<Signal>);
1515

16-
fn output_guard_stdout(self) -> Result<String, OutputError>;
16+
fn output_guard_stdout(self, config: &Config) -> Result<String, OutputError>;
1717

18-
fn status_guard(self) -> (io::Result<ExitStatus>, Option<Signal>);
18+
fn status_guard(self, config: &Config) -> (io::Result<ExitStatus>, Option<Signal>);
1919
}
2020

2121
impl CommandExt for Command {
@@ -53,12 +53,12 @@ impl CommandExt for Command {
5353
}
5454
}
5555

56-
fn output_guard(self) -> (io::Result<process::Output>, Option<Signal>) {
57-
SignalHandler::spawn(self, process::Child::wait_with_output)
56+
fn output_guard(self, config: &Config) -> (io::Result<process::Output>, Option<Signal>) {
57+
SignalHandler::spawn(self, config, process::Child::wait_with_output)
5858
}
5959

60-
fn output_guard_stdout(self) -> Result<String, OutputError> {
61-
let (result, caught) = self.output_guard();
60+
fn output_guard_stdout(self, config: &Config) -> Result<String, OutputError> {
61+
let (result, caught) = self.output_guard(config);
6262

6363
let output = result.map_err(OutputError::Io)?;
6464

@@ -79,7 +79,7 @@ impl CommandExt for Command {
7979
)
8080
}
8181

82-
fn status_guard(self) -> (io::Result<ExitStatus>, Option<Signal>) {
83-
SignalHandler::spawn(self, |mut child| child.wait())
82+
fn status_guard(self, config: &Config) -> (io::Result<ExitStatus>, Option<Signal>) {
83+
SignalHandler::spawn(self, config, |mut child| child.wait())
8484
}
8585
}

src/evaluator.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,7 @@ impl<'src, 'run> Evaluator<'src, 'run> {
291291
})
292292
.stdout(Stdio::piped());
293293

294-
cmd.output_guard_stdout()
294+
cmd.output_guard_stdout(self.context.config)
295295
}
296296

297297
pub(crate) fn evaluate_line(

src/justfile.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ impl<'src> Justfile<'src> {
158158

159159
command.export(&self.settings, &dotenv, &scope, &self.unexports);
160160

161-
let (result, caught) = command.status_guard();
161+
let (result, caught) = command.status_guard(config);
162162

163163
let status = result.map_err(|io_error| Error::CommandInvoke {
164164
binary: binary.clone(),

src/platform/windows.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ impl PlatformInterface for Platform {
2525
.stdout(Stdio::piped())
2626
.stderr(Stdio::piped());
2727

28-
Cow::Owned(cygpath.output_guard_stdout()?)
28+
Cow::Owned(cygpath.output_guard_stdout(config)?)
2929
} else {
3030
// …otherwise use it as-is.
3131
Cow::Borrowed(shebang.interpreter)
@@ -69,7 +69,7 @@ impl PlatformInterface for Platform {
6969
.stdout(Stdio::piped())
7070
.stderr(Stdio::piped());
7171

72-
match cygpath.output_guard_stdout() {
72+
match cygpath.output_guard_stdout(config) {
7373
Ok(shell_path) => Ok(shell_path),
7474
Err(_) => path
7575
.to_str()

src/recipe.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -321,7 +321,7 @@ impl<'src, D> Recipe<'src, D> {
321321
&context.module.unexports,
322322
);
323323

324-
let (result, caught) = cmd.status_guard();
324+
let (result, caught) = cmd.status_guard(context.config);
325325

326326
match result {
327327
Ok(exit_status) => {
@@ -453,7 +453,7 @@ impl<'src, D> Recipe<'src, D> {
453453
);
454454

455455
// run it!
456-
let (result, caught) = command.status_guard();
456+
let (result, caught) = command.status_guard(context.config);
457457

458458
match result {
459459
Ok(exit_status) => exit_status.code().map_or_else(

src/signal_handler.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,10 +105,30 @@ impl SignalHandler {
105105

106106
pub(crate) fn spawn<T>(
107107
mut command: Command,
108+
config: &Config,
108109
f: impl Fn(process::Child) -> io::Result<T>,
109110
) -> (io::Result<T>, Option<Signal>) {
110111
let mut instance = Self::instance();
111112

113+
let color = config.color.context().stderr();
114+
let pfx = color.prefix();
115+
let sfx = color.suffix();
116+
117+
// Print an xtrace of run commands.
118+
if config.verbosity.grandiloquent() {
119+
// At the highest verbosity level, print the exact command as-is.
120+
eprintln!("{pfx}+ {command:?}{sfx}");
121+
} else if config.verbosity.loquacious() {
122+
// For the second highest verbosity level, reconstruct the command but don't include
123+
// environment (can be quite noisy with many `export`ed variables).
124+
let mut dbg_cmd = Command::new(command.get_program());
125+
dbg_cmd.args(command.get_args());
126+
if let Some(cwd) = command.get_current_dir() {
127+
dbg_cmd.current_dir(cwd);
128+
}
129+
eprintln!("{pfx}+ {dbg_cmd:?}{sfx}");
130+
}
131+
112132
let child = match command.spawn() {
113133
Err(err) => return (Err(err), None),
114134
Ok(child) => child,

tests/fallback.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ fn fallback_from_subdir_verbose_message() {
6161
Trying ../justfile
6262
===> Running recipe `bar`...
6363
echo bar
64+
+ COMMAND_XTRACE
6465
",
6566
))
6667
.stdout("bar\n")

tests/misc.rs

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,55 @@ fn verbose() {
232232
.arg("--verbose")
233233
.justfile("default:\n @echo hello")
234234
.stdout("hello\n")
235-
.stderr("===> Running recipe `default`...\necho hello\n")
235+
.stderr(
236+
"\
237+
===> Running recipe `default`...\n\
238+
echo hello\n\
239+
+ COMMAND_XTRACE\n\
240+
",
241+
)
242+
.run();
243+
}
244+
245+
#[test]
246+
fn xtrace() {
247+
Test::new()
248+
.arg("--verbose")
249+
.justfile(
250+
r#"
251+
export ENV := "hi"
252+
a:
253+
echo $ENV
254+
"#,
255+
)
256+
.normalize_xtrace(false)
257+
.stdout("hi\n")
258+
.stderr_fn(|s| {
259+
// The lowest verbosity mode shouldn't print ENV. Easier to check with a function
260+
// than with regex.
261+
if s.contains("ENV=") {
262+
return Err("shouldn't print env".into());
263+
}
264+
Ok(())
265+
})
266+
.stderr_regex(r".*\+ cd.*&&.*echo \$ENV.*")
267+
.run();
268+
}
269+
270+
#[test]
271+
fn xtrace_very_verbose() {
272+
Test::new()
273+
.arg("-vv")
274+
.justfile(
275+
r#"
276+
export ENV := "hi"
277+
a:
278+
echo $ENV
279+
"#,
280+
)
281+
.normalize_xtrace(false)
282+
.stdout("hi\n")
283+
.stderr_regex(r".*\+ cd.*&&.*ENV=.*echo \$ENV.*")
236284
.run();
237285
}
238286

@@ -2037,7 +2085,13 @@ a:
20372085
",
20382086
)
20392087
.stdout("hi\n")
2040-
.stderr("\u{1b}[1;36m===> Running recipe `a`...\u{1b}[0m\n\u{1b}[1mecho hi\u{1b}[0m\n")
2088+
.stderr(
2089+
"\
2090+
\u{1b}[1;36m===> Running recipe `a`...\u{1b}[0m\n\
2091+
\u{1b}[1mecho hi\u{1b}[0m\n\
2092+
\u{1b}[1;34m+ COMMAND_XTRACE\u{1b}[0m\n\
2093+
",
2094+
)
20412095
.run();
20422096
}
20432097

@@ -2057,7 +2111,13 @@ a:
20572111
",
20582112
)
20592113
.stdout("hi\n")
2060-
.stderr("\u{1b}[1;36m===> Running recipe `a`...\u{1b}[0m\necho hi\n")
2114+
.stderr(
2115+
"\
2116+
\u{1b}[1;36m===> Running recipe `a`...\u{1b}[0m\n\
2117+
echo hi\n\
2118+
\u{1b}[1;34m+ COMMAND_XTRACE\u{1b}[0m\n\
2119+
",
2120+
)
20612121
.run();
20622122
}
20632123

tests/test.rs

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use {
22
super::*,
3+
once_cell::sync::Lazy,
34
pretty_assertions::{assert_eq, StrComparison},
45
};
56

@@ -16,10 +17,12 @@ pub(crate) struct Test {
1617
pub(crate) env: BTreeMap<String, String>,
1718
pub(crate) expected_files: BTreeMap<PathBuf, Vec<u8>>,
1819
pub(crate) justfile: Option<String>,
20+
pub(crate) normalize_xtrace: bool,
1921
pub(crate) response: Option<Response>,
2022
pub(crate) shell: bool,
2123
pub(crate) status: i32,
2224
pub(crate) stderr: String,
25+
pub(crate) stderr_fn: Option<Box<dyn Fn(&str) -> Result<(), String>>>,
2326
pub(crate) stderr_regex: Option<Regex>,
2427
pub(crate) stdin: String,
2528
pub(crate) stdout: String,
@@ -46,12 +49,14 @@ impl Test {
4649
status: EXIT_SUCCESS,
4750
stderr: String::new(),
4851
stderr_regex: None,
52+
stderr_fn: None,
4953
stdin: String::new(),
5054
stdout: String::new(),
5155
stdout_regex: None,
5256
tempdir,
5357
test_round_trip: true,
5458
unindent_stdout: true,
59+
normalize_xtrace: true,
5560
}
5661
}
5762

@@ -132,6 +137,11 @@ impl Test {
132137
self
133138
}
134139

140+
pub(crate) fn stderr_fn(mut self, f: impl Fn(&str) -> Result<(), String> + 'static) -> Self {
141+
self.stderr_fn = Some(Box::from(f));
142+
self
143+
}
144+
135145
pub(crate) fn stdin(mut self, stdin: impl Into<String>) -> Self {
136146
self.stdin = stdin.into();
137147
self
@@ -164,6 +174,11 @@ impl Test {
164174
self
165175
}
166176

177+
pub(crate) fn normalize_xtrace(mut self, normalize_xtrace: bool) -> Self {
178+
self.normalize_xtrace = normalize_xtrace;
179+
self
180+
}
181+
167182
pub(crate) fn write(self, path: impl AsRef<Path>, content: impl AsRef<[u8]>) -> Self {
168183
let path = self.tempdir.path().join(path);
169184
fs::create_dir_all(path.parent().unwrap()).unwrap();
@@ -204,6 +219,18 @@ impl Test {
204219
}
205220
}
206221

222+
static XTRACE_RE: Lazy<Regex> = Lazy::new(|| {
223+
Regex::new(
224+
r#"(?mx)^ # multiline, verbose
225+
(?P<pfx>\u{1b}\[[\d;]+m)? # match the color prefix, if available
226+
\+.*bash.*? # match the command. Currently all tests that pass `--verbose` are
227+
# run on bash, so this matcher is "good enough" for now.
228+
(?P<sfx>\u{1b}\[[\d;]+m)? # match the color suffix, if available
229+
$"#,
230+
)
231+
.unwrap()
232+
});
233+
207234
impl Test {
208235
#[track_caller]
209236
pub(crate) fn run(self) -> Output {
@@ -267,7 +294,14 @@ impl Test {
267294
.expect("failed to wait for just process");
268295

269296
let output_stdout = str::from_utf8(&output.stdout).unwrap();
270-
let output_stderr = str::from_utf8(&output.stderr).unwrap();
297+
let mut output_stderr = str::from_utf8(&output.stderr).unwrap();
298+
299+
// The xtrace output can differ by working directory, shell, and flags. Normalize it.
300+
let tmp;
301+
if self.normalize_xtrace {
302+
tmp = XTRACE_RE.replace_all(output_stderr, "${pfx}+ COMMAND_XTRACE${sfx}");
303+
output_stderr = tmp.as_ref();
304+
}
271305

272306
if let Some(ref stdout_regex) = self.stdout_regex {
273307
assert!(
@@ -283,9 +317,18 @@ impl Test {
283317
);
284318
}
285319

320+
if let Some(ref stderr_fn) = self.stderr_fn {
321+
match stderr_fn(output_stderr) {
322+
Ok(()) => (),
323+
Err(e) => panic!("Stderr function mismatch: {e}\n{output_stderr:?}\n",),
324+
}
325+
}
326+
286327
if !compare("status", output.status.code(), Some(self.status))
287328
| (self.stdout_regex.is_none() && !compare_string("stdout", output_stdout, &stdout))
288-
| (self.stderr_regex.is_none() && !compare_string("stderr", output_stderr, &stderr))
329+
| (self.stderr_regex.is_none()
330+
&& self.stderr_fn.is_none()
331+
&& !compare_string("stderr", output_stderr, &stderr))
289332
{
290333
panic!("Output mismatch.");
291334
}

0 commit comments

Comments
 (0)