CLI::Simple(3)

πŸ“¦
version 2.2.5 Β· 3 versions
, in fedora 43

✨ NAME ✨

okay so this is CLI::Simple(3), the manual page, and it is literally about:

CLI::Simple β€” a minimalist object oriented base class for CLI applications πŸ’…


πŸ¦‹ SYNOPSIS πŸ¦‹

like, here's the whole vibe in one go:

#!/usr/bin/env perl

package MyScript;

use strict;
use warnings;

use CLI::Simple::Constants qw(:booleans :chars);
use CLI::Simple qw($AUTO_HELP $AUTO_DEFAULT);

use parent qw(CLI::Simple);

caller or exit __PACKAGE__->main();

sub execute {
  my ($self) = @_;

  # retrieve a CLI option
  my $file = $self->get_file;
  ...
}

sub list {
  my ($self) = @_

  # retrieve a command argument
  my ($file) = $self->get_args();
  ...
}

sub main {

  # Disable auto-default for single commands, enable auto-help
  $AUTO_DEFAULT = 0;
  $AUTO_HELP = 1;

  my $cli = MyScript->new(
   option_specs    => [ qw( help format=s file=s) ],
   default_options => { format => 'json' }, # set some defaults
   extra_options   => [ qw( content ) ], # non-option, setter/getter
   commands        => { execute => \&execute, list => \&list,  }
   alias           => { options => { fmt => 'format' }, commands => { ls => 'list' } },
  );

  return $cli->run();
}

1;

and then the glow-up version β€” role-based CLI Application (2.0.0) 🫢

first you create a YAML manifest my-script.yml in your project root:

---
commands:
  frobnicate: My::Script::Role::Frobnicate
  list:       My::Script::Role::List
options:
  - help|h
  - verbose|v
  - output|o=s

then you create a main module:

package My::Script;

use CLI::Simple qw(:roles);
use parent qw(CLI::Simple);

our $VERSION = '1.0.0';

caller or exit __PACKAGE__->main;

1;

then you create implementation roles:

package My::Script::Role::Frobnicate;

use Role::Tiny;
use CLI::Simple::Constants qw(:booleans);

sub cmd_frobnicate {
  my ($self) = @_;
  ...
  return $SUCCESS;
}

1;

πŸ’œ DESCRIPTION πŸ’œ

Tired of writing the same 'ol boilerplate code for command line scripts? 😭 Want a standard, simple way to create a Perl script that takes options and commands? bestie, CLI::Simple makes it easy to create scripts that take options, commands and arguments. periodt.

CLI::Simple is designed around the modulino pattern β€” Perl modules that can be executed directly as scripts. go peek at "MODULINOS" below πŸ‘‡

For common constant values (like $TRUE, $DASH, or $SUCCESS), see CLI::Simple::Constants, which pairs naturally with this module. they're a matched set fr fr πŸŽ€

Version 2.0.0 introduces optional role-based architecture for applications that have outgrown a single module. Declare your commands and options in a YAML manifest, implement each command in a dedicated Role::Tiny role, and CLI::Simple handles composition, dispatch, and lifecycle automatically. Your main module shrinks to a single line:

caller or exit __PACKAGE__->main;

Not ready for a full refactor? Start smaller. The built-in -dump-spec command introspects your existing module and writes a YAML manifest that makes your configuration data-driven without moving a single line of implementation code. Adopt roles incrementally, one command at a time. no cap, that's the lowkey genius part ✨

When you are ready to scaffold a full role-based project, -scaffold generates role stubs, a slimmed main module, and inter-module dependencies from your manifest. Feed the resulting tarball to CPAN::Maker::Bootstrapper and you have a complete, buildable CPAN distribution in one step. ate. πŸ‘‘


🌈 VERSION 🌈

This documentation refers to version 2.2.5.


πŸ‘‘ FEATURES πŸ‘‘

  • accept command line arguments ala Getopt::Long
  • supports commands and command arguments
  • automatically add a logger
  • global or custom log levels per command
  • easily add usage notes
  • automatically create setter/getters for your script
  • low dependency profile
  • optional role-based architecture via YAML manifest
  • built-in scaffolding tools for migrating legacy scripts to roles
  • bash completion script generation for modulino wrappers
  • optional pager support for help output via IO::Pager
  • customizable help sections via help_sections

🧚 MODULINOS 🧚

A modulino is a Perl module that can also be run directly as a script. The term was coined by Brian D. Foy and the pattern is simple:

caller or exit __PACKAGE__->main();

When the file is required or used by another module, caller returns the calling package and the expression short-circuits β€” main() is never called. When the file is executed directly by Perl, caller returns false and main() runs. The same file serves as both a reusable module and an executable script. it's giving duality, it's giving main character energy πŸ’…

CLI::Simple is designed around this pattern. Every CLI::Simple application is expected to be a modulino. The framework's lifecycle, internal commands, bash completion, and scaffolding tools all assume this dual-use design.

🌸 Why Modulinos? 🌸

The modulino pattern offers several advantages over a traditional script:

  • Testable β€” your script logic lives in a proper Perl module that can be used in test files without executing main()
  • Reusable β€” other scripts and modules can use your modulino and call its methods directly
  • Introspectable β€” tools like -dump-spec and -generate-completion can load your modulino and inspect its live state without running it as a script
  • Installable β€” modulinos distribute cleanly as CPAN modules with full man page support via CPAN::Maker::Bootstrapper

πŸŽ€ The Bash Wrapper πŸŽ€

Perl modulinos are invoked via a thin bash wrapper script that locates the installed module file and passes all arguments through to Perl:

#!/usr/bin/env bash
#-*- mode: sh; -*-

MODULINO_WRAPPER=my-script
MODULE_NAME=My::Script
MODULE_PATH=$(MODULE_PATH="${MODULE_NAME//:://}.pm" \
  perl -M$MODULE_NAME -e 'print $INC{$ENV{MODULE_PATH}};')

MODULINO_WRAPPER=$MODULINO_WRAPPER perl $MODULE_PATH "$@"

The wrapper locates the installed .pm file via %INC and sets MODULINO_WRAPPER in the environment so CLI::Simple knows the name of the script the user actually typed. This is used by -generate-completion to name the bash completion function correctly and by CPAN::Maker::Bootstrapper to create man page symlinks. so many little things hanging off one env var, obsessed 😌

