Repository: lsd-rs/lsd (language: rust)

Issue:
File names with spaces are not quoted
- os: Linux 5.17.7-arch1-1 x86_64
- `lsd --version`: lsd 0.21.0
- `echo $TERM`: tmux-256color
- `echo $LS_COLORS`: rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:mi=00:su=37;41:sg=30;43:ca=00:tw=30;42:ow=34;42:st=37;44:ex=01;32:*.tar=01;31:*.tgz=01;31:*.arc=01;31:*.arj=01;31:*.taz=01;31:*.lha=01;31:*.lz4=01;31:*.lzh=01;31:*.lzma=01;31:*.tlz=01;31:*.txz=01;31:*.tzo=01;31:*.t7z=01;31:*.zip=01;31:*.z=01;31:*.dz=01;31:*.gz=01;31:*.lrz=01;31:*.lz=01;31:*.lzo=01;31:*.xz=01;31:*.zst=01;31:*.tzst=01;31:*.bz2=01;31:*.bz=01;31:*.tbz=01;31:*.tbz2=01;31:*.tz=01;31:*.deb=01;31:*.rpm=01;31:*.jar=01;31:*.war=01;31:*.ear=01;31:*.sar=01;31:*.rar=01;31:*.alz=01;31:*.ace=01;31:*.zoo=01;31:*.cpio=01;31:*.7z=01;31:*.rz=01;31:*.cab=01;31:*.wim=01;31:*.swm=01;31:*.dwm=01;31:*.esd=01;31:*.avif=01;35:*.jpg=01;35:*.jpeg=01;35:*.mjpg=01;35:*.mjpeg=01;35:*.gif=01;35:*.bmp=01;35:*.pbm=01;35:*.pgm=01;35:*.ppm=01;35:*.tga=01;35:*.xbm=01;35:*.xpm=01;35:*.tif=01;35:*.tiff=01;35:*.png=01;35:*.svg=01;35:*.svgz=01;35:*.mng=01;35:*.pcx=01;35:*.mov=01;35:*.mpg=01;35:*.mpeg=01;35:*.m2v=01;35:*.mkv=01;35:*.webm=01;35:*.webp=01;35:*.ogm=01;35:*.mp4=01;35:*.m4v=01;35:*.mp4v=01;35:*.vob=01;35:*.qt=01;35:*.nuv=01;35:*.wmv=01;35:*.asf=01;35:*.rm=01;35:*.rmvb=01;35:*.flc=01;35:*.avi=01;35:*.fli=01;35:*.flv=01;35:*.gl=01;35:*.dl=01;35:*.xcf=01;35:*.xwd=01;35:*.yuv=01;35:*.cgm=01;35:*.emf=01;35:*.ogv=01;35:*.ogx=01;35:*.aac=00;36:*.au=00;36:*.flac=00;36:*.m4a=00;36:*.mid=00;36:*.midi=00;36:*.mka=00;36:*.mp3=00;36:*.mpc=00;36:*.ogg=00;36:*.ra=00;36:*.wav=00;36:*.oga=00;36:*.opus=00;36:*.spx=00;36:*.xspf=00;36:*~=00;90:*#=00;90:*.bak=00;90:*.old=00;90:*.orig=00;90:*.part=00;90:*.rej=00;90:*.swp=00;90:*.tmp=00;90:*.dpkg-dist=00;90:*.dpkg-old=00;90:*.ucf-dist=00;90:*.ucf-new=00;90:*.ucf-old=00;90:*.rpmnew=00;90:*.rpmorig=00;90:*.rpmsave=00;90:


## Expected behavior
File names with spaces are quoted.
```sh
touch aaa\ aaa
ls aaa\ aaa
```
`'aaa aaa'`


## Actual behavior
File names with spaces are not quoted.
`lsd aaa\ aaa`

` aaa aaa`



Discussion / hints:
hi @planet36, may I ask which builtin ls version did you use?

I try `GNU ls` and bsd ls on macOS, and both show `a a` without the quote.
`ls --version | head -n 1`
`ls (GNU coreutils) 9.1`

I guess this behavior was added in coreutils 8.25.
https://git.savannah.gnu.org/cgit/coreutils.git/tree/NEWS#n1046
See also https://unix.stackexchange.com/q/258679/439780
Thanks so much for the information!

then this could be an improvement for us to implement.

I did not have a newer version of `GNU ls`, could you please add some more case outputs for us.

- filename with space
- filename with single-quote
- filename with double-quote
- filename with `$`
- output to a pipe like `ls | cat`
Python script to create the files:
```python
from pathlib import Path
for i in range(32, 127):
    if chr(i) == '/':
        continue
    f = "{:02X}{}{:02X}".format(i, chr(i), i)
    Path(f).touch()
```

`ls -1`

