Skip to content

Commit

Permalink
Merge pull request #49 from cgay/dev
Browse files Browse the repository at this point in the history
Better handling of "--"
  • Loading branch information
cgay authored Jan 6, 2025
2 parents 4e73ed6 + 0810f1e commit 28a1fdd
Show file tree
Hide file tree
Showing 3 changed files with 28 additions and 21 deletions.
31 changes: 10 additions & 21 deletions command-line-parser.dylan
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,8 @@ define abstract class <command> (<object>)
slot command-subcommands :: <sequence> = #[],
init-keyword: subcommands:;
slot selected-subcommand :: false-or(<subcommand>) = #f;
// All arguments following the first "--" positional argument, if any.
slot unconsumed-arguments :: <sequence> = #();
end class;

// The --help option is added by default but we provide a way to turn it off here.
Expand Down Expand Up @@ -598,8 +600,10 @@ end;
//======================================================================

// Break up our arguments around '--' in the traditional fashion.
define function split-args(argv)
define function split-args (argv)
=> (clean-args :: <sequence>, extra-args :: <sequence>)
// TODO(cgay): A <parameter-option> "--foo --" should be valid.
// https://github.com/dylan-lang/command-line-parser/issues/47
let splitter = find-key(argv, curry(\=, "--"));
if (splitter)
let clean-args = copy-sequence(argv, end: splitter);
Expand Down Expand Up @@ -710,9 +714,9 @@ define open generic parse-command-line
(parser :: <command-line-parser>, argv :: <sequence>)
=> ();

// Parse the command line, side-effecting the parser, its options, and its
// subcommands with the parsed values. `args` is a sequence of command line
// arguments (strings). It must not include the command name.
// Parse the command line, side-effecting the parser, its options, and its subcommands
// with the parsed values. `args` is a sequence of command line arguments (strings) that
// does not include the command name.
define method parse-command-line
(parser :: <command-line-parser>, args :: <sequence>)
=> ()
Expand All @@ -721,24 +725,9 @@ define method parse-command-line
let chopped-args = chop-args(clean-args);
tokenize-args(parser, chopped-args);
process-tokens(program-name(), parser, #f);

if (~empty?(extra-args))
// Append any more positional options from after the '--'. If there's a
// subcommand the extra args go with that.
// (This feels hackish. Can we handle this directly in process-tokens?)
let command = parser.selected-subcommand | parser;
let option = last(command.command-options);
if (~(instance?(option, <positional-option>)
& option.option-repeated?))
let opts = command.positional-options;
usage-error("Only %d positional argument%s allowed.",
opts.size,
if (opts.size = 1) "" else "s" end);
end;
for (arg in extra-args)
option.option-value := add!(option.option-value,
parse-option-value(arg, option.option-type));
end for;
command.unconsumed-arguments := extra-args;
end;
end method;

Expand Down Expand Up @@ -838,7 +827,7 @@ end function;
Parameterless options:
-b, --bar, --no-bar
Present or absent. May have opposites; latter values override
Present or absent. May have opposites; later values override
previous values.
Parameter options:
Expand Down
1 change: 1 addition & 0 deletions library.dylan
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ define module command-line-parser
parse-command-line,
get-option-value,
print-help,
unconsumed-arguments,

parse-option-value;

Expand Down
17 changes: 17 additions & 0 deletions tests/options-test.dylan
Original file line number Diff line number Diff line change
Expand Up @@ -217,3 +217,20 @@ define test test-positional-option-parsing ()
assert-equal("d", p4.option-value);
assert-equal(#["e", "f"], p5.option-value);
end test;

define test test-unconsumed-arguments ()
let p1 = make(<positional-option>, name: "p1", help: "?", required?: #f);
let cmd1 = make(<command-line-parser>, help: "?", options: list(p1));
assert-no-errors(parse-command-line(cmd1, #["x", "--", "y", "z"]));
assert-equal(#["y", "z"], cmd1.unconsumed-arguments);
assert-no-errors(parse-command-line(cmd1, #["--", "y", "z"]));
assert-equal(#["y", "z"], cmd1.unconsumed-arguments);

// Same assertions but with the final positional option allowing more than one arg.
let p2 = make(<positional-option>, name: "p1", help: "?", required?: #f, repeated?: #t);
let cmd2 = make(<command-line-parser>, help: "?", options: list(p2));
assert-no-errors(parse-command-line(cmd2, #["x", "--", "y", "z"]));
assert-equal(#["y", "z"], cmd2.unconsumed-arguments);
assert-no-errors(parse-command-line(cmd2, #["--", "y", "z"]));
assert-equal(#["y", "z"], cmd2.unconsumed-arguments);
end test;

0 comments on commit 28a1fdd

Please sign in to comment.