cliffy simplifies the creation, management, and deployment of CLIs. Define your CLI's structure and behavior in a YAML manifest, and let cliffy handle the rest.
- Write CLIs with YAML manifests
- Manage CLIs- load, test, update, list, and remove
- Built-in shell and Python scripting support
- Supports Jinja2 templating
- Hot-reload CLIs on manifest changes for easier development
- Build CLIs into self-contained, single-file portable zipapps for sharing
- Define a manifest
# hello.yaml
name: hello
version: 0.1.0
commands:
shell: $echo "hello from shell"
python: print("hello from python")
- Load CLI
$ cli load hello.yaml
Parses hello.yaml
to generate a Typer CLI and load it into the running Python environment.
- Run CLI directly
hello -h
For more examples, check examples directory.
- Define a manifest
# requires.yaml
name: requires
version: 0.1.0
requires:
- requests >= 2.30.0
- six
imports:
- import six
commands:
shell: $echo "hello from shell"
python: print("hello from python")
py: |
if six.PY2:
print("python 2")
if six.PY3:
print("python 3")
- Build CLI
$ cli build requires.yaml -o dist
Builds a portable zipapp containing the CLI and its package requirements.
- Run CLI
./dist/requires -h
cli <command>
init <cli name> --raw
: Generate a template CLI manifest for a new CLIload <manifest>
: Add a new CLI based on the manifestrender <manifest>
: View generated CLI script for a manifestlist
orls
: Output a list of loaded CLIsupdate <cli name>
: Reload a loaded CLIremove <cli name>
orrm <cli name>
: Remove a loaded CLIrun <manifest> -- <args>
: Runs a CLI manifest as a one-time operationbuild <cli name or manifest>
: Build a CLI manifest or a loaded CLI into a self-contained zipappinfo <cli name>
: Display CLI metadatadev <manifest>
: Start hot-reloader for a manifest for active development
- Define CLI manifests in YAML files
- Run
cli
commands to load, build, and manage CLIs - When loaded, cliffy parses the manifest and generates a Typer CLI that is deployed directly as a script
- Any code starting with
$
will translate to subprocess calls via PyBash - Run loaded CLIs straight from the terminal
- When ready to share, run
build
to generate portable zipapps built with Shiv
Cliffy can be installed using either pip or uv package managers.
pip install "cliffy[rich]"
to include rich-click for colorful CLI help output formatted with rich.
or
pip install cliffy
to use the default help output.
uvx --from cliffy cli --help
- Load:
uvx --from cliffy cli load examples/hello.yaml
- Run:
uvx --from cliffy hello
Generated by cli init
. For a barebones template, run cli init --raw
manifestVersion: v2
# The name of the CLI, used when invoking from command line.
name: cliffy
# CLI version
version: 0.1.0
# Brief description of the CLI
help: A brief description of your CLI
# List of Python package dependencies for the CLI.Supports requirements specifier syntax.
requires: []
# - requests>=2.25.1
# - pyyaml~=5.4
# List of external CLI manifests to include.Performs a deep merge of manifests sequentially in the order given to assemble a merged manifest
# and finally, deep merges the merged manifest with this manifest.
includes: []
# - path/to/other/manifest.yaml
# Mapping defining manifest variables that can be referenced in any other blocks
# Environments variables can be used in this section with ${some_env_var} for dynamic parsing
# Supports jinja2 formatted expressions as values
# Interpolate defined vars in other blocks jinja2-styled {{ var_name }}.
vars:
data_file: "data.json"
debug_mode: "{{ env['DEBUG'] or 'False' }}"
# String block or list of strings containing any module imports
# These can be used to import any python modules that the CLI depends on.
imports: |
import json
import os
from pathlib import Path
# List of helper function definitions
# These functions should be defined as strings that can be executed by the Python interpreter.
functions:
- |
def load_data() -> dict:
data_path = Path("{{ data_file }}")
if data_path.exists():
with data_path.open() as f:
return json.load(f)
return {}
- |
def save_data(data):
with open("{{data_file}}", "w") as f:
json.dump(data, f, indent=2)
# A mapping containing any shared type definitions
# These types can be referenced by name in the args section to provide type annotations for params and options defined in the args section.
types:
Filename: str = typer.Argument(..., help="Name of the file to process")
Verbose: bool = typer.Option(False, "--verbose", "-v", help="Enable verbose output")
# Arguments applied to all commands
global_args:
- verbose: Verbose
# Reusable command templates
command_templates:
with_confirmation:
args:
- "yes": bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt")
pre_run: |
if not yes:
typer.confirm("Are you sure you want to proceed?", abort=True)
# A mapping containing the command definitions for the CLI
# Each command should have a unique key- which can be either a group command or nested subcommands
# Nested subcommands are joined by '.' in between each level
# Aliases for commands can be separated in the key by '|'
# A special '(*)' wildcard can be used to spread the subcommand to all group-level commands
commands:
hello:
help: Greet the user
args:
- name: str = typer.Option("World", "--name", "-n", help="Name to greet")
run: |
print(f"Hello, {name}!")
$ echo "i can also mix-and-match this command script to run shell commands"
file.process:
help: Process a file
args:
- filename: Filename
run: |
data = load_data()
print(f"Processing {filename}")
if verbose:
print("Verbose output enabled")
data["processed"] = [filename]
# Process the file here
save_data(data)
delete|rm:
help: Delete a file
template: with_confirmation
args: [filename: Filename]
run: |
if verbose:
print(f"Deleting {filename}")
os.remove(filename)
print("File deleted successfully")
# Additional CLI configuration options
cli_options:
rich_help_panel: True
# Test cases for commands
tests:
- hello --name Alice: assert 'Hello, Alice!' in result.output
- file process test.txt: assert 'Processing test.txt' in result.output