Skip to content

Cookbook ​

Short answers to "how do I ...?". Each recipe shows the code and its real output; the reference has the details.

A required option ​

f90
program minimal
!< A minimal FLAP program: one required option.
use flap
implicit none
type(command_line_interface) :: cli
character(99)                :: string
integer                      :: error

call cli%init(description='minimal FLAP example')
call cli%add(switch='--string', switch_ab='-s', help='a string', required=.true., act='store', error=error)
if (error /= 0) stop 1, quiet=.true.
call cli%parse(error=error)
if (error /= 0) stop 1, quiet=.true.
call cli%get(switch='-s', val=string, error=error)
if (error /= 0) stop 1, quiet=.true.
print '(A)', 'String = '//trim(string)
endprogram minimal
$ minimal
minimal: error: named option "--string" is required!

usage: minimal --string value [--help] [--markdown] [--version]

minimal FLAP example

Required switches:
  --string value, -s value
      a string

Optional switches:
  --help, -h
      Print this help message
  --markdown, -md
      Save this help message in a Markdown file
  --version, -v
      Print version
Try 'minimal --help' for help.
[exit status 1]

An option with a default, read into a number ​

f90
call cli%init(progname    = 'myapp',                         &
              version     = 'v1.0',                          &
              description = 'Demonstration program',         &
              examples    = ['myapp -i a.dat -o b.dat      ', &
                             'myapp -i a.dat -n 100 --tol 1'])
call cli%add(switch='--input',   switch_ab='-i', help='Input file',     required=.true.,  act='store', error=error)
call cli%add(switch='--output',  switch_ab='-o', help='Output file',    required=.false., act='store', def='out.dat', &
             error=error)
call cli%add(switch='--niter',   switch_ab='-n', help='Iterations',     required=.false., act='store', def='100', &
             error=error)
call cli%add(switch='--tol',     switch_ab='-t', help='Tolerance',      required=.false., act='store', def='1.0e-6', &
             error=error)
call cli%add(switch='--verbose',                 help='Verbose output', required=.false., act='store_true', &
             def='.false.', error=error)
f90
call cli%parse(error=error)
if (error /= 0) stop 1, quiet=.true.

call cli%get(switch='-i',        val=input,   error=error) ; if (error /= 0) stop 1, quiet=.true.
call cli%get(switch='-o',        val=output,  error=error) ; if (error /= 0) stop 1, quiet=.true.
call cli%get(switch='-n',        val=n,       error=error) ; if (error /= 0) stop 1, quiet=.true.
call cli%get(switch='-t',        val=tol,     error=error) ; if (error /= 0) stop 1, quiet=.true.
call cli%get(switch='--verbose', val=verbose, error=error) ; if (error /= 0) stop 1, quiet=.true.
$ myapp -i a.dat -n 50 --verbose
input   = a.dat
output  = out.dat
niter   = 50
tol     =  1.0E-06
verbose = T

get converts to the type and kind of the variable; a value that is not a number is ERROR_CASTING_NUMBER.

A flag and its negation ​

f90
! a flag with its negation: the last one passed wins
call cli%add(switch='--restart', switch_neg='--no-restart', help='Restart from the checkpoint', required=.false., &
             act='store_true', def='.true.')
$ actions --no-restart
verbosity = 0
include   = .
format    = text
restart   = F

A flag that is true unless passed ​

f90
call cli%add(switch='--no-color', help='Plain output', required=.false., act='store_false', def='.true.')
$ store_false --no-color
color = F

A verbosity counter ​

f90
! count: -v -v, -vv or --verbose --verbose give 2
call cli%add(switch='--verbose', switch_ab='-v', help='Verbosity (repeatable)', required=.false., act='count')
$ actions -vvv
verbosity = 3
include   = .
format    = text
restart   = T

A repeatable option ​

f90
! append: one value per occurrence
call cli%add(switch='--include', switch_ab='-I', help='Include directory (repeatable)', required=.false., &
             act='append', def='.')
$ actions -I src --include=lib
verbosity = 0
include   = src, lib
format    = text
restart   = T

Read it with get_varying, as a list.

An option whose value is optional ​

f90
! store*: the value is optional, the switch alone gives the default
call cli%add(switch='--format', help='Output format', required=.false., act='store*', def='text')
$ actions --format=json -v --verbose
verbosity = 2
include   = .
format    = json
restart   = T

