about summary refs log tree commit diff
diff options
context:
space:
mode:
authorProfpatsch <mail@profpatsch.de>2024-03-03 17:15:39 +0100
committerProfpatsch <mail@profpatsch.de>2024-03-03 17:22:39 +0100
commit03ce9742fc6dd47cce89270d863a461e947e1fef (patch)
tree7c149d378d287e54efb90c3393f3eb489234b234
parentcc1a39da14c4f344a1ab625feb035e9535236d16 (diff)
pkgs/profpatsch/nman: print all command calls to stderr
Currently still unconditionally.
The commands are printed in a syntax that can be directly copied and
executed in bash.

The pretty-printing code is transformed form this Haskell function:

```haskell

-- | Simple escaping for bash words. If they contain anything that’s not ascii chars
-- and a bunch of often-used special characters, put the word in single quotes.
simpleBashEscape :: Text -> Text
simpleBashEscape t = do
  case Text.find (not . isSimple) t of
    Just _ -> escapeSingleQuote t
    Nothing -> t
  where
    -- any word that is just ascii characters is simple (no spaces or control characters)
    -- or contains a few often-used characters like - or .
    isSimple c =
      Char.isAsciiLower c
        || Char.isAsciiUpper c
        || Char.isDigit c
        -- These are benign, bash will not interpret them as special characters.
        || List.elem c ['-', '.', ':', '/']
    -- Put the word in single quotes
    -- If there is a single quote in the word,
    -- close the single quoted word, add a single quote, open the word again
    escapeSingleQuote t' = "'" <> Text.replace "'" "'\\''" t' <> "'"

```
-rw-r--r--pkgs/profpatsch/nman/nman.rs112
1 files changed, 98 insertions, 14 deletions
diff --git a/pkgs/profpatsch/nman/nman.rs b/pkgs/profpatsch/nman/nman.rs
index 53ff1403..db2bfe32 100644
--- a/pkgs/profpatsch/nman/nman.rs
+++ b/pkgs/profpatsch/nman/nman.rs
@@ -1,7 +1,9 @@
 extern crate temp;
 
+use std::borrow::Cow;
 use std::ffi::{OsStr, OsString};
 use std::fs::read_dir;
+use std::io::Write;
 use std::os::unix::ffi::OsStrExt;
 use std::os::unix::process::ExitStatusExt;
 use std::path::PathBuf;
@@ -240,14 +242,15 @@ fn open_man_page<'a>(attr: &'a str, section: Option<&'a str>, page: &'a str) ->
     // TODO(sterni): allow selecting other base package sets,
     //               like <vuizvui>, /home/lukas/src/nix/nixpkgs, …
     let expr = format!("with (import <nixpkgs> {{}}); builtins.map (o: {}.\"${{o}}\") {}.outputs", attr, attr);
