1

New to python here - I want to make a command line application where the user will type input I will parse it and execute some command - something in the lines of:

try:
    while True:
        input = raw_input('> ')
        # parse here
except KeyboardInterrupt:
    pass

The user is supposed to type commands like init /path/to/dir. Can I use argparse to parse those ? Is my way too crude ?

Mr_and_Mrs_D
  • 32,208
  • 39
  • 178
  • 361
  • What have you tried so far? Also, have a look at `string.split` and the [`tokenize`](https://docs.python.org/2/library/tokenize.html) module – hlt Aug 15 '14 at 19:29
  • 1
    What is the expected output for `init /path/to/dir`? `argparse` is used to parse the command line arguments, so most likely not what you are looking for. – enrico.bacis Aug 15 '14 at 19:31
  • @hlt: want to use argparse - not reinvent the wheel. Moreover I am nt sure there is not a module for command line apps - and that I am reinventing the wheel anyway :) – Mr_and_Mrs_D Aug 15 '14 at 19:32
  • @enrico.bacis: I do hope I can pass it in a line to parse – Mr_and_Mrs_D Aug 15 '14 at 19:32
  • Why are you ignoring `KeyboardInterrupt`? It doesn't make your program more reliable, and the world itself is so fragile... – Sergey Orshanskiy Aug 15 '14 at 19:52
  • @SergeyOrshanskiy: the KeyboardInterrupt gets me out of while True – Mr_and_Mrs_D Aug 15 '14 at 20:10
  • `plac` builds on `argparse` to provide an interactive shell, http://plac.googlecode.com/hg/doc/plac.html#from-scripts-to-interactive-applications – hpaulj Aug 15 '14 at 23:06

4 Answers4

4

You can take a look at the cmd lib: http://docs.python.org/library/cmd.html


If you want to parse by yourself, you can use split to tokenize the user input, and execute your commands based on the tokens, sort of like this:

try:
    while True:
        input = raw_input('> ')
        tokens = input.split()
        command = tokens[0]
        args = tokens[1:]
        if command == 'init':
            # perform init command
        elif command == 'blah':
            # perform other command


except KeyboardInterrupt:
    pass
