.. _Commands:

========
Commands
========

There are two different ways of registering functions:

1. :meth:`app.default <cyclopts.App.default>` -
   Registers an action for when no registered command is provided.
   This was previously demonstrated in :ref:`Getting Started`.

   A sub-app **cannot** be registered with :meth:`app.default <cyclopts.App.default>`.
   If no ``default`` command is registered, Cyclopts will display the help-page.

2. :meth:`app.command <cyclopts.App.command>` - Registers a function or :class:`.App` as a command.

This section will detail how to use the :meth:`@app.command <cyclopts.App.command>` decorator.

---------------------
Registering a Command
---------------------
The :meth:`@app.command <cyclopts.App.command>` decorator adds a **command** to a Cyclopts application.

.. code-block:: python

   from cyclopts import App

   app = App()

   @app.command
   def fizz(n: int):
       print(f"FIZZ: {n}")

   @app.command
   def buzz(n: int):
       print(f"BUZZ: {n}")

   app()

We can now control which command runs from the CLI:

.. code-block:: console

   $ my-script fizz 3
   FIZZ: 3

   $ my-script buzz 4
   BUZZ: 4

   $ my-script fuzz
   ╭─ Error ────────────────────────────────────────────────────────────────────╮
   │ Unknown command "fuzz". Did you mean "fizz"?                               │
   ╰────────────────────────────────────────────────────────────────────────────╯

------------------------
Registering a SubCommand
------------------------
The :meth:`app.command <cyclopts.App.command>` method can also register another Cyclopts :class:`.App` as a command.

.. code-block:: python

   from cyclopts import App

   app = App()
   sub_app = App(name="foo")  # "foo" would be a better variable name than "sub_app".
   # "sub_app" in this example emphasizes the name comes from name="foo".
   app.command(sub_app)  # Registers sub_app to command "foo"
   # Or, as a one-liner:  sub_app = app.command(App(name="foo"))


   @sub_app.command
   def bar(n: int):
       print(f"BAR: {n}")


   # Alternatively, access subapps from app like a dictionary.
   @app["foo"].command
   def baz(n: int):
       print(f"BAZ: {n}")


   app()


.. code-block:: console

   $ my-script foo bar 3
   BAR: 3

   $ my-script foo baz 4
   BAZ: 4

The subcommand may have their own registered ``default`` action.
Cyclopts's command structure is fully recursive.

.. _Flattening SubCommands:

-----------------------
Flattening SubCommands
-----------------------
Sometimes you want to make all commands from a sub-app directly accessible from the parent app,
without requiring users to type the intermediate subcommand name.

You can flatten a sub-app by registering it with the special ``name="*"``:

.. code-block:: python

   from cyclopts import App

   app = App()
   tools_app = App(name="tools")

   @tools_app.command
   def compress(file: str):
       print(f"Compressing {file}")

   @tools_app.command
   def extract(file: str):
       print(f"Extracting {file}")

   # Flatten: make all tools_app commands directly accessible
   app.command(tools_app, name="*")

   app()

.. code-block:: console

   $ my-script compress data.txt
   Compressing data.txt

   $ my-script extract archive.zip
   Extracting archive.zip

Caveats of flattening:

- Parent app commands take precedence over flattened commands if there are name collisions.
- Multiple sub-apps can be flattened into the same parent app.
- You cannot supply additional configuration kwargs when using ``name="*"``.
- Only :class:`.App` instances can be flattened (not functions or import paths).

Flattening is useful for organizing related commands into logical groups in your code while keeping
the CLI interface simple and flat.

------------------------
SubCommand Configuration
------------------------
Subcommands inherit configuration from their parent apps.

.. code-block:: python

   from cyclopts import App

   # Root app with specific error handling
   root_app = App(
       exit_on_error=False,
       print_error=False,
   )

   # Child app inherits parent's settings
   child_app = root_app.command(App(name="child"))

   @child_app.default
   def child_action():
       return "Child executed successfully"

   # Child can override parent settings if needed
   grandchild_app = child_app.command(App(name="grandchild", exit_on_error=True))

When ``parent_app("child ...")`` is called, the child command will use the parent's
error handling settings unless explicitly overridden.

.. _Command Changing Name:

---------------------
Changing Command Name
---------------------
By default, commands are registered to the python function's name with underscores replaced with hyphens.
Any leading or trailing underscores will be stripped.
For example, the function ``_foo_bar()`` will become the command ``foo-bar``.
This renaming is done because CLI programs generally tend to use hyphens instead of underscores.
The name transform can be configured by :attr:`App.name_transform <cyclopts.App.name_transform>`.
For example, to make CLI command names be identical to their python function name counterparts, we can configure :class:`~cyclopts.App` as follows:

.. code-block:: python

   from cyclopts import App

   app = App(name_transform=lambda s: s)

   @app.command
   def foo_bar():  # will now be "foo_bar" instead of "foo-bar"
       print("running function foo_bar")

   app()

