Skip to content

Advanced Features ​

Where values come from besides the command line (environment variables, configuration files), and the checks FLAP can run on them (exclusive sets, paths, ranges), plus deprecations, alternate actions and case-insensitive matching.

Value sources ​

A value comes from the first source that has one, in this order:

The command line always wins; a value from any explicit source (command line, environment, configuration file) satisfies a required option. cli%get_source and cli%provenance tell where each value comes from (see Parsing).

Environment variables ​

envvar names the variable of an option; init(auto_envvar_prefix=...) generates one for every option:

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]
$ SOLVER_MESH_FILE=wing.grd environment --mesh-file body.grd
mesh = body.grd, token = none, workers = 1
fast = F
--mesh-file = body.grd [command line]
--workers   = 1        [default]
--fast      = .false.  [default]
--token     = none     [default]
  • A variable counts when it is set and not blank; the bare switch (--token alone) reads it too.
  • envvar is valid for named store options (lists included), store_true and store_false; not for positionals, store*, count and append (ERROR_ENVVAR_NOT_STORE, 17).
  • A flag reads 1/0, true/false, t/f, yes/no, y/n, on/off, in any case; anything else makes get fail with ERROR_CASTING_LOGICAL (10).
  • The help shows the name of the variable of each option.
$ environment --help
usage: environment --mesh-file value [--workers value#1 [value#2...]] [--fast] [--token value] [--help] [--markdown] [--version]

Required switches:
  --mesh-file value
      environment variable name "SOLVER_MESH_FILE"
      Mesh

Optional switches:
  --workers value#1 [value#2...]
      environment variable name "SOLVER_WORKERS"
      default value 1
      Workers
  --fast
      environment variable name "SOLVER_FAST"
      default value .false.
      Fast mode
  --token value
      environment variable name "MY_TOKEN"
      default value none
      API token
  --help, -h
      Print this help message
  --markdown, -md
      Save this help message in a Markdown file
  --version, -v
      Print version

Lists from the environment ​

A list option (nargs) reads its variable as one line of comma-separated values, not blank-separated as on the command line and in def=: an environment value is a single shell word, where commas are the convention.

$ SOLVER_MESH_FILE=wing.grd SOLVER_WORKERS='1, 2 ,3' SOLVER_FAST=yes environment
mesh = wing.grd, token = none, workers = 1,2,3
fast = T
--mesh-file = wing.grd [environment: SOLVER_MESH_FILE]
--workers   = 1 2 3    [environment: SOLVER_WORKERS]
--fast      = .true.   [environment: SOLVER_FAST]
--token     = none     [default]
VariableList
WORKERS='1,99'1, 99
WORKERS='1, 2 , 3'1, 2, 3 (blanks around a value trimmed)
FILES='"my file.h5", other.h5'my file.h5, other.h5 (a quoted value may hold commas and blanks)
TITLES='"say ""hi""",x'say "hi", x ("" inside quotes is a ")
WORKERS='1,,3'1, empty, 3 (a numeric get then fails its cast)
WORKERS='1,"99'error ERROR_ENVVAR_CSV (43): unterminated quote

A scalar option takes its variable verbatim, commas included. The number of values of nargs='N' is checked by get.

Generated variable names ​

With init(auto_envvar_prefix='SOLVER'), every named store, store_true or store_false option added without an envvar gets one, PREFIX[_COMMAND]_NAME in upper case, NAME being the long switch without its dashes and - becoming _: --mesh-file is SOLVER_MESH_FILE, the --format of the command post is SOLVER_POST_FORMAT. An explicit envvar= wins over the generated name; positionals, store*, count and append get none. init must come before the add calls.

Ignoring the environment ​

For reproducible runs (a batch job whose environment must not leak in, tests, CI sandboxes), init(ignore_env=.true.) turns every environment lookup off. The envvar definitions are unchanged and still shown in the help:

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

Configuration files ​

cli%set_config(file, required, error) names an INI file supplying values below the environment and above the defaults; an option with act='config' lets the user choose the 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]
$ 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]
  • Keys are the long switches without the dashes; keys before any section belong to the top level, a [section] holds the options of that command.
  • A value is the text after =, blanks trimmed; one pair of quotes (' or ") is stripped, and a quoted value keeps its # and ;. An inline comment starts at a # or ; preceded by a blank. An empty value counts as unset.
  • A list (nargs) is blank separated, as in def=; a flag reads yes/no, on/off, 1/0, true/false, ...
  • An unknown key or section, a key naming an option that takes no value (count, append, store*), or a line that is not key = value, [section] or a comment is ERROR_CONFIG_UNKNOWN_KEY (1007), naming the line; with init(ignore_unknown_clas=.true.) such lines are ignored. The last of repeated keys wins.
  • The file is the one named on the command line (--config my.ini), else in the environment variable of the config option, else the one of set_config, else the default of the config option. A file named on the command line or in the environment must exist (ERROR_CONFIG_NOT_FOUND, 1006); a missing default is skipped, unless set_config(..., required=.true.). get of the config option returns the file name.
  • The file is read by parse after --help/--version, so a broken file never blocks the help. A value from it satisfies a required option and is checked against choices and ranges by get.
$ config --config other.ini
config: error: configuration file "other.ini" not found!

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

Mutually exclusive sets ​

cli%set_mutually_exclusive_switches(switches, required, group, pref, error) declares a set of switches of which at most one may be given; with required=.true., exactly one. It is the recommended mechanism over the pairwise exclude:

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 --mesh m.grd --left
ok
$ exclusive --mesh m.grd --restart r.h5
exclusive: error: switches "--mesh", "--restart" are mutually exclusive!

usage: exclusive [--json] [--csv] (--mesh value | --restart value) [--left | --right] [--help] [--markdown] [--version]
Try 'exclusive --help' for help.
[exit status 1]
$ 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]