A value can always be given inline, --format=json; for an optional value it is the only unambiguous way.

An option hidden from the help ​

f90
! a hidden flag: parsed, but not shown in the help
call cli%add(switch='--debug-internal', help='Internal debug flag', required=.false., act='store_true', def='.false.', &
             hidden=.true.)

A value from a fixed list ​

f90
call cli%add(switch='--level', switch_ab='-l', help='Verbosity level', required=.false., act='store', def='1', &
             choices='1,3,5')
! case_sensitive=.false.: WENO5 matches weno5, and get returns the declared spelling
call cli%add(switch='--scheme', help='Space scheme', required=.false., act='store', def='muscl', &
             choices='weno5,muscl', case_sensitive=.false.)
$ choices --level 3 --scheme WENO5
level = 3, scheme = weno5
$ choices --level 2
choices: error: value of named option "--level" must be chosen in: (1,3,5) but "2" has been passed!

[exit status 1]

A number within bounds ​

f90
call cli%add(switch='--cfl', help='CFL number', required=.false., act='store', def='0.8', &
             min='0', min_open=.true., max='1')                 ! (0, 1]
call cli%add(switch='--threads', help='OpenMP threads', required=.false., act='store', def='1', &
             min='1', max='256', clamp=.true.)                  ! [1, 256], out-of-range values clamped
$ ranges --cfl 0.5 --threads 999
cfl = 0.50, threads = 256
$ ranges --cfl 1.5
ranges: error: value "1.5" of "--cfl" is out of range (0, 1]!

[exit status 1]

A list of files ​

f90
call cli%add(switch='--mesh', help='Mesh file', required=.true., act='store', metavar='FILE')
! exactly 3 values
call cli%add(switch='--coords', help='X Y Z of the probe', required=.false., act='store', nargs='3', def='0 0 0')
! zero or more values: the switch alone is an empty list
call cli%add(switch='--fields', help='Fields to save', required=.false., act='store', nargs='*', def='u', &
             metavar='NAME')
! one or more values
call cli%add(switch='--weights', help='Weights', required=.false., act='store', nargs='+', def='1.0')
f90
call cli%get(switch='--mesh', val=mesh, error=error)
call cli%get(switch='--coords', val=coords, error=error)          ! a fixed-size array of the right size
call cli%get_varying(switch='--fields', val=fields, error=error)   ! allocatable arrays, any size
call cli%get_varying(switch='--weights', val=weights, error=error)
$ lists --mesh wing.grd --coords 1 2 3 --fields u v p --weights 0.5 0.25
mesh    = wing.grd
coords  = 1 2 3
fields  = (3) u v p
weights =  0.50  0.25

An input file given without a switch ​

f90
call cli%add(positional=.true., position=1, help='Input file', required=.true., act='store', metavar='INPUT')
call cli%add(positional=.true., position=2, help='Scale factor', required=.false., act='store', def='1.0', &
             metavar='SCALE')
call cli%add(switch='--verbose', help='Verbose', required=.false., act='store_true', def='.false.')
$ positional in.dat 2.5
input = in.dat, scale = 2.50, verbose = F

A file that must exist ​

f90
call cli%add(switch='--mesh', help='Mesh file', required=.true.,  act='store', readable=.true.)
call cli%add(switch='--log',  help='Log file',  required=.false., act='store', def='-', writable=.true., allow_dash=.true.)
$ paths --mesh wnig.grd
paths: error: option "--mesh": path "wnig.grd" does not exist!

Try 'paths --help' for help.
[exit status 1]

KEY=VALUE overrides of an input deck ​

f90
call cli%add(switch='--set', switch_ab='-s', help='Override input-deck parameters', required=.false., act='store', &
             nargs='+', map=.true., map_keys='cfl,nx,ny,t_end', def='cfl=0.8')
f90
cfl = 0.9_8 ; nx = 128                                                 ! the values of the input deck
call cli%get_map_value(switch='--set', key='cfl', val=cfl, found=found) ! overridden only if given
call cli%get_map_value(switch='--set', key='nx',  val=nx,  found=found)
$ map --set cfl=0.5 nx=256
cfl = 0.50, nx = 256

Two options that cannot go together ​

