Repository: yshavit/mdq (rust)

Issue:
exit 1 if no items found
also, add `--quiet`/`-q` flag, similar to Grep's

Pull request description:
set exit code based on matches; add `--quiet`
resolves #129

Patch:
diff --git a/build.rs b/build.rs
index 000d369..9fc203a 100644
--- a/build.rs
+++ b/build.rs
@@ -113,6 +113,7 @@ struct TestGiven {
 struct TestExpect {
     cli_args: Vec<String>,
     output: String,
+    expect_success: Option<bool>,
 }
 
 impl TestSpecFile {
@@ -123,6 +124,7 @@ impl TestSpecFile {
                 case_name,
                 cli_args: test_expect.cli_args,
                 expect_output: test_expect.output,
+                expect_success: test_expect.expect_success.unwrap_or(true),
             })
         }
         results
@@ -134,6 +136,7 @@ struct Case {
     case_name: String,
     cli_args: Vec<String>,
     expect_output: String,
+    expect_success: bool,
 }
 
 impl Case {
@@ -161,6 +164,9 @@ impl Case {
                         }
                     });
                 }
+                out.write("expect_success: ")
+                    .write(&self.expect_success.to_string())
+                    .writeln(",");
                 out.write("md: MD,");
             });
             out.write("}.check();");
diff --git a/src/lib.rs b/src/lib.rs
index 8b51bc0..28249e4 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -63,6 +63,10 @@ pub struct Cli {
     /// The selectors string
     #[arg(group = "selectors_group", value_name = "selectors")]
     pub(crate) selectors: Option<String>,
+
+    /// Quiet: do not print anything to stdout. The exit code will still be 0 if-and-only-iff any elements match.
+    #[arg(long, short)]
+    pub(crate) quiet: bool,
 }
 
 #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
@@ -105,12 +109,12 @@ impl Cli {
     }
 }
 
-pub fn run_in_memory(cli: &Cli, contents: &str) -> Result<String, &'static str> {
+pub fn run_in_memory(cli: &Cli, contents: &str) -> (bool, String) {
     let mut out = Vec::with_capacity(256); // just a guess
 
-    run(&cli, contents.to_string(), || &mut out);
-    let out_str = String::from_utf8(out).map_err(|_| "UTF-8 decode error")?;
-    Ok(out_str)
+    let result = run(&cli, contents.to_string(), || &mut out);
+    let out_str = String::from_utf8(out).map_err(|_| "UTF-8 decode error").unwrap();
+    (result, out_str)
 }
 
 pub fn run_stdio(cli: &Cli) -> bool {
@@ -150,18 +154,22 @@ where
         },
     };
 
-    let mut stdout = get_out();
-    match cli.output {
-        OutputFormat::Markdown | OutputFormat::Md => {
-            let mut out = Output::new(Stream(&mut stdout));
-            fmt_md::write_md(&md_options, &mut out, pipeline_nodes.into_iter());
-        }
-        OutputFormat::Json => {
-            serde_json::to_writer(&mut stdout, &SerdeDoc::new(&pipeline_nodes, md_options.inline_options)).unwrap();
+    let found_any = !pipeline_nodes.is_empty();
+
+    if !cli.quiet {
+        let mut stdout = get_out();
+        match cli.output {
+            OutputFormat::Markdown | OutputFormat::Md => {
+                let mut out = Output::new(Stream(&mut stdout));
+                fmt_md::write_md(&md_options, &mut out, pipeline_nodes.into_iter());
+            }
+            OutputFormat::Json => {
+                serde_json::to_writer(&mut stdout, &SerdeDoc::new(&pipeline_nodes, md_options.inline_options)).unwrap();
+            }
         }
     }
 
-    true
+    found_any
 }
 
 fn print_select_parse_error(original_string: &str, err: ParseError) {


Test patch:
diff --git a/tests/integ_test.rs b/tests/integ_test.rs
index a8df509..1335bbf 100644
--- a/tests/integ_test.rs
+++ b/tests/integ_test.rs
@@ -5,14 +5,16 @@ struct Case<const N: usize> {
     cli_args: [&'static str; N],
     expect_output: &'static str,
     md: &'static str,
+    expect_success: bool,
 }
 
 impl<const N: usize> Case<N> {
     fn check(&self) {
         let all_cli_args = ["cmd"].iter().chain(&self.cli_args);
         let cli = mdq::Cli::try_parse_from(all_cli_args).unwrap();
-        let actual_out = mdq::run_in_memory(&cli, self.md).unwrap();
+        let (actual_success, actual_out) = mdq::run_in_memory(&cli, self.md);
         assert_eq!(actual_out, self.expect_output);
+        assert_eq!(actual_success, self.expect_success);
     }
 }
 
diff --git a/tests/md_cases/matchers.toml b/tests/md_cases/matchers.toml
index 1d2d19e..2353f37 100644
--- a/tests/md_cases/matchers.toml
+++ b/tests/md_cases/matchers.toml
@@ -46,6 +46,7 @@ output = '''
 [expect."quoted with anchor"]
 cli_args = ['- "world "$'] # nothing matches this
 output = ''
+expect_success = false
 
 
 [expect."regex"]
@@ -65,8 +66,10 @@ output = '''
 [expect."quoted is case-sensitive"]
 cli_args = ['- "OYSTER"']
 output = ''
+expect_success = false
 
 
 [expect."regex is case-sensitive"]
 cli_args = ['- /OYSTER/']
 output = ''
+expect_success = false
diff --git a/tests/md_cases/select_exit_code.toml b/tests/md_cases/select_exit_code.toml
new file mode 100644
index 0000000..8cef450
--- /dev/null
+++ b/tests/md_cases/select_exit_code.toml
@@ -0,0 +1,21 @@
+[given]
+md = '''
+[fizz](https://example.com/buzz)
+'''
+
+
+[expect."match and output"]
+cli_args = ["--link-format", "keep", "[]()"]
+output = '''
+[fizz](https://example.com/buzz)'''
+
+
+[expect."match quiet"]
+cli_args = ["--link-format", "keep", "[]()", "-q"]
+output = ''
+
+
+[expect."no match"]
+cli_args = ["--link-format", "keep", "[no match]()"]
+output = ''
+expect_success = false