Input Types
Parameters that accept a type parameter can accept any callable to transform parsed argument values, but some custom types are defined here to facilitate common use cases.
Paths & Files
Path
To automatically handle common normalization steps / checks that are done for paths, the Path
class is
available. When this input type is used, the parsed value will be provided as a pathlib.Path
object.
Path initialization parameters:
- exists:
If set, then the provided path must already exist if True, or must not already exist if False. By default, existence is not checked.
- expand:
Whether tilde (
~
) should be expanded. Defaults to True.- resolve:
Whether the path should be fully resolved to its absolute path, with symlinks resolved, or not. Defaults to False.
- type:
To restrict the acceptable types of files/directories that are accepted, specify the
StatMode
that matches the desired type. By default, any type is accepted. To accept specifically only regular files or directories, for example, usetype=StatMode.DIR | StatMode.FILE
or'dir|file'
.- readable:
If True, the path must be readable.
- writable:
If True, the path must be writable.
- allow_dash:
Allow a dash (
-
) to be provided to indicate stdin/stdout (default: False).- use_windows_fix:
If True (the default) and the program is running on Windows, then
fix_windows_path()
will be called to attempt to fix an issue caused by auto-completion via Git Bash where Python receives a path that begins with/{drive}/...
instead of{drive}:/...
and doesn’t understand how to handle it.
Given the following (truncated) example:
from cli_command_parser import Command, Option, main, inputs
class InputsExample(Command):
path = Option('-p', type=inputs.Path(exists=True, type='file'), help='The path to a file')
def main(self):
if self.path:
self.print(f'You provided path={self.path}')
The resulting output:
$ custom_inputs.py -p examples
argument --path / -p: bad value='examples' for type=<Path(exists=True, type=<StatMode:FILE>)>: expected a regular file
$ custom_inputs.py -p examples/custom_inputs.py
You provided path=examples/custom_inputs.py
$ custom_inputs.py -p examples/custom_inputs.p
argument --path / -p: bad value='examples/custom_inputs.p' for type=<Path(exists=True, type=<StatMode:FILE>)>: it does not exist
File
The File
custom input extends Path, so it can accept all of the same options, but it provides
functionality for directly reading or writing to the provided path.
Additional File initialization parameters:
- mode:
The mode in which the file should be opened. Defaults to
r
for reading text. For more info, seeopen()
.- encoding:
The encoding to use when reading the file in text mode. Ignored if the parsed path is
-
.- errors:
Error handling when reading the file in text mode. Ignored if the parsed path is
-
.- lazy:
If True, a
FileWrapper
will be stored in the Parameter using this File, otherwise the file will be read immediately upon parsing of the path argument.- parents:
If True and
mode
implies writing, then create parent directories as needed. Ignored otherwise.
Using another snippet from the above example:
class InputsExample(Command):
in_file = Option('-f', type=File(allow_dash=True, lazy=False), help='The path to a file to read')
out_file: FileWrapper = Option('-o', type=File(allow_dash=True, mode='w'), help='The path to a file to write')
def main(self):
if self.in_file:
self.print(f'Content from the provided file: {self.in_file!r}')
def print(self, content):
if self.out_file:
self.out_file.write(content + '\n')
else:
print(content)
We can see the results:
$ echo 'stdin example' | custom_inputs.py -f-
Content from the provided file: 'stdin example\n'
$ echo 'stdin example' | examples/custom_inputs.py -f- -o example_out.txt
$ cat example_out.txt
Content from the provided file: 'stdin example\n'
By setting lazy=False
, the in_file
Option in the above example eagerly loaded the content, so the entire file
contents were stored in the Parameter. The default is for only the path to be stored, and a FileWrapper
that
has FileWrapper.read()
and FileWrapper.write()
methods is returned. The file will only be opened for
reading/writing when those methods are called, as can be seen in the example when self.out_file.write(...)
is
called.
Serialized Files
In addition to plain text or binary files, custom input handlers also exist for Json
and Pickle
,
and a generic handler (Serialized
) exists for any other serialization format. They all extend
File, so the same options are accepted.
Additional Serialized initialization parameters:
- converter:
The function to call to serialize or deserialize the content in the specified file
- pass_file:
True to call the given function with the file, False to handle (de)serialization and read/write as separate steps. If True, when reading, the converter will be called with the file as the only argument; when writing, the converter will be called as
converter(data, f)
. If False, when reading, the converter will be called with the content from the file; when writing, the converter will be called before writing the data to the file.
The JSON and Pickle handlers do not accept the above 2 parameters. The converter is automatically picked to be
dump
or load
based on whether the provided mode
is for reading or writing, and the pass_file
option will be overridden if provided.
Adding another snippet to the above example:
class InputsExample(Command):
json: FileWrapper = Option('-j', type=Json(allow_dash=True), help='The path to a file containing json')
def main(self):
if self.json:
data = self.json.read()
self.print(f'You provided a {type(data).__name__}')
iter_data = data.items() if isinstance(data, dict) else data if isinstance(data, list) else [data]
for n, line in enumerate(iter_data):
self.print(f'[{n}] {line}')
We can see that the JSON content from stdin was automatically deserialized when self.json.read()
was called:
$ echo '{"a": 1, "b": 2}' | examples/custom_inputs.py -j -
You provided a dict
[0] ('a', 1)
[1] ('b', 2)
When using the generic Serialized
directly, the specific (de)serialization function needs to be provided:
Serialized(pickle.loads, mode='rb', lazy=False)
Serialized(pickle.load, pass_file=True, mode='rb', lazy=False)
Serialized(json.loads, lazy=False)
Serialized(json.load, pass_file=True, lazy=False)
Serialized(json.dumps, mode='w')
Serialized(json.dump, pass_file=True, mode='w')
Numeric Ranges
Range
To restrict the allowed values to only integers in a range
, the Range
input type is available.
For convenience, Parameters can be initialized with a normal range
object as type=range(...)
,
and it will automatically be wrapped in a Range
input handler. To use the snap
feature, Range
must be used directly.
Range initialization parameters:
- range:
A
range
object- snap:
If True and a provided value is outside the allowed range, snap to the nearest bound. The min or max of the provided range (not necessarily the start/stop values) will be used, depending on which one the provided value exceeded.
NumRange
The NumRange
input type can be used to restrict values to either integers or floats between a min and max,
or only bound on one side. At least one of min or max is required, and min must be less than max.
By default, the min and max behave like the builtin range
- the min is inclusive, and the max is
exclusive.
NumRange initialization parameters:
- type:
The type for values, or any callable that returns an int/float. The default depends on the provided min/max values - if either is a float, then float will be used, otherwise int will be used.
- snap:
If True and a provided value is outside the allowed range, snap to the nearest bound. Respects inclusivity / exclusivity of the bound. Not supported for floats since there is not an obviously correct behavior for handling them in this context.
- min:
The minimum allowed value, or None to have no lower bound.
- max:
The maximum allowed value, or None to have no upper bound.
- include_min:
Whether the minimum is inclusive (default: True).
- include_max:
Whether the maximum is inclusive (default: False).
- Example use cases:
Restrict input to only positive integers:
NumRange(min=0)
Allow floats between 0 and 1, inclusive:
NumRange(type=float, min=0, max=1, include_max=True)
Choice Inputs
Choice inputs provide a way to validate / normalize input against a pre-defined set of values.
Choices
Validates that values are members of the collection of allowed values. Choices may be provided to Parameters as
either choices=...
or as type=Choices(...)
. If they are provided as choices=...
, then a Choices
input type will automatically be created to handle validating those choices. Any type=...
argument to the
Parameter will be passed through when initializing the Choices
object. To adjust case-sensitivity,
Choices
must be initialized directly.
If choices
is a dict or other type of mapping, then only the keys will be used. See ChoiceMap for
another option for handling dicts.
Choices initialization parameters:
- choices:
A collection of choices allowed for a given Parameter.
- type:
Called before evaluating whether a value matches one of the allowed choices, if provided. Must accept a single string argument.
- case_sensitive:
Whether choices should be case-sensitive. Defaults to True. If the choices values are not all strings, then this cannot be set to False.
ChoiceMap
Similar to Choices, but requires a mapping for allowed values.
ChoiceMap initialization parameters:
- choices:
Mapping (dict) where for a given key=value pair, the key is the value that is expected to be provided as an argument, and the value is what should be stored in the Parameter for that argument.
- type:
Called before evaluating whether a value matches one of the allowed choices, if provided. Must accept a single string argument.
- case_sensitive:
Whether choices should be case-sensitive. Defaults to True. If the choices keys are not all strings, then this cannot be set to False.
EnumChoices
Similar to ChoiceMap, but the EnumChoices
input uses an Enum to validate / normalize input
instead of the keys in a dict. Facilitates the use of Enums as an input type without the need to provide a redundant
choices
argument for accepted values or implement _missing_
to be more permissive.
If incorrect input is received, the error message presented to the user will list the names of the members of the
provided Enum, as they would if they were provided as choices
.
For convenience, Parameters can be initialized with a normal Enum subclass as type=MyEnum
, and it will
automatically be wrapped in a EnumChoices
input handler. If an Enum is provided as the type, and
choices=...
is also specified, then the Enum will not be wrapped. To enable case-sensitive matching,
EnumChoices
must be used directly.
EnumChoices initialization parameters:
- enum:
A subclass of
enum.Enum
.- case_sensitive:
Whether choices should be case-sensitive. Defaults to False.
Regex & Glob Patterns
Regex and Glob patterns provide a way to validate that input strings match an expected pattern. Both related input helper classes support initialization with one or more patterns, which allows more flexibility in case acceptable values cannot easily be represented by a single pattern.
Regex
Validates that values match one of the provided regex patterns. Patterns may be provided as strings, or as
pre-compiled patterns (i.e., the result of calling re.compile()
). To include flags like
re.IGNORECASE
, pre-compiled patterns must be used.
Matches are checked for using re.Pattern.search()
, so if full matches or matches that start at the
beginning of the string are necessary, then start (^
) / end ($
) anchors should be included where appropriate.
See search() vs. match() for more related info, or
regular-expressions.info for more general info about writing regular
expressions.
Regex initialization parameters:
- patterns:
One or more regex pattern strings or pre-compiled Regular Expression Objects.
- group:
Identifier for a capturing group. If specified, the string captured in this group will be returned instead of the full / original input string.
- groups:
Collection of identifiers for capturing groups. If specified, a tuple containing the strings from the specified capturing groups will be returned instead of the full / original input string.
- mode:
The
RegexMode
(or string name of a RegexMode member) representing the type of value that should be returned during parsing. When a value is provided forgroup
orgroups
, this does not need to be explicitly provided - it will automatically pick the appropriate mode. Defaults toSTRING
.
Glob
Validates that values match one of the provided glob / fnmatch patterns.
Glob initialization parameters:
- patterns:
One or more glob pattern strings.
- match_case:
Whether matches should be case sensitive or not (default: False).
- normcase:
Whether
os.path.normcase()
should be called on patterns and values (default: False).
Date & Time
Date and Time inputs provide a way to parse day, month, datetime, date, and time inputs with optional alternate localization support.
Warning
Locale Support
Alternate locale support is handled by using locale.setlocale()
, which may cause problems on some
systems. Using alternate locales in this manner should not be used in a multi-threaded application, as it will
lead to unexpected output from other parts of the program.
If you do not specify a locale
or out_locale
value for any input type in this section, then the locale will
not be modified by this library (setlocale
will not be used).
If you need to handle multiple locales and this is a problem for your application, then you should leave the
locale
parameters empty / None
and use a proper i18n library like babel
for localization.
Day & Month
The Day
and Month
input types accept full, abbreviated, and numeric input values, and return full
names for the parsed values by default. Both can be configured to return numeric values instead. Day supports both
ISO 8601 (1-7) and non-ISO (0-6) numeric weekday values for both input and output (configurable independently).
Input and output locales are configurable independently, but if an input locale is specified, then the output locale defaults to the same locale as the input one. By default, locale modification is not performed.
Day
Input type representing a day of the week.
Day initialization parameters:
- full:
Allow the full day name to be provided
- abbreviation:
Allow abbreviations of day names to be provided
- numeric:
Allow weekdays to be specified as a decimal number
- iso:
Ignored if
numeric
is False. If True, then numeric weekdays are treated as ISO 8601 weekdays, where 1 is Monday and 7 is Sunday. If False, then 0 is Monday and 6 is Sunday.- locale:
An alternate locale to use when parsing input
- out_format:
A
DTFormatMode
or str that matches a format mode. Defaults to full weekday name.- out_locale:
Alternate locale to use for output. Defaults to the same value as
locale
.
Month
Input type representing a month.
Month initialization parameters:
- full:
Allow the full month name to be provided
- abbreviation:
Allow abbreviations of month names to be provided
- numeric:
Allow months to be specified as a decimal number
- locale:
An alternate locale to use when parsing input
- out_format:
A
DTFormatMode
or str that matches a format mode. Defaults to full month name.- out_locale:
Alternate locale to use for output. Defaults to the same value as
locale
.
Full Date / Time Parsing
The DateTime
, Date
, and Time
input types accept multiple format strings for processing
input, and default to formats very close to ISO
8601. Each of these input types also accepts optional earliest and latest bounds
(inclusive) to validate that the provided date/time falls within an expected time range. They all return parsed values
as objects of their respective datetime
classes.
Common initialization parameters for DateTime, Date, and Time inputs:
- formats:
One or more datetime format strings.
- locale:
An alternate locale to use when parsing input
- earliest:
If specified, the parsed value must be later than or equal to this
- latest:
If specified, the parsed value must be earlier than or equal to this
DateTime
Input type that accepts any number of datetime format strings for parsing input. Parsing results in returning
a datetime.datetime
object.
Date
Input type that accepts any number of datetime format strings for parsing input. Parsing results in returning
a datetime.date
object.
Time
Input type that accepts any number of datetime format strings for parsing input. Parsing results in returning
a datetime.time
object.
Other Date / Time Inputs
TimeDelta
Input type that requires a time unit supported by datetime.timedelta
objects. Parsing results in
returning a datetime.timedelta
object. Can parse positive and negative integers and floats.
Does not support automatic parsing of a CLI user-provided unit. The unit must be specified when the TimeDelta
input is initialized.
TimeDelta initialization parameters:
- unit:
The time unit to use when initializing a
datetime.timedelta
object with the parsed value. Supported units: days, seconds, microseconds, milliseconds, minutes, hours, weeks
Manual Input Validation
Sometime it just isn’t easy to define an input validator that can be used by a Parameter, and user input needs to be
validated after parsing is complete. In such cases, the recommended approach for informing users that they provided
invalid arguments / values would be to raise UsageError
or BadArgument
. Use UsageError
to provide a custom message, or BadArgument
to show the usage string for the parameter in question with an
optional custom message.
Example usage:
from cli_command_parser import Command, Option, UsageError, BadArgument, main
class MyCommand(Command):
foo = Option('-f')
bar = Option('-b')
def main(self):
if self.foo == 'foo':
raise BadArgument(self.__class__.foo, "Any value other than 'foo' is allowed")
elif self.foo == self.bar:
raise UsageError('Expected different values for --foo and --bar')
print(f'You provided foo={self.foo!r}, bar={self.bar!r}')
if __name__ == '__main__':
main()
Example outputs (with the above example saved in a file called foo_bar.py
):
$ foo_bar.py --foo=foo
argument --foo FOO / -f FOO: Any value other than 'foo' is allowed
$ foo_bar.py --foo=123
You provided foo='123', bar=None
$ foo_bar.py --foo=123 --bar=123
Expected different values for --foo and --bar
$ foo_bar.py --foo=123 --bar=456
You provided foo='123', bar='456'
Note that when passing the Parameter to BadArgument, the Parameter itself must be used, not the parsed value.
To access the foo
Option instead of its parsed value, the above example used self.__class__.foo
.
An alternative approach that could be used in this example would be to use type(self).foo
.
Raising an exception like this instead of calling sys.exit(1)
from the command makes it easy to both exit and
provide the user with an informative message with a single line / statement. It also makes it easier to unit test.
To further customize handling of exceptions raised from within a Command, see Error Handling.