f90
call cli%add(switch='--json', help='JSON output', required=.false., act='store_true', def='.false.', exclude='--csv')
call cli%add(switch='--csv',  help='CSV output',  required=.false., act='store_true', def='.false.', exclude='--json')
$ exclusive --mesh m.grd --json --csv
exclusive: error: the options "--json" and "--csv" are mutually exclusive, but both have been passed!

Try 'exclusive --help' for help.
[exit status 1]

Exactly one of two options ​

f90
call cli%add(switch='--mesh',    switch_ab='-m', help='Mesh file',    required=.false., act='store', def='')
call cli%add(switch='--restart', switch_ab='-r', help='Restart file', required=.false., act='store', def='')
call cli%add(switch='--left',  help='Go left',  required=.false., act='store_true', def='.false.')
call cli%add(switch='--right', help='Go right', required=.false., act='store_true', def='.false.')
call cli%set_mutually_exclusive_switches(switches='--mesh,--restart', required=.true.) ! exactly one
call cli%set_mutually_exclusive_switches(switches='--left,--right')                    ! at most one
$ exclusive
exclusive: error: one of "--mesh", "--restart" is required!

usage: exclusive [--json] [--csv] (--mesh value | --restart value) [--left | --right] [--help] [--markdown] [--version]
Try 'exclusive --help' for help.
[exit status 1]

Values from environment variables ​

f90
call cli%init(progname='environment', auto_envvar_prefix='SOLVER')
call cli%add(switch='--mesh-file', help='Mesh', required=.true., act='store')                   ! SOLVER_MESH_FILE
call cli%add(switch='--workers', help='Workers', required=.false., act='store', nargs='+', def='1') ! SOLVER_WORKERS
call cli%add(switch='--fast', help='Fast mode', required=.false., act='store_true', def='.false.')  ! SOLVER_FAST
call cli%add(switch='--token', help='API token', required=.false., act='store', def='none', envvar='MY_TOKEN')
$ SOLVER_MESH_FILE=wing.grd MY_TOKEN=abc environment
mesh = wing.grd, token = abc, workers = 1
fast = F
--mesh-file = wing.grd [environment: SOLVER_MESH_FILE]
--workers   = 1        [default]
--fast      = .false.  [default]
--token     = abc      [environment: MY_TOKEN]

Ignoring the environment, for reproducible runs ​

f90
call cli%init(progname='ignore_env', ignore_env=.true.)
call cli%add(switch='--threads', help='Threads', required=.false., act='store', def='1', envvar='OMP_NUM_THREADS')
$ OMP_NUM_THREADS=64 ignore_env
threads = 1

Values from a configuration file ​

f90
call cli%init(progname='config', auto_envvar_prefix='SOLVER')
call cli%add(switch='--mesh-file', help='Mesh', required=.true., act='store')
call cli%add(switch='--cfl', help='CFL', required=.false., act='store', def='0.5')
call cli%add(switch='--config', help='Configuration file', required=.false., act='config', def='solver.ini')
call cli%add_group(group='post', description='Post processing')
call cli%add(group='post', switch='--format', help='Format', required=.false., act='store', def='vtk')
ini
# solver.ini
mesh-file = wing.grd   ; keys are the long switches without the dashes
cfl       = 0.8
[post]                 # a section is a command
format    = vtu
$ config
--mesh-file = wing.grd   [config: solver.ini]
--cfl       = 0.8        [config: solver.ini]
--config    = solver.ini [default]

A configuration file chosen by the program ​

f90
call cli%add(switch='--cfl', help='CFL', required=.false., act='store', def='0.5')
call cli%add(switch='--steps', help='Time steps', required=.false., act='store', def='100')
call cli%set_config(file='defaults.ini') ! skipped when missing; required=.true. makes that an error
ini
# defaults.ini: the defaults of the program
cfl   = 0.8
steps = 500
$ set_config --cfl 0.3
--cfl   = 0.3 [command line]
--steps = 500 [config: defaults.ini]

Logging where every value comes from ​

f90
call cli%parse(error=error)
if (error /= 0) stop 1, quiet=.true.
print '(A)', cli%provenance()
$ SOLVER_CFL=0.6 config --mesh-file body.grd post
--mesh-file   = body.grd   [command line]
--cfl         = 0.6        [environment: SOLVER_CFL]
--config      = solver.ini [default]
post --format = vtu        [config: solver.ini]

Was an option passed, and where does its value come from? ​

