mkaz.blog

Working with Python

Command-line Arguments

I use Python as my go-to tool for command-line scripts, these often requiring passing command-line arguments. Since I use various programming languages I don't remember anything, so I create reference docs for myself and hopefully others.

What Library to Use

I don't know the history, but there are a couple standard libraries you can use to parse command-line arguments. The one you want to use is argparse module. Similar in name to optparse but that is the older deprecated module.

Also confusingly there is a getopt module that handles parsing of command-line arguments but is more complicated and requires writing more code.

Just use argparse module, it works great for both Python2 and Python3.

Basic Example

First, you may not need a module. If all you want to do is grab a single argument and no flags or other parameters passed in, you can just use sys.argv array that contains all of the command-line parameters.

The first element in sys.argv is the script itself. So a parameter passed in will be in the second element: sys.argv[1]

import sys
if len(sys.argv) > 1:
    print( "~ Script: " + sys.argv[0] )
    print( "~ Arg : " + sys.argv[1] )
else:
    print(" No arguments ")

Saving as test.py and running gives:

$ python test.py Foo
~ Script: test.py
~ Arg   : Foo

Multiple Arguments with sys.argv

Since sys.argv is simply a list, you can grab blocks of arguments together or slice around as you would any other list.

Last argument: sys.argv[-1]

All args after first: " ".join(sys.argv[2:])

Flag Parameters

You need to start using a module when you want to start including flags such as --help or want to have optional arguments, or varying length parameters. As mentioned, the best standard module to use is argparse.

Help and Verbose Examples

import argparse
 
parser = argparse.ArgumentParser(description='Demo')
parser.add_argument('--verbose',
    action='store_true',
    help='verbose flag' )
 
args = parser.parse_args()
 
if args.verbose:
    print("~ Verbose!")
else:
    print("~ Not so verbose")

Here's how to run the above example:

$ python test.py
~ Not so verbose
 
$ python test.py --verbose
~ Verbose!

The action parameter tells argparse to store true if the flag is found, otherwise it stores false. Also a great thing about using argparse is you get built-in help. You can try it out by passing in an unknown parameter, -h or --help

$ python test.py --help
usage: test.py [-h] [--verbose]
 
Demo
 
optional arguments:
  -h, --help  show this help message and exit
    --verbose   verbose output

Extended help

If you want to add to the automatically created help use the epilog parameter when creating the object.

parser = argparse.ArgumentParser(
    description='Demo',
    epilog="My extended help text"
)

By default, regardless of your formatting argparse will strip all whitespace in epilog and display as a single long string. If you want it to keep the whitespace, use the RawDescriptionHelpFormatter like so:

parser = argparse.ArgumentParser(
    description='Demo',
    formatter_class=argparse.RawDescriptionHelpFormatter,
    epilog="""
My longer text
    includes fancy
             whitespace formatting
    """
    )

Parameter error checking

A side effect of using argparse, you will get an error if a user passes in a command-line argument not expected, this includes flags or just an extra argument.

$ python test.py filename
usage: test.py [-h] [--verbose]
test.py: error: unrecognized arguments: filename

Multiple, Short or Long Flags

You can specify multiple flags for one argument, typically this is down with short and long flags, such as --verbose and -v

import argparse
 
parser = argparse.ArgumentParser()
parser.add_argument('--verbose', '-v',
    action='store_true',
    help='verbose flag' )
 
args = parser.parse_args()
 
if args.verbose:
    print("~ Verbose!")
else:
    print("~ Not so verbose")

Required Flags

You can make a flag required by setting, required=True this will cause an error if the flag is not specified.

parser = argparse.ArgumentParser()
parser.add_argument('--limit', required=True, type=int)
args = parser.parse_args()

Positional Arguments

The examples so far have been about flags, parameters starting with --, argparse also handles the positional args which are just specified without the flag. Here's an example to illustrate.

parser = argparse.ArgumentParser()
parser.add_argument('filename')
args = parser.parse_args()
 
print("~ Filename: {}".format(args.filename))

Output:

$ python test.py filename.txt
~ Filename: filename.txt

Number of Arguments

Argparse determines the number of arguments based on the action specified, for our verbose example, the store_true action takes no argument. By default, argparse will look for a single argument, shown above in the filename example.

If you want your parameters to accept a list of items you can specify nargs=n for how many arguments to accept. Note, if you set nargs=1, it will return as a list not a single value.

import argparse
 
parser = argparse.ArgumentParser()
parser.add_argument('nums', nargs=2)
args = parser.parse_args()
 
print("~ Nums: {}".format(args.nums))

Output:

$ python test.py 5 2
~ Nums: ['5', '2']

Variable Number of Parameters

The nargs argument accepts a couple of extra special parameters. If you want the argument to accept all of the parameters, you can use * which will return all parameters if present, or empty list if none.

parser = argparse.ArgumentParser()
parser.add_argument('nums', nargs='*')
args = parser.parse_args()
 
print("~ Nums: {}".format(args.nums))

Output:

$ python test.py 5 2 4
~ Nums: ['5', '2', '4']

If you want to require, 1 or more parameters, use nargs='+'

Positional arguments are determined by the position specified. This can be combined with the nargs='*' for example if you want to define a filename and a list of values to store.

parser = argparse.ArgumentParser()
parser.add_argument('filename')
parser.add_argument('nums', nargs='*')
args = parser.parse_args()
 
print("~ Filename: {}".format(args.filename))
print("~ Nums: {}".format(args.nums))

Output:

$ python test.py file.txt 5 2 4
~ Fileanme: file.txt
~ Nums: ['5', '2', '4']

