clk

A very opinionated framework to ease the creation of command line interfaces

View on GitHub

When you want to give some choice to the user of your application, you can make use of the click built-in Choice types.

a simple example to illustrate click.Choice

I have a clk command that allow me to automate some stuffs with some devices I have accumulated along the years.

The group of commands looks like this.

import os
from pathlib import Path

import click
from clk.config import config
from clk.decorators import group, option
from clk.lib import call, check_output
from clk.log import get_logger
LOGGER = get_logger(__name__)

devices = {
    'cink-peax': '192.168.1.10:5555',
    'i9300': '192.168.1.11:5555',
    'klipad': '192.168.1.12:5555',
}

@group()
@option(
    '--device',
    '-d',
    help='What device to connect to',
type=click.Choice(devices),
)
def android(device):
    'Play with android'
    config.override_env['ANDROID_DEVICE'] = device
    device = devices.get(device, device)
    config.override_env['ANDROID_SERIAL'] = device
    config.init()

And I create bash commands for stuff I want to do with my android devices, like getting the battery status.

clk command create bash android.battery.status --force --description "Print the battery level of the device" --body 'echo "Would call adb shell dumpsys battery|grep level|cut -f2 -d:|trim and would get the result for ${ANDROID_DEVICE} available at ${ANDROID_SERIAL}"'

With that code, I can now get the battery status of the cink-peax phone with

clk android -d cink-peax battery status
Would call adb shell dumpsys battery|grep level|cut -f2 -d:|trim and would get the result for cink-peax available at 192.168.1.10:5555

The good think with Choice is that it forces me to stick to the predefined choices.

For instance, if I use a name not known, I get

clk android -d cinkpeax battery status
Usage: clk android [OPTIONS] COMMAND [ARGS]...
error: Invalid value for '--device' / '-d': 'cinkpeax' is not one of 'cink-peax', 'i9300', 'klipad'.

showing the usage of Suggestion

Imagine now that you want to control a new device that you only have temporarily, it would be nice to simply provide its name and have clk use it verbatim. To do so, the Suggestion types might be a good candidate.

Instead of using click.Choice, we will use Suggestion. In case of mismatch, we will simply use the value as is.

import os
from pathlib import Path

import click
from clk.config import config
from clk.decorators import group, option
from clk.lib import call, check_output
from clk.log import get_logger
from clk.types import Suggestion
LOGGER = get_logger(__name__)

devices = {
    'cink-peax': '192.168.1.10:5555',
    'i9300': '192.168.1.11:5555',
    'klipad': '192.168.1.12:5555',
}

@group()
@option(
    '--device',
    '-d',
    help='What device to connect to',
    type=Suggestion(devices),
)
def android(device):
    'Play with android'
    config.override_env['ANDROID_DEVICE'] = device
    device = devices.get(device, device)
    config.override_env['ANDROID_SERIAL'] = device
    config.init()

Now, you can call something like this.

clk android -d 192.168.1.14:5555 battery status
Would call adb shell dumpsys battery|grep level|cut -f2 -d:|trim and would get the result for 192.168.1.14:5555 available at 192.168.1.14:5555

Of course, you don’t have the failsafe that provided click.Choice anymore. The predefined values are still available for completion though.

clk android -d kli<TAB>
klipad

showing the usage of DocumentedChoice

If you have several devices, you might want to have some more information about them in the help. To do that, make use of DocumentedChoice.

import os
from pathlib import Path

import click
from clk.config import config
from clk.decorators import group, option
from clk.lib import call, check_output
from clk.log import get_logger
from clk.types import DocumentedChoice
docs = {
    'cink-peax': 'My pomodoro',
    'i9300': 'My vacuum automator',
    'klipad': 'The photo gallery',
}
LOGGER = get_logger(__name__)

devices = {
    'cink-peax': '192.168.1.10:5555',
    'i9300': '192.168.1.11:5555',
    'klipad': '192.168.1.12:5555',
}

@group()
@option(
    '--device',
    '-d',
    help='What device to connect to',
    type=DocumentedChoice(docs),
)
def android(device):
    'Play with android'
    config.override_env['ANDROID_DEVICE'] = device
    device = devices.get(device, device)
    config.override_env['ANDROID_SERIAL'] = device
    config.init()

Now, in case of error, you get a nicer message indicating the purpose of the devices.

clk android -d cinkpeax battery status
Usage: clk android [OPTIONS] COMMAND [ARGS]...
error: Invalid value for '--device' / '-d': 'cinkpeax'.
error: Choose from:
error:   cink-peax    My pomodoro
error:   i9300        My vacuum automator
error:   klipad       The photo gallery

Oups, I wanted to deal with my pomodoro device…

clk android -d cink-peax battery status
Would call adb shell dumpsys battery|grep level|cut -f2 -d:|trim and would get the result for cink-peax available at 192.168.1.10:5555

It would be even nicer if the documentation was shown in the result of --help or somehow in the completion, but it is not the case for now.

Yet, the completion still works like expected.

clk android -d i<TAB>
i9300

Note that we simply copied DocumentedChoice from the dying project click-completion. It will probably evolve to be more feature complete, or perhaps merged with Suggestion to allow providing both feature at the same time. Pull requests are more than welcome here!