NAME

Getopt::Pad::Tutorial - A step-by-step introduction to Getopt::Pad

DESCRIPTION

This tutorial builds the command line of a small backup program, one feature at a time. Each step shows the complete code or the part that changed, a few command lines, and what the program prints. By the end, the program has typed options, several commands, config files, a useful --help and shell completion.

You need Perl 5.26 or later and Getopt::Pad. Step 6 uses YAML config files, which need the module YAML::XS.

The program only prints what it would do; the tutorial is about the command line, not about making backups. The full reference for every feature is Getopt::Pad.

Step 1: A first script

The program needs to know two things: the directory to back up, and where to write the backup. The first one is a positional argument (an arg), the second a named option:

#!/usr/bin/env perl
use v5.26;
use strict;
use warnings;

use Getopt::Pad;

my $opt = GetOptions(
    options => {
        'target' => {
            type     => 'dir',
            required => 1,
            help     => 'Directory the backup is written to',
        },
    },
    args => [
        { short => 'source', type => 'dir', required => 1, help => 'Directory to back up' },
    ],
);

printf "Backing up %s to %s\n", $opt->source, $opt->target;

GetOptions takes the spec, a list of key/value pairs that describes the command line. options is a hashref of option names and their specs. args is an arrayref of arg specs, in the order the args appear on the command line; short is the arg's name.

GetOptions returns the result object, with one method per option and arg, named after them: $opt->target and $opt->source. These methods are called readers. Save the script as backup, make it executable (chmod +x backup), and try it. The examples call it as backup; run it as ./backup unless the directory is in your PATH:

$ backup --target /mnt/backup photos
Backing up photos to /mnt/backup

Options and args can come in any order, and a value can also follow an =:

$ backup photos --target /mnt/backup
Backing up photos to /mnt/backup
$ backup --target=/mnt/backup photos
Backing up photos to /mnt/backup

You get a --help option without writing any code for it:

$ backup --help
# backup [options] source

## Arguments
   <source>                    [REQ] Directory to back up [Path]

## Completion
   --create-completions <>     Print a completion script for this shell to
                               STDOUT and exit
                                   Valid   = [ bash, zsh ]

## Options
   --target <>                 [REQ] Directory the backup is written to
                               [Path]

[REQ] marks what is required, <> an option that takes a value, and [Path] the type. The --create-completions option is explained in "Step 7: Shell completion".

The dir type does not check that the directory exists; it only marks the value as a path (for the help output and for shell completion). Add mustExist => 1 to an option or arg to require an existing directory.

Mistakes on the command line are reported with the help text, and the program exits with status 2. Your code after GetOptions only runs when the command line is valid:

$ backup photos
ERROR: missing required option '--target'

# backup [options] source
...

The other errors of this step read:

ERROR: missing required argument <source>
ERROR: Unknown option: taget
ERROR: unexpected extra argument 'videos'

Step 2: Types, defaults and short names

Next, the program gets a number of backups to keep, a switch to turn compression off, a dry-run switch and a verbosity level:

my $opt = GetOptions(
    options => {
        'target|t' => {
            type     => 'dir',
            required => 1,
            help     => 'Directory the backup is written to',
        },
        'keep' => {
            type    => 'int',
            default => 7,
            min     => 1,
            help    => 'Number of backups to keep',
        },
        'compress' => {
            type    => 'bool',
            default => 1,
            help    => 'Compress the backup',
        },
        'dry-run|n' => {
            help => 'Only show what would be copied',
        },
        'verbose|v' => {
            type => 'counter',
            help => 'Print more details; repeat for even more',
        },
    },
    args => [
        { short => 'source', type => 'dir', required => 1, help => 'Directory to back up' },
    ],
);

printf "source:   %s\n", $opt->source;
printf "target:   %s\n", $opt->target;
printf "keep:     %d\n", $opt->keep;
printf "compress: %s\n", $opt->compress ? 'yes' : 'no';
printf "dry run:  %s\n", $opt->dryRun ? 'yes' : 'no';
printf "verbose:  %d\n", $opt->verbose // 0;

This step introduces several new features:

  • Aliases. 'target|t' declares the option --target with the alias -t. The first name is the primary name; it names the reader. Names of one letter are written with one dash.

  • Types. int accepts whole numbers only, and min sets the smallest allowed value. bool is a switch that can be turned off with --no-compress. An option without a type, like dry-run, is a simple flag. counter counts how often it is given.

  • Defaults. default is the value when the option is not given. The default is checked like user input, so default => 0 for keep would be reported as a mistake in the spec, as soon as GetOptions runs.

  • Reader names. The method for dry-run is dryRun: dashes and underscores are dropped and the next letter is made upper case.

  • Absent switches. A flag or counter that is not given returns undef, which is why the last line uses // 0.

