Explorar o código

Merge branch 'file-name-refactoring'

Benjamin Sago %!s(int64=8) %!d(string=hai) anos
pai
achega
609bafef49

+ 10 - 0
Vagrantfile

@@ -172,6 +172,16 @@ Vagrant.configure(2) do |config|
         echo -ne "#{test_dir}/file-names/invalid-utf8-2: [\\xc3\\x28]"           | xargs -0 touch
         echo -ne "#{test_dir}/file-names/invalid-utf8-3: [\\xe2\\x82\\x28]"      | xargs -0 touch
         echo -ne "#{test_dir}/file-names/invalid-utf8-4: [\\xf0\\x28\\x8c\\x28]" | xargs -0 touch
+
+        echo -ne "#{test_dir}/file-names/new-line-dir: [\\n]"                | xargs -0 mkdir
+        echo -ne "#{test_dir}/file-names/new-line-dir: [\\n]/subfile"        | xargs -0 touch
+        echo -ne "#{test_dir}/file-names/new-line-dir: [\\n]/another: [\\n]" | xargs -0 touch
+        echo -ne "#{test_dir}/file-names/new-line-dir: [\\n]/broken"         | xargs -0 touch
+
+        mkdir "#{test_dir}/file-names/links"
+        ln -s "#{test_dir}/file-names/new-line-dir"*/* "#{test_dir}/file-names/links"
+
+        echo -ne "#{test_dir}/file-names/new-line-dir: [\\n]/broken" | xargs -0 rm
     EOF
 
 

+ 6 - 1
src/exa.rs

@@ -23,9 +23,12 @@ use std::ffi::OsStr;
 use std::io::{stderr, Write, Result as IOResult};
 use std::path::{Component, Path};
 
+use ansi_term::{ANSIStrings, Style};
+
 use fs::{Dir, File};
 use options::{Options, View};
 pub use options::Misfire;
+use output::escape;
 
 mod fs;
 mod info;
@@ -116,7 +119,9 @@ impl<'w, W: Write + 'w> Exa<'w, W> {
             }
 
             if !is_only_dir {
-                writeln!(self.writer, "{}:", dir.path.display())?;
+                let mut bits = Vec::new();
+                escape(dir.path.display().to_string(), &mut bits, Style::default(), Style::default());
+                writeln!(self.writer, "{}:", ANSIStrings(&bits))?;
             }
 
             let mut children = Vec::new();

+ 5 - 2
src/fs/file.rs

@@ -1,7 +1,5 @@
 //! Files, and methods and fields to access their metadata.
 
-use std::ascii::AsciiExt;
-use std::env::current_dir;
 use std::fs;
 use std::io::Error as IOError;
 use std::io::Result as IOResult;
@@ -14,6 +12,7 @@ use fs::fields as f;
 #[cfg(any(target_os = "macos", target_os = "linux"))]
 use std::os::unix::fs::FileTypeExt;
 
+
 /// Constant table copied from https://doc.rust-lang.org/src/std/sys/unix/ext/fs.rs.html#11-259
 /// which is currently unstable and lacks vision for stabilization,
 /// see https://github.com/rust-lang/rust/issues/27712
@@ -344,6 +343,8 @@ impl<'dir> File<'dir> {
     /// directory, so will not work if this file has just been passed in on
     /// the command line.
     pub fn git_status(&self) -> f::Git {
+        use std::env::current_dir;
+
         match self.dir {
             None    => f::Git { staged: f::GitStatus::NotModified, unstaged: f::GitStatus::NotModified },
             Some(d) => {
@@ -421,6 +422,8 @@ impl<'a> AsRef<File<'a>> for File<'a> {
 /// against a pre-compiled list of extensions which are known to only exist
 /// within ASCII, so it's alright.
 fn ext(path: &Path) -> Option<String> {
+    use std::ascii::AsciiExt;
+
     let name = match path.file_name() {
         Some(f) => f.to_string_lossy().to_string(),
         None => return None,

+ 11 - 0
src/output/cell.rs

@@ -160,10 +160,21 @@ impl TextCellContents {
         ANSIStrings(&self.0)
     }
 
+    /// Calculates the width that a cell with these contents would take up, by
+    /// counting the number of characters in each unformatted ANSI string.
     pub fn width(&self) -> DisplayWidth {
         let foo = self.0.iter().map(|anstr| anstr.chars().count()).sum();
         DisplayWidth(foo)
     }
+
+    /// Promotes these contents to a full cell containing them alongside
+    /// their calculated width.
+    pub fn promote(self) -> TextCell {
+        TextCell {
+            width: self.width(),
+            contents: self,
+        }
+    }
 }
 
 

+ 5 - 31
src/output/details.rs

@@ -99,9 +99,9 @@ use fs::feature::xattr::{Attribute, FileAttributes};
 use options::{FileFilter, RecurseOptions};
 use output::colours::Colours;
 use output::column::{Alignment, Column, Columns, SizeFormat};
-use output::cell::{TextCell, DisplayWidth};
+use output::cell::{TextCell, TextCellContents, DisplayWidth};
 use output::tree::TreeTrunk;
-use super::filename;
+use output::file_name::FileName;
 
 
 /// With the **Details** view, the output gets formatted into columns, with
@@ -307,24 +307,10 @@ impl Details {
             let mut files = Vec::new();
             let mut errors = egg.errors;
 
-            let filename = filename(&egg.file, &self.colours, true, self.classify);
-            let mut width = filename.width();
-
-            if egg.file.dir.is_none() {
-                if let Some(parent) = egg.file.path.parent() {
-                    width = width + 1 + DisplayWidth::from(parent.to_string_lossy().as_ref());
-                }
-            }
-
-            let name = TextCell {
-                contents: filename,
-                width:    width,
-            };
-
             let row = Row {
                 depth:    depth,
                 cells:    Some(egg.cells),
-                name:     name,
+                name:     FileName::new(&egg.file, &self.colours).paint(true, self.classify).promote(),
                 last:     index == num_eggs - 1,
             };
 
@@ -457,20 +443,8 @@ impl<'a, U: Users+Groups+'a> Table<'a, U> {
         self.rows.push(row);
     }
 
-    pub fn filename_cell(&self, file: File, links: bool) -> TextCell {
-        let filename = filename(&file, &self.opts.colours, links, self.opts.classify);
-        let mut width = filename.width();
-
-        if file.dir.is_none() {
-            if let Some(parent) = file.path.parent() {
-                width = width + 1 + DisplayWidth::from(parent.to_string_lossy().as_ref());
-            }
-        }
-
-        TextCell {
-            contents: filename,
-            width:    width,
-        }
+    pub fn filename(&self, file: File, links: bool) -> TextCellContents {
+        FileName::new(&file, &self.opts.colours).paint(links, self.opts.classify)
     }
 
     pub fn add_file_with_cells(&mut self, cells: Vec<TextCell>, name_cell: TextCell, depth: usize, last: bool) {

+ 25 - 0
src/output/escape.rs

@@ -0,0 +1,25 @@
+use ansi_term::{ANSIString, Style};
+
+
+pub fn escape<'a>(string: String, bits: &mut Vec<ANSIString<'a>>, good: Style, bad: Style) {
+    if string.chars().all(|c| c >= 0x20 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 >= 0x20 as char {
+                // TODO: This allocates way too much,
+                // hence the `all` check above.
+                let mut s = String::new();
+                s.push(c);
+                bits.push(good.paint(s));
+            } else {
+                let s = c.escape_default().collect::<String>();
+                bits.push(bad.paint(s));
+            }
+        }
+    }
+}

+ 151 - 0
src/output/file_name.rs

@@ -0,0 +1,151 @@
+use std::path::Path;
+
+use ansi_term::{ANSIString, Style};
+
+use fs::{File, FileTarget};
+use output::Colours;
+use output::escape;
+use output::cell::TextCellContents;
+
+
+pub struct FileName<'a, 'dir: 'a> {
+    file:    &'a File<'dir>,
+    colours: &'a Colours,
+}
+
+impl<'a, 'dir> FileName<'a, 'dir> {
+    pub fn new(file: &'a File<'dir>, colours: &'a Colours) -> FileName<'a, 'dir> {
+        FileName {
+            file: file,
+            colours: colours,
+        }
+    }
+
+    pub fn paint(&self, links: bool, classify: bool) -> TextCellContents {
+        let mut bits = Vec::new();
+
+        if self.file.dir.is_none() {
+            if let Some(parent) = self.file.path.parent() {
+                self.add_parent_bits(&mut bits, parent);
+            }
+        }
+
+        if !self.file.name.is_empty() {
+            for bit in self.coloured_file_name() {
+                bits.push(bit);
+            }
+        }
+
+        if links && self.file.is_link() {
+            match self.file.link_target() {
+                FileTarget::Ok(target) => {
+                    bits.push(Style::default().paint(" "));
+                    bits.push(self.colours.punctuation.paint("->"));
+                    bits.push(Style::default().paint(" "));
+
+                    if let Some(parent) = target.path.parent() {
+                        self.add_parent_bits(&mut bits, parent);
+                    }
+
+                    if !target.name.is_empty() {
+                        let target = FileName::new(&target, self.colours);
+                        for bit in target.coloured_file_name() {
+                            bits.push(bit);
+                        }
+                    }
+                },
+
+                FileTarget::Broken(broken_path) => {
+                    bits.push(Style::default().paint(" "));
+                    bits.push(self.colours.broken_arrow.paint("->"));
+                    bits.push(Style::default().paint(" "));
+                    escape(broken_path.display().to_string(), &mut bits, self.colours.broken_filename, self.colours.control_char.underline());
+                },
+
+                FileTarget::Err(_) => {
+                    // Do nothing -- the error gets displayed on the next line
+                }
+            }
+        }
+        else if classify {
+            if let Some(class) = self.classify_char() {
+                bits.push(Style::default().paint(class));
+            }
+        }
+
+        bits.into()
+    }
+
+    /// Adds the bits of the parent path to the given bits vector.
+    /// The path gets its characters escaped based on the colours.
+    fn add_parent_bits(&self, bits: &mut Vec<ANSIString>, parent: &Path) {
+        let coconut = parent.components().count();
+
+        if coconut == 1 && parent.has_root() {
+            bits.push(self.colours.symlink_path.paint("/"));
+        }
+        else if coconut >= 1 {
+            escape(parent.to_string_lossy().to_string(), bits, self.colours.symlink_path, self.colours.control_char);
+            bits.push(self.colours.symlink_path.paint("/"));
+        }
+    }
+
+    /// The character to be displayed after a file when classifying is on, if
+    /// the file’s type has one associated with it.
+    fn classify_char(&self) -> Option<&'static str> {
+        if self.file.is_executable_file() {
+            Some("*")
+        } else if self.file.is_directory() {
+            Some("/")
+        } else if self.file.is_pipe() {
+            Some("|")
+        } else if self.file.is_link() {
+            Some("@")
+        } else if self.file.is_socket() {
+            Some("=")
+        } else {
+            None
+        }
+    }
+
+    /// Returns at least one ANSI-highlighted string representing this file’s
+    /// name using the given set of colours.
+    ///
+    /// Ordinarily, this will be just one string: the file’s complete name,
+    /// coloured according to its file type. If the name contains control
+    /// characters such as newlines or escapes, though, we can’t just print them
+    /// to the screen directly, because then there’ll be newlines in weird places.
+    ///
+    /// So in that situation, those characters will be escaped and highlighted in
+    /// a different colour.
+    fn coloured_file_name<'unused>(&self) -> Vec<ANSIString<'unused>> {
+        let file_style = self.style();
+        let mut bits = Vec::new();
+        escape(self.file.name.clone(), &mut bits, file_style, self.colours.control_char);
+        bits
+    }
+
+    pub fn style(&self) -> Style {
+        match self.file {
+            f if f.is_directory()        => self.colours.filetypes.directory,
+            f if f.is_executable_file()  => self.colours.filetypes.executable,
+            f if f.is_link()             => self.colours.filetypes.symlink,
+            f if f.is_pipe()             => self.colours.filetypes.pipe,
+            f if f.is_char_device()
+               | f.is_block_device()     => self.colours.filetypes.device,
+            f if f.is_socket()           => self.colours.filetypes.socket,
+            f if !f.is_file()            => self.colours.filetypes.special,
+            f if f.is_immediate()        => self.colours.filetypes.immediate,
+            f if f.is_image()            => self.colours.filetypes.image,
+            f if f.is_video()            => self.colours.filetypes.video,
+            f if f.is_music()            => self.colours.filetypes.music,
+            f if f.is_lossless()         => self.colours.filetypes.lossless,
+            f if f.is_crypto()           => self.colours.filetypes.crypto,
+            f if f.is_document()         => self.colours.filetypes.document,
+            f if f.is_compressed()       => self.colours.filetypes.compressed,
+            f if f.is_temp()             => self.colours.filetypes.temp,
+            f if f.is_compiled()         => self.colours.filetypes.compiled,
+            _                            => self.colours.filetypes.normal,
+        }
+    }
+}

+ 5 - 11
src/output/grid.rs

@@ -3,9 +3,8 @@ use std::io::{Write, Result as IOResult};
 use term_grid as grid;
 
 use fs::File;
-use output::DisplayWidth;
 use output::colours::Colours;
-use super::filename;
+use output::file_name::FileName;
 
 
 #[derive(PartialEq, Debug, Copy, Clone)]
@@ -29,14 +28,8 @@ impl Grid {
         grid.reserve(files.len());
 
         for file in files.iter() {
-            let filename = filename(file, &self.colours, false, self.classify);
-
-            let mut width = filename.width();
-            if file.dir.is_none() {
-                if let Some(parent) = file.path.parent() {
-                    width = width + 1 + DisplayWidth::from(parent.to_string_lossy().as_ref());
-                }
-            }
+            let filename = FileName::new(file, &self.colours).paint(false, self.classify);
+            let width = filename.width();
 
             grid.add(grid::Cell {
                 contents:  filename.strings().to_string(),
@@ -50,7 +43,8 @@ impl Grid {
         else {
             // File names too long for a grid - drop down to just listing them!
             for file in files.iter() {
-                writeln!(w, "{}", filename(file, &self.colours, false, self.classify).strings())?;
+                let name_cell = FileName::new(file, &self.colours).paint(false, self.classify);
+                writeln!(w, "{}", name_cell.strings())?;
             }
             Ok(())
         }

+ 1 - 1
src/output/grid_details.rs

@@ -45,7 +45,7 @@ impl GridDetails {
                               .collect::<Vec<_>>();
 
             let file_names = files.into_iter()
-                                  .map(|file| first_table.filename_cell(file, false))
+                                  .map(|file| first_table.filename(file, false).promote())
                                   .collect::<Vec<_>>();
 
             (cells, file_names)

+ 3 - 2
src/output/lines.rs

@@ -4,7 +4,7 @@ use ansi_term::ANSIStrings;
 
 use fs::File;
 
-use super::filename;
+use output::file_name::FileName;
 use super::colours::Colours;
 
 
@@ -18,7 +18,8 @@ pub struct Lines {
 impl Lines {
     pub fn view<W: Write>(&self, files: Vec<File>, w: &mut W) -> IOResult<()> {
         for file in files {
-            writeln!(w, "{}", ANSIStrings(&filename(&file, &self.colours, true, self.classify)))?;
+            let name_cell = FileName::new(&file, &self.colours).paint(true, self.classify);
+            writeln!(w, "{}", ANSIStrings(&name_cell))?;
         }
         Ok(())
     }

+ 3 - 144
src/output/mod.rs

@@ -1,13 +1,10 @@
-use ansi_term::{ANSIString, Style};
-
-use fs::{File, FileTarget};
-
 pub use self::cell::{TextCell, TextCellContents, DisplayWidth};
 pub use self::colours::Colours;
 pub use self::details::Details;
 pub use self::grid_details::GridDetails;
 pub use self::grid::Grid;
 pub use self::lines::Lines;
+pub use self::escape::escape;
 
 mod grid;
 pub mod details;
@@ -17,143 +14,5 @@ pub mod column;
 mod cell;
 mod colours;
 mod tree;
-
-
-pub fn filename(file: &File, colours: &Colours, links: bool, classify: bool) -> TextCellContents {
-    let mut bits = Vec::new();
-
-    // TODO: This long function could do with some splitting up.
-
-    if file.dir.is_none() {
-        if let Some(parent) = file.path.parent() {
-            let coconut = parent.components().count();
-
-            if coconut == 1 && parent.has_root() {
-                bits.push(colours.symlink_path.paint("/"));
-            }
-            else if coconut >= 1 {
-                bits.push(colours.symlink_path.paint(parent.to_string_lossy().to_string()));
-                bits.push(colours.symlink_path.paint("/"));
-            }
-        }
-    }
-
-    if !file.name.is_empty() {
-        for bit in coloured_file_name(file, colours) {
-            bits.push(bit);
-        }
-    }
-
-    if links && file.is_link() {
-        match file.link_target() {
-            FileTarget::Ok(target) => {
-                bits.push(Style::default().paint(" "));
-                bits.push(colours.punctuation.paint("->"));
-                bits.push(Style::default().paint(" "));
-
-                if let Some(parent) = target.path.parent() {
-                    let coconut = parent.components().count();
-
-                    if coconut == 1 && parent.has_root() {
-                        bits.push(colours.symlink_path.paint("/"));
-                    }
-                    else if coconut >= 1 {
-                        bits.push(colours.symlink_path.paint(parent.to_string_lossy().to_string()));
-                        bits.push(colours.symlink_path.paint("/"));
-                    }
-                }
-
-                if !target.name.is_empty() {
-                    bits.push(file_colour(colours, &target).paint(target.name));
-                }
-            },
-
-            FileTarget::Broken(broken_path) => {
-                bits.push(Style::default().paint(" "));
-                bits.push(colours.broken_arrow.paint("->"));
-                bits.push(Style::default().paint(" "));
-                bits.push(colours.broken_filename.paint(broken_path.display().to_string()));
-            },
-
-            FileTarget::Err(_) => {
-                // Do nothing -- the error gets displayed on the next line
-            }
-        }
-    } else if classify {
-        if file.is_executable_file() {
-            bits.push(Style::default().paint("*"));
-        } else if file.is_directory() {
-            bits.push(Style::default().paint("/"));
-        } else if file.is_pipe() {
-            bits.push(Style::default().paint("|"));
-        } else if file.is_link() {
-            bits.push(Style::default().paint("@"));
-        } else if file.is_socket() {
-            bits.push(Style::default().paint("="));
-        }
-    }
-
-    bits.into()
-}
-
-/// Returns at least one ANSI-highlighted string representing this file’s
-/// name using the given set of colours.
-///
-/// Ordinarily, this will be just one string: the file’s complete name,
-/// coloured according to its file type. If the name contains control
-/// characters such as newlines or escapes, though, we can’t just print them
-/// to the screen directly, because then there’ll be newlines in weird places.
-///
-/// So in that situation, those characters will be escaped and highlighted in
-/// a different colour.
-fn coloured_file_name<'a>(file: &File, colours: &Colours) -> Vec<ANSIString<'a>> {
-    let colour = file_colour(colours, file);
-    let mut bits = Vec::new();
-
-    if file.name.chars().all(|c| c >= 0x20 as char) {
-        bits.push(colour.paint(file.name.clone()));
-    }
-    else {
-        for c in file.name.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 >= 0x20 as char {
-                // TODO: This allocates way too much,
-                // hence the `all` check above.
-                let mut s = String::new();
-                s.push(c);
-                bits.push(colour.paint(s));
-            } else {
-                let s = c.escape_default().collect::<String>();
-                bits.push(colours.control_char.paint(s));
-            }
-        }
-    }
-
-    bits
-}
-
-pub fn file_colour(colours: &Colours, file: &File) -> Style {
-    match file {
-        f if f.is_directory()        => colours.filetypes.directory,
-        f if f.is_executable_file()  => colours.filetypes.executable,
-        f if f.is_link()             => colours.filetypes.symlink,
-        f if f.is_pipe()             => colours.filetypes.pipe,
-        f if f.is_char_device()
-           | f.is_block_device()     => colours.filetypes.device,
-        f if f.is_socket()           => colours.filetypes.socket,
-        f if !f.is_file()            => colours.filetypes.special,
-        f if f.is_immediate()        => colours.filetypes.immediate,
-        f if f.is_image()            => colours.filetypes.image,
-        f if f.is_video()            => colours.filetypes.video,
-        f if f.is_music()            => colours.filetypes.music,
-        f if f.is_lossless()         => colours.filetypes.lossless,
-        f if f.is_crypto()           => colours.filetypes.crypto,
-        f if f.is_document()         => colours.filetypes.document,
-        f if f.is_compressed()       => colours.filetypes.compressed,
-        f if f.is_temp()             => colours.filetypes.temp,
-        f if f.is_compiled()         => colours.filetypes.compiled,
-        _                            => colours.filetypes.normal,
-    }
-}
+pub mod file_name;
+mod escape;

+ 6 - 6
xtests/file_names

@@ -1,6 +1,6 @@
-ansi: [\u{1b}[34mblue\u{1b}[0m]  form-feed: [\u{c}]      return: [\r]
-ascii: hello                     invalid-utf8-1: [�]     tab: [\t]
-backspace: [\u{8}]               invalid-utf8-2: [�(]    utf-8: pâté
-bell: [\u{7}]                    invalid-utf8-3: [�(]    vertical-tab: [\u{b}]
-emoji: [🆒]                       invalid-utf8-4: [�(�(]  
-escape: [\u{1b}]                 new-line: [\n]          
+ansi: [\u{1b}[34mblue\u{1b}[0m]  form-feed: [\u{c}]      new-line-dir: [\n]
+ascii: hello                     invalid-utf8-1: [�]     new-line: [\n]
+backspace: [\u{8}]               invalid-utf8-2: [�(]    return: [\r]
+bell: [\u{7}]                    invalid-utf8-3: [�(]    tab: [\t]
+emoji: [🆒]                       invalid-utf8-4: [�(�(]  utf-8: pâté
+escape: [\u{1b}]                 links                   vertical-tab: [\u{b}]

+ 2 - 0
xtests/file_names_1

@@ -9,6 +9,8 @@ invalid-utf8-1: [�]
 invalid-utf8-2: [�(]
 invalid-utf8-3: [�(]
 invalid-utf8-4: [�(�(]
+links
+new-line-dir: [\n]
 new-line: [\n]
 return: [\r]
 tab: [\t]

+ 12 - 0
xtests/file_names_R

@@ -0,0 +1,12 @@
+ansi: [\u{1b}[34mblue\u{1b}[0m]  form-feed: [\u{c}]      new-line-dir: [\n]
+ascii: hello                     invalid-utf8-1: [�]     new-line: [\n]
+backspace: [\u{8}]               invalid-utf8-2: [�(]    return: [\r]
+bell: [\u{7}]                    invalid-utf8-3: [�(]    tab: [\t]
+emoji: [🆒]                       invalid-utf8-4: [�(�(]  utf-8: pâté
+escape: [\u{1b}]                 links                   vertical-tab: [\u{b}]
+
+/testcases/file-names/links:
+another: [\n]  broken  subfile
+
+/testcases/file-names/new-line-dir: [\n]:
+another: [\n]  subfile

+ 29 - 0
xtests/file_names_T

@@ -0,0 +1,29 @@
+/testcases/file-names
+├── ansi: [\u{1b}[34mblue\u{1b}[0m]
+├── ascii: hello
+├── backspace: [\u{8}]
+├── bell: [\u{7}]
+├── emoji: [🆒]
+├── escape: [\u{1b}]
+├── form-feed: [\u{c}]
+├── invalid-utf8-1: [�]
+│  └── <Error: path somehow contained a NUL?>
+├── invalid-utf8-2: [�(]
+│  └── <Error: path somehow contained a NUL?>
+├── invalid-utf8-3: [�(]
+│  └── <Error: path somehow contained a NUL?>
+├── invalid-utf8-4: [�(�(]
+│  └── <Error: path somehow contained a NUL?>
+├── links
+│  ├── another: [\n] -> /testcases/file-names/new-line-dir: [\n]/another: [\n]
+│  ├── broken -> /testcases/file-names/new-line-dir: [\n]/broken
+│  │  └── <No such file or directory (os error 2)>
+│  └── subfile -> /testcases/file-names/new-line-dir: [\n]/subfile
+├── new-line-dir: [\n]
+│  ├── another: [\n]
+│  └── subfile
+├── new-line: [\n]
+├── return: [\r]
+├── tab: [\t]
+├── utf-8: pâté
+└── vertical-tab: [\u{b}]

+ 3 - 3
xtests/file_names_x

@@ -1,6 +1,6 @@
 ansi: [\u{1b}[34mblue\u{1b}[0m]  ascii: hello            backspace: [\u{8}]
 bell: [\u{7}]                    emoji: [🆒]              escape: [\u{1b}]
 form-feed: [\u{c}]               invalid-utf8-1: [�]     invalid-utf8-2: [�(]
-invalid-utf8-3: [�(]             invalid-utf8-4: [�(�(]  new-line: [\n]
-return: [\r]                     tab: [\t]               utf-8: pâté
-vertical-tab: [\u{b}]            
+invalid-utf8-3: [�(]             invalid-utf8-4: [�(�(]  links
+new-line-dir: [\n]               new-line: [\n]          return: [\r]
+tab: [\t]                        utf-8: pâté             vertical-tab: [\u{b}]

+ 13 - 0
xtests/files_star_100

@@ -0,0 +1,13 @@
+/testcases/files/10_bytes  /testcases/files/1_KiB    /testcases/files/5_MiB
+/testcases/files/10_KiB    /testcases/files/1_MiB    /testcases/files/6_bytes
+/testcases/files/10_MiB    /testcases/files/2_bytes  /testcases/files/6_KiB
+/testcases/files/11_bytes  /testcases/files/2_KiB    /testcases/files/6_MiB
+/testcases/files/11_KiB    /testcases/files/2_MiB    /testcases/files/7_bytes
+/testcases/files/11_MiB    /testcases/files/3_bytes  /testcases/files/7_KiB
+/testcases/files/12_bytes  /testcases/files/3_KiB    /testcases/files/7_MiB
+/testcases/files/12_KiB    /testcases/files/3_MiB    /testcases/files/8_bytes
+/testcases/files/12_MiB    /testcases/files/4_bytes  /testcases/files/8_KiB
+/testcases/files/13_bytes  /testcases/files/4_KiB    /testcases/files/8_MiB
+/testcases/files/13_KiB    /testcases/files/4_MiB    /testcases/files/9_bytes
+/testcases/files/13_MiB    /testcases/files/5_bytes  /testcases/files/9_KiB
+/testcases/files/1_bytes   /testcases/files/5_KiB    /testcases/files/9_MiB

+ 8 - 0
xtests/files_star_150

@@ -0,0 +1,8 @@
+/testcases/files/10_bytes  /testcases/files/12_MiB    /testcases/files/2_KiB    /testcases/files/5_bytes  /testcases/files/7_MiB
+/testcases/files/10_KiB    /testcases/files/13_bytes  /testcases/files/2_MiB    /testcases/files/5_KiB    /testcases/files/8_bytes
+/testcases/files/10_MiB    /testcases/files/13_KiB    /testcases/files/3_bytes  /testcases/files/5_MiB    /testcases/files/8_KiB
+/testcases/files/11_bytes  /testcases/files/13_MiB    /testcases/files/3_KiB    /testcases/files/6_bytes  /testcases/files/8_MiB
+/testcases/files/11_KiB    /testcases/files/1_bytes   /testcases/files/3_MiB    /testcases/files/6_KiB    /testcases/files/9_bytes
+/testcases/files/11_MiB    /testcases/files/1_KiB     /testcases/files/4_bytes  /testcases/files/6_MiB    /testcases/files/9_KiB
+/testcases/files/12_bytes  /testcases/files/1_MiB     /testcases/files/4_KiB    /testcases/files/7_bytes  /testcases/files/9_MiB
+/testcases/files/12_KiB    /testcases/files/2_bytes   /testcases/files/4_MiB    /testcases/files/7_KiB    

+ 6 - 0
xtests/files_star_200

@@ -0,0 +1,6 @@
+/testcases/files/10_bytes  /testcases/files/12_bytes  /testcases/files/1_bytes  /testcases/files/3_bytes  /testcases/files/5_bytes  /testcases/files/7_bytes  /testcases/files/9_bytes
+/testcases/files/10_KiB    /testcases/files/12_KiB    /testcases/files/1_KiB    /testcases/files/3_KiB    /testcases/files/5_KiB    /testcases/files/7_KiB    /testcases/files/9_KiB
+/testcases/files/10_MiB    /testcases/files/12_MiB    /testcases/files/1_MiB    /testcases/files/3_MiB    /testcases/files/5_MiB    /testcases/files/7_MiB    /testcases/files/9_MiB
+/testcases/files/11_bytes  /testcases/files/13_bytes  /testcases/files/2_bytes  /testcases/files/4_bytes  /testcases/files/6_bytes  /testcases/files/8_bytes  
+/testcases/files/11_KiB    /testcases/files/13_KiB    /testcases/files/2_KiB    /testcases/files/4_KiB    /testcases/files/6_KiB    /testcases/files/8_KiB    
+/testcases/files/11_MiB    /testcases/files/13_MiB    /testcases/files/2_MiB    /testcases/files/4_MiB    /testcases/files/6_MiB    /testcases/files/8_MiB    

+ 39 - 0
xtests/files_star_lG_100

@@ -0,0 +1,39 @@
+.rw-r--r--   10 cassowary  1 Jan 12:34 /testcases/files/10_bytes
+.rw-r--r--  10k cassowary  1 Jan 12:34 /testcases/files/10_KiB
+.rw-r--r--  10M cassowary  1 Jan 12:34 /testcases/files/10_MiB
+.rw-r--r--   11 cassowary  1 Jan 12:34 /testcases/files/11_bytes
+.rw-r--r--  11k cassowary  1 Jan 12:34 /testcases/files/11_KiB
+.rw-r--r--  11M cassowary  1 Jan 12:34 /testcases/files/11_MiB
+.rw-r--r--   12 cassowary  1 Jan 12:34 /testcases/files/12_bytes
+.rw-r--r--  12k cassowary  1 Jan 12:34 /testcases/files/12_KiB
+.rw-r--r--  12M cassowary  1 Jan 12:34 /testcases/files/12_MiB
+.rw-r--r--   13 cassowary  1 Jan 12:34 /testcases/files/13_bytes
+.rw-r--r--  13k cassowary  1 Jan 12:34 /testcases/files/13_KiB
+.rw-r--r--  13M cassowary  1 Jan 12:34 /testcases/files/13_MiB
+.rw-r--r--    1 cassowary  1 Jan 12:34 /testcases/files/1_bytes
+.rw-r--r-- 1.0k cassowary  1 Jan 12:34 /testcases/files/1_KiB
+.rw-r--r-- 1.0M cassowary  1 Jan 12:34 /testcases/files/1_MiB
+.rw-r--r--    2 cassowary  1 Jan 12:34 /testcases/files/2_bytes
+.rw-r--r-- 2.0k cassowary  1 Jan 12:34 /testcases/files/2_KiB
+.rw-r--r-- 2.1M cassowary  1 Jan 12:34 /testcases/files/2_MiB
+.rw-r--r--    3 cassowary  1 Jan 12:34 /testcases/files/3_bytes
+.rw-r--r-- 3.1k cassowary  1 Jan 12:34 /testcases/files/3_KiB
+.rw-r--r-- 3.1M cassowary  1 Jan 12:34 /testcases/files/3_MiB
+.rw-r--r--    4 cassowary  1 Jan 12:34 /testcases/files/4_bytes
+.rw-r--r-- 4.1k cassowary  1 Jan 12:34 /testcases/files/4_KiB
+.rw-r--r-- 4.2M cassowary  1 Jan 12:34 /testcases/files/4_MiB
+.rw-r--r--    5 cassowary  1 Jan 12:34 /testcases/files/5_bytes
+.rw-r--r-- 5.1k cassowary  1 Jan 12:34 /testcases/files/5_KiB
+.rw-r--r-- 5.2M cassowary  1 Jan 12:34 /testcases/files/5_MiB
+.rw-r--r--    6 cassowary  1 Jan 12:34 /testcases/files/6_bytes
+.rw-r--r-- 6.1k cassowary  1 Jan 12:34 /testcases/files/6_KiB
+.rw-r--r-- 6.3M cassowary  1 Jan 12:34 /testcases/files/6_MiB
+.rw-r--r--    7 cassowary  1 Jan 12:34 /testcases/files/7_bytes
+.rw-r--r-- 7.2k cassowary  1 Jan 12:34 /testcases/files/7_KiB
+.rw-r--r-- 7.3M cassowary  1 Jan 12:34 /testcases/files/7_MiB
+.rw-r--r--    8 cassowary  1 Jan 12:34 /testcases/files/8_bytes
+.rw-r--r-- 8.2k cassowary  1 Jan 12:34 /testcases/files/8_KiB
+.rw-r--r-- 8.4M cassowary  1 Jan 12:34 /testcases/files/8_MiB
+.rw-r--r--    9 cassowary  1 Jan 12:34 /testcases/files/9_bytes
+.rw-r--r-- 9.2k cassowary  1 Jan 12:34 /testcases/files/9_KiB
+.rw-r--r-- 9.4M cassowary  1 Jan 12:34 /testcases/files/9_MiB

+ 20 - 0
xtests/files_star_lG_150

@@ -0,0 +1,20 @@
+.rw-r--r--   10 cassowary  1 Jan 12:34 /testcases/files/10_bytes    .rw-r--r-- 3.1M cassowary  1 Jan 12:34 /testcases/files/3_MiB
+.rw-r--r--  10k cassowary  1 Jan 12:34 /testcases/files/10_KiB      .rw-r--r--    4 cassowary  1 Jan 12:34 /testcases/files/4_bytes
+.rw-r--r--  10M cassowary  1 Jan 12:34 /testcases/files/10_MiB      .rw-r--r-- 4.1k cassowary  1 Jan 12:34 /testcases/files/4_KiB
+.rw-r--r--   11 cassowary  1 Jan 12:34 /testcases/files/11_bytes    .rw-r--r-- 4.2M cassowary  1 Jan 12:34 /testcases/files/4_MiB
+.rw-r--r--  11k cassowary  1 Jan 12:34 /testcases/files/11_KiB      .rw-r--r--    5 cassowary  1 Jan 12:34 /testcases/files/5_bytes
+.rw-r--r--  11M cassowary  1 Jan 12:34 /testcases/files/11_MiB      .rw-r--r-- 5.1k cassowary  1 Jan 12:34 /testcases/files/5_KiB
+.rw-r--r--   12 cassowary  1 Jan 12:34 /testcases/files/12_bytes    .rw-r--r-- 5.2M cassowary  1 Jan 12:34 /testcases/files/5_MiB
+.rw-r--r--  12k cassowary  1 Jan 12:34 /testcases/files/12_KiB      .rw-r--r--    6 cassowary  1 Jan 12:34 /testcases/files/6_bytes
+.rw-r--r--  12M cassowary  1 Jan 12:34 /testcases/files/12_MiB      .rw-r--r-- 6.1k cassowary  1 Jan 12:34 /testcases/files/6_KiB
+.rw-r--r--   13 cassowary  1 Jan 12:34 /testcases/files/13_bytes    .rw-r--r-- 6.3M cassowary  1 Jan 12:34 /testcases/files/6_MiB
+.rw-r--r--  13k cassowary  1 Jan 12:34 /testcases/files/13_KiB      .rw-r--r--    7 cassowary  1 Jan 12:34 /testcases/files/7_bytes
+.rw-r--r--  13M cassowary  1 Jan 12:34 /testcases/files/13_MiB      .rw-r--r-- 7.2k cassowary  1 Jan 12:34 /testcases/files/7_KiB
+.rw-r--r--    1 cassowary  1 Jan 12:34 /testcases/files/1_bytes     .rw-r--r-- 7.3M cassowary  1 Jan 12:34 /testcases/files/7_MiB
+.rw-r--r-- 1.0k cassowary  1 Jan 12:34 /testcases/files/1_KiB       .rw-r--r--    8 cassowary  1 Jan 12:34 /testcases/files/8_bytes
+.rw-r--r-- 1.0M cassowary  1 Jan 12:34 /testcases/files/1_MiB       .rw-r--r-- 8.2k cassowary  1 Jan 12:34 /testcases/files/8_KiB
+.rw-r--r--    2 cassowary  1 Jan 12:34 /testcases/files/2_bytes     .rw-r--r-- 8.4M cassowary  1 Jan 12:34 /testcases/files/8_MiB
+.rw-r--r-- 2.0k cassowary  1 Jan 12:34 /testcases/files/2_KiB       .rw-r--r--    9 cassowary  1 Jan 12:34 /testcases/files/9_bytes
+.rw-r--r-- 2.1M cassowary  1 Jan 12:34 /testcases/files/2_MiB       .rw-r--r-- 9.2k cassowary  1 Jan 12:34 /testcases/files/9_KiB
+.rw-r--r--    3 cassowary  1 Jan 12:34 /testcases/files/3_bytes     .rw-r--r-- 9.4M cassowary  1 Jan 12:34 /testcases/files/9_MiB
+.rw-r--r-- 3.1k cassowary  1 Jan 12:34 /testcases/files/3_KiB       

+ 13 - 0
xtests/files_star_lG_200

@@ -0,0 +1,13 @@
+.rw-r--r--  10 cassowary  1 Jan 12:34 /testcases/files/10_bytes    .rw-r--r-- 1.0k cassowary  1 Jan 12:34 /testcases/files/1_KiB      .rw-r--r-- 5.2M cassowary  1 Jan 12:34 /testcases/files/5_MiB
+.rw-r--r-- 10k cassowary  1 Jan 12:34 /testcases/files/10_KiB      .rw-r--r-- 1.0M cassowary  1 Jan 12:34 /testcases/files/1_MiB      .rw-r--r--    6 cassowary  1 Jan 12:34 /testcases/files/6_bytes
+.rw-r--r-- 10M cassowary  1 Jan 12:34 /testcases/files/10_MiB      .rw-r--r--    2 cassowary  1 Jan 12:34 /testcases/files/2_bytes    .rw-r--r-- 6.1k cassowary  1 Jan 12:34 /testcases/files/6_KiB
+.rw-r--r--  11 cassowary  1 Jan 12:34 /testcases/files/11_bytes    .rw-r--r-- 2.0k cassowary  1 Jan 12:34 /testcases/files/2_KiB      .rw-r--r-- 6.3M cassowary  1 Jan 12:34 /testcases/files/6_MiB
+.rw-r--r-- 11k cassowary  1 Jan 12:34 /testcases/files/11_KiB      .rw-r--r-- 2.1M cassowary  1 Jan 12:34 /testcases/files/2_MiB      .rw-r--r--    7 cassowary  1 Jan 12:34 /testcases/files/7_bytes
+.rw-r--r-- 11M cassowary  1 Jan 12:34 /testcases/files/11_MiB      .rw-r--r--    3 cassowary  1 Jan 12:34 /testcases/files/3_bytes    .rw-r--r-- 7.2k cassowary  1 Jan 12:34 /testcases/files/7_KiB
+.rw-r--r--  12 cassowary  1 Jan 12:34 /testcases/files/12_bytes    .rw-r--r-- 3.1k cassowary  1 Jan 12:34 /testcases/files/3_KiB      .rw-r--r-- 7.3M cassowary  1 Jan 12:34 /testcases/files/7_MiB
+.rw-r--r-- 12k cassowary  1 Jan 12:34 /testcases/files/12_KiB      .rw-r--r-- 3.1M cassowary  1 Jan 12:34 /testcases/files/3_MiB      .rw-r--r--    8 cassowary  1 Jan 12:34 /testcases/files/8_bytes
+.rw-r--r-- 12M cassowary  1 Jan 12:34 /testcases/files/12_MiB      .rw-r--r--    4 cassowary  1 Jan 12:34 /testcases/files/4_bytes    .rw-r--r-- 8.2k cassowary  1 Jan 12:34 /testcases/files/8_KiB
+.rw-r--r--  13 cassowary  1 Jan 12:34 /testcases/files/13_bytes    .rw-r--r-- 4.1k cassowary  1 Jan 12:34 /testcases/files/4_KiB      .rw-r--r-- 8.4M cassowary  1 Jan 12:34 /testcases/files/8_MiB
+.rw-r--r-- 13k cassowary  1 Jan 12:34 /testcases/files/13_KiB      .rw-r--r-- 4.2M cassowary  1 Jan 12:34 /testcases/files/4_MiB      .rw-r--r--    9 cassowary  1 Jan 12:34 /testcases/files/9_bytes
+.rw-r--r-- 13M cassowary  1 Jan 12:34 /testcases/files/13_MiB      .rw-r--r--    5 cassowary  1 Jan 12:34 /testcases/files/5_bytes    .rw-r--r-- 9.2k cassowary  1 Jan 12:34 /testcases/files/9_KiB
+.rw-r--r--   1 cassowary  1 Jan 12:34 /testcases/files/1_bytes     .rw-r--r-- 5.1k cassowary  1 Jan 12:34 /testcases/files/5_KiB      .rw-r--r-- 9.4M cassowary  1 Jan 12:34 /testcases/files/9_MiB

+ 11 - 0
xtests/run.sh

@@ -33,6 +33,10 @@ COLUMNS=120 $exa $testcases/files | diff -q - $results/files_120  || exit 1
 COLUMNS=160 $exa $testcases/files | diff -q - $results/files_160  || exit 1
 COLUMNS=200 $exa $testcases/files | diff -q - $results/files_200  || exit 1
 
+COLUMNS=100 $exa $testcases/files/* | diff -q - $results/files_star_100   || exit 1
+COLUMNS=150 $exa $testcases/files/* | diff -q - $results/files_star_150  || exit 1
+COLUMNS=200 $exa $testcases/files/* | diff -q - $results/files_star_200  || exit 1
+
 
 # Long grid view tests
 COLUMNS=40  $exa $testcases/files -lG | diff -q - $results/files_lG_40   || exit 1
@@ -41,6 +45,10 @@ COLUMNS=120 $exa $testcases/files -lG | diff -q - $results/files_lG_120  || exit
 COLUMNS=160 $exa $testcases/files -lG | diff -q - $results/files_lG_160  || exit 1
 COLUMNS=200 $exa $testcases/files -lG | diff -q - $results/files_lG_200  || exit 1
 
+COLUMNS=100 $exa $testcases/files/* -lG | diff -q - $results/files_star_lG_100  || exit 1
+COLUMNS=150 $exa $testcases/files/* -lG | diff -q - $results/files_star_lG_150  || exit 1
+COLUMNS=200 $exa $testcases/files/* -lG | diff -q - $results/files_star_lG_200  || exit 1
+
 
 # Attributes
 $exa $testcases/attributes -l@T | diff -q - $results/attributes  || exit 1
@@ -55,9 +63,12 @@ sudo -u cassowary $exa $testcases/permissions -lghR 2>&1 | diff -q - $results/pe
                   $exa $testcases/permissions -lghR 2>&1 | diff -q - $results/permissions       || exit 1
 
 # File names
+# (Mostly escaping control characters in file names)
 COLUMNS=80 $exa $testcases/file-names    2>&1 | diff -q - $results/file_names   || exit 1
 COLUMNS=80 $exa $testcases/file-names -x 2>&1 | diff -q - $results/file_names_x || exit 1
+COLUMNS=80 $exa $testcases/file-names -R 2>&1 | diff -q - $results/file_names_R || exit 1
            $exa $testcases/file-names -1 2>&1 | diff -q - $results/file_names_1 || exit 1
+           $exa $testcases/file-names -T 2>&1 | diff -q - $results/file_names_T || exit 1
 
 # File types
 $exa $testcases/file-names-exts -1 2>&1 | diff -q - $results/file-names-exts  || exit 1