diff --git a/examples/multiline-fn.ilo b/examples/multiline-fn.ilo index 3e858c88..7be89f33 100644 --- a/examples/multiline-fn.ilo +++ b/examples/multiline-fn.ilo @@ -10,6 +10,6 @@ nums>L n [1,2,3] -- run: greet "world" --- out: ~hello "world" +-- out: ~hello world -- run: nums -- out: [1, 2, 3] diff --git a/examples/rsrt.ilo b/examples/rsrt.ilo index 86516cc0..96fcec93 100644 --- a/examples/rsrt.ilo +++ b/examples/rsrt.ilo @@ -10,4 +10,4 @@ top-words ws:L t>L t;rsrt ws -- run: top-nums [3,1,4,1,5,9,2,6] -- out: [9, 6, 5, 4, 3, 2, 1, 1] -- run: top-words ["banana","apple","cherry"] --- out: ["cherry", "banana", "apple"] +-- out: [cherry, banana, apple] diff --git a/examples/sort-by-key.ilo b/examples/sort-by-key.ilo index a4db3404..e72188e6 100644 --- a/examples/sort-by-key.ilo +++ b/examples/sort-by-key.ilo @@ -17,6 +17,6 @@ by-len-desc ws:L t>L t;rev (srt wlen ws) -- run: by-dist [-3,1,-5,2] -- out: [1, 2, -3, -5] -- run: by-len ["banana","fig","apple","kiwi"] --- out: ["fig", "kiwi", "apple", "banana"] +-- out: [fig, kiwi, apple, banana] -- run: by-len-desc ["banana","fig","apple","kiwi"] --- out: ["banana", "apple", "kiwi", "fig"] +-- out: [banana, apple, kiwi, fig] diff --git a/src/main.rs b/src/main.rs index 5816df2a..83836077 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3269,22 +3269,29 @@ print(f"__NS__={{_per}}") } fn parse_cli_arg(s: &str) -> interpreter::Value { - // Bracketed list: [1,2,3] or [] + // Bracketed list: [1,2,3], ["apple","ant"], [] — split top-level commas, + // honoring quoted strings so commas inside strings don't split. if s.starts_with('[') && s.ends_with(']') { let inner = s[1..s.len() - 1].trim(); if inner.is_empty() { return interpreter::Value::List(vec![]); } - let items = inner - .split(',') + let items = split_top_level_commas(inner) + .into_iter() .map(|part| parse_cli_arg(part.trim())) .collect(); return interpreter::Value::List(items); } + // Quoted string: strip surrounding double quotes and treat as text. + // This preserves the raw contents (no escape decoding) which mirrors how + // the rest of the CLI passes bare text. + if s.len() >= 2 && s.starts_with('"') && s.ends_with('"') { + return interpreter::Value::Text(s[1..s.len() - 1].to_string()); + } // Bare comma list: 1,2,3 if s.contains(',') { - let items = s - .split(',') + let items = split_top_level_commas(s) + .into_iter() .map(|part| parse_cli_arg(part.trim())) .collect(); return interpreter::Value::List(items); @@ -3306,6 +3313,49 @@ fn parse_cli_arg(s: &str) -> interpreter::Value { } } +/// Split on commas, but ignore commas inside double-quoted strings or nested +/// brackets so list args like `["a,b","c"]` and `[[1,2],[3,4]]` parse correctly. +fn split_top_level_commas(s: &str) -> Vec { + let mut out = Vec::new(); + let mut cur = String::new(); + let mut depth = 0i32; + let mut in_str = false; + let mut prev_escape = false; + for ch in s.chars() { + if in_str { + cur.push(ch); + if prev_escape { + prev_escape = false; + } else if ch == '\\' { + prev_escape = true; + } else if ch == '"' { + in_str = false; + } + continue; + } + match ch { + '"' => { + in_str = true; + cur.push(ch); + } + '[' => { + depth += 1; + cur.push(ch); + } + ']' => { + depth -= 1; + cur.push(ch); + } + ',' if depth == 0 => { + out.push(std::mem::take(&mut cur)); + } + _ => cur.push(ch), + } + } + out.push(cur); + out +} + /// Coerce CLI args to match a function's parameter types. /// - Wraps a non-list value in `[value]` when the param type is `L _` fn coerce_cli_args( diff --git a/tests/regression_uniqby.rs b/tests/regression_uniqby.rs index 85047f4e..c2e33317 100644 --- a/tests/regression_uniqby.rs +++ b/tests/regression_uniqby.rs @@ -98,8 +98,7 @@ fn uniqby_all_same_key_keeps_first_tree() { // ── Order preservation: first-seen wins, original order preserved ────────── -const PARITY_SRC: &str = - "par n:n>t\n==(mod n 2) 0{ret \"even\"}\n\"odd\"\nf xs:L n>L n;uniqby par xs"; +const PARITY_SRC: &str = "par n:n>t;?=(mod n 2) 0 \"even\" \"odd\"\nf xs:L n>L n;uniqby par xs"; #[test] fn uniqby_preserves_order_tree() {