cam: options: Support parent-child relationship between options

Add support for creating a tree-based hieararchy of options instead of a
flat list. This is useful to support options that need to be interpreted
in the context of a particular occurrence of an array option.

The usage text automatically documents the options in their
corresponding context:

Options:
  -c, --camera camera ...                               Specify which camera to operate on, by id or by index
  -h, --help                                            Display this help message
  -I, --info                                            Display information about stream(s)
  -l, --list                                            List all cameras
      --list-controls                                   List cameras controls
  -p, --list-properties                                 List cameras properties
  -m, --monitor                                         Monitor for hotplug and unplug camera events

Options valid in the context of --camera:
  -C, --capture[=count]                                 Capture until interrupted by user or until <count> frames captured
  -F, --file[=filename]                                 Write captured frames to disk
                                                        If the file name ends with a '/', it sets the directory in which
                                                        to write files, using the default file name. Otherwise it sets the
                                                        full file path and name. The first '#' character in the file name
                                                        is expanded to the camera index, stream name and frame sequence number.
                                                        The default file name is 'frame-#.bin'.
  -s, --stream key=value[,key=value,...] ...            Set configuration of a camera stream
          height=integer                                Height in pixels
          pixelformat=string                            Pixel format name
          role=string                                   Role for the stream (viewfinder, video, still, raw)
          width=integer                                 Width in pixels
      --strict-formats                                  Do not allow requested stream format(s) to be adjusted
      --metadata                                        Print the metadata for completed requests

Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
Reviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com>
This commit is contained in:
Laurent Pinchart 2021-07-06 05:29:57 +03:00
parent b53f68e66c
commit bb682d2585
2 changed files with 199 additions and 32 deletions

View file