$ backup -t /mnt/backup photos
source:   photos
target:   /mnt/backup
keep:     7
compress: yes
dry run:  no
verbose:  0

$ backup -t /mnt/backup --keep 30 --no-compress -nvv photos
source:   photos
target:   /mnt/backup
keep:     30
compress: no
dry run:  yes
verbose:  2

-nvv is -n -v -v: single-letter options can be bundled after one dash. Values are checked against their type:

$ backup -t /mnt/backup --keep 0 photos
ERROR: option '--keep': 0 is smaller than the minimum of 1
...
$ backup -t /mnt/backup --keep many photos
ERROR: option '--keep': 'many' is not an integer
...

All built-in types are listed in "TYPES" in Getopt::Pad.

Step 3: Lists and allowed values

The program should be able to skip files by pattern, as many patterns as the user likes, and to choose between two copy methods. Add these two entries to the options hash from step 2:

'method' => {
    type    => 'string',
    default => 'tar',
    valid   => ['tar', 'rsync'],
    help    => 'How to copy the files',
},
'exclude|x' => {
    type     => 'string',
    multiple => 1,
    csv      => 1,
    help     => 'Skip files matching this pattern (may be repeated)',
},

valid lists the values the option accepts. multiple lets the option be given several times; its reader returns an arrayref. csv also splits each value at commas. Print the two new values:

printf "method:   %s\n", $opt->method;
printf "exclude:  %s\n", join(', ', $opt->exclude->@*);

Both forms of --exclude work and can be mixed (the output below leaves out the six lines from step 2):

$ backup -t /mnt/backup -x '*.tmp' -x '*.log,*.bak' --method rsync photos
method:   rsync
exclude:  *.tmp, *.log, *.bak

$ backup -t /mnt/backup --method zip photos
ERROR: option '--method': 'zip' is not one of: tar, rsync
...

When --exclude is not given, $opt->exclude is an empty arrayref, so $opt->exclude->@* never fails. Options can also hold key=value pairs (hash) and lists of records (objectlist); see "OPTIONS WITH SEVERAL VALUES" in Getopt::Pad::Cookbook.

Step 4: A helpful --help

The help output is generated from the spec, so it improves with every help text you write. A few more keys make it more useful:

our $VERSION = '1.0';

my $opt = GetOptions(
    description => 'Copy a directory to a backup location.',
    examples    => [
        {
            text => 'Back up your photos, skipping temporary files',
            args => '-t /mnt/backup -x "*.tmp" ~/photos',
        },
    ],
    options => {
        'target|t' => {
            type     => 'dir',
            required => 1,
            group    => 'Destination',
            help     => 'Directory the backup is written to',
        },
        'keep' => {
            type    => 'int',
            default => 7,
            min     => 1,
            group   => 'Destination',
            help    => 'Number of backups to keep',
        },
        'compress' => {
            type    => 'bool',
            default => 1,
            group   => 'Destination',
            help    => 'Compress the backup',
        },
        'method' => {
            type    => 'string',
            default => 'tar',
            valid   => ['tar', 'rsync'],
            help    => 'How to copy the files',
        },
        'exclude|x' => {
            type     => 'string',
            multiple => 1,
            csv      => 1,
            typehint => 'Pattern',
            help     => 'Skip files matching this pattern (may be repeated)',
        },
        'dry-run|n' => {
            help => 'Only show what would be copied',
        },
        'verbose|v' => {
            type => 'counter',
            help => 'Print more details; repeat for even more',
        },
    },
    args => [
        { short => 'source', type => 'dir', required => 1, help => 'Directory to back up' },
    ],
);
  • description is shown below the usage line.

  • examples are shown at the end.

  • group puts options under their own heading. Options without a group are listed under Options.

  • typehint sets the label at the end of the help text, here [Pattern].

  • The help output lists the aliases after the primary name, as in --target, -t, so the help texts need not mention them.

$ backup --help
# backup [options] source
# Copy a directory to a backup location.

## Arguments
   <source>                    [REQ] Directory to back up [Path]

## Completion
   --create-completions <>     Print a completion script for this shell to
                               STDOUT and exit
                                   Valid   = [ bash, zsh ]

