Skip to content

Getting Started with Clak

This guide will walk you through the basics of building command-line applications using Clak. We'll start with a minimal example and gradually explore more advanced features.

Base Concepts

Clak is a Python library that simplifies the creation of command-line applications by providing an elegant, class-based interface on top of Python's argparse. The main components are:

  • Parser: The base class for your application
  • Argument: Class to define command-line arguments
  • Command: (Not shown in basic examples) Used for subcommands

Minimal Example

Let's start with a minimal example called script1.py, that demonstrates the core concepts:

script1.py
#!/usr/bin/env python3

from clak import Argument, Parser  # (1)!


class AppMain(Parser):  # (2)!
    """Demo application with two arguments."""  # (3)!

    config = Argument(
        "--config", "-c", help="Config file path", default="config.yaml"  # (4)!
    )
    name = Argument("NAME", help="First Name", nargs="?")  # (5)!

    def cli_run(self, config=None, name=None, **_):  # (6)!
        """The cli_run method holds the code to run"""

        if name is None:
            print("No name provided, please provide a name as first argument.")
            self.cli_exit(1)  # (7)!

        print(f"Store name '{name}' in config file: {config}")


if __name__ == "__main__":
    AppMain()  # (8)!
  1. Import Parser and Argument from clak.
  2. Root parser class — must inherit from Parser.
  3. Class docstring becomes the application help description.
  4. Optional argument (-- / -); same kwargs as argparse.add_argument().
  5. Positional argument (name without - / --).
  6. cli_run() runs when the command is invoked; parameters match destinations.
  7. cli_exit(1) sets a non-zero exit code (see Parser API).
  8. Instantiating the root parses argv and runs the command (auto-dispatch()).

From this example, run it with the Python interpreter. Clak adds -h / --help automatically:

$ python script1.py --help
usage: script1.py [-h] [--config CONFIG] [NAME]

Demo application with two arguments.

positional arguments:
  NAME                        First Name (default: None)

options:
  -h, --help                  show this help message and exit
  --config CONFIG, -c CONFIG  Config file path (default: config.yaml)

Thus, we can try to call it:

$ python script1.py
No name provided, please provide a name as first argument.

$ python script1.py John
Store name 'John' in config file: config.yaml

$ python script1.py John Doe
usage: script1.py [-h] [--config CONFIG] [NAME]
script1.py: error: unrecognized arguments: Doe

It is also possible to run directly if you make your script executable:

$ chmod +x script1.py

# You can directly run it
$ ./script1.py --help
$ ./script1.py
$ ./script1.py John
$ ./script1.py John Doe

This example shows:

  1. Creating a basic application class that inherits from Parser
  2. Defining two arguments:
    • An optional --config argument with a default value
    • A positional NAME argument that's optional (nargs="?")
  3. Implementing the cli_run method that receives the parsed arguments

Advanced Arguments Example

Let's update our previous example with more fields:

script2.py
#!/usr/bin/env python3

from clak import Argument, Parser  # (1)!


class AppMain(Parser):
    """Demo application with many arguments."""

    # Boolean flag
    force = Argument("--force", "-f", action="store_true", help="Force mode")  # (2)!

    # String with default
    config = Argument("-c", help="Config file path", default="config.yaml")  # (3)!

    # Choice from predefined options
    color = Argument(
        "--color",  # (4)!
        choices=["red", "green", "blue", "unknown"],
        default="unknown",
        help="Favorite color",
    )

    # List of items (can be specified multiple times)
    items = Argument("--items", "-m", action="append", help="Preferred items")  # (5)!

    # Required positional argument
    name = Argument("NAME", help="First Name")  # (6)!

    # Optional positional with default
    surname = Argument("SURNAME", nargs="?", default="Doe", help="Last Name")  # (7)!

    # List of remaining arguments
    aliases = Argument(
        "ALIAS", nargs="*", default=["Bond", "agent 007"], help="Aliases"  # (8)!
    )

    def cli_run(  # (9)!
        self,
        name=None,
        surname=None,
        color=None,
        aliases=None,
        items=None,
        force=False,
        config=None,
        **_,
    ):
        """Run the application"""
        if force:
            print(f"Force mode update config file: {config}")
        else:
            print(f"No force mode, config file: {config}")

        print(f"Identity: {name} {surname or 'MISSING_SURNAME'}")
        print(f"Favorite color is: {color}")

        for alias in aliases or []:
            print(f"  Alias: {alias}")

        for item in items or []:
            print(f"  Item: {item}")


