-
Notifications
You must be signed in to change notification settings - Fork 402
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1102 from ApexAI/iox-#1067-create-command-line-pa…
…rser-abstraction iox #1067 create command line parser abstraction
- Loading branch information
Showing
34 changed files
with
4,104 additions
and
14 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,162 @@ | ||
# Command Line Parser | ||
|
||
## Summary | ||
|
||
For command line parsing the POSIX `getopt` and `getopt_long` are available. | ||
Those functions have several downsides: | ||
|
||
* Are not available on all platforms (Windows). | ||
* Are error prone to use. The user has to consider all conversion edge cases | ||
and verify the syntax. | ||
* Are hard to use and the API is hard to read. | ||
* One has to write a lot of code to use them, even for minimal problems. | ||
* How the help is shown, how the help is formatted and certain syntax decision | ||
can vary from binary to binary in iceoryx. A unified appearance and usage | ||
across all applications would increase the user experience. | ||
|
||
Since we would like to offer command line support on all platforms we either | ||
have to rewrite `getopt` and `getopt_long` from scratch or write a modern C++ | ||
alternative. | ||
|
||
## Terminology | ||
|
||
The terminology is close to the terminology used in the POSIX.1-2017 standard. | ||
For more details see | ||
[Utility Conventions](https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html). | ||
|
||
Assume we have a iceoryx tool with command line arguments like: | ||
``` | ||
iox_tool -a --bla -c 123 --dr_dotter blubb | ||
``` | ||
|
||
| Name | Description | | ||
| :---------------- | :------------------------------------------------------- | | ||
| argument | A chain of arbitrary symbols after the command. Arguments are separated by space. Examples: `-a`, `--bla` | | ||
| options | Arguments which start with a single dash `-` or double dash `--`, like `-a` or `--bla`. | | ||
| short option | Argument which starts with a single dash followed by a non-dash symbol, like `-a` or `-c` | | ||
| long option | Argument which starts with double dash followed by at least another non-dash symbol, like `--bla` or `--dr_dotter` | | ||
| option name | The symbols after the leading dash of an option, for instance `a` or `dr_dotter`. | | ||
| option argument | Argument which is separated by a space from an option, like `123` or `blubb`. | | ||
| argc & argv | Taken from `int main(int argc, char* argv[])`. Provides access to the command line arguments. | | ||
|
||
### Option Types | ||
|
||
| Name | Description | | ||
| :---------------- | :------------------------------------------------------- | | ||
| switch | When provided as argument the switch is activated, see `-h` and `--help`. A boolean signals to the user whether the switch was provided (activated) or not. | | ||
| optional | A value which can be provided via command line but is not required. Every optional option requires a default value. | | ||
| required | A value which has to be provided via command line. Does not require a default value. | | ||
|
||
Every option has an option argument but since the switch is boolean in nature | ||
the argument is implicitly provided when stating the option. | ||
|
||
## Design | ||
|
||
### Considerations | ||
|
||
The solution shall be: | ||
|
||
* easy to use by the developer | ||
* provide a unified appearance and syntax in all applications with command line arguments | ||
* the help must be generated by the parser | ||
* the help shall contain the type if an argument requires a value | ||
* the syntax is defined by the parser not the developer as much as possible | ||
* shall be type safe when command line arguments are converted to types like `int` or `float` | ||
* underflow, overflow shall be handled | ||
* a detailed error explanation shall be presented to the user when cast fails | ||
|
||
### Solution | ||
|
||
#### Class Diagram | ||
|
||
The `CommandLineParser` takes an `OptionDefinition` and parses | ||
the raw command line arguments (`argc` and `argv`) based onto the `OptionDefinition` | ||
into `Arguments` which can be used to access the values of those options. | ||
|
||
![class diagram](../website/images/command_line_parser_class_overview.svg) | ||
|
||
#### Sequence Diagram | ||
|
||
Lets assume we would like to add a switch, an optional and a required option, parse | ||
them and print them on the console. | ||
|
||
![sequence diagram](../website/images/command_line_parser_usage.svg) | ||
|
||
#### Macro Based Code Generator | ||
|
||
```cpp | ||
struct UserCLIStruct | ||
{ | ||
IOX_CLI_DEFINITION(UserCLIStruct, "My program description"); | ||
|
||
IOX_CLI_OPTIONAL(string<100>, stringValue, {"default Value"}, 's', "string-value", "some description"); | ||
IOX_CLI_REQUIRED(string<100>, anotherString, 'a', "another-string", "some description"); | ||
IOX_CLI_SWITCH(uint64_t, version, 0, 'v', "version", "print app version"); | ||
}; | ||
|
||
// This struct parses all command line arguments and stores them. In | ||
// the example above the struct provides access to | ||
// .stringValue() | ||
// .anotherString() | ||
// .version() | ||
// Via the command line parameters | ||
// -s or --string-value | ||
// -a or --another-string | ||
// -v or --version | ||
|
||
int main(int argc, char* argv[]) { | ||
UserCLIStruct cmd(argc, argv); | ||
std::cout << cmd.stringValue() << " " << cmd.anotherString() << std::endl; | ||
} | ||
``` | ||
The macros `IOX_CLI_DEFINITION`, `IOX_CLI_SWITCH`, `IOX_CLI_OPTIONAL` and `IOX_CLI_REQUIRED` | ||
provide building blocks so that the user can generate a struct. The members of that | ||
struct are defined via the macros and set in the constructor of that struct which | ||
will use the `OptionManager` to parse and extract the | ||
values safely. | ||
![macro sequence diagram](../website/images/command_line_parser_macro_usage.svg) | ||
The struct constructor can be called with `argc` and `argv` as arguments from | ||
`int main(int argc, char* argv[])`. | ||
## Open issues | ||
Our `iox` tool should be structured similar like the `ros2` tool. This | ||
means `iox COMMAND ARGUMENTS_OF_THE_COMMAND`. The current implementation allows us | ||
only to parse the `ARGUMENTS_OF_THE_COMMAND` but not handle the `COMMAND` in an easy manner. | ||
A follow up pull request will address this issue. | ||
The structure will look like the following (taken from proof of concept): | ||
```cpp | ||
struct Command { | ||
cxx::string command; | ||
cxx::function<void(int, char**)> call; | ||
}; | ||
// this generates a help with an overview of all available commands which can | ||
// be printed with --help or when a syntax error occurs | ||
bool parseCommand(argc, argv, const vector<Command> & availableCommands); | ||
void userDefinedCommand1(argc, argv) { | ||
// here we use the already implemented CommandLineStruct to parse the | ||
// arguments of the command | ||
// The CommandLineStruct generates the help for the command line arguments | ||
// of that specific command | ||
} | ||
void userDefinedCommand2(argc, argv) { | ||
} | ||
int main(int argc, char* argv[]) { | ||
parseCommand(argc, argv, {{"command1", userDefinedCommand1}, | ||
{"anotherCommand", userDefinedCommand2}}); | ||
} | ||
``` | ||
|
||
The idea is to handle every command independently from every other command and | ||
do not define everything in one big command line parser object which would | ||
violate the separation of concerns. |
69 changes: 69 additions & 0 deletions
69
doc/design/diagrams/command_line_parser/command_line_parser_class_overview.puml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
@startuml | ||
|
||
class OptionDefinition { | ||
+OptionDefinition(programDescription, onFailureCallback) | ||
+addSwitch(..) | ||
+addOptional(..) | ||
+addRequired(..) | ||
} | ||
|
||
class Arguments { | ||
+T get(optionName) | ||
+bool has(optionName) | ||
+BinaryName_t binaryName() | ||
} | ||
|
||
class CommandLineParser { | ||
+Arguments parse(OptionDefinition, ..) | ||
} | ||
|
||
class Option { | ||
+isSwitch() | ||
+hasOptionName(optionName) | ||
+isSameOption(otherOption) | ||
+operator<(otherOption) | ||
+isEmpty() | ||
+longOptionNameDoesStartWithDash() | ||
+shortOptionNameIsEqualDash() | ||
+hasLongOptionName(optionName) | ||
+hasShortOptionName(value) | ||
+hasShortOption() | ||
+hasLongOption() | ||
#shortOption | ||
#longOption | ||
#value | ||
} | ||
|
||
class OptionWithDetails::Details{ | ||
#description | ||
#type | ||
#typeName | ||
} | ||
|
||
class OptionWithDetails { | ||
OptionWithDetails(option, description, type, typeName) | ||
operator<(otherOptionWithDetails) | ||
|
||
#details | ||
} | ||
|
||
|
||
class OptionManager { | ||
+OptionManager(programDescription, onFailureCallback) | ||
+T defineOption(..) | ||
+populateDefinedOptions(..) | ||
} | ||
note "Used only by the macro struct builder IOX_CLI_DEFINITION" as N1 | ||
OptionManager .. N1 | ||
|
||
|
||
OptionWithDetails "1" *-- "1" OptionWithDetails::Details | ||
Option <|--- OptionWithDetails | ||
|
||
Arguments "0..n" *-- "1" Option | ||
OptionDefinition "0..n" *-- "1" OptionWithDetails | ||
|
||
CommandLineParser "1" *-- "1" OptionDefinition : borrows | ||
CommandLineParser "1" *-- "1" Arguments : contains | ||
|
||
@enduml |
45 changes: 45 additions & 0 deletions
45
doc/design/diagrams/command_line_parser/command_line_parser_macro_usage.puml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
@startuml | ||
|
||
participant User | ||
participant UserCLIStruct | ||
participant OptionManager | ||
|
||
== Create a struct named UserCLIStruct to store command line option values == | ||
|
||
rnote right User | ||
IOX_CLI_DEFINITION(UserCLIStruct, ProgramDescription) | ||
end note | ||
|
||
== Define options which are stored in struct members == | ||
|
||
rnote right User | ||
IOX_CLI_OPTIONAL(a ...) | ||
IOX_CLI_REQUIRED(b ...) | ||
IOX_CLI_SWITCH(c ...) | ||
end note | ||
|
||
== Instantiate UserCLIStruct, parse command line arguments and populate members == | ||
|
||
create UserCLIStruct | ||
|
||
User -> UserCLIStruct ++ : UserCLIStruct(argc, argv) | ||
|
||
|
||
UserCLIStruct -> OptionManager ++ : OptionManager(ProgramDescription) | ||
return | ||
|
||
UserCLIStruct -> OptionManager ++ : defineOption(a ...) | ||
return | ||
|
||
UserCLIStruct -> OptionManager ++ : defineOption(b ...) | ||
return | ||
|
||
UserCLIStruct -> OptionManager ++ : defineOption(c ...) | ||
return | ||
|
||
UserCLIStruct -> OptionManager ++ : populateDefinedOptions() | ||
return | ||
|
||
return | ||
|
||
@enduml |
36 changes: 36 additions & 0 deletions
36
doc/design/diagrams/command_line_parser/command_line_parser_usage.puml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
@startuml | ||
|
||
== Create a OptionDefinition and define the application options == | ||
|
||
User -> OptionDefinition ++ : OptionDefinition(programName, onFailureCallback) | ||
return OptionDefinition | ||
|
||
User -> OptionDefinition ++ : addSwitch() | ||
return | ||
|
||
User -> OptionDefinition ++ : addOptional() | ||
return | ||
|
||
User -> OptionDefinition ++ : addRequired() | ||
return | ||
|
||
== Parse command line arguments == | ||
|
||
User -> CommandLineParser ++ : parse(OptionDefinition) | ||
return Arguments | ||
|
||
== Acquire command line option values == | ||
|
||
User -> Arguments ++ : has(switchName) | ||
return bool | ||
|
||
User -> Arguments ++ : get<TypeName>(optionalOptionName) | ||
return TypeName | ||
|
||
User -> Arguments ++ : get<TypeName>(requiredOptionName) | ||
return TypeName | ||
|
||
User -> Arguments ++ : binaryName() | ||
return BinaryName_t | ||
|
||
@enduml |
Oops, something went wrong.