Repository: dathere/qsv (language: rust)

Issue:
`stats`: does not handle directories with embedded spaces correctly when creating stats cache files
Hi @jqnatividad,
I have made an interesting observation. I use the MS Windows command line (cmd.exe).
The command does work if there are **no spaces** in the directory name, e,g. **C:\test\qsvtes**t
```
C:\test\qsvtest>type file.csv
col1,col2,col3
a,b,c
d,e,f
C:\test\qsvtest>qsv tojsonl file.csv -o file.jsonl
Enum list generated for field 'col1' (2 value/s)
Enum list generated for field 'col2' (2 value/s)
Enum list generated for field 'col3' (2 value/s)

C:\test\qsvtest>type file.jsonl
{"col1":"a","col2":"b","col3":"c"}
{"col1":"d","col2":"e","col3":"f"}
```
However, it does not work if there **are spaces** in the directory name, e.g. **C:\test\qsv test**
```
C:\test\qsv test>type file.csv
col1,col2,col3
a,b,c
d,e,f
C:\test\qsv test>qsv tojsonl file.csv -o file.jsonl
Failed to infer field types: qsv stats exited with code: 2
```
So this is an argument parsing issue, at least on MS Windows. Does it work on Linux?

_Originally posted by @datatraveller1 in https://github.com/jqnatividad/qsv/discussions/2290#discussioncomment-11279780_


Discussion / hints:

Fix patch:
diff --git a/src/util.rs b/src/util.rs
index 7a34f7a22..35bf70d8f 100644
--- a/src/util.rs
+++ b/src/util.rs
@@ -2017,57 +2017,60 @@ pub fn get_stats_records(
         } else {
             "-".to_string()
         };
+        // we do rustfmt::skip here as it was breaking the stats cmdline along strange
+        // boundaries, causing CI errors.
+        // This is because we're using tab characters (/t) to separate args to fix #2294,
+        #[rustfmt::skip]
         let mut stats_args_str = match mode {
             StatsMode::Schema => {
                 // mode is StatsMode::Schema
                 // we're generating schema, so we need cardinality and to infer-dates
                 format!(
-                    "stats {input} --infer-dates --dates-whitelist {dates_whitelist} --round 4 \
-                     --cardinality --stats-jsonl --force --output {tempfile_path}",
+                    "stats\t{input}\t--round\t4\t--cardinality\
+                    \t--infer-dates\t--dates-whitelist\t{dates_whitelist}\
+                    \t--stats-jsonl\t--force\t--output\t{tempfile_path}",
                     dates_whitelist = stats_args.flag_dates_whitelist
                 )
             },
             StatsMode::Frequency => {
                 // StatsMode::Frequency
                 // we're doing frequency, so we just need cardinality
-                format!("stats {input} --cardinality --stats-jsonl --output {tempfile_path}")
+                format!("stats\t{input}\t--cardinality\t--stats-jsonl\t--output\t{tempfile_path}")
             },
             StatsMode::FrequencyForceStats => {
                 // StatsMode::FrequencyForceStats
                 // we're doing frequency, so we need cardinality from a --forced stats run
                 format!(
-                    "stats {input} --cardinality --stats-jsonl --force --output {tempfile_path}"
+                    "stats\t{input}\t--cardinality\t--stats-jsonl\t--force\
+                    \t--output\t{tempfile_path}"
                 )
             },
             #[cfg(feature = "polars")]
             StatsMode::PolarsSchema => {
                 // StatsMode::PolarsSchema
                 // we need data types and ranges
-                format!("stats {input} --infer-boolean --stats-jsonl --output {tempfile_path}")
+                format!("stats\t{input}\t--infer-boolean\t--stats-jsonl\t--output\t{tempfile_path}")
             },
             StatsMode::None => unreachable!(), // we returned early on None earlier
         };
         if args.flag_prefer_dmy {
-            stats_args_str = format!("{stats_args_str} --prefer-dmy");
+            stats_args_str = format!("{stats_args_str}\t--prefer-dmy");
         }
         if args.flag_no_headers {
-            stats_args_str = format!("{stats_args_str} --no-headers");
+            stats_args_str = format!("{stats_args_str}\t--no-headers");
         }
         if let Some(delimiter) = args.flag_delimiter {
             let delim = delimiter.as_byte() as char;
-            stats_args_str = format!("{stats_args_str} --delimiter {delim}");
+            stats_args_str = format!("{stats_args_str}\t--delimiter\t{delim}");
         }
         if args.flag_memcheck {
-            stats_args_str = format!("{stats_args_str} --memcheck");
+            stats_args_str = format!("{stats_args_str}\t--memcheck");
         }
-        if let Some(mut jobs) = stats_args.flag_jobs {
-            if jobs > 2 {
-                jobs -= 1; // leave one core for the main thread
-            }
-            stats_args_str = format!("{stats_args_str} --jobs {jobs}");
+        if let Some(jobs) = stats_args.flag_jobs {
+            stats_args_str = format!("{stats_args_str}\t--jobs\t{jobs}");
         }
 
-        let stats_args_vec: Vec<&str> = stats_args_str.split_whitespace().collect();
+        let stats_args_vec: Vec<&str> = stats_args_str.split('\t').collect();
 
         let qsv_bin = std::env::current_exe().unwrap();
         let mut stats_cmd = std::process::Command::new(qsv_bin);


Test patch:
diff --git a/tests/test_tojsonl.rs b/tests/test_tojsonl.rs
index 6e9282f93..de3e79807 100644
--- a/tests/test_tojsonl.rs
+++ b/tests/test_tojsonl.rs
@@ -27,6 +27,31 @@ fn tojsonl_simple() {
     assert_eq!(got, expected);
 }
 
+#[test]
+#[serial]
+fn tojsonl_2294() {
+    let wrk = Workdir::new("tojsonl_simple");
+    wrk.create(
+        "file.csv",
+        vec![
+            svec!["col1", "col2", "col3"],
+            svec!["a", "b", "c"],
+            svec!["d", "e", "f"],
+        ],
+    );
+
+    wrk.create_subdir("qsv test").unwrap();
+    std::fs::rename(wrk.path("file.csv"), wrk.path("qsv test").join("file.csv")).unwrap();
+
+    let mut cmd = wrk.command("tojsonl");
+    cmd.arg("qsv test/file.csv");
+
+    let got: String = wrk.stdout(&mut cmd);
+    let expected = r#"{"col1":"a","col2":"b","col3":"c"}
+{"col1":"d","col2":"e","col3":"f"}"#;
+    assert_eq!(got, expected);
+}
+
 #[test]
 #[serial]
 fn tojsonl_boolean() {