diff --git a/src/cli.rs b/src/cli.rs index a60260e87..b4f321fee 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -621,6 +621,14 @@ pub struct Opts { #[arg(long, aliases(&["mount", "xdev"]), hide_short_help = true, long_help)] pub one_file_system: bool, + /// By default we output matched files/dirs raw. When the user specifies + /// --quote we output the files wrapped in quotes per the rules laid out + /// in coreutils: https://www.gnu.org/software/coreutils/quotes.html + /// This should mimic the `ls -lsa` output style + #[cfg(any(unix, windows))] + #[arg(long, aliases(&["quote"]), hide_short_help = true, long_help)] + pub use_quoting: bool, + #[cfg(feature = "completions")] #[arg(long, hide = true, exclusive = true)] gen_completions: Option>, diff --git a/src/config.rs b/src/config.rs index 75b4c2bca..8b5907652 100644 --- a/src/config.rs +++ b/src/config.rs @@ -122,6 +122,9 @@ pub struct Config { /// Whether or not to strip the './' prefix for search results pub strip_cwd_prefix: bool, + + /// Whether to use quoting on the output file names + pub use_quoting: bool, } impl Config { diff --git a/src/exec/foo'bar'baz'exit.txt b/src/exec/foo'bar'baz'exit.txt new file mode 100644 index 000000000..e69de29bb diff --git a/src/exec/foo'bar'baz_exit.txt b/src/exec/foo'bar'baz_exit.txt new file mode 100644 index 000000000..e69de29bb diff --git a/src/main.rs b/src/main.rs index 8c39a1e63..32fc3196e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -249,6 +249,7 @@ fn construct_config(mut opts: Opts, pattern_regexps: &[String]) -> Result( Ok(()) } +// Trying to copy: https://www.gnu.org/software/coreutils/quotes.html +fn path_needs_quoting(path: &str) -> i8 { + // If it contains any special chars we return single quote + if path.contains(" ") || path.contains("$") || path.contains("\"") { + return 1; + // If it ONLY contains a ' we return double quote + } else if path.contains("'") { + return 2; + } + + return 0; +} + +// Quote a path with coreutils style quoting to make copy/paste +// more friendly for shells +fn quote_path(path_str: &str) -> String { + let quote_type = path_needs_quoting(path_str); + let mut tmp_str:String = path_str.into(); + + // Quote with single quotes + if quote_type == 1 { + // Escape single quotes in path + tmp_str = str::replace(&tmp_str, "'", "'\\''"); + + format!("'{}'", tmp_str) + // Quote with double quotes + } else if quote_type == 2 { + // Escape double quotes in path + tmp_str = str::replace(&tmp_str, "\"", "\\\""); + + format!("\"{}\"", tmp_str) + // No quoting required + } else { + path_str.to_string() + } +} + // TODO: this function is performance critical and can probably be optimized fn print_entry_colorized( stdout: &mut W, @@ -62,9 +99,22 @@ fn print_entry_colorized( ls_colors: &LsColors, ) -> io::Result<()> { // Split the path between the parent and the last component - let mut offset = 0; - let path = entry.stripped_path(config); - let path_str = path.to_string_lossy(); + let mut offset = 0; + let path = entry.stripped_path(config); + let mut path_str = path.to_string_lossy(); + let mut needs_quoting = false; + + // Wrap the path in quotes + if config.use_quoting { + let tmp_str = quote_path(&path_str); + + // If the quoted string is new, then we flag that to tweak the offset + // so the colors line up + if tmp_str != path_str { + path_str = tmp_str.into(); + needs_quoting = true; + } + } if let Some(parent) = path.parent() { offset = parent.to_string_lossy().len(); @@ -78,6 +128,11 @@ fn print_entry_colorized( } if offset > 0 { + + if needs_quoting { + offset += 2; + } + let mut parent_str = Cow::from(&path_str[..offset]); if let Some(ref separator) = config.path_separator { *parent_str.to_mut() = replace_path_separator(&parent_str, separator); diff --git a/tests/tests.rs b/tests/tests.rs index 5330f40aa..b1e8f83e5 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -76,6 +76,14 @@ fn test_simple() { ); } +static AND_QUOTE_FILES: &[&str] = &[ + "one'two.quo", + "one two.quo", + "one\"two.quo", + "one$two.quo", + "one'two$.quo", +]; + static AND_EXTRA_FILES: &[&str] = &[ "a.foo", "one/b.foo", @@ -2527,6 +2535,20 @@ fn test_strip_cwd_prefix() { ); } +#[test] +fn test_quoting() { + let te = TestEnv::new(DEFAULT_DIRS, AND_QUOTE_FILES); + + te.assert_output( + &["--quote", ".quo"], + "'one two.quo' + \"one'two.quo\" + \"one'two\\$.quo\" + 'one\"two.quo' + 'one$two.quo'", + ); +} + /// When fd is ran from a non-existent working directory, but an existent /// directory is passed in the arguments, it should still run fine #[test]