-    let inst = Command::new("nix-instantiate")
-                       .arg("-E")
-                       .arg(expr)
-                       .arg("--add-root")
-                       .arg(tmpdir.as_ref().join("instantiation-result"))
-                       .arg("--indirect")
-                       .stderr(Stdio::inherit())
-                       .output()
+    let inst = debug_log_command(
+                        Command::new("nix-instantiate")
+                            .arg("-E")
+                            .arg(expr)
+                            .arg("--add-root")
+                            .arg(tmpdir.as_ref().join("instantiation-result"))
+                            .arg("--indirect")
+                            .stderr(Stdio::inherit()))
+                       .and_then(|cmd| cmd.output())
                        .map_err(|_| NmanError::Execution("nix-instantiate"))?;
 
     if !inst.status.success() {
@@ -277,9 +280,9 @@ fn open_man_page<'a>(attr: &'a str, section: Option<&'a str>, page: &'a str) ->
         match man_file {
             None => continue,
             Some(f) => {
-                let res = Command::new("man")
-                                  .arg("-l").arg(f)
-                                  .spawn()
+                let res = debug_log_command(Command::new("man")
+                                  .arg("-l").arg(f))
+                                  .and_then(|cmd| cmd.spawn())
                                   .and_then(|mut c| c.wait())
                                   .map(|c| c.success());
 
@@ -307,14 +310,15 @@ fn open_man_page<'a>(attr: &'a str, section: Option<&'a str>, page: &'a str) ->
 /// matches exist, the one with an alphanumerically lower section is preferred,
 /// e. g. section 1 is preferred over section 3.
 fn build_man_page<'a>(drv: DrvWithOutput, section: Option<&str>, page: &str, tempdir: &TempDir) -> Result<Option<PathBuf>, NmanError<'a>> {
-    let build = Command::new("nix-store")
+    let build = debug_log_command(
+                            Command::new("nix-store")
                             .arg("--realise")
                             .arg(drv.render())
                             .arg("--add-root")
                             .arg(tempdir.as_ref().join("build-result"))
                             .arg("--indirect")
-                            .stderr(Stdio::inherit())
-                            .output()
+                            .stderr(Stdio::inherit()))
+                            .and_then(|cmd| cmd.output())
                             .map_err(|_| NmanError::Execution("nix-store"))?;
 
     if !build.status.success() {
@@ -433,6 +437,73 @@ fn parse_man_section(section: &str) -> Result<&str, &str> {
     }
 }
 
+
+fn debug_log_command(cmd: &mut Command) -> Result<&mut Command, std::io::Error> {
+
+    let mut formatted = vec![b'$', b' '];
+    formatted.extend(
+        vec![cmd.get_program()]
+        .into_iter()
+        .chain(cmd.get_args())
+        .map(|arg| simple_bash_escape(arg.as_bytes()))
+        .collect::<Vec<_>>()
+        .join(&b' '));
+    formatted.push(b'\n');
+    std::io::stderr().write_all(&formatted).map(|()| cmd)
+}
+
+/// Simple escaping for bash words. If they contain anything that’s not ascii chars
+/// and a bunch of often-used special characters, put the word in single quotes.
+fn simple_bash_escape(arg: &[u8]) -> Cow<[u8]> {
+    let mut is_simple: bool = true;
+    let mut number_of_single_quotes: usize = 0;
+    // any word that is just ascii characters is simple (no spaces or control characters)
+    // or contains a few often-used characters like - or .
+    for c in arg {
+      if ! (c.is_ascii_alphabetic() || c.is_ascii_digit() || [b'-', b'.', b':', b'/'].contains(c)) {
+        is_simple = false;
+      }
+      if *c == b'\'' {
+        number_of_single_quotes += 1;
+      }
+    }
+    if is_simple {
+        return Cow::Borrowed(arg)
+    }
+    // Put the word in single quotes
+    // If there is a single quote in the word,
+    // close the single quoted word, add a single quote, open the word again
+    // replace single quotes with `'\''` (i.e. escape from the string, then add a `'`, then open another string)
+    if number_of_single_quotes > 0 {
+        // we know the capacity we need to build the string, so vec will only allocate once
+        let mut v = Vec::with_capacity(get_bash_escaped_capacity(arg, number_of_single_quotes));
+        v.push(b'\'');
+        for c in arg {
+            if *c == b'\'' {
+                v.extend(b"'\\''")
+            } else {
+                v.push(*c)
+            }
+        }
+        v.push(b'\'');
+        return Cow::Owned(v);
+    }
+
+    let mut v = Vec::with_capacity(arg.len() +2);
+    v.push(b'\'');
+    v.extend(arg);
+    v.push(b'\'');
+    return Cow::Owned(v);
+}
+
+fn get_bash_escaped_capacity(arg: &[u8], number_of_single_quotes: usize) -> usize {
+    arg.len()
+    // initial `'` and final `'`
+    + 2
+    // replacing `'` with `'\''` adds three bytes
+    + number_of_single_quotes * 3
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
@@ -533,4 +604,17 @@ mod tests {
         assert!(match_man_page_file("lowdown.", "", "lowdown"));
         assert!(!match_man_page_file("", "", ""));
     }
+
+    #[test]
+    fn test_simple_bash_escape() {
+        assert_eq!(&simple_bash_escape(b""), &vec![], "empty string");
+        assert_eq!(&simple_bash_escape(b"abc"), &"abc".as_bytes(), "simple word");
+        assert_eq!(&simple_bash_escape(b"12ab3c4"), &"12ab3c4".as_bytes(), "simple word with digits");
+        assert_eq!(&simple_bash_escape(b"a-b.c:d/e"), &"a-b.c:d/e".as_bytes(), "simple word with allowed special chars");
+        assert_eq!(&simple_bash_escape("a$bc€de".as_bytes()), &"'a$bc€de'".as_bytes(), "escaped word with special chars");
+        assert_eq!(&simple_bash_escape("a'bc'".as_bytes()), &"'a'\\''bc'\\'''".as_bytes(), "escaped word with single quotes");
+        assert_eq!(&simple_bash_escape("a'bc'".as_bytes()), &"'a'\\''bc'\\'''".as_bytes(), "escaped word with single quotes");
+        assert_eq!(get_bash_escaped_capacity("a'bc'".as_bytes(), 2), 13, "escaped vec capacity is correct");
+
+    }
 }