about summary refs log tree commit diff
diff options
context:
space:
mode:
authorGuillaume Gomez <guillaume.gomez@huawei.com>2024-08-28 16:19:52 +0200
committerGuillaume Gomez <guillaume1.gomez@gmail.com>2024-12-20 22:35:00 +0100
commitcbb3df41fb48d0a5ebcdd0f4b44cc3ad6a5a837a (patch)
tree3199444a627b4b9b887efc765d96e43cc3abc022
parent24fafe7d14303643de24cc31bb6d1acc0568bcb5 (diff)
downloadrust-cbb3df41fb48d0a5ebcdd0f4b44cc3ad6a5a837a.tar.gz
rust-cbb3df41fb48d0a5ebcdd0f4b44cc3ad6a5a837a.zip
Split arguments from `--doctest-compilation-args` like a shell would
-rw-r--r--src/librustdoc/doctest.rs72
1 files changed, 64 insertions, 8 deletions
diff --git a/src/librustdoc/doctest.rs b/src/librustdoc/doctest.rs
index a53316dc6c1..a93e2f92718 100644
--- a/src/librustdoc/doctest.rs
+++ b/src/librustdoc/doctest.rs
@@ -50,6 +50,46 @@ pub(crate) struct GlobalTestOptions {
     pub(crate) args_file: PathBuf,
 }
 
+/// Function used to split command line arguments just like a shell would.
+fn split_args(args: &str) -> Vec<String> {
+    let mut out = Vec::new();
+    let mut iter = args.chars();
+    let mut current = String::new();
+
+    while let Some(c) = iter.next() {
+        if c == '\\' {
+            if let Some(c) = iter.next() {
+                // If it's escaped, even a quote or a whitespace will be ignored.
+                current.push(c);
+            }
+        } else if c == '"' || c == '\'' {
+            while let Some(new_c) = iter.next() {
+                if new_c == c {
+                    break;
+                } else if new_c == '\\' {
+                    if let Some(c) = iter.next() {
+                        // If it's escaped, even a quote will be ignored.
+                        current.push(c);
+                    }
+                } else {
+                    current.push(new_c);
+                }
+            }
+        } else if " \n\t\r".contains(c) {
+            if !current.is_empty() {
+                out.push(current.clone());
+                current.clear();
+            }
+        } else {
+            current.push(c);
+        }
+    }
+    if !current.is_empty() {
+        out.push(current);
+    }
+    out
+}
+
 pub(crate) fn generate_args_file(file_path: &Path, options: &RustdocOptions) -> Result<(), String> {
     let mut file = File::create(file_path)
         .map_err(|error| format!("failed to create args file: {error:?}"))?;
@@ -79,14 +119,7 @@ pub(crate) fn generate_args_file(file_path: &Path, options: &RustdocOptions) ->
     }
 
     for compilation_args in &options.doctest_compilation_args {
-        for flag in compilation_args
-            .split_whitespace()
-            .map(|flag| flag.trim())
-            .filter(|flag| !flag.is_empty())
-        {
-            // Very simple parsing implementation. Might be a good idea to correctly handle strings.
-            content.push(flag.to_string());
-        }
+        content.extend(split_args(compilation_args));
     }
 
     let content = content.join("\n");
@@ -1003,6 +1036,29 @@ fn doctest_run_fn(
     Ok(())
 }
 
+#[cfg(test)]
+#[test]
+fn check_split_args() {
+    fn compare(input: &str, expected: &[&str]) {
+        let output = split_args(input);
+        let expected = expected.iter().map(|s| s.to_string()).collect::<Vec<_>>();
+        assert_eq!(expected, output, "test failed for {input:?}");
+    }
+
+    compare("'a' \"b\"c", &["a", "bc"]);
+    compare("'a' \"b \"c d", &["a", "b c", "d"]);
+    compare("'a' \"b\\\"c\"", &["a", "b\"c"]);
+    compare("'a\"'", &["a\""]);
+    compare("\"a'\"", &["a'"]);
+    compare("\\ a", &[" a"]);
+    compare("\\\\", &["\\"]);
+    compare("a'", &["a"]);
+    compare("a          ", &["a"]);
+    compare("a          b", &["a", "b"]);
+    compare("a\n\t \rb", &["a", "b"]);
+    compare("a\n\t1 \rb", &["a", "1", "b"]);
+}
+
 #[cfg(test)] // used in tests
 impl DocTestVisitor for Vec<usize> {
     fn visit_test(&mut self, _test: String, _config: LangString, rel_line: MdRelLine) {