diff --git a/completions/fish/eza.fish b/completions/fish/eza.fish index cc0b37acd..577569cff 100644 --- a/completions/fish/eza.fish +++ b/completions/fish/eza.fish @@ -20,6 +20,7 @@ complete -c eza -l color-scale \ -l colour-scale -d "Highlight levels of file sizes distinctly" complete -c eza -l icons -d "Display icons" complete -c eza -l no-icons -d "Don't display icons" +complete -c eza -l no-quotes -d "Don't quote file names with spaces" complete -c eza -l hyperlink -d "Display entries as hyperlinks" # Filtering and sorting options @@ -28,7 +29,7 @@ complete -c eza -l git-ignore -d "Ignore files mentioned in '.gitignore'" complete -c eza -s a -l all -d "Show hidden and 'dot' files. Use this twice to also show the '.' and '..' directories" complete -c eza -s d -l list-dirs -d "List directories like regular files" complete -c eza -s L -l level -d "Limit the depth of recursion" -x -a "1 2 3 4 5 6 7 8 9" -complete -c eza -s w -l width -d "Limits column output of grid, 0 implies auto-width" +complete -c eza -s w -l width -d "Limits column output of grid, 0 implies auto-width" complete -c eza -s r -l reverse -d "Reverse the sort order" complete -c eza -s s -l sort -d "Which field to sort by" -x -a " accessed\t'Sort by file accessed time' diff --git a/completions/zsh/_eza b/completions/zsh/_eza index f2f6981de..26c5e1474 100644 --- a/completions/zsh/_eza +++ b/completions/zsh/_eza @@ -23,6 +23,7 @@ __eza() { --colo{,u}r-scale"[Highlight levels of file sizes distinctly]" \ --icons"[Display icons]" \ --no-icons"[Hide icons]" \ + --no-quotes"[Don't quote filenames with spaces]" \ --hyperlink"[Display entries as hyperlinks]" \ --group-directories-first"[Sort directories before other files]" \ --git-ignore"[Ignore files mentioned in '.gitignore']" \ diff --git a/man/eza.1.md b/man/eza.1.md index 5e429eca2..acfcf05bc 100644 --- a/man/eza.1.md +++ b/man/eza.1.md @@ -75,6 +75,9 @@ Valid settings are ‘`always`’, ‘`automatic`’, and ‘`never`’. `--no-icons` : Don't display icons. (Always overrides --icons) +`--no-quotes` +: Don't quote file names with spaces. + `--hyperlink` : Display entries as hyperlinks diff --git a/src/main.rs b/src/main.rs index 8f81859e1..ef7bb2c44 100644 --- a/src/main.rs +++ b/src/main.rs @@ -35,7 +35,7 @@ use crate::fs::feature::git::GitCache; use crate::fs::filter::GitIgnore; use crate::fs::{Dir, File}; use crate::options::{vars, Options, OptionsResult, Vars}; -use crate::output::{details, escape, grid, grid_details, lines, Mode, View}; +use crate::output::{details, escape, file_name, grid, grid_details, lines, Mode, View}; use crate::theme::Theme; mod fs; @@ -231,6 +231,10 @@ impl<'args> Exa<'args> { is_only_dir: bool, exit_status: i32, ) -> io::Result { + let View { + file_style: file_name::Options { quote_style, .. }, + .. + } = self.options.view; for dir in dir_files { // Put a gap between directories, or between the list of files and // the first directory. @@ -247,6 +251,7 @@ impl<'args> Exa<'args> { &mut bits, Style::default(), Style::default(), + quote_style, ); writeln!(&mut self.writer, "{}:", ANSIStrings(&bits))?; } diff --git a/src/options/file_name.rs b/src/options/file_name.rs index f4214fea3..c69fa7c77 100644 --- a/src/options/file_name.rs +++ b/src/options/file_name.rs @@ -2,17 +2,20 @@ use crate::options::parser::MatchedFlags; use crate::options::vars::{self, Vars}; use crate::options::{flags, NumberSource, OptionsError}; -use crate::output::file_name::{Classify, EmbedHyperlinks, Options, ShowIcons}; +use crate::output::file_name::{Classify, EmbedHyperlinks, Options, QuoteStyle, ShowIcons}; impl Options { pub fn deduce(matches: &MatchedFlags<'_>, vars: &V) -> Result { let classify = Classify::deduce(matches)?; let show_icons = ShowIcons::deduce(matches, vars)?; + + let quote_style = QuoteStyle::deduce(matches)?; let embed_hyperlinks = EmbedHyperlinks::deduce(matches)?; Ok(Self { classify, show_icons, + quote_style, embed_hyperlinks, }) } @@ -54,6 +57,16 @@ impl ShowIcons { } } +impl QuoteStyle { + pub fn deduce(matches: &MatchedFlags<'_>) -> Result { + if matches.has(&flags::NO_QUOTES)? { + Ok(Self::NoQuotes) + } else { + Ok(Self::QuoteSpaces) + } + } +} + impl EmbedHyperlinks { fn deduce(matches: &MatchedFlags<'_>) -> Result { let flagged = matches.has(&flags::HYPERLINK)?; diff --git a/src/options/flags.rs b/src/options/flags.rs index 74f18098d..175a309ed 100644 --- a/src/options/flags.rs +++ b/src/options/flags.rs @@ -14,6 +14,7 @@ pub static TREE: Arg = Arg { short: Some(b'T'), long: "tree", take pub static CLASSIFY: Arg = Arg { short: Some(b'F'), long: "classify", takes_value: TakesValue::Forbidden }; pub static DEREF_LINKS: Arg = Arg { short: Some(b'X'), long: "dereference", takes_value: TakesValue::Forbidden }; pub static WIDTH: Arg = Arg { short: Some(b'w'), long: "width", takes_value: TakesValue::Necessary(None) }; +pub static NO_QUOTES:Arg = Arg { short: None, long: "no-quotes",takes_value: TakesValue::Forbidden }; pub static COLOR: Arg = Arg { short: None, long: "color", takes_value: TakesValue::Necessary(Some(COLOURS)) }; pub static COLOUR: Arg = Arg { short: None, long: "colour", takes_value: TakesValue::Necessary(Some(COLOURS)) }; @@ -75,12 +76,11 @@ pub static EXTENDED: Arg = Arg { short: Some(b'@'), long: "extended", pub static OCTAL: Arg = Arg { short: Some(b'o'), long: "octal-permissions", takes_value: TakesValue::Forbidden }; pub static SECURITY_CONTEXT: Arg = Arg { short: Some(b'Z'), long: "context", takes_value: TakesValue::Forbidden }; - pub static ALL_ARGS: Args = Args(&[ &VERSION, &HELP, &ONE_LINE, &LONG, &GRID, &ACROSS, &RECURSE, &TREE, &CLASSIFY, &DEREF_LINKS, - &COLOR, &COLOUR, &COLOR_SCALE, &COLOUR_SCALE, &WIDTH, + &COLOR, &COLOUR, &COLOR_SCALE, &COLOUR_SCALE, &WIDTH, &NO_QUOTES, &ALL, &ALMOST_ALL, &LIST_DIRS, &LEVEL, &REVERSE, &SORT, &DIRS_FIRST, &IGNORE_GLOB, &GIT_IGNORE, &ONLY_DIRS, &ONLY_FILES, diff --git a/src/options/help.rs b/src/options/help.rs index a511db067..9344737a4 100644 --- a/src/options/help.rs +++ b/src/options/help.rs @@ -23,6 +23,7 @@ DISPLAY OPTIONS --colo[u]r-scale highlight levels of file sizes distinctly --icons display icons --no-icons don't display icons (always overrides --icons) + --no-quotes don't quote file names with spaces --hyperlink display entries as hyperlinks -w, --width COLS set screen width in columns diff --git a/src/output/escape.rs b/src/output/escape.rs index 5a15264bd..19b0c1aff 100644 --- a/src/output/escape.rs +++ b/src/output/escape.rs @@ -1,31 +1,38 @@ +use super::file_name::QuoteStyle; use ansiterm::{ANSIString, Style}; -pub fn escape(string: String, bits: &mut Vec>, good: Style, bad: Style) { - // if the string has no control character - if string.chars().all(|c| !c.is_control()) { - bits.push(good.paint(string)); - return; - } +pub fn escape( + string: String, + bits: &mut Vec>, + good: Style, + bad: Style, + quote_style: QuoteStyle, +) { + let needs_quotes = string.contains(' ') || string.contains('\''); + let quote_bit = good.paint(if string.contains('\'') { "\"" } else { "\'" }); - // the lengthier string of non control character can’t be bigger than the whole string - let mut regular_char_buff = String::with_capacity(string.len()); - for c in string.chars() { - // The `escape_default` method on `char` is *almost* what we want here, but - // it still escapes non-ASCII UTF-8 characters, which are still printable. + if string + .chars() + .all(|c| c >= 0x20 as char && c != 0x7f as char) + { + bits.push(good.paint(string)); + } else { + for c in string.chars() { + // The `escape_default` method on `char` is *almost* what we want here, but + // it still escapes non-ASCII UTF-8 characters, which are still printable. - if c.is_control() { - if !regular_char_buff.is_empty() { - bits.push(good.paint(std::mem::take(&mut regular_char_buff))); + // TODO: This allocates way too much, + // hence the `all` check above. + if c >= 0x20 as char && c != 0x7f as char { + bits.push(good.paint(c.to_string())); + } else { + bits.push(bad.paint(c.escape_default().to_string())); } - regular_char_buff.extend(c.escape_default()); - // biased towards regular characters, we push control characters immediately - bits.push(bad.paint(std::mem::take(&mut regular_char_buff))); - } else { - regular_char_buff.push(c); } } - // if last character was not a control character, the buffer is not empty! - if !regular_char_buff.is_empty() { - bits.push(good.paint(std::mem::take(&mut regular_char_buff))); + + if quote_style != QuoteStyle::NoQuotes && needs_quotes { + bits.insert(0, quote_bit.clone()); + bits.push(quote_bit); } } diff --git a/src/output/file_name.rs b/src/output/file_name.rs index 91c7083a4..b5ffe4615 100644 --- a/src/output/file_name.rs +++ b/src/output/file_name.rs @@ -19,6 +19,9 @@ pub struct Options { /// Whether to prepend icon characters before file names. pub show_icons: ShowIcons, + /// How to display file names with spaces (with or without quotes). + pub quote_style: QuoteStyle, + /// Whether to make file names hyperlinks. pub embed_hyperlinks: EmbedHyperlinks, } @@ -108,6 +111,17 @@ pub enum EmbedHyperlinks { On, } +/// Whether or not to wrap file names with spaces in quotes. +#[derive(PartialEq, Debug, Copy, Clone)] +pub enum QuoteStyle { + /// Don't ever quote file names. + NoQuotes, + + /// Use single quotes for file names that contain spaces and no single quotes + /// Use double quotes for file names that contain single quotes. + QuoteSpaces, +} + /// A **file name** holds all the information necessary to display the name /// of the given file. This is used in all of the views. pub struct FileName<'a, 'dir, C> { @@ -208,6 +222,9 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> { let target_options = Options { classify: Classify::JustFilenames, show_icons: ShowIcons::Off, + + quote_style: QuoteStyle::QuoteSpaces, + embed_hyperlinks: EmbedHyperlinks::Off, }; @@ -243,6 +260,7 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> { &mut bits, self.colours.broken_filename(), self.colours.broken_control_char(), + self.options.quote_style, ); } @@ -287,6 +305,7 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> { bits, self.colours.symlink_path(), self.colours.control_char(), + self.options.quote_style, ); bits.push( self.colours @@ -373,6 +392,7 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> { &mut bits, file_style, self.colours.control_char(), + self.options.quote_style, ); if display_hyperlink {