NAME

Getopt::Pad::Cookbook - Recipes for common command line tasks with Getopt::Pad

DESCRIPTION

Each recipe solves one task. It shows the part of the spec that matters, one or more command lines, and what the readers return. The recipes assume a program that starts like this:

use v5.26;
use Getopt::Pad;

my $opt = GetOptions(
    # the spec from the recipe
);

Command lines are shown with the program name tool. The full description of every key is in Getopt::Pad; if you are new to Getopt::Pad, start with Getopt::Pad::Tutorial.

SWITCHES AND SIMPLE OPTIONS

A switch that can be turned off (bool)

Use the bool type. The user turns it on with --color and off with --no-color. Give it a default, so that the reader never returns undef:

options => {
    color => { type => 'bool', default => 1, help => 'Colored output' },
},
$ tool                  # $opt->color is 1
$ tool --no-color       # $opt->color is 0

A plain flag (an option without a type) can only be turned on, and reads as undef when it is not given.

A verbosity level with -v, -vv and -vvv (counter)

Use the counter type with a single-letter alias:

options => {
    'verbose|v' => { type => 'counter', help => 'More output; repeat for more (-vv)' },
},

# after GetOptions:
my $verbosity = $opt->verbose // 0;
$ tool -vvv             # $opt->verbose is 3
$ tool -v --verbose     # $opt->verbose is 2
$ tool                  # $opt->verbose is undef

Short and long names for one option (aliases)

List all names in the option key, separated by |. The first name is the primary name: it names the reader and is the one the help output shows. Single-letter names are written with one dash and can be bundled:

options => {
    'force|f'  => { help => 'Overwrite existing files (-f)' },
    'output|o' => { type => 'file', help => 'Write to this file (-o)' },
},
$ tool -f -o out.txt        # $opt->force is 1, $opt->output is 'out.txt'
$ tool -fo out.txt          # the same
$ tool --force --output=out.txt

The help output does not list aliases, so mention them in the help text.

Adding -h as a short form of --help

The automatic --help option has no short form. Declare a hidden flag and call the result object's help method yourself:

options => {
    h => { hidden => 1 },
},

# after GetOptions:
$opt->help if $opt->h;

$opt->help prints the help text and exits with status 0. It only runs when GetOptions returns, that is, when the rest of the command line is valid. With a missing required option or a missing command, -h reports that problem instead of printing the help, and after a command word -h is an unknown option. So this works well for programs without commands; in programs with commands, point users to --help.

A required option (required)

options => {
    target => { type => 'dir', required => 1, help => 'Where to write' },
},
$ tool
ERROR: missing required option '--target'

A value from a config file also satisfies required. An option cannot be required and have a default at the same time.

A default taken from an environment variable (default)

default => undef means "no default", so an environment variable can be used directly:

options => {
    target => {
        type    => 'dir',
        default => $ENV{BACKUP_TARGET},
        help    => 'Where to write (default: $BACKUP_TARGET)',
    },
},

# after GetOptions:
die "Set --target or BACKUP_TARGET\n" if !defined $opt->target;

When the variable is not set, the option has no default, and the reader returns undef if the user does not give --target either. The die is the program's own check: required cannot be combined with default.

The default is checked when GetOptions builds the spec. With a type that checks values (for example int, or dir with mustExist), an invalid value in the variable makes GetOptions die with a spec error instead of reporting a user error.

--create-default-config writes an option whose default is undef with an empty value, which the program rejects when it reads the file; remove that line from a generated file.

Hiding an option from the help (hidden)

hidden keeps an option out of the help output and out of shell completion. It works as usual otherwise:

options => {
    'debug-sql' => { hidden => 1 },
    verbose     => { help => 'More output' },
},
$ tool --debug-sql      # $opt->debugSql is 1
$ tool --help           # lists --verbose, but not --debug-sql

CHECKING VALUES

Allowing only certain values (valid)

options => {
    'log-level' => {
        type    => 'string',
        default => 'info',
        valid   => ['debug', 'info', 'warn', 'error'],
    },
},
$ tool --log-level loud
ERROR: option '--log-level': 'loud' is not one of: debug, info, warn, error

