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

Issue:
Return correct error code when error occurred like GNU ls
from https://github.com/Peltoche/lsd/issues/498 I found that `lsd` will not return an error code besides 0 even error occurred

we should implement this like the `GNU ls`

## Expected behavior
GNU ls output:

```
λ \ls a
ls: a: No such file or directory

✗ echo $?
1
```


## Actual behavior
lsd:

```
λ ls a
lsd: a: No such file or directory (os error 2).

λ echo $?
0
```



Discussion / hints:

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f76d091db..4ee25e33f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ### Fixed
 - Fix tab completion for paths in ZSH
+- Fix POSIX-compatible exit status
 
 ## [0.23.0] - 2022-09-05
 ### Added
diff --git a/src/core.rs b/src/core.rs
index 629d6f910..407d42c9d 100644
--- a/src/core.rs
+++ b/src/core.rs
@@ -6,7 +6,7 @@ use crate::flags::{
 };
 use crate::icon::{self, Icons};
 use crate::meta::Meta;
-use crate::{print_error, print_output, sort};
+use crate::{print_error, print_output, sort, ExitCode};
 use std::path::PathBuf;
 
 #[cfg(not(target_os = "windows"))]
@@ -83,14 +83,16 @@ impl Core {
         }
     }
 
-    pub fn run(self, paths: Vec<PathBuf>) {
-        let mut meta_list = self.fetch(paths);
+    pub fn run(self, paths: Vec<PathBuf>) -> ExitCode {
+        let (mut meta_list, exit_code) = self.fetch(paths);
 
         self.sort(&mut meta_list);
-        self.display(&meta_list)
+        self.display(&meta_list);
+        exit_code
     }
 
-    fn fetch(&self, paths: Vec<PathBuf>) -> Vec<Meta> {
+    fn fetch(&self, paths: Vec<PathBuf>) -> (Vec<Meta>, ExitCode) {
+        let mut exit_code = ExitCode::OK;
         let mut meta_list = Vec::with_capacity(paths.len());
         let depth = match self.flags.layout {
             Layout::Tree { .. } => self.flags.recursion.depth,
@@ -103,6 +105,7 @@ impl Core {
                 Ok(meta) => meta,
                 Err(err) => {
                     print_error!("{}: {}.", path.display(), err);
+                    exit_code.set_if_greater(ExitCode::MajorIssue);
                     continue;
                 }
             };
@@ -111,12 +114,14 @@ impl Core {
                 self.flags.layout == Layout::Tree || self.flags.display != Display::DirectoryOnly;
             if recurse {
                 match meta.recurse_into(depth, &self.flags) {
-                    Ok(content) => {
+                    Ok((content, path_exit_code)) => {
                         meta.content = content;
                         meta_list.push(meta);
+                        exit_code.set_if_greater(path_exit_code);
                     }
                     Err(err) => {
                         print_error!("lsd: {}: {}\n", path.display(), err);
+                        exit_code.set_if_greater(ExitCode::MinorIssue);
                         continue;
                     }
                 };
@@ -131,7 +136,7 @@ impl Core {
             }
         }
 
-        meta_list
+        (meta_list, exit_code)
     }
 
     fn sort(&self, metas: &mut Vec<Meta>) {
diff --git a/src/display.rs b/src/display.rs
index 472bf26ba..74788e471 100644
--- a/src/display.rs
+++ b/src/display.rs
@@ -602,6 +602,7 @@ mod tests {
             .unwrap()
             .recurse_into(42, &flags)
             .unwrap()
+            .0
             .unwrap();
         sort(&mut metas, &sort::assemble_sorters(&flags));
         let output = tree(
@@ -633,6 +634,7 @@ mod tests {
             .unwrap()
             .recurse_into(42, &flags)
             .unwrap()
+            .0
             .unwrap();
         let output = tree(
             &metas,
@@ -672,6 +674,7 @@ mod tests {
             .unwrap()
             .recurse_into(42, &flags)
             .unwrap()
+            .0
             .unwrap();
         let output = tree(
             &metas,
@@ -710,6 +713,7 @@ mod tests {
             .unwrap()
             .recurse_into(42, &flags)
             .unwrap()
+            .0
             .unwrap();
         let output = tree(
             &metas,
@@ -739,6 +743,7 @@ mod tests {
             .unwrap()
             .recurse_into(1, &flags)
             .unwrap()
+            .0
             .unwrap();
         let output = grid(
             &metas,
@@ -771,6 +776,7 @@ mod tests {
             .unwrap()
             .recurse_into(1, &flags)
             .unwrap()
+            .0
             .unwrap();
         let output = grid(
             &metas,
diff --git a/src/main.rs b/src/main.rs
index e17567ab7..44829e301 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -42,6 +42,21 @@ use crate::core::Core;
 use crate::flags::Flags;
 use std::path::PathBuf;
 
+#[derive(PartialEq, Eq, PartialOrd, Copy, Clone)]
+pub enum ExitCode {
+    OK,
+    MinorIssue,
+    MajorIssue,
+}
+impl ExitCode {
+    pub fn set_if_greater(&mut self, code: ExitCode) {
+        let self_i32 = *self as i32;
+        let code_i32 = code as i32;
+        if self_i32 < code_i32 {
+            *self = code;
+        }
+    }
+}
 /// Macro used to avoid panicking when the lsd method is used with a pipe and
 /// stderr close before our program.
 #[macro_export]
@@ -115,5 +130,6 @@ fn main() {
     let flags = Flags::configure_from(&matches, &config).unwrap_or_else(|err| err.exit());
     let core = Core::new(flags);
 
-    core.run(inputs);
+    let exit_code = core.run(inputs);
+    std::process::exit(exit_code as i32);
 }
diff --git a/src/meta/mod.rs b/src/meta/mod.rs
index b83228bd2..20c2af348 100644
--- a/src/meta/mod.rs
+++ b/src/meta/mod.rs
@@ -27,7 +27,7 @@ pub use self::symlink::SymLink;
 pub use crate::icon::Icons;
 
 use crate::flags::{Display, Flags, Layout};
-use crate::print_error;
+use crate::{print_error, ExitCode};
 
 use std::io::{self, Error, ErrorKind};
 use std::path::{Component, Path, PathBuf};
@@ -50,30 +50,34 @@ pub struct Meta {
 }
 
 impl Meta {
-    pub fn recurse_into(&self, depth: usize, flags: &Flags) -> io::Result<Option<Vec<Meta>>> {
+    pub fn recurse_into(
+        &self,
+        depth: usize,
+        flags: &Flags,
+    ) -> io::Result<(Option<Vec<Meta>>, ExitCode)> {
         if depth == 0 {
-            return Ok(None);
+            return Ok((None, ExitCode::OK));
         }
 
         if flags.display == Display::DirectoryOnly && flags.layout != Layout::Tree {
-            return Ok(None);
+            return Ok((None, ExitCode::OK));
         }
 
         match self.file_type {
             FileType::Directory { .. } => (),
             FileType::SymLink { is_dir: true } => {
                 if flags.layout == Layout::OneLine {
-                    return Ok(None);
+                    return Ok((None, ExitCode::OK));
                 }
             }
-            _ => return Ok(None),
+            _ => return Ok((None, ExitCode::OK)),
         }
 
         let entries = match self.path.read_dir() {
             Ok(entries) => entries,
             Err(err) => {
                 print_error!("{}: {}.", self.path.display(), err);
-                return Ok(None);
+                return Ok((None, ExitCode::MinorIssue));
             }
         };
 
@@ -91,6 +95,8 @@ impl Meta {
             content.push(parent_meta);
         }
 
+        let mut exit_code = ExitCode::OK;
+
         for entry in entries {
             let entry = entry?;
             let path = entry.path();
@@ -111,6 +117,7 @@ impl Meta {
                 Ok(res) => res,
                 Err(err) => {
                     print_error!("{}: {}.", path.display(), err);
+                    exit_code.set_if_greater(ExitCode::MinorIssue);
                     continue;
                 }
             };
@@ -126,9 +133,13 @@ impl Meta {
             // check dereferencing
             if flags.dereference.0 || !matches!(entry_meta.file_type, FileType::SymLink { .. }) {
                 match entry_meta.recurse_into(depth - 1, flags) {
-                    Ok(content) => entry_meta.content = content,
+                    Ok((content, rec_exit_code)) => {
+                        entry_meta.content = content;
+                        exit_code.set_if_greater(rec_exit_code);
+                    }
                     Err(err) => {
                         print_error!("{}: {}.", path.display(), err);
+                        exit_code.set_if_greater(ExitCode::MinorIssue);
                         continue;
                     }
                 };
@@ -137,7 +148,7 @@ impl Meta {
             content.push(entry_meta);
         }
 
-        Ok(Some(content))
+        Ok((Some(content), exit_code))
     }
 
     pub fn calculate_total_size(&mut self) {


Test patch:
diff --git a/tests/integration.rs b/tests/integration.rs
index 19c4363a0..39e8e93e6 100644
--- a/tests/integration.rs
+++ b/tests/integration.rs
@@ -8,6 +8,8 @@ use std::process::Command;
 
 #[cfg(unix)]
 use std::os::unix::fs;
+#[cfg(unix)]
+use std::os::unix::fs::PermissionsExt;
 
 #[test]
 fn test_runs_okay() {
@@ -641,6 +643,45 @@ fn test_custom_config_file_parsing() {
         .stdout(predicate::str::is_match("folder\n└── file").unwrap());
 }
 
+#[test]
+fn test_cannot_access_file_exit_status() {
+    let dir = tempdir();
+    let does_not_exist = dir.path().join("does_not_exist");
+
+    let status = cmd()
+        .arg("-l")
+        .arg("--ignore-config")
+        .arg(does_not_exist)
+        .status()
+        .unwrap()
+        .code()
+        .unwrap();
+
+    assert_eq!(status, 2)
+}
+
+#[cfg(unix)]
+#[test]
+fn test_cannot_access_subdir_exit_status() {
+    let tmp = tempdir();
+
+    let readonly = std::fs::Permissions::from_mode(0o400);
+    tmp.child("d/subdir/onemore").create_dir_all().unwrap();
+
+    std::fs::set_permissions(tmp.child("d").path().join("subdir"), readonly).unwrap();
+
+    let status = cmd()
+        .arg("--tree")
+        .arg("--ignore-config")
+        .arg(tmp.child("d").path())
+        .status()
+        .unwrap()
+        .code()
+        .unwrap();
+
+    assert_eq!(status, 1)
+}
+
 #[test]
 fn test_date_custom_format_supports_nanos_with_length() {
     let dir = tempdir();