@ -79,6 +79,12 @@
* \var Option::isArray
* \brief Whether the option can appear once or multiple times
*
* \var Option::parent
* \brief The parent option
*
* \var Option::children
* \brief List of child options, storing all options whose parent is this option
*
* \fn Option::hasShortOption()
* \brief Tell if the option has a short option specifier (e.g. `-f`)
* \return True if the option has a short option specifier, false otherwise
@ -96,6 +102,8 @@ struct Option {
const char *help;
KeyValueParser *keyValueParser;
bool isArray;
Option *parent;
std::list<Option> children;
bool hasShortOption() const { return isalnum(opt); }
bool hasLongOption() const { return name != nullptr; }
@ -335,7 +343,7 @@ bool KeyValueParser::addOption(const char *name, OptionType type,
return false;
optionsMap_[name] = Option({ 0, type, name, argument, nullptr,
help, nullptr, false });
help, nullptr, false, nullptr, {} });
return true;
}
@ -472,6 +480,11 @@ void KeyValueParser::usage(int indent)
* option. It supports empty values, integers, strings, key-value lists, as well
* as arrays of those types. For array values, all array elements shall have the
* same type.
*
* OptionValue instances are organized in a tree-based structure that matches
* the parent-child relationship of the options added to the parser. Children
* are retrieved with the children() function, and are stored as an
* OptionsBase<int>.
*/
/**
@ -662,6 +675,15 @@ std::vector<OptionValue> OptionValue::toArray() const
return array_;
}
/**
* \brief Retrieve the list of child values
* \return The list of child values
*/
const OptionsParser::Options &OptionValue::children() const
{
return children_;
}
/* -----------------------------------------------------------------------------
* OptionsParser
*/
@ -724,6 +746,32 @@ std::vector<OptionValue> OptionValue::toArray() const
* accept an argument, the option value can be access by Options::operator[]()
* using the option identifier as the key. The order in which different options
* are specified on the command line isn't preserved.
*
* Options can be created with parent-child relationships to organize them as a
* tree instead of a flat list. When parsing a command line, the child options
* are considered related to the parent option that precedes them. This is
* useful when the parent is an array option. The Options values list generated
* by the parser then turns into a tree, which each parent value storing the
* values of child options that follow that instance of the parent option.
* For instance, with a `capture` option specified as a child of a `camera`
* array option, parsing the command line
*
* `--camera 1 --capture=10 --camera 2 --capture=20`
*
* will return an Options instance containing a single OptionValue instance of
* array type, for the `camera` option. The OptionValue will contain two
* entries, with the first entry containing the integer value 1 and the second
* entry the integer value 2. Each of those entries will in turn store an
* Options instance that contains the respective children. The first entry will
* store in its children a `capture` option of value 10, and the second entry a
* `capture` option of value 20.
*
* The command line
*
* `--capture=10 --camera 1`
*
* would result in a parsing error, as the `capture` option has no preceding
* `camera` option on the command line.
*/
/**
@ -747,13 +795,14 @@ OptionsParser::~OptionsParser() = default;
* mandatory argument, or no argument at all
* \param[in] argumentName The argument name used in the help text
* \param[in] array Whether the option can appear once or multiple times
* \param[in] parent The identifier of the parent option (optional)
*
* \return True if the option was added successfully, false if an error
* occurred.
*/
bool OptionsParser::addOption(int opt, OptionType type, const char *help,
const char *name, OptionArgument argument,
const char *argumentName, bool array)
const char *argumentName, bool array, int parent)
{
/*
* Options must have at least a short or long name, and a text message.
@ -770,9 +819,31 @@ bool OptionsParser::addOption(int opt, OptionType type, const char *help,
if (optionsMap_.find(opt) != optionsMap_.end())
return false;
options_.push_back(Option({ opt, type, name, argument, argumentName,
help, nullptr, array }));
optionsMap_[opt] = &options_.back();
/*
* If a parent is specified, create the option as a child of its parent.
* Otherwise, create it in the parser's options list.
*/
Option *option;
if (parent) {
auto iter = optionsMap_.find(parent);
if (iter == optionsMap_.end())
return false;
Option *parentOpt = iter->second;
parentOpt->children.push_back({
opt, type, name, argument, argumentName, help, nullptr,
array, parentOpt, {}
});
option = &parentOpt->children.back();
} else {
options_.push_back({ opt, type, name, argument, argumentName,
help, nullptr, array, nullptr, {} });
option = &options_.back();
}
optionsMap_[opt] = option;
return true;
}
@ -790,13 +861,13 @@ bool OptionsParser::addOption(int opt, OptionType type, const char *help,
* occurred.
*/
bool OptionsParser::addOption(int opt, KeyValueParser *parser, const char *help,
const char *name, bool array)
const char *name, bool array, int parent)
{
if (!addOption(opt, OptionKeyValue, help, name, ArgumentRequired,
"key=value[,key=value,...]", array))
"key=value[,key=value,...]", array, parent))
return false;
options_.back().keyValueParser = parser;
optionsMap_[opt]->keyValueParser = parser;
return true;
}
@ -821,26 +892,26 @@ OptionsParser::Options OptionsParser::parse(int argc, char **argv)
* Allocate short and long options arrays large enough to contain all
* options.
*/
char shortOptions[options_.size() * 3 + 2];
struct option longOptions[options_.size() + 1];
char shortOptions[optionsMap_.size() * 3 + 2];
struct option longOptions[optionsMap_.size() + 1];
unsigned int ids = 0;
unsigned int idl = 0;
shortOptions[ids++] = ':';
for (const Option &option : options_) {
if (option.hasShortOption()) {
shortOptions[ids++] = option.opt;
if (option.argument != ArgumentNone)
for (const auto [opt, option] : optionsMap_) {
if (option->hasShortOption()) {
shortOptions[ids++] = opt;
if (option->argument != ArgumentNone)
shortOptions[ids++] = ':';
if (option.argument == ArgumentOptional)
if (option->argument == ArgumentOptional)
shortOptions[ids++] = ':';
}
if (option.hasLongOption()) {
longOptions[idl].name = option.name;
if (option->hasLongOption()) {
longOptions[idl].name = option->name;
switch (option.argument) {
switch (option->argument) {
case ArgumentNone:
longOptions[idl].has_arg = no_argument;
break;
@ -853,7 +924,7 @@ OptionsParser::Options OptionsParser::parse(int argc, char **argv)
}
longOptions[idl].flag = 0;
longOptions[idl].val = option.opt;
longOptions[idl].val = option->opt;
idl++;
}
}
@ -881,10 +952,7 @@ OptionsParser::Options OptionsParser::parse(int argc, char **argv)
}
const Option &option = *optionsMap_[c];
if (!options.parseValue(c, option, optarg)) {
std::cerr << "Can't parse " << option.typeName()
<< " argument for option " << option.optionName()
<< std::endl;
if (!parseValue(option, optarg, &options)) {
usage();
return options;
}
@ -906,15 +974,16 @@ void OptionsParser::usage()
{
unsigned int indent = 0;
for (const Option &option : options_) {
for (const auto &opt : optionsMap_) {
const Option *option = opt.second;
unsigned int length = 14;
if (option.hasLongOption())
length += 2 + strlen(option.name);
if (option.argument != ArgumentNone)
length += 1 + strlen(option.argumentName);
if (option.argument == ArgumentOptional)
if (option->hasLongOption())
length += 2 + strlen(option->name);
if (option->argument != ArgumentNone)
length += 1 + strlen(option->argumentName);
if (option->argument == ArgumentOptional)
length += 2;
if (option.isArray)
if (option->isArray)
length += 4;
if (length > indent)
@ -937,6 +1006,8 @@ void OptionsParser::usage()
void OptionsParser::usageOptions(const std::list<Option> &options,
unsigned int indent)
{
std::vector<const Option *> parentOptions;
for (const Option &option : options) {
std::string argument;
if (option.hasShortOption())
@ -981,5 +1052,91 @@ void OptionsParser::usageOptions(const std::list<Option> &options,
if (option.keyValueParser)
option.keyValueParser->usage(indent);
if (!option.children.empty())
parentOptions.push_back(&option);
}
if (parentOptions.empty())
return;
for (const Option *option : parentOptions) {
std::cerr << std::endl << "Options valid in the context of "
<< option->optionName() << ":" << std::endl;
usageOptions(option->children, indent);
}
}
std::tuple<OptionsParser::Options *, const Option *>
OptionsParser::childOption(const Option *parent, Options *options)
{
/*
* The parent argument points to the parent of the leaf node Option,
* and the options argument to the root node of the Options tree. Use
* recursive calls to traverse the Option tree up to the root node while
* traversing the Options tree down to the leaf node:
*/
/*
* - If we have no parent, we've reached the root node of the Option
* tree, the options argument is what we need.
*/
if (!parent)
return { options, nullptr };
/*
* - If the parent has a parent, use recursion to move one level up the
* Option tree. This returns the Options corresponding to parent, or
* nullptr if a suitable Options child isn't found.
*/
if (parent->parent) {
const Option *error;
std::tie(options, error) = childOption(parent->parent, options);
/* Propagate the error all the way back up the call stack. */
if (!error)
return { options, error };
}
/*
* - The parent has no parent, we're now one level down the root.
* Return the Options child corresponding to the parent. The child may
* not exist if options are specified in an incorrect order.
*/
if (!options->isSet(parent->opt))
return { nullptr, parent };
/*
* If the child value is of array type, children are not stored in the
* value .children() list, but in the .children() of the value's array
* elements. Use the last array element in that case, as a child option
* relates to the last instance of its parent option.
*/
const OptionValue *value = &(*options)[parent->opt];
if (value->type() == OptionValue::ValueArray)
value = &value->toArray().back();
return { const_cast<Options *>(&value->children()), nullptr };
}
bool OptionsParser::parseValue(const Option &option, const char *arg,
Options *options)
{
const Option *error;
std::tie(options, error) = childOption(option.parent, options);
if (error) {
std::cerr << "Option " << option.optionName() << " requires a "
<< error->optionName() << " context" << std::endl;
return false;
}
if (!options->parseValue(option.opt, option, arg)) {
std::cerr << "Can't parse " << option.typeName()
<< " argument for option " << option.optionName()
<< std::endl;
return false;
}
return true;
}

View file

@ -10,6 +10,7 @@
#include <ctype.h>
#include <list>
#include <map>
#include <tuple>
#include <vector>
class KeyValueParser;
@ -91,9 +92,11 @@ public:
bool addOption(int opt, OptionType type, const char *help,
const char *name = nullptr,
OptionArgument argument = ArgumentNone,
const char *argumentName = nullptr, bool array = false);
const char *argumentName = nullptr, bool array = false,
int parent = 0);
bool addOption(int opt, KeyValueParser *parser, const char *help,
const char *name = nullptr, bool array = false);
const char *name = nullptr, bool array = false,
int parent = 0);
Options parse(int argc, char *argv[]);
void usage();
@ -104,6 +107,10 @@ private:
void usageOptions(const std::list<Option> &options, unsigned int indent);
std::tuple<OptionsParser::Options *, const Option *>
childOption(const Option *parent, Options *options);
bool parseValue(const Option &option, const char *arg, Options *options);
std::list<Option> options_;
std::map<unsigned int, Option *> optionsMap_;
};
@ -139,12 +146,15 @@ public:
KeyValueParser::Options toKeyValues() const;
std::vector<OptionValue> toArray() const;
const OptionsParser::Options &children() const;
private:
ValueType type_;
int integer_;
std::string string_;
KeyValueParser::Options keyValues_;
std::vector<OptionValue> array_;
OptionsParser::Options children_;
};
#endif /* __CAM_OPTIONS_H__ */