Skip to content

Commit 6f7213a

Browse files
authored
Merge pull request #647 from ytsssun/fix-apiclient-exec-arg-parsing
apiclient: support `--` separator in `exec` subcommand
2 parents eb0ffb6 + ea7257e commit 6f7213a

File tree

3 files changed

+145
-1
lines changed

3 files changed

+145
-1
lines changed

sources/Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

sources/api/apiclient/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,5 +45,8 @@ toml.workspace = true
4545
unindent.workspace = true
4646
url.workspace = true
4747

48+
[dev-dependencies]
49+
test-case.workspace = true
50+
4851
[build-dependencies]
4952
generate-readme.workspace = true

sources/api/apiclient/src/main.rs

Lines changed: 141 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -300,14 +300,30 @@ fn usage_msg<S: AsRef<str>>(msg: S) -> ! {
300300
// Arg parsing
301301

302302
/// Parses user arguments into an Args structure.
303-
fn parse_args(args: env::Args) -> (Args, Subcommand) {
303+
fn parse_args(args: impl Iterator<Item = String>) -> (Args, Subcommand) {
304304
let mut global_args = Args::default();
305305
let mut subcommand = None;
306306
let mut subcommand_args = Vec::new();
307307

308308
let mut iter = args.into_iter().skip(1);
309309
while let Some(arg) = iter.next() {
310310
match arg.as_ref() {
311+
// Handle separator - only valid after exec subcommand is set
312+
"--" => {
313+
if subcommand.is_none() {
314+
usage_msg("'--' separator requires a subcommand to be specified first");
315+
}
316+
// Only do special handling for exec subcommand
317+
if subcommand.as_deref() == Some("exec") {
318+
// For exec, collect all remaining args and stop processing
319+
subcommand_args.extend(iter);
320+
break;
321+
} else {
322+
// For other subcommands, treat -- as a regular argument
323+
subcommand_args.push(arg);
324+
}
325+
}
326+
311327
"-h" | "--help" => usage(),
312328

313329
// Global args
@@ -1160,3 +1176,127 @@ mod error {
11601176
}
11611177
}
11621178
type Result<T> = std::result::Result<T, error::Error>;
1179+
#[cfg(test)]
1180+
mod tests {
1181+
use super::*;
1182+
use test_case::test_case;
1183+
1184+
// Helper macro for creating global Args with consistent formatting
1185+
macro_rules! global_args {
1186+
// Create Args with specified log_level and socket_path
1187+
($log_level:expr, $socket_path:expr) => {
1188+
Args {
1189+
log_level: $log_level,
1190+
socket_path: $socket_path.to_string(),
1191+
}
1192+
};
1193+
// Create Args with just log_level (using default socket_path)
1194+
($log_level:expr) => {
1195+
Args {
1196+
log_level: $log_level,
1197+
socket_path: constants::API_SOCKET.to_string(),
1198+
}
1199+
};
1200+
}
1201+
1202+
// Helper macro for creating exec subcommand
1203+
macro_rules! exec_cmd {
1204+
// For Exec subcommand with target, command args, and tty option
1205+
($target:expr, [$($arg:expr),*], $tty:expr) => {
1206+
Subcommand::Exec(ExecArgs {
1207+
target: $target.to_string(),
1208+
command: vec![$($arg.into()),*],
1209+
tty: $tty,
1210+
})
1211+
};
1212+
}
1213+
1214+
fn parse_command_line(cmd_str: &str) -> (Args, Subcommand) {
1215+
let args: Vec<String> = cmd_str.split_whitespace().map(|s| s.to_string()).collect();
1216+
parse_args(args.into_iter())
1217+
}
1218+
1219+
#[test_case("apiclient exec admin -- sheltie df -h",
1220+
global_args!(LevelFilter::Info),
1221+
exec_cmd!("admin", ["sheltie", "df", "-h"], None);
1222+
"exec with separator")]
1223+
#[test_case("apiclient exec admin sheltie df",
1224+
global_args!(LevelFilter::Info),
1225+
exec_cmd!("admin", ["sheltie", "df"], None);
1226+
"exec without separator")]
1227+
#[test_case("apiclient exec -t admin -- sheltie df -h",
1228+
global_args!(LevelFilter::Info),
1229+
exec_cmd!("admin", ["sheltie", "df", "-h"], Some(true));
1230+
"exec with tty and separator")]
1231+
#[test_case("apiclient exec -T admin sheltie df",
1232+
global_args!(LevelFilter::Info),
1233+
exec_cmd!("admin", ["sheltie", "df"], Some(false));
1234+
"exec with no-tty")]
1235+
fn test_exec_parsing(cmd_str: &str, expected_args: Args, expected_subcommand: Subcommand) {
1236+
let (global_args, subcommand) = parse_command_line(cmd_str);
1237+
1238+
// Check the global arguments match what we expect
1239+
assert_eq!(global_args.log_level, expected_args.log_level);
1240+
assert_eq!(global_args.socket_path, expected_args.socket_path);
1241+
1242+
// Check the subcommand matches what we expect
1243+
match (&subcommand, &expected_subcommand) {
1244+
(Subcommand::Exec(actual), Subcommand::Exec(expected)) => {
1245+
assert_eq!(actual.target, expected.target);
1246+
assert_eq!(actual.tty, expected.tty);
1247+
assert_eq!(actual.command, expected.command);
1248+
}
1249+
_ => panic!(
1250+
"Expected Exec subcommand: {:?}, got: {:?}",
1251+
expected_subcommand, subcommand
1252+
),
1253+
}
1254+
}
1255+
1256+
#[test_case("apiclient -v exec admin -- sheltie df -h",
1257+
global_args!(LevelFilter::Debug),
1258+
exec_cmd!("admin", ["sheltie", "df", "-h"], None);
1259+
"verbose flag with exec as global arg")]
1260+
#[test_case("apiclient --log-level error exec admin sheltie df",
1261+
global_args!(LevelFilter::Error),
1262+
exec_cmd!("admin", ["sheltie", "df"], None);
1263+
"log level with exec")]
1264+
#[test_case("apiclient exec admin -- sheltie -v df -h",
1265+
global_args!(LevelFilter::Info),
1266+
exec_cmd!("admin", ["sheltie", "-v", "df", "-h"], None);
1267+
"verbose flag after separator as command arg")]
1268+
#[test_case("apiclient -v exec admin -- sheltie -v df -h",
1269+
global_args!(LevelFilter::Debug),
1270+
exec_cmd!("admin", ["sheltie", "-v", "df", "-h"], None);
1271+
"verbose flag in both global and command positions")]
1272+
fn test_args_parsing_with_separator(
1273+
cmd_str: &str,
1274+
expected_args: Args,
1275+
expected_subcommand: Subcommand,
1276+
) {
1277+
let (global_args, subcommand) = parse_command_line(cmd_str);
1278+
1279+
// Check the global arguments match what we expect
1280+
assert_eq!(
1281+
global_args.log_level, expected_args.log_level,
1282+
"Global log level should match"
1283+
);
1284+
assert_eq!(
1285+
global_args.socket_path, expected_args.socket_path,
1286+
"Socket path should match"
1287+
);
1288+
1289+
// Check the subcommand matches what we expect
1290+
match (&subcommand, &expected_subcommand) {
1291+
(Subcommand::Exec(actual), Subcommand::Exec(expected)) => {
1292+
assert_eq!(actual.target, expected.target, "Target should match");
1293+
assert_eq!(actual.tty, expected.tty, "TTY setting should match");
1294+
assert_eq!(actual.command, expected.command, "Command should match");
1295+
}
1296+
_ => panic!(
1297+
"Expected Exec subcommand: {:?}, got: {:?}",
1298+
expected_subcommand, subcommand
1299+
),
1300+
}
1301+
}
1302+
}

0 commit comments

Comments
 (0)