diff --git a/Cargo.lock b/Cargo.lock index cb19592..5ef7a22 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -132,7 +132,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -143,7 +143,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -552,7 +552,7 @@ version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -936,7 +936,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -1102,7 +1102,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -2760,6 +2760,7 @@ dependencies = [ "serde_norway", "serde_stacker", "sha2 0.11.0", + "shell-words", "ssh-key", "tokio", "tokio-tungstenite", @@ -3297,7 +3298,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -3354,7 +3355,7 @@ dependencies = [ "security-framework 3.7.0", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -3713,6 +3714,12 @@ dependencies = [ "keccak", ] +[[package]] +name = "shell-words" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77" + [[package]] name = "shlex" version = "1.3.0" @@ -3780,7 +3787,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -3874,7 +3881,7 @@ dependencies = [ "cfg-if", "libc", "psm", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -3954,10 +3961,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.2", "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -4568,7 +4575,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 9145aef..8836be5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -91,6 +91,9 @@ http = "1" # Error handling anyhow = "1" +# POSIX shell word splitting (used for alias expansion) +shell-words = "1" + # UUID parsing (version-matched to DD client) uuid = { version = "1", features = ["v4"], optional = true } diff --git a/src/commands/alias.rs b/src/commands/alias.rs index f0c3c66..fc216a0 100644 --- a/src/commands/alias.rs +++ b/src/commands/alias.rs @@ -87,7 +87,12 @@ fn apply_expansion(args: &[String], name: &str, aliases: &BTreeMap = expansion.split_whitespace().map(String::from).collect(); + // Use POSIX shell word splitting so quoted strings (e.g. --query "SELECT ...") are + // kept as a single token rather than split on interior whitespace. + let expansion_tokens = match shell_words::split(expansion) { + Ok(tokens) => tokens, + Err(_) => return args.to_vec(), + }; let Some(idx) = args.iter().position(|a| a == name) else { return args.to_vec(); }; @@ -189,6 +194,63 @@ mod tests { assert_eq!(result, args("pup --output json infrastructure hosts list")); } + #[test] + fn test_apply_expansion_preserves_quoted_spaces() { + // A quoted SQL string must arrive at clap as a single token, not split + // on the spaces inside the quotes. + let aliases = map(&[( + "host-count", + r#"ddsql table --query "SELECT COUNT(*) FROM dd.hosts""#, + )]); + let result = apply_expansion(&args("pup host-count"), "host-count", &aliases); + assert_eq!( + result, + vec![ + "pup", + "ddsql", + "table", + "--query", + "SELECT COUNT(*) FROM dd.hosts" + ] + .into_iter() + .map(String::from) + .collect::>() + ); + } + + #[test] + fn test_apply_expansion_preserves_multiline_quoted_query() { + // Newlines inside a double-quoted string must be preserved as part of + // the single --query token (mirrors the YAML |- block scalar case). + let aliases = map(&[( + "host-count", + "ddsql table --query \"SELECT COUNT(*)\nFROM dd.hosts\"", + )]); + let result = apply_expansion(&args("pup host-count"), "host-count", &aliases); + assert_eq!( + result, + vec![ + "pup", + "ddsql", + "table", + "--query", + "SELECT COUNT(*)\nFROM dd.hosts", + ] + .into_iter() + .map(String::from) + .collect::>() + ); + } + + #[test] + fn test_apply_expansion_unclosed_quote_returns_unchanged() { + // A malformed alias value (unclosed quote) must not panic or produce + // garbled args — fall back to the original args unchanged. + let aliases = map(&[("bad-alias", "ddsql table --query \"unclosed")]); + let result = apply_expansion(&args("pup bad-alias"), "bad-alias", &aliases); + assert_eq!(result, args("pup bad-alias")); + } + #[test] fn test_set_rejects_builtin_name() { let result = super::set(