1

I'm very new to this module so please bear with me. I have the following code:

reader.py

import argparse

parent_parser = argparse.ArgumentParser(description="Read text files.")
parent_parser.add_argument('filename', help='TXT file', type=file, nargs='+')
parent_parser.add_argument('--verbose', '-v', action='store_true', 
        help="Verbosity on")

child_parser = parent_parser.add_subparsers(title="subcommand",
        help="Subcommand help")
new_file_command = child_parser.add_parser('new', help="New text file")
edit_file_command = child_parser.add_parser('edit', help="Edit existing text file")

args = parent_parser.parse_args()

What I'm trying to achieve might not be the standard way of how parsers and unix command line utilities work. If that is true, please correct me as I'd like to have standardized app.

This is what I'm trying to achieve:

  • if you run bare script with positional argument(s) like this: python reader.py some.txt I'd like to be able to just parse it and pass it to function that reads the text file, of course I want to accept optional arg verbose as well
  • if you run subcommand 'new' (new_file_command), I do not want to have positional argument filename to be required, instead I want to pass a string and create new text file like this: python reader.py new another.txt
  • if you run subcommand 'edit' (edit_file_command) I want to pass existing file in path and check for it (like you use type=int in add_argument) and then maybe pass it to function that opens editor, something like this: python reader.py edit some.txt

Again, I'm not sure if this is how command line apps/scripts are supposed to behave. I read the docs and looked at examples but it's still isn't clear to me how sub parsers work. I tried looking at Click module but that seems to me even more complicated.

Any help appreciated. Thanks!

user3056783
  • 2,265
  • 1
  • 29
  • 55

1 Answers1

3

So three sample calls are:

python reader.py some.txt 
python reader.py new another.txt
python reader.py edit some.txt

The easiest way to handle these is with one 'optional' positional, and one required one.

parser = ArgumentParser...
parser.add_argument('-v','--verbose', ...)
parser.add_argument('cmd', nargs='?', default='open', choices=['open','edit','new'])
parser.add_argument('filename')

For your 3 samples, it should produce something like:

namespace(cmd='open', filename='some.txt')
namespace(cmd='new', filename='another.txt')
namespace(cmd='edit', filename='some.txt')

cmd is an optional positional argument. If it is missing, the one string will be allocated to filename, and cmd gets its default. It's easier to do this than trying make a subparsers optional.


As for your current parser:

parent_parser = argparse.ArgumentParser(description="Read text files.")
parent_parser.add_argument('filename', help='TXT file', type=file, nargs='+')

I would not recommend using type=file. Better to use FileType or the default string (which lets you open the file in a with context later).

As to the nargs='+', do you really want to allocate 1 or more strings to filename? Or were you thinking of '?', which would be 0 or 1, i.e. making it optional?

parent_parser.add_argument('--verbose', '-v', action='store_true', 
        help="Verbosity on")

child_parser = parent_parser.add_subparsers(title="subcommand",
        help="Subcommand help")
new_file_command = child_parser.add_parser('new', help="New text file")
edit_file_command = child_parser.add_parser('edit', help="Edit existing text file")

Mixing this filename positional which accepts a variable number of values, with a subparsers argument (a positional that expects either new or edit) could be a problem.

I expect 'python reader.py some.txt' to object that the subparser command is missing. 'python reader.py new another.txt' will try to allocate new to filename, and another.txt to subparser, and raise an error.

It would be better to expect a subparsers command in all 3 cases:

parent_parser = argparse.ArgumentParser(description="Read text files.")
parent_parser.add_argument('--verbose', '-v', action='store_true', 
        help="Verbosity on")
child_parser = parent_parser.add_subparsers(title="subcommand",
        help="Subcommand help", dest='cmd')
open_file_command = child_parser.add_parser('open', help="Open text file")
open_file_command.add_argument('filename', help='TXT file')
new_file_command = child_parser.add_parser('new', help="New text file")
new_file_command.add_argument('filename', help='TXT file')
edit_file_command = child_parser.add_parser('edit', help="Edit existing text file")
edit_file_command.add_argument('filename', help='TXT file')

Each of commands, 'open','new','edit', expects a 'filename'.

Trying to avoid the use of an open command is going to create more difficulties than it's worth.

(There is a bug/feature in the latest argparse that makes subparsers optional, but you shouldn't take advantage of that without really knowing the issues.)


With:

parser = argparse.ArgumentParser()
parser.add_argument('-v', '--verbose') 
parser.add_argument('cmd', nargs='?', default='open', 
    choices=['open', 'edit', 'new']) 
parser.add_argument('filename', nargs='+') 

I expect reader.py new customstring to give

namespace(cmd='new', filename=[customstring])

which could be used as:

if args.cmd=='new':
with open(args.filename[0] + '.txt', 'w') as f:
     # do something with the newly created file

open and edit would use different open modes.


Beware that in Py3, subparsers are not required. That is, if one of the subcommands is not provided, it won't raise an error. That's an inadvertent change from early versions.

Argparse with required subparser

hpaulj
  • 221,503
  • 14
  • 230
  • 353
  • Hi thanks for the help. I have one last question though. I'm still wondering how to customize positional arguments. Let's say I have following code: parser = argparse.ArgumentParser() parser.add_argument('-v', '--verbose') parser.add_argument('cmd', nargs='?', default='open', choices=['open', 'edit', 'new']) parser.add_argument('filename', nargs='+') What if I want to customize positional arg for command `new`? I do not need/want to pass explicit filename, but just a string like `some` and then it creates file `some.txt`. How do I do this? Thanks – user3056783 May 05 '15 at 03:57
  • You can create a custom `type` or `Action`. But usually it is simpler to make that sort of change after parsing. Especially if you aren't doing any error checking on 'some'. – hpaulj May 05 '15 at 04:11
  • Hi I'm still going through docs and trying to figure this out but I cannot find more examples online. Can you just show me a snippet how you would implement this after parsing? Because if I do `reader.py new customstring` it does error checking for existing filename so it won't let me do anything. – user3056783 May 06 '15 at 01:13