{"id":37074164,"url":"https://github.com/stepan-anokhin/comandante","last_synced_at":"2026-01-14T08:43:57.330Z","repository":{"id":62563781,"uuid":"210865858","full_name":"stepan-anokhin/comandante","owner":"stepan-anokhin","description":"Comandante is a tool for building command-line interfaces in Python.","archived":false,"fork":false,"pushed_at":"2019-10-13T14:49:00.000Z","size":571,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2026-01-06T05:51:20.859Z","etag":null,"topics":["cli","cli-app","command-line","command-line-interface","command-line-parser","command-line-tool","python"],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/stepan-anokhin.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2019-09-25T14:29:38.000Z","updated_at":"2020-01-17T20:11:35.000Z","dependencies_parsed_at":"2022-11-03T16:00:31.788Z","dependency_job_id":null,"html_url":"https://github.com/stepan-anokhin/comandante","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/stepan-anokhin/comandante","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stepan-anokhin%2Fcomandante","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stepan-anokhin%2Fcomandante/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stepan-anokhin%2Fcomandante/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stepan-anokhin%2Fcomandante/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/stepan-anokhin","download_url":"https://codeload.github.com/stepan-anokhin/comandante/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stepan-anokhin%2Fcomandante/sbom","scorecard":{"id":851162,"data":{"date":"2025-08-11","repo":{"name":"github.com/stepan-anokhin/comandante","commit":"9fa7da93ae91e4a3c1cde4ad34b4a78edca5aa16"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":3,"checks":[{"name":"Packaging","score":-1,"reason":"packaging workflow not detected","details":["Warn: no GitHub/GitLab publishing workflow detected."],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#packaging"}},{"name":"Code-Review","score":0,"reason":"Found 0/30 approved changesets -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#code-review"}},{"name":"SAST","score":0,"reason":"no SAST tool detected","details":["Warn: no pull requests merged into dev branch"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}},{"name":"Token-Permissions","score":-1,"reason":"No tokens found","details":null,"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#token-permissions"}},{"name":"Dangerous-Workflow","score":-1,"reason":"no workflows found","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#dangerous-workflow"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#binary-artifacts"}},{"name":"Pinned-Dependencies","score":-1,"reason":"no dependencies found","details":null,"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#pinned-dependencies"}},{"name":"Maintained","score":0,"reason":"0 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#cii-best-practices"}},{"name":"Security-Policy","score":0,"reason":"security policy file not detected","details":["Warn: no security policy file detected","Warn: no security file to analyze","Warn: no security file to analyze","Warn: no security file to analyze"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#security-policy"}},{"name":"Vulnerabilities","score":10,"reason":"0 existing vulnerabilities detected","details":null,"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#fuzzing"}},{"name":"License","score":10,"reason":"license file detected","details":["Info: project has a license file: LICENSE:0","Info: FSF or OSI recognized license: MIT License: LICENSE:0"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#license"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#signed-releases"}},{"name":"Branch-Protection","score":0,"reason":"branch protection not enabled on development/release branches","details":["Warn: branch protection not enabled for branch 'master'"],"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#branch-protection"}}]},"last_synced_at":"2025-08-23T22:36:00.925Z","repository_id":62563781,"created_at":"2025-08-23T22:36:00.925Z","updated_at":"2025-08-23T22:36:00.925Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28414692,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-14T08:38:59.149Z","status":"ssl_error","status_checked_at":"2026-01-14T08:38:43.588Z","response_time":107,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cli","cli-app","command-line","command-line-interface","command-line-parser","command-line-tool","python"],"created_at":"2026-01-14T08:43:56.749Z","updated_at":"2026-01-14T08:43:57.308Z","avatar_url":"https://github.com/stepan-anokhin.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"\n\u003cp align=\"center\"\u003e\n    \u003cimg src=\"https://raw.githubusercontent.com/stepan-anokhin/comandante/master/logo.png\" width=\"500\" alt=\"Comandante Logo\"\u003e\n\u003c/p\u003e\n\nComandante is a toolkit for building command-line interfaces in Python.\nThe closest [Thor](https://github.com/erikhuda/thor) equivalent for Python.\n\n[![Build Status](https://travis-ci.org/stepan-anokhin/comandante.svg?branch=master)](https://travis-ci.org/stepan-anokhin/comandante)\n[![Coverage Status](https://coveralls.io/repos/github/stepan-anokhin/comandante/badge.svg?branch=master)](https://coveralls.io/github/stepan-anokhin/comandante?branch=master)\n[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/stepan-anokhin/comandante/blob/master/LICENSE)\n[![PyPI Version](https://img.shields.io/pypi/v/comandante.svg)](https://pypi.org/project/comandante/)\n[![Python Versions](https://img.shields.io/pypi/pyversions/comandante.svg)](https://pypi.org/project/comandante/)\n\n\n## Table of Contents\n- [Installation](#installation)\n- [Getting Started](#getting-started)\n  - [Example](#Example)\n  - [Just a Normal Classes and Methods!](#just-a-normal-classes-and-methods)\n- [Options](#options)\n  - [Command Options](#command-options)\n  - [Class Options](#class-options)\n- [Subcommands](#subcommands)\n- [Arguments](#arguments)\n  - [Type Library](#type-library)\n  - [Python 2](#python-2)\n- [Printing Help](#printing-help)\n- [Error Handling](#error-handling)\n- [Testing Your CLI](#testing-your-cli)\n- [Alternatives](#alternatives)\n\n## Installation\n\nTo get the latest release simply install it with a `pip`:\n\n```shell\npip install --upgrade comandante\n```\n\n## Getting Started\n\nSome command-line interfaces (like `pip`, `git`, `go`, etc.) \nhave a rich hierarchy of subcommands. Each subcommand may have\nits own set of arguments and options. Consider for example \n`git commit`, `git remote add \u003crepo\u003e`, `git remote rename \u003cold\u003e \u003cnew\u003e`\netc. *Comandante* is a Python library that makes building\nrich command-line interfaces (CLI) in Python extremely simple \nand straightforward. \n\n### Example\nHere is a simple example in **Python3**:\n```python\n#!/usr/bin/env python3\n# content of 'tool' file\nimport sys, comandante as cli\n\n\nclass CliTool(cli.Handler):\n\n    @cli.command() \n    def sum(self, a: int, b: int):\n        print(a + b)\n        \n    @cli.command() \n    def sub(self, a: int, b: int):\n        print(a - b)\n            \n\nCliTool().invoke(sys.argv[1:])\n```\nThe same in **Python2**:\n```python\n#!/usr/bin/env python3\n# content of 'tool' file\nimport sys, comandante as cli\n\n\nclass CliTool(cli.Handler):\n\n    @cli.signature(a=int, b=int)\n    @cli.command() \n    def sum(self, a, b):\n        print(a + b)\n        \n    @cli.signature(a=int, b=int)\n    @cli.command() \n    def sub(self, a, b):\n        print(a - b)\n            \n\nCliTool().invoke(sys.argv[1:])\n```\n\nThat's it! Now let's try it in shell:\n```shell\n$ ./tool sum 21 21\n42\n```\n```shell\n$ ./tool sub 5 7\n-2\n```\nSo in other words to create a command-line interface you simply:\n * define a normal Python class (inherited from the `comandante.Handler`)\n * equip your class methods with a minimal amount of metadata (via decorators)\n * call `invoke(sys.argv[1:])` method on your class instance\n * **comandante** will inspect metadata and decide how to parse \n command-line arguments and which method to call.  \n\n\n### Just a Normal Classes and Methods\nNo surprises! Handlers and commands are just a normal classes and methods.\n\n## Options\nCommand options are declared with `@comandante.option(...)` decorator. \n\nEach command-method receives options as `**kwargs` parameters (if present). \n\nEach command-method has a convenience method `Command#options(kwargs)` to \nmerge default values with specified command-line options.\n\n### Command Options\n\nExample below demonstrates how to define command option:\n\n```python\n#!/usr/bin/env python3\nimport sys, comandante as cli\n\n \nclass DatabaseCli(cli.Handler):\n\n    @cli.option(name='force', short='f', type=bool, default=False)\n    @cli.command()\n    def drop(self, database_name, **specified_options):\n        # merge specified options with default values\n        options = self.drop.options(specified_options)\n        \n        # merged options provides attribute-like element access\n        if options.force or self.confirm():\n            print(\"Database '{name}' was deleted\".format(name=database_name))\n        else: \n            print(\"Aborted\")\n            \n    @staticmethod        \n    def confirm():\n        question = 'Are you sure? [y/N]: '\n        value = input(question).lower()\n        while value not in ['', 'n', 'y']:\n            print(\"Please answer 'y' or 'n'\")\n            value = input(question).lower()\n        return value == 'y'\n\n\ndatabase_cli = DatabaseCli()\ndatabase_cli.invoke(sys.argv[1:])\n```\nThen in shell:\n```shell\n$ ./database drop production\nAre you sure? [y/N]: y\nDatabase 'production' was deleted\n```\nThe following two examples are identical:\n```shell\n$ ./database drop -f production\nDatabase 'production' was deleted\n```\n```shell\n$ ./database drop --force production\nDatabase 'production' was deleted\n```\n\n### Class Options\n\nOptions could be declared on handler itself with `Handler#declare_option` method. \n\nOptions declared on handler will be declared on each of its command and subcommand recursively. \n```python\n#!/usr/bin/env python3\nimport sys, comandante as cli\n\nclass CliTool(cli.Handler):\n    def __init__(self):\n        super().__init__()\n        \n        # define options\n        self.declare_option('verbose', 'v', bool, False)\n    \n    @cli.command()\n    def first(self, **specified_options):\n        if 'verbose' in specified_options:\n            print(\"Hello from the first!\")\n    \n    @cli.command()\n    def second(self, **specified_options):\n        if 'verbose' in specified_options:\n            print(\"Hello from the second!\")\n        \n\ntool = CliTool()\ntool.invoke(sys.argv[1:])\n```\nThen in shell:\n```shell\n$ ./tool first --verbose\nHello from the first!\n```\n```shell\n$ ./tool second -v\nHello from the second!\n```\n\n## Subcommands\n\nAs your CLI becomes more complex and harder to maintain, you might want to \nhave commands that have its own set of commands handled by separate class. \nIn Comandante to compose handlers into hierarchy you may simply \nuse `Handler#declare_command` method. \n\nHere is a simple example:\n```python\n#!/usr/bin/env python3\n\nimport sys, comandante as cli\n\nclass Remote(cli.Handler):\n    @cli.command()\n    def add(self, name, uri):\n        print(\"Adding repository\", name, uri)\n    \n    @cli.command()\n    def rename(self, old, new):\n        print(\"Renaming repository\", old, new) \n        \nclass Git(cli.Handler):\n    def __init__(self):\n        super().__init__() \n        \n        # define subcommands\n        self.declare_command(name='remote', handler=Remote())\n    \n    @cli.option('message', 'm', str, '')\n    @cli.command()\n    def commit(self, **specified_options):\n        options = self.commit.options(specified_options)\n        print(\"Committing changes with message '{}'\".format(options.message))\n        \ngit = Git()\ngit.invoke(sys.argv[1:])\n```\n\nThen in shell\n```shell\n$ ./git commit -m \"Initial commit\"\nCommitting changes with message 'Initial commit' \n```\n```shell\n$ ./git remote add origin git@github.com:stepan-anokhin/comandante.git\nAdding repository origin git@github.com:stepan-anokhin/comandante.git\n```\n```shell\n$ ./git remote rename origin destination\nRenaming repository origin destination\n```\n## Arguments\n\nIn python3 command argument types are declared using annotations:\n```python\nimport comandante as cli\n\nclass CliTool(cli.Handler):\n\n    @cli.command()\n    def do_something(self, a: int, b: float, c: str):\n        print(a + b, c)\n```\nType could be any callable. This callable will be simply called with \na command-line string as the only argument. A result will be passed\nto the *command-method* as argument. The same is true for option types. \n \nThe only exception is `bool` arguments and options. Bool option doesn't \nreceive any value, if specified it is considered to be `True` otherwise \ndefault value is used. \n\nIf no argument type is specified, then `str` is assumed by default. \nType's `__name__` attribute is used in automatic help output to \nrepresent argument/option type. \n\nComandante honors default argument values and varargs:\n```python\n#!/usr/bin/env python3\nimport sys, comandante as cli\n\nclass CliTool(cli.Handler):\n\n    @cli.command()\n    def sum(self, *values: int):\n        print(sum(values))\n        \n    @cli.command()\n    def repeat(self, message, times: int = 2):\n        for i in range(times):\n            print(message)\n\nCliTool().invoke(sys.argv[1:])\n```\n```shell\n$ ./tool sum 1 2 3 4 5\n15\n```\n```shell\n$ ./tool repeat \"Hello world!\"\nHello world!\nHello world!\n```\n### Type Library\n\nComandante also provides several higher-order types:\n * `comandante.types.choice` - to make sure argument value is one of the specified options\n * `comandante.types.listof` - to parse comma-separated lists (e.g. `listof(int)` will parse `\"1,2,3,4\"` into `[1, 2, 3, 4]`)\n \nYou may take a look into the\n[comandante.types](https://github.com/stepan-anokhin/comandante/blob/master/comandante/types.py)\nto get some additional insights.  \n \n### Python 2\n\nPython 2 doesn't support parameter annotations. \nTo specify argument types use `@comandante.signature()`\n```python\nimport comandante as cli\n\nclass CliTool(cli.Handler):\n\n    @cli.signature(a=int, b=float)\n    @cli.command()\n    def do_something(self, a, b, c):\n        print(a + b, c)\n```\n\n## Printing Help\n\nComandante provides predefined `help` command for you which will print\nformatted help information to the stdout. Command and handler descriptions\nare taken from the corresponding docstrings. \n\nExample:\n```python\nimport sys\nimport comandante as cli\n\n\nclass Git(cli.Handler):\n    \"\"\"The stupid content tracker.\n\n    Git is a fast, scalable, distributed revision control system with\n    an unusually *rich command* set that provides both high-level operations\n    and full access to internals.\n\n    See *gittutorial*(7) to get started, then see *giteveryday*(7) for a useful\n    minimum set of commands. The *Git User’s Manual*[1] has a more in-depth\n    introduction.\n    \"\"\"\n\n    @cli.option(name='message', short='m', type=str, default='', descr=\"\"\"\n    Use the given \u003cmsg\u003e as the commit message. If multiple *-m* options are \n    given, their values are concatenated as separate paragraphs.\n    \"\"\")\n    @cli.command()\n    def commit(self):\n        \"\"\"Record changes to the repository\n\n        Create a new commit containing the current contents\n        of the index and the given log message describing\n        the changes. The new commit is a direct child of HEAD,\n        usually the tip of the current branch, and the branch\n        is updated to point to it (unless no branch is associated\n        with the working tree, in which case HEAD is \"detached\"\n        as described in *git-checkout*(1)).\n        \"\"\"\n        print(\"Committing...\")\n\n    @cli.command()\n    def clone(self, repository, directory=None):\n        \"\"\"Clone a repository into a new directory\n\n        Clones a repository into a newly created directory, creates\n        remote-tracking branches for each branch in the cloned\n        repository (visible using git branch *-r*), and creates and\n        checks out an initial branch that is forked from the cloned\n        repository’s currently active branch.\n        \"\"\"\n        print(\"Cloning...\")\n        \nGit().invoke(sys.argv[1:])\n``` \n`./git` output:\n\u003cp align=\"left\"\u003e\n    \u003cimg src=\"https://raw.githubusercontent.com/stepan-anokhin/comandante/master/docs/images/help_git.png\" alt=\"git\"\u003e\n\u003c/p\u003e\n\n`./git help clone` output:\n\u003cp align=\"left\"\u003e\n    \u003cimg src=\"https://raw.githubusercontent.com/stepan-anokhin/comandante/master/docs/images/help_clone.png\" alt=\"git help clone\"\u003e\n\u003c/p\u003e\n\n`./git help commit` output:\n\u003cp align=\"left\"\u003e\n    \u003cimg src=\"https://raw.githubusercontent.com/stepan-anokhin/comandante/master/docs/images/help_commit.png\" alt=\"git help commit\"\u003e\n\u003c/p\u003e\n\n\n## Error Handling\n\nSuccessful calls to `Handler#invoke` and `Command#invoke` methods return \nthe same value as the corresponding *command-method*. \n\nBy design Comandante doesn't hide any exceptions raised in the course of \n`invoke` call. The only special case is subclasses of \n`comandnate.errors.CliSyntaxException` which results in help printing \nbefore being re-raised.\n\nSo it is up to the caller to decide how to handle exceptions. \n\nA reasonable error handling may look like this:\n```python\nimport sys, logging, comandante as cli\n\n# ... initialize handler and logger ... \n\ntry: \n    handler.invoke(sys.argv[1:])\nexcept cli.errors.CliSyntaxException:\n    sys.exit(1)\nexcept:\n    logger.exception('Unexpected exception')\n    sys.exit(1)\n    \n```  \n\n## Testing Your CLI\n\nHandlers and commands could be tested just like \n[any other](#just-a-normal-classes-and-methods) classes and methods. \n\nExample cli:\n```python\nimport comandante as cli\n\n\nclass DatabaseCli(cli.Handler):\n    def __init__(self, database):\n        super().__init__()\n        self._database = database\n\n    @cli.option('force', 'f', bool, False)\n    @cli.command()\n    def drop(self, database_name, **specified_options):\n        options = self.drop.options(specified_options)\n        if options.force or self.confirm():\n            self._database.drop(database_name)\n\n    def confirm(self):\n        question = 'Are you sure? [y/N]: '\n        value = input(question).lower()\n        while value not in ['', 'n', 'y']:\n            print(\"Please answer 'y' or 'n'\")\n            value = input(question)\n        return value == 'y'\n\n``` \nExample tests:\n```python\nfrom unittest import TestCase\nfrom unittest.mock import MagicMock as Mock\n\n\nclass DatabaseCliTests(TestCase):\n    def test_forced_database_drop(self):\n        fake_database = Mock()\n        database_cli = DatabaseCli(database=fake_database)\n\n        database_cli.drop('production', force=True)\n\n        fake_database.drop.assert_called_with('production')\n\n    def test_database_drop(self):\n        fake_database = Mock()\n        database_cli = DatabaseCli(database=fake_database)\n        database_cli.confirm = Mock(return_value=True)  # confirm\n\n        database_cli.drop('production')\n\n        fake_database.drop.assert_called_with('production')\n\n    def test_rejected_database_drop(self):\n        fake_database = Mock()\n        database_cli = DatabaseCli(database=fake_database)\n        database_cli.confirm = Mock(return_value=False)  # reject\n\n        database_cli.drop('production')\n\n        fake_database.drop.assert_not_called()\n```\n\n## Alternatives\n\nComandante is a great way to build command-line interfaces. \nBut it's not the only one! \n\nHere is a list of some alternatives:\n\n* [Fire](https://github.com/google/python-fire)\n* [click](https://click.palletsprojects.com/en/7.x/)\n* [argparse](https://docs.python.org/3/library/argparse.html)\n* [getopt](https://docs.python.org/3.1/library/getopt.html)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstepan-anokhin%2Fcomandante","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fstepan-anokhin%2Fcomandante","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstepan-anokhin%2Fcomandante/lists"}