πŸ¦‹ create-modulino πŸ¦‹

CLI::Simple ships with a create-modulino tool that generates the bash wrapper for any CLI::Simple modulino:

# create wrapper using module name convention (My::Script -> my-script)
create-modulino -m My::Script

# install to a specific directory
create-modulino -m My::Script -i /usr/local/bin

# use a custom wrapper name
create-modulino -m My::Script -a my-alias -i /usr/local/bin

create-modulino is itself a modulino β€” an example of the pattern it creates. The bash wrapper template lives in its __DATA__ section, keeping the tool entirely self-contained. it's self-referential and iconic for that πŸ’€βœ¨

If you are building a CPAN distribution, CPAN::Maker::Bootstrapper integrates create-modulino into the make modulino target, generating and installing the wrapper as part of the build process.

πŸ’œ MODULINO_WRAPPER πŸ’œ

The MODULINO_WRAPPER environment variable tells CLI::Simple the name of the wrapper script that invoked the modulino. It is set by the wrapper and used by:

  • -generate-completion β€” to name the bash completion function and complete target correctly
  • Man page symlinks via CPAN::Maker::Bootstrapper β€” so man my-script resolves to the module's man page

If MODULINO_WRAPPER is not set, CLI::Simple infers the script name from the module name by convention β€” My::Script becomes my-script. Set it explicitly when the wrapper name does not follow this convention.

🫢 heads up bestie: if your wrapper name is Doing Its Own Thing and not following the convention, set MODULINO_WRAPPER yourself or the inferred name will be wrong.

⭐ QUICK START ⭐

πŸ’– Single-Module Application πŸ’–

The simplest way to use CLI::Simple is to subclass it and define your commands as methods in the same module:

package My::Script;

use strict;
use warnings;

use CLI::Simple::Constants qw(:booleans);

use parent qw(CLI::Simple);

caller or exit __PACKAGE__->main;

sub cmd_frobnicate {
  my ($self) = @_;
  my $output = $self->get_output;
  ...
  return $SUCCESS;
}

sub main {
  __PACKAGE__->new(
    option_specs => [ qw( help|h verbose|v output|o=s ) ],
    commands     => { frobnicate => \&cmd_frobnicate },
  )->run;
}

1;

πŸ’– Role-Based Application πŸ’–

For larger applications, declare your commands and options in a YAML manifest and implement each command in a dedicated Role::Tiny role. Your main module becomes a single declaration:

package My::Script;

use strict;
use warnings;

use CLI::Simple qw(:roles);
use parent qw(CLI::Simple);

our $VERSION = '1.0.0';

caller or exit __PACKAGE__->main;

1;

Naming convention: The YAML manifest filename is derived from your module name β€” My::Script looks for my-script.yml in the distribution share directory. You must package the spec file with your distribution. don't forget this part, fr fr, the file has to actually ship πŸ“¦

The manifest maps commands to roles:

---
commands:
  frobnicate: My::Script::Role::Frobnicate
  list:       My::Script::Role::List
options:
  - help|h
  - verbose|v
  - output|o=s

Each role implements one or more commands:

package My::Script::Role::Frobnicate;

use Role::Tiny;
use CLI::Simple::Constants qw(:booleans);

sub cmd_frobnicate {
  my ($self) = @_;
  ...
  return $SUCCESS;
}

1;

To easily generate the directory structure, role stubs, and build files for this architecture, CLI::Simple provides a built-in -scaffold tool. one command and your whole project materialises, slay πŸͺ„

See -scaffold for detailed instructions on generating a role-based project tarball from a monolithic script or a YAML manifest. For a comprehensive guide on transitioning your application, see "ROLE-BASED ARCHITECTURE".

✨ ROLE-BASED ARCHITECTURE ✨

okay bestie so CLI::Simple 2.0.0 dropped something kinda iconic: an optional role-based architecture, made specifically for the apps that outgrew their one little module 😌. Here's the tea β€” your commands live inside dedicated Role::Tiny roles, and you declare them in a YAML manifest. Then CLI::Simple composes the roles, builds the dispatch table, and hands you an inherited main(). Like… your main module could legit be one single declaration. main character energy fr fr πŸ‘‘


🌸 The YAML Manifest

The manifest is a YAML file, and it declares your commands, your options, and your defaults. By convention the filename gets derived from your module name:

My::Script        ->  my-script.yml
CPAN::Maker::Bootstrapper  ->  cpan-maker-bootstrapper.yml

CLI::Simple finds the manifest via File::ShareDir, using the distribution name derived from the module name. And ngl this part is important: the manifest must be installed as part of the distribution β€” it cannot be loaded from an arbitrary location.

Security note: the manifest is loaded exclusively from the distribution share directory via File::ShareDir. A manifest that was not installed as part of the distribution cannot be loaded. This gives you the exact same security model as Perl module loading itself. no sketchy random paths, we love that πŸ’…

A minimal manifest, for when you're keeping it cute and simple:

---
commands:
  frobnicate: My::Script::Role::Frobnicate
  list:       My::Script::Role::List
options:
  - help|h
  - verbose|v
  - output|o=s

And a complete manifest with every supported key, for when you're doing the absolute most (respectfully):

---
commands:
  frobnicate: My::Script::Role::Frobnicate
  list:       My::Script::Role::List
  default:    cmd_frobnicate
options:
  - help|h
  - verbose|v
  - output|o=s
default_options:
  verbose: 0
extra_options:
  - dbh
  - config_data

πŸ¦‹ Command Values

Every command in the manifest maps to either a role class name or a sub name. Two flavors, both slay:

  • Role class name (it contains ::) β€” the role gets composed into your main module, and the method cmd_command is resolved from the role. So code-review resolves to cmd_code_review.
  • Sub name β€” resolved directly via can() on your class. Use this one for alias commands that point at an existing method:
default: cmd_frobnicate

πŸŽ€ Roles With No Commands

Some roles are out here providing framework behavior instead of commands β€” think an init() method for startup validation, that kind of vibe. Since these roles have no command entry in the manifest, you have to compose them manually in your main module:

package My::Script;

