|
| 1 | +The `argparse` module will interpret all the command-line arguments to your program. I suggest you use `argparse` for every command-line program you write so that you always have a standard way to get arguments and present help. |
| 2 | + |
| 3 | +## Types of arguments |
| 4 | + |
| 5 | +Command-line arguments come in a variety of flavors: |
| 6 | + |
| 7 | +* Positional: The order and number of the arguments is what determines their meaning. Some programs might expect, for instance, a file name as the first argument and an output directory as the second. |
| 8 | +* Named options: Standard Unix format allows for a "short" name like `-f` (one dash and a single character) or a "long" name like `--file` (two dashes and a string of characters) followed by some value like a file name or a number. This allows for arguments to be provided in any order or not provided in which case the program can use a reasonable default value. |
| 9 | +* Flag: A "Boolean" value like "yes"/"no" or `True`/`False` usually indicated by something that looks like a named option but without a value, e.g., `-d` or `--debug` to turn on debugging. Typically the presence of the flag indicates a `True` value for the argument; therefore, it's absence would mean `False`, so `--debug` turns *on* debugging while no `--debug` flag means there should not no debugging. |
| 10 | + |
| 11 | +## Datatypes of values |
| 12 | + |
| 13 | +The `argparse` module can save you enormous amounts of time by forcing the user to provide arguments of a particular type. If you run `new.py`, all of the above types of arguments are present along with suggestions for how to get string or integer values: |
| 14 | + |
| 15 | +```` |
| 16 | +# -------------------------------------------------- |
| 17 | +def get_args(): |
| 18 | + """Get command-line arguments""" |
| 19 | +
|
| 20 | + parser = argparse.ArgumentParser( |
| 21 | + description='Argparse Python script', |
| 22 | + formatter_class=argparse.ArgumentDefaultsHelpFormatter) |
| 23 | +
|
| 24 | + parser.add_argument('positional', |
| 25 | + metavar='str', |
| 26 | + help='A positional argument') |
| 27 | +
|
| 28 | + parser.add_argument('-a', |
| 29 | + '--arg', |
| 30 | + help='A named string argument', |
| 31 | + metavar='str', |
| 32 | + type=str, |
| 33 | + default='') |
| 34 | +
|
| 35 | + parser.add_argument('-i', |
| 36 | + '--int', |
| 37 | + help='A named integer argument', |
| 38 | + metavar='int', |
| 39 | + type=int, |
| 40 | + default=0) |
| 41 | +
|
| 42 | + parser.add_argument('-f', |
| 43 | + '--flag', |
| 44 | + help='A boolean flag', |
| 45 | + action='store_true') |
| 46 | +
|
| 47 | + return parser.parse_args() |
| 48 | +```` |
| 49 | + |
| 50 | +You should change the `description` to a short sentence describing your program. The `formatter_class` argument tells `argparse` to show the default values in the the standard help documentation. |
| 51 | + |
| 52 | +The `positional` argument's definition indicates we expect exactly one positional argument. The `-a` argument's `type` must be a `str` while the `-i` option must be something that Python can convert to the `int` type (you can also use `float`). Both of these arguments have `default` values which means the user is not required to provide them. You could instead define them with `required=True` to force the user to provide values themselves. |
| 53 | + |
| 54 | +The `-f` flag notes that the `action` is to `store_true` which means the value's default with be `True` if the argument is present and `False` otherwise. |
| 55 | + |
| 56 | +The `type` of the argument can be something much richer than simple Python types like strings or numbers. You can indicate that an argument must be a existing, readable file. Here is a simple implementation in Python of `cat -n`: |
| 57 | + |
| 58 | +```` |
| 59 | +#!/usr/bin/env python3 |
| 60 | +"""Python version of `cat -n`""" |
| 61 | +
|
| 62 | +import argparse |
| 63 | +
|
| 64 | +
|
| 65 | +# -------------------------------------------------- |
| 66 | +def get_args(): |
| 67 | + """Get command-line arguments""" |
| 68 | +
|
| 69 | + parser = argparse.ArgumentParser( |
| 70 | + description='Argparse Python script', |
| 71 | + formatter_class=argparse.ArgumentDefaultsHelpFormatter) |
| 72 | +
|
| 73 | + parser.add_argument('file', |
| 74 | + metavar='FILE', |
| 75 | + type=argparse.FileType('r'), |
| 76 | + help='Input file') |
| 77 | +
|
| 78 | + return parser.parse_args() |
| 79 | +
|
| 80 | +
|
| 81 | +# -------------------------------------------------- |
| 82 | +def main(): |
| 83 | + """Make a jazz noise here""" |
| 84 | +
|
| 85 | + args = get_args() |
| 86 | + fh = args.file |
| 87 | +
|
| 88 | + print('Reading "{}"'.format(fh.name)) |
| 89 | + for i, line in enumerate(fh): |
| 90 | + print(i, line, end='') |
| 91 | +
|
| 92 | +
|
| 93 | +# -------------------------------------------------- |
| 94 | +if __name__ == '__main__': |
| 95 | + main() |
| 96 | +```` |
| 97 | + |
| 98 | +The `type` of the input `file` argument is an *open file handle* which we can directly read line-by-line with a `for` loop! Because it's a file *handle* and not a file *name*, I chose to call the variable `fh` to help me remember what it is. You can access the file's name via `fh.name`. |
| 99 | + |
| 100 | +## Number of arguments |
| 101 | + |
| 102 | +If you want one positional argument, you can define them like so: |
| 103 | + |
| 104 | +```` |
| 105 | +#!/usr/bin/env python3 |
| 106 | +"""One positional argument""" |
| 107 | +
|
| 108 | +import argparse |
| 109 | +
|
| 110 | +parser = argparse.ArgumentParser( |
| 111 | + description='One positional argument', |
| 112 | + formatter_class=argparse.ArgumentDefaultsHelpFormatter) |
| 113 | +
|
| 114 | +parser.add_argument('first', metavar='str', help='First argument') |
| 115 | +args = parser.parse_args() |
| 116 | +print('first =', args.first) |
| 117 | +```` |
| 118 | + |
| 119 | +If the user provides anything other exactly one argument, they get a help message: |
| 120 | + |
| 121 | +```` |
| 122 | +$ ./one_arg.py |
| 123 | +usage: one_arg.py [-h] str |
| 124 | +one_arg.py: error: the following arguments are required: str |
| 125 | +$ ./one_arg.py foo bar |
| 126 | +usage: one_arg.py [-h] str |
| 127 | +one_arg.py: error: unrecognized arguments: bar |
| 128 | +$ ./one_arg.py foo |
| 129 | +first = foo |
| 130 | +```` |
| 131 | + |
| 132 | +If you want two different positional arguments: |
| 133 | + |
| 134 | +```` |
| 135 | +#!/usr/bin/env python3 |
| 136 | +"""Two positional arguments""" |
| 137 | +
|
| 138 | +import argparse |
| 139 | +
|
| 140 | +parser = argparse.ArgumentParser( |
| 141 | + description='Two positional arguments', |
| 142 | + formatter_class=argparse.ArgumentDefaultsHelpFormatter) |
| 143 | +
|
| 144 | +parser.add_argument('first', metavar='str', help='First argument') |
| 145 | +
|
| 146 | +parser.add_argument('second', metavar='int', help='Second argument') |
| 147 | +
|
| 148 | +return parser.parse_args() |
| 149 | +
|
| 150 | +print('first =', args.first) |
| 151 | +print('second =', args.second) |
| 152 | +```` |
| 153 | + |
| 154 | +Again, the user must provide exactly this number of positional arguments: |
| 155 | + |
| 156 | +```` |
| 157 | +$ ./two_args.py |
| 158 | +usage: two_args.py [-h] str str |
| 159 | +two_args.py: error: the following arguments are required: str, str |
| 160 | +$ ./two_args.py foo |
| 161 | +usage: two_args.py [-h] str str |
| 162 | +two_args.py: error: the following arguments are required: str |
| 163 | +$ ./two_args.py foo bar |
| 164 | +first = foo |
| 165 | +second = bar |
| 166 | +```` |
| 167 | + |
| 168 | +You can also use the `nargs=N` option to specify some number of arguments. It only makes sense if the arguments are the same thing like two files: |
| 169 | + |
| 170 | +```` |
| 171 | +#!/usr/bin/env python3 |
| 172 | +"""nargs=2""" |
| 173 | +
|
| 174 | +import argparse |
| 175 | +
|
| 176 | +parser = argparse.ArgumentParser( |
| 177 | + description='nargs=2', |
| 178 | + formatter_class=argparse.ArgumentDefaultsHelpFormatter) |
| 179 | +
|
| 180 | +parser.add_argument('files', metavar='FILE', nargs=2, help='Two files') |
| 181 | +
|
| 182 | +args = parser.parse_args() |
| 183 | +
|
| 184 | +file1, file2 = args.files |
| 185 | +print('file1 =', file1) |
| 186 | +print('file2 =', file2) |
| 187 | +```` |
| 188 | + |
| 189 | +The help indicates we want two files: |
| 190 | + |
| 191 | +```` |
| 192 | +$ ./nargs2.py foo |
| 193 | +usage: nargs2.py [-h] FILE FILE |
| 194 | +nargs2.py: error: the following arguments are required: FILE |
| 195 | +```` |
| 196 | + |
| 197 | +And we can unpack the two file arguments and use them: |
| 198 | + |
| 199 | +```` |
| 200 | +$ ./nargs2.py foo bar |
| 201 | +file1 = foo |
| 202 | +file2 = bar |
| 203 | +```` |
| 204 | + |
| 205 | +If you want one or more of some argument, you can use `nargs='+'`: |
| 206 | + |
| 207 | +```` |
| 208 | +$ cat nargs+.py |
| 209 | +#!/usr/bin/env python3 |
| 210 | +"""nargs=+""" |
| 211 | +
|
| 212 | +import argparse |
| 213 | +
|
| 214 | +parser = argparse.ArgumentParser( |
| 215 | + description='nargs=+', |
| 216 | + formatter_class=argparse.ArgumentDefaultsHelpFormatter) |
| 217 | +
|
| 218 | +parser.add_argument('files', metavar='FILE', nargs='+', help='Some files') |
| 219 | +
|
| 220 | +args = parser.parse_args() |
| 221 | +files = args.files |
| 222 | +
|
| 223 | +print('number = {}'.format(len(files))) |
| 224 | +print('files = {}'.format(', '.join(files))) |
| 225 | +```` |
| 226 | + |
| 227 | +Note that this will return a `list` -- even a single argument will become a `list` of one value: |
| 228 | + |
| 229 | +```` |
| 230 | +$ ./nargs+.py |
| 231 | +usage: nargs+.py [-h] FILE [FILE ...] |
| 232 | +nargs+.py: error: the following arguments are required: FILE |
| 233 | +$ ./nargs+.py foo |
| 234 | +number = 1 |
| 235 | +files = foo |
| 236 | +$ ./nargs+.py foo bar |
| 237 | +number = 2 |
| 238 | +files = foo, bar |
| 239 | +```` |
| 240 | + |
| 241 | +## Automatic help |
| 242 | + |
| 243 | +The `argparse` module reserves the `-h` and `--help` flags for generating help documentation. You do not need to add these nor are you allowed to use these flags for other purposes. Using the above definition, this is the help that `argparse` will generate: |
| 244 | + |
| 245 | +```` |
| 246 | +$ ./foo.py |
| 247 | +usage: foo.py [-h] [-a str] [-i int] [-f] str |
| 248 | +foo.py: error: the following arguments are required: str |
| 249 | +[cholla@~/work/python/playful_python/article]$ ./foo.py -h |
| 250 | +usage: foo.py [-h] [-a str] [-i int] [-f] str |
| 251 | +
|
| 252 | +Argparse Python script |
| 253 | +
|
| 254 | +positional arguments: |
| 255 | + str A positional argument |
| 256 | +
|
| 257 | +optional arguments: |
| 258 | + -h, --help show this help message and exit |
| 259 | + -a str, --arg str A named string argument (default: ) |
| 260 | + -i int, --int int A named integer argument (default: 0) |
| 261 | + -f, --flag A boolean flag (default: False) |
| 262 | +```` |
| 263 | + |
| 264 | +Notice how unhelpful a name like `positional` is? |
| 265 | + |
| 266 | +## Getting the argument values |
| 267 | + |
| 268 | +The values for the arguments will be accessible through the "long" name you define and will have been coerced to the Python data type you indicated. If I change `main` to this: |
| 269 | + |
| 270 | +```` |
| 271 | +# -------------------------------------------------- |
| 272 | +def main(): |
| 273 | + """Make a jazz noise here""" |
| 274 | +
|
| 275 | + args = get_args() |
| 276 | + str_arg = args.arg |
| 277 | + int_arg = args.int |
| 278 | + flag_arg = args.flag |
| 279 | + pos_arg = args.positional |
| 280 | +
|
| 281 | + print('str_arg = "{}" ({})'.format(str_arg, type(str_arg))) |
| 282 | + print('int_arg = "{}" ({})'.format(int_arg, type(int_arg))) |
| 283 | + print('flag_arg = "{}" ({})'.format(flag_arg, type(flag_arg))) |
| 284 | + print('positional = "{}" ({})'.format(pos_arg, type(pos_arg))) |
| 285 | +```` |
| 286 | + |
| 287 | +And then run it: |
| 288 | + |
| 289 | +```` |
| 290 | +$ ./foo.py -a foo -i 4 -f bar |
| 291 | +str_arg = "foo" (<class 'str'>) |
| 292 | +int_arg = "4" (<class 'int'>) |
| 293 | +flag_arg = "True" (<class 'bool'>) |
| 294 | +positional = "bar" (<class 'str'>) |
| 295 | +```` |
| 296 | + |
| 297 | +Notice how we might think that `-f` takes the argument `bar`, but it is defined as a `flag` and the `argparse` knows that the program take |
| 298 | + |
| 299 | +```` |
| 300 | +$ ./foo.py foo -a bar -i 4 -f |
| 301 | +str_arg = "bar" (<class 'str'>) |
| 302 | +int_arg = "4" (<class 'int'>) |
| 303 | +flag_arg = "True" (<class 'bool'>) |
| 304 | +positional = "foo" (<class 'str'>) |
| 305 | +```` |
0 commit comments