The help output lists the values, and shell completion offers them.

Allowed values that are only known at run time (valid coderef)

Give valid a coderef that returns an arrayref. It is called whenever a value is checked and whenever the shell asks for completions, so the list is always current:

options => {
    profile => {
        type  => 'string',
        valid => sub {
            opendir(my $dir, "$ENV{HOME}/.tool/profiles") or return [];
            return [ sort grep { !/\A\./ } readdir $dir ];
        },
        help => 'One of the profiles in ~/.tool/profiles',
    },
},
$ ls ~/.tool/profiles
home  work
$ tool --profile work       # $opt->profile is 'work'
$ tool --profile play
ERROR: option '--profile': 'play' is not one of: home, work

The help output does not list the values of a coderef. Keep the coderef fast: shell completion calls it on every tab press.

Validating a value with your own code (lazyValid)

When the allowed values cannot be listed, use lazyValid. It is called with the value (after the type's conversion) and returns true to accept it:

options => {
    name => {
        type      => 'string',
        lazyValid => sub { $_[0] =~ /\A[a-z][a-z0-9-]*\z/ },
        help      => 'Lowercase letters, digits and dashes',
    },
    port => {
        type      => 'int',
        lazyValid => sub { $_[0] != 22 },
        help      => 'Any port except 22',
    },
},
$ tool --name My_Tool
ERROR: option '--name': 'My_Tool' is not a valid value

For a more specific error message, write a custom type (see Getopt::Pad::Type): its check method returns the message.

Numbers within bounds (min, max)

min and max are inclusive and work for int and float:

options => {
    workers => { type => 'int',   min => 1, max => 64, default => 4 },
    ratio   => { type => 'float', min => 0, max => 1 },
},
$ tool --workers 100
ERROR: option '--workers': 100 is larger than the maximum of 64
$ tool --ratio 0.25     # $opt->ratio is 0.25

A file that must exist (mustExist)

options => {
    input => { type => 'file', mustExist => 1, help => 'File to read' },
},
args => [
    { short => 'dir', type => 'dir', mustExist => 1 },
],
$ tool --input missing.txt .
ERROR: option '--input': file 'missing.txt' does not exist

A file must be a file and a dir a directory; a directory given to a file option is reported as not existing. Do not give a mustExist option a default path that might be missing on the user's machine: the default is checked when GetOptions builds the spec, and a missing default path makes GetOptions die.

A directory that is created when missing (createPathIfMissing)

options => {
    'cache-dir' => {
        type                => 'dir',
        createPathIfMissing => 1,
        default             => "$ENV{HOME}/.cache/tool",
    },
},
$ tool                          # creates ~/.cache/tool if it is missing
$ tool --cache-dir /tmp/x/y     # creates /tmp/x and /tmp/x/y, not the default

The directory and its missing parents are created while the command line is processed, after the value passed its checks, and only for the value that is finally used. If a later option or arg turns out to be invalid, the directory may already exist. For a file option, an empty file and its parent directories are created. Nothing is created for --help.

OPTIONS WITH SEVERAL VALUES

Repeating an option (multiple)

options => {
    tag => { type => 'string', multiple => 1 },
},

# after GetOptions:
foreach my $tag ($opt->tag->@*) { ... }
$ tool --tag red --tag blue     # $opt->tag is ['red', 'blue']
$ tool                          # $opt->tag is []

The reader always returns an arrayref, so no undef check is needed. A default is an arrayref too: default => ['red'].

Comma-separated lists (csv)

Add csv to a multiple option to accept a,b,c as well:

options => {
    tag => { type => 'string', multiple => 1, csv => 1 },
},
$ tool --tag red,blue --tag green     # ['red', 'blue', 'green']
$ tool --tag 'red, blue'              # ['red', 'blue']
$ tool --tag red,,blue
ERROR: option '--tag': 'red,,blue' contains an empty item

Key/value pairs (hash)

Use hash for options like --define NAME=VALUE. The reader returns a hashref:

options => {
    define => { type => 'string', hash => 1, help => 'Set a variable' },
    limit  => { type => 'int',    hash => 1, default => { cpu => 1 } },
},

# after GetOptions:
my %vars = $opt->define->%*;
$ tool --define os=linux --define arch=x86_64
# $opt->define is { os => 'linux', arch => 'x86_64' }
# $opt->limit  is { cpu => 1 }  (the default)

$ tool --limit cpu=2 --limit memory=512
# $opt->limit  is { cpu => 2, memory => 512 }

$ tool --limit cpu=two
ERROR: option '--limit': key 'cpu': 'two' is not an integer

The type, valid and lazyValid apply to the values. A value given on the command line replaces the whole default (and the whole mapping from a config file); the keys are not merged.

Lists of records (objectlist)

Use objectlist when the user describes several things with several fields each, such as servers with a host and a port. Every word has the form INDEX.FIELD=VALUE; the reader returns an arrayref of hashrefs:

options => {
    server => {
        type       => 'string',
        objectlist => 1,
        help       => 'Servers as N.host=... N.port=...',
    },
},

# after GetOptions:
foreach my $server ($opt->server->@*) {
    printf "%s:%s\n", $server->{host}, $server->{port} // 80;
}
$ tool --server 0.host=alpha --server 0.port=8080 --server 1.host=beta
# $opt->server is [ { host => 'alpha', port => '8080' }, { host => 'beta' } ]

$ tool --server 1.host=beta
ERROR: option '--server': missing index 0

The indices must be 0, 1, 2 and so on without gaps, in any order. All fields share the option's type, valid list and lazyValid check, and these see only the value, not the field name. Check fields that need rules of their own (for example, that port is a number) in your program after GetOptions returns.

POSITIONAL ARGUMENTS

Optional arguments

Args without required are optional. They read as undef when they are missing. Required args must come first:

args => [
    { short => 'source', required => 1 },
    { short => 'target' },
],
$ tool a            # $opt->source is 'a', $opt->target is undef
$ tool a b          # $opt->source is 'a', $opt->target is 'b'
$ tool a b c
ERROR: unexpected extra argument 'c'

Any number of arguments (multiple arg)

Mark the last arg with multiple. It takes all remaining words, and its reader returns an arrayref:

args => [
    { short => 'action', required => 1 },
    { short => 'files', type => 'file', multiple => 1, mustExist => 1 },
],
$ tool check a.txt b.txt     # $opt->files is ['a.txt', 'b.txt']
$ tool check                 # $opt->files is []

Add required to the last arg to demand at least one word.

Arguments that start with a dash (negative numbers)

A word that starts with a dash is read as an option. Put -- in front of such arguments; every word after -- is an argument:

$ tool -- -5            # the arg is '-5'
$ tool -5
ERROR: Unknown option: 5

In a program with commands, put -- after the command words (tool resize -- -5): the command's level reads options again.

The values of options do not need this: --offset -5 works, because an option that takes a value always takes the next word.

COMMANDS

Dispatching commands to subroutines

Map command names to subroutines and call the one the user chose. Pass both result objects, so the handler can read the options of the top level and of its command:

my $opt = GetOptions(
    options  => { 'verbose|v' => { type => 'counter', inherit => 1 } },
    commands => {
        add    => { args => [{ short => 'name', required => 1 }] },
        remove => { args => [{ short => 'name', required => 1 }] },
        list   => {},
    },
);

my %handlers = (
    add    => \&addItem,
    remove => \&removeItem,
    list   => \&listItems,
);
$handlers{ $opt->command }->($opt, $opt->subcommand);

sub addItem {
    my ($opt, $add) = @_;
    printf "Adding %s\n", $add->name;
}

Give every command an entry in %handlers: $opt->command is always one of the declared names (unless the command is optional, see below), and a missing entry would call undef.

Options for all commands (inherit)

Declare the option once, on the level with the commands, with inherit => 1. It is then accepted before and after the command word, and its value is read from the declaring level:

options  => { 'dry-run|n' => { inherit => 1, help => 'Change nothing' } },
commands => { add => { ... }, remove => { ... } },
$ tool -n add foo        # $opt->dryRun is 1
$ tool add -n foo        # the same

Read it as $opt->dryRun, not from $opt->subcommand. A config file sets it in the top level's groups.

A command that is optional (commandRequired)

By default, a level with commands requires one. With commandRequired => 0, the command line may stop before the command word, and command returns undef:

my $opt = GetOptions(
    commandRequired => 0,
    commands        => { status => {}, sync => {} },
);

my $command = $opt->command // 'status';
$ tool          # $opt->command is undef, so $command is 'status'
$ tool sync     # $opt->command is 'sync'

Without a command word there is no result object for a command: $opt->subcommand is undef, so code that runs for the default must not read the options of a command.

Nested commands

A command spec can have commands of its own:

commands => {
    remote => {
        description => 'Manage remotes',
        commands    => {
            add => {
                args => [
                    { short => 'name', required => 1 },
                    { short => 'url', type => 'url', required => 1 },
                ],
            },
            remove => { args => [{ short => 'name', required => 1 }] },
        },
    },
},
$ tool remote add origin https://example.com/repo.git
$opt->command;                          # 'remote'
$opt->subcommand->command;              # 'add'
$opt->subcommand->subcommand->url;      # 'https://example.com/repo.git'

To dispatch on the full command path, collect the names first:

my @path;
for (my $level = $opt; defined $level->command; $level = $level->subcommand) {
    push @path, $level->command;
}
my $handler = join ' ', @path;          # 'remote add'

HELP AND VERSION

Grouping options in the help (group)

Options with the same group are listed under one heading. The groups are sorted alphabetically; options without a group are listed under Options:

options => {
    host => { type => 'string', group => 'Connection' },
    port => { type => 'int',    group => 'Connection' },
    quiet => {},
},
## Connection
   --host <>
   --port <>

## Options
   --quiet

The group is also the section name in config files.

Usage examples in the help (examples)

description => 'Resize images.',
examples    => [
    { text => 'Make all images at most 800 pixels wide', args => '--width 800 *.jpg' },
],
# Examples:
## Make all images at most 800 pixels wide
##   tool --width 800 *.jpg

A custom label in the help (typehint)

typehint replaces the type label at the end of an entry:

options => {
    host => { type => 'string', typehint => 'Hostname', help => 'Server to connect to' },
},
--host <>                   Server to connect to [Hostname]

Printing the program version (--version)

--version prints the program name and the spec's version key or, without it, $main::VERSION:

our $VERSION = '1.4.2';
my $opt = GetOptions(...);
$ tool --version
tool 1.4.2

Printing the help from your program

The result object's help method prints the help of its level and exits with status 0. For example, to show the help when a program is called without any work to do:

$opt->help if !$opt->files->@*;

To print the help of a command, call help on that command's result object: $opt->subcommand->help.

CONFIG FILES

A config file in the user's home directory (config)

config => {
    format => 'yaml',
    paths  => ['~/.config/tool.yaml'],
},

If the file exists, it is read on every run. The top-level keys of the file are the group names, and each group maps option names to values:

Options:
  log-level: debug
Connection:
  host: db.example.com
  port: 5432

The user can create a file with all defaults with tool --create-default-config ~/.config/tool.yaml.

A system-wide and a user config file

List both in paths. Every existing file is read, in order, and a later file overrides an earlier one option by option:

config => {
    format => 'yaml',
    paths  => ['/etc/tool.yaml', '~/.config/tool.yaml', '.tool.yaml'],
},

With /etc/tool.yaml setting host and port, and ~/.config/tool.yaml setting only port, the program uses host from the first file and port from the second. The value of a list or hash option is replaced as a whole, not merged.

Config files only on request (autoload, defaultPath)

With autoload => 0, no file is read unless the user passes --config. A bare --config reads defaultPath:

config => {
    format      => 'json',
    autoload    => 0,
    defaultPath => '~/.tool.json',
},
$ tool                        # no config file
$ tool --config               # reads ~/.tool.json
$ tool --config other.json    # reads other.json

A bare --config takes the next word as its path unless that word starts with a dash. Write --config= before an argument: tool --config= input.txt.

Settings for commands in a config file

The commands key of a level holds one section per command, with the same layout as the file itself:

Options:
  verbose: 1
commands:
  image:
    commands:
      resize:
        Options:
          width: 800

Each section only sets the options of its own command. Inherited options are set in the section of the level that declares them.

JSON instead of YAML

Set format => 'json'. JSON needs no extra module:

{
  "Options": { "log-level": "debug" },
  "commands": { "resize": { "Options": { "width": 800 } } }
}

TESTING AND SPECIAL CASES

Testing a command line

For command lines that should succeed, pass the words with the argv key instead of setting @ARGV. Keep the spec in a function that the program and the tests share:

# lib/My/Tool.pm
package My::Tool;
use Getopt::Pad;

sub parseCommandLine {
    my (@words) = @_;
    return GetOptions(
        argv    => \@words,
        options => { keep => { type => 'int', default => 7, min => 1 } },
        args    => [{ short => 'source', required => 1 }],
    );
}

1;

# bin/tool
use FindBin;
use lib "$FindBin::Bin/../lib";
use My::Tool;
my $opt = My::Tool::parseCommandLine(@ARGV);

# t/cli.t
use Test2::V0;
use My::Tool;

my $opt = My::Tool::parseCommandLine('--keep', '3', 'photos');
is($opt->keep, 3, 'keep');
is($opt->source, 'photos', 'source');

done_testing;

Run the tests with prove -l t, so that lib is in @INC.

A command line that fails makes GetOptions exit the process, and so do --help, --version and the other automatic options. Test those by running the program as a separate process and checking its exit status and output. This helper collects STDOUT and STDERR together:

use IPC::Open3 qw(open3);

sub runTool {
    my (@args) = @_;
    my $pid = open3(my $stdin, my $output, undef, $^X, 'bin/tool', @args);
    close $stdin;
    my $text = do { local $/; <$output> } // '';
    waitpid($pid, 0);
    return ($? >> 8, $text);
}

my ($status, $output) = runTool('--keep', '0', 'photos');
is($status, 2, 'exit status 2');
like($output, qr/0 is smaller than the minimum of 1/, 'error message');

($status, $output) = runTool('--help');
is($status, 0, '--help exits with status 0');

Put the helper and these tests into t/cli.t, before done_testing. The path bin/tool is relative to the distribution directory, where prove runs.

Passing undef as the third argument of open3 sends STDERR into the same pipe as STDOUT, so the helper cannot block on a full pipe. Set COLUMNS in the environment if a test compares help output, so the wrapping does not depend on the terminal.

Parsing words that do not come from @ARGV

argv accepts any list of words, for example options from an environment variable in front of the real command line. split only splits at whitespace; quotes in the variable are not interpreted the way a shell would:

my $opt = GetOptions(
    argv    => [ split(' ', $ENV{TOOL_OPTIONS} // ''), @ARGV ],
    options => { ... },
);

Non-ASCII values

Words from the command line are byte strings; values from config files are decoded character strings. If your program works with non-ASCII values, decode @ARGV before calling GetOptions:

use Encode qw(decode);
@ARGV = map { decode('UTF-8', $_) } @ARGV;

my $opt = GetOptions(...);

Types and config formats of your own

A custom type gives an option its own check, conversion, help label and error messages. See Getopt::Pad::Type for a type that accepts only even numbers and one that converts durations like 5m to seconds.

A custom config format reads files that are neither YAML nor JSON. See Getopt::Pad::Config::Format for a TOML format.

SEE ALSO

Getopt::Pad, Getopt::Pad::Tutorial

AUTHOR

davenonymous <perl@davenonymous.com>

COPYRIGHT AND LICENSE

Copyright 2026 davenonymous

This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself.