0

I was wondering how to create a flexible CLI interface with Python. So far I have come up with the following:

$ cat cat.py
#!/usr/bin/env python

from sys import stdin
from fileinput import input
from argparse import ArgumentParser, FileType

def main(args):

   for line in input():
      print line.strip()

if __name__ == "__main__":
   parser = ArgumentParser()
   parser.add_argument('FILE', nargs='?', type=FileType('r'), default=stdin)
   main(parser.parse_args())

This handles both stdin and file input:

$ echo 'stdin test' | ./cat.py
stdin test

$ ./cat.py file
file test

The problem is it doesn't handle multiple input or no input the way I would like:

$ ./cat.py file file
usage: cat.py [-h] [FILE]
cat.py: error: unrecognized arguments: file

$ ./cat.py 

For multiple inputs it should cat the file multiple times and for no input input should ideally have same the behaviour as -h:

$ ./cat.py -h
usage: cat.py [-h] [FILE]

positional arguments:
  FILE

optional arguments:
  -h, --help  show this help message and exit

Any ideas on creating a flexible CLI interface with Python?

2
  • Why are you using both fileinput and argparse? input() does: "This iterates over the lines of all files listed in sys.argv[1:], defaulting to sys.stdin if the list is empty." Do you realize that argparse FileType opens the files that you name? As written you don't do anything with those opened files. Commented Oct 5, 2013 at 18:29
  • @hpaulj you are right I should pass the files to fileinput see my answer. Commented Oct 9, 2013 at 19:59

2 Answers 2

4

Use nargs='*' to allow for 0 or more arguments:

if __name__ == "__main__":
    parser = ArgumentParser()
    parser.add_argument('FILE', nargs='*', type=FileType('r'), default=stdin)
    main(parser.parse_args())

The help output now is:

$ bin/python cat.py -h
usage: cat.py [-h] [FILE [FILE ...]]

positional arguments:
  FILE

optional arguments:
  -h, --help  show this help message and exit

and when no arguments are given, stdout is used.

If you want to require at least one FILE argument, use nargs='+' instead, but then the default is ignored, so you may as well drop that:

if __name__ == "__main__":
    parser = ArgumentParser()
    parser.add_argument('FILE', nargs='+', type=FileType('r'))
    main(parser.parse_args())

Now not specifying a command-line argument gives:

$ bin/python cat.py
usage: cat.py [-h] FILE [FILE ...]
cat.py: error: too few arguments

You can always specify stdin still by passing in - as an argument:

$ echo 'hello world!' | bin/python cat.py -
hello world!
Sign up to request clarification or add additional context in comments.

3 Comments

This gets me 90% of the way there +1. Just handling stdin without - would be perfect.
@sudo_O: That's handled by the nargs='*' and no arguments case; that's how defaults work. How did you envision stdin being handled in the nargs='+' case? You are then specifically stating there needs to be at least one argument.
Thanks gave me a push in the right direction, I figured it out in the end
2

A pretty good CLI interface the handles file input, standard input, no input, file output and inplace editing:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

def main(args, help):
    '''
    Simple line numbering program to demonstrate CLI interface
    '''

    if not (select.select([sys.stdin,],[],[],0.0)[0] or args.files):
        help()
        return

    if args.output and args.output != '-':
        sys.stdout = open(args.output, 'w')

    try:
        for i, line in enumerate(fileinput.input(args.files, inplace=args.inplace)):
            print i + 1, line.strip()
    except IOError:
        sys.stderr.write("%s: No such file %s\n" % 
                        (os.path.basename(__file__), fileinput.filename()))

if __name__ == "__main__":
    import os, sys, select, argparse, fileinput
    parser = argparse.ArgumentParser()
    parser.add_argument('files', nargs='*', help='input files')
    group = parser.add_mutually_exclusive_group()
    group.add_argument('-i', '--inplace', action='store_true', 
                       help='modify files inplace')
    group.add_argument('-o', '--output', 
                       help='output file. The default is stdout')
    main(parser.parse_args(), parser.print_help)

The code simply emulates nl and numbers the lines but should serve as a good skeleton for many applications.

1 Comment

Right, so with no arguments and no stdin pipe attached you show help, otherwise you read from stdin?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.