Skip to content

Commit 9e1a13c

Browse files
pretty print binary path when possible
Co-authored-by: Alex Huszagh <[email protected]>
1 parent 1ff907b commit 9e1a13c

File tree

5 files changed

+141
-19
lines changed

5 files changed

+141
-19
lines changed

src/docker/engine.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ use std::process::Command;
55
use crate::errors::*;
66
use crate::extensions::CommandExt;
77

8-
const DOCKER: &str = "docker";
9-
const PODMAN: &str = "podman";
8+
pub const DOCKER: &str = "docker";
9+
pub const PODMAN: &str = "podman";
1010

1111
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
1212
pub enum EngineType {

src/errors.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ pub enum CommandError {
2828
}
2929

3030
impl CommandError {
31-
/// Attach valuable information to this CommandError
31+
/// Attach valuable information to this [`CommandError`](Self)
3232
pub fn to_section_report(self) -> eyre::Report {
3333
match &self {
3434
CommandError::NonZeroExitCode { stderr, stdout, .. } => {

src/extensions.rs

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@ use std::process::{Command, ExitStatus, Output};
44

55
use crate::errors::*;
66

7+
pub const STRIPPED_BINS: &[&str] = &[crate::docker::DOCKER, crate::docker::PODMAN, "cargo"];
8+
79
pub trait CommandExt {
810
fn print_verbose(&self, verbose: bool);
911
fn status_result(
1012
&self,
13+
verbose: bool,
1114
status: ExitStatus,
1215
output: Option<&Output>,
1316
) -> Result<(), CommandError>;
@@ -19,44 +22,57 @@ pub trait CommandExt {
1922
) -> Result<ExitStatus, CommandError>;
2023
fn run_and_get_stdout(&mut self, verbose: bool) -> Result<String>;
2124
fn run_and_get_output(&mut self, verbose: bool) -> Result<std::process::Output>;
22-
fn command_pretty(&self) -> String;
25+
fn command_pretty(&self, verbose: bool, strip: impl for<'a> Fn(&'a str) -> bool) -> String;
2326
}
2427

2528
impl CommandExt for Command {
26-
fn command_pretty(&self) -> String {
29+
fn command_pretty(&self, verbose: bool, strip: impl for<'a> Fn(&'a str) -> bool) -> String {
2730
// a dummy implementor of display to avoid using unwraps
28-
struct C<'c>(&'c Command);
29-
impl std::fmt::Display for C<'_> {
31+
struct C<'c, F>(&'c Command, bool, F);
32+
impl<F> std::fmt::Display for C<'_, F>
33+
where
34+
F: for<'a> Fn(&'a str) -> bool,
35+
{
3036
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3137
let cmd = self.0;
32-
write!(f, "{}", cmd.get_program().to_string_lossy())?;
38+
write!(
39+
f,
40+
"{}",
41+
// if verbose, never strip, if not, let user choose
42+
crate::file::pretty_path(cmd.get_program(), move |c| if self.1 {
43+
false
44+
} else {
45+
self.2(c)
46+
})
47+
)?;
3348
let args = cmd.get_args();
3449
if args.len() > 1 {
35-
write!(f, " ")?;
3650
write!(
3751
f,
38-
"{}",
52+
" {}",
3953
shell_words::join(args.map(|o| o.to_string_lossy()))
4054
)?;
4155
}
4256
Ok(())
4357
}
4458
}
45-
format!("{}", C(self))
59+
format!("{}", C(self, verbose, strip))
4660
}
4761

4862
fn print_verbose(&self, verbose: bool) {
63+
// TODO: introduce verbosity levels, v = 1, strip cmd, v > 1, don't strip cmd
4964
if verbose {
5065
if let Some(cwd) = self.get_current_dir() {
51-
println!("+ {:?} {}", cwd, self.command_pretty());
66+
println!("+ {:?} {}", cwd, self.command_pretty(true, |_| false));
5267
} else {
53-
println!("+ {}", self.command_pretty());
68+
println!("+ {}", self.command_pretty(true, |_| false));
5469
}
5570
}
5671
}
5772

5873
fn status_result(
5974
&self,
75+
verbose: bool,
6076
status: ExitStatus,
6177
output: Option<&Output>,
6278
) -> Result<(), CommandError> {
@@ -65,7 +81,8 @@ impl CommandExt for Command {
6581
} else {
6682
Err(CommandError::NonZeroExitCode {
6783
status,
68-
command: self.command_pretty(),
84+
command: self
85+
.command_pretty(verbose, |ref cmd| STRIPPED_BINS.iter().any(|f| f == cmd)),
6986
stderr: output.map(|out| out.stderr.clone()).unwrap_or_default(),
7087
stdout: output.map(|out| out.stdout.clone()).unwrap_or_default(),
7188
})
@@ -75,7 +92,7 @@ impl CommandExt for Command {
7592
/// Runs the command to completion
7693
fn run(&mut self, verbose: bool, silence_stdout: bool) -> Result<(), CommandError> {
7794
let status = self.run_and_get_status(verbose, silence_stdout)?;
78-
self.status_result(status, None)
95+
self.status_result(verbose, status, None)
7996
}
8097

8198
/// Runs the command to completion
@@ -91,15 +108,16 @@ impl CommandExt for Command {
91108
self.status()
92109
.map_err(|e| CommandError::CouldNotExecute {
93110
source: Box::new(e),
94-
command: self.command_pretty(),
111+
command: self
112+
.command_pretty(verbose, |ref cmd| STRIPPED_BINS.iter().any(|f| f == cmd)),
95113
})
96114
.map_err(Into::into)
97115
}
98116

99117
/// Runs the command to completion and returns its stdout
100118
fn run_and_get_stdout(&mut self, verbose: bool) -> Result<String> {
101119
let out = self.run_and_get_output(verbose)?;
102-
self.status_result(out.status, Some(&out))
120+
self.status_result(verbose, out.status, Some(&out))
103121
.map_err(CommandError::to_section_report)?;
104122
out.stdout().map_err(Into::into)
105123
}
@@ -114,7 +132,8 @@ impl CommandExt for Command {
114132
self.output().map_err(|e| {
115133
CommandError::CouldNotExecute {
116134
source: Box::new(e),
117-
command: self.command_pretty(),
135+
command: self
136+
.command_pretty(verbose, |ref cmd| STRIPPED_BINS.iter().any(|f| f == cmd)),
118137
}
119138
.to_section_report()
120139
})

src/file.rs

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use std::borrow::Cow;
2+
use std::ffi::OsStr;
13
use std::fs::{self, File};
24
use std::io::Read;
35
use std::path::{Path, PathBuf};
@@ -36,6 +38,55 @@ fn _canonicalize(path: &Path) -> Result<PathBuf> {
3638
}
3739
}
3840

41+
/// Pretty format a file path. Also removes the path prefix from a command if wanted
42+
pub fn pretty_path(path: impl AsRef<Path>, strip: impl for<'a> Fn(&'a str) -> bool) -> String {
43+
let path = path.as_ref();
44+
// TODO: Use Path::file_prefix
45+
let file_stem = path.file_stem();
46+
let file_name = path.file_name();
47+
let path = if let (Some(file_stem), Some(file_name)) = (file_stem, file_name) {
48+
if let Some(file_name) = file_name.to_str() {
49+
if strip(file_name) {
50+
Cow::Borrowed(file_stem)
51+
} else {
52+
Cow::Borrowed(path.as_os_str())
53+
}
54+
} else {
55+
Cow::Borrowed(path.as_os_str())
56+
}
57+
} else {
58+
maybe_canonicalize(path)
59+
};
60+
61+
if let Some(path) = path.to_str() {
62+
shell_escape(path).to_string()
63+
} else {
64+
format!("{path:?}")
65+
}
66+
}
67+
68+
pub fn shell_escape(string: &str) -> Cow<'_, str> {
69+
let escape: &[char] = if cfg!(target_os = "windows") {
70+
&['%', '$', '`', '!', '"']
71+
} else {
72+
&['$', '\'', '\\', '!', '"']
73+
};
74+
75+
if string.contains(escape) {
76+
Cow::Owned(format!("{string:?}"))
77+
} else if string.contains(' ') {
78+
Cow::Owned(format!("\"{string}\""))
79+
} else {
80+
Cow::Borrowed(string)
81+
}
82+
}
83+
84+
pub fn maybe_canonicalize(path: &Path) -> Cow<'_, OsStr> {
85+
canonicalize(path)
86+
.map(|p| Cow::Owned(p.as_os_str().to_owned()))
87+
.unwrap_or_else(|_| path.as_os_str().into())
88+
}
89+
3990
pub fn write_file(path: impl AsRef<Path>, overwrite: bool) -> Result<File> {
4091
let path = path.as_ref();
4192
fs::create_dir_all(
@@ -57,3 +108,55 @@ pub fn write_file(path: impl AsRef<Path>, overwrite: bool) -> Result<File> {
57108
open.open(&path)
58109
.wrap_err(format!("couldn't write to file `{}`", path.display()))
59110
}
111+
112+
#[cfg(test)]
113+
mod tests {
114+
use super::*;
115+
116+
#[test]
117+
#[cfg(target_family = "windows")]
118+
fn pretty_path_windows() {
119+
assert_eq!(
120+
pretty_path("C:\\path\\bin\\cargo.exe", |f| f.contains("cargo")),
121+
"cargo".to_string()
122+
);
123+
assert_eq!(
124+
pretty_path("C:\\Program Files\\Docker\\bin\\docker.exe", |_| false),
125+
"\"C:\\Program Files\\Docker\\bin\\docker.exe\"".to_string()
126+
);
127+
assert_eq!(
128+
pretty_path("C:\\Program Files\\single'quote\\cargo.exe", |c| c
129+
.contains("cargo")),
130+
"cargo".to_string()
131+
);
132+
assert_eq!(
133+
pretty_path("C:\\Program Files\\single'quote\\cargo.exe", |_| false),
134+
"\"C:\\Program Files\\single'quote\\cargo.exe\"".to_string()
135+
);
136+
assert_eq!(
137+
pretty_path("C:\\Program Files\\%not_var%\\cargo.exe", |_| false),
138+
"\"C:\\\\Program Files\\\\%not_var%\\\\cargo.exe\"".to_string()
139+
);
140+
}
141+
142+
#[test]
143+
#[cfg(target_family = "unix")]
144+
fn pretty_path_linux() {
145+
assert_eq!(
146+
pretty_path("/usr/bin/cargo", |f| f.contains("cargo")),
147+
"cargo".to_string()
148+
);
149+
assert_eq!(
150+
pretty_path("/home/user/my rust/bin/cargo", |_| false),
151+
"\"/home/user/my rust/bin/cargo\"".to_string(),
152+
);
153+
assert_eq!(
154+
pretty_path("/home/user/single'quote/cargo", |c| c.contains("cargo")),
155+
"cargo".to_string()
156+
);
157+
assert_eq!(
158+
pretty_path("/home/user/single'quote/cargo", |_| false),
159+
"\"/home/user/single'quote/cargo\"".to_string()
160+
);
161+
}
162+
}

src/rustup.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ pub fn available_targets(toolchain: &str, verbose: bool) -> Result<AvailableTarg
5454
eyre::bail!("{toolchain} is a custom toolchain. To use it, you'll need to set the environment variable `CROSS_CUSTOM_TOOLCHAIN=1`")
5555
}
5656
return Err(cmd
57-
.status_result(output.status, Some(&output))
57+
.status_result(verbose, output.status, Some(&output))
5858
.unwrap_err()
5959
.to_section_report());
6060
}

0 commit comments

Comments
 (0)