- adding options and arguments
- adding a default value
- possible mistake: forgetting the decorator
- possible mistake: using periods in python command names
- shipping data along with the command
- when the command breaks
To create a python command, you can simply call the following command.
clk command create python mycommand
Your editor will be used to first edit the command. This command will already contain some code to get you started.
Note that you can always get the help of any command using --help. So don’t hesitate to try.
clk command create python --help
Usage: clk command create python [OPTIONS] NAME
Create a python custom command
This is a built-in command.
Positional arguments:
NAME The name of the new command
Options:
--open / --no-open Also open the file after its creation, else print where it is [default: open]
--force Overwrite a file if it already exists
--group / --command Bootstrap a command or a group of commands [default: command]
--with-data Create a directory module instead of a single file. So that you can ship data with it
--body TEXT The initial body to put [default: ""]
--description TEXT The initial description to put [default: Description]
--from-file TEXT Copy this file instead of using the template
--help-all Show the full help message, automatic options included.
--help Show this message and exit.
Let’s look at the file that was created.
cat $(clk command which mycommand)
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
from pathlib import Path
import click
from clk.decorators import (
argument,
flag,
option,
command,
use_settings,
table_format,
table_fields,
)
from clk.lib import (
TablePrinter,
call,
)
from clk.config import config
from clk.log import get_logger
from clk.types import DynamicChoice
LOGGER = get_logger(__name__)
@command()
def mycommand():
"Description"
The @command() decorator is provided by clk. It is a thin wrapper around the click @command() decorator that adds some features like automatic option handling.
Let’s run this command.
clk mycommand
warning: The command 'mycommand' has no documentation
If you keep the word Description in the help message, clk will warn you that you should replace it with something more interesting.
Let’s write something in here.
sed -i 's/"Description"/"Command that says something"/g' "$(clk command which mycommand)"
clk mycommand --help | head -10
Usage: clk mycommand [OPTIONS]
Command that says something
Edit this custom command by running `clk command edit mycommand`
Or edit ./clk-root/python/mycommand.py directly.
Options:
--help-all Show the full help message, automatic options included.
--help Show this message and exit.
Let’s make this command say something.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from clk.decorators import command
@command()
def mycommand():
"Command that says something"
print("something")
clk mycommand
something
adding options and arguments
You can add options and arguments to your command using click decorators. clk provides wrappers for them in clk.decorators.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from clk.decorators import command, argument
@command()
@argument("name", help="Your name")
def mycommand(name):
"A command that requires a name"
print(f"Hello, {name}!")
An argument is required by default. Calling the command without it produces an error.
clk mycommand 2>&1
Usage: clk mycommand [OPTIONS] NAME
error: Missing argument 'NAME'.
Providing the argument works as expected.
clk mycommand World
Hello, World!
adding a default value
You can make an argument optional by providing a default value. You can also add options and flags.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from clk.decorators import command, option, argument
@command()
@option("--name", default="world", help="Name to greet")
@option("--use-name", default="world", help="Name to greet", deprecated="since forever")
@argument("greeting", default="Hello")
def mycommand(name, greeting, use_name):
"A greeting command"
name = name or use_name
print(f"{greeting}, {name}!")
clk mycommand
warning: The parameter 'greeting' in the command 'mycommand' has no documentation
Hello, world!
clk mycommand --name clk Goodbye
warning: The parameter 'greeting' in the command 'mycommand' has no documentation
Goodbye, clk!
clk mycommand --use-name clk Goodbye
DeprecationWarning: The option 'use_name' is deprecated. since forever warning: The parameter 'greeting' in the command 'mycommand' has no documentation Goodbye, world!
That warning about greeting shows in the help as well.
clk mycommand --help 2>/dev/null
Usage: clk mycommand [OPTIONS] [GREETING]
A greeting command
Edit this custom command by running `clk command edit mycommand`
Or edit ./clk-root/python/mycommand.py directly.
Options:
--name TEXT Name to greet [default: world]
--use-name TEXT Name to greet (DEPRECATED: since forever) [default: world]
--help-all Show the full help message, automatic options included.
--help Show this message and exit.
Give it a help of its own.
@argument("greeting", default="Hello", help="What to say")
clk mycommand Goodbye
clk mycommand --help | grep -A1 "Positional arguments:"
Goodbye, world!
Positional arguments:
[GREETING] What to say [default: Hello]
possible mistake: forgetting the decorator
When creating python custom commands manually, you need to use the @command() or @group() decorator from clk. If you forget to do so, clk will provide a helpful error message. Note however that if you use clk command create, you should not worry about that.
Let’s create a python file that defines a function but forgets to decorate it.
def pyenv():
"""My environment command"""
print("hello")
Now try reaching it.
clk py<TAB>
python
It does not come up. That is the hint that something is wrong with it, and running it says what.
clk pyenv 2>&1|sed "s|$(pwd)|.|"
error: Found the command pyenv in the resolver customcommand but could not load it. warning: Failed to get the command pyenv: The file ./clk-root/python/pyenv.py must contain a click command or group named pyenv, but found a function instead. Did you forget the @command or @group decorator? error: clk.pyenv could not be loaded. Re run with clk --develop to see the stacktrace or clk --debug-on-command-load-error to debug the load error
Let’s take a look at the stack trace with --develop.
clk --develop pyenv 2>&1 | grep -o 'raise BadCustomCommandError('
raise BadCustomCommandError(
Of course, you won’t be able to complete on that command.
clk pyenv --<TAB>
To fix this, simply add the @command() decorator from clk.
from clk.decorators import command
@command()
def pyenv():
"""My environment command"""
print("hello")
Now the command works as expected.
clk pyenv
hello
possible mistake: using periods in python command names
Unlike bash commands, where the period character can be used to put a command inside a group (e.g. somegroup.somecommand), python command names cannot contain periods. The name is used as a Python identifier, so periods would produce invalid code.
clk command create python something.with.periods 2>&1
Usage: clk command create python [OPTIONS] NAME
error: 'something.with.periods' is not a valid Python command name (it contains periods). Python command names must be valid Python identifiers. If you want to create a command inside a group, first create the group with 'clk command create python --group mygroup', then add the command inside it.
To create a command inside a group, first create the group as a python command with --group, then edit it to add the child command inside.
clk command create python --group mygroup
Then edit the group file to add the child command.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from clk.decorators import group, command
@group()
def mygroup():
"My group of commands"
@mygroup.command()
def child():
"A command inside mygroup"
print("hello from mygroup child")
clk mygroup child
hello from mygroup child
shipping data along with the command
A command sometimes needs files of its own: a template, a key, a picture. With --with-data, it becomes a folder instead of a single file, and those files live in it.
from pathlib import Path
from clk.decorators import command
@command()
def greet():
"""Greet the way this machine greets"""
print((Path(__file__).parent / "greeting.txt").read_text().strip())
clk command create python greet --with-data --description "Greet someone" --body '
from pathlib import Path
from clk.decorators import command
@command()
def greet():
"""Greet the way this machine greets"""
print((Path(__file__).parent / "greeting.txt").read_text().strip())
'
It is a package now, so the command is its __init__.py.
clk command which greet | sed "s|$(pwd)|.|"
./clk-root/python/greet/__init__.py
Put the file it wants next to it.
echo "Hello, and welcome aboard" > "$(dirname "$(clk command which greet)")/greeting.txt"
clk greet
Hello, and welcome aboard
when the command breaks
Some days you sketch a command and leave the hard part for later.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from clk.decorators import command
@command()
def notyet():
"Not written yet"
raise NotImplementedError("the hard part")
Whoever runs it is told where to send the report.
clk notyet
This command reached a part of the code yet to implement. Please help us by either submitting patches or sending report files to us. (clk --report-file .../somefile RESTOFCOMMAND, then send .../somefile to us on https://github.com/clk-project/clk/issues/new)
error: the hard part
Other days it breaks in a way nobody saw coming, and clk says as much rather than pretending.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from clk.decorators import command
@command()
def boom():
"Breaks"
raise ValueError("chaos")
clk boom 2>&1 | tail -1
error: Hmm, it looks like we did not properly catch this error. Please help us improve clk by telling us what caused the error on https://github.com/clk-project/clk/issues/new . If you feel like a pythonista, you can try debugging the issue yourself, running the command with clk --post-mortem or clk --develop