use CLI::Simple qw(:roles);
use Role::Tiny::With;
use parent qw(CLI::Simple);

with 'My::Script::Role::Init';

caller or exit __PACKAGE__->main;

1;
Note: a future version of CLI::Simple will support an extra_roles key in the manifest to handle this automatically. it's coming, bestie, we're manifesting it 🌈

πŸ’– Activating Role-Based Architecture

Just add :roles to your use CLI::Simple statement:

use CLI::Simple qw(:roles);

That single flag triggers manifest loading at compile time. The manifest gets located using the fallback chain described above, the roles are composed into your class, and the dispatch table is built β€” all before new() is even called. iconic behavior ✨


πŸ‘‘ The Inherited main()

When you're using :roles, your class inherits main() straight from CLI::Simple. It reads the manifest, constructs the object with the manifest's options and dispatch table, and calls run():

caller or exit __PACKAGE__->main;

Only override main() in your subclass if you genuinely need to add behavior that can't be expressed in the manifest or in init(). like, don't touch it unless you must, it's fine as is 🫢


🧚 Distributing the Manifest

Add the manifest to your distribution's share directory. If you're a CPAN::Maker user, you can add it to extra-files in buildspec.yml so it gets installed into the share directory:

extra-files:
  - share:
    - my-script.yml

During development the manifest is found via %INC. After installation it's found via File::ShareDir. No code changes required between the two environments. we love zero-drama deployment πŸ’œ


🌈 PHILOSOPHY AND DESIGN PRINCIPLES

CLI::Simple is intentionally minimalist. It gives you just enough structure to build command-line tools with subcommands, option parsing, and help handling β€” but without enforcing any particular framework or lifecycle. lowkey the whole point.


⭐ Not a Framework

This module is not App::Cmd, it's not MooseX::Getopt, and it's not a full application toolkit. Instead, it offers:

  • An object-oriented base class with a clean run() dispatcher
  • Command-line parsing via Getopt::Long
  • Built-in logging via Log::Log4perl
  • Subclass hooks like init() for setup and validation
  • Optional role-based architecture via YAML manifest for larger applications

The philosophy is: provide just enough infrastructure, then get out of your way. periodt πŸ’…


🌸 Validation, Defaults, and Configuration

CLI::Simple does not impose a validation model. You may:

  • Use Getopt::Long features (e.g., type constraints, default values)
  • Write your own validation logic in init()
  • Throw exceptions, emit usage, or exit early at any point

The lifecycle is explicit and under your control. You decide how much structure you want to add on top of it. no opinions, just options πŸŽ€


πŸ’œ When to Use

CLI::Simple is ideal for:

  • Internal tools and admin scripts
  • Bootstrapped CLIs where you don't want a framework
  • Users who want to subclass a clean, minimal interface
  • Applications that have grown beyond a single module and benefit from role-based command composition

For interactive CLI handling or complex command trees, consider App::Cmd or CLI::Framework instead. know your lane, pick your tool πŸ¦‹


✨ The init-run Lifecycle

  • Phase 0: Internal Commands

Before anything else, CLI::Simple checks @ARGV for internal commands prefixed with -. If one is found it executes immediately and exits. See "INTERNAL COMMANDS".

  • Phase 1: Manifest Loading

For role-based applications using use CLI::Simple qw(:roles), the YAML manifest is loaded at compile time during import. Roles are composed into the calling class and the dispatch table is built before new() is ever called. Single-module applications skip this phase entirely.

  • Phase 2: Initialization (new = init)

The constructor parses command-line arguments via Getopt::Long, creates accessors for all options, and calls your init() method. Inside init(), your application has full access to the parsed options and arguments. This phase is the ideal hook for all final setup tasks, such as:

  • Validating command-line arguments.
  • Loading configuration files based on a --config option.
  • Dynamically overriding the command (e..g, $self->command('new_default')).
  • Performing any setup required before a command is run.
  • Phase 3: Execution (run)

Dispatches to the command method determined during initialization.


🫢 "opt-in" Default Command

By design, CLI::Simple does not impose a default command. This provides total flexibility for the application author:

  • You Can Set a Default: If your application needs a default command (e.g., to run help when no command is given), you can set $AUTO_HELP, explicitly set the default command in the command hash you pass to the constructor, or use command() to set one inside the init() method.
  • You Can Have No Default: If you do not set a default, run() will simply do nothing and return cleanly if no command is provided on the command line.

This "no default by default" behavior is what enables a powerful "setup-only" execution mode. A user can run your script without specifying a command. This will:

  1. Run the entire new() / init() phase, performing all setup.
  2. Call run(), which will find no command and exit cleanly.

This provides an ideal hook for applications that need to perform "on-demand initialization" (e.g., seeding a database, authenticating) by checking for a specific flag inside init(), without also triggering an unwanted command. chef's kiss setup, honestly πŸ˜­πŸ’–

In role-based applications using a YAML manifest, a default command that aliases another command should map to the sub name directly rather than a role class:

commands:
  default: cmd_install
  install: My::Module::Role::Installer

🌟 $AUTO_HELP and $AUTO_DEFAULT

Two package variables can be used to further control the lifecycle. By default, the framework provides no default command as explained in the sections above. Some scripters may want default behaviors that assume a command or provide usage if no command is provided.

🌸 $AUTO_HELP

Set the package variable $AUTO_HELP to a true value if you want CLI::Simple to provide help when no command is provided.

default: false

🌸 $AUTO_DEFAULT

Set the package variable $AUTO_DEFAULT to a true value if you want CLI::Simple to automatically select a command if you have only 1 command defined and no command is provided on the command line. When true, it will prepend the single command name to the argument list, allowing any subsequent arguments to be correctly parsed as args for that command.

default: false

πŸ“ $PAGER

Set the package variable $PAGER to a true value to route help output through IO::Pager when --help is invoked. When enabled, IO::Pager selects an appropriate pager (less, more, etc.) based on the PAGER environment variable, falling back to a sensible default. Set to false to suppress pager use and write help directly to STDOUT.

use CLI::Simple qw($PAGER);
$PAGER = 0;  # disable pager

default: true

Note: IO::Pager must be installed for pager support. If it is not available, help output is written directly to STDOUT regardless of the value of $PAGER.

πŸŽ€ CONSTANTS

CLI::Simple does not define its own constants directly, but it is often used in conjunction with CLI::Simple::Constants, which provides a collection of exportable values commonly needed in command-line scripts.

These include:

  • Boolean flags like $TRUE, $FALSE, $SUCCESS, and $FAILURE
  • Common character tokens such as $COLON, $DASH, $EQUALS_SIGN, etc.
  • Log level names compatible with Log::Log4perl

To use them in your script:

use CLI::Simple::Constants qw(:all);

πŸ’… ADDITIONAL NOTES

  • All options are case insensitive
  • See CLI::Simple::Utils to learn about additional utilities useful when writing scripts, including choose, slurp, and dmp.
  • %INTERNAL_COMMANDS is a package variable β€” subclasses can add their own internal commands by pushing entries into the hash before calling new().

πŸŽ€ CUSTOMIZING HELP OUTPUT πŸŽ€

help_sections

ok so by default CLI::Simple is gonna render these POD sections in your help when they actually exist in your file:

SYNOPSIS
DESCRIPTION/Commands
DESCRIPTION/Options
OPTIONS

SYNOPSIS is the preferred spot for your usage info, bestie β€” that's the main character. But for backward compat reasons, if your POD doesn't have a SYNOPSIS section, it'll fall back to USAGE instead.

✨ important: SYNOPSIS and USAGE are never both displayed by default. pick a lane.

You can override that default selection whenever you want by passing an array reference of section names at construction time:

my $cli = CLI::Simple->new(
  help_sections => [qw(SYNOPSIS COMMANDS OPTIONS)],
  ...
);

The way that works is: once you supply help_sections explicitly, CLI::Simple honors that list exactly as provided, no edits, no opinions. Like, an application could totally ask for both SYNOPSIS and USAGE:

help_sections => [qw(SYNOPSIS USAGE)]

Section names follow Pod::Usage conventions, iykyk. Subsections get specified with a / separator β€” so for example DESCRIPTION/Commands renders only the Commands subsection that lives under DESCRIPTION. slay.


🌈 INTERNAL COMMANDS 🌈

CLI::Simple reserves command names that begin with - for its own use. These get intercepted before option parsing even starts and they execute immediately, bypassing the normal lifecycle entirely. (go peek at "The init-run Lifecycle" if you wanna know more.)

Internal commands get dispatched through the %INTERNAL_COMMANDS package variable:

our %INTERNAL_COMMANDS = (
  '-generate-completion' => \&_cmd_generate_completion,
  '-dump-spec'           => \&_cmd_dump_spec,
  '-scaffold'            => \&_cmd_scaffold,
  '-migrate'             => \&_cmd_migrate,
);

And subclasses can absolutely add their own internal commands by extending that hash before new() gets called:

our %INTERNAL_COMMANDS = (
  %CLI::Simple::INTERNAL_COMMANDS,
  '-my-command' => \&_cmd_my_command,
);

-generate-completion

This one generates a bash completion script for your script's commands and options, derived from the live object state. For context: bash completions are that feature where the shell auto-finishes commands, file paths, and options when you hit the Tab key. 🫢

my-script -generate-completion > \
  ~/.local/share/bash-completion/completions/my-script

After you've generated that bash completion script, source it in your current shell to test it out:

source ~/.local/share/bash-completion/completions/my-script

Then test by typing your script name, a space, and pressing Tab. You should see the available commands pop up. Wanna verify option completion? Type -- and press Tab. πŸ’…

To make completions permanent, most systems will automatically source files placed in ~/.local/share/bash-completion/completions/ whenever bash-completion 2.x is installed. If your system isn't picking them up automatically (rude), add this line to your ~/.bashrc:

source ~/.local/share/bash-completion/completions/my-script

Alternatively you can drop the generated file in the system-wide completion directory β€” heads up, that one requires root:

my-script -generate-completion > \
  /etc/bash_completion.d/my-script

ok so where does the script name come from? It's taken from the first argument if one's provided, then MODULINO_WRAPPER if that's set, then finally inferred from the module name. And if that inferred name can't be found in PATH, you get a warning β€” but the completion script still gets generated anyway. we persist. πŸ’œ

Note: if you created the modulino with the supplied create-modulino tool, then MODULINO_WRAPPER is already set inside the bash script that invokes the modulino. one less thing, bestie.

Case 1: Your modulino wrapper and module name are aligned

The modulino script my-modulino refers to My::Modulino

my-modulino -generate-completion

Case 2: Your modulino wrapper was created using create-modulino

The modulino script my-alias refers to My::Modulino. They are not aligned however MODULINO_WRAPPER is set by the bash wrapper.

my-alias -generate-completion

Case 3: Your modulino is an alias not created by create-modulino

The script name my-alias is not aligned with your module name My::Module and your modulino wrapper does not set MODULINO_WRAPPER. The -generate-completion script called by your custom wrapper most likely only resolves the program name as the path to your Perl module:

path-to-modules/My/Module.pm

...in this case you need to supply the alias name or set MODULINO_WRAPPER in the environment.

my-alias -generate-completion my-alias

-dump-spec

This one introspects the running modulino and writes a YAML manifest to the current directory. The filename is derived from the module name, by convention.

my-script -dump-spec           # sub names - baby step toward roles
my-script -dump-spec roles     # role class names - full commitment

Without the roles argument, commands map to their existing sub names, so the manifest is immediately usable without moving a single line of code. With roles, commands map to derived role class names that are suitable for use with -scaffold. 🌸

Alias commands β€” those whose coderef resolves to a sub name that doesn't match the command key β€” are always written as sub names regardless of mode. no exceptions, periodt.

-scaffold

Generates a role-based project tarball from the running modulino, or from an explicit spec file.

ok so there are basically three architectures you can employ when you build a CLI::Simple based application. Let's go.

One: an application that contains all of the options, command specifications and the command subroutines themselves in one package. That's the simplest. This monolithic architeture looks something like this:

package FooBar;

use strict;
use warning;

use parent qw(CLI::Simple);

caller or exit __PACKAGE__->main();

sub cmd_foo {
}

sub cmd_bar {
}