jh314
  • 27,144
  • 16
  • 62
  • 82
  • 1
    I would like to avoid the "ifelseheimer" and have this logic in the argparse.Parser instead – Mr_and_Mrs_D Aug 15 '14 at 19:42
  • To avoid "ifelifelseinson" use `dictionary_of_commands[command](args)` in a `try: ... except: ...` structure. – loa_in_ Aug 15 '14 at 19:45
  • 1
    Or you can try the `cmd` lib – jh314 Aug 15 '14 at 19:49
  • 2
    `shlex.split` would correctly deal with `init '/path/to/spacey dir'` – Eric Aug 15 '14 at 19:55
  • `Ipython` uses `argparse` to handle its main command line, as well as the command lines of the magic commands. So `input -> shlex.split -> argparse` is certainly possible. – hpaulj Aug 15 '14 at 22:59
  • @hpaulj: I followed the argparse way (see my answer: http://stackoverflow.com/a/25368374/281545) - is it too upythonic ? Anyway it seems the cmd module is the way to go - I would appreciate some skeleton code using it – Mr_and_Mrs_D Aug 18 '14 at 17:01
  • http://pymotw.com/2/cmd/index.html has more information and examples on using the `cmd` module. – hpaulj Aug 18 '14 at 18:21
2

arparse is a perfect solution for what you propose. The docs are well written and show dozens of example of how to invoke it simply. Keep in mind, it wants to read sys.argv by default, so when you invoke parse_args, you want to give it args (https://docs.python.org/2.7/library/argparse.html?highlight=argparse#the-parse-args-method).

The only downsize is argparse expects the items to be in "parameter" format, which means prefixed with dashes.

>>> import argparse
>>> parser = argparse.ArgumentParser(prog='PROG')
>>> parser.add_argument('-init', nargs=1)
>>> parser.parse_args('-init /path/to/something'.split())
Namespace(init="/path/to/something")
user590028
  • 11,364
  • 3
  • 40
  • 57
  • Hmm - can I somehow avoid the dashes ? Also I would like argparse treat different coomands as mutually exclusive – Mr_and_Mrs_D Aug 15 '14 at 19:44
  • I'm pretty sure the dashes are a deal breaker. Without those, you might be better of just going with the splitter() suggestions from others. You could also look at shlex (https://docs.python.org/2/library/shlex.html) which gives you alot of parsing power without the overhead of argparse. – user590028 Aug 15 '14 at 19:51
  • Thanks - how about the mutual exclusion ? – Mr_and_Mrs_D Aug 15 '14 at 19:53
  • 1
    It might be possible to get rid of the dashes by using [subparsers](https://docs.python.org/dev/library/argparse.html#sub-commands) with `argparse`. – hlt Aug 15 '14 at 20:42
  • 1
    If the `'init'` is meant to be some sort of command, and `'/path...'` its parameter, then setting up `argparse` with `subparsers` makes sense, especially if there will be other `commands` ('init', 'build','list', 'quit', etc) with their own parameters and options. – hpaulj Aug 15 '14 at 23:04
1

It depends on what you want to do, but you could have your script use ipython (interactive python). For instance:

    #!/bin/ipython -i
    def init(path_to_dir):
        print(path_to_dir)

Usage: after staring the script,

init("pathToFile.txt")

You are running in an interactive python session, so you get features like tab completion that would be difficult to implement manually. On the other hand, you are stuck with python syntax. It depends on your application.

Mr.P
  • 81
  • 5
1

What I did was:

# main
parser = Parser('blah')
try:
    while True:
        # http://stackoverflow.com/a/17352877/281545
        cmd = shlex.split(raw_input('> ').strip())
        logging.debug('command line: %s', cmd)
        try:
            parser.parse(cmd)
        except SystemExit: # DUH http://stackoverflow.com/q/16004901/281545
            pass
except KeyboardInterrupt:
    pass

Where the parser:

class Parser(argparse.ArgumentParser):
    def __init__(self, desc, add_h=True):
        super(Parser, self).__init__(description=desc, add_help=add_h,
                                     formatter_class=argparse.
                                    ArgumentDefaultsHelpFormatter)
        # https://docs.python.org/dev/library/argparse.html#sub-commands
        self.subparsers = subparsers = self.add_subparsers(
            help='sub-command help')
        # http://stackoverflow.com/a/8757447/281545
        subparsers._parser_class = argparse.ArgumentParser
        from  watcher.commands import CMDS
        for cmd in CMDS: cmd()(subparsers)

    def parse(self, args):
        return self.parse_args(args)

And a command (CMDS=[watch.Watch]):

class Watch(Command):

    class _WatchAction(argparse.Action):
        def __call__(self, parser, namespace, values, option_string=None):
            # here is the actual logic of the command
            logging.debug('%r %r %r' % (namespace, values, option_string))
            setattr(namespace, self.dest, values)
            Sync.addObserver(path=values)

    CMD_NAME = 'watch'
    CMD_HELP = 'Watch a directory tree for changes'
    ARGUMENTS = {'path': Arg(hlp='Path to a directory to watch. May be '
                                  'relative or absolute', action=_WatchAction)}

where:

class Command(object):
    """A command given by the users - subclasses must define  the CMD_NAME,
    CMD_HELP and ARGUMENTS class fields"""

    def __call__(self, subparsers):
        parser_a = subparsers.add_parser(self.__class__.CMD_NAME,
                                         help=self.__class__.CMD_HELP)
        for dest, arg in self.__class__.ARGUMENTS.iteritems():
            parser_a.add_argument(dest=dest, help=arg.help, action=arg.action)
        return parser_a

class Arg(object):
    """Wrapper around cli arguments for a command"""

    def __init__(self, hlp=None, action='store'):
        self.help = hlp
        self.action = action

Only tried with one command so far so this is rather untested. I used the shlex and subparsers tips from comments. I had a look at the cmd module suggested by @jh314 but did not quite grok it - however I think it is the tool for the job - I am interested in an answer with code doing what I do but using the cmd module.

Mr_and_Mrs_D
  • 32,208
  • 39
  • 178
  • 361