CLIs

Ape uses the click framework for handling all CLI functionality. There are CLIs found in a couple areas in the Ape framework:

  1. Plugins

  2. Scripts

Both plugins and scripts utilize click for their CLIs.

For plugins, CLIs are an option for extending the framework. You can read more about plugin development and CLIs in the developing plugins guide.

Scripts utilize CLIs as an option for users to develop their scripts. You can read more about scripting and CLIs in the scripting guide.

This guide is for showcasing utilities that ship with Ape to assist in your CLI development endeavors.

Ape Context Decorator

The @ape_cli_context gives you access to all the root Ape objects (accounts, networks etc.), the ape logger, and an abort method for stopping execution of your CLI gracefully. Here is an example using all of those features from the cli_ctx:

import click
from ape.cli import ape_cli_context


@click.command()
@ape_cli_context()
def cmd(cli_ctx):
    cli_ctx.logger.info("Test")
    account = cli_ctx.account_manager.load("metamask")
    cli_ctx.abort(f"Bad account: {account.address}")

In Ape, it is easy to extend the CLI context object and use the extended version in your CLIs:

from ape.cli import ApeCliContextObject, ape_cli_context
import click

class MyManager:
   """My custom manager."""

class CustomContext(ApeCliContextObject):
   """Add new managers to your custom context"""
   my_manager: MyManager = MyManager()
   
   @property
   def signer(self):
      """Utilize existing managers in your custom context."""
      return self.account_manager.load("my_account")

@click.command()
@ape_cli_context(obj_type=CustomContext)
def cli(cli_ctx):
    # Access your manager.
    print(cli_ctx.my_manager)
    # Access other Ape managers.
    print(cli_ctx.account_manager)

Network Tools

The @network_option() allows you to select an ecosystem, network, and provider. To specify the network option, use values like:

--network ethereum
--network ethereum:sepolia
--network ethereum:mainnet:alchemy
--network ::foundry

To use default values automatically, omit sections of the choice, but leave the semi-colons for parsing. For example, ::test means use the default ecosystem and network and the test provider.

Use ecosystem, network, and provider argument names in your command implementation to access their corresponding class instances:

import click
from ape.cli import network_option

@click.command()
@network_option()
def cmd(provider):
   # This command only needs the provider.
   click.echo(provider.name)

@click.command()
@network_option()
def cmd_2(ecosystem, network, provider):
   # This command uses all parts of the parsed network choice.
   click.echo(ecosystem.name)
   click.echo(network.name)
   click.echo(provider.name)

The ConnectedProviderCommand automatically uses the --network option and connects to the network before any of your code executes and then disconnects afterward. This is useful if your script or command requires a provider connection in order for it to run. Additionally, specify ecosystem, network, or provider in your command function if you need any of those instances in your ConnectedProviderCommand, just like when using network_option.

import click
from ape.cli import ConnectedProviderCommand

@click.command(cls=ConnectedProviderCommand)
def cmd(network, provider):
   click.echo(network.name)
   click.echo(provider.is_connected)  # True

@click.command(cls=ConnectedProviderCommand)
def cmd(provider):
   click.echo(provider.is_connected)  # True

@click.command(cls=ConnectedProviderCommand)
def cmd():
   click.echo("Using params from ConnectedProviderCommand is optional")

Account Tools

Use the @account_option() for adding an option to your CLIs to select an account. This option does several things:

  1. If you only have a single account in Ape (from both test accounts and other accounts), it will use that account as the default. (this case is rare, as most people have more than one test account by default).

  2. If you have more than one account, it will prompt you to select the account to use.

  3. You can pass in an account alias or index to the option flag to have it use that account.

  4. It allows you to specify test accounts by using a choice of TEST::{index_of_test_account}.

Thus, if you use this option, no matter what, your script will have an account to use by the time the script starts. Here is an example:

import click
from ape.cli import account_option


@click.command()
@account_option()
def cmd(account):
    # Will prompt the user to select an account if needed.
    click.echo(account.alias)

And when invoking the command from the CLI, it would look like the following: (where <prefix> is either ape run for scripts or ape <custom-plugin-cmd> for plugins)

<prefix> cmd  # Use the default account.
<prefix> cmd --account 0  # Use first account that would show up in `get_user_selected_account()`.
<prefix> cmd --account metamask  # Use account with alias "metamask".
<prefix> cmd --account TEST::0  # Use the test account at index 0.

Alternatively, you can call the get_user_selected_account() directly to have more control of when the account gets selected:

import click
from ape.cli import select_account


@click.command()
def cmd():
   account = select_account("Select an account to use")
   click.echo(f"You selected {account.address}.")

Similarly, there are a couple custom arguments for aliases alone that are useful when making CLIs for account creation. If you use @existing_alias_argument() and specify an alias does not already exist, it will error. And visa-versa when using @non_existing_alias_argument().

import click
from ape.cli import existing_alias_argument, non_existing_alias_argument


@click.command()
@existing_alias_argument()
def delete_account(alias):
    # We know the alias is an existing account at this point.
    click.echo(alias)


@click.command()
@non_existing_alias_argument()
def create_account(alias):
    # We know the alias is not yet used in Ape at this point.
    click.echo(alias)

You can control additional filtering of the accounts by using the account_type kwarg. Use account_type to filter the choices by specific types of AccountAPI, or you can give it a list of already known accounts, or you can provide a callable-filter that takes an account and returns a boolean.

import click
from ape import accounts
from ape.cli import existing_alias_argument, get_user_selected_account
from ape_accounts.accounts import KeyfileAccount

# NOTE: This is just an example and not anything specific or recommended.
APPLICATION_PREFIX = "<FOO_BAR>"

@click.command()
@existing_alias_argument(account_type=KeyfileAccount)
def cli_0(alias):
   pass
   
@click.command()
@existing_alias_argument(account_type=lambda a: a.alias.startswith(APPLICATION_PREFIX))
def cli_1(alias):
   pass
    
   
# Select from the given accounts directly.
my_accounts = [accounts.load("me"), accounts.load("me2")]
selected_account = get_user_selected_account(account_type=my_accounts)