chore(release): prepare public source release

This commit is contained in:
MercuryToolbox Release
2026-07-18 15:33:01 +08:00
commit 34d6a57f38
510 changed files with 163501 additions and 0 deletions
+26
View File
@@ -0,0 +1,26 @@
[package]
name = "argv"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
authors.workspace = true
readme.workspace = true
publish.workspace = true
description = "Inspect argv and optional stdin line payloads for shell quoting/debug workflows."
keywords.workspace = true
categories.workspace = true
[lints]
workspace = true
[dependencies]
common = { path = "../common", default-features = false }
lexopt.workspace = true
runtimekit = { path = "../runtimekit" }
serde.workspace = true
serde_json.workspace = true
[dev-dependencies]
assert_cmd.workspace = true
predicates.workspace = true
+922
View File
@@ -0,0 +1,922 @@
//! The `argv` command renders shell-safe argument text and verifies real argv round-trips.
use std::ffi::OsString;
use std::fs::{self, OpenOptions};
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::{SystemTime, UNIX_EPOCH};
use common::{
CliError, CommonArgs, ExitCode, RenderMode, parse_color_choice, parse_format_choice,
print_json, print_quick_help_error, print_structured, print_text,
};
use lexopt::prelude::{Long, Short, Value as ArgValue};
use runtimekit::{ShellMode, collect_command_values, parse_shell_mode, parser_value_string};
use serde::{Deserialize, Serialize};
const HELP: &str = "\
Quote argument vectors for Windows shells and inspect what a shell really passed to a native process.
Usage:
argv quote --shell <pwsh|cmd|raw> [VALUE...]
argv inspect [--json] --shell <pwsh|cmd|raw> [VALUE...]
Options:
--format <FORMAT> Structured output format: text, json, toon
--json Shortcut for --format json
--toon Shortcut for --format toon
--color <WHEN> Control ANSI color output: auto, never
--quiet Suppress non-essential status output
-h, --help Show this help text
-V, --version Show the command version
Notes:
Windows-first: quote and inspect focus on native invocation behavior for pwsh, cmd, and raw process launches.
cmd inspect is Windows-only.
quote accepts positional args or a JSON array of strings from stdin when no positional args are provided.
raw bypasses any shell and shows the direct native-process argv path.
Examples:
argv quote --shell pwsh -- git commit -m \"two words\"
'[\"tool.exe\",\"two words\"]' | argv quote --shell cmd
argv inspect --json --shell pwsh -- alpha \"two words\"
";
const QUOTE_HELP: &str = "\
Render a shell-safe invocation string for pwsh, cmd, or raw native argv.
Usage:
argv quote --shell <pwsh|cmd|raw> [VALUE...]
Options:
--format <FORMAT> Structured output format: text, json, toon
--json Shortcut for --format json
--toon Shortcut for --format toon
--color <WHEN> Control ANSI color output: auto, never
--quiet Suppress non-essential status output
--shell <SHELL> Target shell: pwsh, cmd, raw
-h, --help Show this help text
Examples:
argv quote --shell pwsh -- git commit -m \"two words\"
'[\"tool.exe\",\"two words\"]' | argv quote --shell cmd
";
const INSPECT_HELP: &str = "\
Launch a native helper and show what argv actually arrived after shell parsing.
Usage:
argv inspect [--json] --shell <pwsh|cmd|raw> [VALUE...]
Options:
--format <FORMAT> Structured output format: text, json, toon
--json Shortcut for --format json
--toon Shortcut for --format toon
--color <WHEN> Control ANSI color output: auto, never
--quiet Suppress non-essential status output
--shell <SHELL> Target shell: pwsh, cmd, raw
-h, --help Show this help text
Examples:
argv inspect --shell pwsh -- alpha \"two words\"
argv inspect --json --shell cmd -- alpha \"--filter=x|y\"
";
const HELPER_SUBCOMMAND: &str = "__native_helper";
#[derive(Debug, Clone)]
struct Cli {
common: CommonArgs,
command: CommandMode,
}
#[derive(Debug, Clone)]
enum CommandMode {
Help(&'static str),
Quote {
shell: ShellMode,
values: Vec<OsString>,
},
Inspect {
shell: ShellMode,
values: Vec<OsString>,
},
NativeHelper {
values: Vec<String>,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ParseOutcome {
Help,
Version,
Run,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
struct QuoteReport {
shell: String,
text: String,
argv: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
struct InspectReport {
shell: String,
invocation: String,
probe_invocation: Option<String>,
count: usize,
argv: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
struct HelperReport {
count: usize,
argv: Vec<String>,
}
/// Parses CLI arguments and returns a process exit code.
#[must_use]
pub fn main_entry() -> i32 {
match parse_cli_from(std::env::args_os()) {
Ok((ParseOutcome::Help, _)) => {
print!("{HELP}");
ExitCode::Success.as_i32()
}
Ok((ParseOutcome::Version, _)) => {
println!("argv {}", env!("CARGO_PKG_VERSION"));
ExitCode::Success.as_i32()
}
Ok((ParseOutcome::Run, cli)) => match run(&cli) {
Ok(code) => code.as_i32(),
Err(error) => {
print_quick_help_error(&error, HELP);
error.exit_code().as_i32()
}
},
Err(error) => {
print_quick_help_error(&error, HELP);
error.exit_code().as_i32()
}
}
}
fn parse_cli_from<I, T>(args: I) -> Result<(ParseOutcome, Cli), CliError>
where
I: IntoIterator<Item = T>,
T: Into<OsString>,
{
let mut parser = lexopt::Parser::from_iter(args);
let mut cli = Cli {
common: CommonArgs::default(),
command: CommandMode::Quote {
shell: ShellMode::Raw,
values: Vec::new(),
},
};
while let Some(argument) = parser
.next()
.map_err(|error| CliError::usage(error.to_string()))?
{
match argument {
Long("help") | Short('h') => return Ok((ParseOutcome::Help, cli)),
Long("version") | Short('V') => return Ok((ParseOutcome::Version, cli)),
Long("json") => cli.common.set_render_mode(RenderMode::Json),
Long("toon") => cli.common.set_render_mode(RenderMode::Toon),
Long("format") => {
let value = parser_value_string(&mut parser, "--format")?;
cli.common.set_render_mode(parse_format_choice(&value)?);
}
Long("quiet") => cli.common.quiet = true,
Long("color") => {
cli.common.color =
parse_color_choice(&parser_value_string(&mut parser, "--color")?)?;
}
ArgValue(value) => {
let token = runtimekit::os_to_utf8(value, "subcommand")?;
cli.command = match token.as_str() {
"quote" => parse_quote_command(&mut parser, &mut cli.common)?,
"inspect" => parse_inspect_command(&mut parser, &mut cli.common)?,
HELPER_SUBCOMMAND => parse_native_helper_command(&mut parser)?,
_ => {
return Err(CliError::usage(
"unknown subcommand; expected quote or inspect",
));
}
};
return Ok((ParseOutcome::Run, cli));
}
_ => {
return Err(CliError::usage(
"unsupported argument; use --help to see available options",
));
}
}
}
Err(CliError::usage("provide a subcommand: quote or inspect"))
}
fn parse_quote_command(
parser: &mut lexopt::Parser,
common: &mut CommonArgs,
) -> Result<CommandMode, CliError> {
let mut shell = None::<ShellMode>;
let mut values = Vec::new();
while let Some(argument) = parser
.next()
.map_err(|error| CliError::usage(error.to_string()))?
{
match argument {
Long("help") | Short('h') => return Ok(CommandMode::Help(QUOTE_HELP)),
Long("json") => common.set_render_mode(RenderMode::Json),
Long("toon") => common.set_render_mode(RenderMode::Toon),
Long("format") => {
let value = parser_value_string(parser, "--format")?;
common.set_render_mode(parse_format_choice(&value)?);
}
Long("quiet") => common.quiet = true,
Long("color") => {
common.color = parse_color_choice(&parser_value_string(parser, "--color")?)?;
}
Long("shell") => {
shell = Some(parse_shell_mode(
"--shell",
&parser_value_string(parser, "--shell")?,
)?);
}
ArgValue(value) => {
values = collect_command_values(parser, value)?;
break;
}
_ => {
return Err(CliError::usage(
"unsupported quote argument; use --help to see available options",
));
}
}
}
Ok(CommandMode::Quote {
shell: shell.ok_or_else(|| CliError::usage("quote requires --shell <pwsh|cmd|raw>"))?,
values,
})
}
fn parse_inspect_command(
parser: &mut lexopt::Parser,
common: &mut CommonArgs,
) -> Result<CommandMode, CliError> {
let mut shell = None::<ShellMode>;
let mut values = Vec::new();
while let Some(argument) = parser
.next()
.map_err(|error| CliError::usage(error.to_string()))?
{
match argument {
Long("help") | Short('h') => return Ok(CommandMode::Help(INSPECT_HELP)),
Long("json") => common.set_render_mode(RenderMode::Json),
Long("toon") => common.set_render_mode(RenderMode::Toon),
Long("format") => {
let value = parser_value_string(parser, "--format")?;
common.set_render_mode(parse_format_choice(&value)?);
}
Long("quiet") => common.quiet = true,
Long("color") => {
common.color = parse_color_choice(&parser_value_string(parser, "--color")?)?;
}
Long("shell") => {
shell = Some(parse_shell_mode(
"--shell",
&parser_value_string(parser, "--shell")?,
)?);
}
ArgValue(value) => {
values = collect_command_values(parser, value)?;
break;
}
_ => {
return Err(CliError::usage(
"unsupported inspect argument; use --help to see available options",
));
}
}
}
Ok(CommandMode::Inspect {
shell: shell.ok_or_else(|| CliError::usage("inspect requires --shell <pwsh|cmd|raw>"))?,
values,
})
}
fn parse_native_helper_command(parser: &mut lexopt::Parser) -> Result<CommandMode, CliError> {
let mut values = Vec::new();
while let Some(argument) = parser
.next()
.map_err(|error| CliError::usage(error.to_string()))?
{
match argument {
ArgValue(value) => values.push(runtimekit::os_to_utf8(value, "helper arg")?),
_ => {
return Err(CliError::usage(
"internal helper expects only positional values",
));
}
}
}
Ok(CommandMode::NativeHelper { values })
}
fn run(cli: &Cli) -> Result<ExitCode, CliError> {
match &cli.command {
CommandMode::Help(text) => {
print_text(text)?;
Ok(ExitCode::Success)
}
CommandMode::Quote { shell, values } => run_quote(cli, *shell, values),
CommandMode::Inspect { shell, values } => run_inspect(cli, *shell, values),
CommandMode::NativeHelper { values } => run_native_helper(values),
}
}
fn run_quote(cli: &Cli, shell: ShellMode, values: &[OsString]) -> Result<ExitCode, CliError> {
let argv = resolve_values(cli, values)?;
let report = QuoteReport {
shell: shell_label(shell).to_string(),
text: render_shell_invocation(shell, &argv),
argv,
};
match cli.common.render_mode() {
RenderMode::Json => print_json(&report)?,
RenderMode::Toon => print_structured(&report, RenderMode::Toon)?,
RenderMode::Text => print_text(&report.text)?,
}
Ok(ExitCode::Success)
}
fn run_inspect(cli: &Cli, shell: ShellMode, values: &[OsString]) -> Result<ExitCode, CliError> {
let argv = resolve_values(cli, values)?;
let inspection = inspect_arguments(shell, &argv)?;
let report = InspectReport {
shell: shell_label(shell).to_string(),
invocation: inspection.invocation,
probe_invocation: Some(inspection.probe_invocation),
count: inspection.helper.argv.len(),
argv: inspection.helper.argv,
};
match cli.common.render_mode() {
RenderMode::Json => print_json(&report)?,
RenderMode::Toon => print_structured(&report, RenderMode::Toon)?,
RenderMode::Text => print_text(render_inspect_text(&report))?,
}
Ok(ExitCode::Success)
}
fn run_native_helper(values: &[String]) -> Result<ExitCode, CliError> {
let report = HelperReport {
count: values.len(),
argv: values.to_vec(),
};
print_json(&report)?;
Ok(ExitCode::Success)
}
fn resolve_values(cli: &Cli, values: &[OsString]) -> Result<Vec<String>, CliError> {
if !values.is_empty() {
return values
.iter()
.cloned()
.map(|value| runtimekit::os_to_utf8(value, "argv value"))
.collect();
}
if cli.common.stdin_is_terminal() {
return Err(CliError::usage(
"provide positional values or pipe a JSON array of strings into stdin",
));
}
let mut buffer = String::new();
std::io::stdin()
.read_to_string(&mut buffer)
.map_err(|error| CliError::runtime(format!("failed to read stdin: {error}")))?;
let trimmed = buffer.trim();
if trimmed.is_empty() {
return Err(CliError::usage(
"stdin was empty; expected a JSON array of strings",
));
}
serde_json::from_str::<Vec<String>>(trimmed).map_err(|error| {
CliError::usage(format!(
"expected a JSON array of strings on stdin, got parse error: {error}"
))
})
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct InspectionResult {
invocation: String,
probe_invocation: String,
helper: HelperReport,
}
fn inspect_arguments(shell: ShellMode, values: &[String]) -> Result<InspectionResult, CliError> {
let current_exe = std::env::current_exe().map_err(|error| {
CliError::runtime(format!("failed to resolve current executable: {error}"))
})?;
let helper_exe = current_exe.display().to_string();
let mut helper_argv = vec![helper_exe, HELPER_SUBCOMMAND.to_string(), "--".to_string()];
helper_argv.extend(values.iter().cloned());
let invocation = render_shell_invocation(shell, values);
let probe_invocation = render_shell_invocation(shell, &helper_argv);
let output = match shell {
ShellMode::Raw => Command::new(&current_exe)
.arg(HELPER_SUBCOMMAND)
.arg("--")
.args(values)
.output()
.map_err(|error| CliError::runtime(format!("failed to launch raw helper: {error}")))?,
ShellMode::Pwsh => Command::new("pwsh")
.arg("-NoProfile")
.arg("-Command")
.arg(&probe_invocation)
.output()
.map_err(|error| CliError::runtime(format!("failed to launch pwsh helper: {error}")))?,
ShellMode::Cmd => {
if !cfg!(windows) {
return Err(CliError::usage("cmd inspect is Windows-only"));
}
let wrapper = unique_cmd_wrapper_path();
let wrapper_body = format!(
"@echo off\r\nsetlocal DisableDelayedExpansion\r\n{}\r\n",
probe_invocation.replace('%', "%%")
);
write_cmd_wrapper_file(&wrapper, &wrapper_body)?;
let output = Command::new("cmd")
.arg("/d")
.arg("/v:off")
.arg("/c")
.arg(&wrapper)
.output()
.map_err(|error| {
CliError::runtime(format!("failed to launch cmd helper: {error}"))
});
let _ = fs::remove_file(&wrapper);
output?
}
};
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(CliError::runtime(format!(
"native helper failed with status {}: {}",
output.status,
stderr.trim()
)));
}
let helper = serde_json::from_slice::<HelperReport>(&output.stdout)
.map_err(|error| CliError::runtime(format!("failed to parse helper output: {error}")))?;
Ok(InspectionResult {
invocation,
probe_invocation,
helper,
})
}
fn render_inspect_text(report: &InspectReport) -> String {
let mut lines = vec![format!(
"shell={} count={} invocation={}",
report.shell, report.count, report.invocation
)];
lines.extend(
report
.argv
.iter()
.enumerate()
.map(|(index, value)| format!("argv[{index}]={}", quote_text(value))),
);
lines.join("\n")
}
fn render_shell_invocation(shell: ShellMode, values: &[String]) -> String {
match shell {
ShellMode::Pwsh => {
let mut parts = vec!["&".to_string()];
parts.extend(values.iter().map(|value| quote_pwsh(value)));
parts.join(" ")
}
ShellMode::Cmd => values
.iter()
.map(|value| quote_cmd(value))
.collect::<Vec<_>>()
.join(" "),
ShellMode::Raw => values
.iter()
.map(|value| quote_raw(value))
.collect::<Vec<_>>()
.join(" "),
}
}
fn quote_pwsh(value: &str) -> String {
format!("'{}'", value.replace('\'', "''"))
}
fn quote_cmd(value: &str) -> String {
if value.is_empty()
|| value
.chars()
.any(|ch| ch.is_whitespace() || "\"^&|<>()%!".contains(ch))
{
quote_windows_arg_inner(value, true)
} else {
value.to_string()
}
}
fn quote_raw(value: &str) -> String {
quote_windows_arg(value)
}
fn quote_windows_arg(value: &str) -> String {
quote_windows_arg_inner(value, false)
}
fn quote_windows_arg_inner(value: &str, force_quote: bool) -> String {
if force_quote || value.is_empty() || value.chars().any(|ch| ch.is_whitespace() || ch == '"') {
let mut rendered = String::from("\"");
let mut backslashes = 0_usize;
for ch in value.chars() {
match ch {
'\\' => backslashes += 1,
'"' => {
rendered.push_str(&"\\".repeat(backslashes * 2 + 1));
rendered.push('"');
backslashes = 0;
}
_ => {
rendered.push_str(&"\\".repeat(backslashes));
backslashes = 0;
rendered.push(ch);
}
}
}
rendered.push_str(&"\\".repeat(backslashes * 2));
rendered.push('"');
rendered
} else {
value.to_string()
}
}
fn quote_text(value: &str) -> String {
if value.is_empty() || value.chars().any(char::is_whitespace) {
format!("\"{}\"", value.replace('"', "\\\""))
} else {
value.to_string()
}
}
const fn shell_label(shell: ShellMode) -> &'static str {
match shell {
ShellMode::Raw => "raw",
ShellMode::Pwsh => "pwsh",
ShellMode::Cmd => "cmd",
}
}
fn unique_cmd_wrapper_path() -> PathBuf {
let unique = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_or(0, |value| value.as_nanos());
std::env::temp_dir().join(format!(
"mercury-argv-helper-{}-{unique}.cmd",
std::process::id()
))
}
fn write_cmd_wrapper_file(path: &Path, body: &str) -> Result<(), CliError> {
let mut file = OpenOptions::new()
.create_new(true)
.write(true)
.open(path)
.map_err(|error| {
CliError::runtime(format!(
"refusing to replace existing cmd inspect wrapper {}: {error}",
path.display()
))
})?;
file.write_all(body.as_bytes()).map_err(|error| {
CliError::runtime(format!("failed to write {}: {error}", path.display()))
})?;
file.flush()
.map_err(|error| CliError::runtime(format!("failed to flush {}: {error}", path.display())))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parser_accepts_quote_and_inspect_commands() {
let (_, quote_cli) = parse_cli_from([
"argv",
"quote",
"--shell",
"pwsh",
"--",
"tool",
"two words",
])
.expect("quote cli");
assert!(matches!(
quote_cli.command,
CommandMode::Quote {
shell: ShellMode::Pwsh,
..
}
));
let (_, inspect_cli) =
parse_cli_from(["argv", "--json", "inspect", "--shell", "raw", "--", "alpha"])
.expect("inspect cli");
assert!(inspect_cli.common.json);
assert!(matches!(
inspect_cli.command,
CommandMode::Inspect {
shell: ShellMode::Raw,
..
}
));
let (_, inspect_help) =
parse_cli_from(["argv", "inspect", "--help"]).expect("inspect help");
assert!(matches!(
inspect_help.command,
CommandMode::Help(text) if text.contains("argv inspect")
));
}
#[test]
fn parser_covers_global_and_subcommand_common_flags() {
let (_, quote_cli) = parse_cli_from([
"argv", "--quiet", "--color", "never", "quote", "--json", "--quiet", "--color",
"never", "--shell", "cmd", "--", "tool.exe",
])
.expect("quote cli");
assert!(quote_cli.common.json);
assert!(quote_cli.common.quiet);
assert_eq!(quote_cli.common.color, common::ColorChoice::Never);
assert!(matches!(
quote_cli.command,
CommandMode::Quote {
shell: ShellMode::Cmd,
..
}
));
let (_, inspect_cli) = parse_cli_from([
"argv", "inspect", "--quiet", "--color", "never", "--shell", "pwsh", "--", "alpha",
])
.expect("inspect cli");
assert!(inspect_cli.common.quiet);
assert_eq!(inspect_cli.common.color, common::ColorChoice::Never);
assert!(matches!(
inspect_cli.command,
CommandMode::Inspect {
shell: ShellMode::Pwsh,
..
}
));
}
#[test]
fn quote_rendering_is_shell_aware() {
assert_eq!(
render_shell_invocation(
ShellMode::Pwsh,
&["tool".to_string(), "two words".to_string()]
),
"& 'tool' 'two words'"
);
assert_eq!(
render_shell_invocation(
ShellMode::Cmd,
&["tool.exe".to_string(), "two words".to_string()]
),
"tool.exe \"two words\""
);
assert_eq!(quote_pwsh("a'b"), "'a''b'");
}
#[test]
fn helper_text_output_is_compact() {
let report = InspectReport {
shell: "raw".to_string(),
invocation: "alpha".to_string(),
probe_invocation: Some("tool.exe __native_helper -- alpha".to_string()),
count: 2,
argv: vec!["alpha".to_string(), "two words".to_string()],
};
let text = render_inspect_text(&report);
assert!(text.contains("shell=raw count=2"));
assert!(text.contains("argv[1]=\"two words\""));
}
#[test]
fn native_helper_parser_rejects_flags_and_quote_rendering_covers_raw() {
assert!(matches!(
parse_cli_from(["argv", HELPER_SUBCOMMAND, "--json"]),
Err(CliError::Usage(message)) if message.contains("only positional values")
));
assert_eq!(
render_shell_invocation(
ShellMode::Raw,
&["tool.exe".to_string(), "two words".to_string()]
),
"tool.exe \"two words\""
);
assert_eq!(quote_raw(""), "\"\"");
assert_eq!(quote_text("needs quotes"), "\"needs quotes\"");
}
#[test]
fn parser_and_helper_paths_reject_invalid_arguments() {
assert!(matches!(
parse_cli_from(["argv"]),
Err(CliError::Usage(message)) if message.contains("provide a subcommand")
));
assert!(matches!(
parse_cli_from(["argv", "--bogus"]),
Err(CliError::Usage(message)) if message.contains("unsupported argument")
));
assert!(matches!(
parse_cli_from(["argv", "quote", "--shell", "pwsh", "--bogus"]),
Err(CliError::Usage(message)) if message.contains("unsupported quote argument")
));
assert!(matches!(
parse_cli_from(["argv", "inspect", "--shell", "raw", "--bogus"]),
Err(CliError::Usage(message)) if message.contains("unsupported inspect argument")
));
let (_, helper_cli) =
parse_cli_from(["argv", HELPER_SUBCOMMAND, "alpha", "two words"]).expect("helper cli");
assert!(matches!(
helper_cli.command,
CommandMode::NativeHelper { values } if values == vec!["alpha".to_string(), "two words".to_string()]
));
}
#[test]
fn parser_rejects_unknown_subcommands_and_missing_shell_flags() {
assert!(matches!(
parse_cli_from(["argv", "wat"]),
Err(CliError::Usage(message)) if message.contains("expected quote or inspect")
));
assert!(matches!(
parse_cli_from(["argv", "quote", "--", "tool.exe"]),
Err(CliError::Usage(message)) if message.contains("quote requires --shell")
));
assert!(matches!(
parse_cli_from(["argv", "inspect", "--", "tool.exe"]),
Err(CliError::Usage(message)) if message.contains("inspect requires --shell")
));
}
#[test]
fn quote_helpers_cover_metacharacters_backslashes_and_empty_values() {
assert_eq!(quote_cmd("a&b"), "\"a&b\"");
assert_eq!(quote_cmd("two words"), "\"two words\"");
assert_eq!(quote_windows_arg("two words\\"), "\"two words\\\\\"");
assert_eq!(quote_windows_arg("say \"hi\""), "\"say \\\"hi\\\"\"");
assert_eq!(quote_windows_arg("plain"), "plain");
assert_eq!(quote_text(""), "\"\"");
assert_eq!(shell_label(ShellMode::Cmd), "cmd");
}
#[test]
fn run_and_resolve_values_cover_quote_and_helper_dispatch() {
let quote_cli = Cli {
common: CommonArgs::default(),
command: CommandMode::Quote {
shell: ShellMode::Raw,
values: vec![OsString::from("tool.exe"), OsString::from("two words")],
},
};
assert_eq!(
resolve_values(&quote_cli, &quote_cli_values(&quote_cli)).expect("values"),
vec!["tool.exe".to_string(), "two words".to_string()]
);
assert_eq!(run(&quote_cli).expect("quote"), ExitCode::Success);
let helper_cli = Cli {
common: CommonArgs {
json: true,
format: None,
..CommonArgs::default()
},
command: CommandMode::NativeHelper {
values: vec!["alpha".to_string()],
},
};
assert_eq!(run(&helper_cli).expect("helper"), ExitCode::Success);
}
#[test]
fn inspect_arguments_attempt_pwsh_and_cmd_launch_paths() {
let pwsh_error =
inspect_arguments(ShellMode::Pwsh, &["alpha".to_string()]).expect_err("pwsh helper");
assert!(matches!(
pwsh_error,
CliError::Runtime(message)
if message.contains("failed to parse helper output")
|| message.contains("native helper failed with status")
|| message.contains("failed to launch pwsh helper")
));
#[cfg(windows)]
{
let cmd_error =
inspect_arguments(ShellMode::Cmd, &["alpha".to_string()]).expect_err("cmd helper");
assert!(matches!(
cmd_error,
CliError::Runtime(message)
if message.contains("failed to parse helper output")
|| message.contains("native helper failed with status")
|| message.contains("failed to write")
|| message.contains("failed to launch cmd helper")
));
let wrapper_one = unique_cmd_wrapper_path();
let wrapper_two = unique_cmd_wrapper_path();
assert_ne!(wrapper_one, wrapper_two);
assert_eq!(
wrapper_one.extension().and_then(|value| value.to_str()),
Some("cmd")
);
}
}
#[test]
fn resolve_values_rejects_missing_terminal_input() {
let cli = Cli {
common: CommonArgs::default(),
command: CommandMode::Quote {
shell: ShellMode::Raw,
values: Vec::new(),
},
};
let error = resolve_values(&cli, &[]).expect_err("missing values");
assert!(matches!(
error,
CliError::Usage(message)
if message.contains("provide positional values or pipe a JSON array")
|| message.contains("stdin was empty; expected a JSON array of strings")
));
}
#[test]
fn inspect_arguments_in_unit_tests_surface_helper_failures() {
let error = inspect_arguments(ShellMode::Raw, &["alpha".to_string()])
.expect_err("unit test binary is not the helper entrypoint");
assert!(matches!(
error,
CliError::Runtime(message)
if message.contains("failed to parse helper output")
|| message.contains("native helper failed with status")
));
}
#[test]
fn cmd_wrapper_writer_refuses_preexisting_paths() {
let path = unique_cmd_wrapper_path();
fs::write(&path, "original").expect("preexisting wrapper");
let error =
write_cmd_wrapper_file(&path, "replacement").expect_err("preexisting path refused");
assert!(matches!(
error,
CliError::Runtime(message)
if message.contains("refusing to replace existing cmd inspect wrapper")
));
assert_eq!(
fs::read_to_string(&path).expect("preserved content"),
"original"
);
let _ = fs::remove_file(path);
}
#[test]
fn run_native_helper_returns_success_exit_code() {
let code =
run_native_helper(&["alpha".to_string(), "two words".to_string()]).expect("helper");
assert_eq!(code, ExitCode::Success);
}
fn quote_cli_values(cli: &Cli) -> Vec<OsString> {
match &cli.command {
CommandMode::Quote { values, .. } => values.clone(),
_ => Vec::new(),
}
}
}
+5
View File
@@ -0,0 +1,5 @@
//! Binary entry point for `argv`.
fn main() {
std::process::exit(argv::main_entry());
}
+233
View File
@@ -0,0 +1,233 @@
//! Integration tests for the `argv` command.
use assert_cmd::Command;
use predicates::prelude::*;
fn cargo_command() -> Command {
Command::cargo_bin("argv").expect("binary")
}
#[test]
fn no_args_prints_quick_help_card() {
let mut command = cargo_command();
command
.assert()
.code(2)
.stdout(predicate::str::is_empty())
.stderr(predicate::str::contains(
"error: provide a subcommand: quote or inspect",
))
.stderr(predicate::str::contains("argv - Mercury Toolbox"))
.stderr(predicate::str::contains("Usage:"))
.stderr(predicate::str::contains("argv quote"))
.stderr(predicate::str::contains("argv inspect"))
.stderr(predicate::str::contains(
"Type 'argv --help' for the full command reference.",
));
}
#[test]
fn help_mentions_quote_inspect_and_platform_boundaries() {
let mut command = cargo_command();
command
.arg("--help")
.assert()
.success()
.stdout(predicate::str::contains("argv quote"))
.stdout(predicate::str::contains("argv inspect"))
.stdout(predicate::str::contains("Windows-first"))
.stdout(predicate::str::contains("cmd inspect is Windows-only"));
}
#[test]
fn subcommand_help_mentions_common_output_flags() {
for subcommand in ["quote", "inspect"] {
let mut command = cargo_command();
command
.args([subcommand, "--help"])
.assert()
.success()
.stdout(predicate::str::contains("--format <FORMAT>"))
.stdout(predicate::str::contains("--json"))
.stdout(predicate::str::contains("--toon"))
.stdout(predicate::str::contains("--color <WHEN>"))
.stdout(predicate::str::contains("--quiet"))
.stdout(predicate::str::contains("--shell <SHELL>"));
}
}
#[test]
fn quote_accepts_json_array_from_stdin() {
let mut command = cargo_command();
command
.arg("quote")
.arg("--shell")
.arg("pwsh")
.write_stdin("[\"tool\",\"two words\"]")
.assert()
.success()
.stdout(predicate::str::contains("& 'tool' 'two words'"));
}
#[cfg(windows)]
#[test]
fn quote_renders_cmd_safe_text_for_positional_args() {
let mut command = cargo_command();
command
.arg("quote")
.arg("--shell")
.arg("cmd")
.arg("--")
.arg("tool.exe")
.arg("two words")
.assert()
.success()
.stdout(predicate::str::contains("tool.exe"))
.stdout(predicate::str::contains("\"two words\""));
}
#[cfg(windows)]
#[test]
fn quote_cmd_wraps_metacharacters_for_cmd_shell() {
let mut command = cargo_command();
command
.arg("quote")
.arg("--shell")
.arg("cmd")
.arg("--")
.arg("tool.exe")
.arg("a&b")
.assert()
.success()
.stdout(predicate::str::contains("tool.exe"))
.stdout(predicate::str::contains("\"a&b\""));
}
#[cfg(windows)]
#[test]
fn inspect_round_trips_through_powershell_helper() {
let mut command = cargo_command();
command
.arg("inspect")
.arg("--json")
.arg("--shell")
.arg("pwsh")
.arg("--")
.arg("alpha")
.arg("two words")
.assert()
.success()
.stdout(predicate::str::contains("\"shell\":\"pwsh\""))
.stdout(predicate::str::contains(
"\"argv\":[\"alpha\",\"two words\"]",
))
.stdout(predicate::str::contains("\"count\":2"));
}
#[cfg(windows)]
#[test]
fn inspect_round_trips_positional_values_that_look_like_options() {
let mut command = cargo_command();
command
.arg("inspect")
.arg("--json")
.arg("--shell")
.arg("pwsh")
.arg("--")
.arg("alpha")
.arg("--filter=x|y")
.assert()
.success()
.stdout(predicate::str::contains(
"\"argv\":[\"alpha\",\"--filter=x|y\"]",
))
.stdout(predicate::str::contains("\"count\":2"));
}
#[cfg(windows)]
#[test]
fn inspect_round_trips_through_cmd_helper() {
let mut command = cargo_command();
command
.arg("inspect")
.arg("--json")
.arg("--shell")
.arg("cmd")
.arg("--")
.arg("alpha")
.arg("two words")
.assert()
.success()
.stdout(predicate::str::contains("\"shell\":\"cmd\""))
.stdout(predicate::str::contains(
"\"argv\":[\"alpha\",\"two words\"]",
))
.stdout(predicate::str::contains("\"count\":2"));
}
#[cfg(windows)]
#[test]
fn inspect_round_trips_cmd_caret_literals() {
let mut command = cargo_command();
command
.arg("inspect")
.arg("--json")
.arg("--shell")
.arg("cmd")
.arg("--")
.arg("--flag")
.arg("literal^caret")
.assert()
.success()
.stdout(predicate::str::contains(
"\"argv\":[\"--flag\",\"literal^caret\"]",
));
}
#[test]
fn inspect_raw_uses_helper_without_shell() {
let mut command = cargo_command();
command
.arg("inspect")
.arg("--json")
.arg("--shell")
.arg("raw")
.arg("--")
.arg("alpha")
.arg("two words")
.assert()
.success()
.stdout(predicate::str::contains("\"shell\":\"raw\""))
.stdout(predicate::str::contains(
"\"argv\":[\"alpha\",\"two words\"]",
));
}
#[test]
fn inspect_text_accepts_json_array_from_stdin() {
let mut command = cargo_command();
command
.arg("inspect")
.arg("--shell")
.arg("raw")
.write_stdin("[\"alpha\",\"two words\"]")
.assert()
.success()
.stdout(predicate::str::contains("shell=raw count=2"))
.stdout(predicate::str::contains("argv[1]=\"two words\""));
}
#[test]
fn quote_rejects_invalid_stdin_json() {
let mut command = cargo_command();
command
.arg("quote")
.arg("--shell")
.arg("raw")
.write_stdin("{\"argv\":[\"alpha\"]}")
.assert()
.code(2)
.stderr(predicate::str::contains(
"expected a JSON array of strings on stdin",
));
}