CLI Walkthrough

This document walks you through building a more advanced CLI than that shown in the Basic Usage section.

Help Text

We’ll start with the JSH layout from the Basic Usage section, and add some more descriptive help text to describe the exit command. To do so, you need to turn exit into a dictionary:

layout =  {
    'exit': {
        '?': 'Quit this silly application',
        None: jsh.exit,
    },
}

The action to take when Enter is pressed after typing the command is defined under the None key, and the help text is defined under '?'.

This makes the help look like this:

> ?
Possible completions:
  exit   Quit this silly application

Custom Handlers and Multi-Word Commands

The CLI would be useless without the ability to define your own methods to run when a command is submitted. Let’s now add some commands with more than one word, and some custom handlers. We’ll define two commands, show version and show pid. First, we’ll need to write the functions to handle them. When executed, these functions will be passed a single argument, the JSH instance.

import os

def show_version(cli):
    print 'Useless CLI version 0.0.1'

def show_pid(cli):
    print 'My PID is {0}'.format(os.getpid())

Now we’ll add these to the layout, along with some help text. The individual words in the commands will correspond to levels in the layout tree:

layout = {
    'show': {
        '?': 'Display various information',
        'pid': {
            '?': 'Display my PID',
            None: show_pid,
        },
        'version': {
            '?': 'Display my version',
            None: show_version,
        },
    },
    'exit': {
        '?': 'Quit this silly application',
        None: jsh.exit,
    },
}

Now our CLI looks like this:

> ?
Possible completions:
  exit   Quit this silly application
  show   Display various information
> show ?
Possible completions:
  pid       Display my PID
  version   Display my version
> show
Incomplete command 'show'
> show pid ?
Possible completions:
  <[Enter]>   Execute this command
> show pid
My PID is 4633
> show version
Useless CLI version 0.0.1
>

Notice how the command show by itself is not allowed? This is because there is no None key under the show level of the layout tree - the CLI does not know what to do if that is the only command entered.

Command Variables

Often, your CLI will need to accept a variable from the user - something you cannot know in advance. To demonstrate how this is possible with JSH, we’ll add some shopping list functionality: adding items to the list, viewing the list and removing items from the list.

Viewing the list is easy:

shopping_list = []

def show_list(cli):
    if not shopping_list:
        print 'Shopping list is empty'
    else:
        print 'Items:'
        print '\n'.join(shopping_list)

layout = {
    ...
    'show': {
        ...
        'list': {
            '?': 'Display shopping list',
            None: show_list,
        }
        ...
    },
    ...
}

Adding items is just as easy, but this time the handler function will be passed another argument - whatever the user typed on the command line at that point:

def add_item(cli, item):
    shopping_list.append(item)

Adding the following to the layout will implement the add item <name> command:

layout = {
    ...
    'add': {
        '?': 'Add stuff',
        'item': {
            '?': 'Add item to shopping list',
            str: {
                '?': ('item', 'Item description'),
                None: add_item,
            },
        },
    },
    ...
}

Let’s take a look at the new stuff introduced. Using str as a key says that the parser should expect an arbitrary string at this point in the command. Pressing Enter after the arbitrary string will run the add_item function with two arguments: the JSH instance and the arbitrary string entered by the user. Also notice that the help text is now a tuple with the descriptive text as the second element - the first element is a metavariable, and you will see how this is used below.

Our CLI now looks like this:

> show ?
Possible completions:
  list      Display shopping list
  pid       Display my PID
  version   Display my version
> show list
Shopping list is empty
> add ?
Possible completions:
  item   Add item to shopping list
> add item ?
Possible completions:
  <item>   Item description
> add item carrots ?
Possible completions:
  <[Enter]>   Execute this command
> add item carrots
> add item courgettes
> show list
Items:
carrots
courgettes
>

Custom Completion

Now for our command to remove items from the list. Here’s the function to do it:

def remove_item(cli, item):
    try:
        shopping_list.remove(item)
    except ValueError:
        print 'Item not in list'

layout = {
    ...
    'remove': {
        '?': 'Get rid of stuff',
        'item': {
            '?': 'Remove item to shopping list',
            str: {
                '?': ('item', 'Item to remove'),
                None: remove_item,
            },
        },
    },
    ...
}

Now our CLI shows:

> add item bananas
> add item oranges
> add item strawberries
> show list
Items:
bananas
oranges
strawberries
> remove ?
Possible completions:
  item   Remove item from shopping list
> remove item ?
Possible completions:
  <item>   Item to remove
> remove item apples
Item not in list
> remove item oranges
> show list
Items:
bananas
strawberries
>

That works, but it would be great if we could offer completion of items that have already been added to the list when removing them... and we can! First, we need a function to provide a list of the items in the shopping list (again, it takes the JSH instance as the first argument, and any arbitrary arguments that preceed it in the command - in this case, none). As we’re storing our shopping list as a list already, this is pretty easy:

def complete_items(cli):
    return shopping_list

And now we integrate this into the layout using the '\t' key, which signifies that this function should be called when searching for a list of valid completions:

layout = {
    ...
    'remove': {
        '?': 'Get rid of stuff',
        'item': {
            '?': 'Remove item from shopping list',
            '\t': complete_items,
            str: {
                '?': ('item', 'Item to remove'),
                None: remove_item
            },
        },
    },
    ...
}

Finally, the items already in the shopping list appear in the list of possible completions when removing an item:

> add item carrots
> add item courgettes
> add item beetroot
> show list
Items:
carrots
courgettes
beetroot
> remove item ?
Possible completions:
  <item>       Item to remove
  beetroot
  carrots
  courgettes
> remove item c?
Possible completions:
  <item>       Item to remove
  carrots
  courgettes
> remove item carrots
> show list
Items:
courgettes
beetroot
>

Note

It’s also possible for the completion function to return a dictionary. In this case, the keys are the possible completions and the values are used as descriptions in the help output.

And that’s it - you’ve built your first CLI with JSH, and it wasn’t all that hard. Check out the other options available to you by reading the rest of this documentation.