Skip to content

Commit 47abf41

Browse files
committed
utils: Add PathQuotedDisplay
There's probably an equivalent of this somewhere in a crate, but basically dealing with `&Path` and printing it is annoying because we always end up with quotes around a path, even if it's UTF-8 without any spaces. This takes a Path and displays it in a way that will be parsable by a shell, and takes care not to emit quotes in the simple case where a path has no shell metacharacters, just `/`, `.` and alphanumerics. Signed-off-by: Colin Walters <[email protected]>
1 parent 82c5ec6 commit 47abf41

File tree

4 files changed

+68
-0
lines changed

4 files changed

+68
-0
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

utils/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ anyhow = { workspace = true }
1111
rustix = { workspace = true }
1212
serde = { workspace = true, features = ["derive"] }
1313
serde_json = { workspace = true }
14+
shlex = "1.3"
1415
tempfile = { workspace = true }
1516
tracing = { workspace = true }
1617
tokio = { workspace = true, features = ["process", "rt", "macros"] }

utils/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
//!
55
mod command;
66
pub use command::*;
7+
mod path;
8+
pub use path::*;
79
mod iterators;
810
pub use iterators::*;
911
mod tracing_util;

utils/src/path.rs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
use std::fmt::Display;
2+
use std::os::unix::ffi::OsStrExt;
3+
use std::path::Path;
4+
5+
/// Helper to format a path.
6+
#[derive(Debug)]
7+
pub struct PathQuotedDisplay<'a> {
8+
path: &'a Path,
9+
}
10+
11+
impl<'a> Display for PathQuotedDisplay<'a> {
12+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
13+
if let Some(s) = self.path.to_str() {
14+
if s.chars()
15+
.all(|c| matches!(c, '/' | '.') || c.is_alphanumeric())
16+
{
17+
return f.write_str(s);
18+
}
19+
}
20+
if let Ok(r) = shlex::bytes::try_quote(self.path.as_os_str().as_bytes()) {
21+
if let Ok(s) = std::str::from_utf8(&r) {
22+
return f.write_str(s);
23+
}
24+
}
25+
// Should not happen really
26+
return Err(std::fmt::Error);
27+
}
28+
}
29+
30+
impl<'a> PathQuotedDisplay<'a> {
31+
/// Given a path, quote it in a way that it would be parsed by a default
32+
/// POSIX shell. If the path is UTF-8 with no spaces or shell meta-characters,
33+
/// it will be exactly the same as the input.
34+
pub fn new<P: AsRef<Path>>(path: &'a P) -> PathQuotedDisplay<'a> {
35+
PathQuotedDisplay {
36+
path: path.as_ref(),
37+
}
38+
}
39+
}
40+
41+
#[cfg(test)]
42+
mod tests {
43+
use super::*;
44+
45+
#[test]
46+
fn test_unquoted() {
47+
for v in ["", "foo", "/foo/bar", "/foo/bar/../baz", "/foo9/bar10"] {
48+
assert_eq!(v, format!("{}", PathQuotedDisplay::new(&v)));
49+
}
50+
}
51+
52+
#[test]
53+
fn test_quoted() {
54+
let cases = [
55+
(" ", "' '"),
56+
("/some/path with spaces/", "'/some/path with spaces/'"),
57+
("/foo/!/bar&", "'/foo/!/bar&'"),
58+
(r#"/path/"withquotes'"#, r#""/path/\"withquotes'""#),
59+
];
60+
for (v, quoted) in cases {
61+
assert_eq!(quoted, format!("{}", PathQuotedDisplay::new(&v)));
62+
}
63+
}
64+
}

0 commit comments

Comments
 (0)