f90
print '(A,I0,A,L1)', 'threads = ', threads, ', passed on the command line: ', cli%is_passed(switch='--threads')
select case(cli%get_source(switch='--threads'))
case(SOURCE_COMMANDLINE) ; print '(A)', 'from the command line'
case(SOURCE_ENVIRONMENT) ; print '(A)', 'from the environment'
case(SOURCE_CONFIG)      ; print '(A)', 'from the configuration file'
case(SOURCE_DEFAULT)     ; print '(A)', 'the default'
endselect
$ OMP_NUM_THREADS=8 passed
threads = 8, passed on the command line: F
from the environment
$ OMP_NUM_THREADS=8 passed --threads 2
threads = 2, passed on the command line: T
from the command line

Subcommands ​

f90
call cli%init(progname    = 'fake_git',                                  &
              version     = 'v2.1.5',                                    &
              authors     = 'Stefano Zaghi',                             &
              license     = 'MIT',                                       &
              description = 'A toy git-like program demonstrating FLAP', &
              examples    = ['fake_git --help                ',          &
                             'fake_git init                  ',          &
                             'fake_git commit -m "fix bug #1"',          &
                             'fake_git tag -a "v2.1.5"       '])
! a top-level option
call cli%add(switch='--authors', switch_ab='-a', help='Print the authors', required=.false., act='store_true', &
             def='.false.', error=error)
! the commands
call cli%add_group(group='init',   description='Initialise versioning')
call cli%add_group(group='commit', description='Commit changes to the current branch')
call cli%add_group(group='tag',    description='Tag the current commit')
! the options of the commands
call cli%add(group='commit', switch='--message', switch_ab='-m', help='Commit message', required=.false., act='store', &
             def='', error=error)
call cli%add(group='tag', switch='--annotate', switch_ab='-a', help='Tag annotation', required=.false., act='store', &
             def='', error=error)
f90
call cli%parse(error=error)
if (error /= 0) stop 1, quiet=.true.

call cli%get(switch='-a', val=authors, error=error)
if (authors) print '(A)', 'Authors: '//cli%authors
if (cli%run_command('init')) print '(A)', 'Initialising versioning'
if (cli%run_command('commit')) then
  call cli%get(group='commit', switch='-m', val=message, error=error)
  print '(A)', 'Committing with message: "'//trim(message)//'"'
endif
if (cli%run_command('tag')) then
  call cli%get(group='tag', switch='-a', val=annotation, error=error)
  print '(A)', 'Tagging with annotation: "'//trim(annotation)//'"'
endif
$ fake_git init commit -m first
Initialising versioning
Committing with message: "first"

Short names for commands, options shared by commands ​

f90
call cli%add_group(group='compile', aliases='co, c', description='Compile the sources')
call cli%add_group(group='link', description='Link the objects')
f90
! defined once at the top level, then copied into the commands
call cli%add(switch='--jobs',    switch_ab='-j', help='Parallel jobs', required=.false., act='store', def='1')
call cli%add(switch='--verbose', help='Verbose', required=.false., act='store_true', def='.false.')
call cli%copy_options(to_group='compile')                     ! --jobs and --verbose
call cli%copy_options(to_group='link', switches='--verbose')  ! --verbose only
$ aliases co -j 4 link --verbose
compile, jobs = 4
link, verbose = T

Two commands that cannot go together ​

f90
call cli%add_group(group='run', description='Run a simulation')
call cli%add_group(group='clean', description='Remove the results')
call cli%set_mutually_exclusive_groups(group1='run', group2='clean')
$ exclusive_groups run clean
exclusive_groups: error: the group "run" and "clean" are mutually exclusive, but both have been called!

Try 'exclusive_groups run --help' for help.
[exit status 1]

An option that lists something and exits ​

f90
call cli%add(switch='--mesh',        help='Mesh file',                       required=.true., act='store')
call cli%add(switch='--list-models', help='List the turbulence models and exit', act='alternate')
f90
call cli%parse(error=error)
if (error == STATUS_ALTERNATE) then
  if (cli%is_passed(switch='--list-models')) print '(A)', 'spalart-allmaras, k-omega-sst'
  stop
elseif (error /= 0) then
  stop 1, quiet=.true.
endif
$ alternate --list-models
spalart-allmaras, k-omega-sst

Retiring an option ​

