argparse: optional argument between positional arguments

Viewed 1058

I want to emulate the behavior of most command-line utilities, where optional arguments can be put anywhere in the command line, including between positional arguments, such as in this mkdir example:

mkdir before --mode 077 after

In this case, we know that --mode takes exactly 1 argument, so before and after are both considered positional arguments. The optional part, --mode 077, can really be put anywhere in the command line.

However, with argparse, the following code does not work with this example:

# mkdir.py
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('--mode', nargs=1)
parser.add_argument('dirs', nargs='*')
args = parser.parse_args()

Running ./mkdir.py before --mode 077 after results in:

mkdir.py: error: unrecognized arguments: after

How can I get argparse to accept an optional argument (with a fixed, known number of items) between positional ones?

2 Answers

Starting from Python 3.7, it seems argparse now supports this kind of Unix-style parsing:

Intermixed parsing

ArgumentParser.parse_intermixed_args(args=None, namespace=None)

A number of Unix commands allow the user to intermix optional arguments with positional arguments. The parse_intermixed_args() and parse_known_intermixed_args() methods support this parsing style.

There is a caveat, but for "simple" options, it does not affect them:

These parsers do not support all the argparse features, and will raise exceptions if unsupported features are used. In particular, subparsers, argparse.REMAINDER, and mutually exclusive groups that include both optionals and positionals are not supported.

(I posted this FAQ-style question after spending 1 hour trying to understand why the examples in the Python argparse documentation didn't seem to include it, and only by chance found a somewhat unrelated question which contained the mention to this "intermixed" function in a comment, which I am unable to find again to cite it properly.)

I'm not familiar with argparse so I would write my own code to handle arguments.

import sys

#the first argument is the name of the program, so we skip that
args = sys.argv[1:]
#just for debugging purposes:
argsLen = len(args)
print(args, argsLen)

#Create a list that will contain all of the indeces that you will have parsed through
completedIndeces = []
i = 0

#add the index to the completed indeces list.
def goToNextIndex(j):
    global i
    completedIndeces.append(j)
    i += 1

def main():
    global i
    ###core logic example
    #Go through each item in args and decide what to do based on the arugments passed in
    for argu in args:
        if i in completedIndeces:
            print("breaking out")
            #increment i and break out of the current loop
            i += 1
            # If the indeces list has the index value then nothing else is done.
            pass 
        elif argu == "joe":
            print("did work with joe")
            goToNextIndex(i)
        elif argu == "sam":
            print("did work with sam")
            goToNextIndex(i)
        elif argu == "school":
            print("going to school")
            goToNextIndex(i)

            # If the argument has other arguments after it that are associated with it 
            # then add those indeces also to the completed indeces list. 
    
            #take in the argument following school
            nextArg = i
            #Do some work with the next argument
            schoolName = args[nextArg]
            print(f"You're going to the school called {schoolName}")
            #make sure to skip the next argument as it has already been handled
            completedIndeces.append(nextArg)
        else:
            print(f"Error the following argument is invalid: {argu}")
            goToNextIndex(i)


    print(f"Value of i: {i}")
    print(f"completed indeces List: {completedIndeces}")

main()
Related