CLI-Simple-2.0.3/0000755000175100017510000000000015213050032012711 5ustar rlauerrlauerCLI-Simple-2.0.3/META.json0000664000175100017510000000556315213050032014345 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.23", "JSON" : "4.07", "List::Util" : "1.56", "Log::Log4perl" : "1.57", "Log::Log4perl::Level" : "0", "Readonly" : "2.05", "Role::Tiny" : "0", "YAML::Tiny" : "1.76", "perl" : "5.010" } }, "test" : { "requires" : { "Role::Tiny" : "2.002004", "Test::Exit" : "0.11", "Test::Output" : "1.036", "YAML::Tiny" : "1.76" } } }, "provides" : { "CLI::Simple" : { "file" : "lib/CLI/Simple.pm", "version" : "v2.0.3" }, "CLI::Simple::Constants" : { "file" : "lib/CLI/Simple/Constants.pm", "version" : "v2.0.3" }, "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::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.0.3" } }, "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.0.3", "x_serialization_backend" : "JSON::PP version 4.16" } CLI-Simple-2.0.3/META.yml0000664000175100017510000000330615213050032014166 0ustar rlauerrlauer--- abstract: 'Simple command line script accelerator' author: - 'Rob Lauer ' build_requires: ExtUtils::MakeMaker: '6.64' File::ShareDir::Install: '0' Role::Tiny: '2.002004' 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.0.3 CLI::Simple::Constants: file: lib/CLI/Simple/Constants.pm version: v2.0.3 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::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.0.3 requires: Class::Accessor::Fast: '0.51' File::ShareDir: '1.118' File::Which: '1.23' JSON: '4.07' List::Util: '1.56' Log::Log4perl: '1.57' Log::Log4perl::Level: '0' Readonly: '2.05' Role::Tiny: '0' 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.0.3 x_serialization_backend: 'CPAN::Meta::YAML version 0.018' CLI-Simple-2.0.3/README.md0000644000175100017510000014031215213050030014167 0ustar rlauerrlauer# Table of Contents * [NAME](#name) * [SYNOPSIS](#synopsis) * [DESCRIPTION](#description) * [VERSION](#version) * [FEATURES](#features) * [MODULINOS](#modulinos) * [Why Modulinos?](#why-modulinos) * [The Bash Wrapper](#the-bash-wrapper) * [create-modulino](#create-modulino) * [MODULINO\_WRAPPER](#modulino\wrapper) * [QUICK START](#quick-start) * [Single-Module Application](#single-module-application) * [Role-Based Application](#role-based-application) * [ROLE-BASED ARCHITECTURE](#role-based-architecture) * [The YAML Manifest](#the-yaml-manifest) * [Command Values](#command-values) * [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) * [Not a Framework](#not-a-framework) * [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) * [ADDITIONAL NOTES](#additional-notes) * [INTERNAL COMMANDS](#internal-commands) * [-generate-completion](#-generate-completion) * [-dump-spec](#-dump-spec) * [-scaffold](#-scaffold) * [-migrate](#-migrate) * [METHODS AND SUBROUTINES](#methods-and-subroutines) * [new](#new) * [command](#command) * [commands (required)](#commands-required) * [main](#main) * [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) * [set\_args](#set\args) * [COMMAND ARGUMENTS](#command-arguments) * [CUSTOM ERROR HANDLER](#custom-error-handler) * [SETTING DEFAULT VALUES FOR OPTIONS](#setting-default-values-for-options) * [ADDING USAGE TO YOUR SCRIPTS](#adding-usage-to-your-scripts) * [Custom help() Method](#custom-help-method) * [ADDING ADDITIONAL SETTERS](#adding-additional-setters) * [LOGGING](#logging) * [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 CLI::Simple::Constants qw(:booleans :chars); use CLI::Simple qw($AUTO_HELP $AUTO_DEFAULT); use parent qw(CLI::Simple); caller or __PACKAGE__->main(); sub execute { my ($self) = @_; # retrieve a CLI option my $file = $self->get_file; ... } sub list { my ($self) = @_ # retrieve a command argument my ($file) = $self->get_args(); ... } sub main { # Disable auto-default for single commands, enable auto-help $AUTO_DEFAULT = 0; $AUTO_HELP = 1; my $cli = MyScript->new( option_specs => [ qw( help format=s file=s) ], default_options => { format => 'json' }, # set some defaults extra_options => [ qw( content ) ], # non-option, setter/getter commands => { execute => \&execute, list => \&list, } alias => { options => { fmt => 'format' }, commands => { ls => 'list' } }, ); return $cli->run(); } 1; \# role-based CLI Application (2.0.0) \# create a YAML manifest `my-script.yml` in your project root: --- commands: frobnicate: My::Script::Role::Frobnicate list: My::Script::Role::List options: - help|h - verbose|v - output|o=s \# 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 This documentation refers to version 2.0.3. # 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 - optional role-based architecture via YAML manifest - built-in scaffolding tools for migrating legacy scripts to roles - bash completion script generation for modulino wrappers # 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 __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. # 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 __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: --- commands: frobnicate: My::Script::Role::Frobnicate list: My::Script::Role::List options: - help|h - verbose|v - output|o=s Each role implements one or more commands: package My::Script::Role::Frobnicate; use Role::Tiny; use CLI::Simple::Constants qw(:booleans); sub cmd_frobnicate { my ($self) = @_; ... return $SUCCESS; } 1; To scaffold role stubs from an existing modulino, run the built-in `-scaffold` command: my-script -scaffold To scaffold from an existing manifest - including a new one written by hand or generated by `-dump-spec` - pass the spec file path: cli-simple -scaffold my-script.yml Or let `CLI::Simple` generate the manifest and scaffold from an existing modulino in one step: my-script -migrate See ["ROLE-BASED ARCHITECTURE"](#role-based-architecture) for the complete workflow including the baby-step migration path. # ROLE-BASED ARCHITECTURE `CLI::Simple` 2.0.0 introduces an optional role-based architecture for applications that have grown beyond a single module. Commands are implemented in dedicated [Role::Tiny](https://metacpan.org/pod/Role%3A%3ATiny) roles and declared in a YAML manifest. `CLI::Simple` composes the roles, builds the dispatch table, and provides an inherited `main()` - potentially reducing your main module to a single declaration. ## The YAML Manifest The manifest is a YAML file that declares your commands, options, and defaults. By convention the filename is derived from your module name: My::Script -> my-script.yml CPAN::Maker::Bootstrapper -> cpan-maker-bootstrapper.yml `CLI::Simple` locates the 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 - it cannot be loaded from an arbitrary location. _Security note: The manifest is loaded exclusively from the distribution share directory via [File::ShareDir](https://metacpan.org/pod/File%3A%3AShareDir). A manifest that was not installed as part of the distribution cannot be loaded. This provides the same security model as Perl module loading itself._ A minimal manifest: --- commands: frobnicate: My::Script::Role::Frobnicate list: My::Script::Role::List options: - help|h - verbose|v - output|o=s A complete manifest with all supported keys: --- commands: frobnicate: My::Script::Role::Frobnicate list: My::Script::Role::List default: cmd_frobnicate options: - help|h - verbose|v - output|o=s default_options: verbose: 0 extra_options: - dbh - config_data ## Command Values Each command in the manifest maps to either a role class name or a sub name: - **Role class name** (contains `::`) - the role is composed into your main module and the method `cmd__command_` is resolved from the role. `code-review` resolves to `cmd_code_review`. - **Sub name** - resolved directly via `can()` on your class. Use this for alias commands that point to an existing method: default: cmd_frobnicate ## Roles With No Commands Some roles provide framework behavior rather than commands - for example an `init()` method for startup validation. Since these roles have no command entry in the manifest they must be composed manually in your main module: package My::Script; use CLI::Simple qw(:roles); use Role::Tiny::With; use parent qw(CLI::Simple); with 'My::Script::Role::Init'; caller or exit __PACKAGE__->main; 1; _Note: A future version of `CLI::Simple` will support an `extra_roles` key in the manifest to handle this automatically._ ## Activating Role-Based Architecture Add `:roles` to your `use CLI::Simple` statement: use CLI::Simple qw(:roles); This triggers manifest loading at compile time. The manifest is located using the fallback chain described above. Roles are composed into your class and the dispatch table is built before `new()` is called. ## The Inherited main() When using `:roles`, your class inherits `main()` from `CLI::Simple`. It reads the manifest, constructs the object with the manifest's options and dispatch table, and calls `run()`: caller or exit __PACKAGE__->main; Override `main()` in your subclass only if you need to add behaviour that cannot be expressed in the manifest or `init()`. ## Distributing the Manifest Add the manifest to your distribution's share directory. `CPAN::Maker` users can add it `extra-files` in `buildspec.yml` so it is installed into the share directory: extra-files: - share: - my-script.yml During development the manifest is found via `%INC`. After installation it is found via [File::ShareDir](https://metacpan.org/pod/File%3A%3AShareDir). No code changes required between the two environments. =head1 PHILOSOPHY AND DESIGN PRINCIPLES `CLI::Simple` is intentionally minimalist. It provides just enough structure to build command-line tools with subcommands, option parsing, and help handling -- but without enforcing any particular framework or lifecycle. ## Not a Framework This module is not [App::Cmd](https://metacpan.org/pod/App%3A%3ACmd), [MooseX::Getopt](https://metacpan.org/pod/MooseX%3A%3AGetopt), or a full application toolkit. Instead, it offers: - An object-oriented base class with a clean `run()` dispatcher - Command-line parsing via `Getopt::Long` - Built-in logging via `Log::Log4perl` - Subclass hooks like `init()` for setup and validation - Optional role-based architecture via YAML manifest for larger applications The philosophy is: provide just enough infrastructure, then get out of your way. ## Validation, Defaults, and Configuration `CLI::Simple` does not impose a validation model. You may: - Use `Getopt::Long` features (e.g., type constraints, default values) - Write your own validation logic in `init()` - Throw exceptions, emit usage, or exit early at any point The lifecycle is explicit and under your control. You decide how much structure you want to add on top of it. ## 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: 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 1: Manifest Loading** For role-based applications using `use CLI::Simple qw(:roles)`, the YAML manifest is loaded at compile time during `import`. Roles are composed into the calling class and the dispatch table is built before `new()` is ever called. Single-module applications skip this phase entirely. - **Phase 2: Initialization (`new` =** `init`)> The constructor parses command-line arguments via `Getopt::Long`, creates accessors for all options, and calls your `init()` method. Inside `init()`, your application has full access to the parsed options and arguments. This phase is the ideal hook for all final setup tasks, such as: - Validating command-line arguments. - Loading configuration files based on a `--config` option. - Dynamically overriding the command (e..g, `$self->command('new_default')`). - Performing any setup required **before** a command is run. - **Phase 3: Execution (`run`)** Dispatches to the command method determined during initialization. ## "opt-in" Default Command By design, `CLI::Simple` **does not impose a default command**. This provides total flexibility for the application author: - **You Can Set a Default:** If your application needs a default command (e.g., to run `help` when no command is given), you can set `$AUTO_HELP`, explicitly set the `default` command in the `command` hash you pass to the constructor or use `command()` to set one inside the `init()` method. - **You Can Have No Default:** If you do **not** set a default, `run()` will simply do nothing and return cleanly if no command is provided on the command line. This "no default by default" behavior is what enables a powerful "setup-only" execution mode. A user can run your script _without_ specifying a command. This will: - 1. Run the entire `new()` / `init()` phase, performing all setup. - 2. Call `run()`, which will find no command and exit cleanly. This provides an ideal hook for applications that need to perform "on-demand initialization" (e.g., seeding a database, authenticating) by checking for a specific flag inside `init()`, without also triggering an unwanted command. In role-based applications using a YAML manifest, a `default` command that aliases another command should map to the sub name directly rather than a role class: commands: default: cmd_install install: My::Module::Role::Installer ## `$AUTO_HELP` and `$AUTO_DEFAULT` Two package variables can be used to further control the lifecycle. By default, the framework provides no default command as explained in the sections above. Some scripters may want default behaviors that assume a command or provide usage if no command is provided. - `$AUTO_HELP` Set the package variable `$AUTO_HELP` to a true value if you want `CLI::Simple` to provide help when no command is provided. default: false - `$AUTO_DEFAULT` Set the package variable `$AUTO_DEFAULT` to a true value if you want `CLI::Simple` to automatically select a command if you have only 1 command defined and no command is provided on the command line. When true, it will prepend the single command name to the argument list, allowing any subsequent arguments to be correctly parsed as args for that command. default: false # CONSTANTS `CLI::Simple` does not define its own constants directly, but it is often used in conjunction with [CLI::Simple::Constants](https://metacpan.org/pod/CLI%3A%3ASimple%3A%3AConstants), which provides a collection of exportable values commonly needed in command-line scripts. These include: - Boolean flags like `$TRUE`, `$FALSE`, `$SUCCESS`, and `$FAILURE` - Common character tokens such as `$COLON`, `$DASH`, `$EQUALS_SIGN`, etc. - Log level names compatible with [Log::Log4perl](https://metacpan.org/pod/Log%3A%3ALog4perl) To use them in your script: use CLI::Simple::Constants qw(:all); # ADDITIONAL NOTES - All options are case insensitive - See [CLI::Simple::Utils](https://metacpan.org/pod/CLI%3A%3ASimple%3A%3AUtils) to learn about additional utilities useful when writing scripts, including `choose`, `slurp`, and `dmp`. - `%INTERNAL_COMMANDS` is a package variable - subclasses can add their own internal commands by pushing entries into the hash before calling `new()`. # 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). Internal commands are dispatched via the `%INTERNAL_COMMANDS` package variable: our %INTERNAL_COMMANDS = ( '-generate-completion' => \&_cmd_generate_completion, '-dump-spec' => \&_cmd_dump_spec, '-scaffold' => \&_cmd_scaffold, '-migrate' => \&_cmd_migrate, ); Subclasses can add their own internal commands by extending the hash before `new()` is called: our %INTERNAL_COMMANDS = ( %CLI::Simple::INTERNAL_COMMANDS, '-my-command' => \&_cmd_my_command, ); ## -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 `--` 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 the first argument if provided, then `MODULINO_WRAPPER` if set, then inferred from the module name. If the inferred name cannot be found in `PATH`, a warning is issued but the completion script is still generated. _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. They are not aligned however `MODULINO_WRAPPER` is set by the bash wrapper. my-alias -generate-completion - Case 3: Your modulino is an alias not created by `create-modulino` The script name `my-alias` is not aligned with your module name `My::Module` and your modulino wrapper does not set `MODULINO_WRAPPER`. The `-generate-completion` script called by your custom wrapper most likely only resolves the program name as the path to your Perl module: path-to-modules/My/Module.pm ...in this case you need to supply the alias name or set `MODULINO_WRAPPER` in the environment. my-alias -generate-completion my-alias ## -dump-spec 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 tarball contains role stubs, a slimmed main module with extracted POD, a `project.mk` with inter-module dependencies, and the YAML manifest. my-script -scaffold # introspect live module cli-simple -scaffold my-script.yml # scaffold from spec file The tarball is named `my-script-roles.tar.gz` by convention (the lower case snake cased version of the class name). 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. Feed the tarball to [CPAN::Maker::Bootstrapper](https://metacpan.org/pod/CPAN%3A%3AMaker%3A%3ABootstrapper) via the `import-scaffold` command to 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 ## new new( args ) Instantiates a new `CLI::Simple` instance, parses options, optionally initializes logging, and makes options available via dynamically generated accessors. _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, the `run()` method will treat the provided command as a prefix and compare it to the keys in the command hash. If exactly one match is found, it will be used. If more than one match is found, or if no match is found, `run()` will throw an exception. 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 apply if the corresponding option is not given on the command line. - 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. ## command command command(command) Get 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. ## commands (required) commands commands(command, handler) Returns the hash you passed in the constructor as `commands` or can be used to insert a new command into the `commands` hash. `handler` should be a code reference. commands(foo => sub { return 'foo' }); ## main __PACKAGE__->main; For role-based applications, `main` is inherited from `CLI::Simple` and reads the YAML manifest loaded during `import`. It constructs the object with the manifest's options, default options, extra options, and dispatch table, 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. ## run Execute the script with the given options, commands and arguments. The `run` method interprets the command line and passes control to your command subroutines. Your subroutines should return a 0 for success and a non-zero value for failure. This error code is passed to the shell as the script return code. ## 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'); # args 1 and 3 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 you define your own `init()` method, it will be called by the constructor. Use this method to perform any actions you require before you execute the `run()` method. # USING PACKAGE VARIABLES You can pass the necessary parameter required to implement your command line scripts in the constructor or some people prefer to see them clearly defined in the code. Accordingly, you can use package variables with the same name as the constructor arguments (in upper case). our $OPTION_SPECS = [ qw( help|h log-level=s|L debug|d ) ]; our $COMMANDS = { foo => \&foo, bar => \&bar, }; Subclasses can also extend the built-in internal commands by adding entries to `%INTERNAL_COMMANDS`: our %INTERNAL_COMMANDS = ( %CLI::Simple::INTERNAL_COMMANDS, '-my-command' => \&_cmd_my_command, ); # 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 ) ] ); In your command subroutines, you can access these values using automatically generated getter methods: $cli->get_foo(); $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(); ## set\_args Resets the positional arguments. $self->set_args(qw(foo 1)); This method overrides the positional arguments originally passed to the script. You can achieve the same behavior by calling the `get_args` in scalar context and modifying the reference. my $args = $self->get_args; $args->[1] = '2'; Use this technique when you want don't want to alter the entire set of arguments. # 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._ # CUSTOM ERROR HANDLER By default, `CLI::Simple` will exit if `GetOptions` returns a false value, indicating an error while parsing options. You can override this behavior in one of two ways: - 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. # 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. # ADDING USAGE TO YOUR SCRIPTS To provide built-in usage/help output, include a `=head1 USAGE` section in your script's POD: =head1 USAGE usage: myscript [options] command args Options ------- --help, -h Display help ... If the user supplies the command `help`, or the `--help` option, `CLI::Simple` will display this section automatically: perl myscript.pm --help perl myscript.pm help ## 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 usage information that differs from the embedded POD. Without a custom handler, `CLI::Simple` defaults to displaying the `USAGE` POD section. # ADDING ADDITIONAL SETTERS 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. # LOGGING `CLI::Simple` integrates with [Log::Log4perl](https://metacpan.org/pod/Log%3A%3ALog4perl) to provide structured logging for your scripts. To enable logging, call the class method `use_log4perl()` in your module or script: __PACKAGE__->use_log4perl( level => 'info', config => $log4perl_config_string ); If you do not explicitly include a `log-level` option in your `option_specs`, CLI::Simple will automatically add one for you. 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. ## 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 array reference should contain at least two elements: - A code reference to the command subroutine - A log level string: one of 'trace', 'debug', 'info', 'warn', 'error', or 'fatal' Example: CLI::Simple->new( option_specs => [qw( help format=s )], default_options => { format => 'json' }, # set some defaults extra_options => [qw( content )], # non-option, setter/getter commands => { execute => \&execute, list => [ \&list, 'error' ], } )->run; _TIP: add other elements to the array for your command to process._ _Note: Per-command log levels are not currently supported in the YAML manifest. Define them programmatically by overriding `main()` if needed._ # FAQ - How do I execute startup code before my command runs? Implement an `init()` method in your class. The `new()` constructor will invoke this method before returning and before `run()` is executed. Your `init()` method will have access to all options and arguments. Logging will also be initialized, so you can use `get_logger()` to emit messages. - Do I need to implement commands? No. If your script doesn't support multiple commands, you can specify a `default` key instead: commands => { default => \&main } - Must I subclass `CLI::Simple`? No. You can use it procedurally or functionally. - 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 __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? Your script must be invoked via a bash modulino wrapper with `MODULINO_WRAPPER` set. Then run: my-script -generate-completion > \ ~/.local/share/bash-completion/completions/my-script Wrappers generated by [CPAN::Maker::Bootstrapper](https://metacpan.org/pod/CPAN%3A%3AMaker%3A%3ABootstrapper) set `MODULINO_WRAPPER` automatically. - How do I add my own internal commands? Add entries to `%INTERNAL_COMMANDS` before calling `new()`: our %INTERNAL_COMMANDS = ( %CLI::Simple::INTERNAL_COMMANDS, '-my-command' => \&_cmd_my_command, ); # 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!`. _Note: If your option includes a one-letter short-cut and the alias does not start with the same letter it will not be automatically enabled as a short-cut._ - Accessors are created for both names Accessors 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. If both the canonical name and its alias are provided on the command line, the alias wins and becomes the final value for both names. - No duplicate injection If the alias already exists in `option_specs`, it will not be injected again; value mirroring still occurs. - 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 `--cfg` (the alias) is used. _Note: In role-based applications using a YAML manifest, command aliases are expressed by mapping the alias command directly to the target sub name rather than a role class. See ["ROLE-BASED ARCHITECTURE"](#role-based-architecture)._ ## Recommendations - Keep the canonical spec single-named Define a single canonical name in `option_specs` and add other spellings via `alias`. Avoid multi-name specs like `config|cfg=s`; use `alias` instead. - Document your precedence If you prefer the alias name to win when both are supplied, enforce that in your application or adjust the mirroring order. By default, the canonical name wins. # ERRORS/EXIT CODES When you execute the `run()` method it passes control to the method that implements the command specified on the command line. Your method is expected to return 0 for success or an error code that you can pass to the shell on exit. exit CLI::Simple->new(commands => { foo => \&cmd_foo })->run(); ## Exit Codes `CLI::Simple` uses conventional exit codes so that calling scripts can distinguish between normal completion and error conditions. - '0' Successful completion of a command (`SUCCESS`). - '1' General usage error, such as `--help` display via `pod2usage`, or an invalid command line (`FAILURE`). - '2' Option parsing failure, such as an unrecognized option or invalid argument (also reported as `FAILURE`). - Any other code If a user-supplied command callback explicitly calls `exit()` or returns a numeric value other than 0 - 2, that code is passed through unchanged to the shell. This allows application-specific exit codes. # 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.0.3/bin/0000755000175100017510000000000015213050032013461 5ustar rlauerrlauerCLI-Simple-2.0.3/bin/cli-simple0000755000175100017510000000043215213050030015442 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.0.3/bin/create-modulino.pl0000755000175100017510000000633115213050030017111 0ustar rlauerrlauer#!/usr/bin/env perl package 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 FindBin qw($RealBin); use parent qw(CLI::Simple); caller or __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 // $RealBin; $installbindir = abs_path($installbindir); die "ERROR: no such directory or inaccessible\n" if !-d $installbindir; local $RS = undef; my $script = ; # remove pod $script =~ s/\A(.*)^=pod.*\z/$1/xsm; print {*STDERR} $script; # customize $script =~ s/[@]MODULINO_WRAPPER[@]/$alias/xsm; $script =~ s/[@]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@ 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 "$@" =pod =head1 NAME CLI::Simple::Modulino - Create CLI wrapper around a modulino =head1 SYNOPSIS # create $RealBin/cli-simple create-modulino -m CLI::Simple # create /usr/local/bin/cli-simple create-modulino -i /usr/local/bin -m CLI::Simple # create /usr/local/bin simple create-modulino -a simple -i /usr/local/bin -m CLI::Simple =head1 USAGE =head2 Options -h, --help help -a, --alias name of the modulino (default: lower cased snake cased module name) -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 __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.0.3/postamble0000644000175100017510000000031415213050030014616 0ustar rlauerrlauerinstall :: $(NOECHO) ln -sf $(INSTALLMAN3DIR)/CLI::Simple::Shell.3pm $(INSTALLMAN3DIR)/cli-simple.3pm $(NOECHO) echo $(INSTALLMAN3DIR)/cli-simple.3pm >> $(DESTINSTALLSITEARCH)/auto/$(FULLEXT)/.packlist CLI-Simple-2.0.3/MANIFEST0000644000175100017510000000132615213050032014044 0ustar rlauerrlauerbin/cli-simple bin/create-modulino.pl 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/Scaffold.pm lib/CLI/Simple/Shell.pm lib/CLI/Simple/Utils.pm Makefile.PL MANIFEST This list of files MANIFEST.SKIP postamble README.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/cli-simple-manifest.t META.yml Module YAML meta-data (added by MakeMaker) META.json Module JSON meta-data (added by MakeMaker) CLI-Simple-2.0.3/lib/0000755000175100017510000000000015213050032013457 5ustar rlauerrlauerCLI-Simple-2.0.3/lib/CLI/0000755000175100017510000000000015213050032014066 5ustar rlauerrlauerCLI-Simple-2.0.3/lib/CLI/Simple.pm0000444000175100017510000017337315213050030015667 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); 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 List::Util qw(zip none pairs any); use Log::Log4perl qw(); use Pod::Usage; use Scalar::Util qw(reftype); our $VERSION = '2.0.3'; our $GETOPT_EXIT_ON_ERROR = $TRUE; our $GETOPT_STATUS; our $GETOPT_ERROR_MESSAGE; __PACKAGE__->follow_best_practice; __PACKAGE__->mk_accessors( qw( _command _command_args _commands _program _abbreviations ) ); our $USE_LOGGER = $FALSE; our $AUTO_DEFAULT = $FALSE; our $AUTO_HELP = $FALSE; 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); use parent qw(Exporter Class::Accessor::Fast); # CLI::Simple 2.0.0 additions our $MANIFEST; # package-level, per-consumer class caller or __PACKAGE__->main(); ######################################################################## sub import { ######################################################################## my ( $class, @args ) = @_; if ( any { $_ eq ':roles' } @args ) { my $caller = caller; 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 ) }; undef $path if $path && !-e $path; $class->_load_manifest( $caller, $path ) if $path; } } # preserve existing Exporter behaviour $class->export_to_level( 1, $class, grep { $_ ne ':roles' && !/[.]ya?ml\z/xsm } @args ); return; } ######################################################################## sub _load_manifest { ######################################################################## my ( $class, $target, $yaml_file ) = @_; require YAML::Tiny; my $manifest = YAML::Tiny::LoadFile($yaml_file) or die "ERROR: could not load manifest: $yaml_file\n"; if ( my $commands = $manifest->{commands} ) { # derive unique roles from command values my %seen; my @roles = grep { !$seen{$_}++ && _is_class_name($_) } values %{$commands}; require Role::Tiny; Role::Tiny->apply_roles_to_package( $target, @roles ); # build dispatch table: 'code-review' => \&cmd_code_review my %dispatch; for my $cmd ( keys %{$commands} ) { my $value = $commands->{$cmd}; my $method = choose { return $value if !_is_class_name($value); # derive method from command key, not role class name # create-config -> cmd_create_config # install -> cmd_install ( my $m = "cmd_$cmd" ) =~ s/-/_/gxsm; return $m; }; die sprintf "ERROR: %s does not implement %s\n", $value, $method if !$target->can($method); $dispatch{$cmd} = $target->can($method); } $manifest->{_dispatch} = \%dispatch; } # store on the target class no strict 'refs'; ## no critic ${"${target}::_CLI_MANIFEST"} = $manifest; 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} // [] ) : [], default_options => $manifest ? ( $manifest->{default_options} // {} ) : {}, extra_options => $manifest ? ( $manifest->{extra_options} // [] ) : [], commands => $manifest ? $manifest->{_dispatch} : { default => \&usage }, abbreviations => $manifest ? ( $manifest->{abbreviations} // $FALSE ) : $FALSE, ); return $cli->run; } ######################################################################## sub _use_logger { ######################################################################## return $USE_LOGGER; } ######################################################################## sub use_log4perl { ######################################################################## my ( $self, %args ) = @_; my $class = ref $self || $self; my ( $log_level, $level, $log4perl_conf ) = @args{qw(log_level level config)}; $level //= $log_level; { no strict 'refs'; ## no critic (ProhibitNoStrict) $USE_LOGGER = $TRUE; *{"${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'); } if ( !$self->can('set_log_level') ) { $self->mk_accessors('log_level'); } return $self; } ######################################################################## sub new { ######################################################################## my ( $class, @params ) = @_; my %args = ref $params[0] ? %{ $params[0] } : @params; my ( $default_options, $option_specs, $commands, $extra_options, $abbreviations, $error_handler, $alias ) = @args{qw(default_options option_specs commands extra_options abbreviations error_handler alias)}; 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 //= []; croak sprintf "ERROR: 'commands' is required\nusage: %s->new( option_specs => specs, commands => commands);\n", __PACKAGE__ if !$option_specs || !$commands; $default_options //= {}; my $options = { %{$default_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) { s/\-/_/xsmg; if ( !$class->can( 'get_' . $_ ) ) { $class->mk_accessors($_); } $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} } ) { croak sprintf "ERROR: no command: %s\n" if !$commands->{ $p->[1] }; $commands->{ $p->[0] } = $commands->{ $p->[1] }; } } } my $self = $class->SUPER::new( \%cli_options ); # 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 = 'default'; } elsif ($AUTO_HELP) { $command = 'help'; } } $self->set__command($command); $self->set__command_args( [@ARGV] ); $self->set__commands($commands); 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); } else { $help->($self); } } $self->set__abbreviations( $abbreviations // $FALSE ); $self->set__program("$RealBin/$RealScript"); $self->validate_command; $self->init_logger; $self->can('init') && $self->init(); return $self; } ######################################################################## sub _leave { ######################################################################## my (@args) = @_; my ($exit_code) = ref $args[0] ? $args[1] : $args[0]; exit $exit_code; } ######################################################################## sub init_logger { ######################################################################## my ($self) = @_; if ( $self->_use_logger ) { if ( my $config = $self->get_log4perl_conf ) { Log::Log4perl->init( ref $config ? $config : \$config ); } else { Log::Log4perl->import(':easy'); Log::Log4perl->easy_init( $LOG_LEVELS{error} ); } my $logger = Log::Log4perl->get_logger(q{}); $self->set_logger($logger); my $level = $self->get_log_level // $self->get_log4perl_level; $self->set_log_level($level); if ($level) { $logger->level( $LOG_LEVELS{$level} ); } } my $commands = $self->commands; my $command = $self->command; return $self if !$commands->{$command} || reftype( $commands->{$command} ) ne 'ARRAY'; my ( $sub, $log_level ) = @{ $commands->{$command} }; return $self if !$self->get_logger; $log_level = $self->get_log_level // $log_level; $self->get_logger->level( $LOG_LEVELS{$log_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 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 = map { @{$_} } zip \@vars, [ @{$command_args}[ 0 .. $#vars ] ]; delete $args{''}; return wantarray ? %args : \%args; } ######################################################################## sub default_command { ######################################################################## goto &usage; } ######################################################################## sub usage { ######################################################################## my ($self) = @_; my $input = $ENV{MODULINO_WRAPPER} eq 'cli-simple' ? $INC{'CLI/Simple/Shell.pm'} : $self->get__program; pod2usage( -noperldoc => 1, -exitval => 'NOEXIT', -input => $input, ); 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 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; 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}; return $handler->($self) if ref $handler ne 'ARRAY'; 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 =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 CLI::Simple::Constants qw(:booleans :chars); use CLI::Simple qw($AUTO_HELP $AUTO_DEFAULT); use parent qw(CLI::Simple); caller or __PACKAGE__->main(); sub execute { my ($self) = @_; # retrieve a CLI option my $file = $self->get_file; ... } sub list { my ($self) = @_ # retrieve a command argument my ($file) = $self->get_args(); ... } sub main { # Disable auto-default for single commands, enable auto-help $AUTO_DEFAULT = 0; $AUTO_HELP = 1; my $cli = MyScript->new( option_specs => [ qw( help format=s file=s) ], default_options => { format => 'json' }, # set some defaults extra_options => [ qw( content ) ], # non-option, setter/getter commands => { execute => \&execute, list => \&list, } alias => { options => { fmt => 'format' }, commands => { ls => 'list' } }, ); return $cli->run(); } 1; # role-based CLI Application (2.0.0) # create a YAML manifest C in your project root: --- commands: 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. =head1 VERSION This documentation refers to version 2.0.3. =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 * optional role-based architecture via YAML manifest =item * built-in scaffolding tools for migrating legacy scripts to roles =item * bash completion script generation for modulino wrappers =back =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 __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 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 __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: --- commands: frobnicate: My::Script::Role::Frobnicate list: My::Script::Role::List options: - help|h - verbose|v - output|o=s Each role implements one or more commands: package My::Script::Role::Frobnicate; use Role::Tiny; use CLI::Simple::Constants qw(:booleans); sub cmd_frobnicate { my ($self) = @_; ... return $SUCCESS; } 1; To scaffold role stubs from an existing modulino, run the built-in C<-scaffold> command: my-script -scaffold To scaffold from an existing manifest - including a new one written by hand or generated by C<-dump-spec> - pass the spec file path: cli-simple -scaffold my-script.yml Or let C generate the manifest and scaffold from an existing modulino in one step: my-script -migrate See L for the complete workflow including the baby-step migration path. =head1 ROLE-BASED ARCHITECTURE C 2.0.0 introduces an optional role-based architecture for applications that have grown beyond a single module. Commands are implemented in dedicated L roles and declared in a YAML manifest. C composes the roles, builds the dispatch table, and provides an inherited C - potentially reducing your main module to a single declaration. =head2 The YAML Manifest The manifest is a YAML file that declares your commands, options, and defaults. By convention the filename is derived from your module name: My::Script -> my-script.yml CPAN::Maker::Bootstrapper -> cpan-maker-bootstrapper.yml C locates the manifest via L using the distribution name derived from the module name. The manifest must be installed as part of the distribution - it cannot be loaded from an arbitrary location. I. A manifest that was not installed as part of the distribution cannot be loaded. This provides the same security model as Perl module loading itself.> A minimal manifest: --- commands: frobnicate: My::Script::Role::Frobnicate list: My::Script::Role::List options: - help|h - verbose|v - output|o=s A complete manifest with all supported keys: --- commands: frobnicate: My::Script::Role::Frobnicate list: My::Script::Role::List default: cmd_frobnicate options: - help|h - verbose|v - output|o=s default_options: verbose: 0 extra_options: - dbh - config_data =head2 Command Values Each command in the manifest maps to either a role class name or a sub name: =over 4 =item * B (contains C<::>) - the role is composed into your main module and the method C> is resolved from the role. C resolves to C. =item * B - resolved directly via C on your class. Use this for alias commands that point to an existing method: default: cmd_frobnicate =back =head2 Roles With No Commands Some roles provide framework behavior rather than commands - for example an C method for startup validation. Since these roles have no command entry in the manifest they must be composed manually in your main module: package My::Script; use CLI::Simple qw(:roles); use Role::Tiny::With; use parent qw(CLI::Simple); with 'My::Script::Role::Init'; caller or exit __PACKAGE__->main; 1; I will support an C key in the manifest to handle this automatically.> =head2 Activating Role-Based Architecture Add C<:roles> to your C statement: use CLI::Simple qw(:roles); This triggers manifest loading at compile time. The manifest is located using the fallback chain described above. Roles are composed into your class and the dispatch table is built before C is called. =head2 The Inherited main() When using C<:roles>, your class inherits C from C. It reads the manifest, constructs the object with the manifest's options and dispatch table, and calls C: caller or exit __PACKAGE__->main; Override C in your subclass only if you need to add behaviour that cannot be expressed in the manifest or C. =head2 Distributing the Manifest Add the manifest to your distribution's share directory. C users can add it C in C so it is installed into the share directory: extra-files: - share: - my-script.yml During development the manifest is found via C<%INC>. After installation it is found via L. No code changes required between the two environments. =head1 PHILOSOPHY AND DESIGN PRINCIPLES C is intentionally minimalist. It provides just enough structure to build command-line tools with subcommands, option parsing, and help handling -- but without enforcing any particular framework or lifecycle. =head2 Not a Framework This module is not L, L, or a full application toolkit. Instead, it offers: =over 4 =item * An object-oriented base class with a clean C dispatcher =item * Command-line parsing via C =item * Built-in logging via C =item * Subclass hooks like C for setup and validation =item * Optional role-based architecture via YAML manifest for larger applications =back 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 features (e.g., type constraints, 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 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 For role-based applications using C, the YAML manifest is loaded at compile time during C. Roles are composed into the calling class and the dispatch table is built before C is ever called. Single-module applications skip this phase entirely. =item * B => C)> The constructor parses command-line arguments via C, creates accessors for all options, and calls your C method. Inside C, your application has full access to the parsed options and arguments. This phase is the ideal hook for all final setup tasks, such as: =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 * 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 (e.g., to run C when no command is given), you can set C<$AUTO_HELP>, explicitly set the C command in the C hash you pass to the constructor or use C to set one inside the C method. =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 role-based applications using a YAML manifest, a C command that aliases another command should map to the sub name directly rather than a role class: commands: default: cmd_install install: My::Module::Role::Installer =head2 C<$AUTO_HELP> and C<$AUTO_DEFAULT> Two package variables can be used to further control the lifecycle. By default, the framework provides no default command as explained in the sections above. Some scripters may want default behaviors that assume a command or provide usage if no command is provided. =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 if you have only 1 command defined and no command is provided on the command line. When true, it will prepend the single command name to the argument list, allowing any subsequent arguments to be correctly parsed as args for that command. default: false =back =head1 CONSTANTS C does not define its own constants directly, but it is often used in conjunction with L, which provides a collection of exportable values commonly needed in command-line scripts. 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 ADDITIONAL NOTES =over 4 =item * All options are case insensitive =item * See L to learn about additional utilities useful when writing scripts, including C, C, and C. =item * C<%INTERNAL_COMMANDS> is a package variable - subclasses can add their own internal commands by pushing entries into the hash before calling C. =back =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. Internal commands are dispatched via the C<%INTERNAL_COMMANDS> package variable: our %INTERNAL_COMMANDS = ( '-generate-completion' => \&_cmd_generate_completion, '-dump-spec' => \&_cmd_dump_spec, '-scaffold' => \&_cmd_scaffold, '-migrate' => \&_cmd_migrate, ); Subclasses can add their own internal commands by extending the hash before C is called: our %INTERNAL_COMMANDS = ( %CLI::Simple::INTERNAL_COMMANDS, '-my-command' => \&_cmd_my_command, ); =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 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 the first argument if provided, then C if set, then inferred from the module name. If the inferred name cannot be found in C, a warning is issued but the completion script is still generated. 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. They are not aligned however C is set by the bash wrapper. my-alias -generate-completion =item Case 3: Your modulino is an alias not created by C The script name C is not aligned with your module name C and your modulino wrapper does not set C. The C<-generate-completion> script called by your custom wrapper most likely only resolves the program name as the path to your Perl module: path-to-modules/My/Module.pm ...in this case you need to supply the alias name or set C in the environment. my-alias -generate-completion my-alias =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 tarball contains role stubs, a slimmed main module with extracted POD, a C with inter-module dependencies, and the YAML manifest. my-script -scaffold # introspect live module cli-simple -scaffold my-script.yml # scaffold from spec file The tarball is named C by convention (the lower case snake cased version of the class name). 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. Feed the tarball to L via the C command to 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 new new( args ) Instantiates a new C instance, parses options, optionally initializes logging, and makes options available via dynamically generated accessors. 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, the C method will treat the provided command as a prefix and compare it to the keys in the command hash. If exactly one match is found, it will be used. If more than one match is found, or if no match is found, C will throw an exception. 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 apply if the corresponding option is not given on the command line. =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. =back =head2 command command command(command) Get 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 commands (required) commands commands(command, handler) Returns the hash you passed in the constructor as C or can be used to insert a new command into the C hash. C should be a code reference. commands(foo => sub { return 'foo' }); =head2 main __PACKAGE__->main; For role-based applications, C
is inherited from C and reads the YAML manifest loaded during C. It constructs the object with the manifest's options, default options, extra options, and dispatch table, 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 run Execute the script with the given options, commands and arguments. The C method interprets the command line and passes control to your command subroutines. Your subroutines should return a 0 for success and a non-zero value for failure. This error code is passed to the shell as the script return code. =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'); # args 1 and 3 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 you define your own C method, it will be called by the constructor. Use this method to perform any actions you require before you execute the C method. =head1 USING PACKAGE VARIABLES You can pass the necessary parameter required to implement your command line scripts in the constructor or some people prefer to see them clearly defined in the code. Accordingly, you can use package variables with the same name as the constructor arguments (in upper case). our $OPTION_SPECS = [ qw( help|h log-level=s|L debug|d ) ]; our $COMMANDS = { foo => \&foo, bar => \&bar, }; Subclasses can also extend the built-in internal commands by adding entries to C<%INTERNAL_COMMANDS>: our %INTERNAL_COMMANDS = ( %CLI::Simple::INTERNAL_COMMANDS, '-my-command' => \&_cmd_my_command, ); =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 ) ] ); In your command subroutines, you can access these values using automatically generated getter methods: $cli->get_foo(); $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 set_args Resets the positional arguments. $self->set_args(qw(foo 1)); This method overrides the positional arguments originally passed to the script. You can achieve the same behavior by calling the C in scalar context and modifying the reference. my $args = $self->get_args; $args->[1] = '2'; Use this technique when you want don't want to alter the entire set of arguments. =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.> =head1 CUSTOM ERROR HANDLER By default, C will exit if C returns a false value, indicating an error while parsing options. You can override this behavior in one of two ways: =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 =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. =head1 ADDING USAGE TO YOUR SCRIPTS To provide built-in usage/help output, include a C<=head1 USAGE> section in your script's POD: =head1 USAGE usage: myscript [options] command args Options ------- --help, -h Display help ... If the user supplies the command C, or the C<--help> option, C will display this section automatically: perl myscript.pm --help perl myscript.pm help =head2 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 usage information that differs from the embedded POD. Without a custom handler, C defaults to displaying the C POD section. =head1 ADDING ADDITIONAL SETTERS 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. =head1 LOGGING C integrates with L to provide structured logging for your scripts. To enable logging, call the class method C in your module or script: __PACKAGE__->use_log4perl( level => 'info', config => $log4perl_config_string ); If you do not explicitly include a C option in your C, 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 C, C, C, etc. =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 array reference should contain at least two elements: =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; 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? No. If your script doesn't support multiple commands, you can specify a C key instead: commands => { default => \&main } =item * Must I subclass C? No. You can use it procedurally or functionally. =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 __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? Your script must be invoked via a bash modulino wrapper with C set. Then run: my-script -generate-completion > \ ~/.local/share/bash-completion/completions/my-script Wrappers generated by L set C automatically. =item * How do I add my own internal commands? Add entries to C<%INTERNAL_COMMANDS> before calling C: our %INTERNAL_COMMANDS = ( %CLI::Simple::INTERNAL_COMMANDS, '-my-command' => \&_cmd_my_command, ); =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. I =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. If both the canonical name and its alias are provided on the command line, the alias wins and becomes the final value for both names. =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<--cfg> (the alias) is used. I.> =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 When you execute the C method it passes control to the method that implements the command specified on the command line. Your method is expected to return 0 for success or an error code that you can pass to the shell on exit. exit CLI::Simple->new(commands => { foo => \&cmd_foo })->run(); =head2 Exit Codes C uses conventional exit codes so that calling scripts can distinguish between normal completion and error conditions. =over 4 =item * '0' Successful completion of a command (C). =item * '1' General usage error, such as C<--help> display via C, or an invalid command line (C). =item * '2' Option parsing failure, such as an unrecognized option or invalid argument (also reported as C). =item * Any other code If a user-supplied command callback explicitly calls C or returns a numeric value other than 0 - 2, that code is passed through unchanged to the shell. This allows application-specific exit codes. =back =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.0.3/lib/CLI/Simple/0000755000175100017510000000000015213050032015317 5ustar rlauerrlauerCLI-Simple-2.0.3/lib/CLI/Simple/Constants.pm0000444000175100017510000001004415213050030017624 0ustar rlauerrlauerpackage CLI::Simple::Constants; use strict; use warnings; use Log::Log4perl::Level; use parent qw(Exporter); our $VERSION = '2.0.3'; use Readonly; Readonly::Hash our %LOG_LEVELS => ( debug => $DEBUG, trace => $TRACE, warn => $WARN, error => $ERROR, fatal => $FATAL, info => $INFO, ); 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 $FAT_ARROW $OCTOTHORP $PERIOD $QUESTION_MARK $SLASH $SPACE $TEMPLATE_DELIMITER $UNDERSCORE } ], 'strings' => [ qw{ $PADDING } ], ); # 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 $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; foreach my $k ( keys %EXPORT_TAGS ) { push @EXPORT_OK, @{ $EXPORT_TAGS{$k} }; } $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.0.3/lib/CLI/Simple/Shell.pm0000444000175100017510000000173315213050030016724 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.0.3/lib/CLI/Simple/Scaffold.pm0000444000175100017510000000555215213050030017401 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.0.3/lib/CLI/Simple/DumpSpec.pm0000444000175100017510000000206515213050030017374 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__ CLI-Simple-2.0.3/lib/CLI/Simple/Helpers.pm0000444000175100017510000001627715213050030017270 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.0.3/lib/CLI/Simple/Utils.pm0000444000175100017510000001221415213050030016751 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 parent qw(Exporter); our @EXPORT_OK = qw( choose dmp dump_json normalize_options slurp slurp_json toPascalCase toCamelCase to_snake_case ); our $VERSION = '2.0.3'; 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 @result = shift->(); return wantarray ? @result : $result[0]; } ######################################################################## 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; 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 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. =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.0.3/lib/CLI/Simple/Migrate.pm0000444000175100017510000000170115213050030017240 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.0.3/MANIFEST.SKIP0000644000175100017510000000004115213050032014602 0ustar rlauerrlauerMakefile$ MYMETA.json MYMETA.yml CLI-Simple-2.0.3/Makefile.PL0000644000175100017510000000643715213050032014675 0ustar rlauerrlauer# autogenerated by /home/rlauer/lib/perl5/CPAN/Maker.pm on Fri Jun 12 14:29:45 2026 use 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/cli-simple', 'bin/create-modulino.pl' ], MAN1PODS => { 'bin/create-modulino.pl' => 'blib/man1/create-modulino.1' }, PREREQ_PM => { 'Class::Accessor::Fast' => '0.51', 'File::ShareDir' => '1.118', 'File::Which' => '1.23', 'JSON' => '4.07', 'List::Util' => '1.56', 'Log::Log4perl' => '1.57', 'Log::Log4perl::Level' => '0', 'Readonly' => '2.05', 'Role::Tiny' => '0', '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.002004', 'Test::Exit' => '0.11', 'Test::Output' => '1.036', 'YAML::Tiny' => '1.76' }, META_MERGE => { 'meta-spec' => { 'version' => 2 }, 'provides' => { 'CLI::Simple' => { 'file' => 'lib/CLI/Simple.pm', 'version' => '2.0.3' }, 'CLI::Simple::Constants' => { 'file' => 'lib/CLI/Simple/Constants.pm', 'version' => '2.0.3' }, '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::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.0.3' } }, '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.0.3/t/0000755000175100017510000000000015213050032013154 5ustar rlauerrlauerCLI-Simple-2.0.3/t/04-cli-simple-help.t0000644000175100017510000000147315213050030016551 0ustar rlauerrlauer#!/usr/bin/env perl # Test::Exit must be compiled before other code that calls exit use Test::Exit; use Test::More; use Test::Output; use Pod::Usage; package Foo; use strict; use warnings; use parent qw(CLI::Simple); =pod =head1 USAGE Some usage =head2 Options blah blah =cut package main; use strict; use warnings; local @ARGV = qw(--help); local $ENV{PAGER} = q{}; local $ENV{PERLDOC} = q{}; use CLI::Simple qw($AUTO_HELP); ######################################################################## subtest 'help' => sub { ######################################################################## $AUTO_HELP = 1; stdout_like( sub { exits_ok { Foo->new( commands => { foo => \&foo }, option_specs => ['help'] ); }, 'exits ok'; }, qr/^Usage/xsmi ); }; done_testing; 1; CLI-Simple-2.0.3/t/05-cli-simple-args.t0000644000175100017510000000217315213050030016554 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.0.3/t/00-cli-simple-constants.t0000644000175100017510000000017315213050030017625 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.0.3/t/03-cli-simple-types.t0000644000175100017510000000221015213050030016752 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.0.3/t/06-cli-simple-default.t0000644000175100017510000000331015213050030017237 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; use CLI::Simple qw($AUTO_DEFAULT); use vars qw(@ARGV); my @options = qw( foo bar=s ); ######################################################################## subtest 'one command' => sub { ######################################################################## local @ARGV = qw(); local $CLI::Simple::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); local $CLI::Simple::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(); use CLI::Simple qw($AUTO_HELP); $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.0.3/t/00-cli-simple.t0000644000175100017510000000016015213050030015607 0ustar rlauerrlauer#!/usr/bin/env perl use strict; use warnings; use Test::More tests => 1; use_ok('CLI::Simple'); 1; __END__ CLI-Simple-2.0.3/t/00-cli-simple-utils.t0000644000175100017510000000016715213050030016754 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.0.3/t/01-cli-simple.t0000644000175100017510000001164515213050030015622 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'; }; done_testing; 1; __END__ 1; CLI-Simple-2.0.3/t/02-cli-simple-logging.t0000644000175100017510000000157215213050030017245 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_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.0.3/t/cli-simple-manifest.t0000644000175100017510000001375415213050030017213 0ustar rlauerrlauer#!/usr/bin/env perl use strict; use warnings; use File::Temp qw(tempfile); use Test::More; use YAML::Tiny qw(DumpFile); 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; } ######################################################################## # Command name -> method name transformation ######################################################################## { my @cases = ( [ 'annotate', 'cmd_annotate' ], [ 'code-review', 'cmd_code_review' ], [ 'update-annotations', 'cmd_update_annotations' ], [ 'pod-finding', 'cmd_pod_finding' ], [ 'release-notes', 'cmd_release_notes' ], ); for my $case (@cases) { my ( $cmd, $expected ) = @{$case}; ( my $got = "cmd_$cmd" ) =~ s/-/_/gxsm; is( $got, $expected, "name transform: $cmd -> $expected" ); } } ######################################################################## # _load_manifest: roles applied and dispatch table built ######################################################################## { 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 ); ok( CLI::Simple::Test::Consumer->can('cmd_foo'), '_load_manifest: cmd_foo composed into consumer' ); ok( CLI::Simple::Test::Consumer->can('cmd_bar'), '_load_manifest: cmd_bar composed into consumer' ); ok( CLI::Simple::Test::Consumer->can('cmd_baz'), '_load_manifest: cmd_baz composed from second role' ); my $manifest = CLI::Simple::Test::Consumer->_manifest; ok( $manifest, '_manifest returns stored manifest' ); ok( exists $manifest->{_dispatch}{foo}, 'dispatch table has foo' ); ok( exists $manifest->{_dispatch}{bar}, 'dispatch table has bar' ); ok( exists $manifest->{_dispatch}{baz}, 'dispatch table has baz' ); my $obj = bless {}, 'CLI::Simple::Test::Consumer'; is( $manifest->{_dispatch}{foo}->($obj), 'foo', 'dispatch foo calls cmd_foo' ); is( $manifest->{_dispatch}{baz}->($obj), 'baz', 'dispatch baz calls cmd_baz' ); } ######################################################################## # Role deduplication - same role for multiple commands ######################################################################## { package CLI::Simple::Test::RoleC; use Role::Tiny; sub cmd_one { return 'one' } sub cmd_two { return 'two' } } { my $yaml = write_manifest( options => [qw(help|h)], commands => { 'one' => 'CLI::Simple::Test::RoleC', 'two' => 'CLI::Simple::Test::RoleC', }, ); { package CLI::Simple::Test::ConsumerC; use parent qw(CLI::Simple); } my $ok = eval { CLI::Simple->_load_manifest( 'CLI::Simple::Test::ConsumerC', $yaml ); 1; }; ok( $ok, 'deduplication: same role for two commands does not die' ); ok( CLI::Simple::Test::ConsumerC->can('cmd_one'), 'cmd_one available after dedup' ); ok( CLI::Simple::Test::ConsumerC->can('cmd_two'), 'cmd_two available after dedup' ); } ######################################################################## # Error: role does not implement the expected method ######################################################################## { package CLI::Simple::Test::RoleEmpty; use Role::Tiny; # deliberately no cmd_missing } { my $yaml = write_manifest( options => [qw(help|h)], commands => { 'missing' => 'CLI::Simple::Test::RoleEmpty' }, ); { package CLI::Simple::Test::ConsumerBad; use parent qw(CLI::Simple); } my $err = do { local $@; eval { CLI::Simple->_load_manifest( 'CLI::Simple::Test::ConsumerBad', $yaml ) }; $@; }; like( $err, qr/does not implement cmd_missing/, 'error when role missing required method' ); } ######################################################################## # Backward compatibility: classes without YAML are unaffected ######################################################################## { package CLI::Simple::Test::Legacy; use parent qw(CLI::Simple); sub cmd_legacy { return 'legacy' } } { ok( !CLI::Simple::Test::Legacy->_manifest, 'backward compat: no manifest on class that did not load YAML' ); ok( CLI::Simple::Test::Legacy->can('cmd_legacy'), 'backward compat: own methods still present' ); } ######################################################################## # manifest option/default_options/extra_options pass-through ######################################################################## { my $yaml = write_manifest( options => [qw(help|h verbose! format=s)], default_options => { format => 'json' }, extra_options => [qw(content)], commands => { 'foo' => 'CLI::Simple::Test::RoleA' }, ); { package CLI::Simple::Test::ConsumerD; use parent qw(CLI::Simple); } CLI::Simple->_load_manifest( 'CLI::Simple::Test::ConsumerD', $yaml ); my $m = CLI::Simple::Test::ConsumerD->_manifest; is_deeply( $m->{default_options}, { format => 'json' }, 'manifest preserves default_options' ); is_deeply( $m->{extra_options}, [qw(content)], 'manifest preserves extra_options' ); is_deeply( $m->{options}, [qw(help|h verbose! format=s)], 'manifest preserves options' ); } done_testing; CLI-Simple-2.0.3/ChangeLog0000644000175100017510000004412115213050030014463 0ustar rlauerrlauerTue Jun 9 14:44:42 2026 Rob Lauer [2.0.3]: * release-notes.mod: 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