CLI::Simple(3)
β¨ 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
useyour modulino and call its methods directly - Introspectable β tools like
-dump-specand-generate-completioncan 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 andcompletetarget correctly- Man page symlinks via CPAN::Maker::Bootstrapper β so
man my-scriptresolves 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 methodcmd_commandis resolved from the role. Socode-reviewresolves tocmd_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 ofCLI::Simplewill support anextra_roleskey 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::Longfeatures (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
--configoption. - 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
helpwhen no command is given), you can set$AUTO_HELP, explicitly set thedefaultcommand in thecommandhash you pass to the constructor, or usecommand()to set one inside theinit()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:
- Run the entire
new()/init()phase, performing all setup. - 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::Pagermust be installed for pager support. If it is not available, help output is written directly toSTDOUTregardless 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::Utilsto learn about additional utilities useful when writing scripts, includingchoose,slurp, anddmp. %INTERNAL_COMMANDSis a package variable β subclasses can add their own internal commands by pushing entries into the hash before callingnew().
π 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 suppliedcreate-modulinotool, thenMODULINO_WRAPPERis 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'sGetOptionsunder the hood, and that thing directly mutates@ARGVby yanking out every option it recognizes. Whatever's left in@ARGVafter 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 ofoption_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 normallyCLI::Simplevalidates 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_ERRORto a false value.
This disables the automatic exiting and lets your program decide what to do after an option-parsing failure.
- Provide an
error_handlercallback 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::Simpledoes not itself depend on Log::Log4perl. If your application callsuse_log4perl, it owns that dependency and must declare it in its ownrequires/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:colorandconfigare mutually exclusive βuse_log4perl()dies if you pass both.coloris specifically for usingCLI::Simple's own built-in colorized appender; if you need a custom config, write it to include coloring yourself rather than passingcolor => 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:
- A code reference to the command subroutine
- 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_log4perlcall.
# 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::Simplefinds the canonical option's spec tail (like=s,:i,!,+) and appends it to the alias. so in the example above,cfgbehaves as if you'd literally writtencfg=s, andvbehaves as if you'd writtenv!.
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, bothget_config()andget_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::Simplecroaks with a clear error. loud and clear, no ghosting. π - case sensitivity β
Getopt::Longis 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,lsdispatches to thelistcommand. 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::Simplecroaks 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_specsand add other spellings viaalias. avoid multi-name specs likeconfig|cfg=s; usealiasinstead. - 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--helpdisplay viapod2usage, or an invalid command line (FAILURE).'2'β option parsing failure, such as an unrecognized option or invalid argument (also reported asFAILURE).- any other code β if a user-supplied command callback explicitly calls
exit()or returns a numeric value other than0-2, that code is passed through unchanged to the shell. this allows application-specific exit codes. make it your own, bestie. π
π LICENSE AND COPYRIGHT π
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
- fedora 43: perl-CLI-Simple 2.2.5-1.fc43 Β· GPL-1.0-or-later OR Artistic-1.0-Perl
- π girlified by deepseek-ai/DeepSeek-V4.1-Flash on : a language model's rewrite of the original, under its terms