sub main {
  return __PACKAGE__->new(commands => { foo => \&cmd_foo, bar => \&cmd_bar,
                          options => [ qw(h|help infile|i=s) ],
                         )->run;
}

1;

Two: however, a better architecture as your application gets more complicated is to use a role (e.g using Role::Tiny for each command). In a hybrid role/monolith you split the commands into separate files and compose them into your package.

package FooBar::Foo;

use Role::Tiny;

sub cmd_foo { };

1;

package FooBar::Bar;

use Role::Tiny;

sub cmd_bar { };

1;

package FooBar;

use strict;
use warnings;

use Role::Tiny::With;
with 'FooBar::Foo';
with 'FooBar::Bar';

sub main {
  return __PACKAGE__->new(commands => { foo => \&cmd_foo, bar => \&cmd_bar,
                          options => [ qw(h|help infile|i=s) ],
                         )->run;
}

Three: the third architecture uses role based command files and a YAML file that contains all of your options and command specifications. You include that file (named after your package) with the distribution.

---
commands:
  foo: FooBar::Foo
  bar: Foobar::Bar
options:
  - help|h
  - infile|i=s

Your true role based application then becomes:

package FooBar;

use CLI::Simple qw(:roles);
use parent qw(CLI::Simple);

caller or exit __PACKAGE__->main;

1;

The -scaffold command can take a monolithic application or a YAML file like the one above and create the project hierarchy for a role based application. πŸ’« The command will create a tarball that contains role stubs, a slimmed main module with extracted POD (if your monolith contained any), a project.mk with inter-module dependencies, and the YAML manifest.

If you've turned your monolith's package into a modulino:

my-script -scaffold                        # introspect live module

...or use cli-simple if you have a .yml file.

cli-simple -scaffold my-script.yml         # scaffold from spec file

The tarball will be named my-script-roles.tar.gz by convention (that's the lower case snake cased version of the class name). That name is used to infer the class name. So if your filename is different than the classes you want to scaffold, you will need to edit the files. πŸ™ˆ

Extract the content to a directory and start editing. And if you feed the tarball to CPAN::Maker::Bootstrapper via the import-scaffold command, you can produce a complete buildable CPAN distribution. iconic, honestly.

-migrate

Combines -dump-spec roles and -scaffold in a single step.

my-script -migrate

It writes the YAML manifest then generates the role-based tarball. Use this one when you're ready for a full migration and you do not need to inspect or edit the manifest first. If you do wanna review or adjust the manifest before scaffolding, run -dump-spec and -scaffold separately instead. 😌

new ✨

new( args )

Okay bestie, this is where it all begins πŸ’– It spins up a brand-new CLI::Simple instance, parses your options, optionally kicks off logging, and then hands you a bunch of auto-generated accessors so you can actually get at those options. It's giving main character energy.

Heads up, queen β€” new() calls Getopt::Long's GetOptions under the hood, and that thing directly mutates @ARGV by yanking out every option it recognizes. Whatever's left in @ARGV after that is treated as your command name plus its arguments. No cap. 🫢

args is a hash (or a hash reference, we're flexible) with these keys:

  • abbreviations β€” a boolean that decides whether shortened command names are allowed.

When it's true, the run() method takes your provided command and treats it like a prefix, comparing it against the keys in your command hash. Find exactly one match? Slay, that's your command. Find more than one match, or zero matches? run() throws an exception, sorry babe πŸ’€

This lets you do cute shorthand like:

mytool disable-sched    # expands to 'disable-scheduled-task'

default: false

  • commands β€” (required, iykyk) a hash mapping command names to either a subroutine reference or an array reference.

If you use an array reference, element one has to be a subroutine reference and element two should be a valid log level. (See "Per Command Log Levels." for the tea ✨)

Example:

{
  send          => \&send_message,
  receive       => \&receive_message,
  list_messages => [ \&list_messages, 'error' ],
}

Not using command names at all? That's valid πŸ’… Just set a "default" key pointing at the subroutine or method you want to run:

{ default => \&main }

No default provided? Then the behavior gets decided by the $AUTO_DEFAULT and $AUTO_HELP package variables. Flipping $AUTO_DEFAULT to true when your commands hash holds exactly one command makes that command run automatically whenever no command name shows up on the command line β€” so you can treat your whole program like a single-command tool and pass arguments straight in without ever naming the command. Obsessed. πŸ”₯

  • default_options β€” (optional) a hash reference of default values for options. These kick in whenever the matching option isn't given on the command line.
  • extra_options β€” (optional) an array reference naming extra accessors you want created, even if they're not part of option_specs.

Example:

extra_options => [ qw(foo bar baz) ]
  • option_specs β€” (optional) an array reference of option specs, exactly as Getopt::Long accepts them. These define the command-line options your program knows about.
  • validate_command β€” so normally CLI::Simple validates your command and throws an exception if that command was never registered. Set this attribute to a non-true value and it'll stop doing that.

The classic use case? Letting a script assume a default command while still allowing loose arguments. Say you've got a script foo with a command get that takes arguments:

foo get something

...but you also want people to be able to do:

foo something

Then follow this recipe, bestie:

sub init {
  my ($self) = @__;

  my @args = $self->get_args;

  if ( ! @args ) {
    $self->command_args($self->command()); # set the args to the command
    $self->command('get'); # set the command to your default
  }
  else {
    die "ERROR: unknown command\n"
      if !$self->commands->{$self->command}; # validate the command
  }
  ...
  return;
}
NOTE: this only works if your commands have a deterministic number of arguments. Like, maybe you always require at least 1 argument. If you have no arguments β€” as in the recipe above β€” you'd be assuming the command itself is the argument to your default command. 🌸

command πŸ‘‘

command
command(command)

Gets or sets the command to execute. Usually it's just the first argument on the command line once all the options have been parsed. But sometimes you wanna override that argument β€” go off πŸŽ€ β€” and you can: pass in a new command and that's what gets executed when you call run().

command_args πŸ’¬

my $args = $self->command_args();

Gets or sets the argument list. It's basically get_args when you don't pass any arguments, except it hands you back an array reference instead. Little difference, big energy.

Want to replace or add to the argument list? Just pass an array or a list:

my $args = $self->command_args;
$self->command_args(@{$args}, 'foo');

commands (required) πŸ“‹

commands
commands(command, handler)

Returns the hash you handed to the constructor as commands. You can also use it to insert a brand-new command into the commands hash β€” handler should be a code reference, no exceptions:

commands(foo => sub { return 'foo' });

main 🌟

__PACKAGE__->main;

Role-based application? Then main is inherited straight from CLI::Simple and it reads the YAML manifest loaded during import. It builds the object using the manifest's options, default options, extra options, and dispatch table β€” then calls run(). Chef's kiss fr fr.

In a role-based modulino, the entire main sub shrinks down to:

caller or exit __PACKAGE__->main;

Single-module application instead? Then you override main in your subclass like you normally would. Periodt. πŸ’…

run πŸƒβ€β™€οΈ

Runs the script with the given options, commands and arguments. The run method interprets the command line and passes control over to your command subroutines. Your subroutines should return 0 for success and a non-zero value for failure β€” that error code gets passed to the shell as the script's return code. So, you know, be honest about your failures. πŸ’€

get_args πŸŽ€

Returns the arguments that follow the command.

get_args(NAME, ... )     # with names
get_args()               # raw positional args

With names 🫢

In scalar context, returns a hash reference mapping each NAME to the corresponding positional argument. In list context, returns a flat list of (name => value) pairs.

Example:

sub send_message {
  my ($self) = @_;

  my %args = $self->get_args(qw(message email));

  _send_message($args{message}, $args{email});
}

When you call get_args with a list of names, values get assigned in order: first name takes the first argument, second name takes the second, and so on. Only want specific positions? You can use undef as a placeholder, iconic move:

my %args = $self->get_args('message', undef, 'cc');  # args 1 and 3

Fewer positional arguments than names? The leftover names get set to undef. Extra positional arguments beyond the names you provided? Ignored, respectfully. πŸ™ˆ

With no names ✨

In scalar context it returns an array reference holding the command's positional arguments. In list context it returns a plain old list holding the command's positional arguments.

init 🌸

Define your own init() method and the constructor will call it for you. Use it to do all the little setup things you need done before you execute the run() method. Your pre-game routine, basically.


USING PACKAGE VARIABLES 🌈

You can pass all the parameters you need for your command-line script right into the constructor β€” or some people prefer seeing them laid out clearly in the code like little trophies. So, accordingly: you can use package variables with the same name as the constructor arguments, in upper case. πŸ’œ

our $OPTION_SPECS = [
  qw(
    help|h
    log-level=s|L
    debug|d
  )
];

our $COMMANDS = {
  foo => \&foo,
  bar => \&bar,
};

Subclasses can extend the built-in internal commands too, just by adding entries to %INTERNAL_COMMANDS:

our %INTERNAL_COMMANDS = (
  %CLI::Simple::INTERNAL_COMMANDS,
  '-my-command' => \&_cmd_my_command,
);

COMMAND LINE OPTIONS πŸ”₯

Command-line options get defined using Getopt::Long-style specifications. You pass them into the constructor through the option_specs parameter:

my $cli = CLI::Simple->new(
  option_specs => [ qw( help|h foo-bar=s log-level=s ) ]
);

Inside your command subroutines you reach those values through automatically generated getter methods:

$cli->get_foo();
$cli->get_log_level();

And here's the cute part πŸ¦‹ β€” option names containing dashes (-) get auto-converted to snake_case for the accessor methods. So:

option_specs => [ 'foo-bar=s' ]

...results in:

$cli->get_foo_bar();

set_args πŸ”§

Resets the positional arguments.

$self->set_args(qw(foo 1));

This method overrides the positional arguments that were originally passed to the script. You can get the same behavior by calling get_args in scalar context and modifying the reference directly:

my $args = $self->get_args;
$args->[1] = '2';

Reach for that technique when you don't want to alter the entire set of arguments β€” just a lil slice. ✨


COMMAND ARGUMENTS πŸ’¬

If your commands accept positional arguments, you pull them out with the get_args method.

You may optionally hand over a list of argument names, and then the arguments come back as a hash (or a hashref in scalar context) with named values. So convenient.

Example:

sub send_message {
  my ($self) = @_;

  my %args = $self->get_args(qw(phone_number message));

  send_sms_message($args{phone_number}, $args{message});
}

Call get_args() with no argument names and it just returns all the remaining arguments as a list:

my ($phone_number, $message) = $self->get_args;
Note: when it's called with names, get_args returns a hash in list context and a hash reference in scalar context. Context matters, bestie. πŸ‘‘

CUSTOM ERROR HANDLER 😭

By default, CLI::Simple exits if GetOptions returns a false value β€” which means something went wrong while parsing options. You can override that behavior in one of two ways:

  • Set $CLI::Simple::GETOPT_EXIT_ON_ERROR to a false value.

This disables the automatic exiting and lets your program decide what to do after an option-parsing failure.

  • Provide an error_handler callback in the constructor.
my $cli = CLI::Simple->new(
  commands        => \%commands,
  default_options => \%default_options,
  extra_options   => \@extra_options,
  option_specs    => \@option_specs,
  abbreviations   => $TRUE,
  error_handler   => sub {
    my ($msg) = @_;
    print {*STDERR} $msg;
    return $TRUE;   # continue processing
  },
);

The error handler gets called with the error message from GetOptions. It must return a boolean: a true value lets processing continue, while a false value makes CLI::Simple exit immediately. Choose wisely πŸ’€

πŸŽ€ SETTING DEFAULT VALUES FOR OPTIONS πŸŽ€

okay bestie so you want your options to already have a value before anyone types anything?? periodt. just pass a hash reference as the default_options argument to the constructor. Those values get used unless the user explicitly overrides them on the command line. user wins, always, no cap.

Example:

my $cli = CLI::Simple->new(
  default_options => { foo => 'bar' },
  option_specs    => [ qw(foo=s bar=s) ],
  commands        => {
    foo => \&foo,
    bar => \&bar,
  },
);

Defaulted options are accessible through their corresponding getter methods, just like options set via the command line. πŸ’– same vibe, same access, iconic.


πŸ“œ ADDING USAGE TO YOUR SCRIPTS πŸ“œ

wanna give your script built-in usage/help output?? yes you do. include a =head1 SYNOPSIS section in your script's POD:

=head1 SYNOPSIS

usage: myscript [options] command args

Options


--help, -h Display help ...

If the user supplies the command help, or the --help option, CLI::Simple displays the configured help sections using Pod::Usage. πŸ¦‹ so convenient.

For backward compatibility, USAGE is also supported. If a USAGE section is present, it is used as the usage section.

If no USAGE section is present, SYNOPSIS is used instead.

When both SYNOPSIS and USAGE are present, USAGE is used by default. Applications that explicitly configure help_sections may select the desired section. tl;dr: USAGE is the main character when both are around.

Custom help() Method

If you need full control over the help output, you can define a custom help method and assign it as a command:

commands => {
  help => &help,
  ...
};

This is useful if your module follows the modulino pattern and you want to present help information that differs from the embedded POD. πŸ’… you're the author, you decide.


🎁 ADDING ADDITIONAL SETTERS 🎁

So every single command-line option is automatically available through getter methods named get_*. cute, right??

If you need to create additional accessors (getters and setters) for values that are not derived from the command line, use the extra_options parameter. Perfect for passing runtime configuration or computed values throughout your application. ✨

Example:

my $cli = CLI::Simple->new(
  default_options => { foo => 'bar' },
  option_specs    => [ qw(foo=s bar=s) ],
  extra_options   => [ qw(biz buz baz) ],
  commands        => {
    foo => \&foo,
    bar => \&bar,
  },
);

This will generate get_biz, set_biz, get_buz, etc., for internal use. setters AND getters, you love to see it.


πŸ“Š LOGGING πŸ“Š

CLI::Simple integrates with Log::Log4perl to provide structured logging for your scripts. 🫢

CLI::Simple will initialize Log::Log4perl for you when you call use_log4perl(). This is a convenience, not a requirement β€” you can log however you like. To enable logging via CLI::Simple, call the class method use_log4perl() in your module or script:

__PACKAGE__->use_log4perl(
  level  => 'info',
  config => $log4perl_config_string
);

If you do not explicitly include a log-level option in your option_specs, CLI::Simple will automatically add one for you. free option, we love a generous queen πŸ‘‘

Once enabled, you can access the logger instance via:

my $logger = $self->get_logger;

This logger supports the standard Log4perl methods like info, debug, warn, etc.

Note: Because it is opt-in, CLI::Simple does not itself depend on Log::Log4perl. If your application calls use_log4perl, it owns that dependency and must declare it in its own requires/cpanfile. Static dependency scanners cannot see it β€” the module is loaded dynamically inside the method call β€” so you must add it by hand. 😭

Do not call use_log4perl if you use a different logging framework, or if you initialize Log::Log4perl yourself; it would override your configuration. Call it only when you want CLI::Simple to own logging setup.

🌈 Colored Output 🌈

Pass color => 1 to use_log4perl() to default your script to colorized log output using a built-in appender, instead of supplying your own config:

__PACKAGE__->use_log4perl(
  level => 'info',
  color => 1,
);

Colorizing requires Term::ANSIColor. If it isn't installed, CLI::Simple quietly falls back to uncolored output rather than failing β€” color => 1 is a request, not a hard dependency. it's giving chill, not demanding.

If you'd like the person running your script to be able to override that default from the command line, add color! to your option_specs:

my @option_specs = qw(
  color!
  ...
);

This gives you --color and --no-color for free. Whichever way use_log4perl() set the default, an explicit flag on the command line always wins; if neither --color nor --no-color is passed, your use_log4perl() setting is left alone. Declaring color! is therefore safe to add at any time β€” it only changes behavior for scripts whose users actually pass the flag. πŸ”₯

Note: color and config are mutually exclusive β€” use_log4perl() dies if you pass both. color is specifically for using CLI::Simple's own built-in colorized appender; if you need a custom config, write it to include coloring yourself rather than passing color => 1.

πŸ” Per Command Log Levels πŸ”

Some commands may require more verbose logging than others. For example, certain commands might perform complex actions that benefit from detailed logs, while others are designed solely to produce clean, structured output. different commands, different energy.

To assign a custom log level to a command, use an array reference as the value for that command in the commands hash passed to the constructor.

The array reference should contain at least two elements:

  1. A code reference to the command subroutine
  2. A log level string: one of 'trace', 'debug', 'info', 'warn', 'error', or 'fatal'

Example:

CLI::Simple->new(
  option_specs    => [qw( help format=s )],
  default_options => { format => 'json' },  # set some defaults
  extra_options   => [qw( content )],       # non-option, setter/getter
  commands        => {
    execute => \&execute,
    list    => [ \&list, 'error' ],
  }
)->run;
TIP: add other elements to the array for your command to process.
Note: Per-command log levels are not currently supported in the YAML manifest. Define them programmatically by overriding main() if needed.

πŸ’… FAQ πŸ’…

  • How do I execute startup code before my command runs?

Implement an init() method in your class. The new() constructor will invoke this method before returning and before run() is executed.

Your init() method will have access to all options and arguments. Logging will also be initialized, so you can use get_logger() to emit messages. ✨

  • Do I need to implement commands?

No. If your script doesn't support multiple commands, you can specify a default key instead:

commands => { default => \&main }
  • Must I subclass CLI::Simple?

No. You can use it procedurally or functionally. no inheritance required, she's flexible like that.

  • How do I turn my class into a script?

Use the modulino pattern: create a class that checks whether it is being invoked directly:

package MyScript;

caller or exit __PACKAGE__->main();

sub main {
  ...
}

This lets the file be used as both a module and an executable script. 🌈 iconic behavior honestly.

  • How do I migrate an existing script to role-based architecture?

Run the built-in -dump-spec command to generate a YAML manifest from your existing script, then -scaffold to generate role stubs:

my-script -dump-spec        # generates my-script.yml
my-script -scaffold         # generates my-script-roles.tar.gz

See ROLE-BASED ARCHITECTURE for the full migration workflow. πŸ¦‹

  • How do I start a new role-based project from scratch?

Write a YAML manifest and use the cli-simple wrapper to scaffold it:

cli-simple -scaffold my-script.yml

See ROLE-BASED ARCHITECTURE for the manifest format.

  • How do I enable bash completion for my script?

Your script must be invoked via a bash modulino wrapper with MODULINO_WRAPPER set. Then run:

my-script -generate-completion > \
  ~/.local/share/bash-completion/completions/my-script

Wrappers generated by CPAN::Maker::Bootstrapper set MODULINO_WRAPPER automatically. πŸ’…

  • How do I add my own internal commands?

Add entries to %INTERNAL_COMMANDS before calling new():

our %INTERNAL_COMMANDS = (
  %CLI::Simple::INTERNAL_COMMANDS,
  '-my-command' => \&_cmd_my_command,
);
  • My application dies with use_log4perl() requires Log::Log4perl...

Something in your code calls __PACKAGE__->use_log4perl(...) but Log::Log4perl is not installed in the environment. This commonly first appears in a clean CI run, a hermetic build, or a fresh install β€” anywhere the module was not already lying around. 😭

You have two choices, depending on what you meant:

  • You want Log::Log4perl logging. Add it to your distribution's dependencies (requires 'Log::Log4perl';). It is not a CLI::Simple prerequisite by design, and static scanners will not add it for you because the call is dynamic β€” so declare it yourself.
  • You did not mean to use it. If you log another way, or manage Log::Log4perl yourself, just remove the use_log4perl call.
# audit any CLI::Simple dist for the mismatch:
grep -rl use_log4perl lib bin && grep -q Log::Log4perl cpanfile \
  || echo 'use_log4perl() called but Log::Log4perl not in cpanfile'

🌈 ALIASING OPTIONS AND COMMANDS 🌈

okay bestie, this is the glow-up feature fr fr. CLI::Simple lets you define short, human-friendly aliases for both option names and command names. you use the alias parameter to new():

my $app = CLI::Simple->new(
  option_specs    => [ qw(config=s verbose!) ],
  commands        => { list => \&list, execute => \&execute },
  alias => {
    options  => { cfg => 'config', v => 'verbose' },
    commands => { ls  => 'list'   }
  },
);

✨ How option aliases work ✨

  • spec tail is copied automatically β€” you only name the canonical option in option_specs. for each alias, CLI::Simple finds the canonical option's spec tail (like =s, :i, !, +) and appends it to the alias. so in the example above, cfg behaves as if you'd literally written cfg=s, and v behaves as if you'd written v!.
heads up bestie: if your option includes a one-letter short-cut and the alias does not start with the same letter, it will not be automatically enabled as a short-cut. πŸ‘€
  • accessors are created for both names β€” accessors get generated from all option names (canonical and aliases), with - normalized to _. in the example, both get_config() and get_cfg() are available. two names, one slay. πŸ’…
  • values are mirrored after parsing β€” after option parsing and normalization, values get mirrored so either name can be used consistently. and if both the canonical name and its alias show up on the command line, the alias wins and becomes the final value for both names.
  • no duplicate injection β€” if the alias already exists in option_specs, it will not be injected again; value mirroring still occurs. no double-dipping. ✨
  • errors are explicit β€” if an alias points at a canonical option that does not exist, CLI::Simple croaks with a clear error. loud and clear, no ghosting. πŸ’€
  • case sensitivity β€” Getopt::Long is used with :config no_ignore_case, so option names (and therefore aliases) are case sensitive by default. capital letters MATTER here, bestie.

πŸŽ€ How command aliases work πŸŽ€

  • simple mapping β€” provide alias => { commands => { alias => canonical } } to map an alias to an existing command. in the example, ls dispatches to the list command. iconic behaviour honestly.
  • applied before abbreviations β€” aliases are installed before command abbreviation resolution. if you enable abbreviations, they apply to the full set of command names, including any aliases.
  • errors are explicit β€” if an alias points at a command that does not exist, CLI::Simple croaks with a clear error. it will not let that slide, periodt.

🌸 Usage examples 🌸

# Using an option alias
script.pl --cfg app.json execute

# Using a command alias
script.pl ls

after parsing, both get_config() and get_cfg() will return the same value. and if the user passes both --config and --cfg, the value from --cfg (the alias) is used.

note: in role-based applications using a YAML manifest, command aliases are expressed by mapping the alias command directly to the target sub name rather than a role class. see ROLE-BASED ARCHITECTURE.

πŸ’œ Recommendations πŸ’œ

  • keep the canonical spec single-named β€” define a single canonical name in option_specs and add other spellings via alias. avoid multi-name specs like config|cfg=s; use alias instead.
  • document your precedence β€” if you prefer the alias name to win when both are supplied, enforce that in your application or adjust the mirroring order. by default, the canonical name wins.

πŸ¦‹ ERRORS/EXIT CODES πŸ¦‹

when you execute the run() method it passes control to the method that implements the command specified on the command line. your method is expected to return 0 for success or an error code that you can pass to the shell on exit.

exit CLI::Simple->new(commands => { foo => \&cmd_foo })->run();

⭐ Exit Codes ⭐

CLI::Simple uses conventional exit codes so that calling scripts can distinguish between normal completion and error conditions. very gives-you-clarity of it all.

  • '0' β€” successful completion of a command (SUCCESS). we love a happy ending. 🫢
  • '1' β€” general usage error, such as --help display via pod2usage, or an invalid command line (FAILURE).
  • '2' β€” option parsing failure, such as an unrecognized option or invalid argument (also reported as FAILURE).
  • any other code β€” if a user-supplied command callback explicitly calls exit() or returns a numeric value other than 0 - 2, that code is passed through unchanged to the shell. this allows application-specific exit codes. make it your own, bestie. πŸ‘‘

this module is free software; you can redistribute it and/or modify it under the same terms as Perl itself. see <https://dev.perl.org/licenses/> for more information. free as in freedom, slay as in slay. 🌈


🧚 SEE ALSO 🧚

Getopt::Long, CLI::Simple::Constants, CLI::Simple::Utils, Pod::Usage, App::Cmd, CLI::Framework, Role::Tiny, CPAN::Maker::Bootstrapper


πŸ‘‘ AUTHOR πŸ‘‘

Rob Lauer - <rlauer@treasurersbriefcase.com>


and that's a wrap on the whole page, bestie!! πŸŽ€ this was perl v5.42.3, dated 2026-10-06, for CLI::Simple(3) β€” go forth and write the most iconic little CLI of your life, you absolutely ate this. πŸ’…βœ¨πŸ¦‹

πŸ“¦ where this page comes from