The usage shows the sets in docopt notation, (a | b) for a required set and [a | b] otherwise. Rules:

  • the members are comma separated, named by switch or abbreviation, and must be already added to the group (pass group= for the options of a command); a set of a command is checked only when the command is called;
  • a member cannot be individually required, and a switch belongs to at most one set;
  • every explicit value counts, from the command line, the environment or a configuration file; a default neither satisfies a required set nor violates a set. When a member is on the command line, the environment and configuration values of the other members fall back to their defaults: the command line wins (a variable set for a batch job never makes a command line alternative a violation);
  • the sets are checked after help/version and after the required options, as the last validation;
  • an invalid set is not added: the call returns ERROR_M_EXCLUDE_SET_DEFINITION (104), and parse returns the same error, so a wrong definition cannot go unnoticed.

Path checks ​

must_exist, readable, writable and allow_dash make parse check a file name, whatever its source (command line, environment, configuration file or default):

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]
KeywordCheck
must_existthe file exists (ERROR_PATH_NOT_FOUND, 33)
readableit exists and opens for reading (ERROR_PATH_NOT_READABLE, 34, with the reason given by the system)
writableif it exists, it opens for writing, nothing written (ERROR_PATH_NOT_WRITABLE, 35); a missing file passes and is not created
allow_dash- passes every check (your program maps it to standard input/output); otherwise - is a file name
  • Every item of a list is checked; an empty value (def='') is not checked. The options of a command are checked only when the command is called.
  • Only for options taking a value (store, store*, append): elsewhere the keywords are ERROR_PATH_INCONSISTENT (49).
  • Standard Fortran leaves the existence of a directory to the compiler: with gfortran a directory exists and opens for reading, with Intel ifx it does not exist.
  • nvfortran 26.5: opening a read-only file for writing succeeds (the error comes at the first write), so writable does not detect a read-only file with that compiler.

Numeric ranges ​

min= and max= (strings, like def=) give a numeric option a range; min_open=.true./max_open=.true. exclude the bound, clamp=.true. replaces an out-of-range value with the bound instead of failing:

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]
  • The value is checked by get, after its conversion, in the kind of your variable, whatever its source; every element of a list is checked. ERROR_OUT_OF_RANGE is 31.
  • With clamp, an open integer bound clamps to the next integer inside (bound + 1 / bound - 1); a real cannot be clamped to an open bound: get reports ERROR_RANGE_DEFINITION (30) when it would have to.
  • An invalid range (a bound that is not a number, min > max, an empty open interval, a range on a flag) is ERROR_RANGE_DEFINITION (30) at add; get into a character or logical is ERROR_RANGE_TYPE (32).
  • The help shows it: range (0, 1].

Deprecated options and commands ​

add(..., deprecated='message') and add_group(..., deprecated='message') mark an option or a command as deprecated (deprecated='': without a message). Using it is not an error: parse prints a warning on the error unit and goes on.

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
  • An option warns when its value comes from the command line or from its environment variable, not when it comes from a configuration file or its default (as in click); a command warns when it is called.
  • The help marks them: Grid file (DEPRECATED: use --mesh instead).
  • A required option cannot be deprecated: ERROR_DEPRECATED_REQUIRED (44).

Alternate actions ​

An option with act='alternate' is an auxiliary action of the program (--list-models, --dump-config), not an input value: when it is passed, parse returns STATUS_ALTERNATE (also in standalone mode: FLAP never stops on it) and skips the value validation, so it works even without the required options. The program dispatches on is_passed:

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
  • Skipped: required options, mutually exclusive sets, exclude= pairs, exclusive commands, path checks, unknown configuration keys and invalid environment lists. Still reported: syntax errors (an unknown or repeated switch); --help, --version and --markdown come first. Deprecation warnings are still printed.
  • get works as usual after it (a value out of choices is still reported by get).
  • An alternate is a flag: nargs, envvar, choices, exclude, required and positional are ERROR_ALTERNATE_INCONSISTENT (37). The usage shows it as [--list-models].

Case-insensitive matching ​

init(case_insensitive=.true.) matches switches (abbreviations and negations included) and command names in any case; values keep their case (for choices in any case, see case_sensitive=.false. in Defining Arguments):

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

Two switches of a group differing only by case are then a consistency error (100). The "did you mean" suggestions compare in any case too.