.. code-block:: console

   $ my-script foo_bar
   running function foo_bar


Alternatively, the name can be **manually** changed in the :meth:`@app.command <cyclopts.App.command>` decorator.
Manually set names are **not** subject to :attr:`App.name_transform <cyclopts.App.name_transform>`.

.. code-block:: python

   from cyclopts import App

   app = App()

   @app.command(name="bar")
   def foo():  # function name will NOT be used.
       print("Hello World!")

   app()

.. code-block:: console

   $ my-script bar
   Hello World!

Finally, if you would like to register an **additional** name to the Cyclopts-derived names, you can set an :attr:`~.App.alias`:

.. code-block:: python

    from cyclopts import App

    app = App()

    @app.command(alias="bar")
    def foo():  # both "foo" and "bar" will trigger this function.
        print("Running foo.")

    app()

.. code-block:: console

    $ my-script foo
    Running bar.

    $ my-script bar
    Running bar.

.. _Synonyms:

--------
Synonyms
--------
A common CLI problem is remembering subcommand synonyms. Did this CLI's developer decide to call it ``remove``, ``uninstall``, or ``delete``?

The :attr:`~.App.synonym` field declares **alternate names** that trigger a "Did you mean..."
suggestion **without** registering the command under those names.
It accepts either a single :obj:`str` or an iterable of strings:

.. code-block:: python

    from cyclopts import App

    app = App()

    @app.command(synonym="add")               # single string
    def install(name: str): ...

    @app.command(synonym=["remove", "rm"])    # list of strings
    def uninstall(name: str):
        print(f"uninstalling {name}")

    app()

.. code-block:: console

    $ my-script uninstall mypackage
    uninstalling mypackage

    $ my-script remove mypackage
    ╭─ Error ────────────────────────────────────────────────────────────────────╮
    │ Unknown command "remove". Did you mean "uninstall"? Available commands:    │
    │ uninstall.                                                                 │
    ╰────────────────────────────────────────────────────────────────────────────╯

Synonyms differ from :attr:`~.App.alias` in two important ways:

* **Synonyms are not runnable.** ``my-script remove`` raises an error; it doesn't invoke ``uninstall``.
  Use :attr:`~.App.alias` when you want the command to be callable under multiple names.
* **Synonyms are hidden from** ``--help``. They exist only to guide users who type the "wrong" word.

If two commands declare the same synonym, both are suggested:

.. code-block:: python

    @app.command(synonym="erase")
    def uninstall(name: str): ...

    @app.command(synonym="erase")
    def purge(name: str): ...

.. code-block:: console

    $ my-script erase mypackage
    ╭─ Error ────────────────────────────────────────────────────────────────────╮
    │ Unknown command "erase". Did you mean "uninstall" or "purge"? ...          │
    ╰────────────────────────────────────────────────────────────────────────────╯

When a typed token is not declared as any synonym, Cyclopts falls back to its standard
typo-fuzzy-match suggestion.

-----------
Adding Help
-----------
There are a few ways to add a help string to a command:

1. If the function has a docstring, the **short description** will be used as the help string for the command.
   This is generally the preferred method of providing help strings.

2. If the registered command is a sub app, the sub app's :attr:`help <cyclopts.App.help>` field will be used.

   .. code-block:: python

      sub_app = App(name="foo", help="Help text for foo.")
      app.command(sub_app)

3. The :attr:`help <cyclopts.App.help>` field of :meth:`@app.command <cyclopts.App.command>`. If provided, the docstring or subapp help field will **not** be used.

   .. code-block:: python

      from cyclopts import App

      app = App()

      @app.command
      def foo():
          """Help string for foo."""
          pass

      @app.command(help="Help string for bar.")
      def bar():
          """This got overridden."""

      app()

   .. code-block:: console

      $ my-script --help
      ╭─ Commands ────────────────────────────────────────────────────────────╮
      │ bar        Help string for bar.                                       │
      │ foo        Help string for foo.                                       │
      │ --help,-h  Display this message and exit.                             │
      │ --version  Display application version.                               │
      ╰───────────────────────────────────────────────────────────────────────╯

-----
Async
-----
Cyclopts also works with **async** commands; when an async command is encountered, an event loop will be automatically created using the specified ``backend`` parameter (default :mod:`asyncio`).

.. code-block:: python

   import asyncio
   from cyclopts import App

   app = App()

   @app.command
   async def foo():
       await asyncio.sleep(10)

   app()

When calling from within an existing async context, :keyword:`await` the async method :meth:`~cyclopts.App.run_async`:

.. code-block:: python

   async def main():
       result = await app.run_async(["foo"])
       # Instead of: app(["foo"]) which would raise RuntimeError

--------------------------
Decorated Function Details
--------------------------
Cyclopts **does not modify the decorated function in any way**.
The returned function is the **exact same function** being decorated and can be used exactly as if it were not decorated by Cyclopts.

--------
See Also
--------

For improved CLI startup performance with large applications, see :ref:`Lazy Loading`.
