1

I am writing a utility (call it runner) which, as its main functionality, runs another script. That script to be run is passed as an argument, as well as any arguments that should be passed to the script. So, for example:

runner script.py arg1 arg2

will run script.py as a subprocess, with arg1 and arg2 as arguments. This is easy enough, using the following:

parser = argparse.ArgumentParser()

parser.add_argument("script", help="The script to run")
parser.add_argument("script_args", nargs="*", help="Script arguments", default=[])

args = parser.parse_args()

The complication is that I have some additional arguments passed to runner, which modify how the script will be called. One of these is --times, which will be used to determine how many times to run the script. So:

runner --times 3 script.py arg1 arg2

will run script.py three times, with arg1 and arg2 as arguments.

The problem I am trying to solve is this. I want any arguments after the script name to be treated as arguments to pass to the script, even if they match argument names of runner. So:

runner script.py arg1 arg2 --times 3

should run script.py once, and pass the arguments arg1, arg2, --times, and 3 to script.py.

I can't find any way to tell the ArgumentParser to treat all arguments after the script arg as positional.

I know the caller of the script could force this behavior using the -- special argument, so the following would yield the desired behavior:

runner -- script.py arg1 arg2 --times 3

but I want the program to take care of this instead. Is this behavior possible?

Christopher Shroba
  • 7,006
  • 8
  • 40
  • 68

1 Answers1

1

First of all, you should not do this. It is more clear, if you make it explicit which arguments apply to which program (call), maybe you can sourround the whole script parameters of the called script in quotations like:

runner script.py "arg1 arg2 --times 3"

But if you want to do this your way, you can overwrite the ArgumentParser.parse_arguments function with your own. I only build an example to illustrate how it is possible (for me it's working), but i do not know if there are any sideeffects:

import argparse
from typing import Optional, Sequence


class ErrorCatchingArgumentParser(argparse.ArgumentParser):

    def parse_args(self, args: Optional[Sequence[str]] = ...) -> argparse.Namespace:
        ns, a = super().parse_known_args(args=args)
        args.insert(args.index(ns.script), "--")

        return super().parse_args(args=args)


if __name__ == '__main__':
    arguments = ["--time", "3", "myscript", "my_args", "myargs2", "--time", "5"]

    parser = ErrorCatchingArgumentParser()

    parser.add_argument("script", help="The script to run")
    parser.add_argument("--time", help="The script to run")
    parser.add_argument("script_args", nargs="*", help="Script arguments", default=[])

    print(arguments)
    namespace = parser.parse_args(arguments)

    print(arguments)
    print(namespace)

Output:

['--time', '3', 'myscript', 'my_args', 'myargs2', '--time', '5']
['--time', '3', '--', 'myscript', 'my_args', 'myargs2', '--time', '5']
Namespace(script='myscript', time='3', script_args=['my_args', 'myargs2', '--time', '5'])

It

  1. parses the given args with parse_known_args of superclass to get all arguments set (and do not raise an error)
  2. search for your script variable and insert the -- before it in the argument list
  3. parses the arguments with the parse_args function of superclass to get "the real" arguments

In the output you can see the original arguments, the manipulated arguments and the created namespace.

But i think you should NOT do this, anyway.

D-E-N
  • 1,242
  • 7
  • 14
  • Or you could tweak the `sys.argv` list like this and then pass it to `parse_args(new_argv)`. – hpaulj Nov 18 '21 at 00:56
  • In general this is correct, but with the given Information (especially no `--script` before the script name) it is hard to find the correct position of the script name, because it can be everything. So you have to parse your parameters first and for that `argparse` is built. – D-E-N Nov 18 '21 at 08:40
  • Thank you for the time you put into this answer - it is much appreciated! I agree it is kind of a gross requirement. I ruled out quoting the whole script because that leads to escaping hell when args happen to have characters the shell considers special, and I ruled out requiring the caller to include "--" because then forgetting to include it means behavior will be unexpected (and can change if I add new flags in the future). In case you're curious, I'm using this runner to wrap around scripts in cron, so that stdout/stderr are automatically logged to a file (plus some other features). – Christopher Shroba Nov 18 '21 at 21:47