CLI-Simple-2.4.0/0000755000175100017510000000000015270644577012744 5ustar rlauerrlauerCLI-Simple-2.4.0/META.json0000664000175100017510000000613115270644577014370 0ustar rlauerrlauer{ "abstract" : "Simple command line script accelerator", "author" : [ "Rob Lauer " ], "dynamic_config" : 1, "generated_by" : "ExtUtils::MakeMaker version 7.76, CPAN::Meta::Converter version 2.150010", "license" : [ "perl_5" ], "meta-spec" : { "url" : "http://search.cpan.org/perldoc?CPAN::Meta::Spec", "version" : 2 }, "name" : "CLI-Simple", "no_index" : { "directory" : [ "t", "inc" ] }, "prereqs" : { "build" : { "requires" : { "ExtUtils::MakeMaker" : "6.64", "File::ShareDir::Install" : "0" } }, "configure" : { "requires" : { "ExtUtils::MakeMaker" : "6.64", "File::ShareDir::Install" : "0" } }, "runtime" : { "requires" : { "Class::Accessor::Fast" : "0.51", "File::ShareDir" : "1.118", "File::Which" : "1.27", "IO::Interactive" : "1.027", "JSON" : "4.10", "List::Util" : "1.33", "Readonly" : "2.05", "Role::Tiny" : "2.002005", "YAML::Tiny" : "1.76", "perl" : "5.010" }, "suggests" : { "IO::Pager" : "2.10", "Log::Log4perl" : "1.58", "Perl::Tidy" : "20260204" } }, "test" : { "requires" : { "Role::Tiny" : "2.002005", "Test::Exit" : "0.11", "Test::Output" : "1.036", "YAML::Tiny" : "1.76" } } }, "provides" : { "CLI::Simple" : { "file" : "lib/CLI/Simple.pm", "version" : "v2.4.0" }, "CLI::Simple::Constants" : { "file" : "lib/CLI/Simple/Constants.pm", "version" : "v2.4.0" }, "CLI::Simple::DumpSpec" : { "file" : "lib/CLI/Simple/DumpSpec.pm", "version" : "0" }, "CLI::Simple::Helpers" : { "file" : "lib/CLI/Simple/Helpers.pm", "version" : "0" }, "CLI::Simple::Migrate" : { "file" : "lib/CLI/Simple/Migrate.pm", "version" : "0" }, "CLI::Simple::Modulino" : { "file" : "lib/CLI/Simple/Modulino.pm", "version" : "0" }, "CLI::Simple::Scaffold" : { "file" : "lib/CLI/Simple/Scaffold.pm", "version" : "0" }, "CLI::Simple::Shell" : { "file" : "lib/CLI/Simple/Shell.pm", "version" : "0" }, "CLI::Simple::Utils" : { "file" : "lib/CLI/Simple/Utils.pm", "version" : "v2.4.0" } }, "release_status" : "stable", "resources" : { "bugtracker" : { "mailto" : "rlauer6@comcast.net", "web" : "http://github.com/rlauer6/CLI-Simple/issues" }, "homepage" : "http://github.com/rlauer6/CLI-Simple", "repository" : { "type" : "git", "url" : "git://github.com/rlauer6/CLI-Simple.git", "web" : "http://github.com/rlauer6/CLI-Simple" } }, "version" : "v2.4.0", "x_serialization_backend" : "JSON::PP version 4.16" } CLI-Simple-2.4.0/META.yml0000664000175100017510000000340315270644577014217 0ustar rlauerrlauer--- abstract: 'Simple command line script accelerator' author: - 'Rob Lauer ' build_requires: ExtUtils::MakeMaker: '6.64' File::ShareDir::Install: '0' Role::Tiny: '2.002005' Test::Exit: '0.11' Test::Output: '1.036' YAML::Tiny: '1.76' configure_requires: ExtUtils::MakeMaker: '6.64' File::ShareDir::Install: '0' dynamic_config: 1 generated_by: 'ExtUtils::MakeMaker version 7.76, CPAN::Meta::Converter version 2.150010' license: perl meta-spec: url: http://module-build.sourceforge.net/META-spec-v1.4.html version: '1.4' name: CLI-Simple no_index: directory: - t - inc provides: CLI::Simple: file: lib/CLI/Simple.pm version: v2.4.0 CLI::Simple::Constants: file: lib/CLI/Simple/Constants.pm version: v2.4.0 CLI::Simple::DumpSpec: file: lib/CLI/Simple/DumpSpec.pm version: '0' CLI::Simple::Helpers: file: lib/CLI/Simple/Helpers.pm version: '0' CLI::Simple::Migrate: file: lib/CLI/Simple/Migrate.pm version: '0' CLI::Simple::Modulino: file: lib/CLI/Simple/Modulino.pm version: '0' CLI::Simple::Scaffold: file: lib/CLI/Simple/Scaffold.pm version: '0' CLI::Simple::Shell: file: lib/CLI/Simple/Shell.pm version: '0' CLI::Simple::Utils: file: lib/CLI/Simple/Utils.pm version: v2.4.0 requires: Class::Accessor::Fast: '0.51' File::ShareDir: '1.118' File::Which: '1.27' IO::Interactive: '1.027' JSON: '4.10' List::Util: '1.33' Readonly: '2.05' Role::Tiny: '2.002005' YAML::Tiny: '1.76' perl: '5.010' resources: bugtracker: http://github.com/rlauer6/CLI-Simple/issues homepage: http://github.com/rlauer6/CLI-Simple repository: git://github.com/rlauer6/CLI-Simple.git version: v2.4.0 x_serialization_backend: 'CPAN::Meta::YAML version 0.018' CLI-Simple-2.4.0/MANIFEST.SKIP0000644000175100017510000000005215270644577014637 0ustar rlauerrlauerMakefile$ MYMETA.json MYMETA.yml provides CLI-Simple-2.4.0/README.md0000644000175100017510000022340015270644577014224 0ustar rlauerrlauer# Table of Contents * [NAME](#name) * [SYNOPSIS](#synopsis) * [DESCRIPTION](#description) * [VERSION](#version) * [FEATURES](#features) * [QUICK START](#quick-start) * [Single-Module Application](#single-module-application) * [Role-Based Application](#role-based-application) * [MODULINOS](#modulinos) * [Why Modulinos?](#why-modulinos) * [The Bash Wrapper](#the-bash-wrapper) * [create-modulino](#create-modulino) * [MODULINO\_WRAPPER](#modulino\wrapper) * [ROLE-BASED ARCHITECTURE](#role-based-architecture) * [The YAML Manifest](#the-yaml-manifest) * [Loading Multiple Manifests](#loading-multiple-manifests) * [Option Initialization](#option-initialization) * [Command Values](#command-values) * [`roles:` vs `commands:`](#roles-vs-commands) * [When to Use Each Approach](#when-to-use-each-approach) * [Sharing Methods Between Commands](#sharing-methods-between-commands) * [Roles With No Commands](#roles-with-no-commands) * [Activating Role-Based Architecture](#activating-role-based-architecture) * [The Inherited main()](#the-inherited-main) * [Distributing the Manifest](#distributing-the-manifest) * [PHILOSOPHY AND DESIGN PRINCIPLES](#philosophy-and-design-principles) * [Validation, Defaults, and Configuration](#validation-defaults-and-configuration) * [When to Use](#when-to-use) * [The init-run Lifecycle](#the-init-run-lifecycle) * [Opt-in Default Command](#opt-in-default-command) * [`$AUTO_HELP` and `$AUTO_DEFAULT`](#$autohelp-and-$autodefault) * [CONSTANTS](#constants) * [ADDING USAGE TO YOUR SCRIPTS](#adding-usage-to-your-scripts) * [Customizing Help Output](#customizing-help-output) * [Custom help() Method](#custom-help-method) * [`help_sections`](#helpsections) * [INTERNAL COMMANDS](#internal-commands) * [-generate-completion](#-generate-completion) * [-dump-spec](#-dump-spec) * [-scaffold](#-scaffold) * [-migrate](#-migrate) * [METHODS AND SUBROUTINES](#methods-and-subroutines) * [use\_log4perl](#use\log4perl) * [use\_options](#use\options) * [new](#new) * [command](#command) * [command\_args](#command\args) * [commands](#commands) * [main](#main) * [options](#options) * [run](#run) * [get\_args](#get\args) * [With names](#with-names) * [With no names](#with-no-names) * [init](#init) * [USING PACKAGE VARIABLES](#using-package-variables) * [COMMAND LINE OPTIONS](#command-line-options) * [Getopt::Long Configuration](#getoptlong-configuration) * [COMMAND ARGUMENTS](#command-arguments) * [set\_args](#set\args) * [CUSTOM ERROR HANDLER](#custom-error-handler) * [SETTING DEFAULT VALUES FOR OPTIONS](#setting-default-values-for-options) * [OPTION PROVIDERS](#option-providers) * [Implementing an Option Provider](#implementing-an-option-provider) * [Option Resolution and Precedence](#option-resolution-and-precedence) * [ADDING ADDITIONAL ACCESSORS](#adding-additional-accessors) * [LOGGING](#logging) * [Colored Output](#colored-output) * [Per Command Log Levels](#per-command-log-levels) * [FAQ](#faq) * [ALIASING OPTIONS AND COMMANDS](#aliasing-options-and-commands) * [How option aliases work](#how-option-aliases-work) * [How command aliases work](#how-command-aliases-work) * [Usage examples](#usage-examples) * [Recommendations](#recommendations) * [ERRORS/EXIT CODES](#errorsexit-codes) * [Exit Codes](#exit-codes) * [LICENSE AND COPYRIGHT](#license-and-copyright) * [SEE ALSO](#see-also) * [AUTHOR](#author) # NAME CLI::Simple - a minimalist object oriented base class for CLI applications # SYNOPSIS #!/usr/bin/env perl package MyScript; use strict; use warnings; use parent qw(CLI::Simple); caller or exit __PACKAGE__->main(); sub main { 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 => \&cmd_execute, list => \&cmd_list, }, alias => { options => { fmt => 'format' }, commands => { ls => 'list' } }, ); return $cli->run(); } sub cmd_execute { my ($self) = @_; # retrieve a CLI option my $file = $self->get_file; ... return 0; } sub cmd_list { my ($self) = @_; # retrieve a command argument my ($file) = $self->get_args(); ... return 0; } 1; \# role-based CLI Application (2.0.0) \# create a YAML manifest `my-script.yml` in your project root: --- roles: frobnicate: My::Script::Role::Frobnicate list: My::Script::Role::List options: - help|h - verbose|v - output|o=s \# 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; \# 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 [![CLI-Simple](https://github.com/rlauer6/CLI-Simple/actions/workflows/build.yml/badge.svg)](https://github.com/rlauer6/CLI-Simple/actions/workflows/build.yml) 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? `CLI::Simple` makes it easy to create scripts that take _options_, _commands_ and _arguments_. `CLI::Simple` is designed around the _modulino_ pattern - Perl modules that can be executed directly as scripts. See ["MODULINOS"](#modulinos). For common constant values (like `$TRUE`, `$DASH`, or `$SUCCESS`), see [CLI::Simple::Constants](https://metacpan.org/pod/CLI%3A%3ASimple%3A%3AConstants), which pairs naturally with this module. 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](https://metacpan.org/pod/Role%3A%3ATiny) 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. 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](https://metacpan.org/pod/CPAN%3A%3AMaker%3A%3ABootstrapper) and you have a complete, buildable CPAN distribution in one step. Version 2.3.0 adds selective role composition. Commands declared with the `roles` manifest key compose only the roles required by the selected command. The original `commands` form remains supported for backward compatibility and retains its original behavior of composing the complete set of command roles. Version 2.4.0 introduces pluggable option providers. CLI applications frequently obtain option values from multiple sources, including the command line, environment variables, configuration files, and credential stores. Traditionally, each application must implement its own logic for retrieving these values and determining their precedence, often duplicating much of the same code in its `init()` method. Option providers move that responsibility into reusable [Role::Tiny](https://metacpan.org/pod/Role%3A%3ATiny) roles. Applications register the providers they need, in precedence order, and `CLI::Simple` coordinates resolution of options not supplied on the command line. This eliminates repetitive initialization logic while leaving each provider responsible for how and where it obtains its values. # VERSION This documentation refers to version 2.4.0. # FEATURES - accept command line arguments ala [Getopt::Long](https://metacpan.org/pod/Getopt%3A%3ALong) - 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 - selective role composition through YAML command manifests - 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](https://metacpan.org/pod/IO%3A%3APager) - customizable help sections via `help_sections` - pluggable option providers with ordered resolution and precedence # 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](https://metacpan.org/pod/Role%3A%3ATiny) 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. The manifest maps commands to roles: --- roles: 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. See ["-scaffold"](#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). # 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 `require`d or `use`d 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. `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 `use`d 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](https://metacpan.org/pod/CPAN%3A%3AMaker%3A%3ABootstrapper) ## 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](https://metacpan.org/pod/CPAN%3A%3AMaker%3A%3ABootstrapper) to create man page symlinks. ## 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. If you are building a CPAN distribution, [CPAN::Maker::Bootstrapper](https://metacpan.org/pod/CPAN%3A%3AMaker%3A%3ABootstrapper) 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](https://metacpan.org/pod/CPAN%3A%3AMaker%3A%3ABootstrapper) - 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. # ROLE-BASED ARCHITECTURE `CLI::Simple` 2.0.0 introduced an optional role-based architecture for applications that have grown beyond a single module. Commands may be implemented in dedicated [Role::Tiny](https://metacpan.org/pod/Role%3A%3ATiny) roles and declared in a YAML manifest, allowing `CLI::Simple` to build the dispatch table and provide an inherited `main()` - potentially reducing your main module to a single declaration. Version 2.3.0 adds selective role composition through the `roles:` manifest key. When `roles:` is used, only the role or roles required by the selected command are composed. The original `commands:` manifest form remains supported for backward compatibility and retains its original behavior of composing the complete set of command roles. ## The YAML Manifest By default, `CLI::Simple` locates the application's manifest via [File::ShareDir](https://metacpan.org/pod/File%3A%3AShareDir) using the distribution name derived from the module name. The manifest must be installed as part of the distribution. Additional YAML manifests may be supplied as arguments to `use CLI::Simple`. These may be explicit filenames or glob patterns, allowing applications to discover command registrations from project-local configuration files. _Note: Version 2.3.0 introduces `roles:` for selective role composition. The original `commands:` form remains supported for backward compatibility. See ["roles: vs commands:"](#roles-vs-commands)._ A minimal manifest: --- roles: frobnicate: My::Script::Role::Frobnicate list: My::Script::Role::List options: - help|h - verbose|v - output|o=s A manifest defining commands, options, defaults, and additional accessors: --- roles: frobnicate: My::Script::Role::Frobnicate list: My::Script::Role::List options: - help|h - verbose|v - output|o=s default_options: verbose: 0 extra_options: - dbh - config_data Manifest section names use underscores, not hyphens. For example, `default_options` and `extra_options` are valid section names; `default-options` and `extra-options` are not. Unknown manifest keys and values that do not match the expected section types cause an error when the manifest is loaded. ## Loading Multiple Manifests `CLI::Simple` supports loading multiple YAML manifests during import. This allows an application to extend its commands, roles, and options without modifying its primary manifest. Each additional manifest may contribute command definitions, option specifications, default values, and additional accessors. Additional manifests may be specified as filenames: use CLI::Simple qw( :roles extensions/publish.yml extensions/deploy.yml ); Glob patterns are also supported: use CLI::Simple qw( :roles .roles/*/role.yml ); When `:roles` is specified, `CLI::Simple` first attempts to load the application's installed manifest through [File::ShareDir](https://metacpan.org/pod/File%3A%3AShareDir). Additional manifests are then loaded in the order supplied. All manifests contribute `options`, `default_options`, and `extra_options` to the application's global option namespace. Duplicate option names (including aliases) and duplicate default keys are errors. Repeated `extra_options` declarations are harmless, and an accessor may appear in both `options` and `extra_options`. The first manifest alone supplies application-wide `alias` and `abbreviations` metadata. Subsequent manifests also contribute `roles:` and `commands:` entries. Command names must be unique across all manifests. An attempt to register a command more than once causes an error. An unmatched glob pattern contributes no manifests and does not cause an error. An explicitly named YAML file that cannot be loaded causes an error. Explicit YAML filenames may also be supplied without `:roles`: use CLI::Simple qw( config/application.yml .roles/*/role.yml ); In this case, the first explicitly supplied manifest provides the application-wide configuration. Loading a manifest does not compose its roles. For commands declared under `roles:`, only the roles associated with the selected command are composed during application startup. See ["roles: vs commands:"](#roles-vs-commands) for the distinction between selective and legacy role composition. ## Option Initialization Option specifications and defaults from all manifests are registered globally, but only the **roles** for the selected command are composed. Because option specifications and defaults are merged from all manifests, they are available regardless of which command is selected. Manifest defaults are equivalent to values supplied through the `default_options` argument to `new()`. Applications should use `init()` when option values require additional initialization or resolution after command-line parsing. Manifest defaults are applied before `init()`, but after command-line options have been parsed. Applications may inspect `options()` to determine which options were explicitly supplied on the command line and override default values during `init()`. ## Command Values Entries beneath `roles:` map a command name to either a single role or a list of roles required by that command. For example: roles: frobnicate: My::Script::Role::Frobnicate publish: - My::Script::Role::Publish - My::Script::Role::Packages When `frobnicate` is selected, only `My::Script::Role::Frobnicate` is composed into the application. When `publish` is selected, both `My::Script::Role::Publish` and `My::Script::Role::Packages` are composed. The selected role set must provide the command method corresponding to the command name. Hyphens are converted to underscores when resolving the method name, so: code-review resolves to: cmd_code_review The original `commands:` form remains supported for backward compatibility. Its values may be role class names or method names and retain the pre-2.3.0 behavior described in ["roles: vs commands:"](#roles-vs-commands). ## `roles:` vs `commands:` Version 2.3.0 introduces `roles:` for selective role composition. New applications should use `roles:`. The original `commands:` manifest key remains supported for backward compatibility. With `commands:`, selecting a command causes all roles referenced by the loaded manifests' command definitions to be composed into the application, regardless of which command is being executed. This makes methods from every composed role available throughout the application. However, it can also load modules that are not needed by the selected command, increasing startup time. For applications invoked repeatedly, such as utilities called from `make` recipes, this additional startup overhead can become significant. With `roles:`, only the roles associated with the selected command are composed. A command may require one role or several roles. Selective composition reduces unnecessary dependencies and makes the role requirements of each command explicit. ## When to Use Each Approach `CLI::Simple` supports several approaches to organizing command-line applications. The appropriate choice depends on the size of the application and how its commands share functionality. - Single-module application For small utilities with a limited number of commands, defining command handlers in a single module is often the simplest approach. Commands are registered directly through the `commands` constructor parameter. No YAML manifest or role composition is required. - Legacy `commands:` manifest Existing applications using a YAML manifest with `commands:` retain the original behavior of composing all command roles together. This approach may be appropriate for applications whose commands depend on methods supplied by other command roles. However, every role class referenced by the manifest's command definitions is composed regardless of which command is selected. - Selective `roles:` manifest For new or growing applications, `roles:` provides a more modular approach. Each command declares the roles it requires, and only those roles are composed when the command is selected. This is particularly useful when commands have different dependencies or when minimizing startup time is important. Applications can migrate from `commands:` to `roles:` incrementally, provided each migrated command declares the roles it requires. ## Sharing Methods Between Commands With legacy `commands:` manifests, all command roles are composed into the application. Methods provided by one command role are therefore available to other commands. With selective `roles:` composition, a command cannot assume that roles associated with other commands have been composed. Functionality shared by multiple commands can be placed in a separate role and included in each command's role list: roles: publish: - My::Script::Role::Publish - My::Script::Role::Common deploy: - My::Script::Role::Deploy - My::Script::Role::Common Alternatively, functionality required by every command can be composed directly into the main application class using [Role::Tiny::With](https://metacpan.org/pod/Role%3A%3ATiny%3A%3AWith). Shared functionality can also be implemented in ordinary Perl modules without using roles. See ["Roles With No Commands"](#roles-with-no-commands) for an example of composing an application-wide role. ## Roles With No Commands Some roles provide application-wide behavior rather than implementing a command. For example, a role may provide an `init()` method for startup validation or other functionality required by every command. Because these roles are not associated with a command beneath `roles:`, they must be composed explicitly into the 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; Roles composed this way are always available to the application, regardless of which command is selected. ## Activating Role-Based Architecture Role-based applications load their YAML manifests through `use CLI::Simple`. To load the application's installed manifest, specify `:roles`: use CLI::Simple qw(:roles); `CLI::Simple` locates the manifest through [File::ShareDir](https://metacpan.org/pod/File%3A%3AShareDir) and retains its command, role, option, alias, and abbreviation metadata for the application. Alternatively, specify an explicit YAML manifest: use CLI::Simple qw(config/application.yml); Both forms may be combined with additional YAML manifests or glob patterns. See ["Loading Multiple Manifests"](#loading-multiple-manifests). Role composition does not occur while manifests are being loaded. When the application starts, `CLI::Simple` resolves the selected command, including aliases and abbreviations, before composing the required roles. For commands declared beneath `roles:`, only the associated role set is composed. For legacy commands declared beneath `commands:`, the original all-role composition behavior is retained for backward compatibility. ## The Inherited main() When using a YAML manifest, your class can inherit `main()` from `CLI::Simple`: caller or exit __PACKAGE__->main; The inherited `main()` uses the manifest metadata to resolve the requested command, composes the role or roles required for that command, constructs the application object, and calls `run()`. Aliases and command abbreviations are resolved before selective role composition, so they select the same role set as the canonical command. Override `main()` in your subclass only if you need application startup behavior that cannot be expressed through the manifest, `init()`, or explicitly composed application roles. ## Distributing the Manifest When using `:roles`, the application's primary YAML manifest must be installed in the distribution's share directory. `CPAN::Maker` users can add it to `extra-files` in `buildspec.yml`: extra-files: - share: - my-script.yml Additional manifests supplied as import arguments are loaded from the filesystem. They do not need to be installed in the distribution's share directory. For example: use CLI::Simple qw( :roles .roles/*/role.yml ); This allows applications to maintain additional commands, roles, and options independently of their primary manifest. For example, an application may load optional extensions from `.roles/`. Each extension can provide its own manifest and implementation without modifying the application's primary manifest. Extensions are available only when their manifests are present and loaded. See ["Loading Multiple Manifests"](#loading-multiple-manifests) for manifest precedence and merging behavior. # PHILOSOPHY AND DESIGN PRINCIPLES Getting started with `CLI::Simple` is intentionally simple. It provides a structured, convention-based approach to building command-line applications, including option parsing, command dispatch, defaults, logging, and help handling, without requiring developers to adopt its optional role-based framework. For a single-module application, you register commands directly using the `commands` constructor parameter. Command handlers can be implemented as methods or subroutines, allowing you to organize application logic according to your own conventions. For applications that need greater modularity, `CLI::Simple` also provides an optional role-based framework. YAML manifests declare commands, options, and the roles required to implement them. The framework manages manifest loading and role composition, including selective composition of the roles required by a particular command. You can use `CLI::Simple` without adopting the role-based framework. The philosophy is: provide just enough infrastructure, then get out of your way. ## Validation, Defaults, and Configuration `CLI::Simple` does not impose a validation model. You may: - Use `Getopt::Long` option specifications for argument types and `default_options` to supply 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. ## 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](https://metacpan.org/pod/App%3A%3ACmd) or [CLI::Framework](https://metacpan.org/pod/CLI%3A%3AFramework). ## The init-run Lifecycle - **Phase 0: Manifest Loading** For role-based applications, YAML manifests are loaded during `import`. The application's installed manifest may be loaded through `:roles`, and additional manifests may be supplied as filenames or glob patterns. The manifests are merged and normalized before their command, role, option, alias, and abbreviation metadata is retained for the application. Roles are not composed during manifest loading. The selected command is resolved later, during application startup. For commands declared beneath `roles:`, only the role or roles required by that command are composed. Legacy `commands:` manifests retain the original all-role composition behavior. Single-module applications skip this phase entirely. - **Phase 1: 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"](#internal-commands). - **Phase 2: Initialization (`new`)** The constructor parses command-line arguments using [Getopt::Long](https://metacpan.org/pod/Getopt%3A%3ALong), normalizes option names, and creates accessors for the declared options. After completing command and logger initialization, `CLI::Simple` resolves any remaining options through registered option providers, applies application defaults, and invokes the application's `init()` method, if defined. The order of initialization is significant. Command-line values take precedence over provider-supplied values, providers are consulted in registration order, and defaults are applied only to options that remain unresolved. By the time `init()` is called, all available option values have been assigned to their respective accessors. The `init()` method remains available for application-specific initialization, but applications no longer need to implement their own option-source resolution and precedence logic when using option providers. See ["OPTION PROVIDERS"](#option-providers) for details. - Validating command-line arguments. - Loading configuration files based on a `--config` option. - Dynamically overriding the command (e.g., `$self->command('new_default')`). - Overriding default options - 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, define a `default` entry in the `commands` hash passed to the constructor, or set the command during `init()` using `command()`. Alternatively, enable `$AUTO_HELP` to display help when no command is supplied. - **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. In applications using a YAML manifest, a default command can be declared using `commands:`. For applications using selective `roles:` composition, the default command can be associated with a role directly: roles: default: My::Module::Role::Installer ## `$AUTO_HELP` and `$AUTO_DEFAULT` The following package variables control automatic command selection, help behavior, and output paging. By default, the `CLI::Simple` 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 when exactly one command is registered and no command is provided on the command line. default: false - `$PAGER` Set the package variable `$PAGER` to a true value to route help output through [IO::Pager](https://metacpan.org/pod/IO%3A%3APager) 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](https://metacpan.org/pod/IO%3A%3APager) 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::Constants` provides a collection of exportable constants commonly used in command-line applications. 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](https://metacpan.org/pod/Log%3A%3ALog4perl) To use them in your script: use CLI::Simple::Constants qw(:all); # ADDING USAGE TO YOUR SCRIPTS To provide built-in usage/help output, include a `=head1 SYNOPSIS` or `=head1 USAGE` 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](https://metacpan.org/pod/Pod%3A%3AUsage). 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. ## Customizing Help Output ### 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. ### `help_sections` By default, `CLI::Simple` renders the following POD sections when present: USAGE (or SYNOPSIS if USAGE is absent) DESCRIPTION/Commands DESCRIPTION/Options OPTIONS You can override the default selection by passing an array reference of section names during construction: my $cli = CLI::Simple->new( help_sections => [qw(SYNOPSIS COMMANDS OPTIONS)], ... ); When `help_sections` differs from the default selection, `CLI::Simple` honors the list exactly as provided. help_sections => [qw(SYNOPSIS USAGE)] Section names follow [Pod::Usage](https://metacpan.org/pod/Pod%3A%3AUsage) conventions. Subsections are specified with a `/` separator; for example, `DESCRIPTION/Commands` renders only the `Commands` subsection under `DESCRIPTION`. # INTERNAL COMMANDS `CLI::Simple` reserves command names beginning with `-` for its own use. These commands are intercepted before option parsing begins and execute immediately, bypassing the normal lifecycle entirely. See ["The init-run Lifecycle"](#the-init-run-lifecycle). ## -generate-completion Generates a bash completion script for the script's commands and options, derived from the live object state. Bash completions are a feature that allows the shell to automatically finish commands, file paths, and options when you press the Tab key. my-script -generate-completion > \ ~/.local/share/bash-completion/completions/my-script After generating the bash completion script, source it in your current shell to test: source ~/.local/share/bash-completion/completions/my-script Test by typing your script name followed by a space and pressing Tab. You should see the available commands. To verify option completion, type your script name followed by a space and `--` and press Tab. To make completions permanent, most systems automatically source files placed in `~/.local/share/bash-completion/completions/` when `bash-completion` 2.x is installed. If your system does not pick them up automatically, add the following to your `~/.bashrc`: source ~/.local/share/bash-completion/completions/my-script Alternatively, place the generated file in the system-wide completion directory (requires root): my-script -generate-completion > \ /etc/bash_completion.d/my-script The script name is taken from `MODULINO_WRAPPER` when set, otherwise it is inferred from the module name. _Note: If you created the modulino with the supplied `create-modulino` tool `MODULINO_WRAPPER` is already set inside the bash script that invokes the modulino._ - 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. Although the wrapper name differs from the module name, `MODULINO_WRAPPER` is set by the generated bash wrapper. my-alias -generate-completion ## -dump-spec 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 can be used immediately without moving any code. With `roles`, commands map to derived role class names suitable for use with `-scaffold`. Alias commands - those whose coderef resolves to a sub name that does not match the command key - are always written as sub names regardless of mode. ## -scaffold Generates a role-based project tarball from the running modulino or from an explicit spec file. 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 generated tarball is named from the distribution identifier derived from the application class or supplied specification file, with a `-roles.tar.gz` suffix. The name is used to infer the class name. 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. If you feed the tarball to [CPAN::Maker::Bootstrapper](https://metacpan.org/pod/CPAN%3A%3AMaker%3A%3ABootstrapper) via the `import-scaffold` command you can produce a complete buildable CPAN distribution. ## -migrate Combines `-dump-spec roles` and `-scaffold` in a single step. my-script -migrate Writes the YAML manifest then generates the role-based tarball. Use this when you are ready for a full migration and do not need to inspect or edit the manifest first. If you want to review or adjust the manifest before scaffolding, run `-dump-spec` and `-scaffold` separately. # METHODS AND SUBROUTINES ## use\_log4perl __PACKAGE__->use_log4perl( log_level => 'info', color => 1, ); Enables [Log::Log4perl](https://metacpan.org/pod/Log%3A%3ALog4perl) integration for the application class. `CLI::Simple` initializes logging during object construction, before invoking the application's `init()` method. See ["LOGGING"](#logging) for configuration and usage details. ## use\_options __PACKAGE__->use_options( provider_name => { 'Provider::Role' => [qw(option1 option2)], }, ); Registers one or more option providers with the application class. Each provider is identified by a name and implemented as a [Role::Tiny](https://metacpan.org/pod/Role%3A%3ATiny) role, which `CLI::Simple` composes into the application class. Providers are registered in the order supplied. This order determines their precedence when multiple providers can resolve the same option. The array reference specifies which application options the provider may resolve. An empty array reference allows the provider to resolve any eligible application option declared through `option_specs` or `extra_options`. The provider must implement a resolver method named `_provider_name_resolve()` and may optionally implement `_provider_name_init()`. Returns the application class. See ["OPTION PROVIDERS"](#option-providers) for the complete provider contract, examples, and resolution rules. ## new new( args ) Instantiates a new `CLI::Simple` instance, parses command-line options, creates the corresponding accessors, and optionally initializes logging. Registered option providers are then consulted, application defaults are applied, and the application's `init()` method is invoked, if defined. See ["OPTION PROVIDERS"](#option-providers) for details on option resolution and precedence. _Note: The `new()` constructor uses [Getopt::Long](https://metacpan.org/pod/Getopt%3A%3ALong)'s `GetOptions`, which directly modifies `@ARGV` by removing any recognized options. The remaining elements of `@ARGV` are treated as the command name and its arguments._ `args` is a hash or hash reference containing the following keys: - abbreviations A boolean that determines whether abbreviated command names are allowed. When true, `validate_command()` treats the provided command as a prefix and compares it to the keys in the command hash. This allows for convenient shorthand like: mytool disable-sched # expands to 'disable-scheduled-task' default: false - commands (required) A hash mapping command names to either a subroutine reference or an array reference. If an array reference is used, the first element must be a subroutine reference and the second should be a valid log level. (See ["Per Command Log Levels"](#per-command-log-levels).) Example: { send => \&send_message, receive => \&receive_message, list_messages => [ \&list_messages, 'error' ], } If your script does not use command names, you may set a `default` key to the subroutine or method to run: { default => \&main } If no default is provided, the behavior is controlled by the `$AUTO_DEFAULT` and `$AUTO_HELP` package variables. Setting `$AUTO_DEFAULT` to true when your `commands` hash contains only a single command, will cause that command to be run automatically when no command name is given on the command line. This allows you to treat the program like a single-command tool, where arguments can be passed directly without explicitly naming the command. - default\_options (optional) A hash reference providing default values for options. These values are applied only when the corresponding option has not been resolved by the command line or a registered option provider. - extra\_options (optional) An array reference of names for additional accessors you want to create, even if they are not part of `option_specs`. Example: extra_options => [ qw(foo bar baz) ] - option\_specs (optional) An array reference of option specifications, as accepted by [Getopt::Long](https://metacpan.org/pod/Getopt%3A%3ALong). These define the command-line options your program recognizes. - validate\_command By default, `CLI::Simple` validates the selected command against the registered commands. Set `validate_command` to a false value to disable this validation. Typically you might use this to allow a script to assume a default command and allow arguments. For example suppose you have a script `foo` with a command "get" with arguments: foo get something ...but want to allow users to also do: foo something To do this you should follow this recipe: sub init { my ($self) = @_; my $command = $self->command; if ( !exists $self->commands->{$command} ) { $self->command_args($command, $self->get_args); $self->command('get'); } return; } ## command command command(command) Gets or sets the command to execute. Usually this is the first argument on the command line after all options have been parsed. There are times when you might want to override the argument. You can pass a new command that will be executed when you call the `run()` method. ## command\_args my $args = $self->command_args(); Gets or sets the argument list. Similar to `get_args` when no arguments are passed except it returns an array reference. To replace the argument list, pass an array reference or a list. my $args = $self->command_args; $self->command_args(@{$args}, 'foo'); ## commands commands commands(command, handler) Returns the command dispatch hash supplied to the constructor. When called with a command name and handler, adds the command to the dispatch hash. `handler` must be a code reference. commands(foo => sub { return 'foo' }); ## main __PACKAGE__->main; For role-based applications, `main` is inherited from `CLI::Simple` and uses the manifest configuration assembled during `import`. It constructs the application with its options, defaults, and registered commands, then calls `run()`. In a role-based modulino, the entire `main` sub reduces to: caller or exit __PACKAGE__->main; For single-module applications, override `main` in your subclass as usual. ## options Returns a hash reference containing the command-line options parsed by `Getopt::Long`. Option names are normalized to snake\_case. The hash contains only options explicitly supplied on the command line. It does not include values supplied by `default_options` or subsequently initialized by `init()`. Applications can use this hash to distinguish command-line options from default values when resolving configuration. ## run Executes the selected command using the parsed options and arguments. The `run` method dispatches control to the corresponding command subroutine. Command subroutines should return `0` for success and a non-zero value for failure. `run()` returns the command subroutine's return value, which can be used as the script's exit status. ## get\_args Return 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 are assigned in order: the first name gets the first argument, the second name gets the second argument, and so on. If you only want specific positions, you may use `undef` as a placeholder: my %args = $self->get_args('message', undef, 'cc'); # skip argument 2 If there are fewer positional arguments than names, the remaining names are set to `undef`. Extra positional arguments (beyond the provided names) are ignored. ### With no names - In scalar context returns an array reference containing the command's positional arguments. - In list context returns a list containing the command's positional arguments. ## init If defined, `init()` is invoked during application initialization, after command-line options and arguments have been processed and before command dispatch. Use this method to perform application-specific initialization and validation. The `init()` method is invoked after command-line options have been parsed, registered option providers have resolved any remaining options, and application defaults have been applied. Consequently, all resolved option values are available through their corresponding accessors when `init()` executes. Applications may use `init()` to perform additional initialization, including validation of resolved values, establishing connections, or preparing resources needed by command handlers. See ["OPTION PROVIDERS"](#option-providers) for details on option resolution and precedence. # USING PACKAGE VARIABLES The `default_options`, `extra_options`, `option_specs`, and `commands` constructor arguments may also be defined using package variables. This provides a declarative alternative to passing these configuration values directly to `new()`. The corresponding package variables are `$DEFAULT_OPTIONS`, `$EXTRA_OPTIONS`, `$OPTION_SPECS`, and `$COMMANDS`. our $OPTION_SPECS = [ qw( help|h log-level=s|L debug|d ) ]; our $COMMANDS = { foo => \&foo, bar => \&bar, }; # COMMAND LINE OPTIONS Command-line options are defined using [Getopt::Long](https://metacpan.org/pod/Getopt%3A%3ALong)-style specifications. You pass these into the constructor via the `option_specs` parameter: my $cli = CLI::Simple->new( option_specs => [ qw( help|h foo-bar=s log-level=s ) ] ); Option values are accessible through automatically generated getter methods: $cli->get_foo_bar(); $cli->get_log_level(); Option names that contain dashes (`-`) are automatically converted to snake\_case for the accessor methods. For example: option_specs => [ 'foo-bar=s' ] ...results in: $cli->get_foo_bar(); ## Getopt::Long Configuration `CLI::Simple` uses [Getopt::Long](https://metacpan.org/pod/Getopt%3A%3ALong) to parse command-line options, with the `no_ignore_case` configuration enabled. Consequently: - Option names are case-sensitive. - Automatic option abbreviation is enabled. An option name may be abbreviated to any unambiguous prefix. - Multiple option names may be declared using Getopt::Long's `|` syntax, such as `config|c=s`. - When multiple spellings of the same option are supplied, the last occurrence determines its value. All other Getopt::Long configuration settings retain their defaults. # COMMAND ARGUMENTS If your commands accept positional arguments, you can retrieve them using the `get_args` method. You may optionally provide a list of argument names, in which case the arguments will be returned as a hash (or hashref in scalar context) with named values. Example: sub send_message { my ($self) = @_; my %args = $self->get_args(qw(phone_number message)); send_sms_message($args{phone_number}, $args{message}); } If you call `get_args()` without any argument names, it simply returns all remaining arguments as a list: my ($phone_number, $message) = $self->get_args; _Note: When called with names, `get_args` returns a hash in list context and a hash reference in scalar context._ ## set\_args Replaces the positional argument list with the supplied array reference. $self->set_args([qw(foo 1)]); This method overrides the positional arguments originally passed to the script. You can also modify individual arguments by retrieving the existing array reference using `get_args()` in scalar context: my $args = $self->get_args; $args->[1] = '2'; This avoids replacing the entire argument list. # CUSTOM ERROR HANDLER By default, `CLI::Simple` exits if `Getopt::Long::GetOptions` returns a false value, indicating an error while parsing options. - Set `$CLI::Simple::GETOPT_EXIT_ON_ERROR` to a false value. This disables 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 is called with the error message from `GetOptions`. It must return a boolean: a true value allows processing to continue, while a false value causes `CLI::Simple` to exit immediately. When an `error_handler` callback is supplied, its return value determines whether processing continues, regardless of the value of `$CLI::Simple::GETOPT_EXIT_ON_ERROR`. Without a callback, the global variable controls whether an option-parsing failure causes an immediate exit. # SETTING DEFAULT VALUES FOR OPTIONS To assign default values to your options, pass a hash reference as the `default_options` argument to the constructor. These values will be used unless explicitly overridden by the user on the command line. 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. Default values are applied after command-line arguments have been parsed and registered option providers have been consulted. A default is used only when the corresponding option remains unresolved. Consequently, values supplied by option providers take precedence over application defaults, just as command-line values take precedence over provider values. See ["OPTION PROVIDERS"](#option-providers) for details on option resolution and precedence. # OPTION PROVIDERS `CLI::Simple` supports pluggable option providers that resolve application options from sources other than the command line. Providers are implemented as [Role::Tiny](https://metacpan.org/pod/Role%3A%3ATiny) roles and registered using the `use_options()` class method. Option providers separate the source of an option's value from its use within the application. Regardless of whether an option was supplied on the command line, retrieved from a configuration file, or resolved by another provider, application code accesses it through the same `get_` and `set_` accessors. This makes the accessors the application's consistent interface to option values, eliminating the need for application code to know where those values originated or how precedence was established. An application can register one or more providers: __PACKAGE__->use_options( environment => { 'CLI::Simple::Role::Environment' => [], }, config => { 'MyApp::Role::Config' => [qw(host port database)], }, secrets_manager => { 'CLI::Simple::Role::SecretsManager' => [qw(database_password api_key)], }, ); Each registration consists of a provider name, a role implementing the provider, and an array reference identifying the application options the provider may resolve. Providers are consulted in registration order. When multiple providers declare the same option, the first provider to successfully resolve it takes precedence. Subsequent providers are asked to resolve only those options that remain unresolved. An empty option list indicates that the provider may resolve any unresolved application option declared through `option_specs` or `extra_options`. The provider name determines the methods `CLI::Simple` invokes. For example, a provider registered as `config` implements `_config_resolve()` and may optionally implement `_config_init()`. ## Implementing an Option Provider An option provider is a [Role::Tiny](https://metacpan.org/pod/Role%3A%3ATiny) role that implements a resolver method named ` _foo_config_resolve() `, where _name_ is the name used when registering the provider. For example, the following provider resolves options from a Perl hash: package Foo::Config; use strict; use warnings; use Role::Tiny; our %FOO_CONFIG = ( foo => 'bar', ); sub _foo_config_resolve { my ( $self, @names ) = @_; my %values; foreach my $name (@names) { next if !exists $FOO_CONFIG{$name}; $values{$name} = $FOO_CONFIG{$name}; } return \%values; } 1; The application registers the provider using `use_options()`: package Foo; use parent qw(CLI::Simple); __PACKAGE__->use_options( foo_config => { 'Foo::Config' => [qw(foo)], }, ); # In the application's main() method: my $cli = __PACKAGE__->new( option_specs => ['foo|f=s'], ); Once registered, `CLI::Simple` composes the role into the application class and invokes `_foo_config_resolve()` when `foo` has not been supplied on the command line. The resolved value is available through the normal `get_foo()` accessor. The resolver receives the names of the application options that remain unresolved and returns a hash reference containing the values it can supply. The returned keys must be the application option names, regardless of how the values are represented in the underlying source. For example, a provider might resolve the application option `foo` from an environment variable or configuration entry named `FOO_TASTIC`. Any such translation is the responsibility of the provider. An option is considered resolved when its name exists as a key in the returned hash reference. Values of `undef`, `0`, and the empty string are valid resolved values. Options omitted from the returned hash reference remain unresolved and may be supplied by subsequent providers or application defaults. A provider may also implement an optional initialization method named ` __name__init() `. `CLI::Simple` invokes this method before the provider's resolver, but only when the provider has eligible unresolved options to resolve. This allows providers to defer expensive operations, such as establishing database connections or initializing AWS clients, until they are actually needed. ## Option Resolution and Precedence CLI::Simple resolves option values during object construction, before invoking the application's `init()` method. Options are resolved in the following order: - 1. Command-line arguments Values supplied on the command line take precedence over all providers. Options already supplied on the command line are not passed to providers. - 2. Registered option providers Providers are consulted in registration order. Each provider receives only the eligible option names that remain unresolved. A provider resolves an option by including its name in the hash reference returned by its resolver. Options omitted from that hash reference remain eligible for subsequent providers. When a provider has no eligible unresolved options, neither its optional initializer nor its resolver is invoked. - 3. Application defaults Any remaining unresolved options may be initialized from the application's `default_options`. After resolution and default initialization are complete, `CLI::Simple` invokes the application's `init()` method, if defined. Consequently, application initialization and command handlers can access resolved option values through their normal accessors without needing to know which source supplied them. # ADDING ADDITIONAL ACCESSORS All command-line options are automatically available through getter methods named `get_*`. 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. This is useful 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. Unlike `option_specs`, `extra_options` does not register command-line options. It only creates accessors for values that can be initialized or modified programmatically. # LOGGING `CLI::Simple` integrates with [Log::Log4perl](https://metacpan.org/pod/Log%3A%3ALog4perl) to provide structured logging for your scripts. `CLI::Simple` provides convenient initialization of [Log::Log4perl](https://metacpan.org/pod/Log%3A%3ALog4perl) through `use_log4perl()`. __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. 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](https://metacpan.org/pod/Log%3A%3ALog4perl). **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._ ## 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](https://metacpan.org/pod/Term%3A%3AANSIColor). 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. 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. When `color!` is declared, `--color` and `--no-color` override the default color setting. These switches control `CLI::Simple`'s built-in colorized configuration; they do not add coloring to a custom Log4perl configuration. 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. 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 first two elements of the array reference are: - 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; Log levels are resolved in the following order of precedence: - 1. An explicit `--log-level` command-line option - 2. The log level associated with the selected command - 3. The application default supplied to `use_log4perl()` The effective level is available through `get_log_level()`. _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? You do not need to define multiple named commands. For an application that performs a single operation, register a default command: commands => { default => \&main } If no command is selected and no default command is registered, `run()` returns successfully without invoking a command handler. The application's `init()` method still executes during construction. - Must I subclass `CLI::Simple`? No. You can instantiate `CLI::Simple` directly and supply the `commands` and other configuration through the constructor. Subclassing is useful when you want to provide application-specific methods such as `init()`. - 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. - 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"](#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"](#role-based-architecture) for the manifest format. - How do I enable bash completion for my script? Run the built-in `-generate-completion` command. The script name is taken from `MODULINO_WRAPPER` when set, or derived from the module name otherwise. my-script -generate-completion > \ ~/.local/share/bash-completion/completions/my-script Wrappers generated by [CPAN::Maker::Bootstrapper](https://metacpan.org/pod/CPAN%3A%3AMaker%3A%3ABootstrapper) set `MODULINO_WRAPPER` automatically. - My application dies with "use\_log4perl() requires Log::Log4perl..." Since not all scripts require logging, `Log::Log4perl` is an _optional dependency_ of `CLI::Simple`. If your application calls `use_log4perl()`, `Log::Log4perl` must be installed. # ALIASING OPTIONS AND COMMANDS `CLI::Simple` lets you define short, human-friendly aliases for both option names and command names. 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 (for example `=s`, `:i`, `!`, `+`) and appends it to the alias. In the example above, `cfg` behaves as if you had written `cfg=s`, and `v` behaves as if you had written `v!`. - Accessors are created for both names Accessors are generated from all option names (canonical and aliases), with '-' normalized to '\_'. In the example, both `get_config()` and `get_cfg()` are available. - Values are mirrored after parsing After option parsing and normalization, values are mirrored so either name can be used consistently. When both the canonical option and an alias are supplied the canonical name wins. - No duplicate injection If the alias already exists in `option_specs`, it will not be injected again; value mirroring still occurs. - Errors are explicit If an alias points at a canonical option that does not exist, `CLI::Simple` croaks with a clear error. - Case sensitivity `Getopt::Long` is used with `:config no_ignore_case`, so option names (and therefore aliases) are case sensitive by default. ## 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. - 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. ## 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. If the user passes both `--config` and `--cfg`, the value from `--config` (the canonical version) is used. _Note: Applications using YAML manifests may define command aliases through the `alias:` section, just as applications using the `alias` constructor parameter do. Aliases resolve to their canonical commands before role composition. See ["ROLE-BASED ARCHITECTURE"](#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 The `run()` method dispatches the selected command and returns its exit status. Command handlers should return `0` for success or a non-zero value to indicate failure. exit CLI::Simple->new(commands => { foo => \&cmd_foo })->run(); ## Exit Codes `CLI::Simple` provides conventional success and failure exit codes for its own operations. Applications may return additional exit codes from their command handlers. - '0' Successful completion of a command (`SUCCESS`). - '1' General usage error, `--help` display via `pod2usage`, an invalid command line (`FAILURE`) or option parsing errors. - Any other code A command handler may return an application-specific numeric exit code, which `run()` passes through to the caller. A handler that calls `exit()` terminates the process directly. Some errors, including unknown commands, raise exceptions rather than returning an exit code. Unless caught by the application, these exceptions terminate the process with a non-zero status. # 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/](https://dev.perl.org/licenses/) for more information. # SEE ALSO [Getopt::Long](https://metacpan.org/pod/Getopt%3A%3ALong), [CLI::Simple::Constants](https://metacpan.org/pod/CLI%3A%3ASimple%3A%3AConstants), [CLI::Simple::Utils](https://metacpan.org/pod/CLI%3A%3ASimple%3A%3AUtils), [Pod::Usage](https://metacpan.org/pod/Pod%3A%3AUsage), [App::Cmd](https://metacpan.org/pod/App%3A%3ACmd), [CLI::Framework](https://metacpan.org/pod/CLI%3A%3AFramework), [Role::Tiny](https://metacpan.org/pod/Role%3A%3ATiny), [CPAN::Maker::Bootstrapper](https://metacpan.org/pod/CPAN%3A%3AMaker%3A%3ABootstrapper) # AUTHOR Rob Lauer - CLI-Simple-2.4.0/Makefile.PL0000644000175100017510000001216015270644577014716 0ustar rlauerrlaueruse strict; use warnings; use ExtUtils::MakeMaker; use File::ShareDir::Install; $File::ShareDir::Install::INCLUDE_DOTFILES = 1; if ( -d 'share' ) { install_share 'share'; } WriteMakefile( NAME => 'CLI::Simple', MIN_PERL_VERSION => '5.010', AUTHOR => 'Rob Lauer ', VERSION_FROM => 'lib/CLI/Simple.pm', ABSTRACT => 'Simple command line script accelerator', LICENSE => 'perl', PL_FILES => {} , EXE_FILES => [ 'bin/create-modulino', 'bin/cli-simple' ] , MAN1PODS => {} , PREREQ_PM => { 'Class::Accessor::Fast' => '0.51', 'File::ShareDir' => '1.118', 'File::Which' => '1.27', 'IO::Interactive' => '1.027', 'JSON' => '4.10', 'List::Util' => '1.33', 'Readonly' => '2.05', 'Role::Tiny' => '2.002005', 'YAML::Tiny' => '1.76' }, BUILD_REQUIRES => { 'ExtUtils::MakeMaker' => '6.64', 'File::ShareDir::Install' => 0 }, CONFIGURE_REQUIRES => { 'ExtUtils::MakeMaker' => '6.64', 'File::ShareDir::Install' => 0, }, TEST_REQUIRES => { 'Role::Tiny' => '2.002005', 'Test::Exit' => '0.11', 'Test::Output' => '1.036', 'YAML::Tiny' => '1.76' }, META_MERGE => { 'meta-spec'=>{ 'version'=>2 }, 'prereqs'=>{ 'runtime'=>{ 'suggests'=>{ 'IO::Pager'=>'2.10', 'Log::Log4perl'=>'1.58', 'Perl::Tidy'=>'20260204' } } }, 'provides'=>{ 'CLI::Simple'=>{ 'file'=>'lib/CLI/Simple.pm', 'version'=>'2.4.0' }, 'CLI::Simple::Constants'=>{ 'file'=>'lib/CLI/Simple/Constants.pm', 'version'=>'2.4.0' }, 'CLI::Simple::DumpSpec'=>{ 'file'=>'lib/CLI/Simple/DumpSpec.pm', 'version'=>'undef' }, 'CLI::Simple::Helpers'=>{ 'file'=>'lib/CLI/Simple/Helpers.pm', 'version'=>'undef' }, 'CLI::Simple::Migrate'=>{ 'file'=>'lib/CLI/Simple/Migrate.pm', 'version'=>'undef' }, 'CLI::Simple::Modulino'=>{ 'file'=>'lib/CLI/Simple/Modulino.pm', 'version'=>'undef' }, 'CLI::Simple::Scaffold'=>{ 'file'=>'lib/CLI/Simple/Scaffold.pm', 'version'=>'undef' }, 'CLI::Simple::Shell'=>{ 'file'=>'lib/CLI/Simple/Shell.pm', 'version'=>'undef' }, 'CLI::Simple::Utils'=>{ 'file'=>'lib/CLI/Simple/Utils.pm', 'version'=>'2.4.0' } }, 'resources'=>{ 'bugtracker'=>{ 'mailto'=>'rlauer6@comcast.net', 'web'=>'http://github.com/rlauer6/CLI-Simple/issues' }, 'homepage'=>'http://github.com/rlauer6/CLI-Simple', 'repository'=>{ 'type'=>'git', 'url'=>'git://github.com/rlauer6/CLI-Simple.git', 'web'=>'http://github.com/rlauer6/CLI-Simple' } } } ); package MY; use File::ShareDir::Install; use English qw(-no_match_vars); sub postamble { my $self = shift; my @ret = File::ShareDir::Install::postamble($self); my $postamble = join "\n", @ret; if ( -e 'postamble' ) { local $RS = undef; open my $fh, '<', 'postamble' or die "could not open postamble\n"; $postamble .= <$fh>; close $fh; } return $postamble; } 1; CLI-Simple-2.4.0/postamble0000644000175100017510000000032315270644577014653 0ustar rlauerrlauerinstall :: -$(NOECHO) ln -sf $(INSTALLMAN3DIR)/CLI::Simple::Shell.3pm $(DESTINSTALLMAN3DIR)/cli-simple.3pm; $(NOECHO) echo $(INSTALLMAN3DIR)/cli-simple.3pm >> $(DESTINSTALLSITEARCH)/auto/$(FULLEXT)/.packlist CLI-Simple-2.4.0/release-notes.md0000644000175100017510000001450615270644577016042 0ustar rlauerrlauer# CLI::Simple 2.4.0 Release Notes ## Overview Release 2.4.0 introduces pluggable option providers, allowing CLI applications to obtain option values from multiple sources without implementing repetitive retrieval and precedence logic in each application. Providers are implemented as reusable Role::Tiny roles and registered in precedence order. CLI::Simple coordinates resolution of options not supplied on the command line, allowing applications to access their options through the same generated accessors regardless of where the values originated. This release also migrates the PAUSE publishing workflow to a CPAN::Maker::Bootstrapper role. --- ## Option Providers The primary feature of this release is the new `use_options` class method and its companion `resolve_option_providers`, which together implement a named, ordered mechanism for supplying option values from external sources such as configuration files, credential stores, or environment lookups. ### Registering Providers: `use_options` Providers are registered at the class level before construction: ```perl MyApp->use_options( aws => { 'MyApp::Role::AWSConfig' => [qw(region bucket)] }, db => { 'MyApp::Role::DBConfig' => [qw(dsn username password)] }, ); ``` Each provider is identified by a short lowercase name and maps to a single `Role::Tiny` role class paired with the list of option names that role is expected to supply. `use_options` immediately applies the role to the calling class via `Role::Tiny->apply_roles_to_package`. The option list identifies the application options that a provider is eligible to resolve. An empty array reference (`[]`) indicates that the provider may resolve any application option declared through `option_specs` or `extra_options`. The provider is responsible for translating application option names into whatever naming convention its underlying source requires. For example, an application option named `foo` could be resolved from a configuration entry named `FOO_TASTIC`. Registration order is preserved and determines resolution precedence among providers. Multiple providers may register the same option. The first provider to resolve that option takes precedence, and subsequent providers are asked only for options that remain unresolved. `use_options` croaks on: - an odd number of arguments; - a provider name that does not match `[a-z][a-z0-9_]*`; - a duplicate provider name for the class; - a definition that does not map exactly one role to an option list; - an option list that is not an array reference. ### Resolution Protocol: `resolve_option_providers` `resolve_option_providers` is called automatically from `new()` after command-line parsing and before `init_defaults`. For each registered provider, in registration order: 1. Options already supplied on the command line or resolved by a prior provider are skipped. 2. If the provider's option list is empty, all eligible accessors not yet resolved are requested. 3. The optional hook `_${name}_init` is called if the class provides it. 4. `_${name}_resolve` is called with the list of unresolved option names it should supply. 5. The resolver must return a hash reference. Any key not in the requested list causes a croak. Returned values are applied via `set()` and marked resolved. An option is considered resolved when its name exists as a key in the hash reference returned by the provider. The value itself need not be defined. Values such as `undef`, `0`, and the empty string are valid resolved values. To leave an option unresolved, the provider must omit its key from the returned hash reference. Subsequent providers may then attempt to resolve it. Before invoking a provider's resolver, CLI::Simple determines which of its registered options remain unresolved. If no eligible options remain, the provider is skipped entirely. When unresolved options remain, CLI::Simple invokes the provider's optional initialization method before calling its resolver. This allows providers to defer expensive operations, such as opening database connections or initializing external service clients, until their values are actually needed. ### Precedence The resolution order is: 1. Command-line arguments (highest priority) 2. Provider results, in registration order 3. `default_options` from the constructor (lowest priority) This means a value explicitly supplied by the user on the command line cannot be overwritten by a provider, and a value supplied by an earlier-registered provider will not be overwritten by a later one or by `init_defaults`. ### `init_defaults` Change `init_defaults` now accepts a `$resolved` hash reference. It skips any option whose key appears in `$resolved`, preventing provider-supplied values from being replaced by constructor defaults. Existing applications that do not register option providers retain their previous default-option behavior. --- ## Documentation The POD section previously titled "Not a Framework" has been replaced with a description of the two supported usage paths: single-module applications and role-based applications. This better reflects the current capabilities of the module. --- ## Test Coverage Two new test files accompany this release: - `t/07-option-providers.t` — covers `use_options` registration: valid and invalid provider names, duplicate detection, odd-argument errors, invalid definition structures, and non-array option lists. - `t/08-option-provider-resolution.t` — covers `resolve_option_providers`: provider supplies values for unresolved options, command-line options take precedence, a provider returning an unrequested option croaks, an empty option list causes all eligible accessors to be offered, and the optional `_init` hook is called when present. --- ## Build and Release Workflow - `.includes/publish.mk` has been deleted and replaced by the role `.roles/CPAN-Maker-Bootstrapper-Role-PAUSEUpload/`, which provides `role.mk` and `role.yml`. PAUSE upload behavior is now delivered as a bootstrapper role rather than a standalone make fragment. --- ### Backward Compatibility Option providers are entirely opt-in. Applications that do not call `use_options()` retain their existing command-line parsing, accessor generation, and default-option behavior. Existing applications do not need to change their option specifications, command definitions, or initialization methods to use CLI::Simple 2.4.0. --- CLI-Simple-2.4.0/t/0000755000175100017510000000000015270644577013207 5ustar rlauerrlauerCLI-Simple-2.4.0/t/05-cli-simple-args.t0000644000175100017510000000217315270644577016611 0ustar rlauerrlauer#!/usr/bin/env perl use strict; use warnings; use Data::Dumper; use English qw(no_match_vars); use Test::More; use_ok('CLI::Simple'); use vars qw(@ARGV); my @options = qw( foo bar=s ); ######################################################################## subtest 'get_args' => sub { ######################################################################## local @ARGV = qw(foo bar biz buz); my $app = CLI::Simple->new( commands => { foo => sub { return 0 } }, option_specs => \@options ); my @args = $app->get_args(); # - get list of all args ok( 3 == @args, 'got three args' ); ok( 'barbizbuz' eq join( q{}, @args ), 'got bar biz buz' ); # -- get hash ref of keys my $args = $app->get_args(qw(bar biz buz)); ok( ref $args && keys %{$args} == 3, 'got hash ref' ); is_deeply( $args, { bar => 'bar', biz => 'biz', buz => 'buz' }, 'got hash values' ); # -- skip a key my %args = $app->get_args( 'bar', undef, 'buz' ); ok( 2 == keys %args, 'got two args' ); is_deeply( \%args, { bar => 'bar', buz => 'buz' }, 'got bar buz' ); }; done_testing; 1; __END__ 1; 1; CLI-Simple-2.4.0/t/02-cli-simple-logging.t0000644000175100017510000000172415270644577017301 0ustar rlauerrlauer#!/usr/bin/env perl use strict; use warnings; use Data::Dumper; use English qw(no_match_vars); use Test::More; use Test::Output; plan skip_all => 'no Log::Log4perl installed' if !eval { require Log::Log4perl; }; use_ok('CLI::Simple'); use vars qw(@ARGV); ######################################################################## sub foo { ######################################################################## print {*STDOUT} 'Hello World!'; return 0; } my @options = qw( foo bar=s ); ######################################################################## subtest 'logging' => sub { ######################################################################## CLI::Simple->use_log4perl( level => 'info' ); local @ARGV = qw(--foo --bar=buz foo); my $app = CLI::Simple->new( commands => { foo => \&foo }, option_specs => \@options ); stderr_like( sub { $app->get_logger->info('hello world') }, qr/hello\sworld/xsm ); }; done_testing; 1; __END__ 1; CLI-Simple-2.4.0/t/log-level-precedence.t0000644000175100017510000000277415270644577017367 0ustar rlauerrlauer#!/usr/bin/env perl use strict; use warnings; use English qw(-no_match_vars); use Test::More; use CLI::Simple; plan skip_all => 'no Log::Log4perl installed' if !eval { require Log::Log4perl; }; { package Local::LogLevelTest; use parent qw(CLI::Simple); __PACKAGE__->use_log4perl( level => 'error' ); } my $commands = { plain => sub { return 0 }, logged => [ sub { return 0 }, 'debug' ], }; subtest 'application default' => sub { local @ARGV = ('plain'); my $cli = Local::LogLevelTest->new( option_specs => ['log-level=s'], commands => $commands, ); is( $cli->get_log_level, 'error', 'application default selected' ); is( $cli->get_logger->level, $Log::Log4perl::ERROR, 'logger uses application default' ); }; subtest 'command-specific level' => sub { local @ARGV = ('logged'); my $cli = Local::LogLevelTest->new( option_specs => ['log-level=s'], commands => $commands, ); is( $cli->get_log_level, 'debug', 'command level overrides default' ); is( $cli->get_logger->level, $Log::Log4perl::DEBUG, 'logger uses command level' ); }; subtest 'explicit CLI level' => sub { local @ARGV = ( '--log-level', 'warn', 'logged' ); my $cli = Local::LogLevelTest->new( option_specs => ['log-level=s'], commands => $commands, ); is( $cli->get_log_level, 'warn', 'CLI level overrides command level' ); is( $cli->get_logger->level, $Log::Log4perl::WARN, 'logger uses CLI level' ); }; done_testing; CLI-Simple-2.4.0/t/cli-simple-manifest.t0000644000175100017510000002424715270644577017247 0ustar rlauerrlauer#!/usr/bin/env perl use strict; use warnings; use File::Temp qw(tempfile); use Test::More; use YAML::Tiny qw(DumpFile); use English qw(-no_match_vars); use_ok('CLI::Simple'); if ( !CLI::Simple->can('_load_manifest') ) { plan skip_all => 'CLI::Simple 2.0.0 manifest methods not yet implemented'; } ######################################################################## # Helpers ######################################################################## sub write_manifest { my (%manifest) = @_; my ( $fh, $path ) = tempfile( 'manifest-XXXX', SUFFIX => '.yml', UNLINK => 1 ); close $fh; DumpFile( $path, \%manifest ); return $path; } ######################################################################## # _load_manifest stores legacy command metadata without composing roles ######################################################################## { package CLI::Simple::Test::RoleA; use Role::Tiny; sub cmd_foo { return 'foo'; } sub cmd_bar { return 'bar'; } package CLI::Simple::Test::RoleB; use Role::Tiny; sub cmd_baz { return 'baz'; } } { my $yaml = write_manifest( options => [qw(help|h verbose!)], commands => { foo => 'CLI::Simple::Test::RoleA', bar => 'CLI::Simple::Test::RoleA', baz => 'CLI::Simple::Test::RoleB', }, ); { package CLI::Simple::Test::Consumer; use parent qw(CLI::Simple); } CLI::Simple->_load_manifest( 'CLI::Simple::Test::Consumer', $yaml ); my $manifest = CLI::Simple::Test::Consumer->_manifest; ok( $manifest, '_manifest returns stored manifest', ); is_deeply( $manifest->{_commands}, { foo => 'CLI::Simple::Test::RoleA', bar => 'CLI::Simple::Test::RoleA', baz => 'CLI::Simple::Test::RoleB', }, 'legacy command metadata preserved', ); ok( !CLI::Simple::Test::Consumer->can('cmd_foo'), '_load_manifest does not compose first legacy role', ); ok( !CLI::Simple::Test::Consumer->can('cmd_baz'), '_load_manifest does not compose second legacy role', ); } ######################################################################## # _load_manifest normalizes selective role metadata ######################################################################## { my $yaml = write_manifest( roles => { foo => 'CLI::Simple::Test::RoleA', baz => [ 'CLI::Simple::Test::RoleA', 'CLI::Simple::Test::RoleB', ], }, ); { package CLI::Simple::Test::Selective; use parent qw(CLI::Simple); } CLI::Simple->_load_manifest( 'CLI::Simple::Test::Selective', $yaml ); my $manifest = CLI::Simple::Test::Selective->_manifest; is_deeply( $manifest->{_roles}{foo}, ['CLI::Simple::Test::RoleA'], 'scalar selective role normalized to array', ); is_deeply( $manifest->{_roles}{baz}, [ 'CLI::Simple::Test::RoleA', 'CLI::Simple::Test::RoleB', ], 'selective role array preserved', ); ok( !CLI::Simple::Test::Selective->can('cmd_foo'), '_load_manifest does not compose selective roles', ); } ######################################################################## # command cannot be declared in both commands and roles ######################################################################## { my $yaml = write_manifest( commands => { foo => 'CLI::Simple::Test::RoleA', }, roles => { foo => 'CLI::Simple::Test::RoleA', }, ); { package CLI::Simple::Test::Duplicate; use parent qw(CLI::Simple); } my $err = do { local $@; eval { CLI::Simple->_load_manifest( 'CLI::Simple::Test::Duplicate', $yaml ); }; $@; }; like( $err, qr/command 'foo' is defined in both commands and roles/, 'command cannot appear in both commands and roles', ); } ######################################################################## # selective role specification must be scalar or array ######################################################################## { my $yaml = write_manifest( roles => { foo => { role => 'CLI::Simple::Test::RoleA', }, }, ); { package CLI::Simple::Test::InvalidRoles; use parent qw(CLI::Simple); } my $err = do { local $@; eval { CLI::Simple->_load_manifest( 'CLI::Simple::Test::InvalidRoles', $yaml ); }; $@; }; like( $err, qr/invalid roles specification for command 'foo'/, 'invalid selective role specification rejected', ); } ######################################################################## # selective role names must be valid class names ######################################################################## { my $yaml = write_manifest( roles => { foo => 'not a class name', }, ); { package CLI::Simple::Test::InvalidRoleName; use parent qw(CLI::Simple); } my $err = do { local $@; eval { CLI::Simple->_load_manifest( 'CLI::Simple::Test::InvalidRoleName', $yaml ); }; $@; }; like( $err, qr/invalid role 'not a class name' for command 'foo'/, 'invalid selective role class name rejected', ); } ######################################################################## # manifest values pass through unchanged ######################################################################## { my $yaml = write_manifest( options => [qw(help|h verbose! format=s)], default_options => { format => 'json' }, extra_options => [qw(content)], abbreviations => 1, alias => { commands => { f => 'foo', }, }, commands => { foo => 'CLI::Simple::Test::RoleA', }, ); { package CLI::Simple::Test::PassThrough; use parent qw(CLI::Simple); } CLI::Simple->_load_manifest( 'CLI::Simple::Test::PassThrough', $yaml ); my $manifest = CLI::Simple::Test::PassThrough->_manifest; is_deeply( $manifest->{default_options}, { format => 'json' }, 'manifest preserves default_options', ); is_deeply( $manifest->{extra_options}, [qw(content)], 'manifest preserves extra_options', ); is_deeply( $manifest->{options}, [qw(help|h verbose! format=s)], 'manifest preserves options', ); is( $manifest->{abbreviations}, 1, 'manifest preserves abbreviations', ); is_deeply( $manifest->{alias}, { commands => { f => 'foo', }, }, 'manifest preserves aliases', ); } ######################################################################## # classes without manifests are unaffected ######################################################################## { package CLI::Simple::Test::Legacy; use parent qw(CLI::Simple); sub cmd_legacy { return 'legacy'; } } { ok( !CLI::Simple::Test::Legacy->_manifest, 'class without manifest has no manifest metadata', ); ok( CLI::Simple::Test::Legacy->can('cmd_legacy'), 'class without manifest retains its own methods', ); } subtest 'additional YAML manifest supplied during import' => sub { require File::Temp; my ( $primary_fh, $primary_file ) = File::Temp::tempfile( SUFFIX => '.yml', UNLINK => 1 ); my ( $extension_fh, $extension_file ) = File::Temp::tempfile( SUFFIX => '.yml', UNLINK => 1 ); print {$primary_fh} <<'YAML'; roles: primary-command: Local::PrimaryRole YAML print {$extension_fh} <<'YAML'; roles: extension-command: Local::ExtensionRole YAML close $primary_fh; close $extension_fh; my $package = 'Local::ManifestImportTest'; my $code = sprintf q{ package %s; use parent qw(CLI::Simple); use CLI::Simple '%s', '%s'; 1; }, $package, $primary_file, $extension_file; my $loaded = eval $code; ok( $loaded, 'YAML manifests accepted as import arguments' ) or diag($EVAL_ERROR); if ($loaded) { my $manifest = $package->_manifest; ok( exists $manifest->{_roles}{'primary-command'}, 'primary command registered' ); ok( exists $manifest->{_roles}{'extension-command'}, 'extension command registered' ); ok( !$package->can('cmd_primary_command'), 'primary role not composed during import' ); ok( !$package->can('cmd_extension_command'), 'extension role not composed during import' ); } }; subtest 'YAML manifest glob expansion during import' => sub { require File::Temp; require File::Path; my $dir = File::Temp::tempdir( CLEANUP => 1 ); for my $spec ( [ 'Primary', 'primary-command', 'Local::PrimaryRole' ], [ 'Extension', 'extension-command', 'Local::ExtensionRole' ], ) { my ( $name, $command, $role ) = @{$spec}; my $path = "$dir/$name"; File::Path::make_path($path); open my $fh, '>', "$path/role.yml" or die "Cannot create $path/role.yml: $OS_ERROR"; print {$fh} "roles:\n $command: $role\n"; close $fh; } my $package = 'Local::ManifestGlobTest'; my $pattern = "$dir/*/role.yml"; my $code = sprintf q{ package %s; use parent qw(CLI::Simple); use CLI::Simple '%s'; 1; }, $package, $pattern; my $loaded = eval $code; ok( $loaded, 'glob manifest import succeeds' ) or diag($EVAL_ERROR); if ($loaded) { my $manifest = $package->_manifest; ok( exists $manifest->{_roles}{'primary-command'}, 'primary command registered' ); ok( exists $manifest->{_roles}{'extension-command'}, 'extension command registered' ); ok( !$package->can('cmd_primary_command'), 'primary role not composed during import' ); ok( !$package->can('cmd_extension_command'), 'extension role not composed during import' ); } }; subtest 'unmatched YAML manifest glob' => sub { require File::Temp; my $dir = File::Temp::tempdir( CLEANUP => 1 ); my $package = 'Local::ManifestUnmatchedGlobTest'; my $pattern = "$dir/*/role.yml"; my $code = sprintf q{ package %s; use parent qw(CLI::Simple); use CLI::Simple '%s'; 1; }, $package, $pattern; my $loaded = eval $code; ok( $loaded, 'unmatched glob does not cause an error' ) or diag($EVAL_ERROR); if ($loaded) { ok( !defined $package->_manifest, 'no manifest registered for unmatched glob' ); } }; subtest 'missing explicit YAML manifest' => sub { require File::Temp; my $dir = File::Temp::tempdir( CLEANUP => 1 ); my $package = 'Local::ManifestMissingFileTest'; my $missing = "$dir/missing.yml"; my $code = sprintf q{ package %s; use parent qw(CLI::Simple); use CLI::Simple '%s'; 1; }, $package, $missing; my $loaded = eval $code; ok( !$loaded, 'missing explicit manifest rejected' ); like( $EVAL_ERROR, qr/File '\Q$missing\E' does not exist/, 'error identifies missing manifest' ); }; done_testing; CLI-Simple-2.4.0/t/cli-simple-selective-roles.t0000644000175100017510000001641115270644577020540 0ustar rlauerrlauer#!/usr/bin/env perl use strict; use warnings; use File::Temp qw(tempfile); use Test::More; use CLI::Simple; ######################################################################## sub write_manifest { ######################################################################## my ($yaml) = @_; my ( $fh, $filename ) = tempfile(); print {$fh} $yaml; close $fh; return $filename; } ######################################################################## subtest 'single selective role' => sub { ######################################################################## { package Local::Role::Foo; use Role::Tiny; sub cmd_foo { return 'foo'; } } { package Local::Role::Bar; use Role::Tiny; sub cmd_bar { return 'bar'; } } { package Local::App::Single; use parent qw(CLI::Simple); } my $manifest = write_manifest(<<'YAML'); --- roles: foo: Local::Role::Foo bar: Local::Role::Bar YAML local @ARGV = qw(foo); CLI::Simple->_load_manifest( 'Local::App::Single', $manifest ); is( Local::App::Single->main, 'foo', 'selective role command runs' ); ok( Local::App::Single->can('cmd_foo'), 'selected role was composed' ); ok( !Local::App::Single->can('cmd_bar'), 'unselected role was not composed' ); return; }; ######################################################################## subtest 'multiple roles for one command' => sub { ######################################################################## { package Local::Role::Packages; use Role::Tiny; sub package_name { return 'Foo-Bar'; } } { package Local::Role::Publish; use Role::Tiny; sub cmd_publish { my ($self) = @_; return $self->package_name; } } { package Local::App::Multiple; use parent qw(CLI::Simple); } my $manifest = write_manifest(<<'YAML'); --- roles: publish: - Local::Role::Publish - Local::Role::Packages YAML local @ARGV = qw(publish); CLI::Simple->_load_manifest( 'Local::App::Multiple', $manifest ); is( Local::App::Multiple->main, 'Foo-Bar', 'all roles required by command are composed' ); ok( Local::App::Multiple->can('cmd_publish'), 'command role was composed' ); ok( Local::App::Multiple->can('package_name'), 'supporting role was composed' ); return; }; ######################################################################## subtest 'legacy command selection composes all legacy roles' => sub { ######################################################################## { package Local::Role::LegacyFoo; use Role::Tiny; sub cmd_foo { return 'foo'; } } { package Local::Role::LegacyBar; use Role::Tiny; sub cmd_bar { return 'bar'; } } { package Local::App::Legacy; use parent qw(CLI::Simple); } my $manifest = write_manifest(<<'YAML'); --- commands: foo: Local::Role::LegacyFoo bar: Local::Role::LegacyBar YAML local @ARGV = qw(foo); CLI::Simple->_load_manifest( 'Local::App::Legacy', $manifest ); ok( !Local::App::Legacy->can('cmd_foo'), 'legacy selected role not composed during manifest load', ); ok( !Local::App::Legacy->can('cmd_bar'), 'legacy unselected role not composed during manifest load', ); is( Local::App::Legacy->main, 'foo', 'legacy command still runs', ); ok( Local::App::Legacy->can('cmd_foo'), 'legacy selected role composed after selecting legacy command', ); ok( Local::App::Legacy->can('cmd_bar'), 'all legacy roles composed after selecting legacy command', ); return; }; ######################################################################## subtest 'abbreviation resolves before selective composition' => sub { ######################################################################## { package Local::Role::PublishLong; use Role::Tiny; sub cmd_publish_to_cpan { return 'published'; } } { package Local::App::Abbreviation; use parent qw(CLI::Simple); } my $manifest = write_manifest(<<'YAML'); --- roles: publish-to-cpan: Local::Role::PublishLong abbreviations: 1 YAML local @ARGV = qw(publish-to); CLI::Simple->_load_manifest( 'Local::App::Abbreviation', $manifest ); is( Local::App::Abbreviation->main, 'published', 'abbreviation resolves to selective command' ); ok( Local::App::Abbreviation->can('cmd_publish_to_cpan'), 'canonical command role was composed' ); return; }; ######################################################################## subtest 'command alias uses canonical selective command' => sub { ######################################################################## { package Local::Role::PublishAlias; use Role::Tiny; sub cmd_publish_to_cpan { return 'published'; } } { package Local::App::Alias; use parent qw(CLI::Simple); } my $manifest = write_manifest(<<'YAML'); --- roles: publish-to-cpan: Local::Role::PublishAlias alias: commands: pub: publish-to-cpan YAML local @ARGV = qw(pub); CLI::Simple->_load_manifest( 'Local::App::Alias', $manifest ); is( Local::App::Alias->main, 'published', 'alias dispatches to canonical selective command' ); ok( Local::App::Alias->can('cmd_publish_to_cpan'), 'canonical command method was composed' ); ok !Local::App::Alias->can('cmd_pub'), 'alias does not require a separate command method'; return; }; ######################################################################## subtest 'command cannot appear in commands and roles' => sub { ######################################################################## { package Local::App::Duplicate; use parent qw(CLI::Simple); } my $manifest = write_manifest(<<'YAML'); --- commands: foo: Local::Role::Foo roles: foo: Local::Role::Foo YAML eval { CLI::Simple->_load_manifest( 'Local::App::Duplicate', $manifest ); }; like( $@, qr/command 'foo' is defined in both commands and roles/, 'duplicate command definition rejected' ); return; }; ######################################################################## subtest 'invalid selective role specification' => sub { ######################################################################## { package Local::App::InvalidSpec; use parent qw(CLI::Simple); } my $manifest = write_manifest(<<'YAML'); --- roles: foo: role: Local::Role::Foo YAML eval { CLI::Simple->_load_manifest( 'Local::App::InvalidSpec', $manifest ); }; like( $@, qr/invalid roles specification for command 'foo'/, 'non-scalar and non-array role specification rejected' ); return; }; ######################################################################## subtest 'selected roles must implement command' => sub { ######################################################################## { package Local::Role::NoCommand; use Role::Tiny; sub something_else { return 'nope'; } } { package Local::App::MissingCommand; use parent qw(CLI::Simple); } my $manifest = write_manifest(<<'YAML'); --- roles: foo: Local::Role::NoCommand YAML local @ARGV = qw(foo); CLI::Simple->_load_manifest( 'Local::App::MissingCommand', $manifest ); eval { Local::App::MissingCommand->main; }; like( $@, qr/roles for command 'foo' do not implement cmd_foo/, 'selected role set must provide command method' ); return; }; done_testing; CLI-Simple-2.4.0/t/00-cli-simple-utils.t0000644000175100017510000000016715270644577017011 0ustar rlauerrlauer#!/usr/bin/env perl use strict; use warnings; use Test::More tests => 1; use_ok('CLI::Simple::Utils'); 1; __END__ CLI-Simple-2.4.0/t/00-cli-simple-constants.t0000644000175100017510000000017315270644577017662 0ustar rlauerrlauer#!/usr/bin/env perl use strict; use warnings; use Test::More tests => 1; use_ok('CLI::Simple::Constants'); 1; __END__ CLI-Simple-2.4.0/t/03-cli-simple-types.t0000644000175100017510000000221015270644577017007 0ustar rlauerrlauer#!/usr/bin/env perl use strict; use warnings; use Data::Dumper; use English qw(no_match_vars); use Test::More; use Test::Exit; use Test::Output; use_ok(qw(CLI::Simple)); ######################################################################## subtest 'type mismatch croaks' => sub { ######################################################################## local @ARGV = qw(--count foo go); stderr_like( sub { exits_ok { CLI::Simple->new( commands => { go => sub { } }, option_specs => ['count=i'], )->run, 1, 'exits on option error' } }, qr/invalid\sfor\soption\scount/xsmi ); }; ######################################################################## subtest 'multi options accumulate' => sub { ######################################################################## local @ARGV = qw(--tag a --tag b go); my $seen; CLI::Simple->new( commands => { go => sub { my ($self) = @_; $seen = $self->get_tag; } }, option_specs => ['tag=s@'], )->run; is_deeply $seen, [qw(a b)], 'tags accumulated'; }; done_testing; 1; CLI-Simple-2.4.0/t/07-option-providers.t0000644000175100017510000000716115270644577017150 0ustar rlauerrlauer#!/usr/bin/env perl use strict; use warnings; use Test::More; use CLI::Simple (); # Inline roles keep this test self-contained. Role::Tiny still requires # a module to be present in %INC before composing it. { package Local::Options::First; use Role::Tiny; sub _first_init { my ($self) = @_; return; } sub _first_resolve { my ( $self, @names ) = @_; return { map { $_ => qq(first:$_) } @names }; } } { package Local::Options::Second; use Role::Tiny; sub _second_resolve { my ( $self, @names ) = @_; return { map { $_ => qq(second:$_) } @names }; } } BEGIN { $INC{'Local/Options/First.pm'} = __FILE__; $INC{'Local/Options/Second.pm'} = __FILE__; } { package Local::CLI::Multiple; use parent qw(CLI::Simple); } { package Local::CLI::InvalidName; use parent qw(CLI::Simple); } { package Local::CLI::InvalidDefinition; use parent qw(CLI::Simple); } { package Local::CLI::InvalidOptions; use parent qw(CLI::Simple); } subtest 'multiple providers compose into the application class' => sub { my $class = 'Local::CLI::Multiple'; is( $class->use_options( first => { 'Local::Options::First' => [qw(rabbit_password rabbit_username)], }, second => { 'Local::Options::Second' => [qw(rabbit_password api_key)], }, ), $class, 'registration returns the application class', ); ok( $class->can('_first_init'), 'optional first-provider initializer composed' ); ok( $class->can('_first_resolve'), 'first-provider resolver composed' ); ok( $class->can('_second_resolve'), 'second-provider resolver composed' ); my $object = bless {}, $class; is_deeply( $object->_first_resolve(qw(rabbit_password rabbit_username)), { rabbit_password => 'first:rabbit_password', rabbit_username => 'first:rabbit_username', }, 'composed role method can be called on an application instance', ); # Overlap is intentional and must not be rejected at registration. # Provider invocation order and unresolved-name filtering will be # tested separately when the orchestrator is implemented. }; subtest 'registration rejects duplicate provider names' => sub { my $class = 'Local::CLI::Multiple'; my $ok = eval { $class->use_options( first => { 'Local::Options::First' => [qw(rabbit_password)] }, ); 1; }; ok( !$ok, 'duplicate name rejected' ); like( $@, qr/Duplicate option provider: first/, 'useful diagnostic' ); }; subtest 'registration validates its input' => sub { my $ok = eval { Local::CLI::InvalidName->use_options( 'invalid-name' => { 'Local::Options::First' => [] }, ); 1; }; ok( !$ok, 'invalid provider name rejected' ); like( $@, qr/Invalid provider name/, 'invalid name diagnostic' ); $ok = eval { Local::CLI::InvalidDefinition->use_options( mixed => { 'Local::Options::First' => [], 'Local::Options::Second' => [], }, ); 1; }; ok( !$ok, 'multiple roles in one provider definition rejected' ); like( $@, qr/Invalid provider definition/, 'invalid definition diagnostic' ); $ok = eval { Local::CLI::InvalidOptions->use_options( first => { 'Local::Options::First' => 'rabbit_password' }, ); 1; }; ok( !$ok, 'non-array option specification rejected' ); like( $@, qr/Invalid option list/, 'invalid option list diagnostic' ); }; subtest 'odd argument count is rejected' => sub { my $ok = eval { Local::CLI::InvalidName->use_options(orphan => ()); 1; }; ok( !$ok, 'odd number of arguments rejected' ); like( $@, qr/Odd number of arguments/, 'odd argument diagnostic' ); }; done_testing; 1; CLI-Simple-2.4.0/t/01-cli-simple.t0000644000175100017510000001260115270644577015650 0ustar rlauerrlauer#!/usr/bin/env perl use strict; use warnings; use Data::Dumper; use English qw(no_match_vars); use Test::More; use Test::Exit; use Test::Output; use_ok('CLI::Simple'); use vars qw(@ARGV); ######################################################################## sub foo { ######################################################################## print {*STDOUT} 'Hello World!'; return 0; } my @options = qw( foo bar=s ); ######################################################################## subtest 'happy path' => sub { ######################################################################## local @ARGV = qw(--foo --bar=buz foo); my $app = CLI::Simple->new( commands => { foo => \&foo }, option_specs => \@options ); ok( $app->get_foo, 'foo set' ); ok( $app->get_bar eq 'buz', 'bar set' ); }; ######################################################################## subtest 'bad option' => sub { ######################################################################## local @ARGV = '--bad-option foo'; stderr_like( sub { exits_ok { CLI::Simple->new( commands => { foo => \&foo }, option_specs => \@options ), 1, 'called exit' } }, qr/bad-option/xsmi ); }; ######################################################################## subtest 'option alias' => sub { ######################################################################## local @ARGV = qw(--foo --bar=buz foo); my $app = CLI::Simple->new( commands => { foo => \&foo }, alias => { options => { biz => 'bar' } }, option_specs => \@options ); ok( $app->get_foo, 'foo set' ); ok( $app->get_bar eq 'buz', 'bar set' ); ok( $app->get_biz eq 'buz', 'biz set' ); local @ARGV = qw(--foo --biz=buz foo); $app = CLI::Simple->new( commands => { foo => \&foo }, alias => { options => { biz => 'bar' } }, option_specs => \@options ); ok( $app->get_foo, 'foo set' ); ok( $app->get_bar eq 'buz', 'bar set' ); ok( $app->get_biz eq 'buz', 'biz set' ); }; ######################################################################## subtest 'run' => sub { ######################################################################## local @ARGV = qw(--foo --bar=buz foo); my $app = CLI::Simple->new( commands => { foo => \&foo }, alias => { options => { biz => 'bar' } }, option_specs => \@options ); stdout_is( sub { $app->run() }, 'Hello World!' ); }; ######################################################################## subtest 'alias precedence and symmetry' => sub { ######################################################################## local @ARGV = qw(--bar=2 --biz=9 go); # biz is alias for bar my $got; my $app = CLI::Simple->new( commands => { go => sub { $got = \%ENV } }, # or capture parsed opts via a hook alias => { options => { biz => 'bar' } }, option_specs => ['bar=i'], ); # however you surface parsed options, assert both entries reflect last value is( $app->get_bar, 2, 'canonical reflects first' ); is( $app->get_biz, 2, 'alias mirrors canonical' ); }; ######################################################################## subtest 'command alias' => sub { ######################################################################## local @ARGV = qw(--foo --bar=buz fiz); my $app = CLI::Simple->new( commands => { foo => \&foo }, alias => { options => { biz => 'bar' }, commands => { fiz => 'foo' } }, option_specs => \@options ); stdout_is( sub { $app->run() }, 'Hello World!' ); }; ######################################################################## subtest 'command abbreviations' => sub { ######################################################################## local @ARGV = qw(--foo --bar=buz fuzz); my $app = CLI::Simple->new( commands => { fuzzball => \&foo }, alias => { options => { biz => 'bar' } }, option_specs => \@options, abbreviations => 1, ); stdout_is( sub { $app->run() }, 'Hello World!' ); local @ARGV = qw(--foo --bar=buz fuzz); eval { CLI::Simple->new( commands => { fuzzball => \&foo, buzzball => sub { return 0; }, }, alias => { options => { biz => 'bar' } }, option_specs => \@options )->run(); }; my $err = $EVAL_ERROR // q{}; like( $err, qr/unknown\s+command/xsmi, 'bad command' ); }; ######################################################################## subtest 'ambiguous abbrev croaks' => sub { ######################################################################## local @ARGV = qw(run); # both runit and runner exist eval { CLI::Simple->new( commands => { runit => sub { }, runner => sub { } }, abbreviations => 1, option_specs => [], )->run; }; my $err = $EVAL_ERROR; like $err, qr/\bambiguous\b/ixsm, 'croaks on ambiguous command'; }; ######################################################################## subtest 'unknown option' => sub { ######################################################################## eval { CLI::Simple->new( commands => { runit => sub { }, runner => sub { } }, abbreviations => 1, option_specs => [], foo => 'bar', )->run; }; my $err = $EVAL_ERROR; like $err, qr/\bunknown\soption\b/ixsm, 'croaks on unknow option'; }; done_testing; 1; __END__ 1; CLI-Simple-2.4.0/t/04-cli-simple-help.t0000644000175100017510000000600115270644577016576 0ustar rlauerrlauer#!/usr/bin/env perl # Test::Exit must be compiled before other code that calls exit use English qw(-no_match_vars); use Test::Exit; use Test::More; use lib 't/lib'; use CLISimpleHelpBoth; use CLISimpleHelpSynopsis; use CLISimpleHelpUsage; BEGIN { use_ok( qw(CLI::Simple), qw($AUTO_HELP $PAGER) ); } $PAGER = 0; ######################################################################## sub capture_usage { ######################################################################## my ( $cli, $module ) = @_; my $output = q{}; open my $stdout, '>', \$output or die "Could not open scalar filehandle: $OS_ERROR"; $cli->set__program( $INC{$module} ); { local *STDOUT = $stdout; exits_ok { $cli->usage(); } 'usage exits'; } close $stdout or die "Could not close scalar filehandle: $OS_ERROR"; return $output; } ######################################################################## subtest 'USAGE is used for default help' => sub { ######################################################################## local @ARGV; my $cli = CLISimpleHelpUsage->new( commands => { foo => \&CLISimpleHelpUsage::cmd_foo }, option_specs => [], ); my $output = capture_usage( $cli, 'CLISimpleHelpUsage.pm' ); like( $output, qr/LEGACY USAGE TEXT/sm, 'USAGE content displayed', ); return; }; ######################################################################## subtest 'SYNOPSIS is used for default help' => sub { ######################################################################## local @ARGV; my $cli = CLISimpleHelpSynopsis->new( commands => { foo => \&CLISimpleHelpSynopsis::cmd_foo }, option_specs => [], ); my $output = capture_usage( $cli, 'CLISimpleHelpSynopsis.pm' ); like( $output, qr/SYNOPSIS TEXT/sm, 'SYNOPSIS content displayed', ); return; }; ######################################################################## subtest 'USAGE takes precedence over SYNOPSIS' => sub { ######################################################################## local @ARGV; my $cli = CLISimpleHelpBoth->new( commands => { foo => \&CLISimpleHelpBoth::cmd_foo }, option_specs => [], ); my $output = capture_usage( $cli, 'CLISimpleHelpBoth.pm' ); like( $output, qr/LEGACY USAGE TEXT/sm, 'USAGE content displayed', ); unlike( $output, qr/SYNOPSIS TEXT/sm, 'SYNOPSIS content suppressed', ); return; }; ######################################################################## subtest 'explicit help_sections are honored' => sub { ######################################################################## local @ARGV; my $cli = CLISimpleHelpBoth->new( commands => { foo => \&CLISimpleHelpBoth::cmd_foo }, option_specs => [], ); $cli->set_help_sections( [qw(SYNOPSIS USAGE)] ); my $output = capture_usage( $cli, 'CLISimpleHelpBoth.pm' ); like( $output, qr/SYNOPSIS TEXT/sm, 'SYNOPSIS content displayed', ); like( $output, qr/LEGACY USAGE TEXT/sm, 'explicit USAGE content displayed', ); return; }; done_testing; 1; CLI-Simple-2.4.0/t/00-cli-simple.t0000644000175100017510000000016015270644577015644 0ustar rlauerrlauer#!/usr/bin/env perl use strict; use warnings; use Test::More tests => 1; use_ok('CLI::Simple'); 1; __END__ CLI-Simple-2.4.0/t/06-cli-simple-default.t0000644000175100017510000000323315270644577017300 0ustar rlauerrlauer#!/usr/bin/env perl use strict; use warnings; use Data::Dumper; use English qw(no_match_vars); use Test::More; use Test::Output; use Test::Exit; BEGIN { use_ok( qw(CLI::Simple), qw($AUTO_DEFAULT $AUTO_HELP) ); } use vars qw(@ARGV); my @options = qw( foo bar=s ); ######################################################################## subtest 'one command' => sub { ######################################################################## local @ARGV = qw(); $AUTO_DEFAULT = 1; my $app = CLI::Simple->new( commands => { foo => sub { print "Hello World\n"; return 0; } } ); stdout_like( sub { $app->run(); }, qr/hello/xsmi, 'defaults to only command' ); }; ######################################################################## subtest 'one command w/args' => sub { ######################################################################## local @ARGV = qw(bar biz); $AUTO_DEFAULT = 1; my $app = CLI::Simple->new( commands => { foo => sub { print join q{,}, $_[0]->get_args; return 0; } } ); stdout_like( sub { $app->run(); }, qr/bar,biz/xsmi, 'defaults to only command' ); }; ######################################################################## subtest 'AUTO_HELP' => sub { ######################################################################## local @ARGV = qw(); $AUTO_HELP = 1; stdout_like( sub { exits_ok( sub { CLI::Simple->new( commands => { bar => sub { return 0; }, foo => sub { print "Hello World\n"; return 0; } } ); } ); }, qr/usage/xsmi ); }; done_testing; 1; __END__ =pod =head1 USAGE blah blah =cut CLI-Simple-2.4.0/t/08-option-provider-resolution.t0000644000175100017510000000450515270644577021166 0ustar rlauerrlauer#!/usr/bin/env perl use strict; use warnings; use Test::More; use CLI::Simple (); our @CALLS; { package Local::Boundaries::First; use Role::Tiny; sub _first_init { push @main::CALLS, q{first:init}; return; } sub _first_resolve { my ( $self, @names ) = @_; push @main::CALLS, q{first:} . join q{,}, sort @names; return { map { $_ => q{provider} } @names }; } } { package Local::Boundaries::Bad; use Role::Tiny; sub _bad_resolve { my ( $self, @names ) = @_; return { unexpected => q{value} }; } } BEGIN { $INC{'Local/Boundaries/First.pm'} = __FILE__; $INC{'Local/Boundaries/Bad.pm'} = __FILE__; } { package Local::Boundaries::AllCLI; use parent qw(CLI::Simple); } { package Local::Boundaries::Unrequested; use parent qw(CLI::Simple); } { package Local::Boundaries::Isolated; use parent qw(CLI::Simple); } Local::Boundaries::AllCLI->use_options( first => { 'Local::Boundaries::First' => [qw(token)] }, ); Local::Boundaries::Unrequested->use_options( bad => { 'Local::Boundaries::Bad' => [qw(token)] }, ); subtest 'skip initializer and resolver when CLI supplied every requested option' => sub { @CALLS = (); local @ARGV = qw(--token cli-value); my $app = Local::Boundaries::AllCLI->new( option_specs => [qw(token=s)], commands => { noop => sub { return 0; } }, ); is( $app->get_token, q{cli-value}, q{CLI value retained} ); is_deeply( \@CALLS, [], q{provider was never initialized or invoked} ); }; subtest 'reject provider output outside requested names' => sub { local @ARGV = (); my $ok = eval { Local::Boundaries::Unrequested->new( option_specs => [qw(token=s)], commands => { noop => sub { return 0; } }, ); 1; }; ok( !$ok, q{constructor rejects unrequested name} ); like( $@, qr/returned unrequested option 'unexpected'/, q{useful diagnostic} ); }; subtest 'registration belongs to its application class' => sub { @CALLS = (); local @ARGV = (); my $app = Local::Boundaries::Isolated->new( option_specs => [qw(token=s)], default_options => { token => q{fallback} }, commands => { noop => sub { return 0; } }, ); is( $app->get_token, q{fallback}, q{unregistered class receives its default} ); is_deeply( \@CALLS, [], q{another application's provider is not invoked} ); }; done_testing; 1; CLI-Simple-2.4.0/t/lib/0000755000175100017510000000000015270644577013755 5ustar rlauerrlauerCLI-Simple-2.4.0/t/lib/CLISimpleHelpSynopsis.pm0000644000175100017510000000060315270644577020454 0ustar rlauerrlauerpackage CLISimpleHelpSynopsis; use strict; use warnings; use parent qw(CLI::Simple); ######################################################################## sub cmd_foo { ######################################################################## return 0; } 1; __END__ =pod =head1 NAME CLISimpleHelpSynopsis - CLI::Simple help test fixture =head1 SYNOPSIS SYNOPSIS TEXT =cut CLI-Simple-2.4.0/t/lib/CLISimpleHelpUsage.pm0000644000175100017510000000057615270644577017702 0ustar rlauerrlauerpackage CLISimpleHelpUsage; use strict; use warnings; use parent qw(CLI::Simple); ######################################################################## sub cmd_foo { ######################################################################## return 0; } 1; __END__ =pod =head1 NAME CLISimpleHelpUsage - CLI::Simple help test fixture =head1 USAGE LEGACY USAGE TEXT =cut CLI-Simple-2.4.0/t/lib/CLISimpleHelpBoth.pm0000644000175100017510000000063415270644577017525 0ustar rlauerrlauerpackage CLISimpleHelpBoth; use strict; use warnings; use parent qw(CLI::Simple); ######################################################################## sub cmd_foo { ######################################################################## return 0; } 1; __END__ =pod =head1 NAME CLISimpleHelpBoth - CLI::Simple help test fixture =head1 SYNOPSIS SYNOPSIS TEXT =head1 USAGE LEGACY USAGE TEXT =cut CLI-Simple-2.4.0/MANIFEST0000644000175100017510000000170715270644577014102 0ustar rlauerrlauerbin/cli-simple bin/create-modulino ChangeLog lib/CLI/Simple.pm lib/CLI/Simple/Constants.pm lib/CLI/Simple/DumpSpec.pm lib/CLI/Simple/Helpers.pm lib/CLI/Simple/Migrate.pm lib/CLI/Simple/Modulino.pm lib/CLI/Simple/Scaffold.pm lib/CLI/Simple/Shell.pm lib/CLI/Simple/Utils.pm Makefile.PL MANIFEST This list of files MANIFEST.SKIP postamble README.md release-notes.md t/00-cli-simple-constants.t t/00-cli-simple-utils.t t/00-cli-simple.t t/01-cli-simple.t t/02-cli-simple-logging.t t/03-cli-simple-types.t t/04-cli-simple-help.t t/05-cli-simple-args.t t/06-cli-simple-default.t t/07-option-providers.t t/08-option-provider-resolution.t t/cli-simple-manifest.t t/cli-simple-selective-roles.t t/lib/CLISimpleHelpBoth.pm t/lib/CLISimpleHelpSynopsis.pm t/lib/CLISimpleHelpUsage.pm t/log-level-precedence.t META.yml Module YAML meta-data (added by MakeMaker) META.json Module JSON meta-data (added by MakeMaker) CLI-Simple-2.4.0/ChangeLog0000644000175100017510000011224715270644577014525 0ustar rlauerrlauerThu Oct 29 08:21:10 2026 Rob Lauer [2.4.0]: * VERSION: bump * README.md: generated * lib/CLI/Simple.pm.in - replace "Not a Framework" POD section with description of single-module and role-based usage paths (use_options): new - register named option providers keyed by name => { role => \@options } - applies role via Role::Tiny::apply_roles_to_package - records registration order in %OPTION_PROVIDER_ORDER and provider details in %OPTION_PROVIDERS - croaks on odd args, invalid name, duplicate name, or non-array option list (resolve_option_providers): new - called from new() before init_defaults() - iterates registered providers in registration order - skips options already resolved from command-line or a prior provider - calls optional _${name}_init then _${name}_resolve - validates resolver returns a HASH ref containing only requested option names - sets resolved options via set() and marks them resolved (init_defaults) - accept $resolved hashref parameter - skip options already present in $resolved before checking defined value, preventing provider-supplied values from being overwritten by defaults (new) - call resolve_option_providers() with a map of command-line-supplied options and the full accessor list before calling init_defaults() - pass resolved map to init_defaults() * t/07-option-providers.t: new - test use_options registration: valid and invalid provider names, duplicate provider detection, odd-argument error, invalid definition structure, non-array option list * t/08-option-provider-resolution.t: new - test resolve_option_providers(): provider supplies values for unresolved options - command-line options take precedence over provider - provider returning unrequested option croaks - empty option list causes provider to resolve all eligible accessors - optional _init hook is called when present * .includes/publish.mk: deleted * .roles/CPAN-Maker-Bootstrapper-Role-PAUSEUpload/role.mk: new * .roles/CPAN-Maker-Bootstrapper-Role-PAUSEUpload/role.yml: new Thu Oct 15 15:18:45 2026 Rob Lauer [2.3.2]: * VERSION: bump * README.md: generated * lib/CLI/Simple.pm.in - POD updates (main) - allow multple instances of CLI::Simple enable logging separately (import) - accept explicit YAML filenames and glob patterns as import arguments - expand glob patterns before manifest loading - prepend File::ShareDir path to yaml_files list when :roles specified - dispatch to _load_manifest for any collected yaml_files (_validate_manifest): new - validate manifest is a HASH, reject unknown keys, enforce section types - reject non-scalar values in options and extra_options sections (_load_manifest) - accept multiple yaml_files; merge commands, roles, options, default_options, extra_options across all manifests - enforce uniqueness of command names, option names (including aliases), and default_option keys across manifests - allow repeated extra_options declarations without error - restrict alias and abbreviations metadata to primary manifest only - call _validate_manifest per file before merging (_normalize_manifest): extracted from _load_manifest; performs role normalization and selective_roles construction (init_options): new no-op hook called after init_logger and before init() (new) - call init_defaults() after init() to allow init() to add defaults (init_logger) - resolve log level as: --log-level > command-level > log4perl default - set log level unconditionally after logger is available - remove early-return path that skipped level resolution when command lacked an ARRAY handler (usage) - use MODULINO_MODULE_NAME env var to locate POD input when wrapper differs from module name * bin/create-modulino.in: aligned with .includes/modulino.mk * lib/CLI/Simple/Modulino.pm.in: likewise * t/cli-simple-manifest.t - add English qw(-no_match_vars) - test loading multiple explicit YAML manifests via import arguments - test glob pattern expansion during import - test unmatched glob produces no error and no manifest - test missing explicit YAML file produces an error identifying the path * t/log-level-precedence.t: new - test log level precedence: --log-level > command level > use_log4perl default * .includes/add-plugin.mk: new * .includes/release-notes.mk: deleted * .includes/test.mk - replace GSOURCE_FILES with SOURCE_FILES in test, test-all, and check targets * .includes/update.mk - add INCLUDES_DIR and BOOTSTRAPPER_DIST_DIR variable definitions - add pre-update phony target with extensible double-colon rule - make post-update depend on pre-update with double-colon rule - move Makefile copy and chmod into post-update - call pre-update explicitly from update before copying managed files * Makefile - add add-plugin.mk to MANAGED_MK_FILES; remove publish.mk and release-notes.mk - include $(wildcard .roles/*/role.mk) for project-registered command role extensions - make local/.installed order-only prerequisite for tarball conditional on SKIP_TESTS - add DEPENDENCY_FILES, MISSING_DEPENDENCY_FILES, and force-reconcile target - trigger reconciliation when any dependency file is absent - check for missing dependency file in reconcile condition alongside .reconciled sentinel - remove --log-level info from test-requires.scan scandeps invocation * .roles/CPAN-Maker-Bootstrapper-Role-LLM/role.mk: new * .roles/CPAN-Maker-Bootstrapper-Role-LLM/role.yml: new Fri Oct 9 16:49:25 2026 Rob Lauer [2.3.1]: * README.md: generated * VERSION: bump * lib/CLI/Simple.pm.in: POD update Fri Oct 9 07:46:59 2026 Rob Lauer [2.3.0]: * VERSION: bump * README.md: generated * .gitignore - +*.reconciled, *.rendered, build-config.mk - +release artifact files * .includes/build-init.mk: new * .includes/local.mk - honor QUIET for dependency installation - install runtime and test dependencies separately with carton * .includes/perl.mk - move build configuration and helper discovery out of perl.mk - create .rendered module files before POD processing - support POD extract, remove and passthrough from rendered sources - check POD against rendered source - batch perltidy and perlcritic checks through cmb - batch resolve-vars processing - allow perltidy/perlcritic default profiles when none configured * .includes/publish.mk - require PAUSE credentials before publishing - add pre-publish and post-publish extension points - test unpacked distribution before upload * .includes/test.mk - require local dependencies before tests - add author, release and smoke test targets - add test-all - honor AUTHOR_TESTING, RELEASE_TESTING and AUTOMATED_TESTING * .includes/update.mk - move managed include definitions to Makefile (update-available) - use cmb update-available - use cmb dist-file --path-only to locate cmb_md5sums.txt * Makefile - require GNU Make 4.3 or newer - generate and include build-config.mk - centralize managed make fragment inclusion - add rendered source files as intermediate build artifacts - batch dependency reconciliation - avoid rewriting scanned dependency files when content has not changed - only update provides when content changes - remove provided modules from test-requires after reconciliation - add real-quick target - quick also disables POD checking - build tarball explicitly from buildspec.yml - clean rendered sources, reconciliation sentinels and build-config.mk * builder - clean before CI build * cpanfile - +Test::Exit, Test::Output, Log::Log4perl 1.58 * lib/CLI/Simple.pm.in (_load_manifest) - add selective command role declarations through roles: - normalize scalar and array role specifications - reject commands declared in both commands: and roles: - retain commands: as legacy manifest metadata - defer role composition until command selection (main) - pass legacy command and selective role metadata to constructor (new) - register manifest commands before command validation - resolve aliases and abbreviations before selective role composition - compose only the roles required by a selected roles: command - preserve legacy commands: behavior by composing the complete legacy role set - restore command aliases after legacy role composition - validate that selected roles implement the command method * lib/CLI/Simple/Constants.pm.in - fix COLRO typo (@VALID_OPTIONS) - +command_roles, manifest_commands * t/cli-simple-manifest.t - test manifest parsing and normalization without eager role composition - test duplicate commands and invalid selective role specifications - test manifest metadata passthrough * t/cli-simple-selective-roles.t: new - test single and multiple selective roles - test legacy all-role composition - test abbreviation and command alias resolution - test invalid role specifications and missing command methods * test-requires - Role::Tiny, Test::Exit, Test::Output, YAM::Tiny * requires.skip: removed * suggests: +Perl::Tidy Sun Oct 4 09:29:56 2026 Rob Lauer [2.2.5]: * VERSION: bump * README.md: generated * release-notes.md: updated * release-notes/release-notes-2.2.5.md: new * .gitignore - add deps.mk - updated by CPAN::Maker::Bootstrapper * .includes/help.mk: likewise * .includes/local.mk: likewise * .includes/perl.mk: likewise * .includes/update.mk: likewise * Makefile: likewise * builder: likewise * requires: pin List::Util to 1.33 * cpanfile: likewise * recommends: removed blank line * .includes/builder.mk: added by CPAN::Maker::Bootstrapper * .includes/test.mk: new * t/02-cli-simple-logging.t - skip test if no Log::Log4perl * lib/CLI/Simple.pm.in (get_args): replace zip with in-line perl Wed Sep 30 09:36:10 2026 Rob Lauer [2.2.4]: * .includes/publish.mk: added by CPAN::Maker::Bootstrapper * .includes/update.mk: updated by CPAN::Maker::Bootstrapper * Makefile: likewise * .prompts/release-notes.prompt: new * README.md: generated * VERSION: bump * lib/CLI/Simple.pm.in - POD updates (_get_help_sections): prefer USAGE when both USAGE & SYNOPSIS present * lib/CLI/Simple/Constants.pm.in (@VALID_OPTIONS): + help_sections (@DEFAULT_HELP_SECTIONS): +USAGE * recommends: generated * t/04-cli-simple-help.t - fix precedence tests Thu Sep 24 16:11:55 2026 Rob Lauer [2.2.3]: * release-notes.md: updated * release-notes/release-notes-2.2.3.md: new * README.md: generated * VERSION: bump * .includes/bootstrap.mk: added by CPAN::Maker::Bootstrapper * .includes/perl.mk: updated by CPAN::Maker::Bootstrapper * .includes/update.mk: likewise * Makefile: likewise * builder: likewise * buildspec.yml: removed -provides: section (unnecessary) * cpanfile: Role::Tiny 2.002005 * lib/CLI/Simple.pm.in (new) - initialize help sections from default (_pod_has_section): new (_get_help_sections): new (usage) - set sections dynamically * lib/CLI/Simple/Constants.pm.in (@DEFAULT_HELP_SECTION): new * t/04-cli-simple-help.t - new tests for pod sections (synopsis vs usage) * test-requires: Role::Tiny 2.002005 Tue Aug 18 10:39:25 2026 Rob Lauer [2.2.2]: * release-notes.md: updated * release-notes/release-notes-2.2.2.md: new * README.md: generated * VERSION: bump * .includes/local.mk: updated by CPAN::Maker::Bootstrapper * .gitignore - extra-files.mk, local/* * .includes/git.mk: likewise * .includes/local.mk: likewise * .includes/perl.mk: likewise * .includes/update.mk: likewise * .includes/version.mk: likewise * builder: likewise * Makefile: likewise * lib/CLI/Simple.pm.in - pod updates (use_log4perl) - $o, not $_ - better error message around eval { Log::Log4perl } (new) - check for bad value in extra_options - if default points to a non-ref, that is the command - send flag to custom help method (init_logger) - avoid undefined warning (set_args): new (run): dereference commands with handler if not a ref * lib/CLI/Simple/Modulino.pm.in - lower case to avoid err from cmb that validates values for upper case template vars * lib/CLI/Simple/Utils.pm.in: export ToCamelCase Mon Aug 3 15:58:42 2026 Rob Lauer [2.2.1]: * release-notes.md: updated * release-notes/release-notes-2.2.1.md: new * README.md: generated * VERSION: bump * .includes/help.mk: updated by CPAN::Maker::Bootstrapper * .includes/release-notes.mk: likewise * .includes/update.mk: likewise * Makefile: likewise * requires: likewise * cpanfile: likewise * lib/CLI/Simple.pm.in (main): inject alias from manifest * .includes/bash-completion.mk: new * .includes/modulino.mk: new Mon Aug 3 11:58:23 2026 Rob Lauer [2.2.0]: * release-notes.md: updated * release-notes/release-notes-2.2.0.md: new * README.md: generated * VERSION: bump * t/04-cli-simple-help.t - use_ok to avoid identifying CLI::Simple as a dependency * t/06-cli-simple-default.t: likewise * test-requires.skip: deleted Sun Aug 2 10:43:02 2026 Rob Lauer [2.1.3]: * release-notes.md: updated * release-notes/release-notes-2.1.3.md: new * README.md: generated * VERSION: bump * .includes/perl.mk: updated by CPAN::Maker::Bootstrapper * Makefile: likewise * builder: likewise * cpanfile: suggests * lib/CLI/Simple/Utils.pm.in - POD update Fri Jul 24 13:39:27 2026 Rob Lauer [2.1.2]: * release-notes.md: updated * release-notes/release-notes-2.1.2.md: new * build-requires: new * .gitignore - +*.checked, *.raw, *.tdy, test.t.tmpl, module.pm.tmp, builspec.yml.tmpl * .includes/perl.mk: updated by CPAN::Maker::Bootstrapper * .includes/release-notes.mk: likewise * .includes/update.mk: likewise * ChangeLog: modified * Makefile: likewise * README.md: generated * VERSION: bump * cpanfile - Log::Log4perl, -Log::Log4perl::Level * lib/CLI/Simple.pm.in - track generated accessors vs hand-rolled - lazy load Log::Log4perl, Pod::Usage (use_log4perl) - support for configuring log level colors - lazy load Log::Log4perl - setup log level colors (new): error out if duplicate getter/setters (_set_color_config): new (init_logger) - check if can get_log4perl_conf - set the color config if no config && color - set color off if someone tried to use color but does not have Log::Log4perl (usage) - lazy load Pod::Usage - use full class name since we don't import pod2usage * lib/CLI/Simple/Constants.pm.in - use a sprintf pattern for the ScreenColoredLevels config - hard code Log4perl log levels (avoid Log::Log4perl::Level loading) - export log level colors and config as :color-config * project.mk (install): new * recommends: moved IO::Pager, Tern::ANSIColor to suggests * suggests: what he said above * requires: -Log::Log4perl, -Log::Log4perl::Level Mon Jul 20 07:07:38 2026 Rob Lauer [2.1.1]: * release-notes.md: updated * release-notes/release-notes-2.1.1.md: new * README.md: generated * VERSION: bump * deps.mk: new * .includes/perl.mk: updated by CPAN::Maker::Bootstrapper * Makefile: likewise * deps.mk: generated by CPAN::Maker::Bootstrapper * lib/CLI/Simple.pm.in - use IO::Pager if ! interactive * project.mk - removed recipes, now autogenerated by CPAN::Maker::Bootstrapper * requires: +IO::Interactive * cpanfile: likewise Fri Jul 17 11:22:02 2026 Rob Lauer [2.1.0]: * release-notes.md: updated * release-notes/release-notes-2.1.0.md: new * README.md: generated * VERSION: bump, feature release (auto-color) * project.mk: new * .includes/git.mk: updated by CPAN::Maker::Bootstrapper * .includes/update.mk: likewise * Makefile: likewise * lib/CLI/Simple.pm.in - exit code - POD updates (use_log4perl) - add support for color appender (init_logger): likewise * lib/CLI/Simple/Constants.pm.in ($LOG4PERL_CONF): new * requires.skip - make Term::ANSIColor optional * recommends: +Term::ANSIColor Mon Jul 13 13:38:12 2026 Rob Lauer [2.0.14]: * release-notes.md: updated * release-notes/release-notes-2.0.14.md: new * README.md: generated * VERSION: bump * lib/CLI/Simple.pm.in - document proper use of help_sections (new) - accept help_sections - set default help sections if not provided in constructor * lib/CLI/Simple/Utils.pm.in (choose): fix context error (file_or_stdout): new Sat Jul 11 09:12:02 2026 Rob Lauer [2.0.13]: * release-notes.md: updated * release-notes/release-notes-2.0.13.md: new * .includes/perl.mk: podchecker * README.md: generated * VERSION: bump * lib/CLI/Simple.pm.in: fixed pod Thu Jul 9 08:58:18 2026 Rob Lauer [2.0.12]: * release-notes.md: updated * release-notes/release-notes-2.0.12.md: new * README.md: generated * VERSION: bump * lib/CLI/Simple.pm.in - pod updates for new features (new) - check options against current valid option list (validate_command) - bypass command validation check if _validate_command is false (command_args): new * lib/CLI/Simple/Constants.pm.in - +@VALID_OPTIONS * t/01-cli-simple.t - test for unknown options Tue Jul 7 10:48:12 2026 Rob Lauer [2.0.11]: * VERSION: modified * release-notes.md: updated * release-notes/release-notes-2.0.11.md: new * lib/CLI/Simple.pm.in (use_log4perl) - accept log-level, log_level, loglevel level - die if unknown argument * lib/CLI/Simple/Constants.pm.in (chars): +$EQUALS * lib/CLI/Simple/Utils.pm.in (slurp): allow file handle argument Sun Jul 5 06:16:50 2026 Rob Lauer [2.0.10]: * VERSION: modified * lib/CLI/Simple.pm.in - remove use IO::Pager Sat Jul 4 15:33:47 2026 Rob Lauer [2.0.9]: * release-notes.md: updated * release-notes/release-notes-2.0.9.md: new * Makefile: updated by CPAN::Maker::Bootstrapper * builder: likewise * .includes/perl.mk: likewise * .includes/release-notes.mk: likewise * cpanfile: sorted * README.md: generated * VERSION: bump * lib/CLI/Simple.pm.in - add pager support - add support for custom help sections * recommends: new Tue Jun 30 09:26:00 2026 Rob Lauer [2.0.8]: * VERSION: bump * README.md: generated * release-notes.md: updated * release-notes/release-notes-2.0.8.md: new * lib/CLI/Simple/Modulino.pm.in (create_modulino) - default install directory = PERL_LOCAL_LIB_ROOT/bin or installsitebin Mon Jun 29 12:29:27 2026 Rob Lauer [2.0.7]: * release-notes.md: updated * release-notes/release-notes-2.0.7.md: new * VERSION: bump * README.md: generated * lib/CLI/Simple/Modulino.pm.in (cmd_create_modulino) - set default insta0ll path to installsitebin Thu Jun 25 05:02:45 2026 Rob Lauer [2.0.6]: * release-notes.md: updated * release-notes/release-notes-2.0.6.md: new * VERSION: bump * README.md: modified * bin/create-modulino.pl.in - moved to CLI/Simple/Modulino.pl.in - exit with error code (cmd_create_modulino) - @MODULE_NAME@ => @PERL_MODULE_NAME@ to avoid conflict with @MODULE_NAME@ * .gitignore: + bin/create-modulino * buildspec.yml: +CLI::Simple::Modulino Wed Jun 24 07:53:30 2026 Rob Lauer [2.0.5]: * VERSION: bump Tue Jun 23 15:49:39 2026 Rob Lauer [2.0.4]: * release-notes.md: updated * release-notes/release-notes-2.0.4.md: new * VERSION: bump * README.md: updated * lib/CLI/Simple.pm.in (usage): avoid uninitialized warning Tue Jun 9 14:44:42 2026 Rob Lauer [2.0.3]: * release-notes.md: updated * release-notes/release-notes-2.0.3.md: new * VERSION: bump * README.md: updated * lib/CLI/Simple/Utils.pm.in (toPascalCase): new (ToCamelCase): new (toCamelCase): new (to_snake_case): new Tue May 19 14:51:05 2026 Rob Lauer [2.0.2]: * bin/cli-simple.in: renamed from .in.sh * lib/CLI/Simple/Shell.pm.in: new * lib/CLI/Simple.pm.in (usage) - display shell usage if cli-simple * .gitignore: add bin/cli-simple * README.md: generated * VERSION: bump * buildspec.yml: man link for cli-simple Mon May 4 13:54:48 2026 Rob Lauer [2.0.1]: * release-notes/release-notes-2.0.1.md: new * VERSION: bump * builder - renamed - updated from CPAN::Maker::Bootstrapper * Makefile: likewise * .github/workflows/build.yml: likewise * .includes/perl.mk: likewise * .includes/update.mk: likewise * build-requires - removed Text::CSV_XS * buildspec.yml: replace '_' with '-' * lib/CLI/Simple.pm.in (_load_manifest): YAML::XS => YAML::Tiny * lib/CLI/Simple/Helpers.pm.in (_resolve_spec): likewise (_generate_spec_file): likewise * lib/CLI/Simple/Scaffold.pm.in (_cmd_scaffold): likewise * requires: likewise * cpanfile: likewise * test-requires: likewise * t/cli-simple-manifest.t: likewise * lib/CLI/Simple/Constants.pm.in - + $FAT_ARROW * lib/CLI/Simple/Utils.pm.in: typo (NAAME) Thu Apr 23 13:25:42 2026 Rob Lauer [2.0.0]: CPAN::Maker::Bootstrapper build update * .github/workflows/build.yml: new * .includes/git.mk: new * .includes/help.mk: new * .includes/perl.mk: new * .includes/update.mk: new * .includes/upgrade.mk: new * Makefile: new * build-requires: new * test-requires.skip: new * build-github: new * .autoconf-template-perlrc: deleted * COPYING: deleted * LICENSE: deleted * Makefile.am: deleted * README: deleted * autotools/ads_PERL_INCLUDES.m4: deleted * autotools/ads_PERL_MODULE.m4: deleted * autotools/ads_PROG_PERL.m4: deleted * autotools/am_build_mode.m4: deleted * autotools/am_perlcritic_mode.m4: deleted * autotools/apache_config.m4: deleted * autotools/ax-extra-opts.m4: deleted * autotools/ax_am_conditional_example.m4: deleted * autotools/ax_deps_check.m4: deleted * autotools/ax_distcheck_hack.m4: deleted * autotools/ax_perlcritic_config.m4: deleted * autotools/ax_requirements_check.m4: deleted * autotools/prove.sh.in: deleted * config/Makefile.am: deleted * configure.ac: deleted * cpan/Makefile.am: deleted * cpan/requires: deleted * cpan/test-requires: deleted * includes/apache-directories.inc: deleted * includes/bash-bin.inc: deleted * includes/directories.inc: deleted * includes/perl-bin.inc: deleted * includes/perl-cgi-bin.inc: deleted * includes/perl-modules.inc: deleted * includes/perlcritic.inc: deleted * install-from-cpan.in: deleted * perl-CLI-Simple.spec.in: deleted * perlcriticrc: deleted * perltidyrc: deleted * project.yaml: deleted * requires.json: deleted * requires.txt: deleted * resources/Makefile.am: deleted * src/Makefile.am: deleted * src/examples/MyScript.pm: deleted * src/main/Makefile.am: deleted * src/main/bash/Makefile.am: deleted * src/main/bash/bin/Makefile.am: deleted * src/main/perl/Makefile.am: deleted * src/main/perl/bin/Makefile.am: deleted * src/main/perl/lib/Makefile.am: deleted * bootstrap: deleted * cpan/buildspec.yml => buildspec.yml * release-notes.mk => .includes/release-notes.mk * src/main/perl/lib/t/00-cli-simple-constants.t.in => t/00-cli-simple-constants.t * src/main/perl/lib/t/00-cli-simple-utils.t.in => t/00-cli-simple-utils.t * src/main/perl/lib/t/00-cli-simple.t.in => t/00-cli-simple.t * src/main/perl/lib/t/01-cli-simple.t.in => t/01-cli-simple.t * src/main/perl/lib/t/02-cli-simple-logging.t.in => t/02-cli-simple-logging.t * src/main/perl/lib/t/03-cli-simple-types.t.in => t/03-cli-simple-types.t * src/main/perl/lib/t/04-cli-simple-help.t.in => t/04-cli-simple-help.t * src/main/perl/lib/t/05-cli-simple-args.t.in => t/05-cli-simple-args.t * src/main/perl/lib/t/06-cli-simple-default.t.in => t/06-cli-simple-default.t * version.mk => .includes/version.mk * src/main/bash/bin => bin}/cli-simple.sh.in * src/main/perl/bin => bin}/create-modulino.pl.in * src/main/perl/lib => lib}/CLI/Simple.pm.in * src/main/perl/lib => lib}/CLI/Simple/Constants.pm.in * src/main/perl/lib => lib}/CLI/Simple/DumpSpec.pm.in * src/main/perl/lib => lib}/CLI/Simple/Helpers.pm.in * src/main/perl/lib => lib}/CLI/Simple/Migrate.pm.in * src/main/perl/lib => lib}/CLI/Simple/Scaffold.pm.in * src/main/perl/lib => lib}/CLI/Simple/Utils.pm.in * src/main/perl/lib/t => t}/cli-simple-manifest.t [2.0.0]: * VERSION: modified * release-notes-2.0.0.md: new * Makefile.am: + release-notes.mk * includes/bash.inc: + cli-simple.sh.in, -modulino.sh.in, -cli-simple-example.sh.in * cpan/buildspec.yml - +CLI::Simple::Helper, CLI::Simple::DumpSpec, CLI::Simple::Migrate, CLI::Simple::Scaffold * includes/perl-modules.inc: add above to build * src/main/perl/bin/create-modulino.pl.in: refactored to use CLI::Simple * cpan/requires - +Role::Tiny, Role::Tiny::With, YAML:::XS * src/main/perl/lib/CLI/Simple/Utils.pm.in: modified * src/main/perl/lib/CLI/Simple/README.md: generated * src/main/perl/lib/CLI/Simple.pm.in - load manifest to specify commands and options - pod updates - caller or __PACKAGE__->main (main): don't croak if no manifest (%INTERNAL_COMMANDS): new ($MANIFEST): new (import): new (load_manifest): new (new) - dispatch to internal commands (get_args): return ref or list (set_args): new (_cmd_generate_completion) - generate a bash completion script based on options and commands * src/main/perl/lib/CLI/Simple/DumpSpec.pm.in: new * src/main/perl/lib/CLI/Simple/Helpers.pm.in: new * src/main/perl/lib/CLI/Simple/Migrate.pm.in: new * src/main/perl/lib/CLI/Simple/Scaffold.pm.in: new Sat Feb 14 16:03:49 2026 Rob Lauer [1.0.12]: * VERSION: bump * LICENSE: align with code * cpan/Makefile.am: add README.md * cpan/buildspec.yml: likewise * src/main/perl/lib/CLI/Simple/Utils.pm.in: align license * src/main/perl/lib/CLI/Simple/README.md: generated * src/main/perl/lib/CLI/Simple/Utils/README.md: likewise Tue Feb 3 16:20:20 2026 Rob Lauer [1.0.11]: * VERSION: bump * src/main/bash/bin/modulino.sh.in - MODULINO_PATH, not $MODULION_PATH * src/main/perl/lib/CLI/Simple.pm.in - whitespace - fixed pod alias example (init_logger) - allow ref or scalar for config * src/main/perl/lib/CLI/Simple/README.md: generated Mon Dec 15 11:57:08 2025 Rob Lauer [1.0.10]: * VERSION: bump * src/main/perl/lib/CLI/Simple.pm.in (new): create accessors in actual class, not base class Wed Nov 5 07:57:44 2025 Rob Lauer [1.0.9]: * VERSION: bump * src/main/perl/lib/CLI/Simple.pm.in - pod updates to explain lifecycle (@EXPORT_OK): + $AUTO_HELP, $AUTO_DEFAULT (new) - only use single command as default if $AUTO_DEFAULT enabled - only provide usage if no command when $AUTO_HELP is enabled (validate_command) - just return if command $EMPTY (run): likewise * src/main/perl/lib/CLI/Simple/README.md: generated * src/main/perl/lib/t/04_cli-simple-help.t.in: $AUTO_HELP * src/main/perl/lib/t/06-cli-simple-default.t.in: likewise * src/main/perl/lib/t/01-cli-simple.t: check bad-option output Tue Oct 7 14:23:24 2025 Rob Lauer [1.0.8]: * VERSION: bump * src/main/perl/lib/CLI/Simple/README.md: generated * src/main/perl/lib/CLI/Simple/Utils.pm.in (choose): new - export choose - added pod * src/main/perl/lib/CLI/Simple/Utils/README.md: generated Untracked files: (use "git add ..." to include in what will be committed) example.pl quick-start.pl Sun Sep 28 15:43:18 2025 Rob Lauer [1.0.7]: * VERSION: bump * cpan/test-requires: add missing depdencies * src/main/perl/lib/CLI/Simple/README.md: generated Sun Sep 28 07:37:59 2025 Rob Lauer [1.0.5]: * VERSION: bump * src/main/perl/lib/CLI/Simple/README.md: generated * src/main/perl/lib/CLI/Simple.pm.in (command): allow setting new command (commands): allow insert of new command Fri Aug 22 07:59:10 2025 Rob Lauer [1.0.4]: * VERSION: bump * src/main/perl/lib/CLI/Simple/README.md: generated * src/main/perl/lib/CLI/Simple.pm.in (new) - normalize alias definitions too Thu Aug 21 15:23:34 2025 Rob Lauer [1.0.3]: * VERSION: bump * src/main/perl/lib/CLI/Simple/README.md: generated * src/main/perl/lib/t/06-cli-simple-default.t.in: new * src/main/perl/lib/Makefile.am: add above to build * src/main/perl/lib/CLI/Simple.pm.in - pod updates (new) - if only one command make the command name optional - check ref $help - pod2usage, not return pod2usage (run): default command gets set earlier now * src/main/perl/t/01-cli-simple.t.in - fix test for bad command, single command does not require command Thu Aug 21 12:45:30 2025 Rob Lauer [1.0.2]: * .gitignore: +cli-simple-example, create-modulino * VERSION: bump * test-requires: Test::Exit, Test::Output * cpan/buildspec.yml - + LICENSE, README, move examples to share/ * includes/bash-bin.inc: + cli-simple-example.sh.in * src/examples/MyScript.pm - more example usage * src/main/perl/bin/Makefile.am - add create_modulino, but make sure .pl does not get in distribution * src/main/perl/bin/create-modulino.pl.in: /usr/bin/env perl * src/main/perl/lib/CLI/Simple.pm.in - $USE_LOGGER to avoid redefinition of _use_logger - pod updates (_use_logger): use above (use_log4perl) - allow log_level or level - remove _use_logger creation (new) - + alias option - don't require option_specs - process option and command aliases - use _leave, not exit (_leave): new (init_logger): get root logger (get_args): allow skipping args (usage) - don't exit, use _leave() - noperldoc (example): new (validate_command): refactored * src/main/perl/lib/CLI/Simple/README.md: generated * src/main/perl/lib/CLI/Simple/Utils.pm.in (dmp): new (args): new * src/main/perl/lib/Makefile.am: add new tests * src/main/perl/lib/t/01-cli-simple.t.in - added more tests for alias * src/main/perl/lib/t/04-cli-simple-help.t.in * README: new * src/main/bash/bin/cli-simple-example.sh.in: new * src/main/perl/lib/t/01-cli-simple.t.in: new * src/main/perl/lib/t/02-cli-simple-logging.t.in: new * src/main/perl/lib/t/03-cli-simple-types.t.in: new * src/main/perl/lib/t/04-cli-simple-help.t.in: new * src/main/perl/lib/t/05-cli-simple-args.t.in: new Fri Aug 8 08:15:06 2025 Rob Lauer [1.0.1]: * VERSION:bump * .gitignore: cpan/*.tmp * requires: new * test-requires: new * src/main/perl/lib/CLI/Simple.pm.in - package variables GETOPT_EXIT_ON_ERROR, GETOPT_STATUS, GETOPT_ERROR_MESSAGE - + abbreviations, error_handler (new) - trap GetOptions error - allow any word character in options - support array ref command spec - validate command (use abbreviations) (init_logger) - support command specific log levels (validate_command): new (run) - don't set log level here, set in init_logger * src/main/perl/lib/CLI/Simple/Constants.pm.in - pod updates * src/main/perl/lib/CLI/Simple/Constants/README.md * src/main/perl/lib/CLI/Simple/README.md Fri Aug 1 13:23:16 2025 Rob Lauer [1.0.0]: * version.mk: new * Makefile.am: add above * VERSION: bump major * src/main/perl/lib/CLI/Simple.pm.in (program): new (commands): new (run) - support custom per command log levels - refactoring (init_logger): likewise (use_log4perl) - _get_log4perlconf -> get_log4perl_conf - _get_log4perl_level -> get_lgo4perl_level - + set_log4_perl() * src/main/perl/lib/CLI/Simple/README.md: generated Tue Feb 25 10:00:08 2025 Rob Lauer [0.0.9]: * VERSION: bump * src/main/perl/lib/CLI/Simple.pm.in - pod tweaks (new) - allow for custom help() method (command): new (run): remove help invocation as it now happens in new() * src/main/perl/lib/CLI/Simple/README.md: generated Sat Feb 15 09:25:47 2025 Rob Lauer [0.0.8]: * VERSION: bump * cpan/buildspec.yml (extra-files): examples * src/main/bash/bin/modulino.sh.in - help - use command, not which * src/main/perl/lib/CLI/Simple.pm.in - minor formatting tweaks - documenation, pod updates (new): invoke help before init * src/main/perl/lib/CLI/Simple/README.md: generated Sat Jun 22 10:52:48 2024 Rob Lauer [0.0.7]: * VERSION: bump * src/main/perl/lib/CLI/Simple.pm.in: pod fixes * src/main/perl/lib/CLI/Simple/README.md: generated Wed May 22 07:37:11 2024 Rob Lauer [0.0.6]: * VERSION: bump * requires: List::Util 1.56 * autotools/ax_requirements_check.m4: likewise Tue May 21 16:43:42 2024 Rob Lauer [0.0.5]: * VERSION: bump * src/main/perl/bin/create-modulino.pl.in - use GetOptions to pass options - use INSTALLSITESCRIPT, DESTDIR to install modulino Tue May 21 09:33:40 2024 Rob Lauer [0.0.4]: * VERSION: bump * src/main/perl/bin/create-modulino.pl.in: new * cpan/buildspec.yml: add above to build * src/main/includes/perl-bin.inc: likewise, +x * src/main/bash/bin/modulino.sh.in - use realpath/readlink to allow alias names * src/main/perl/lib/CLI/Simple.pm.in - use JSON, not JSON::PP - pod fixes, better explanation of modulinos * src/main/perl/lib/CLI/Simple/Utils.pm.in - use JSON, not JSON::PP * autotools/ax_requirements_check.m4: update requirements * cpan/requires: likewise * requires.txt: likewise Sun Dec 10 16:46:14 2023 Rob Lauer [0.0.3]: * VERSION: bump * src/main/oerl/lib/CLI/Simple.pm.in - pod tweaks * src/main/bash/bin/modulino.sh.in - use -, not _ for separator - use perl, not tr Sat Nov 18 08:26:26 2023 Rob Lauer [dependencies]: * VERSION: bump * autotools/ax_requirements_check.m4: update dependencies * requires.json: likewise * requires.txt: likewise * cpan/requires: likewise * cpan/buildspec.yml: typo in web address * configure.ac: add perlcritic config to configure output * manifest.yaml: removed * src/main/perl/lib/CLI/Simple/Utils.pm.in: @PACKAGE_VERSION@ Thu Nov 16 17:14:27 2023 Rob Lauer [modulino]: * .gitignore: add modulino * cpan/buildspec.yml - add src/examples - add modulino script * src/examples/MyScript.pm: renamed from myscript.pm * includes/bash-bin.inc: add modulino.sh.in * src/main/bash/bin/Makefile.am: likewise * src/main/perl/lib/Makefile.am: build .t files * src/main/perl/lib/CLI/Simple/README.md: generated * src/main/perl/lib/CLI/Simple.pm.in - pod tweaks (get_args): return all args if empty list of var names Thu Nov 16 15:18:56 2023 Rob Lauer [pod tweaks]: * src/main/perl/lib/CLI/Simple.pm.in - pod tweaks (new): accept hash or hash ref Thu Nov 16 13:36:17 2023 Rob Lauer [make check]: * includes/perlcritic.inc: add pbp to theme * .gitignore: + generatedd files cpan/, t/ * Makefile.am: remove README.md from build * README.md: symlink to module README.md * cpan/Makefile.am: rm directory if created * src/main/perl/lib/CLI/Simple.pm.in: pod tweaks * src/main/perl/lib/Makefile.am: build .t files from .t.in * src/main/perl/lib/README.md: generated * src/main/perl/lib/CLI/Simple/README.md: likewise Thu Nov 16 07:53:09 2023 Rob Lauer [docs/wip]: * README.md: generated * src/main/perl/lib/CLI/Simple.pm.in * src/main/perl/lib/CLI/Simple/README.md Wed Nov 15 16:25:19 2023 Rob Lauer [docs]: * Makefile.am: make docs * README.md: generated * src/main/perl/lib/CLI/Simple/README.md: likewise * src/main/perl/lib/CLI/Simple.pm.in: pod tweaks Wed Nov 15 15:51:35 2023 Rob Lauer [BigBang]: * .gitignore: add .3man * README.md: new * includes/perl-modules.inc: dependencies * src/examples/myscript.pm: new * src/main/perl/lib/CLI/Simple.pm.in - replace template with actual file * src/main/perl/lib/CLI/Simple/Constants.pm.in: likewise * src/main/perl/lib/CLI/Simple/Utils.pm.in: likewise * src/main/perl/lib/CLI/Simple/Constants/README.md: new * src/main/perl/lib/CLI/Simple/README.md: new * src/main/perl/lib/CLI/Simple/Utils/README.md: new * src/main/perl/lib/README.md: new Wed Nov 15 08:15:23 2023 [BigBang]: * VERSION: new * .autoconf-template-perlrc: new * .gitignore: new * bootstrap: new * ChangeLog: new * configure.ac: new * COPYING: new * COPYRIGHT: new * Makefile.am: new * manifest.yaml: new * perl-CLI-Simple.spec.in: new * perlcriticrc: new * perltidyrc: new * README.md: new * requires.json: new * requires.txt: new * autotools/ads_PERL_INCLUDES.m4: new * autotools/ads_PERL_MODULE.m4: new * autotools/ads_PROG_PERL.m4: new * autotools/am_build_mode.m4: new * autotools/am_perlcritic_mode.m4: new * autotools/apache_config.m4: new * autotools/ax-extra-opts.m4: new * autotools/ax_am_conditional_example.m4: new * autotools/ax_deps_check.m4: new * autotools/ax_distcheck_hack.m4: new * autotools/ax_perlcritic_config.m4: new * autotools/ax_requirements_check.m4: new * autotools/prove.sh.in: new * autotools/test-driver: new * config/Makefile.am: new * cpan/Makefile.am: new * cpan/requries: new * cpan/test-requires: new * cpan/buildspec.yml * includes/apache-directories.inc: new * includes/bash-bin.inc: new * includes/directories.inc: new * includes/perl-bin.inc: new * includes/perl-cgi-bin.inc: new * includes/perl-modules.inc: new * includes/perlcritic.inc: new * install-from-cpan.in: new * resources/Makefile.am: new * src/main/bash/bin/Makefile.am: new * src/main/bash/Makefile.am: new * src/main/Makefile.am: new * src/main/perl/bin/Makefile.am: new * src/main/perl/lib/CLI/Simple.pm.in: new * src/main/perl/lib/CLI/Simple/Constants.pm.in: new * src/main/perl/lib/CLI/Simple/Utils.pm.in: new * src/main/perl/lib/Makefile.am: new * src/main/perl/lib/t/00-cli-simple-constants.t: new * src/main/perl/lib/t/00-cli-simple-utils.t: new * src/main/perl/lib/t/00-cli-simple.t: new * src/main/perl/Makefile.am: new * src/Makefile.am: new CLI-Simple-2.4.0/bin/0000755000175100017510000000000015270644577013514 5ustar rlauerrlauerCLI-Simple-2.4.0/bin/create-modulino0000755000175100017510000000047515270644577016537 0ustar rlauerrlauer#!/usr/bin/env bash #-*- mode: sh; -*- # modulino invocation MODULINO_WRAPPER=create-modulino MODULINO_MODULE_NAME=CLI::Simple::Modulino MODULINO_WRAPPER="$MODULINO_WRAPPER" \ MODULINO_MODULE_NAME="$MODULINO_MODULE_NAME" \ perl -M"$MODULINO_MODULE_NAME" \ -e 'exit $ENV{MODULINO_MODULE_NAME}->main' \ -- "$@" CLI-Simple-2.4.0/bin/cli-simple0000755000175100017510000000043215270644577015477 0ustar rlauerrlauer#!/usr/bin/env bash #-*- mode: sh; -*- # modulino invocation MODULINO_WRAPPER=cli-simple MODULE_NAME=CLI::Simple MODULE_PATH=$(MODULE_PATH="${MODULE_NAME//:://}.pm" perl -M$MODULE_NAME -e 'print $INC{$ENV{MODULE_PATH}};') MODULINO_WRAPPER=$MODULINO_WRAPPER perl $MODULE_PATH "$@" CLI-Simple-2.4.0/lib/0000755000175100017510000000000015270644577013512 5ustar rlauerrlauerCLI-Simple-2.4.0/lib/CLI/0000755000175100017510000000000015270644577014121 5ustar rlauerrlauerCLI-Simple-2.4.0/lib/CLI/Simple.pm0000644000175100017510000031403515270644577015716 0ustar rlauerrlauerpackage CLI::Simple; # a Simple, Fast & Easy way to create scripts use strict; use warnings; use CLI::Simple::Constants qw(:booleans :chars :log-levels @VALID_OPTIONS @DEFAULT_HELP_SECTIONS :color-config); use CLI::Simple::Utils qw(normalize_options slurp dmp choose); use CLI::Simple::DumpSpec qw(_cmd_dump_spec); use CLI::Simple::Migrate qw(_cmd_migrate); use CLI::Simple::Scaffold qw(_cmd_scaffold); use CLI::Simple::Helpers qw(_is_class_name); use CLI::Simple::Shell; use Carp; use Data::Dumper; use English qw(-no_match_vars); use FindBin qw($RealBin $RealScript); use File::Basename qw(basename); use File::Which qw(which); use Getopt::Long qw(:config no_ignore_case); use IO::Interactive; use List::Util qw(none pairs any); use Scalar::Util qw(reftype); our $VERSION = '2.4.0'; our $GETOPT_EXIT_ON_ERROR = $TRUE; our $GETOPT_STATUS; our $GETOPT_ERROR_MESSAGE; my %GENERATED_ACCESSOR; # tracks accessors mk_accessors itself created, per class __PACKAGE__->follow_best_practice; __PACKAGE__->mk_accessors( qw( _validate_command _command _command_args _commands _program _abbreviations _options ) ); our $USE_LOGGER = $FALSE; our $AUTO_DEFAULT = $FALSE; our $AUTO_HELP = $FALSE; our $PAGER = $TRUE; our %INTERNAL_COMMANDS = ( '-generate-completion' => \&_cmd_generate_completion, '-migrate' => \&_cmd_migrate, '-dump-spec' => \&_cmd_dump_spec, '-scaffold' => \&_cmd_scaffold, ); our @EXPORT_OK = qw($AUTO_HELP $AUTO_DEFAULT $PAGER $USE_LOGGER); use parent qw(Exporter Class::Accessor::Fast); # CLI::Simple 2.0.0 additions our $MANIFEST; # package-level, per-consumer class caller or exit __PACKAGE__->main(); ######################################################################## sub import { ######################################################################## my ( $class, @args ) = @_; my $caller = caller; my @yaml_files = grep {/[.]ya?ml\z/xsm} @args; @yaml_files = map { /[*?\[]/xsm ? glob($_) : $_ } @yaml_files; if ( any { $_ eq ':roles' } @args ) { if ( !${^COMPILING} ) { ( my $dist = $caller ) =~ s/::/-/gxsm; my $yaml_file = lc($dist) . '.yml'; require File::ShareDir; my $path = eval { File::ShareDir::dist_file( $dist, $yaml_file ) }; if ( $path && !-e $path ) { undef $path; } unshift @yaml_files, $path if $path; } } if (@yaml_files) { $class->_load_manifest( $caller, @yaml_files ); } # preserve existing Exporter behaviour $class->export_to_level( 1, $class, grep { $_ ne ':roles' && !/[.]ya?ml\z/xsm } @args ); return; } ######################################################################## sub _validate_manifest { ######################################################################## my ( $class, $manifest, $filename ) = @_; my %section_types = ( commands => 'HASH', roles => 'HASH', options => 'ARRAY', default_options => 'HASH', extra_options => 'ARRAY', alias => 'HASH', abbreviations => 'SCALAR', version => 'SCALAR', distribution => 'SCALAR', ); die sprintf "ERROR: manifest %s must contain a YAML mapping\n", $filename if !ref $manifest || reftype($manifest) ne 'HASH'; foreach my $key ( keys %{$manifest} ) { die sprintf "ERROR: unknown manifest key '%s' in %s\n", $key, $filename if !exists $section_types{$key}; my $expected = $section_types{$key}; my $value = $manifest->{$key}; if ( $expected eq 'SCALAR' ) { die sprintf "ERROR: manifest key '%s' in %s must be a scalar\n", $key, $filename if ref $value; next; } die sprintf "ERROR: manifest key '%s' in %s must be a %s\n", $key, $filename, $expected if !ref $value || reftype($value) ne $expected; } foreach my $section (qw(options extra_options)) { foreach my $value ( @{ $manifest->{$section} // [] } ) { die sprintf "ERROR: manifest key '%s' in %s must contain scalar values\n", $section, $filename if ref $value; } } return; } ######################################################################## sub _load_manifest { ######################################################################## my ( $class, $target, @yaml_files ) = @_; require YAML::Tiny; my $manifest = {}; my %option_owner; my %default_owner; foreach my $yaml_file (@yaml_files) { my $additional = YAML::Tiny::LoadFile($yaml_file) or die "ERROR: could not load manifest: $yaml_file\n"; $class->_validate_manifest( $additional, $yaml_file ); foreach my $section (qw(commands roles)) { my $entries = $additional->{$section} // {}; foreach my $cmd ( keys %{$entries} ) { die sprintf "ERROR: duplicate command '%s' in %s\n", $cmd, $yaml_file if $yaml_file ne $yaml_files[0] && ( exists $manifest->{commands}{$cmd} || exists $manifest->{roles}{$cmd} ); $manifest->{$section}{$cmd} = $entries->{$cmd}; } } # Options belong to a single global namespace, including aliases. foreach my $spec ( @{ $additional->{options} // [] } ) { my ($names) = $spec =~ /\A([\w|-]+)/xsm; die sprintf "ERROR: invalid option specification '%s' in %s\n", $spec, $yaml_file if !$names; foreach my $name ( split /[|]/xsm, $names ) { ( my $accessor = $name ) =~ s/-/_/gxsm; die sprintf "ERROR: duplicate option '%s' in %s (already declared in %s)\n", $name, $yaml_file, $option_owner{$accessor} if exists $option_owner{$accessor}; $option_owner{$accessor} = $yaml_file; } push @{ $manifest->{options} }, $spec; } foreach my $name ( keys %{ $additional->{default_options} // {} } ) { ( my $accessor = $name ) =~ s/-/_/gxsm; die sprintf "ERROR: duplicate default option '%s' in %s (already declared in %s)\n", $name, $yaml_file, $default_owner{$accessor} if exists $default_owner{$accessor}; $default_owner{$accessor} = $yaml_file; $manifest->{default_options}{$name} = $additional->{default_options}{$name}; } # Accessor declarations may repeat; they do not introduce new switches. my %extra_seen; foreach my $name ( @{ $manifest->{extra_options} // [] } ) { ( my $accessor = $name ) =~ s/-/_/gxsm; $extra_seen{$accessor} = 1; } foreach my $name ( @{ $additional->{extra_options} // [] } ) { ( my $accessor = $name ) =~ s/-/_/gxsm; next if $extra_seen{$accessor}++; push @{ $manifest->{extra_options} }, $name; } # Only the primary manifest controls application-wide metadata. if ( $yaml_file eq $yaml_files[0] ) { foreach my $key ( keys %{$additional} ) { next if any { $key eq $_ } qw(commands roles options default_options extra_options); $manifest->{$key} = $additional->{$key}; } } } $class->_normalize_manifest($manifest); no strict 'refs'; ## no critic ${"${target}::_CLI_MANIFEST"} = $manifest; return; } ######################################################################## sub _normalize_manifest { ######################################################################## my ( $class, $manifest ) = @_; my $commands = $manifest->{commands} // {}; my $roles = $manifest->{roles} // {}; foreach my $cmd ( keys %{$roles} ) { die sprintf "ERROR: command '%s' is defined in both commands and roles\n", $cmd if exists $commands->{$cmd}; } my %selective_roles; foreach my $cmd ( keys %{$roles} ) { my $value = $roles->{$cmd}; my @roles = choose { return @{$value} if ref $value && reftype($value) eq 'ARRAY'; return ($value) if !ref $value; die sprintf "ERROR: invalid roles specification for command '%s'\n", $cmd; }; foreach my $role (@roles) { die sprintf "ERROR: invalid role '%s' for command '%s'\n", $role, $cmd if !_is_class_name($role); } $selective_roles{$cmd} = \@roles; } $manifest->{_commands} = $commands; $manifest->{_roles} = \%selective_roles; return; } ######################################################################## sub _manifest { ######################################################################## my ($class) = @_; no strict 'refs'; ## no critic return ${"${class}::_CLI_MANIFEST"}; } ######################################################################## sub main { ######################################################################## my ($class) = @_; my $manifest = $class->_manifest; my $cli = $class->new( option_specs => $manifest ? ( $manifest->{options} // [] ) : [], alias => $manifest ? ( $manifest->{alias} // {} ) : {}, default_options => $manifest ? ( $manifest->{default_options} // {} ) : {}, extra_options => $manifest ? ( $manifest->{extra_options} // [] ) : [], commands => $manifest ? {} : { default => \&usage }, manifest_commands => $manifest ? ( $manifest->{_commands} // {} ) : {}, command_roles => $manifest ? ( $manifest->{_roles} // {} ) : {}, abbreviations => $manifest ? ( $manifest->{abbreviations} // $FALSE ) : $FALSE, ); return $cli->run; } my %LOGGER_ENABLED; my %OPTION_PROVIDERS; my %OPTION_PROVIDER_ORDER; ######################################################################## sub _use_logger { ######################################################################## my ($self) = @_; my $class = ref $self || $self; return $LOGGER_ENABLED{$class} // $FALSE; } ######################################################################## sub use_options { ######################################################################## my ( $class, @providers ) = @_; require Role::Tiny; croak 'Odd number of arguments to use_options' if @providers % 2; while (@providers) { my ( $name, $definition ) = splice @providers, 0, 2; croak sprintf q{Invalid provider name: %s}, $name if $name !~ /\A[a-z][a-z0-9_]*\z/; croak sprintf q{Duplicate option provider: %s}, $name if exists $OPTION_PROVIDERS{$class}{$name}; my ($role) = keys %{$definition}; croak sprintf q{Invalid provider definition: %s}, $name if keys %{$definition} != 1; my $options = $definition->{$role}; croak sprintf q{Invalid option list for provider: %s}, $name if ref $options ne 'ARRAY'; Role::Tiny->apply_roles_to_package( $class, $role ); $OPTION_PROVIDER_ORDER{$class} //= []; push @{ $OPTION_PROVIDER_ORDER{$class} }, $name; $OPTION_PROVIDERS{$class}{$name} = { role => $role, options => [ @{$options} ], }; } return $class; } ######################################################################## sub use_log4perl { ######################################################################## my ( $self, %args ) = @_; my @valid_options = qw( log_level level loglevel log-level config color debug_color info_color warn_color error_color trace_color fatal_color ); foreach my $o ( keys %args ) { die "ERROR: unknown argument ($o)\n" if none { $o eq $_ } @valid_options; } eval { require Log::Log4perl; 1; } or die "use_log4perl() requires Log::Log4perl, which is not installed.\n" . "It is an opt-in feature, so CLI::Simple does not depend on it.\n" . "If your app calls use_log4perl(), add 'Log::Log4perl' to your requires/cpanfile.\n"; my $class = ref $self || $self; die "ERROR: color and config are mutually exclusive - color uses CLI::Simple's built-in colorized config\n" if $args{color} && $args{config}; my ($level) = grep {defined} @args{qw(log_level log-level loglevel level)}; $level //= 'error'; my $log4perl_conf = $args{config}; if ( $args{color} && !$log4perl_conf ) { $log4perl_conf = $self->_set_color_config(%args); } { $LOGGER_ENABLED{$class} = $TRUE; no strict 'refs'; ## no critic (ProhibitNoStrict) *{"${class}::get_log4perl_conf"} = sub { return $log4perl_conf }; *{"${class}::set_log4perl_conf"} = sub { $log4perl_conf = $_[1] }; *{"${class}::get_log4perl_level"} = sub { return $level }; } if ( !$self->can('set_logger') ) { $self->mk_accessors('logger'); $GENERATED_ACCESSOR{$class}{logger} = $TRUE; } if ( !$self->can('set_log_level') ) { $self->mk_accessors('log_level'); $GENERATED_ACCESSOR{$class}{log_level} = $TRUE; } return $self; } ######################################################################## sub new { ######################################################################## my ( $class, @params ) = @_; my $t0 = time; my %args = ref $params[0] ? %{ $params[0] } : @params; foreach my $o ( keys %args ) { die "ERROR: unknown option '$o'\n" if none { $o eq $_ } @VALID_OPTIONS; } my ( $default_options, $option_specs, $commands, $command_roles, $manifest_commands, $extra_options, $abbreviations, $error_handler, $alias, $validate_command, $help_sections ) = @args{ qw(default_options option_specs commands command_roles manifest_commands extra_options abbreviations error_handler alias validate_command help_sections) }; $validate_command //= $TRUE; no strict 'refs'; ## no critic my $stash = \%{ $class . $DOUBLE_COLON }; local *alias = *alias; ## no critic ProhibitLocalVars use vars qw($DEFAULT_OPTIONS $EXTRA_OPTIONS $OPTION_SPECS $COMMANDS $LOGGING); *DEFAULT_OPTIONS = $stash->{DEFAULT_OPTIONS} // $EMPTY; *EXTRA_OPTIONS = $stash->{EXTRA_OPTIONS} // $EMPTY; *OPTION_SPECS = $stash->{OPTION_SPECS} // $EMPTY; *COMMANDS = $stash->{COMMANDS} // $EMPTY; $default_options //= $DEFAULT_OPTIONS; $extra_options //= $EXTRA_OPTIONS; $option_specs //= $OPTION_SPECS; $commands //= $COMMANDS; $option_specs //= []; $commands //= {}; $manifest_commands //= {}; $command_roles //= {}; foreach my $command ( keys %{$manifest_commands}, keys %{$command_roles} ) { $commands->{$command} //= sub { die "not implemented\n" }; } croak sprintf "ERROR: 'commands' is required\nusage: %s->new( option_specs => specs, commands => commands);\n", __PACKAGE__ if !$option_specs || !$commands; my %command_target = map { $_ => $_ } keys %{$commands}; $default_options //= {}; # my $options = { %{$default_options} }; my $options = {}; if ( $class->_use_logger && none { $_ eq 'log-level' } @{$option_specs} ) { push @{$option_specs}, 'log-level=s'; } # if we have an option alias, make sure the option alias spec is set too if ( $alias && ref $alias && $alias->{options} ) { foreach my $p ( pairs %{ $alias->{options} } ) { my ( $aka, $name ) = @{$p}; # Does an option named $aka already exist (with or without a spec)? my $aka_exists = any {/^\Q$aka\E(?:[^\w-].*)?$/xsm} @{$option_specs}; if ( !$aka_exists ) { # Find the canonical spec for $name my ($spec) = grep {/^\Q$name\E(?:[^\w-].*)?$/xsm} @{$option_specs}; croak sprintf 'ERROR: no such option defined: %s', $name if !$spec; # Pull just the specifier part (e.g., '=s', ':i', '!', '+', etc.) my ($spec_part) = $spec =~ /^\Q$name\E(?:[|].)?([^\w-].*)?$/xsm; $spec_part ||= q{}; push @{$option_specs}, $aka . $spec_part; } } } if ( @ARGV && $ARGV[0] =~ /^-[[:alpha:]]/xsm ) { if ( my $handler = $INTERNAL_COMMANDS{ $ARGV[0] } ) { exit $handler->( $class, $commands, $option_specs ); } } $GETOPT_STATUS = sub { local $SIG{__WARN__} = sub { $GETOPT_ERROR_MESSAGE = shift; return; }; return GetOptions( $options, @{$option_specs} ); } ->(); normalize_options($options); my %cli_options; my @accessors = ( @{ $extra_options || [] }, map { ( split /[^\w\-]/xsm )[0] } @{$option_specs} ); foreach (@accessors) { die "ERROR: accessor must be a scalar\n" # somebody did @extra_options = [qw( a b c )] ??? if ref $_; s/\-/_/xsmg; # can() can't tell "a human hand-wrote this method" apart from # "mk_accessors already generated this on an earlier ->new() call # for this same class" -- both look identical. Only the latter is # fine (constructing multiple instances of the same class within # one process is completely normal); only the former is the # genuine naming collision this check exists to catch. Track what # this class has already generated, per class, so a repeat # construction doesn't get mistaken for a hand-written conflict. if ( !$GENERATED_ACCESSOR{$class}{$_} ) { die "ERROR: accessor for $_ already exists!\n" if $class->can( 'get_' . $_ ) || $class->can( 'set_' . $_ ); $class->mk_accessors($_); $GENERATED_ACCESSOR{$class}{$_} = $TRUE; } $cli_options{$_} = $options->{$_}; } if ( !$error_handler && !$GETOPT_STATUS ) { print {*STDERR} $GETOPT_ERROR_MESSAGE; if ($GETOPT_EXIT_ON_ERROR) { _leave($FAILURE); } } elsif ( !$GETOPT_STATUS ) { if ( !$error_handler->($GETOPT_ERROR_MESSAGE) ) { _leave($FAILURE); } } croak "ERROR: alias must be a hash ref with keys 'options' or 'commands'\n" if $alias && !ref $alias; if ($alias) { if ( $alias->{options} ) { foreach my $p ( pairs %{ $alias->{options} } ) { my ( $aka, $name ) = @{$p}; $aka =~ s/[-]/_/gxsm; $name =~ s/[-]/_/gxsm; if ( defined $cli_options{$name} ) { $cli_options{$aka} = $cli_options{$name}; } elsif ( defined $cli_options{$aka} ) { $cli_options{$name} = $cli_options{$aka}; } } } # command aliases are a convenience so someone doesn't have to add # to $commands manually if ( $alias->{commands} ) { foreach my $p ( pairs %{ $alias->{commands} } ) { my ( $aka, $name ) = @{$p}; croak sprintf "ERROR: no command: %s\n", $name if !$commands->{$name}; $commands->{$aka} = $commands->{$name}; $command_target{$aka} = $name; } } } my $self = $class->SUPER::new( \%cli_options ); $self->set__options($options); if ( !$self->can('get_help_sections') ) { $self->mk_accessors('help_sections'); $GENERATED_ACCESSOR{$class}{help_sections} = $TRUE; } else { $help_sections //= $self->get_help_sections; } $help_sections //= $default_options->{help_sections} // [@DEFAULT_HELP_SECTIONS]; $self->set_help_sections($help_sections); # AUTO_DEFAULT uses the only command as the default if ( $AUTO_DEFAULT && scalar keys %{$commands} == 1 ) { my ($command) = keys %{$commands}; if ( @ARGV && $ARGV[0] ne $command ) { unshift @ARGV, $command; } elsif ( !@ARGV ) { unshift @ARGV, $command; } } my $command = shift @ARGV // $EMPTY; if ( !$command ) { if ( $commands->{default} ) { $command = ref $commands->{default} ? 'default' : $commands->{default}; } elsif ($AUTO_HELP) { $command = 'help'; } } $self->set__validate_command($validate_command); $self->set__command($command); $self->set__command_args( [@ARGV] ); $self->set__commands($commands); $self->set__program("$RealBin/$RealScript"); if ( $command eq 'help' || ( $self->can('get_help') && $self->get_help ) ) { # custom help function? my $help = $commands->{help}; if ( !$help ) { $self->usage; } if ( ref $help && reftype($help) eq 'ARRAY' ) { $help->[0]->( $self, $TRUE ); } else { $help->( $self, $TRUE ); } } $self->set__abbreviations( $abbreviations // $FALSE ); $self->validate_command; $command = $self->command; if ($command) { my $target_command = $command_target{$command} // $command; if ( exists $command_roles->{$target_command} ) { my $roles = $command_roles->{$target_command}; require Role::Tiny; Role::Tiny->apply_roles_to_package( $class, @{$roles} ); ( my $method = "cmd_$target_command" ) =~ s/-/_/gxsm; die sprintf "ERROR: roles for command '%s' do not implement %s\n", $target_command, $method if !$class->can($method); my $handler = $class->can($method); $commands->{$target_command} = $handler; $commands->{$command} = $handler; } elsif ( exists $manifest_commands->{$target_command} ) { # # Legacy semantics: selecting ONE legacy command opts into # composition of the entire legacy role set. # my %seen; my @roles = grep { !$seen{$_}++ && _is_class_name($_) } values %{$manifest_commands}; require Role::Tiny; Role::Tiny->apply_roles_to_package( $class, @roles ); foreach my $cmd ( keys %{$manifest_commands} ) { my $value = $manifest_commands->{$cmd}; my $method = choose { return $value if !_is_class_name($value); ( my $m = "cmd_$cmd" ) =~ s/-/_/gxsm; return $m; }; die sprintf "ERROR: %s does not implement %s\n", $value, $method if !$class->can($method); $commands->{$cmd} = $class->can($method); } # # Restore aliases now that the placeholder handlers have # been replaced by the real legacy dispatch handlers. # foreach my $aka ( keys %command_target ) { my $target = $command_target{$aka}; next if $aka eq $target; $commands->{$aka} = $commands->{$target} if exists $commands->{$target}; } } } $self->init_logger; my %resolved = map { $_ => 1 } keys %{$options}; $self->resolve_option_providers( \%resolved, \@accessors ); $self->init_defaults( $default_options, \%resolved ); $self->can('init') && $self->init(); return $self; } ######################################################################## sub options { ######################################################################## my ($self) = @_; return $self->get__options; } ######################################################################## sub resolve_option_providers { ######################################################################## my ( $self, $resolved, $eligible_names ) = @_; my $class = ref $self || $self; $resolved //= {}; my %eligible = map { $_ => 1 } @{ $eligible_names // [] }; foreach my $name ( @{ $OPTION_PROVIDER_ORDER{$class} // [] } ) { my $provider = $OPTION_PROVIDERS{$class}{$name}; my @requested = @{ $provider->{options} }; if ( !@requested ) { @requested = keys %eligible; } my %seen; my @unresolved = grep { !$seen{$_}++ && exists $eligible{$_} && !exists $resolved->{$_} } @requested; next if !@unresolved; my $init = q{_} . $name . q{_init}; my $resolver = q{_} . $name . q{_resolve}; croak sprintf q{Option provider '%s' has no resolver '%s'}, $name, $resolver if !$self->can($resolver); if ( $self->can($init) ) { $self->$init(); } my $values = $self->$resolver(@unresolved); croak sprintf q{Option provider '%s' must return a hash reference}, $name if !ref $values || reftype($values) ne 'HASH'; my %allowed = map { $_ => 1 } @unresolved; foreach my $option ( keys %{$values} ) { croak sprintf q{Option provider '%s' returned unrequested option '%s'}, $name, $option if !exists $allowed{$option}; $self->set( $option, $values->{$option} ); $resolved->{$option} = 1; } } return $self; } ######################################################################## sub init_defaults { ######################################################################## my ( $self, $default_options, $resolved ) = @_; $resolved //= {}; foreach my $o ( keys %{$default_options} ) { next if !$self->can( 'set_' . $o ); next if exists $resolved->{$o}; next if defined $self->get($o); $self->set( $o, $default_options->{$o} ); } return $self; } ######################################################################## sub _pod_has_section { ######################################################################## my ( $filename, $section ) = @_; open my $fh, '<', $filename or return $FALSE; while ( my $line = <$fh> ) { return $TRUE if $line =~ /\A=head1\s+\Q$section\E\s*\z/xsm; } return $FALSE; } ######################################################################## sub _get_help_sections { ######################################################################## my ( $self, $input ) = @_; my $sections = $self->get_help_sections; return $sections if @{$sections} != @DEFAULT_HELP_SECTIONS; for my $idx ( 0 .. $#DEFAULT_HELP_SECTIONS ) { return $sections if $sections->[$idx] ne $DEFAULT_HELP_SECTIONS[$idx]; } return [ _pod_has_section( $input, 'USAGE' ) ? 'USAGE' : 'SYNOPSIS', 'DESCRIPTION/Commands', 'DESCRIPTION/Options', 'OPTIONS', ]; } ######################################################################## sub _leave { ######################################################################## my (@args) = @_; my ($exit_code) = ref $args[0] ? $args[1] : $args[0]; exit $exit_code; } ######################################################################## sub _set_color_config { ######################################################################## my ( $self, %args ) = @_; return sprintf $LOG4PERL_CONF, $args{debug_color} // $LOG4PERL_COLOR_DEBUG, $args{info_color} // $LOG4PERL_COLOR_INFO, $args{warn_color} // $LOG4PERL_COLOR_WARN, $args{error_color} // $LOG4PERL_COLOR_ERROR, $args{fatal_color} // $LOG4PERL_COLOR_FATAL, $args{trace_color} // $LOG4PERL_COLOR_TRACE; } ######################################################################## sub init_logger { ######################################################################## my ($self) = @_; if ( $self->_use_logger && $self->can('get_log4perl_conf') ) { my $config = $self->get_log4perl_conf; my $color = $self->can('get_color') && defined $self->get_color ? $self->get_color : $config && $config eq $LOG4PERL_CONF; $color = $color && eval { require Term::ANSIColor; 1; }; # color on, but not done through use_log4perl? if ( $color && !$config ) { $config = $self->_set_color_config; } elsif ( !$color && $config && $config =~ /screencoloredlevels/xsmi ) { $config = q{}; } $self->set_log4perl_conf($config); if ($config) { Log::Log4perl->init( ref $config ? $config : \$config ); } else { Log::Log4perl->easy_init( $LOG_LEVELS{error} ); } my $logger = Log::Log4perl->get_logger(q{}); $self->set_logger($logger); } return $self if !$self->_use_logger || !$self->can('get_logger') || !$self->get_logger; my $commands = $self->commands; my $command = $self->command; my $command_level; if ( $command && ref( $commands->{$command} ) && reftype( $commands->{$command} ) eq 'ARRAY' ) { ( undef, $command_level ) = @{ $commands->{$command} }; } my $level = $self->get_log_level // $command_level // $self->get_log4perl_level; $self->set_log_level($level); $self->get_logger->level( $LOG_LEVELS{$level} // $LOG_LEVELS{info} ); return $self; } ######################################################################## sub get_kv_args { ######################################################################## my ($self) = @_; my @arg_list = @{ $self->get__command_args }; my %args; foreach (@arg_list) { my ( $k, $v ) = split /=/xsm; $args{$k} = $v; } return %args; } ######################################################################## sub set_args { ######################################################################## my ( $self, $args ) = @_; $self->set__command_args($args); return $args; } ######################################################################## sub get_args { ######################################################################## my ( $self, @vars ) = @_; my $command_args = $self->get__command_args; return wantarray ? @{$command_args} : $command_args if !@vars; @vars = map { $_ ? $_ : '' } @vars; my %args; @args{@vars} = @{$command_args}[ 0 .. $#vars ]; delete $args{''}; return wantarray ? %args : \%args; } ######################################################################## sub default_command { ######################################################################## goto &usage; } ######################################################################## sub usage { ######################################################################## my ($self) = @_; require Pod::Usage; my $wrapper = $ENV{MODULINO_WRAPPER} // q{}; my $input = $wrapper eq 'cli-simple' ? $INC{'CLI/Simple/Shell.pm'} : $self->get__program; if ( $wrapper && $wrapper ne 'cli-simple' && $ENV{MODULINO_MODULE_NAME} ) { ( my $module_file = $ENV{MODULINO_MODULE_NAME} ) =~ s{::}{/}gxsm; $input = $INC{"$module_file.pm"}; } if ( $PAGER && IO::Interactive::is_interactive ) { eval { require IO::Pager; IO::Pager->new(*STDOUT); }; } my $sections = $self->_get_help_sections($input); Pod::Usage::pod2usage( -noperldoc => 1, -exitval => 'NOEXIT', -input => $input, -verbose => 99, -sections => $sections, ); return _leave($FAILURE); } ######################################################################## sub example { ######################################################################## require File::ShareDir; print {*STDOUT} slurp File::ShareDir::dist_file( 'CLI-Simple', 'MyScript.pm' ); return 0; } ######################################################################## sub command { ######################################################################## my ( $self, $command ) = @_; if ($command) { $self->set__command($command); } return $self->get__command; } ######################################################################## sub command_args { ######################################################################## my ( $self, @args ) = @_; return $self->get__command_args if !@args; if ( ref $args[0] ) { $self->set__command_args( $args[0] ); return; } $self->set__command_args( [@args] ); return; } ######################################################################## sub commands { ######################################################################## my ( $self, $command, $handler ) = @_; my $commands = $self->get__commands; if ( $command && $handler ) { croak "ERROR: usage: commands([command, subref])\n" if reftype($handler) ne 'CODE'; $commands->{$command} = $handler; } return $commands; } ######################################################################## sub program { ######################################################################## my ($self) = @_; return $self->get__program; } ######################################################################## sub validate_command { ######################################################################## my ($self) = @_; my $command = $self->command; return if !$command || !$self->get__validate_command; my $commands = $self->commands; return $command if defined $commands->{$command}; croak sprintf "Unknown command: %s\n", $command if !$self->get__abbreviations; my $abbreviation = $command; my @matches = grep {/^$abbreviation/xsm} keys %{$commands}; croak sprintf "Unknown command: %s\n", $command if !@matches; if ( @matches == 1 ) { $command = $matches[0]; # Unique match - accept $self->set__command($command); return $command; } croak sprintf "Ambiguous command '$abbreviation'; could match: %s\n", join q{,}, @matches; } ######################################################################## sub _cmd_generate_completion { ######################################################################## my ( $class, $commands, $option_specs ) = @_; no strict 'refs'; ## no critic my $stash = \%{ $class . '::' }; my $cmd_list = join q{ }, sort grep { !/^-/xsm } keys %{$commands}; my $program = $ENV{MODULINO_WRAPPER} // do { ( my $name = $class ) =~ s/::/-/gxsm; lc $name; }; my @flags; my @value_opts; for my $spec ( @{$option_specs} ) { my ($name) = $spec =~ /\A([\w-]+)/xsm; $name =~ s/_/-/gxsm; if ( $spec =~ /[=:]/xsm ) { push @value_opts, "--$name"; } else { push @flags, "--$name"; } } my $flags = join q{ }, sort @flags; my $value_opts = join q{ }, sort @value_opts; printf {*STDOUT} <<'END_COMPLETION', $program, $cmd_list, $value_opts, $flags, $program, $program; _%s() { local cur prev words cword _init_completion || return local commands="%s" local value_opts="%s" local flags="%s" if [[ $cword -eq 1 ]]; then if [[ "$cur" == --* ]]; then COMPREPLY=( $(compgen -W "$flags $value_opts" -- "$cur") ) else COMPREPLY=( $(compgen -W "$commands" -- "$cur") ) fi return fi case $prev in $value_opts) COMPREPLY=( $(compgen -f -- "$cur") ) return ;; esac COMPREPLY=( $(compgen -W "$flags $value_opts" -- "$cur") ) } complete -F _%s %s END_COMPLETION return $SUCCESS; } ######################################################################## sub run { ######################################################################## my ($self) = @_; my $command = $self->command; ###################################################################### # a blank command means that we did not have a default && AUTO_HELP # is off scripter has deliberately decided to allow running of # init() phase w/o a run phase ###################################################################### return $SUCCESS if !$command; my $commands = $self->commands; my $program = $self->program; my $handler = $commands->{$command}; if ( $handler && !ref $handler ) { $handler = $commands->{$handler}; # default? } die "ERROR: no such command '$command' has been registered.\n" if !$handler; if ( ref $handler ne 'ARRAY' ) { my $result = $handler->($self); return $result; } my ( $sub, $log_level ) = @{$handler}; croak "ERROR: invalid specification for $command\n" if !ref $sub || reftype($sub) ne 'CODE'; return $sub->($self); } 1; ## no critic (RequirePodSections) __END__ =pod =encoding utf8 =head1 NAME CLI::Simple - a minimalist object oriented base class for CLI applications =head1 SYNOPSIS #!/usr/bin/env perl package MyScript; use strict; use warnings; use parent qw(CLI::Simple); caller or exit __PACKAGE__->main(); sub main { 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 => \&cmd_execute, list => \&cmd_list, }, alias => { options => { fmt => 'format' }, commands => { ls => 'list' } }, ); return $cli->run(); } sub cmd_execute { my ($self) = @_; # retrieve a CLI option my $file = $self->get_file; ... return 0; } sub cmd_list { my ($self) = @_; # retrieve a command argument my ($file) = $self->get_args(); ... return 0; } 1; # role-based CLI Application (2.0.0) # create a YAML manifest C in your project root: --- roles: frobnicate: My::Script::Role::Frobnicate list: My::Script::Role::List options: - help|h - verbose|v - output|o=s # 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; # 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; =head1 DESCRIPTION =begin markdown [![CLI-Simple](https://github.com/rlauer6/CLI-Simple/actions/workflows/build.yml/badge.svg)](https://github.com/rlauer6/CLI-Simple/actions/workflows/build.yml) =end markdown 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? C makes it easy to create scripts that take I, I and I. C is designed around the I pattern - Perl modules that can be executed directly as scripts. See L. For common constant values (like C<$TRUE>, C<$DASH>, or C<$SUCCESS>), see L, which pairs naturally with this module. 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 L role, and C 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 C<-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. When you are ready to scaffold a full role-based project, C<-scaffold> generates role stubs, a slimmed main module, and inter-module dependencies from your manifest. Feed the resulting tarball to L and you have a complete, buildable CPAN distribution in one step. Version 2.3.0 adds selective role composition. Commands declared with the C manifest key compose only the roles required by the selected command. The original C form remains supported for backward compatibility and retains its original behavior of composing the complete set of command roles. Version 2.4.0 introduces pluggable option providers. CLI applications frequently obtain option values from multiple sources, including the command line, environment variables, configuration files, and credential stores. Traditionally, each application must implement its own logic for retrieving these values and determining their precedence, often duplicating much of the same code in its C method. Option providers move that responsibility into reusable L roles. Applications register the providers they need, in precedence order, and C coordinates resolution of options not supplied on the command line. This eliminates repetitive initialization logic while leaving each provider responsible for how and where it obtains its values. =head1 VERSION This documentation refers to version 2.4.0. =head1 FEATURES =over 5 =item * accept command line arguments ala L =item * supports commands and command arguments =item * automatically add a logger =item * global or custom log levels per command =item * easily add usage notes =item * automatically create setter/getters for your script =item * low dependency profile =item * selective role composition through YAML command manifests =item * built-in scaffolding tools for migrating legacy scripts to roles =item * bash completion script generation for modulino wrappers =item * optional pager support for help output via L =item * customizable help sections via C =item * pluggable option providers with ordered resolution and precedence =back =head1 QUICK START =head2 Single-Module Application The simplest way to use C 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; =head2 Role-Based Application For larger applications, declare your commands and options in a YAML manifest and implement each command in a dedicated L 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; B The YAML manifest filename is derived from your module name - C looks for C in the distribution share directory. You must package the spec file with your distribution. The manifest maps commands to roles: --- roles: 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, C provides a built-in C<-scaffold> tool. See L 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 L. =head1 MODULINOS A I 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 Cd or Cd by another module, C returns the calling package and the expression short-circuits - C is never called. When the file is executed directly by Perl, C returns false and C runs. The same file serves as both a reusable module and an executable script. C is designed around this pattern. Every C application is expected to be a modulino. The framework's lifecycle, internal commands, bash completion, and scaffolding tools all assume this dual-use design. =head2 Why Modulinos? The modulino pattern offers several advantages over a traditional script: =over 4 =item * B - your script logic lives in a proper Perl module that can be Cd in test files without executing C =item * B - other scripts and modules can C your modulino and call its methods directly =item * B - tools like C<-dump-spec> and C<-generate-completion> can load your modulino and inspect its live state without running it as a script =item * B - modulinos distribute cleanly as CPAN modules with full man page support via L =back =head2 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 C<.pm> file via C<%INC> and sets C in the environment so C knows the name of the script the user actually typed. This is used by C<-generate-completion> to name the bash completion function correctly and by L to create man page symlinks. =head2 create-modulino C ships with a C tool that generates the bash wrapper for any C 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 C is itself a modulino - an example of the pattern it creates. The bash wrapper template lives in its C<__DATA__> section, keeping the tool entirely self-contained. If you are building a CPAN distribution, L integrates C into the C target, generating and installing the wrapper as part of the build process. =head2 MODULINO_WRAPPER The C environment variable tells C the name of the wrapper script that invoked the modulino. It is set by the wrapper and used by: =over 4 =item * C<-generate-completion> - to name the bash completion function and C target correctly =item * Man page symlinks via L - so C resolves to the module's man page =back If C is not set, C infers the script name from the module name by convention - C becomes C. Set it explicitly when the wrapper name does not follow this convention. =head1 ROLE-BASED ARCHITECTURE C 2.0.0 introduced an optional role-based architecture for applications that have grown beyond a single module. Commands may be implemented in dedicated L roles and declared in a YAML manifest, allowing C to build the dispatch table and provide an inherited C - potentially reducing your main module to a single declaration. Version 2.3.0 adds selective role composition through the C manifest key. When C is used, only the role or roles required by the selected command are composed. The original C manifest form remains supported for backward compatibility and retains its original behavior of composing the complete set of command roles. =head2 The YAML Manifest By default, C locates the application's manifest via L using the distribution name derived from the module name. The manifest must be installed as part of the distribution. Additional YAML manifests may be supplied as arguments to C. These may be explicit filenames or glob patterns, allowing applications to discover command registrations from project-local configuration files. I for selective role composition. The original C form remains supported for backward compatibility. See L.> A minimal manifest: --- roles: frobnicate: My::Script::Role::Frobnicate list: My::Script::Role::List options: - help|h - verbose|v - output|o=s A manifest defining commands, options, defaults, and additional accessors: --- roles: frobnicate: My::Script::Role::Frobnicate list: My::Script::Role::List options: - help|h - verbose|v - output|o=s default_options: verbose: 0 extra_options: - dbh - config_data Manifest section names use underscores, not hyphens. For example, C and C are valid section names; C and C are not. Unknown manifest keys and values that do not match the expected section types cause an error when the manifest is loaded. =head2 Loading Multiple Manifests C supports loading multiple YAML manifests during import. This allows an application to extend its commands, roles, and options without modifying its primary manifest. Each additional manifest may contribute command definitions, option specifications, default values, and additional accessors. Additional manifests may be specified as filenames: use CLI::Simple qw( :roles extensions/publish.yml extensions/deploy.yml ); Glob patterns are also supported: use CLI::Simple qw( :roles .roles/*/role.yml ); When C<:roles> is specified, C first attempts to load the application's installed manifest through L. Additional manifests are then loaded in the order supplied. All manifests contribute C, C, and C to the application's global option namespace. Duplicate option names (including aliases) and duplicate default keys are errors. Repeated C declarations are harmless, and an accessor may appear in both C and C. The first manifest alone supplies application-wide C and C metadata. Subsequent manifests also contribute C and C entries. Command names must be unique across all manifests. An attempt to register a command more than once causes an error. An unmatched glob pattern contributes no manifests and does not cause an error. An explicitly named YAML file that cannot be loaded causes an error. Explicit YAML filenames may also be supplied without C<:roles>: use CLI::Simple qw( config/application.yml .roles/*/role.yml ); In this case, the first explicitly supplied manifest provides the application-wide configuration. Loading a manifest does not compose its roles. For commands declared under C, only the roles associated with the selected command are composed during application startup. See L for the distinction between selective and legacy role composition. =head2 Option Initialization Option specifications and defaults from all manifests are registered globally, but only the B for the selected command are composed. Because option specifications and defaults are merged from all manifests, they are available regardless of which command is selected. Manifest defaults are equivalent to values supplied through the C argument to C. Applications should use C when option values require additional initialization or resolution after command-line parsing. Manifest defaults are applied before C, but after command-line options have been parsed. Applications may inspect C to determine which options were explicitly supplied on the command line and override default values during C. =head2 Command Values Entries beneath C map a command name to either a single role or a list of roles required by that command. For example: roles: frobnicate: My::Script::Role::Frobnicate publish: - My::Script::Role::Publish - My::Script::Role::Packages When C is selected, only C is composed into the application. When C is selected, both C and C are composed. The selected role set must provide the command method corresponding to the command name. Hyphens are converted to underscores when resolving the method name, so: code-review resolves to: cmd_code_review The original C form remains supported for backward compatibility. Its values may be role class names or method names and retain the pre-2.3.0 behavior described in L. =head2 C vs C Version 2.3.0 introduces C for selective role composition. New applications should use C. The original C manifest key remains supported for backward compatibility. With C, selecting a command causes all roles referenced by the loaded manifests' command definitions to be composed into the application, regardless of which command is being executed. This makes methods from every composed role available throughout the application. However, it can also load modules that are not needed by the selected command, increasing startup time. For applications invoked repeatedly, such as utilities called from C recipes, this additional startup overhead can become significant. With C, only the roles associated with the selected command are composed. A command may require one role or several roles. Selective composition reduces unnecessary dependencies and makes the role requirements of each command explicit. =head2 When to Use Each Approach C supports several approaches to organizing command-line applications. The appropriate choice depends on the size of the application and how its commands share functionality. =over 4 =item * Single-module application For small utilities with a limited number of commands, defining command handlers in a single module is often the simplest approach. Commands are registered directly through the C constructor parameter. No YAML manifest or role composition is required. =item * Legacy C manifest Existing applications using a YAML manifest with C retain the original behavior of composing all command roles together. This approach may be appropriate for applications whose commands depend on methods supplied by other command roles. However, every role class referenced by the manifest's command definitions is composed regardless of which command is selected. =item * Selective C manifest For new or growing applications, C provides a more modular approach. Each command declares the roles it requires, and only those roles are composed when the command is selected. This is particularly useful when commands have different dependencies or when minimizing startup time is important. =back Applications can migrate from C to C incrementally, provided each migrated command declares the roles it requires. =head2 Sharing Methods Between Commands With legacy C manifests, all command roles are composed into the application. Methods provided by one command role are therefore available to other commands. With selective C composition, a command cannot assume that roles associated with other commands have been composed. Functionality shared by multiple commands can be placed in a separate role and included in each command's role list: roles: publish: - My::Script::Role::Publish - My::Script::Role::Common deploy: - My::Script::Role::Deploy - My::Script::Role::Common Alternatively, functionality required by every command can be composed directly into the main application class using L. Shared functionality can also be implemented in ordinary Perl modules without using roles. See L for an example of composing an application-wide role. =head2 Roles With No Commands Some roles provide application-wide behavior rather than implementing a command. For example, a role may provide an C method for startup validation or other functionality required by every command. Because these roles are not associated with a command beneath C, they must be composed explicitly into the 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; Roles composed this way are always available to the application, regardless of which command is selected. =head2 Activating Role-Based Architecture Role-based applications load their YAML manifests through C. To load the application's installed manifest, specify C<:roles>: use CLI::Simple qw(:roles); C locates the manifest through L and retains its command, role, option, alias, and abbreviation metadata for the application. Alternatively, specify an explicit YAML manifest: use CLI::Simple qw(config/application.yml); Both forms may be combined with additional YAML manifests or glob patterns. See L. Role composition does not occur while manifests are being loaded. When the application starts, C resolves the selected command, including aliases and abbreviations, before composing the required roles. For commands declared beneath C, only the associated role set is composed. For legacy commands declared beneath C, the original all-role composition behavior is retained for backward compatibility. =head2 The Inherited main() When using a YAML manifest, your class can inherit C from C: caller or exit __PACKAGE__->main; The inherited C uses the manifest metadata to resolve the requested command, composes the role or roles required for that command, constructs the application object, and calls C. Aliases and command abbreviations are resolved before selective role composition, so they select the same role set as the canonical command. Override C in your subclass only if you need application startup behavior that cannot be expressed through the manifest, C, or explicitly composed application roles. =head2 Distributing the Manifest When using C<:roles>, the application's primary YAML manifest must be installed in the distribution's share directory. C users can add it to C in F: extra-files: - share: - my-script.yml Additional manifests supplied as import arguments are loaded from the filesystem. They do not need to be installed in the distribution's share directory. For example: use CLI::Simple qw( :roles .roles/*/role.yml ); This allows applications to maintain additional commands, roles, and options independently of their primary manifest. For example, an application may load optional extensions from F<.roles/>. Each extension can provide its own manifest and implementation without modifying the application's primary manifest. Extensions are available only when their manifests are present and loaded. See L for manifest precedence and merging behavior. =head1 PHILOSOPHY AND DESIGN PRINCIPLES Getting started with C is intentionally simple. It provides a structured, convention-based approach to building command-line applications, including option parsing, command dispatch, defaults, logging, and help handling, without requiring developers to adopt its optional role-based framework. For a single-module application, you register commands directly using the C constructor parameter. Command handlers can be implemented as methods or subroutines, allowing you to organize application logic according to your own conventions. For applications that need greater modularity, C also provides an optional role-based framework. YAML manifests declare commands, options, and the roles required to implement them. The framework manages manifest loading and role composition, including selective composition of the roles required by a particular command. You can use C without adopting the role-based framework. The philosophy is: provide just enough infrastructure, then get out of your way. =head2 Validation, Defaults, and Configuration C does not impose a validation model. You may: =over 4 =item * Use C option specifications for argument types and C to supply default values =item * Write your own validation logic in C =item * Throw exceptions, emit usage, or exit early at any point =back The lifecycle is explicit and under your control. You decide how much structure you want to add on top of it. =head2 When to Use C is ideal for: =over 4 =item * Internal tools and admin scripts =item * Bootstrapped CLIs where you don't want a framework =item * Users who want to subclass a clean, minimal interface =item * Applications that have grown beyond a single module and benefit from role-based command composition =back For interactive CLI handling or complex command trees, consider L or L. =head2 The init-run Lifecycle =over 4 =item * B For role-based applications, YAML manifests are loaded during C. The application's installed manifest may be loaded through C<:roles>, and additional manifests may be supplied as filenames or glob patterns. The manifests are merged and normalized before their command, role, option, alias, and abbreviation metadata is retained for the application. Roles are not composed during manifest loading. The selected command is resolved later, during application startup. For commands declared beneath C, only the role or roles required by that command are composed. Legacy C manifests retain the original all-role composition behavior. Single-module applications skip this phase entirely. =item * B Before anything else, C checks C<@ARGV> for internal commands prefixed with C<->. If one is found it executes immediately and exits. See L. =item * B)> The constructor parses command-line arguments using L, normalizes option names, and creates accessors for the declared options. After completing command and logger initialization, C resolves any remaining options through registered option providers, applies application defaults, and invokes the application's C method, if defined. The order of initialization is significant. Command-line values take precedence over provider-supplied values, providers are consulted in registration order, and defaults are applied only to options that remain unresolved. By the time C is called, all available option values have been assigned to their respective accessors. The C method remains available for application-specific initialization, but applications no longer need to implement their own option-source resolution and precedence logic when using option providers. See L for details. =over 4 =item * Validating command-line arguments. =item * Loading configuration files based on a C<--config> option. =item * Dynamically overriding the command (e.g., C<$self-Ecommand('new_default')>). =item * Overriding default options =item * Performing any setup required B a command is run. =back =item * B)> Dispatches to the command method determined during initialization. =back =head2 Opt-in Default Command By design, C B. This provides total flexibility for the application author: =over 4 =item * B If your application needs a default command, define a C entry in the C hash passed to the constructor, or set the command during C using C. Alternatively, enable C<$AUTO_HELP> to display help when no command is supplied. =item * B If you do B set a default, C will simply do nothing and return cleanly if no command is provided on the command line. =back This "no default by default" behavior is what enables a powerful "setup-only" execution mode. A user can run your script I specifying a command. This will: =over 4 =item 1. Run the entire C / C phase, performing all setup. =item 2. Call C, which will find no command and exit cleanly. =back 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 C, without also triggering an unwanted command. In applications using a YAML manifest, a default command can be declared using C. For applications using selective C composition, the default command can be associated with a role directly: roles: default: My::Module::Role::Installer =head2 C<$AUTO_HELP> and C<$AUTO_DEFAULT> The following package variables control automatic command selection, help behavior, and output paging. By default, the C 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. =over 4 =item C<$AUTO_HELP> Set the package variable C<$AUTO_HELP> to a true value if you want C to provide help when no command is provided. default: false =item C<$AUTO_DEFAULT> Set the package variable C<$AUTO_DEFAULT> to a true value if you want C to automatically select a command when exactly one command is registered and no command is provided on the command line. default: false =item C<$PAGER> Set the package variable C<$PAGER> to a true value to route help output through L when C<--help> is invoked. When enabled, C selects an appropriate pager (C, C, etc.) based on the C 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: L must be installed for pager support. If it is not available, help output is written directly to STDOUT regardless of the value of C<$PAGER>. =back =head1 CONSTANTS C provides a collection of exportable constants commonly used in command-line applications. These include: =over 4 =item * Boolean flags like C<$TRUE>, C<$FALSE>, C<$SUCCESS>, and C<$FAILURE> =item * Common character tokens such as C<$COLON>, C<$DASH>, C<$EQUALS_SIGN>, etc. =item * Log level names compatible with L =back To use them in your script: use CLI::Simple::Constants qw(:all); =head1 ADDING USAGE TO YOUR SCRIPTS To provide built-in usage/help output, include a C<=head1 SYNOPSIS> or C<=head1 USAGE> section in your script's POD: =head1 SYNOPSIS ``` usage: myscript [options] command args Options ------- --help, -h Display help ... ``` If the user supplies the command C, or the C<--help> option, C displays the configured help sections using L. For backward compatibility, C is also supported. If a C section is present, it is used as the usage section. If no C section is present, C is used instead. When both C and C are present, C is used by default. Applications that explicitly configure C may select the desired section. =head2 Customizing Help Output =head3 Custom help() Method If you need full control over the help output, you can define a custom C 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. =head3 C By default, C renders the following POD sections when present: USAGE (or SYNOPSIS if USAGE is absent) DESCRIPTION/Commands DESCRIPTION/Options OPTIONS You can override the default selection by passing an array reference of section names during construction: my $cli = CLI::Simple->new( help_sections => [qw(SYNOPSIS COMMANDS OPTIONS)], ... ); When C differs from the default selection, C honors the list exactly as provided. help_sections => [qw(SYNOPSIS USAGE)] Section names follow L conventions. Subsections are specified with a C separator; for example, C renders only the C subsection under C. =head1 INTERNAL COMMANDS C reserves command names beginning with C<-> for its own use. These commands are intercepted before option parsing begins and execute immediately, bypassing the normal lifecycle entirely. See L. =head2 -generate-completion Generates a bash completion script for the script's commands and options, derived from the live object state. Bash completions are a feature that allows the shell to automatically finish commands, file paths, and options when you press the Tab key. my-script -generate-completion > \ ~/.local/share/bash-completion/completions/my-script After generating the bash completion script, source it in your current shell to test: source ~/.local/share/bash-completion/completions/my-script Test by typing your script name followed by a space and pressing Tab. You should see the available commands. To verify option completion, type your script name followed by a space and C<--> and press Tab. To make completions permanent, most systems automatically source files placed in C<~/.local/share/bash-completion/completions/> when C 2.x is installed. If your system does not pick them up automatically, add the following to your C<~/.bashrc>: source ~/.local/share/bash-completion/completions/my-script Alternatively, place the generated file in the system-wide completion directory (requires root): my-script -generate-completion > \ /etc/bash_completion.d/my-script The script name is taken from C when set, otherwise it is inferred from the module name. I tool C is already set inside the bash script that invokes the modulino.> =over 4 =item Case 1: Your modulino wrapper and module name are aligned The modulino script C refers to My::Modulino my-modulino -generate-completion =item Case 2: Your modulino wrapper was created using C The modulino script C refers to My::Modulino. Although the wrapper name differs from the module name, C is set by the generated bash wrapper. my-alias -generate-completion =back =head2 -dump-spec 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 C argument, commands map to their existing sub names so the manifest can be used immediately without moving any code. With C, commands map to derived role class names suitable for use with C<-scaffold>. Alias commands - those whose coderef resolves to a sub name that does not match the command key - are always written as sub names regardless of mode. =head2 -scaffold Generates a role-based project tarball from the running modulino or from an explicit spec file. The C<-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 C 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 C if you have a .yml file. cli-simple -scaffold my-script.yml # scaffold from spec file The generated tarball is named from the distribution identifier derived from the application class or supplied specification file, with a C<-roles.tar.gz> suffix. The name is used to infer the class name. 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. If you feed the tarball to L via the C command you can produce a complete buildable CPAN distribution. =head2 -migrate Combines C<-dump-spec roles> and C<-scaffold> in a single step. my-script -migrate Writes the YAML manifest then generates the role-based tarball. Use this when you are ready for a full migration and do not need to inspect or edit the manifest first. If you want to review or adjust the manifest before scaffolding, run C<-dump-spec> and C<-scaffold> separately. =head1 METHODS AND SUBROUTINES =head2 use_log4perl __PACKAGE__->use_log4perl( log_level => 'info', color => 1, ); Enables L integration for the application class. C initializes logging during object construction, before invoking the application's C method. See L for configuration and usage details. =head2 use_options __PACKAGE__->use_options( provider_name => { 'Provider::Role' => [qw(option1 option2)], }, ); Registers one or more option providers with the application class. Each provider is identified by a name and implemented as a L role, which C composes into the application class. Providers are registered in the order supplied. This order determines their precedence when multiple providers can resolve the same option. The array reference specifies which application options the provider may resolve. An empty array reference allows the provider to resolve any eligible application option declared through C or C. The provider must implement a resolver method named C<_provider_name_resolve()> and may optionally implement C<_provider_name_init()>. Returns the application class. See L for the complete provider contract, examples, and resolution rules. =head2 new new( args ) Instantiates a new C instance, parses command-line options, creates the corresponding accessors, and optionally initializes logging. Registered option providers are then consulted, application defaults are applied, and the application's C method is invoked, if defined. See L for details on option resolution and precedence. I constructor uses L's C, which directly modifies C<@ARGV> by removing any recognized options. The remaining elements of C<@ARGV> are treated as the command name and its arguments.> C is a hash or hash reference containing the following keys: =over 4 =item * abbreviations A boolean that determines whether abbreviated command names are allowed. When true, C treats the provided command as a prefix and compares it to the keys in the command hash. This allows for convenient shorthand like: mytool disable-sched # expands to 'disable-scheduled-task' default: false =item * commands (required) A hash mapping command names to either a subroutine reference or an array reference. If an array reference is used, the first element must be a subroutine reference and the second should be a valid log level. (See L.) Example: { send => \&send_message, receive => \&receive_message, list_messages => [ \&list_messages, 'error' ], } If your script does not use command names, you may set a C key to the subroutine or method to run: { default => \&main } If no default is provided, the behavior is controlled by the C<$AUTO_DEFAULT> and C<$AUTO_HELP> package variables. Setting C<$AUTO_DEFAULT> to true when your C hash contains only a single command, will cause that command to be run automatically when no command name is given on the command line. This allows you to treat the program like a single-command tool, where arguments can be passed directly without explicitly naming the command. =item * default_options (optional) A hash reference providing default values for options. These values are applied only when the corresponding option has not been resolved by the command line or a registered option provider. =item * extra_options (optional) An array reference of names for additional accessors you want to create, even if they are not part of C. Example: extra_options => [ qw(foo bar baz) ] =item * option_specs (optional) An array reference of option specifications, as accepted by L. These define the command-line options your program recognizes. =item * validate_command By default, C validates the selected command against the registered commands. Set C to a false value to disable this validation. Typically you might use this to allow a script to assume a default command and allow arguments. For example suppose you have a script C with a command "get" with arguments: foo get something ...but want to allow users to also do: foo something To do this you should follow this recipe: sub init { my ($self) = @_; my $command = $self->command; if ( !exists $self->commands->{$command} ) { $self->command_args($command, $self->get_args); $self->command('get'); } return; } =back =head2 command command command(command) Gets or sets the command to execute. Usually this is the first argument on the command line after all options have been parsed. There are times when you might want to override the argument. You can pass a new command that will be executed when you call the C method. =head2 command_args my $args = $self->command_args(); Gets or sets the argument list. Similar to C when no arguments are passed except it returns an array reference. To replace the argument list, pass an array reference or a list. my $args = $self->command_args; $self->command_args(@{$args}, 'foo'); =head2 commands commands commands(command, handler) Returns the command dispatch hash supplied to the constructor. When called with a command name and handler, adds the command to the dispatch hash. C must be a code reference. commands(foo => sub { return 'foo' }); =head2 main __PACKAGE__->main; For role-based applications, C
is inherited from C and uses the manifest configuration assembled during C. It constructs the application with its options, defaults, and registered commands, then calls C. In a role-based modulino, the entire C
sub reduces to: caller or exit __PACKAGE__->main; For single-module applications, override C
in your subclass as usual. =head2 options Returns a hash reference containing the command-line options parsed by C. Option names are normalized to snake_case. The hash contains only options explicitly supplied on the command line. It does not include values supplied by C or subsequently initialized by C. Applications can use this hash to distinguish command-line options from default values when resolving configuration. =head2 run Executes the selected command using the parsed options and arguments. The C method dispatches control to the corresponding command subroutine. Command subroutines should return C<0> for success and a non-zero value for failure. C returns the command subroutine's return value, which can be used as the script's exit status. =head2 get_args Return the arguments that follow the command. get_args(NAME, ... ) # with names get_args() # raw positional args =head3 With names =over 4 =item In scalar context, returns a hash reference mapping each NAME to the corresponding positional argument. =item In list context, returns a flat list of C<(name => value)> pairs. =back Example: sub send_message { my ($self) = @_; my %args = $self->get_args(qw(message email)); _send_message($args{message}, $args{email}); } When you call C with a list of names, values are assigned in order: the first name gets the first argument, the second name gets the second argument, and so on. If you only want specific positions, you may use C as a placeholder: my %args = $self->get_args('message', undef, 'cc'); # skip argument 2 If there are fewer positional arguments than names, the remaining names are set to C. Extra positional arguments (beyond the provided names) are ignored. =head3 With no names =over 4 =item In scalar context returns an array reference containing the command's positional arguments. =item In list context returns a list containing the command's positional arguments. =back =head2 init If defined, C is invoked during application initialization, after command-line options and arguments have been processed and before command dispatch. Use this method to perform application-specific initialization and validation. The C method is invoked after command-line options have been parsed, registered option providers have resolved any remaining options, and application defaults have been applied. Consequently, all resolved option values are available through their corresponding accessors when C executes. Applications may use C to perform additional initialization, including validation of resolved values, establishing connections, or preparing resources needed by command handlers. See L for details on option resolution and precedence. =head1 USING PACKAGE VARIABLES The C, C, C, and C constructor arguments may also be defined using package variables. This provides a declarative alternative to passing these configuration values directly to C. The corresponding package variables are C<$DEFAULT_OPTIONS>, C<$EXTRA_OPTIONS>, C<$OPTION_SPECS>, and C<$COMMANDS>. our $OPTION_SPECS = [ qw( help|h log-level=s|L debug|d ) ]; our $COMMANDS = { foo => \&foo, bar => \&bar, }; =head1 COMMAND LINE OPTIONS Command-line options are defined using L-style specifications. You pass these into the constructor via the C parameter: my $cli = CLI::Simple->new( option_specs => [ qw( help|h foo-bar=s log-level=s ) ] ); Option values are accessible through automatically generated getter methods: $cli->get_foo_bar(); $cli->get_log_level(); Option names that contain dashes (C<->) are automatically converted to snake_case for the accessor methods. For example: option_specs => [ 'foo-bar=s' ] ...results in: $cli->get_foo_bar(); =head2 Getopt::Long Configuration C uses L to parse command-line options, with the C configuration enabled. Consequently: =over 4 =item * Option names are case-sensitive. =item * Automatic option abbreviation is enabled. An option name may be abbreviated to any unambiguous prefix. =item * Multiple option names may be declared using Getopt::Long's C<|> syntax, such as C. =item * When multiple spellings of the same option are supplied, the last occurrence determines its value. =back All other Getopt::Long configuration settings retain their defaults. =head1 COMMAND ARGUMENTS If your commands accept positional arguments, you can retrieve them using the C method. You may optionally provide a list of argument names, in which case the arguments will be returned as a hash (or hashref in scalar context) with named values. Example: sub send_message { my ($self) = @_; my %args = $self->get_args(qw(phone_number message)); send_sms_message($args{phone_number}, $args{message}); } If you call C without any argument names, it simply returns all remaining arguments as a list: my ($phone_number, $message) = $self->get_args; I returns a hash in list context and a hash reference in scalar context.> =head2 set_args Replaces the positional argument list with the supplied array reference. $self->set_args([qw(foo 1)]); This method overrides the positional arguments originally passed to the script. You can also modify individual arguments by retrieving the existing array reference using C in scalar context: my $args = $self->get_args; $args->[1] = '2'; This avoids replacing the entire argument list. =head1 CUSTOM ERROR HANDLER By default, C exits if C returns a false value, indicating an error while parsing options. =over 4 =item * Set C<$CLI::Simple::GETOPT_EXIT_ON_ERROR> to a false value. This disables automatic exiting and lets your program decide what to do after an option-parsing failure. =item * Provide an C 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 is called with the error message from C. It must return a boolean: a true value allows processing to continue, while a false value causes C to exit immediately. =back When an C callback is supplied, its return value determines whether processing continues, regardless of the value of C<$CLI::Simple::GETOPT_EXIT_ON_ERROR>. Without a callback, the global variable controls whether an option-parsing failure causes an immediate exit. =head1 SETTING DEFAULT VALUES FOR OPTIONS To assign default values to your options, pass a hash reference as the C argument to the constructor. These values will be used unless explicitly overridden by the user on the command line. 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. Default values are applied after command-line arguments have been parsed and registered option providers have been consulted. A default is used only when the corresponding option remains unresolved. Consequently, values supplied by option providers take precedence over application defaults, just as command-line values take precedence over provider values. See L for details on option resolution and precedence. =head1 OPTION PROVIDERS C supports pluggable option providers that resolve application options from sources other than the command line. Providers are implemented as L roles and registered using the C class method. Option providers separate the source of an option's value from its use within the application. Regardless of whether an option was supplied on the command line, retrieved from a configuration file, or resolved by another provider, application code accesses it through the same C and C accessors. This makes the accessors the application's consistent interface to option values, eliminating the need for application code to know where those values originated or how precedence was established. An application can register one or more providers: __PACKAGE__->use_options( environment => { 'CLI::Simple::Role::Environment' => [], }, config => { 'MyApp::Role::Config' => [qw(host port database)], }, secrets_manager => { 'CLI::Simple::Role::SecretsManager' => [qw(database_password api_key)], }, ); Each registration consists of a provider name, a role implementing the provider, and an array reference identifying the application options the provider may resolve. Providers are consulted in registration order. When multiple providers declare the same option, the first provider to successfully resolve it takes precedence. Subsequent providers are asked to resolve only those options that remain unresolved. An empty option list indicates that the provider may resolve any unresolved application option declared through C or C. The provider name determines the methods C invokes. For example, a provider registered as C implements C<_config_resolve()> and may optionally implement C<_config_init()>. =head2 Implementing an Option Provider An option provider is a L role that implements a resolver method named C< _foo_config_resolve() >, where I is the name used when registering the provider. For example, the following provider resolves options from a Perl hash: package Foo::Config; use strict; use warnings; use Role::Tiny; our %FOO_CONFIG = ( foo => 'bar', ); sub _foo_config_resolve { my ( $self, @names ) = @_; my %values; foreach my $name (@names) { next if !exists $FOO_CONFIG{$name}; $values{$name} = $FOO_CONFIG{$name}; } return \%values; } 1; The application registers the provider using C: package Foo; use parent qw(CLI::Simple); __PACKAGE__->use_options( foo_config => { 'Foo::Config' => [qw(foo)], }, ); # In the application's main() method: my $cli = __PACKAGE__->new( option_specs => ['foo|f=s'], ); Once registered, C composes the role into the application class and invokes C<_foo_config_resolve()> when C has not been supplied on the command line. The resolved value is available through the normal C accessor. The resolver receives the names of the application options that remain unresolved and returns a hash reference containing the values it can supply. The returned keys must be the application option names, regardless of how the values are represented in the underlying source. For example, a provider might resolve the application option C from an environment variable or configuration entry named C. Any such translation is the responsibility of the provider. An option is considered resolved when its name exists as a key in the returned hash reference. Values of C, C<0>, and the empty string are valid resolved values. Options omitted from the returned hash reference remain unresolved and may be supplied by subsequent providers or application defaults. A provider may also implement an optional initialization method named C< _I_init() >. C invokes this method before the provider's resolver, but only when the provider has eligible unresolved options to resolve. This allows providers to defer expensive operations, such as establishing database connections or initializing AWS clients, until they are actually needed. =head2 Option Resolution and Precedence CLI::Simple resolves option values during object construction, before invoking the application's C method. Options are resolved in the following order: =over 4 =item 1. Command-line arguments Values supplied on the command line take precedence over all providers. Options already supplied on the command line are not passed to providers. =item 2. Registered option providers Providers are consulted in registration order. Each provider receives only the eligible option names that remain unresolved. A provider resolves an option by including its name in the hash reference returned by its resolver. Options omitted from that hash reference remain eligible for subsequent providers. When a provider has no eligible unresolved options, neither its optional initializer nor its resolver is invoked. =item 3. Application defaults Any remaining unresolved options may be initialized from the application's C. =back After resolution and default initialization are complete, C invokes the application's C method, if defined. Consequently, application initialization and command handlers can access resolved option values through their normal accessors without needing to know which source supplied them. =head1 ADDING ADDITIONAL ACCESSORS All command-line options are automatically available through getter methods named C. If you need to create additional accessors (getters and setters) for values that are not derived from the command line, use the C parameter. This is useful 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 C, C, C, etc., for internal use. Unlike C, C does not register command-line options. It only creates accessors for values that can be initialized or modified programmatically. =head1 LOGGING C integrates with L to provide structured logging for your scripts. C provides convenient initialization of L through C. __PACKAGE__->use_log4perl( level => 'info', config => $log4perl_config_string ); If you do not explicitly include a C option in your C, C will automatically add one for you. Once enabled, you can access the logger instance via: my $logger = $self->get_logger; This logger supports the standard Log4perl methods like C, C, C, etc. I does not itself depend on L. B, it owns that dependency> and must declare it in its own C/C. Static dependency scanners cannot see it -- the module is loaded dynamically inside the method call -- so you must add it by hand.> =head2 Colored Output Pass C 1> to C to default your script to colorized log output using a built-in appender, instead of supplying your own C: __PACKAGE__->use_log4perl( level => 'info', color => 1, ); Colorizing requires L. If it isn't installed, C quietly falls back to uncolored output rather than failing - C 1> is a request, not a hard dependency. If you'd like the person running your script to be able to override that default from the command line, add C to your C: my @option_specs = qw( color! ... ); This gives you C<--color> and C<--no-color> for free. When C is declared, C<--color> and C<--no-color> override the default color setting. These switches control C's built-in colorized configuration; they do not add coloring to a custom Log4perl configuration. Declaring C is therefore safe to add at any time - it only changes behavior for scripts whose users actually pass the flag. I and C are mutually exclusive - C dies if you pass both. C is specifically for using C's own built-in colorized appender; if you need a custom config, write it to include coloring yourself rather than passing C 1>.> =head2 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. 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 first two elements of the array reference are: =over 4 =item A code reference to the command subroutine =item A log level string: one of 'trace', 'debug', 'info', 'warn', 'error', or 'fatal' =back 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; Log levels are resolved in the following order of precedence: =over 4 =item 1. An explicit C<--log-level> command-line option =item 2. The log level associated with the selected command =item 3. The application default supplied to C =back The effective level is available through C. I I if needed.> =head1 FAQ =over 4 =item * How do I execute startup code before my command runs? Implement an C method in your class. The C constructor will invoke this method before returning and before C is executed. Your C method will have access to all options and arguments. Logging will also be initialized, so you can use C to emit messages. =item * Do I need to implement commands? You do not need to define multiple named commands. For an application that performs a single operation, register a default command: commands => { default => \&main } If no command is selected and no default command is registered, C returns successfully without invoking a command handler. The application's C method still executes during construction. =item * Must I subclass C? No. You can instantiate C directly and supply the C and other configuration through the constructor. Subclassing is useful when you want to provide application-specific methods such as C. =item * 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. =item * How do I migrate an existing script to role-based architecture? Run the built-in C<-dump-spec> command to generate a YAML manifest from your existing script, then C<-scaffold> to generate role stubs: my-script -dump-spec # generates my-script.yml my-script -scaffold # generates my-script-roles.tar.gz See L for the full migration workflow. =item * How do I start a new role-based project from scratch? Write a YAML manifest and use the C wrapper to scaffold it: cli-simple -scaffold my-script.yml See L for the manifest format. =item * How do I enable bash completion for my script? Run the built-in C<-generate-completion> command. The script name is taken from C when set, or derived from the module name otherwise. my-script -generate-completion > \ ~/.local/share/bash-completion/completions/my-script Wrappers generated by L set C automatically. =item * My application dies with "use_log4perl() requires Log::Log4perl..." Since not all scripts require logging, C is an I of C. If your application calls C, C must be installed. =back =head1 ALIASING OPTIONS AND COMMANDS C lets you define short, human-friendly aliases for both option names and command names. Use the C parameter to C 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' } }, ); =head2 How option aliases work =over 4 =item * Spec tail is copied automatically You only name the canonical option in C. For each alias, C finds the canonical option's spec tail (for example C<=s>, C<:i>, C, C<+>) and appends it to the alias. In the example above, C behaves as if you had written C, and C behaves as if you had written C. =item * Accessors are created for both names Accessors are generated from all option names (canonical and aliases), with '-' normalized to '_'. In the example, both C and C are available. =item * Values are mirrored after parsing After option parsing and normalization, values are mirrored so either name can be used consistently. When both the canonical option and an alias are supplied the canonical name wins. =item * No duplicate injection If the alias already exists in C, it will not be injected again; value mirroring still occurs. =item * Errors are explicit If an alias points at a canonical option that does not exist, C croaks with a clear error. =item * Case sensitivity C is used with C<:config no_ignore_case>, so option names (and therefore aliases) are case sensitive by default. =back =head2 How command aliases work =over 4 =item * Simple mapping Provide C { commands => { alias => canonical } }> to map an alias to an existing command. In the example, C dispatches to the C command. =item * 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. =item * Errors are explicit If an alias points at a command that does not exist, C croaks with a clear error. =back =head2 Usage examples # Using an option alias script.pl --cfg app.json execute # Using a command alias script.pl ls After parsing, both C and C will return the same value. If the user passes both C<--config> and C<--cfg>, the value from C<--config> (the canonical version) is used. I section, just as applications using the C constructor parameter do. Aliases resolve to their canonical commands before role composition. See L.> =head2 Recommendations =over 4 =item * Keep the canonical spec single-named Define a single canonical name in C and add other spellings via C. Avoid multi-name specs like C; use C instead. =item * 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. =back =head1 ERRORS/EXIT CODES The C method dispatches the selected command and returns its exit status. Command handlers should return C<0> for success or a non-zero value to indicate failure. exit CLI::Simple->new(commands => { foo => \&cmd_foo })->run(); =head2 Exit Codes C provides conventional success and failure exit codes for its own operations. Applications may return additional exit codes from their command handlers. =over 4 =item * '0' Successful completion of a command (C). =item * '1' General usage error, C<--help> display via C, an invalid command line (C) or option parsing errors. =item * Any other code A command handler may return an application-specific numeric exit code, which C passes through to the caller. A handler that calls C terminates the process directly. =back Some errors, including unknown commands, raise exceptions rather than returning an exit code. Unless caught by the application, these exceptions terminate the process with a non-zero status. =head1 LICENSE AND COPYRIGHT This module is free software; you can redistribute it and/or modify it under the same terms as Perl itself. See L for more information. =head1 SEE ALSO L, L, L, L, L, L, L, L =head1 AUTHOR Rob Lauer - =cut CLI-Simple-2.4.0/lib/CLI/Simple/0000755000175100017510000000000015270644577015352 5ustar rlauerrlauerCLI-Simple-2.4.0/lib/CLI/Simple/Utils.pm0000644000175100017510000001422115270644577017010 0ustar rlauerrlauerpackage CLI::Simple::Utils; use strict; use warnings; use Carp; use Data::Dumper; use English qw(-no_match_vars); use JSON qw(decode_json); use List::Util qw(none); use Scalar::Util qw(openhandle); use parent qw(Exporter); our @EXPORT_OK = qw( choose dmp dump_json file_or_stdout normalize_options slurp slurp_json toPascalCase toCamelCase ToCamelCase to_snake_case ); our $VERSION = '2.4.0'; sub toPascalCase { goto &_toCamelCase; } sub ToCamelCase { goto &_toCamelCase; } sub toCamelCase { return _toCamelCase( $_[0], $_[1], 1 ); } ######################################################################## sub _toCamelCase { ######################################################################## my ( $snake_case, $want_hash, $lc_first ) = @_; $snake_case = ref $snake_case ? $snake_case : [$snake_case]; $want_hash //= wantarray ? 0 : 1; my @CamelCase = map { ( $want_hash ? $_ : (), join q{}, map {ucfirst} split /[_-]/xsm ) } @{$snake_case}; return $want_hash ? {@CamelCase} : @CamelCase if !$lc_first; return map {lcfirst} @CamelCase if !$want_hash; my %camelCase = @CamelCase; %camelCase = map { $_ => lcfirst $camelCase{$_} } keys %camelCase; return \%camelCase; } ######################################################################## sub to_snake_case { ######################################################################## my ($str) = @_; return q{} if !defined $str; # 1. Handle acronym boundaries (e.g., HTMLParser -> HTML_Parser) # We look for a sequence of UpperCase followed by (UpperCase + LowerCase) $str =~ s/([[:upper:]]+)([[:upper:]][[:lower:]])/$1_$2/xsmg; # 2. Handle normal Camel/Pascal boundaries (e.g., UserID -> User_ID) # We look for (LowerCase/Digit) followed by (UpperCase) $str =~ s/([[:lower:]\d])([[:upper:]])/$1_$2/xsmg; # 3. Lowercase everything return lc $str; } ######################################################################## sub choose(&) { ## no critic ######################################################################## my $code = shift; # Pass the correct context down to the block if (wantarray) { return $code->(); } elsif ( defined wantarray ) { return scalar $code->(); } else { $code->(); return; } } ######################################################################## sub file_or_stdout { ######################################################################## my ( $self, $outfile, $mode ) = @_; $mode //= 'r'; $outfile //= $self->can('get_outfile') ? $self->get_outfile : qw{-}; my $fh = choose { if ( !$outfile || $outfile eq q{-} ) { return \*STDOUT; } require IO::File; return IO::File->new( $outfile, $mode ); }; return $fh; } ######################################################################## sub dmp { ######################################################################## my (@args) = @_; return Dumper( \@args ); } ######################################################################## sub slurp_json { ######################################################################## my ($file) = @_; my $json = eval { return decode_json( slurp($file) ) }; croak "ERROR: could not decode JSON string:\n$EVAL_ERROR\n" if !$json || $EVAL_ERROR; return $json; } ######################################################################## sub slurp { ######################################################################## my ($file) = @_; local $RS = undef; return <$file> if openhandle $file; open my $fh, '<:raw', $file or croak "ERROR: could not open $file\n"; my $content = <$fh>; close $fh or carp "ERROR: could not close $file\n"; return $content; } ######################################################################## sub normalize_options { ######################################################################## my ($options) = @_; foreach my $k ( keys %{$options} ) { next if $k !~ /\-/xsm; my $val = delete $options->{$k}; $k =~ s/\-/_/gxsm; $options->{$k} = $val; } return %{$options}; } ######################################################################## sub dump_json { ######################################################################## my ($obj) = @_; return JSON->new->pretty->encode($obj); } ######################################################################## sub args { ######################################################################## my ( $args, @valid_keys ) = @_; croak 'args(): first argument must be a hashref' if ref($args) ne 'HASH'; my %ok = map { $_ => 1 } @valid_keys; my @bad = grep { !$ok{$_} } keys %{$args}; croak sprintf 'bad argument(s): %s', join ', ', @bad if @bad; # return in the caller-specified order return @{$args}{@valid_keys}; } 1; __END__ =pod =head1 NAME CLI::Simple::Utils - Useful utility functions for CLI::Simple-based applications =head1 SYNOPSIS CLI::Simple::Utils qw(choose); =head1 DESCRIPTION Utilities that might be useful when writing command line scripts. =head1 METHODS AND SUBROUTINES =head2 choose An anonymous subroutine disguising as a block level internal subroutine (of sorts). Use when a ternary or a cascading if/else block just seems wrong. choose { return "foo" if $bar; return "bar" if $foo; }; =head2 dmp dmp this => $this, that => $that; Shortcut for: print {*STDERR} Dumper([this => $this, that => $that]); =head2 file_or_stdout file_or_stdout([file], [mode]); Returns an open file handle by opening a passed file and optional mode or returning \*STDOUT if file is empty or '-'. =head2 slurp_json slurp_json($file) Returns a Perl object from a presumably JSON encoded file. =head2 slurp slurp(file) Return the entire contents of a file. C can be the path to a file or an open file handle. =head1 VERSION This documentation referes to version 2.4.0 =head1 LICENSE AND COPYRIGHT This module is free software; you can redistribute it and/or modify it under the same terms as Perl itself. See L for more information. =head1 AUTHOR Rob Lauer - =head1 SEE ALSO L =cut CLI-Simple-2.4.0/lib/CLI/Simple/Helpers.pm0000644000175100017510000001627715270644577017327 0ustar rlauerrlauerpackage CLI::Simple::Helpers; use strict; use warnings; use parent qw(Exporter); our @EXPORT_OK = qw( _class_to_dist _class_to_filename _cmd_to_role _extract_pod _find_module_path _generate_spec_yaml _is_class_name _main_module_stub _pm_path _resolve_spec _role_stub _spec_filename _sub_to_role _tarball_filename ); our %EXPORT_TAGS = ( all => [@EXPORT_OK] ); ######################################################################## sub _class_to_dist { ######################################################################## my ($class) = @_; ( my $dist = $class ) =~ s/::/-/gxsm; return $dist; } ######################################################################## sub _class_to_filename { ######################################################################## my ($class) = @_; return lc _class_to_dist($class); } ######################################################################## sub _spec_filename { ######################################################################## my ($class) = @_; return _class_to_filename($class) . '.yml'; } ######################################################################## sub _tarball_filename { ######################################################################## my ($class) = @_; return _class_to_filename($class) . '-roles.tar.gz'; } ######################################################################## sub _pm_path { ######################################################################## my ($package) = @_; ( my $path = "lib/$package.pm" ) =~ s|::|/|gxsm; return $path; } ######################################################################## sub _extract_pod { ######################################################################## my ($content) = @_; my @pods = $content =~ /(^=(?:pod|head\d|over|item|begin|for|encoding).+?^=cut)/gxsm; return join q{}, @pods; } ######################################################################## sub _is_class_name { ######################################################################## my ($value) = @_; return $value =~ /::/xsm; } ######################################################################## sub _cmd_to_role { ######################################################################## my ( $class, $cmd ) = @_; my $suffix = join q{::}, map { ucfirst lc $_ } split /[-_]/xsm, $cmd; return "${class}::Role::${suffix}"; } ######################################################################## sub _sub_to_role { ######################################################################## my ( $class, $sub ) = @_; ( my $name = $sub ) =~ s/\Acmd_//xsm; return _cmd_to_role( $class, $name ); } ######################################################################## sub _resolve_spec { ######################################################################## my ( $class, $commands, $option_specs ) = @_; # 1. explicit spec file from command line my $spec_file = $ARGV[1]; if ($spec_file) { die "ERROR: spec file not found: $spec_file\n" if !-e $spec_file; require YAML::Tiny; return YAML::Tiny::LoadFile($spec_file); } # 2. conventional spec file in cwd my $default_spec = _spec_filename($class); if ( -e $default_spec ) { require YAML::Tiny; return YAML::Tiny::LoadFile($default_spec); } # 3. introspect from running module return { options => $option_specs, commands => { map { $_ => $commands->{$_} } grep { !/\A-/xsm } keys %{$commands} }, }; } ######################################################################## sub _generate_spec_yaml { ######################################################################## my ( $class, $commands, $option_specs, $use_roles ) = @_; require YAML::Tiny; my %spec_commands; for my $cmd ( sort grep { !/\A-/xsm } keys %{$commands} ) { my $val = $commands->{$cmd}; # resolve coderef to actual sub name regardless of mode my $sub_name; if ( ref $val ) { require B; my $cv = B::svref_2object($val); $sub_name = $cv->GV->NAME; } else { $sub_name = $val; } ( my $expected = "cmd_$cmd" ) =~ s/-/_/gxsm; if ( $use_roles && $sub_name eq $expected ) { # derive role from the actual sub name, not the command key # cmd_extract -> extract -> Pod::Extract::Role::Extract ( my $name = $sub_name ) =~ s/\Acmd_//xsm; $spec_commands{$cmd} = _cmd_to_role( $class, $name ); } else { $spec_commands{$cmd} = $sub_name; } } # extra_options live in the class stash no strict 'refs'; ## no critic my $stash = \%{ $class . '::' }; my @extra_options = $stash->{EXTRA_OPTIONS} ? @{ ${ $stash->{EXTRA_OPTIONS} } } : (); return YAML::Tiny::Dump( { options => $option_specs, extra_options => \@extra_options, commands => \%spec_commands, } ); } ######################################################################## sub _main_module_stub { ######################################################################## my ($class) = @_; my $stub = <<'END_MODULE'; package $class; use strict; use warnings; use CLI::Simple qw(:roles); use parent qw(CLI::Simple); our $VERSION = '%s'; caller or exit __PACKAGE__->main; 1; END_MODULE $stub = sprintf $stub, q{@} . 'PACKAGE' . q{@}; # to avoid replacement of PACKAGE_VERSION return _tidy_source($stub); } ######################################################################## sub _role_stub { ######################################################################## my ( $role, $method, $class ) = @_; my $stub = <<"END_ROLE"; package $role; use strict; use warnings; use English qw(-no_match_vars); use CLI::Simple::Constants qw(:booleans); #use CLI::Simple::Utils qw(choose slurp slurp_json); #use Cwd qw(abs_path getcwd); #use File::Basename qw(basename dirname); use Role::Tiny; ######################################################################## sub $method { ######################################################################## my (\$self) = \@_; # TODO: migrated from $class - move implementation here die "ERROR: not yet implemented\\n"; return \$SUCCESS; } 1; END_ROLE return _tidy_source($stub); } ######################################################################## sub _find_module_path { ######################################################################## my ($class) = @_; ( my $rel = $class ) =~ s|::|/|gxsm; $rel .= '.pm'; # check %INC first - module is already loaded return $INC{$rel} if $INC{$rel}; # fall back to @INC search for my $dir (@INC) { my $path = "$dir/$rel"; return $path if -e $path; } return; } ######################################################################## sub _tidy_source { ######################################################################## my ($source) = @_; return $source if !eval { require Perl::Tidy; 1 }; my $perltidyrc = glob('~/.perltidyrc'); return $source if !$perltidyrc || !-e $perltidyrc; my $tidied = q{}; my $errors = q{}; Perl::Tidy::perltidy( source => \$source, destination => \$tidied, stderr => \$errors, perltidyrc => $perltidyrc, argv => [], ); return length($tidied) ? $tidied : $source; } 1; __END__ CLI-Simple-2.4.0/lib/CLI/Simple/Modulino.pm0000644000175100017510000000676115270644577017510 0ustar rlauerrlauerpackage CLI::Simple::Modulino; use strict; use warnings; use CLI::Simple::Constants qw(:booleans); use Cwd qw(abs_path); use English qw(-no_match_vars); use parent qw(CLI::Simple); caller or exit __PACKAGE__->main; ######################################################################## sub cmd_create_modulino { ######################################################################## my ($self) = @_; my $module_name = $self->get_module; my $alias = $self->get_alias; if ( !$alias ) { $alias = $module_name; $alias =~ s/::/-/xsmg; $alias = lc $alias; } my $installbindir = $self->get_installbindir; if ( !$installbindir ) { if ( defined $ENV{PERL_LOCAL_LIB_ROOT} ) { $installbindir = "$ENV{PERL_LOCAL_LIB_ROOT}/bin"; } else { require Config; ($installbindir) = Config::config_re(qr/installsitebin/xsm); ($installbindir) = $installbindir =~ /=\'([^\']+)/xsm; } } $installbindir = abs_path($installbindir); die "ERROR: no such directory ($installbindir) or inaccessible\n" if !-d $installbindir; local $RS = undef; my $script = ; # remove pod $script =~ s/\A(.*)^=pod.*\z/$1/xsm; # customize $script =~ s/[@]modulino_wrapper[@]/$alias/xsm; $script =~ s/[@]perl_module_name[@]/$module_name/xsm; my $modulino = sprintf '%s/%s', $installbindir, $alias; open my $fh, '>', $modulino or die "ERROR: could not open $installbindir for writing:\n$OS_ERROR\n"; print {$fh} $script; close $fh or die "ERROR: could not close handle for $modulino\n:$OS_ERROR\n"; chmod 0755, $modulino; print "$alias installed as $modulino\n"; return $SUCCESS; } ######################################################################## sub main { ######################################################################## my $option_specs = [ qw( alias|a=s help|h installbindir|i=s module|m=s ) ]; my $commands = { 'create-modulino' => \&cmd_create_modulino, default => \&cmd_create_modulino, }; return __PACKAGE__->new( commands => $commands, option_specs => $option_specs )->run; } 1; __DATA__ #!/usr/bin/env bash #-*- mode: sh; -*- # modulino invocation MODULINO_WRAPPER=@modulino_wrapper@ MODULINO_MODULE_NAME=@perl_module_name@ MODULINO_WRAPPER="$MODULINO_WRAPPER" \ MODULINO_MODULE_NAME="$MODULINO_MODULE_NAME" \ perl -M"$MODULINO_MODULE_NAME" \ -e 'exit $ENV{MODULINO_MODULE_NAME}->main' \ -- "$@" =pod =head1 NAME CLI::Simple::Modulino - Create CLI wrapper around a modulino =head1 SYNOPSIS # create my-app in the default installation directory create-modulino -m My::App # create /usr/local/bin/my-app create-modulino -i /usr/local/bin -m My::App # create /usr/local/bin app create-modulino -a app -i /usr/local/bin -m My::App =head2 Options -h, --help help -a, --alias wrapper name (default: lowercase module name with :: replaced by -) -m, --module module name - Perl module implementing the modulino -i, --installbindir executable directory Example: create-modulino -i /usr/local/bin -a find-requires -m Module::ScanDeps::FindRequires =head1 DESCRIPTION Creates a so called wrapper for a so-called "modulino". Modulinos are Perl modules that use the pattern: caller or exit __PACKAGE__->main ...to flexibly use a Perl module as a script. See L for more information about modulinos. =head1 AUTHOR Rob Lauer - =head1 SEE ALSO L CLI-Simple-2.4.0/lib/CLI/Simple/Shell.pm0000644000175100017510000000173315270644577016763 0ustar rlauerrlauerpackage CLI::Simple::Shell; use strict; use warnings; 1; __END__ =pod =head1 NAME cli-simple - Scaffold a new project from a .yml file =head1 SYNOPSIS cat >my-project.yml < Scaffold role stubs from an existing modulino =head1 DESCRIPTION The C script's sole use is to scaffold a new project from a .yml file. You can also run the same command from any C based modulino script. =head1 LICENSE AND COPYRIGHT This module is free software; you can redistribute it and/or modify it under the same terms as Perl itself. See L for more information. =head1 SEE ALSO L =head1 AUTHOR Rob Lauer - =cut CLI-Simple-2.4.0/lib/CLI/Simple/Scaffold.pm0000644000175100017510000000555215270644577017440 0ustar rlauerrlauerpackage CLI::Simple::Scaffold; use strict; use warnings; use CLI::Simple::Constants qw(:booleans); use CLI::Simple::Helpers qw(:all); use English qw(-no_match_vars); use parent qw(Exporter); our @EXPORT_OK = qw(_cmd_scaffold); ######################################################################## sub _cmd_scaffold { ######################################################################## my ( $class, $commands, $option_specs ) = @_; require Archive::Tar; require YAML::Tiny; my $spec_file = $ARGV[1]; my ( $spec, $effective_class ); if ( $spec_file && -e $spec_file ) { $spec = YAML::Tiny::LoadFile($spec_file); # derive class from spec filename: cpan-maker-bootstrapper.yml -> Cpan::Maker::Bootstrapper ( my $stem = $spec_file ) =~ s/[.]ya?ml\z//xsm; $effective_class = join '::', map { ucfirst $_ } split /-/xsm, $stem; } else { my $yaml = _generate_spec_yaml( $class, $commands, $option_specs, $TRUE ); $spec = YAML::Tiny::Load($yaml); $effective_class = $class; } # use effective_class for all naming my $tarball = _tarball_filename($effective_class); my $dist = _class_to_filename($effective_class); my $tar = Archive::Tar->new; my $main_stub = _main_module_stub($effective_class); my $source_path = _find_module_path($effective_class); if ( $source_path && -e $source_path ) { local $RS = undef; open my $fh, '<', $source_path or die "ERROR: $OS_ERROR\n"; my $pod = _extract_pod(<$fh>); close $fh; if ($pod) { $main_stub .= "\n$pod"; } $main_stub .= "\n" if $main_stub !~ /\n\z/xsm; } $tar->add_data( _pm_path($effective_class), $main_stub ); my %seen_roles; for my $cmd ( sort keys %{ $spec->{commands} } ) { my $value = $spec->{commands}{$cmd}; my $role = _is_class_name($value) ? $value : _sub_to_role( $effective_class, $value ); next if $seen_roles{$role}++; # derive method name from role class, not command key # Pod::Extract::Role::Extract -> Extract -> cmd_extract ( my $method_suffix = $role ) =~ s/\A.*::Role:://xsm; $method_suffix =~ s/::/_/gxsm; my $method = 'cmd_' . lc $method_suffix; $tar->add_data( _pm_path($role), _role_stub( $role, $method, $effective_class ) ); } # spec yaml into share/ $tar->add_data( "$dist.yml", YAML::Tiny::Dump($spec), ); # help out the bootstrapper... my @role_paths = map { _pm_path($_); } keys %seen_roles; my $main_pm = _pm_path($effective_class); ( my $role_dir = $effective_class ) =~ s|::|/|gxsm; $role_dir = "lib/$role_dir/Role"; my $project_mk = <<"END_MK"; # inter-module dependencies \$(eval \$(call find-files,ROLES,$role_dir,*.pm.in)) $main_pm: \\ \$(ROLES) END_MK $tar->add_data( 'project.mk', $project_mk ); $tar->write( $tarball, Archive::Tar::COMPRESS_GZIP() ); printf {*STDOUT} "created %s\n", $tarball; return $SUCCESS; } 1; __END__ CLI-Simple-2.4.0/lib/CLI/Simple/Constants.pm0000644000175100017510000001351015270644577017664 0ustar rlauerrlauerpackage CLI::Simple::Constants; use strict; use warnings; use parent qw(Exporter); our $VERSION = '2.4.0'; use Readonly; Readonly::Scalar our $LOG4PERL_COLOR_DEBUG => 'magenta'; Readonly::Scalar our $LOG4PERL_COLOR_INFO => 'green'; Readonly::Scalar our $LOG4PERL_COLOR_WARN => 'yellow'; Readonly::Scalar our $LOG4PERL_COLOR_ERROR => 'red'; Readonly::Scalar our $LOG4PERL_COLOR_FATAL => 'bold red'; Readonly::Scalar our $LOG4PERL_COLOR_TRACE => 'bold magenta'; Readonly::Scalar our $LOG4PERL_CONF => <<'END_OF_CONF'; log4perl.logger = INFO, Screen log4perl.appender.Screen = Log::Log4perl::Appender::ScreenColoredLevels log4perl.appender.Screen.stderr = 1 log4perl.appender.Screen.layout = Log::Log4perl::Layout::PatternLayout log4perl.appender.Screen.layout.ConversionPattern = [%%d] %%m%%n log4perl.appender.Screen.color.DEBUG=%s log4perl.appender.Screen.color.INFO=%s log4perl.appender.Screen.color.WARN=%s log4perl.appender.Screen.color.ERROR=%s log4perl.appender.Screen.color.FATAL=%s log4perl.appender.Screen.color.TRACE=%s END_OF_CONF # this bad but let's avoid load Log::Log4perl::Level Readonly::Hash our %LOG_LEVELS => ( trace => 5000, debug => 10_000, info => 20_000, warn => 30_000, error => 40_000, fatal => 50_000, ); our @EXPORT_OK = (); our %EXPORT_TAGS = ( 'log-levels' => [qw(%LOG_LEVELS)], 'booleans' => [ qw{ $TRUE $FALSE $SUCCESS $FAILURE } ], 'chars' => [ qw{ $AMPERSAND $COLON $COMMA $DOUBLE_COLON $DASH $DOT $EMPTY $EQUALS_SIGN $EQUALS $FAT_ARROW $OCTOTHORP $PERIOD $QUESTION_MARK $SLASH $SPACE $TEMPLATE_DELIMITER $UNDERSCORE } ], 'strings' => [ qw{ $PADDING } ], 'color-config' => [ qw( $LOG4PERL_CONF $LOG4PERL_COLOR_DEBUG $LOG4PERL_COLOR_INFO $LOG4PERL_COLOR_WARN $LOG4PERL_COLOR_ERROR $LOG4PERL_COLOR_FATAL $LOG4PERL_COLOR_TRACE ) ], ); # chars Readonly::Scalar our $AMPERSAND => q{&}; Readonly::Scalar our $COLON => q{:}; Readonly::Scalar our $COMMA => q{,}; Readonly::Scalar our $DOUBLE_COLON => q{::}; Readonly::Scalar our $DASH => q{-}; Readonly::Scalar our $DOT => q{.}; Readonly::Scalar our $EMPTY => q{}; Readonly::Scalar our $EQUALS_SIGN => q{=}; Readonly::Scalar our $EQUALS => q{=}; Readonly::Scalar our $FAT_ARROW => q{=>}; Readonly::Scalar our $OCTOTHORP => q{#}; Readonly::Scalar our $PERIOD => q{.}; Readonly::Scalar our $QUESTION_MARK => q{?}; Readonly::Scalar our $SLASH => q{/}; Readonly::Scalar our $SPACE => q{ }; Readonly::Scalar our $TEMPLATE_DELIMITER => q{@}; Readonly::Scalar our $UNDERSCORE => q{_}; # strings Readonly::Scalar our $PADDING => $SPACE x 4; # booleans Readonly::Scalar our $TRUE => 1; Readonly::Scalar our $FALSE => 0; # shell booleans Readonly::Scalar our $SUCCESS => 0; Readonly::Scalar our $FAILURE => 1; # valid args Readonly::Array our @VALID_OPTIONS => qw( abbreviations alias commands command_roles default_options error_handler extra_options manifest_commands option_specs validate_command help_sections ); Readonly::Array our @DEFAULT_HELP_SECTIONS => qw( SYNOPSIS DESCRIPTION/Commands DESCRIPTION/Options OPTIONS USAGE ); foreach my $k ( keys %EXPORT_TAGS ) { push @EXPORT_OK, @{ $EXPORT_TAGS{$k} }, qw( @VALID_OPTIONS @DEFAULT_HELP_SECTIONS $LOG4PERL_CONF $LOG4PERL_COLOR_DEBUG $LOG4PERL_COLOR_INFO $LOG4PERL_COLOR_WARN $LOG4PERL_COLOR_ERROR $LOG4PERL_COLOR_FATAL $LOG4PERL_COLOR_TRACE ); } $EXPORT_TAGS{'all'} = [@EXPORT_OK]; 1; ## no critic (RequirePodSections) __END__ =pod =head1 NAME CLI::Simple::Constants - Exportable constants for CLI::Simple-based applications =head1 SYNOPSIS use CLI::Simple::Constants qw(:booleans :chars :log-levels); return $SUCCESS if $flag; print $PADDING, "=>", $SPACE, $EQUALS_SIGN, "\n" if $DEBUG; =head1 DESCRIPTION This module provides a collection of constants commonly needed when building command-line tools, especially those using C. It includes: =over 4 =item * Boolean values for use in control flow or shell-style success/failure =item * Character constants for formatting and CLI-friendly output =item * Predefined log level names for use with Log::Log4perl =item * Export tags for grouping constants by intent =back =head1 EXPORT TAGS =over 4 =item * :booleans Semantic truthy and shell-style constants: $TRUE => 1 $FALSE => 0 $SUCCESS => 0 # shell success $FAILURE => 1 # shell failure =item * :chars Export commonly used single-character string constants: $AMPERSAND => '&' $COLON => ':' $COMMA => ',' $DOUBLE_COLON => '::' $DASH => '-' $DOT => '.' $EMPTY => '' $EQUALS_SIGN => '=' $OCTOTHORP => '#' $PERIOD => '.' $QUESTION_MARK => '?' $SLASH => '/' $SPACE => ' ' $TEMPLATE_DELIMITER => '@' $UNDERSCORE => '_' Note: C<$DOT> and C<$PERIOD> are synonyms provided for semantic clarity. =item * :strings String constants used for formatting: $PADDING => ' ' # 4 spaces, commonly used for indentation =item * :log-levels Provides a hash mapping symbolic log level names to L constants: %LOG_LEVELS => ( debug => $DEBUG, trace => $TRACE, info => $INFO, warn => $WARN, error => $ERROR, fatal => $FATAL, ) =item * :all Exports all constants from the above tags. =back =head1 SEE ALSO L, L =head1 AUTHOR Rob Lauer - =head1 LICENSE Same terms as Perl itself. =cut CLI-Simple-2.4.0/lib/CLI/Simple/Migrate.pm0000644000175100017510000000170115270644577017277 0ustar rlauerrlauerpackage CLI::Simple::Migrate; use strict; use warnings; use CLI::Simple::Constants qw(:booleans); use CLI::Simple::Helpers qw(:all); use CLI::Simple::Scaffold qw(_cmd_scaffold); use English qw(-no_match_vars); use parent qw(Exporter); our @EXPORT_OK = qw(_cmd_migrate); ######################################################################## sub _cmd_migrate { ######################################################################## my ( $class, $commands, $option_specs ) = @_; # dump spec with role class names then scaffold my $yaml = _generate_spec_yaml( $class, $commands, $option_specs, $TRUE ); my $spec_file = _spec_filename($class); open my $fh, '>', $spec_file or die "ERROR: could not write $spec_file: $OS_ERROR\n"; print {$fh} $yaml; close $fh; printf {*STDOUT} "wrote %s\n", $spec_file; # now scaffold from the spec we just wrote _cmd_scaffold( $class, $commands, $option_specs ); return $SUCCESS; } 1; CLI-Simple-2.4.0/lib/CLI/Simple/DumpSpec.pm0000644000175100017510000000206515270644577017433 0ustar rlauerrlauerpackage CLI::Simple::DumpSpec; use strict; use warnings; use CLI::Simple::Constants qw(:booleans); use CLI::Simple::Helpers qw(:all); use English qw(-no_match_vars); use parent qw(Exporter); our @EXPORT_OK = qw(_cmd_dump_spec); ######################################################################## sub _cmd_dump_spec { ######################################################################## my ( $class, $commands, $option_specs ) = @_; my $use_roles = ( $ARGV[1] // q{} ) eq 'roles'; my $spec_file = _spec_filename($class); my $yaml = _generate_spec_yaml( $class, $commands, $option_specs, $use_roles ); open my $fh, '>', $spec_file or die "ERROR: could not write $spec_file: $OS_ERROR\n"; print {$fh} $yaml; close $fh or warn "WARNING: could not close $spec_file: $OS_ERROR\n"; printf {*STDOUT} "wrote %s\n", $spec_file; printf {*STDOUT} "your main() can now be replaced with:\n\n"; printf {*STDOUT} " caller or exit __PACKAGE__->main;\n\n"; printf {*STDOUT} "see: perldoc CLI::Simple\n"; return $SUCCESS; } 1; __END__