if __name__ == "__main__":
    AppMain()  # (10)!
  1. Import Parser and Argument classes from clak

  2. Boolean Flags (force):

    • Uses action="store_true" to create a flag
    • No value needed, just presence/absence
    • Use both long and short form
  3. String Options (config):

    • Basic string argument with default value
    • Can be specified with -c
    • Use short form only
  4. Choice Options (color):

    • Restricts input to predefined choices
    • Provides validation out of the box
    • Can be specified with --color
    • Use long form only
  5. List Arguments (items):

    • Uses action="append" to collect multiple values
    • Can be specified multiple times: -m item1 -m item2
  6. Positional Arguments:

    • Required (name): Must be provided
  7. Optional Positional Arguments (surname):

    • Optional with default (surname): Uses nargs="?" and default
  8. Variable number of arguments (aliases):

    • Variable number (aliases): Uses nargs="*" for zero or more
  9. Run method (cli_run):

    • This method is executed when the command is run.
    • It receives the parsed arguments as keyword arguments.
    • You can directly use them in your code.
  10. Instantiate App (AppMain):

    • Instantiating the root starts parsing and runs the matched command

When executed with -h or --help:

$ python script2.py -h
usage: script2.py [-h] [--force] [-c CONFIG] [--color {red,green,blue,unknown}] [--items ITEMS]
                  NAME [SURNAME] [ALIAS ...]

Demo application with many arguments.

positional arguments:
  NAME                        First Name
  SURNAME                     Last Name (default: Doe)
  ALIAS                       Aliases (default: ['Bond', 'agent 007'])

options:
  -h, --help                  show this help message and exit
  --force, -f                 Force mode (default: False)
  -c CONFIG                   Config file path (default: config.yaml)
  --color {red,green,blue,unknown}
                              Favorite color (default: unknown)
  --items ITEMS, -m ITEMS     Preferred items (default: None)

Example Usage

# Basic usage
python script2.py John Smith

# With options
python script2.py --force --config custom.yaml John Smith

# With color choice
python script2.py --color red John Smith

# With multiple items
python script2.py --items apple --items banana John Smith

# With aliases
python script2.py John Smith nickname1 nickname2 nickname3

# Full example
python script2.py --force --config custom.yaml --color blue \
                --items apple --items banana \
                John Smith nickname1 nickname2

Optional Arg and Opt helpers

Argument is the canonical descriptor and still accepts both flags and positionals. Opt and Arg are optional sugar if you want that difference checked when the class is defined:

  • Opt(...) : option flags only (- / --); empty Opt() derives --attr from the field name
  • Arg(...) : positional names only (no leading -); at least one name is required

Mixing names raises ValueError (Arg("--flag"), Opt("NAME"), or both kinds in one call). Same kwargs as Argument / add_argument(). You never have to use them.

The previous example, rewritten:

from clak import Arg, Opt, Parser


class AppMain(Parser):
    """Demo application with many arguments."""

    force = Opt("--force", "-f", action="store_true", help="Force mode")
    config = Opt("-c", help="Config file path", default="config.yaml")
    color = Opt(
        "--color",
        choices=["red", "green", "blue", "unknown"],
        default="unknown",
        help="Favorite color",
    )
    items = Opt("--items", "-m", action="append", help="Preferred items")
    name = Arg("NAME", help="First Name")
    surname = Arg("SURNAME", nargs="?", default="Doe", help="Last Name")
    aliases = Arg(
        "ALIAS", nargs="*", default=["Bond", "agent 007"], help="Aliases"
    )

    def cli_run(self, name=None, surname=None, **_):
        print(f"Identity: {name} {surname}")

Next Steps

Next: Nested command-line applications — subcommands and multi-level trees like git or docker.