Welcome to JSH’s documentation¶
JSH is a Junos-inspired CLI library for your Python apps. If you’ve ever logged into a Junos device, you’ll know how good the CLI is. It offers:
- tab-completion, including completion of names of items in the config
- help by pressing ”?” at any point
- completion on pressing either space, tab or enter
JSH attempts to reproduce some of these features (and others) in a Python library based on Readline, to allow you to build better quality CLIs for your apps.
Requirements¶
- Python 2.6+
Installation¶
Install from PyPI using pip install jsh
.
Basic Usage¶
The library takes a CLI “layout”, which is a dictionary-based tree structure
describing your CLI commands. For example, a completely useless CLI with
just an exit
command, you would define it like this:
import jsh
layout = {
'exit': jsh.exit,
}
jsh.run(layout)
jsh.run
is a shortcut for the following:
cli = jsh.JSH(layout)
while True:
try:
cli.read_and_execute()
except jsh.JSHError as err:
print err
except EOFError:
break
This creates a basic layout with a single available command (exit
), passes
it to an instance jsh.JSH
, and starts an infinite loop, using the read_and_execute
method of the JSH
CLI object to interact with the user. For more control
over this loop, you should write your own instead of using jsh.run
.
This provides a CLI that looks like the following:
> ?
Possible completions:
exit
> ex?
Possible completions:
exit
> exit ?
Possible completions:
<[Enter]> Execute this command
> exit
Full Documentation¶
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.
Command Options¶
The following options are available as keys in the layout dictionary.
-
'?'
A string containing a description of what the command at this level does, used in the help text output.
-
None
¶ The handler function or method to call when the command at this point is executed by pressing Enter. The function is passed at least one argument (the
JSH
instance), and any other arbitrary strings entered by the user previously in the command (seestr
below).
-
'\t'
A completion function that should return either a list of available completions at this point in the command, or a dictionary of available tab completions and their descriptions. The function is passed at least one argument (the
JSH
instance), and any other arbitrary strings entered by the user previously in the command (seestr
below).
-
str
¶ A level in the layout tree that accepts an arbitrary user string instead of a pre-defined command. Like any other level of the tree, the value can either be a single function (which will be executed as in the
None
key above), or a dictionary representing the next level of the tree.
-
'_validate'
A function that is passed the prior token in the command and returns either
True
(if the token is valid) or a string containing an error message if not. Designed to be used under thestr
key, this validates the user-defined input and will stop the user tab completing an invalid value. JSH provides some built-in validators, see validators for more details.
-
'_kwarg'
A string containing the name of a keyword argument, or
None
. This flag is designed to be used under thestr
key, and will pass the user-defined input as a keyword argument to the final handler function instead of as a plain argument. This allows you to decouple the handler function’s signature from the layout tree. IfNone
, the name of the token prior to thestr
token is used as the keyword.
-
'_hidden'
Default:
False
A boolean value determining whether this command should be shown in completion and help output. Can be used to implement hidden commands that are only available if the user knows they are there.
Validators¶
JSH provides the following built-in validators for use with the _validate
option.
-
validate_int
¶ Validates that the provided string is an integer.
import jsh def print_num(cli, num): print 'User entered {}'.format(num) layout = { ... 'num': { '?': 'A number', 'str': { '?': ('num', 'A number'), '_validate': jsh.validate_int, None, print_num, }, }, ... }
Produces the following CLI:
> ? Possible completions: num Enter a number > num ? Possible completions: num A number > number foo Invalid argument: 'foo' is not a valid integer. > num 5 User entered 5 >
-
validate_range
(min, max)¶ Validates that the provided string is an integer in a given range. Takes two integer arguments
min
andmax
which the entered integer must be between (inclusive).import jsh def print_num(cli, num): print 'User entered {}'.format(num) layout = { ... 'num': { '?': 'A number', 'str': { '?': ('num', 'A number 2..5'), '_validate': jsh.validate_range(2, 5), None, print_num, }, }, ... }
Produces the following CLI:
> ? Possible completions: num Enter a number > num ? Possible completions: num A number 2..5 > number foo Invalid argument: Value 'foo' is not within range (2, 5) > num 10 Invalid argument: Value 10 is not within range (2, 5) > num 3 User entered 3
-
validate_in
(iter)¶ Validates that the provided string is one of a given list. Takes an iterable of strings, and validates that the user entered string is one of them.
import jsh def print_data(cli, data): print 'User entered {}'.format(data) layout = { ... 'foo': { '?': 'Something', 'str': { '?': ('data', 'Something'), '_validate': jsh.validate(['one', 'two', 'three']), None, print_data, }, }, ... }
Produces the following CLI:
> ? Possible completions: foo Something > foo ? Possible completions: data Something > foo four Invalid argument: 'four' is not valid. Choices are: one, two, three > num one User entered one
Sections¶
Another feature, inspired not by the Junos CLI, but by the F5 CLI is sections. Sections let the user focus on a particular part of the CLI. Taking the example from the walkthrough, we can focus on the items in the shopping list.
Let’s add some commands to our layout to handle this:
layout = {
'/': {
'?': 'Go to top level',
None: jsh.set_section(None)
},
'/item': {
'?': 'Work on items',
None: jsh.set_section('item')
},
'add': {
'?': 'Add stuff',
'item': {
'?': 'Add item to shopping list',
str: {
'?': ('item', 'Item description'),
None: add_item
}
}
},
'remove': {
'?': 'Get rid of stuff',
'item': {
'?': 'Remove item from shopping list',
'\t': complete_items,
str: {
'?': ('item', 'Item to remove'),
None: remove_item
}
}
},
'show': {
'?': 'Display various information',
'list': {
'?': 'Display shopping list',
None: show_list
},
'pid': {
'?': 'Display my PID',
None: show_pid
},
'version': {
'?': 'Display my version',
None: show_version
}
},
'exit': {
'?': 'Quit this silly application',
None: jsh.exit
}
}
This now lets us interact with the CLI like this:
> ?
Possible completions:
/ Go to top level
/item Work on items
add Add stuff
exit Quit this silly application
remove Get rid of stuff
show Display various information
> add ?
Possible completions:
item Add item to shopping list
> /item
> add ?
Possible completions:
<item> Item description
> add carrots
> add potatoes
> show list
Items:
carrots
potatoes
> remove potatoes
> show list
Items:
carrots
>
Being inside the “item” section means that we can (and, in fact, must)
miss out the second word of a command when that word is item
.
Finally, it would be nice if the CLI told us which section we are currently
in. We can do this by customising the prompt and including the string
{section}
in it, which will be replaced by the name of the current
section:
cli = jsh.JSH(
layout,
prompt='shopping{section}> '
)
This gives us this:
shopping> /item
shopping(item)> /
shopping>
We can customise the brackets around the section name, for example:
cli = jsh.JSH(
layout,
prompt='shopping{section}> ',
section_delims=('/', '')
)
This gives:
shopping> /item
shopping/item> /
shopping>
However, section support is quite basic at the moment and needs more work. It’s currently nowhere near what the F5 CLI does.
CLI Options¶
The following options are available as arguments to the jsh.JSH
object to customise
the CLI for your usage.
-
prompt
¶ Default:
'> '
A string containing the prompt to display before every command. If using sections, the use of
{section}
within the string will be replaced with the section the user is currently inside.
-
section_delims
¶ Default:
('(', ')')
A tuple of two strings to wrap around the section name when sections are used and the prompt contains
{section}
.
-
ignore_case
¶ Default:
False
A boolean to control whether or not the CLI is case-sensitive when completing commands.
-
complete_on_space
¶ Default:
True
A boolean to control whether or not a partially-entered command is completed when the user presses space.