You can also specify nargs='?' if you want to make a positional argument optional, but you need to be careful how you combine ? and * parameters, especially if you put an optional positional parameter before another one.

This makes sense, not requiring the last args:

parser = argparse.ArgumentParser()
parser.add_argument('filename')
parser.add_argument('nums', nargs='?')
args = parser.parse_args()

Output:

$ python test.py test.txt 3
~ Filename: test.txt
~ Nums: 3
 
$ python test.py test.txt
~ Filename: test.txt
~ Nums: None

However, using the nargs='?' first will give unexpected results when arguments are missing, for example:

parser = argparse.ArgumentParser()
parser.add_argument('filename', nargs='?')
parser.add_argument('nums', nargs='*')
args = parser.parse_args()

Output:

$ python test.py 3 2 1
~ Filename: 3
~ Nums: ['2', '1']

You can use nargs with flag arguments as well.

parser = argparse.ArgumentParser()
parser.add_argument('--geo', nargs=2)
parser.add_argument('--pos', nargs=2)
parser.add_argument('type')
args = parser.parse_args()

Output:

$ python test.py --geo 5 10 --pos 100 50 square
~ Geo: ['5', '10']
~ Pos: ['100', '50']
~ Type: square

Variable Type

You might notice that the parameters passed in are being treated like strings and not numbers, you can specify the variable type by specifying type=int. By specifying the type, argparse will also fail if an invalid type is passed in.

parser = argparse.ArgumentParser()
parser.add_argument('nums', nargs=2, type=int)
args = parser.parse_args()
 
print("~ Nums: {}".format(args.nums))

Output:

$ python test.py 5 2
~ Nums: [5, 2]

File Types

Argparse has built-in filetypes that make it easier to open files specified on the command line. Here's an example of reading a file, you can do the same writing a file.

parser = argparse.ArgumentParser()
parser.add_argument('f', type=argparse.FileType('r'))
args = parser.parse_args()
 
for line in args.f:
    print( line.strip() )

Default Value

You may specify a default value if the user does not pass one in. Here's an example using a flag.

parser = argparse.ArgumentParser()
parser.add_argument('--limit', default=5, type=int)
args = parser.parse_args()
 
print("~ Limit: {}".format(args.limit))

Output:

$ python test.py
~ Limit: 5

Remainder

If you want to gather the extra arguments passed in, you can use remainder which gathers up all arguments not specified into a list.

import argparse
 
parser = argparse.ArgumentParser()
parser.add_argument('--verbose',
    action='store_true',
    help='verbose flag' )
parser.add_argument('args', nargs=argparse.REMAINDER)
args = parser.parse_args()
 
print(args.args)

Specifying remainder will create a list of all remaining arguments:

$ python test.py --verbose foo bar
['foo', 'bar']

Actions

The default action is to assign the variable specified, but there are a couple of other actions that can be specified.

Booleans

We have already seen the boolean flag action which is action='store_true' which also has a counter action for action='store_false'

Count

You can use the count action, which will return how many times a flag was called, this can be useful for verbosity or silent flags.

parser = argparse.ArgumentParser()
parser.add_argument('--verbose', '-v', action='count')
args = parser.parse_args()
 
print("~ Verbose: {}".format(args.verbose))

Output:

$ python test.py
~ Verbose: None
 
$ python test.py --verbose
~ Verbose: 1
 
$ python test.py --verbose -v --verbose
~ Verbose: 3

Append

You can also use the append action to create a list if multiple flags are passed in.

parser = argparse.ArgumentParser()
parser.add_argument('-c', action='append')
args = parser.parse_args()
 
print("~ C: {}".format(args.c))

Output:

$ python test.py
~ C: None
 
$ python test.py -c hi
~ C: ['hi']
 
$ python test.py -c hi -c hello -c hey
~ C: ['hi', 'hello', 'hey']

Choices

If you only want a set of allowed values to be used, you can set the choices list, which will display an error if invalid entry.

parser = argparse.ArgumentParser(prog='roshambo.py')
parser.add_argument('throw', choices=['rock', 'paper', 'scissors'])
args = parser.parse_args()
 
print("~ Throw: {}".format(args.throw))

Examples

I'll end with two complete examples; many of the examples above are not as complete, they were kept short to focus on the idea being illustrated.

Copy Script Example

import argparse
import sys
 
parser = argparse.ArgumentParser(description='script to copy one file to another')
 
parser.add_argument('-v', '--verbose',
    action="store_true",
    help="verbose output" )
 
parser.add_argument('-R',
    action="store_false",
    help="Copy all files and directories recursively")
 
parser.add_argument('infile',
    type=argparse.FileType('r'),
    help="file to be copied")
 
parser.add_argument('outfile',
    type=argparse.FileType('w'),
    help="file to be created")
 
args = parser.parse_args()

Bug Script Example

Here is an example of a script that closes a bug

import argparse
import sys
 
parser = argparse.ArgumentParser(description='close bug')
 
parser.add_argument('-v', '--verbose',
    action="store_true",
    help="verbose output" )
 
parser.add_argument('-s',
    default="closed",
    choices=['closed', 'wontfix', 'notabug'],
    help="bug status")
 
parser.add_argument('bugnum',
    type=int,
    help="Bug number to be closed")
 
parser.add_argument('message',
    nargs='*',
    help="optional message")
 
args = parser.parse_args()
 
print("~ Bug Num: {}".format(args.bugnum))
print("~ Verbose: {}".format(args.verbose))
print("~ Status : {}".format(arg.s))
print("~ Message: {}".format(" ".join(args.message)))

Resources