f90
call cli%add(switch='--grid', help='Grid file', required=.false., act='store', def='g.grd', &
             deprecated='use --mesh instead')
call cli%add(switch='--mesh', help='Mesh file', required=.false., act='store', def='m.grd')
call cli%add_group(group='legacy', description='The old solver', deprecated='use run')
call cli%add_group(group='run', description='The solver')
$ deprecated --grid w.grd legacy
deprecated: warning: option "--grid" is deprecated: use --mesh instead
deprecated: warning: command "legacy" is deprecated: use run
ok

Reporting a check of the program in FLAP's style ​

f90
call cli%get(switch='--nx', val=nx, error=error)
if (mod(nx, 2) /= 0) error = cli%raise_error('must be even', switch='--nx')
if (error /= 0) stop 1, quiet=.true.
$ raise_error --nx 33
raise_error: error: switch "--nx": must be even

usage: raise_error [--nx value] [--help] [--markdown] [--version]
[exit status 1]

A shorter output after an error ​

f90
call cli%init(progname='usage_on_error', usage_on_error='usage')
$ usage_on_error
usage_on_error: error: named option "--mesh" is required!

usage: usage_on_error --mesh value [--cfl value] [--help] [--markdown] [--version]
Try 'usage_on_error --help' for help.
[exit status 1]

The help and the errors in a file ​

f90
open(newunit=log, file='flap.log', action='write')
call cli%init(progname='units', usage_lun=log, error_lun=log) ! the help and the errors go to the log
$ units --nxx 4 || cat flap.log
units: error: switch "--nxx" is unknown! Did you mean "--nx"?

Try 'units --help' for help.

The error message and the usage as text ​

f90
open(newunit=quiet, status='scratch')
call cli%init(progname='messages', description='Messages as text', &
              error_lun=quiet, usage_lun=quiet)                    ! FLAP prints nowhere: the program reports
call cli%add(switch='--nx', help='Cells', required=.false., act='store', def='64')
call cli%parse(args='--nxx 4', error=error)
if (error /= 0) print '(A,I0,A)', 'code ', error, ': '//cli%error_message
print '(A)', 'signature:'//cli%signature() ! the arguments, as in the usage line
print '(A)', cli%usage(g=0)                 ! the help of the top level (g: the index of a command)
$ messages
code 15: messages: error: switch "--nxx" is unknown! Did you mean "--nx"?
signature: [--nx value] [--help] [--markdown] [--version]
usage: messages [--nx value] [--help] [--markdown] [--version]

Messages as text

Optional switches:
  --nx value
      default value 64
      Cells
  --help, -h
      Print this help message
  --markdown, -md
      Save this help message in a Markdown file
  --version, -v
      Print version

Colours ​

f90
call cli%init(progname='colors', description='Coloured output', error_color='red', error_style='bold_on')
call cli%add(switch='--mesh', switch_ab='-m', help='Mesh file', required=.false., act='store', def='m.grd', &
             help_color='cyan', help_style='bold_on')
call cli%add(switch='--cfl', help='CFL number', required=.false., act='store', def='0.5', help_color='yellow')
$ colors --mehs x
colors: error: switch "--mehs" is unknown! Did you mean "--mesh"?

Try 'colors --help' for help.
[exit status 1]

Switches in any case ​

f90
call cli%init(progname='case_insensitive', case_insensitive=.true.) ! before any add
$ case_insensitive --MESH wing.grd RUN
mesh = wing.grd, run = T

A man page, Markdown, shell completion ​

f90
call cli%init(progname='completion', version='v1.0', description='A solver', &
              completion_options=.true., man_option=.true.)
$ completion --install-completion bash
completion script installed in "/home/user/.completion-completion.bash", loaded by "/home/user/.bashrc"
f90
call cli%save_man_page(man_file='save_outputs.1', error=error)
call cli%save_usage_to_markdown(markdown_file='save_outputs.md', error=error)
call cli%save_bash_completion(bash_file='save_outputs.bash', error=error)
call cli%save_zsh_completion(zsh_file='save_outputs.zsh', error=error)
call cli%save_fish_completion(fish_file='save_outputs.fish', error=error)
call cli%save_powershell_completion(powershell_file='save_outputs.ps1', error=error)

Cleaning up before exiting on --help ​

f90
call cli%init(progname='statuses', version='v2.4.0', standalone=.false., no_args_is_help=.true.)
f90
call cli%parse(error=error)
select case(error)
case(0)
  print '(A)', 'running'
