Procházet zdrojové kódy

Merge pull request #318 from eza-community/p-quote_filenames

feat: add quotations around filenames with spaces. exa pr#1165
Christina Sørensen před 2 roky
rodič
revize
51790edba0

+ 2 - 1
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'

+ 1 - 0
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']" \

+ 3 - 0
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
 

+ 6 - 1
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<i32> {
+        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))?;
             }

+ 14 - 1
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<V: Vars>(matches: &MatchedFlags<'_>, vars: &V) -> Result<Self, OptionsError> {
         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<Self, OptionsError> {
+        if matches.has(&flags::NO_QUOTES)? {
+            Ok(Self::NoQuotes)
+        } else {
+            Ok(Self::QuoteSpaces)
+        }
+    }
+}
+
 impl EmbedHyperlinks {
     fn deduce(matches: &MatchedFlags<'_>) -> Result<Self, OptionsError> {
         let flagged = matches.has(&flags::HYPERLINK)?;

+ 2 - 2
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,

+ 1 - 0
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
 

+ 29 - 22
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<ANSIString<'_>>, 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<ANSIString<'_>>,
+    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);
     }
 }

+ 20 - 0
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 {