## Destination
   --[no-]compress             Compress the backup
                                   Default = 1
   --keep <>                   Number of backups to keep
                                   Default = 7
   --target, -t <>             [REQ] Directory the backup is written to
                               [Path]

## Options
   --dry-run, -n               Only show what would be copied
   --exclude, -x <a,b,...>     Skip files matching this pattern (may be
                               repeated) [Pattern]
   --method <>                 How to copy the files
                                   Valid   = [ tar, rsync ]
                                   Default = tar
   --verbose, -v               Print more details; repeat for even more

# Examples:
## Back up your photos, skipping temporary files
##   backup -t /mnt/backup -x "*.tmp" ~/photos

The our $VERSION of the script is what the automatic --version option prints:

$ backup --version
backup 1.0

On a terminal, the help output is colored and wrapped to the terminal width. See "HELP OUTPUT" in Getopt::Pad for all details.

Step 5: Commands

The program grows: besides creating a backup, it should list the existing backups and delete old ones. Each of these tasks needs different options, so they become commands, like git commit and git log:

backup run SOURCE        create a backup
backup list              list the backups
backup prune             delete old backups

Commands are declared under commands. Each command has a spec of its own, with the same keys as the top level (except config, version and argv, which only the top level has). The top level and each command are called levels; every level has its own options:

our $VERSION = '2.0';

my $opt = GetOptions(
    description => 'Create and manage backups of a directory.',
    options     => {
        'target|t' => {
            type     => 'dir',
            required => 1,
            inherit  => 1,
            help     => 'Directory the backups are kept in',
        },
        'verbose|v' => {
            type    => 'counter',
            inherit => 1,
            help    => 'Print more details; repeat for even more',
        },
    },
    commands => {
        run => {
            description => 'Create a new backup',
            options     => {
                'compress' => {
                    type    => 'bool',
                    default => 1,
                    help    => 'Compress the backup',
                },
                'exclude|x' => {
                    type     => 'string',
                    multiple => 1,
                    csv      => 1,
                    help     => 'Skip files matching this pattern',
                },
            },
            args => [
                {
                    short    => 'source',
                    type     => 'dir',
                    required => 1,
                    help     => 'Directory to back up',
                },
            ],
        },
        list => {
            description => 'List the existing backups',
        },
        prune => {
            description => 'Delete old backups',
            options     => {
                'keep' => {
                    type    => 'int',
                    default => 7,
                    min     => 1,
                    help    => 'Number of backups to keep',
                },
                'dry-run|n' => { help => 'Only show what would be deleted' },
            },
        },
    },
);

The top level keeps the options that every command needs: --target and --verbose. Normally, the options of the top level must come before the command word, and the options of an outer level are unknown after it. inherit => 1 changes that: the option is also accepted after the command word, on every command below.

GetOptions returns the result object of the top level. Its command method returns the name of the chosen command, and its subcommand method returns another result object for that command, with the readers of the command's own options and args. A hash of subroutines turns this into a dispatcher:

my %handlers = (
    run   => \&runBackup,
    list  => \&listBackups,
    prune => \&pruneBackups,
);
$handlers{ $opt->command }->($opt, $opt->subcommand);

sub runBackup {
    my ($opt, $run) = @_;
    printf "Backing up %s to %s (verbosity %d)\n",
        $run->source, $opt->target, $opt->verbose // 0;
}

sub listBackups {
    my ($opt) = @_;
    printf "Backups in %s:\n", $opt->target;
}

sub pruneBackups {
    my ($opt, $prune) = @_;
    printf "Keeping the newest %d backups in %s%s\n",
        $prune->keep, $opt->target, $prune->dryRun ? ' (dry run)' : '';
}

An inherited option is always read from the level that declares it, so target and verbose come from $opt, while source, keep and dryRun come from the command's result object:

$ backup -t /mnt/backup run photos
Backing up photos to /mnt/backup (verbosity 0)
$ backup run -t /mnt/backup -v photos
Backing up photos to /mnt/backup (verbosity 1)
$ backup -v -t /mnt/backup prune --keep 3 -nv
Keeping the newest 3 backups in /mnt/backup (dry run)

A missing command is an error, and so is an option given on the wrong level:

$ backup -t /mnt/backup
ERROR: missing command, expected one of: list, prune, run
...
$ backup -t /mnt/backup --keep 3 prune
ERROR: Unknown option: keep
...

Every level has its own help. The top level lists the commands, and a command's help shows its own options followed by the inherited ones:

$ backup --help
# backup [options] <command>
# Create and manage backups of a directory.

## Completion
   --create-completions <>     Print a completion script for this shell to
                               STDOUT and exit
                                   Valid   = [ bash, zsh ]

## Options
   --target, -t <>             [REQ] Directory the backups are kept in
                               [Path]
   --verbose, -v               Print more details; repeat for even more

## Commands
   list                        List the existing backups
   prune                       Delete old backups
   run                         Create a new backup

$ backup prune --help
# backup prune [options]
# Delete old backups

## Options
   --dry-run, -n       Only show what would be deleted
   --keep <>           Number of backups to keep
                           Default = 7
   --target, -t <>     [REQ] Directory the backups are kept in [Path]
   --verbose, -v       Print more details; repeat for even more

Commands can have commands of their own, to any depth. See "COMMANDS" in Getopt::Pad.

Step 6: Config files

Typing -t /mnt/backup every time is tedious. A config file can hold the values the user always wants. Add a config block to the top level of the spec:

my $opt = GetOptions(
    description => 'Create and manage backups of a directory.',
    config      => {
        format => 'yaml',
        paths  => ['/etc/backup.yaml', '~/.config/backup.yaml'],
    },
    options     => { ... },     # as in step 5
    commands    => { ... },
);

paths lists the files that are read on every run, in order; files that do not exist are skipped, and a later file overrides an earlier one. A config file mirrors the help output: the top-level keys are the group names (Options for options without a group), and the commands key holds one section per command, with the same layout:

Options:
  target: /mnt/backup
commands:
  run:
    Options:
      exclude:
        - '*.tmp'
        - '*.log'
  prune:
    Options:
      keep: 14

To see where the values come from, let runBackup print the patterns it skips:

sub runBackup {
    my ($opt, $run) = @_;
    printf "Backing up %s to %s, skipping %s\n",
        $run->source, $opt->target, join(', ', $run->exclude->@*);
}

With the file above as ~/.config/backup.yaml, --target is no longer needed on the command line. The command line still wins over the file, and the file over the defaults in the spec:

$ backup run photos
Backing up photos to /mnt/backup, skipping *.tmp, *.log
$ backup prune
Keeping the newest 14 backups in /mnt/backup
$ backup prune --keep 3
Keeping the newest 3 backups in /mnt/backup
$ backup -t /media/usb run -x '*.iso' photos
Backing up photos to /media/usb, skipping *.iso

Note that -x '*.iso' replaces the whole list from the config file: the value of an option always comes from one place.

The config block adds two options. --config FILE reads that file instead of the ones in paths. --create-default-config FILE writes a starter file with all defaults of the spec. It never overwrites a file, so choose a new name:

$ backup --create-default-config ~/backup-defaults.yaml
Wrote default config to /home/user/backup-defaults.yaml

(The shell replaces ~ with your home directory before the program sees the path.)

---
commands:
  prune:
    Options:
      keep: 7
  run:
    Options:
      compress: 1

Config files are checked as strictly as the command line. The structure of every file is always checked completely; a value is checked when it is used, that is, for the commands that run and when the command line does not override it. With this bad.yaml, which misspells prune:

commands:
  prnue:
    Options:
      keep: 3

even a command that does not use the file's values fails:

$ backup -t x list --config bad.yaml
ERROR: config file 'bad.yaml': unknown command 'prnue', expected one of: list, prune, run
...

See "CONFIG FILES" in Getopt::Pad for the details, including defaultPath and JSON files.

Step 7: Shell completion

Your users can let their shell complete command names, options, allowed values and paths. The automatic --create-completions option prints a completion script for bash or zsh. The script completes the command backup, so this step assumes that the program is installed as backup in a directory of your PATH:

$ backup --create-completions bash > ~/.local/share/bash-completion/completions/backup
$ backup --create-completions zsh  > ~/.zsh/completions/_backup

(For zsh, ~/.zsh/completions must be in $fpath.) After starting a new shell, backup pr<TAB> completes to backup prune, and backup prune --<TAB> offers:

--config  --create-default-config  --dry-run  --keep  --target  --verbose

The script calls your program on every tab press to ask for the candidates, so it never has to be regenerated when the spec changes. It also means that everything your program does before GetOptions runs on every tab press: call GetOptions first. See "SHELL COMPLETION" in Getopt::Pad.

Where to go from here

SEE ALSO

Getopt::Pad, Getopt::Pad::Cookbook

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.