%--------------------------------------------------%
% vim: ft=mercury ts=4 sw=4 et
%--------------------------------------------------%
% Copyright (C) 1994-1999,2001-2007, 2011 The University of Melbourne.
% Copyright (C) 2014-2018,2020 The Mercury team.
% This file is distributed under the terms specified in COPYING.LIB.
%--------------------------------------------------%
%
% File: getopt.m.
% Authors: fjh, zs.
% Stability: medium.
%
% This module defines predicates that parse command line options.
%
% These predicates allow both short (single-character) options,
% which are preceded on command lines with a single dash, and GNU-style
% long options, which are preceded on command lines with a double dash.
% An argument starting with a single dash can specify more than one
% short option, so that e.g. `-cde' is equivalent to `-c -d -e', while
% each long option name must be in an argument of its own.
%
% The predicates in this module support the GNU extension of recognizing
% options anywhere in the command line, not just at its start.
%
% To use this module:
%
% - You must provide an `option' type which is an enumeration of
% all your different options.
% - You must provide predicates `short_option(Char, Option)' and
% `long_option(String, Option)' which convert the short and long names
% respectively for the option to this enumeration type.
% (An option can have as many names as you like, long or short.)
% - You must provide a predicate `option_default(Option, OptionData)'
% which specifies both the type and the default value for every option.
%
% You may optionally provide a predicate `special_handler(Option, SpecialData,
% OptionTable, MaybeOptionTable)' for handling special option types.
% (See below.)
%
% We support the following "simple" option types:
%
% - bool
% - int
% - string
% - maybe_int (which have a value of `no' or `yes(int)')
% - maybe_string (which have a value of `no' or `yes(string)')
%
% We also support one "accumulating" option type:
%
% - accumulating (which accumulates a list of strings)
%
% And the following "special" option types:
%
% - special
% - bool_special
% - int_special
% - string_special
% - maybe_string_special
% - file_special (in the predicate variants that do I/O; see below)
%
% For the "simple" option types, if there are multiple occurrences of the same
% option on the command line, then the last (right-most) occurrence will take
% precedence. For "accumulating" options, multiple occurrences will be
% appended together into a list.
%
% With the exception of file_special, the "special" option types are handled
% by a special option handler (see `special_handler' below), which may perform
% arbitrary modifications to the option_table. For example, an option which
% is not yet implemented could be handled by a special handler which produces
% an error report, or an option which is a synonym for a set of more
% "primitive" options could be handled by a special handler which sets those
% "primitive" options.
%
% It is an error to use a "special" option (other than file_special)
% for which there is no handler, or for which the handler fails.
%
% The file_special option type requires no handler, and is implemented
% entirely by this module. It always takes a single argument, a file name.
% Its handling always consists of
%
% - reading the named file,
% - converting its contents into a sequence of words separated by white space,
% and
% - interpreting those words as options in the usual manner.
%
% The reading of the file obviously requires doing I/O, which means that
% only the predicate variants that take an I/O state pair of arguments
% support file_special options. If a call to a predicate variant that
% does not take a pair of I/O states does nevertheless specify a file_special
% option, that predicate will report an error when processing a command line
% that contains that option.
%
% Boolean (i.e. bool or bool_special), maybe_int, maybe_string and
% accumulating options can be negated. Negating an accumulating option
% empties the accumulated list of strings. Single-character options
% can be negated by following them with another `-', e.g. `-x-' will negate
% the `-x' option. Long options can be negated by preceding them with `--no-',
% e.g. `--no-foo' will negate the `--foo' option.
%
% Note that arguments following an option may be separated from the option by
% either whitespace or the equals character `=', so that e.g. `--foo 3' and
% `--foo=3' both specify the option `--foo' with the integer argument `3'.
%
% If the argument `--' is encountered on the command line, then option
% processing will immediately terminate, without processing any remaining
% arguments. This is sometimes needed to tell a program to treat strings
% that start with a dash as non-option arguments.
%
%--------------------------------------------------%
%--------------------------------------------------%
:- module getopt.
:- interface.
:- import_module bool.
:- import_module char.
:- import_module io.
:- import_module list.
:- import_module map.
:- import_module maybe.
:- import_module set.
%--------------------------------------------------%
:- type short_option(OptionType) == (pred(char, OptionType)).
:- inst short_option == (pred(in, out) is semidet).
:- type long_option(OptionType) == (pred(string, OptionType)).
:- inst long_option == (pred(in, out) is semidet).
:- type option_default_value(OptionType) == (pred(OptionType, option_data)).
:- inst option_default_value_nondet == (pred(out, out) is nondet).
:- inst option_default_value_multi == (pred(out, out) is multi).
:- type special_handler(OptionType) ==
(pred(OptionType, special_data,
option_table(OptionType), maybe_option_table(OptionType))).
:- inst special_handler ==
(pred(in, in, in, out) is semidet).
% The predicates below that process options, namely
%
% - process_options
% - process_options_io
% - process_options_track
% - process_options_track_io
%
% all take an argument of the option_ops type to tell them
%
% - what the default value of each option is;
% - what the short and long names of the options are;
% (see the comment at the top for a description of the distinction), and
% - if there are any special options, how they should be handled.
%
% The job of the option_ops type is to hold the three or four predicates
% used to categorize a set of options. Their interfaces should be
% like these:
%
% % True if the character names a valid single-character short option.
% %
% :- pred short_option(char::in, option::out) is semidet.
%
% % True if the string names a valid long option.
% %
% :- pred long_option(string::in, option::out) is semidet.
%
% % Nondeterministically returns all the options with their
% % corresponding types and default values.
% %
% :- pred option_default(option::out, option_data::out) is multi.
%
% % This predicate is invoked whenever getopt finds an option
% % designated as special (by either a short or long name),
% % with special_data holding the argument of the option (if any).
% % The predicate can change the option table in arbitrary ways
% % in the course of handling the option, or it can return
% % an error message.
% %
% % The canonical examples of special options are -O options setting
% % optimization levels in compilers, which set many other options
% % at once.
% %
% :- pred special_handler(option::in, special_data::in,
% option_table::in, maybe_option_table(_)::out) is semidet.
%
% The four function symbols in the option_ops type differ in
%
% - whether they contain a special_handler or not, and
% - whether the determinism of option_default is nondet or multi.
%
:- type option_ops(OptionType)
---> option_ops(
short_option(OptionType),
long_option(OptionType),
option_default_value(OptionType)
)
; option_ops(
short_option(OptionType),
long_option(OptionType),
option_default_value(OptionType),
special_handler(OptionType)
)
; option_ops_multi(
short_option(OptionType),
long_option(OptionType),
option_default_value(OptionType)
)
; option_ops_multi(
short_option(OptionType),
long_option(OptionType),
option_default_value(OptionType),
special_handler(OptionType)
).
:- inst option_ops for option_ops/1
---> option_ops(
short_option,
long_option,
option_default_value_nondet
)
; option_ops(
short_option,
long_option,
option_default_value_nondet,
special_handler
)
; option_ops_multi(
short_option,
long_option,
option_default_value_multi
)
; option_ops_multi(
short_option,
long_option,
option_default_value_multi,
special_handler
).
%--------------------------------------------------%
:- type special_handler_track(OptionType) ==
(pred(OptionType, special_data,
option_table(OptionType), maybe_option_table(OptionType),
set(OptionType))).
:- inst special_handler_track ==
(pred(in, in, in, out, out) is semidet).
% A version of the option_ops type for the process_options_track
% predicate and its process_options_track_io variant.
% Unlike the option_ops type, it does not contain a predicate
% for setting the initial default values of options, since
% process_options_track expects that to be done separately.
%
:- type option_ops_track(OptionType)
---> option_ops_track(
short_option(OptionType),
long_option(OptionType),
special_handler_track(OptionType)
).
:- inst option_ops_track for option_ops_track/1
---> option_ops_track(
short_option,
long_option,
special_handler_track
).
%--------------------------------------------------%
:- type user_data_handler(OptionType, UserDataType) ==
(pred(OptionType, special_data,
option_table(OptionType), maybe_option_table(OptionType),
UserDataType, UserDataType)).
:- inst user_data_handler ==
(pred(in, in, in, out, in, out) is semidet).
% A version of the option_ops type for the process_options_userdata
% predicate and its process_options_userdata_io variant.
%
:- type option_ops_userdata(OptionType, UserDataType)
---> option_ops_userdata(
short_option(OptionType),
long_option(OptionType),
user_data_handler(OptionType, UserDataType)
).
:- inst option_ops_userdata for option_ops_userdata/2
---> option_ops_userdata(
short_option,
long_option,
user_data_handler
).
%--------------------------------------------------%
:- type option_data
---> bool(bool)
; int(int)
; string(string)
; maybe_int(maybe(int))
; maybe_string(maybe(string))
; accumulating(list(string))
; special
; bool_special
; int_special
; string_special
; maybe_string_special
; file_special.
:- type special_data
---> none
; bool(bool)
; int(int)
; string(string)
; maybe_string(maybe(string)).
:- type option_table(OptionType) == map(OptionType, option_data).
%--------------------------------------------------%
:- type maybe_option_table(OptionType)
---> ok(option_table(OptionType))
; error(string).
:- type maybe_option_table_se(OptionType)
---> ok(option_table(OptionType))
; error(option_error(OptionType)).
%--------------------------------------------------%
:- type option_error(OptionType)
---> unrecognized_option(string)
% An option that is not recognized appeared on the command line.
% The argument gives the option as it appeared on the command line.
; option_error(OptionType, string, option_error_reason).
% An error occurred with a specific option. The first argument
% identifies the option enumeration value; the second identifies
% the string that appeared on the command line for that option;
% the third argument describes the nature of the error with that
% option.
:- type option_error_reason
---> unknown_type
% No type for this option has been specified in the
% `option_default'/2 predicate.
; requires_argument
% The option requires an argument but it occurred on the command
% line without one.
; does_not_allow_argument(string)
% The option does not allow an argument but it was provided with
% one on the command line.
% The argument gives the contents of the argument position on the
% command line.
; cannot_negate
% The option cannot be negated but its negated form appeared on the
% command line.
; special_handler_failed
% The special option handler predicate for the option failed.
; special_handler_missing
% A special option handler predicate was not provided
% for the option.
; special_handler_error(string)
% The special option handler predicate for the option returned an
% error.
% The argument is a string describing the error.
; requires_numeric_argument(string)
% The option requires a numeric argument but it occurred on the
% command line with a non-numeric argument.
% The argument gives the contents of the argument position on the
% command line.
; file_special_but_no_io(string)
% The option is a file_special option whose argument is the file
% named by the first argument, but the user has not given the
% predicate access to the I/O state.
; file_special_cannot_open(string, io.error)
% The option is a file_special option whose argument is the file
% named by the first argument.
% Attempting to open this file resulted in the I/O error given
% by the second argument.
; file_special_cannot_read(string, io.error)
% The option is a file_special option whose argument is the file
% named by the first argument.
% Attempting to read from this file resulted in the I/O error given
% by the second argument.
; file_special_contains_non_option_args(string)
% The option is a file_special option whose argument is the file
% named by the argument. This file contained some non-option
% arguments.
; file_special_circular_inclusion(string).
% The option is a file_special option whose argument is the file
% named by the argument. This file contained either a direct
% or an indirect reference to an option that called for its
% inclusion, which, if followed, would have resulted in
% an infinite loop of inclusions.
%--------------------------------------------------%
% process_options(OptionOps, Args, NonOptionArgs, Result):
% process_options(OptionOps, Args, OptionArgs, NonOptionArgs, Result):
% process_options_io(OptionOps, Args, NonOptionArgs,
% Result, !IO):
% process_options_io(OptionOps, Args, OptionArgs, NonOptionArgs,
% Result, !IO):
%
% These four predicates do effectively the same job, differing
% from each other in two minor ways.
%
% The job they all do is scanning through 'Args' looking for options.
% The various fields of the OptionOps structure specify the names
% (both short and long) of the options to look for, as well as their
% default values, and possibly the handler for the special options.
% The structure of the `OptionOps' argument is documented above,
% at the definition of the option_ops type.
%
% All these predicates place all the non-option arguments in
% 'NonOptionArgs', and the predicates that have an `OptionArgs' argument
% place the option arguments there. (While some callers will want
% the arguments contain the options, other callers will not, considering
% that the only information they want from them is that contained in
% the option table.)
%
% If they find a problem, such as an unknown option name, an option
% being given an argument of the wrong type, or the failure of the handler
% for a special option, all the predicates will put into `Result'
% an `error' wrapped around an error code. That error code can be turned
% into an error message using the option_error_to_string function below.
%
% If they do not find a problem, all these predicates will place into
% `Result' an `ok' wrapped around an option table, which maps each option
% to its final value. Unless updated by an option in `Args', this will be
% its default value.
%
% The predicate versions whose names end in `io' take a pair of I/O state
% arguments. This is so that they can handle file_special options, which
% require reading a named file, and treating the file's contents as
% specifying additional options. The predicate versions whose names
% do not end in `io' cannot do I/O, and will report an error if they
% encounter a file_special option.
%
:- pred process_options(option_ops(OptionType)::in(option_ops),
list(string)::in, list(string)::out,
maybe_option_table_se(OptionType)::out) is det.
:- pred process_options(option_ops(OptionType)::in(option_ops),
list(string)::in, list(string)::out, list(string)::out,
maybe_option_table_se(OptionType)::out) is det.
:- pred process_options_io(option_ops(OptionType)::in(option_ops),
list(string)::in, list(string)::out,
maybe_option_table_se(OptionType)::out, io::di, io::uo) is det.
:- pred process_options_io(option_ops(OptionType)::in(option_ops),
list(string)::in, list(string)::out, list(string)::out,
maybe_option_table_se(OptionType)::out, io::di, io::uo) is det.
% process_options_track(OptionOps, Args, OptionArgs, NonOptionArgs,
% OptionTable0, Result, OptionsSet):
% process_options_track_io(OptionOps, Args, OptionArgs, NonOptionArgs,
% OptionTable0, Result, OptionsSet, !IO):
%
% These predicates differ from the non-track variants above
% in only two respects.
%
% First, they expect the caller to supply an argument containing
% the initial contents of the option table, instead of calling
% the initialization predicate themselves. This allows a program
% to initialize the option table just once (using either the
% init_option_table or the init_option_table_multi predicate below),
% but then call process_options_track or process_options_track_io
% several times, with different sets of arguments, perhaps obtained
% from different sources (such as command line, configuration file,
% and so on).
%
% Second, each call to one of these predicates returns the set of options
% that were set by that call. This helps with the same objective.
% For example, the caller can tell whether an option was set from
% a configuration file, the command line, both, or neither.
%
:- pred process_options_track(
option_ops_track(OptionType)::in(option_ops_track),
list(string)::in, list(string)::out, list(string)::out,
option_table(OptionType)::in, maybe_option_table_se(OptionType)::out,
set(OptionType)::out) is det.
:- pred process_options_track_io(
option_ops_track(OptionType)::in(option_ops_track),
list(string)::in, list(string)::out, list(string)::out,
option_table(OptionType)::in, maybe_option_table_se(OptionType)::out,
set(OptionType)::out, io::di, io::uo) is det.
% process_options_userdata(OptionOps, Args, OptionArgs, NonOptionArgs,
% MaybeError, OptionsSet, !OptionTable, !UserData):
% process_options_userdata_io(OptionOps, Args, OptionArgs, NonOptionArgs,
% MaybeError, OptionsSet, !OptionTable, !UserData, !IO):
%
% These predicates are similar to the track predicates above, but differ
% in two ways.
%
% - They also thread a piece of state of a user-specified "userdata" type
% through all the handlers of special options, so that each
% special handler can read from and/or write to this state.
% Amongst other things, this can be used by the caller to recover
% the *sequence* in which special options are specified,
% information that is not present in the (orderless) set
% of specified options.
%
% - Even if they find an error, they return the option table as it was
% just before it found the error. This option table will reflect
% all the previous options that could be correctly processed.
%
:- pred process_options_userdata(
option_ops_userdata(OptionType, UserDataType)::in(option_ops_userdata),
list(string)::in, list(string)::out, list(string)::out,
maybe(option_error(OptionType))::out, set(OptionType)::out,
option_table(OptionType)::in, option_table(OptionType)::out,
UserDataType::in, UserDataType::out) is det.
:- pred process_options_userdata_io(
option_ops_userdata(OptionType, UserDataType)::in(option_ops_userdata),
list(string)::in, list(string)::out, list(string)::out,
maybe(option_error(OptionType))::out, set(OptionType)::out,
option_table(OptionType)::in, option_table(OptionType)::out,
UserDataType::in, UserDataType::out, io::di, io::uo) is det.
%--------------------------------------------------%
% init_option_table(InitPred, OptionTable):
% init_option_table_multi(InitPred, OptionTable):
%
% Create an initial option table that maps each option to the default
% value specified for it by InitPred.
%
:- pred init_option_table(
pred(OptionType, option_data)::in(pred(out, out) is nondet),
option_table(OptionType)::out) is det.
:- pred init_option_table_multi(
pred(OptionType, option_data)::in(pred(out, out) is multi),
option_table(OptionType)::out) is det.
%--------------------------------------------------%
% Each value of this type specifies
%
% - the identity of an option (in its first argument),
% - the string on the command line setting that option
% (in its second argument), and, if the option takes a value,
% - the value of the option (in its third argument).
%
% "special" options have no third argument, because they have
% no associated value.
%
% Each occurrence of an accumulating option adds only one string
% to the option's value, which is why ov_accumulating has one string.
% Options that reset an accumulating option to the empty list
% obviously have no associated value.
:- type option_value(OptionType)
---> ov_bool(OptionType, string, bool)
; ov_int(OptionType, string, int)
; ov_string(OptionType, string, string)
; ov_maybe_int(OptionType, string, maybe(int))
; ov_maybe_string(OptionType, string, maybe(string))
; ov_accumulating(OptionType, string, string)
; ov_accumulating_reset(OptionType, string)
; ov_special(OptionType, string)
; ov_bool_special(OptionType, string, bool)
; ov_int_special(OptionType, string, int)
; ov_string_special(OptionType, string, string)
; ov_maybe_string_special(OptionType, string, maybe(string))
; ov_file_special(OptionType, string, string).
:- type maybe_option_error(OptionType)
---> no_option_error
; found_option_error(option_error(OptionType)).
% record_arguments(ShortOptionPred, LongOptionPred, OptionTable,
% Args, NonOptionArgs, OptionArgs, MaybeError, OptionValues):
%
% Given Args, which is a list of command line arguments,
%
% - classify them into arguments that are and are not option args,
% returning them as OptionArgs and NonOptionArgs respectively,
%
% - use the ShortOptionPred and LongOptionPred predicates
% to figure out which user-defined options the OptionArgs refer to,
%
% - use OptionTable to figure out what kind of value, if any,
% each of those user-defined options has as its argument,
%
% - find those arguments and convert them to the expected type, and
%
% - provided no errors occurred in any of the above steps,
% return a list of those options and their values in OptionValues,
% and set MaybeError to no_option_error.
%
% - If some errors *did* occur, then set MaybeError to found_option_error
% wrapped around a description of one of them. This will probably be
% the first, but we do not guarantee that. Also, in this error case,
% OptionValues will probably contain the values of the options processed
% before the error, but we do not guarantee that either.
%
% Note that unlike the process_options_... predicates above,
% this predicate does *not* update the option table in any way.
% It also simply returns file_special options in OptionValues;
% it does not process them. That processing can be done by
% expand_file_specials below.
%
:- pred record_arguments(short_option(OptionType)::in(short_option),
long_option(OptionType)::in(long_option), option_table(OptionType)::in,
list(string)::in, list(string)::out, list(string)::out,
maybe_option_error(OptionType)::out,
list(option_value(OptionType))::out) is det.
%--------------------------------------------------%
% An inst that lists all the function symbols of the option_value type
% *except* ov_file_special.
:- inst non_file_special for option_value/1
---> ov_bool(ground, ground, ground)
; ov_int(ground, ground, ground)
; ov_string(ground, ground, ground)
; ov_maybe_int(ground, ground, ground)
; ov_maybe_string(ground, ground, ground)
; ov_accumulating(ground, ground, ground)
; ov_accumulating_reset(ground, ground)
; ov_special(ground, ground)
; ov_bool_special(ground, ground, ground)
; ov_int_special(ground, ground, ground)
; ov_string_special(ground, ground, ground)
; ov_maybe_string_special(ground, ground, ground).
% expand_file_specials(ShortOptionPred, LongOptionPred, OptionTable,
% OptionValues, MaybeError, NonFileSpecialOptionValues, !MaybeIO):
%
% Given a list of OptionValues as generated for example by
% record_arguments, replace each ov_file_special option value in that list
% with the option values in the file named by that option.
% If there are any errors, return a description of one of them
% in MaybeError; otherwise, return the fully expanded list of options
% in NonFileSpecialOptionValues, and set MaybeError to no_option_error.
%
% The ShortOptionPred, LongOptionPred and OptionTable arguments
% play the same role as in record_arguments, since expand_file_specials
% must of course record all the options in files named by ov_file_special
% option values.
%
:- pred expand_file_specials(short_option(OptionType)::in(short_option),
long_option(OptionType)::in(long_option), option_table(OptionType)::in,
list(option_value(OptionType))::in, maybe_option_error(OptionType)::out,
list(option_value(OptionType))::out(list_skel(non_file_special)),
io::di, io::uo) is det.
%--------------------------------------------------%
% The following functions and predicates search the option table
% for an option of the specified kind. If the option is not in the table,
% they throw an exception.
:- func lookup_bool_option(option_table(Option), Option) = bool.
:- pred lookup_bool_option(option_table(Option)::in, Option::in,
bool::out) is det.
:- func lookup_int_option(option_table(Option), Option) = int.
:- pred lookup_int_option(option_table(Option)::in, Option::in,
int::out) is det.
:- func lookup_string_option(option_table(Option), Option) = string.
:- pred lookup_string_option(option_table(Option)::in, Option::in,
string::out) is det.
:- func lookup_maybe_int_option(option_table(Option), Option) = maybe(int).
:- pred lookup_maybe_int_option(option_table(Option)::in, Option::in,
maybe(int)::out) is det.
:- func lookup_maybe_string_option(option_table(Option), Option) =
maybe(string).
:- pred lookup_maybe_string_option(option_table(Option)::in, Option::in,
maybe(string)::out) is det.
:- func lookup_accumulating_option(option_table(Option), Option) =
list(string).
:- pred lookup_accumulating_option(option_table(Option)::in, Option::in,
list(string)::out) is det.
%--------------------------------------------------%
% Convert the structured representation of an error
% to an error message.
%
:- func option_error_to_string(option_error(OptionType)) = string.
%--------------------------------------------------%
% If the argument represents an error, then convert that error from
% the structured representation to an error message.
%
:- func convert_to_maybe_option_table(maybe_option_table_se(OptionType))
= maybe_option_table(OptionType).
%--------------------------------------------------%
%--------------------------------------------------%