> '20 20'
> '22"22'
> '24$24'
> "27'27"
> ...


`ls -1 | cat`
> 20 20
> 22"22
> 24$24
> 27'27
> ...





Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c074b0981..8b60c097f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
 ## [Unreleased]
+### Added
+- In keeping with the coreutils change, add quotes and escapes for necessary filenames from [merelymyself](https://github.com/merelymyself)
 
 ## [0.23.1] - 2022-09-13
 
diff --git a/src/color/theme.rs b/src/color/theme.rs
index 148753bfd..6a49261da 100644
--- a/src/color/theme.rs
+++ b/src/color/theme.rs
@@ -84,7 +84,7 @@ where
 
 /// A struct holding the theme configuration
 /// Color table: https://upload.wikimedia.org/wikipedia/commons/1/15/Xterm_256color_chart.avg
-#[derive(Debug, Deserialize, PartialEq)]
+#[derive(Debug, Deserialize, PartialEq, Eq)]
 #[serde(rename_all = "kebab-case")]
 #[serde(deny_unknown_fields)]
 #[serde(default)]
@@ -105,7 +105,7 @@ pub struct Theme {
     pub file_type: FileType,
 }
 
-#[derive(Debug, Deserialize, PartialEq)]
+#[derive(Debug, Deserialize, PartialEq, Eq)]
 #[serde(rename_all = "kebab-case")]
 #[serde(deny_unknown_fields)]
 #[serde(default)]
@@ -128,7 +128,7 @@ pub struct Permission {
     pub context: Color,
 }
 
-#[derive(Debug, Deserialize, PartialEq)]
+#[derive(Debug, Deserialize, PartialEq, Eq)]
 #[serde(rename_all = "kebab-case")]
 #[serde(deny_unknown_fields)]
 #[serde(default)]
@@ -148,7 +148,7 @@ pub struct FileType {
     pub special: Color,
 }
 
-#[derive(Debug, Deserialize, PartialEq)]
+#[derive(Debug, Deserialize, PartialEq, Eq)]
 #[serde(rename_all = "kebab-case")]
 #[serde(deny_unknown_fields)]
 #[serde(default)]
@@ -163,7 +163,7 @@ pub struct File {
     pub no_exec_no_uid: Color,
 }
 
-#[derive(Debug, Deserialize, PartialEq)]
+#[derive(Debug, Deserialize, PartialEq, Eq)]
 #[serde(rename_all = "kebab-case")]
 #[serde(deny_unknown_fields)]
 #[serde(default)]
@@ -174,7 +174,7 @@ pub struct Dir {
     pub no_uid: Color,
 }
 
-#[derive(Debug, Deserialize, PartialEq)]
+#[derive(Debug, Deserialize, PartialEq, Eq)]
 #[serde(rename_all = "kebab-case")]
 #[serde(deny_unknown_fields)]
 #[serde(default)]
@@ -187,7 +187,7 @@ pub struct Symlink {
     pub missing_target: Color,
 }
 
-#[derive(Debug, Deserialize, PartialEq)]
+#[derive(Debug, Deserialize, PartialEq, Eq)]
 #[serde(rename_all = "kebab-case")]
 #[serde(deny_unknown_fields)]
 #[serde(default)]
@@ -200,7 +200,7 @@ pub struct Date {
     pub older: Color,
 }
 
-#[derive(Debug, Deserialize, PartialEq)]
+#[derive(Debug, Deserialize, PartialEq, Eq)]
 #[serde(rename_all = "kebab-case")]
 #[serde(deny_unknown_fields)]
 #[serde(default)]
@@ -215,7 +215,7 @@ pub struct Size {
     pub large: Color,
 }
 
-#[derive(Debug, Deserialize, PartialEq)]
+#[derive(Debug, Deserialize, PartialEq, Eq)]
 #[serde(rename_all = "kebab-case")]
 #[serde(deny_unknown_fields)]
 #[serde(default)]
@@ -226,7 +226,7 @@ pub struct INode {
     pub invalid: Color,
 }
 
-#[derive(Debug, Deserialize, PartialEq)]
+#[derive(Debug, Deserialize, PartialEq, Eq)]
 #[serde(rename_all = "kebab-case")]
 #[serde(deny_unknown_fields)]
 #[serde(default)]
diff --git a/src/display.rs b/src/display.rs
index 74788e471..21139a36f 100644
--- a/src/display.rs
+++ b/src/display.rs
@@ -423,7 +423,7 @@ mod tests {
             ("ASCII1234-_", 11),
             ("制作样本。", 10),
             ("日本語", 6),
-            ("샘플은 무료로 드리겠습니다", 26),
+            ("샘플은 무료로 드리겠습니다", 28),
             ("👩🐩", 4),
             ("🔬", 2),
         ] {
@@ -454,10 +454,10 @@ mod tests {
             // Add 3 characters for the icons.
             ("Ｈｅｌｌｏ,ｗｏｒｌｄ!", 24),
             ("ASCII1234-_", 13),
-            ("File with space", 17),
+            ("File with space", 19),
             ("制作样本。", 12),
             ("日本語", 8),
-            ("샘플은 무료로 드리겠습니다", 28),
+            ("샘플은 무료로 드리겠습니다", 30),
             ("👩🐩", 6),
             ("🔬", 4),
         ] {
@@ -487,10 +487,10 @@ mod tests {
         for (s, l) in [
             ("Ｈｅｌｌｏ,ｗｏｒｌｄ!", 22),
             ("ASCII1234-_", 11),
-            ("File with space", 15),
+            ("File with space", 17),
             ("制作样本。", 10),
             ("日本語", 6),
-            ("샘플은 무료로 드리겠습니다", 26),
+            ("샘플은 무료로 드리겠습니다", 28),
             ("👩🐩", 4),
             ("🔬", 2),
         ] {
@@ -528,10 +528,10 @@ mod tests {
         for (s, l) in [
             ("Ｈｅｌｌｏ,ｗｏｒｌｄ!", 22),
             ("ASCII1234-_", 11),
-            ("File with space", 15),
+            ("File with space", 17),
             ("制作样本。", 10),
             ("日本語", 6),
-            ("샘플은 무료로 드리겠습니다", 26),
+            ("샘플은 무료로 드리겠습니다", 28),
             ("👩🐩", 4),
             ("🔬", 2),
         ] {
diff --git a/src/meta/name.rs b/src/meta/name.rs
index 8bfa5d569..a9e092ca9 100644
--- a/src/meta/name.rs
+++ b/src/meta/name.rs
@@ -79,11 +79,21 @@ impl Name {
     }
 
     fn escape(&self, string: &str) -> String {
+        let mut name = string.to_string();
+        if name.contains('\\') || name.contains('"') {
+            name = name.replace('\'', "\'\\\'\'");
+            name = format!("\'{}\'", &name);
+        } else if name.contains('\'') {
+            name = format!("\"{}\"", &name);
+        } else if name.contains(' ') || name.contains('$') {
+            name = format!("\'{}\'", &name);
+        }
+        let string = name;
         if string
             .chars()
             .all(|c| c >= 0x20 as char && c != 0x7f as char)
         {
-            string.to_string()
+            string
         } else {
             let mut chars = String::new();
             for c in string.chars() {
@@ -644,6 +654,62 @@ mod test {
             )
         );
 
+        let file_path = tmp_dir.path().join("a$a.txt");
+        File::create(&file_path).expect("failed to create file");
+        let meta = file_path.metadata().expect("failed to get metas");
+
+        let colors = Colors::new(color::ThemeOption::NoLscolors);
+        let file_type = FileType::new(&meta, None, &Permissions::from(&meta));
+        let name = Name::new(&file_path, file_type);
+
+        assert_eq!(
+            " \'a$a.txt\'".to_string().with(Color::AnsiValue(184)),
+            name.render(
+                &colors,
+                &icons,
+                &DisplayOption::FileName,
+                HyperlinkOption::Never
+            )
+        );
+
+        let file_path = tmp_dir.path().join("\\.txt");
+        File::create(&file_path).expect("failed to create file");
+        let meta = file_path.metadata().expect("failed to get metas");
+
+        let colors = Colors::new(color::ThemeOption::NoLscolors);
+        let file_type = FileType::new(&meta, None, &Permissions::from(&meta));
+        let name = Name::new(&file_path, file_type);
+
+        assert_eq!(
+            " \'\\.txt\'".to_string().with(Color::AnsiValue(184)),
+            name.render(
+                &colors,
+                &icons,
+                &DisplayOption::FileName,
+                HyperlinkOption::Never
+            )
+        );
+
+        let file_path = tmp_dir.path().join("\"\'.txt");
+        File::create(&file_path).expect("failed to create file");
+        let meta = file_path.metadata().expect("failed to get metas");
+
+        let colors = Colors::new(color::ThemeOption::NoLscolors);
+        let file_type = FileType::new(&meta, None, &Permissions::from(&meta));
+        let name = Name::new(&file_path, file_type);
+
+        assert_eq!(
+            " \'\"\'\\\'\'.txt\'"
+                .to_string()
+                .with(Color::AnsiValue(184)),
+            name.render(
+                &colors,
+                &icons,
+                &DisplayOption::FileName,
+                HyperlinkOption::Never
+            )
+        );
+
         let file_path = tmp_dir.path().join("file\nnewline.txt");
         File::create(&file_path).expect("failed to create file");
         let meta = file_path.metadata().expect("failed to get metas");


Test patch: