Skip to main content

a declarative, modular, lightweight, and versatile python library for building cli applications

Project description

Tests PyPI - License GitHub top language PyPI - Python Version PyPI version PyPI - Wheel

befehl

befehl (german for command) is a

  • declarative
  • modular (easily reuse definitions),
  • lightweight (no external dependencies), and
  • versatile (highly customizable behavior through custom parsers and validation)

python library for building CLI applications.

It features

  • a modern, declarative API,
  • QoL features like short-option grouping,
  • automatic generation of help-options, and
  • generation of bash-autocomplete source files.

Example

from befehl import Parser, Option, Argument, Command, Cli

# define subcommand
class MySubCommand(Command):
    opt = Option("--sub-option")
    arg = Argument("subarg", nargs=-1)

    def run(self, args):
        # run business logic on parsed input
        # ...

    def validate(self, args):
        # perform custom validation on parsed input
        # ...

# define base-command
class MyCli(Cli):
    cmd = MySubCommand("subcommand")

    opt0 = Option(("-o", "--option-zero"))
    opt1 = Option(("-p", "--option-one"))

    arg0 = Argument("arg", parser=Parser.parse_as_path)

    def run(self, args):
        # run business logic on parsed input
        # ...

    def validate(self, args):
        # perform custom validation on parsed input
        # ...

# validate + build entry-point
cli = MyCli("my-cli").build()

Documentation

Command declaration

One of the few limitation imposed by this library is that all CLIs have the following structure:

[command] [subcommand1 [subcommand 2 [...]]] [options] [--] [arguments]

When defining a CLI, the command-tree is built from classes inheriting from Command. A Command-class encapsulates all (immediate) subcommands (i.e., instances of previously defined Commands), Options, and Arguments as class attributes:

class MySubCommand(Command):
    ...

class MyCli(Cli):
    cmd = MySubCommand("subcommand")

    opt0 = Option(("-o", "--option-zero"))
    opt1 = Option(("-p", "--option-one"))

    arg0 = Argument("arg", parser=Parser.parse_as_path)

    ...

(Cli is an optional alias for Command)

Business logic

A Command's business logic is defined in its run method, e.g.,

class MyCli(Cli):
    ...

    def run(self, args):
        print(args)

When being executed, this function receives a singular argument args: dict[Option | Argument, list[Any]], where values are lists of parsed values.

For example, invoking the Command from above with a call like

command -o path/to/file

results in an args-mapping of

args = {
    MyCli.opt0: [],
    MyCli.arg0: [Path(path/to/file)]
}

Multiple values to a single Option or Argument keep their order in the generated list.

Validation

Optionally, before entering the business-logic, a validation-step can be defined. To this end, the empty validate method of the Command class can be overwritten. For example, the following definition validates that, if -o is given, option -p is needed as well.

class MyCli(Cli):
    ...

    def validate(self, args):
        if self.opt0 in args and self.opt1 not in args:
            return (
                False,
                f"option {self.opt0} also requires option {self.opt1}"
            )
        return True, None

Invoking the cli with command -o returns with the above message (and exit code 1) whereas command -op continues past the validation into the run-method.

Build

In order to create a callable function that can be used as an entry-point for python packages, a build-step has to be performed.

For example, for the above CLI, one can enter

...

cli = MyCli("my-cli").build(help_=True, completion=False)

The variable cli then serves as the entry-point. Suppose, this variable is in the namespace of the module cli, then it can be used with, for example, setuptools as

setup(
    ...
    entry_points={
        "console_scripts": [
            "command = cli:cli",
        ],
    },
    ...
)

When building, it has to be decided, whether

  • a help-option (-h, --help) should be generated (enabled by default)
  • an option (--generate-autocomplete) for generating a sourcable bash-autocomplete script should be added (disabled by default; enabled if environment sets _BEFEHL_COMPLETION). See this section for details.

Parsers

Both Options and Arguments accept keyword arguments for a parser. A parser is a function that accepts a single string-value and returns a tuple of

  • boolean (whether input is valid),
  • string (message in case the input is invalid), and
  • any object (parsed data in case of success).

This library provides some basic parsers to be used in Options and Arguments. These range from parsing of primitive types like boolean or integer to more involved parsers for paths/files/directories (using pathlib.Path) or requiring values to satisfy regular expressions.

Pre-defined parsers are accessible via the abstract collection class Parser and can be accessed via its static methods like

class AnotherCommand(Command):
    opt = Option("-o", parser=Parser.parse_as_int)
    arg = Argument("arg", parser=Parser.parse_with_regex(r"[0-9]+"))

    ...

Lastly, by using the methods Parser.first or Parser.chain, multiple parsers can be applied to single values.

Other features

(Short) Option grouping

This library supports Options in short and long format:

  • short corresponds to starting with single - followed by a single character
  • long Options start with -- followed by at least one character

For convenience, Options with short name and nargs=0 can be grouped. For example, when using the Command from above, the two inputs

command -o -p

and

command -op

are equivalent.

Equal-sign syntax

In order to avoid problems with Option values starting with - (could be ambiguous regarding other Options), Options can be used with the following syntax

command --delta=-1

(instead of command --delta -1 which would fail).

Separator for options and arguments

Similar to the problem described in the previous section: In order to avoid problems with Argument values starting with - (could be ambiguous regarding Options), the separator -- can be used like

command -o -- -1

(instead of command -o -1 which would fail).

Autocomplete

This library can generate shell-script files that, when sourced in bash, enable basic autocomplete functionality. The script can be built by first setting the completion-option during build and then call the CLI with the --generate-autocomplete option (will be printed to stdout).

The auto-completion can also be sourced immediately by entering

eval "$(_BEFEHL_COMPLETION= <entry-point> --generate-autocomplete)"

(replace <entry-point> with your custom entry-point).

Tests

Automated (pytest-)tests can be run by first installing this package as well as its dev-dependencies via

pip install .
pip install -r dev-requirements.txt

Afterwards, simply enter

pytest

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

befehl-0.1.0.tar.gz (18.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

befehl-0.1.0-py3-none-any.whl (15.5 kB view details)

Uploaded Python 3

File details

Details for the file befehl-0.1.0.tar.gz.

File metadata

  • Download URL: befehl-0.1.0.tar.gz
  • Upload date:
  • Size: 18.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.12.3

File hashes

Hashes for befehl-0.1.0.tar.gz
Algorithm Hash digest
SHA256 0856965af44a903626d66a7ce66e7b95dfed937df0be6ed3c7a8f065ee53d3d3
MD5 610bb0310edce3954e153445f0294c85
BLAKE2b-256 df8ce971f8ab81f6c20e9342e723ba93f5498738eda908d6500b3068f12f07a7

See more details on using hashes here.

File details

Details for the file befehl-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: befehl-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 15.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.12.3

File hashes

Hashes for befehl-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 fd223b1c5d7663e1b8057a20c22b1e64d0973ccd43c11499d0599b56070a3670
MD5 06bf47873d94eddefd28e010acdb32e1
BLAKE2b-256 5111dcd4e40d796bdd36d356b747e2f7e8450381c9943dd994a6f60dcb3e8217

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page