I'm writing a tool which passes arguments to another command provided with the arguments like this:
foo --arg1 -b -c bar -v --number=42
In this example foo
is my tool and --arg1 -b -c
should be the arguments parsed by foo
, while -v --number=42
are the arguments going to bar
, which is the command called by foo
.
So this is quite similar to strace
where you can provide arguments to strace
while still providing a command with custom arguments.
argparse
provides parse_known_arguments()
but it will parse all arguments it knows even those coming after bar
.
Some tools use a special syntax element (e.g. --
) to separate arguments with different semantics, but since I know foo
will only process names arguments, I'd like to avoid this.
That can't be too hard to manually find the first argument you might think, and this is what I'm currently doing:
parser.add_argument("--verbose", "-v", action="store_true")
all_args = args or sys.argv[1:]
with suppress(StopIteration):
split_at = next(i for i, e in enumerate(all_args) if not e.startswith("-" ))
return parser.parse_args(all_args[:split_at]), all_args[split_at:]
raise RuntimeError("No command provided")
And this works with the example I've provided. But with argparse
you can specify arguments with values which can but don't have to be provided with a =
:
foo --file=data1 --file data2 bar -v --number=42
So here it would be much harder to manually identify data2
to be a value for the second --file
argument, and bar
to be the first positional argument.
My current approach is to manually split arguments (backwards) and see, if all 'left hand' arguments successfully parse:
def parse_args():
class MyArgumentParser(ArgumentParser):
def error(self, message: str) -> NoReturn:
raise RuntimeError()
parser = MyArgumentParser()
parser.add_argument("--verbose", "-v", action="store_true")
parser.add_argument("--with-value", type=str)
all_args = sys.argv[1:]
for split_at in (i for i, e in reversed(list(enumerate(all_args))) if not e.startswith("-")):
with suppress(RuntimeError):
return parser.parse_args(all_args[:split_at]), all_args[split_at:]
parser.print_help(sys.stderr)
print("No command provided", file=sys.stderr)
raise SystemExit(-1)
That works for me, but next to the clumsy extra MyArgumentParser
needed just to be able to manually handle parser errors I now need to manually classify mistakes, since an ArgumentError
turns into something that occurs naturally.
So is there a way to tell argparse
to parse only until the first positional argument and then stop even if there are arguments it knows after that one?