case(STATUS_PRINT_H, STATUS_PRINT_V, STATUS_PRINT_M)
  print '(A)', 'help, version or Markdown printed: cleaning up, then exit status 0'
case(STATUS_NO_ARGS)
  stop 2, quiet=.true.
case default
  stop 1, quiet=.true.
endselect
$ statuses --help
usage: statuses [--mesh value] [--help] [--markdown] [--version]

Optional switches:
  --mesh value
      default value m.grd
      Mesh file
  --help, -h
      Print this help message
  --markdown, -md
      Save this help message in a Markdown file
  --version, -v
      Print version
help, version or Markdown printed: cleaning up, then exit status 0

Testing the command line ​

f90
! a command line given as a string
call cli%parse(args='--nx 128', error=error)
call check(error == 0 .and. cli%get_source(switch='--nx') == SOURCE_COMMANDLINE, 'parse --nx 128')
call cli%get(switch='--nx', val=nx, error=error)
call check(nx == 128, 'get --nx')

! parse another command line with the same definitions
call cli%reset_parse
call cli%parse(args='', error=error)
call check(cli%get_source(switch='--nx') == SOURCE_DEFAULT, 'the default')

! errors and statuses are returned, never stop the program (standalone=.false.)
call cli%reset_parse
call cli%parse(args='--nx 1 --nx 2', error=error)
call check(error == ERROR_DUPLICATED_CLAS, 'a repeated switch')
call cli%reset_parse
call cli%parse(args='--help', error=error)
call check(error == STATUS_PRINT_H, '--help returns a status')
call check(caught('Optional switches:'), 'the help is written on the unit')

See chapter 8 of the tutorial.

Arguments that are not yours ​

With ignore_unknown_clas, parse reads every known value, prints nothing about the unknown arguments and returns ERROR_UNKNOWN_CLAS_IGNORED when there are some (cli%error_message then describes one of them):

f90
call cli%init(progname='ignore_unknown', ignore_unknown_clas=.true.) ! unknown arguments are not an error
call cli%add(switch='--nx', help='Cells', required=.false., act='store', def='64')
call cli%parse(error=error)
if (error == ERROR_UNKNOWN_CLAS_IGNORED) then
  print '(A)', 'some arguments are left to the solver library' ! FLAP prints nothing: the program may say something
elseif (error /= 0) then
  stop 1, quiet=.true.
endif
$ ignore_unknown --nx 8 --solver-option fast
some arguments are left to the solver library
nx = 8

Passing arguments through to another program ​

Everything after -- is collected, unparsed, in the hidden list of --:

fortran
character(256), allocatable :: rest(:)

call cli%get_varying(switch='--', val=rest, error=error)   ! prog --nx 4 -- mpirun -np 8  ->  rest = [mpirun, -np, 8]

Asking the user ​

f90
call m%init(question='What is your favorite food?')
call m%add_option(text='Pizza')
call m%add_option(text='Ice Cream')
call m%add_option(text='Tacos')
call m%run(choice, merror)
$ printf '2\n' | menus single
1) Pizza
2) Ice Cream
3) Tacos
What is your favorite food? 
choice = 2

Asking for several choices ​

f90
call m%init(question='Which toppings?', multiple=.true.)
call m%add_option(text='Cheese', is_default=.true.)
call m%add_option(text='Mushrooms')
call m%add_option(text='Olives', is_default=.true.)
call m%run(choices, merror) ! "3 1" gives [3, 1]; an empty answer the defaults, [1, 3]
$ printf '3 1\n' | menus multiple
1) *Cheese
2) Mushrooms
3) *Olives
Which toppings? 
choices = 3,1

A yes/no question ​

f90
call m%init(question='Overwrite the restart file?')
call m%yes_no(answer, default='n', error=merror)
$ printf 'yes\n' | menus yes_no
Overwrite the restart file? (y/N) 
answer = T

Asking again after a wrong answer ​

f90
call m%init(question='What is your favorite food?', loop_on_invalid=.true., tries=3)
$ printf '7\n2\n' | menus retry
1) Pizza
2) Ice Cream
3) Tacos
What is your favorite food? error: invalid response: 7 (2 tries left)
1) Pizza
2) Ice Cream
3) Tacos
What is your favorite food? 
choice = 2