Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/p-ranav/structopt
Parse command line arguments by defining a struct
https://github.com/p-ranav/structopt
argparse argument-parser arguments clap command-line cpp17 cross-platform header-library header-only library lightweight magic-enum mit-license modern-cpp reflection single-header-lib structopt type-safe type-safety visit-struct-library
Last synced: 7 days ago
JSON representation
Parse command line arguments by defining a struct
- Host: GitHub
- URL: https://github.com/p-ranav/structopt
- Owner: p-ranav
- License: mit
- Created: 2020-08-11T14:32:51.000Z (over 4 years ago)
- Default Branch: master
- Last Pushed: 2024-10-27T15:29:04.000Z (3 months ago)
- Last Synced: 2024-12-29T03:07:07.197Z (14 days ago)
- Topics: argparse, argument-parser, arguments, clap, command-line, cpp17, cross-platform, header-library, header-only, library, lightweight, magic-enum, mit-license, modern-cpp, reflection, single-header-lib, structopt, type-safe, type-safety, visit-struct-library
- Language: C++
- Homepage:
- Size: 648 KB
- Stars: 469
- Watchers: 13
- Forks: 26
- Open Issues: 6
-
Metadata Files:
- Readme: README.md
- Contributing: CONTRIBUTING.md
- License: LICENSE
Awesome Lists containing this project
- awesome-hpp - structopt - ranav/structopt?style=social)](https://github.com/p-ranav/structopt/stargazers/) | Parse command line arguments by defining a struct. | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) | (Argument Parsers)
README
Parse command line arguments by defining a struct## Quick Start
```cpp
#includestruct Options {
// positional argument
// e.g., ./main
std::string config_file;// optional argument
// e.g., -b "192.168.5.3"
// e.g., --bind_address "192.168.5.3"
//
// options can be delimited with `=` or `:`
// note: single dash (`-`) is enough for short & long option
// e.g., -bind_address=localhost
// e.g., -b:192.168.5.3
//
// the long option can also be provided in kebab case:
// e.g., --bind-address 192.168.5.3
std::optional bind_address;
// flag argument
// Use `std::optional` and provide a default value.
// e.g., -v
// e.g., --verbose
// e.g., -verbose
std::optional verbose = false;// directly define and use enum classes to limit user choice
// e.g., --log-level debug
// e.g., -l error
enum class LogLevel { debug, info, warn, error, critical };
std::optional log_level = LogLevel::info;// pair argument
// e.g., -u
// e.g., --user
std::optional> user;// use containers like std::vector
// to collect "remaining arguments" into a list
std::vector files;
};
STRUCTOPT(Options, config_file, bind_address, verbose, log_level, user, files);
```Create a `structopt::app` and parse the command line arguments into the `Options` struct:
```cpp
int main(int argc, char *argv[]) {try {
// Line of code that does all the work:
auto options = structopt::app("my_app").parse(argc, argv);// Print out parsed arguments:
// std::cout << "config_file = " << options.config_file << "\n";
// std::cout << "bind_address = " << options.bind_address.value_or("not provided") << "\n";
// std::cout << "verbose = " << std::boolalpha << options.verbose.value() << "\n";
// ...} catch (structopt::exception& e) {
std::cout << e.what() << "\n";
std::cout << e.help();
}
}
```Now let's pass some arguments to this program:
```console
foo@bar:~$ ./main config.csv file5.csv file6.json
config_file = config.csv
bind_address = not provided
verbose = false
log_level = 1
user = not provided
files = { file5.csv file6.json }foo@bar:~$ ./main config.csv --bind-address localhost:9000 -v -log-level error file1.txt file2.txt
config_file = config.csv
bind_address = localhost:9000
verbose = true
log_level = 3
user = not provided
files = { file1.txt file2.txt }foo@bar:~$ ./main config_2.csv --bind-address 192.168.7.3 -log-level debug file1.txt file3.txt file4.txt --user "John Doe" "[email protected]"
config_file = config_2.csv
bind_address = 192.168.7.3
verbose = false
log_level = 0
user = John Doe
files = { file1.txt file3.txt file4.txt }
```## Table of Contents
* [Getting Started](#getting-started)
* [Positional Arguments](#positional-arguments)
* [Optional Arguments](#optional-arguments)
* [Double dash (`--`) Argument](#double-dash----argument)
* [Flag Arguments](#flag-arguments)
* [Enum Class Arguments (Choices)](#enum-class-arguments)
* [Tuple Arguments](#tuple-arguments)
* [Vector Arguments](#vector-arguments)
* [Compound Arguments](#compound-arguments)
* [Parsing Numbers](#parsing-numbers)
* [Integer Literals](#integer-literals)
* [Floating point Literals](#floating-point-literals)
* [Nested Structures (Sub-commands)](#nested-structures)
* [Sub-Commands, Vector Arguments, and Delimited Positional Arguments](#sub-commands-vector-arguments-and-delimited-positional-arguments)
* [Printing Help](#printing-help)
* [Printing CUSTOM Help](#printing-custom-help)
* [Building Samples and Tests](#building-samples-and-tests)
* [Compiler Compatibility](#compiler-compatibility)
* [Generating Single Header](#generating-single-header)
* [Contributing](#contributing)
* [License](#license)## Getting Started
`structopt` is a header-only library. Just add `include/` to your _include_directories_ and you should be good to go. A single header file version is also available in `single_include/`.
### Positional Arguments
Here's an example of two positional arguments: `input_file` and `output_file`. `input_file` is expected to be the first argument and `output_file` is expected to be the second argument
```cpp
#includestruct FileOptions {
// Positional arguments
// ./main
std::string input_file;
std::string output_file;
};
STRUCTOPT(FileOptions, input_file, output_file);int main(int argc, char *argv[]) {
try {
auto options = structopt::app("my_app").parse(argc, argv);// Print parsed arguments:
std::cout << "\nInput file : " << options.input_file << "\n";
std::cout << "Output file : " << options.output_file << "\n";} catch (structopt::exception& e) {
std::cout << e.what() << "\n";
std::cout << e.help();
}
}
``````console
foo@bar:~$ ./main foo.txt bar.csvInput file : foo.txt
Output file : bar.csvfoo@bar:~$ ./main foo.csv
Error: expected value for positional argument `output_file`.USAGE: ./my_app input_file output_file
ARGS:
input_file
output_file
```### Optional Arguments
Now, let's look at optional arguments. To configure an optional argument, use `std::optional` in the options struct like below.
```cpp
#includestruct GccOptions {
// language standard
// e.g., -std=c++17
// e.g., --std c++20
std::optional std = "c++11";// verbosity enabled with `-v` or `--verbose`
// or `-verbose`
std::optional verbose = false;// enable all warnings with `-Wall`
std::optional Wall = false;// produce only the compiled code
// e.g., gcc -C main.c
std::optional Compile = false;// produce output with `-o `
std::optional output = "a.out";std::string input_file;
};
STRUCTOPT(GccOptions, std, verbose, Wall, Compile, output, input_file);int main(int argc, char *argv[]) {
try {
auto options = structopt::app("gcc").parse(argc, argv);// Print parsed arguments
std::cout << "std : " << options.std.value() << "\n";
std::cout << "verbose : " << std::boolalpha << options.verbose.value() << "\n";
std::cout << "Wall : " << std::boolalpha << options.Wall.value() << "\n";
std::cout << "Compile : " << std::boolalpha << options.Compile.value() << "\n";
std::cout << "Output : " << options.output.value() << "\n";
std::cout << "Input file : " << options.input_file << "\n";
} catch (structopt::exception &e) {
std::cout << e.what() << "\n";
std::cout << e.help();
}
}
```***NOTE*** `structopt` supports two option delimiters, `=` and `:` for optional arguments. This is meaningful and commonly used in single-valued optional arguments, e.g., `--std=c++17`.
```console
foo@bar:~$ ./main -C main.cpp
std : c++11
verbose : false
Wall : false
Compile : true
Output : a.out
Input file : main.cppfoo@bar:~$ ./main -std=c++17 -o main main.cpp
std : c++17
verbose : false
Wall : false
Compile : false
Output : main
Input file : main.cppfoo@bar:~$ ./main main.cpp -v -std:c++14 --output:main -Wall
std : c++14
verbose : true
Wall : true
Compile : false
Output : main
Input file : main.cpp
```***NOTE*** In summary, for a field in your struct named `bind_address`, the following are all legal ways to provide a value:
* Short form:
* `-b `
* Long form:
* `--bind_address `
* `-bind_address `
* Kebab case:
* `--bind-address `
* `-bind-address `
* Equal (`'='`) option delimiter
* `-b=`
* `--bind_address=`
* `-bind_address=`
* `--bind-address=`
* `-bind-address=`
* Colon `':'` option delimiter
* `-b:`
* `--bind_address:`
* `-bind_address:`
* `--bind-address:`
* `-bind-address:`#### Double dash (`--`) Argument
A double dash (`--`) is used in most bash built-in commands and many other commands to signify the end of command options, after which only positional parameters are accepted.
Example use: lets say you want to `grep` a file for the string `-v` - normally `-v` will be considered the option to reverse the matching meaning (only show lines that do not match), but with `--` you can `grep` for string `-v` like this:
```cpp
#includestruct GrepOptions {
// reverse the matching
// enable with `-v`
std::optional v = false;
// positional arguments
std::string search;
std::string pathspec;
};
STRUCTOPT(GrepOptions, v, search, pathspec);int main(int argc, char *argv[]) {
try {
auto options = structopt::app("my_app").parse(argc, argv);if (options.v == true) {
std::cout << "`-v` provided - Matching is now reversed\n";
}std::cout << "Search : " << options.search << "\n";
std::cout << "Pathspec : " << options.pathspec << "\n";
}
catch (structopt::exception& e) {
std::cout << e.what();
std::cout << e.help();
}}
``````console
foo@bar:~$ ./main -v foo bar.txt
`-v` provided - Matching is now reversed
Search : foo
Pathspec : bar.txtfoo@bar:~$ ./main -- -v bar.txt
Search : -v
Pathspec : bar.txt
```### Flag Arguments
Flag arguments are `std::optional` with a default value.
***NOTE*** The default value here is important. It is not a flag if a default value isn't provided. It will simply be an optional argument.
***NOTE*** If `--verbose` is a flag argument with a default value of `false`, then providing the argument will set it to `true`. If `--verbose` does not have a default value, then `structopt` will expect the user to provide a value, e.g., `--verbose true`.
```cpp
#includestruct Options {
// verbosity flag
// -v, --verbose
// remember to provide a default value
std::optional verbose = false;
};
STRUCTOPT(Options, verbose);int main(int argc, char *argv[]) {
auto options = structopt::app("my_app").parse(argc, argv);if (options.verbose == true) {
std::cout << "Verbosity enabled\n";
}
}
``````console
foo@bar:~$ ./mainfoo@bar:~$ ./main -v
Verbosity enabledfoo@bar:~$ ./main --verbose
Verbosity enabled
```### Enum Class Arguments
Thanks to [magic_enum](https://github.com/Neargye/magic_enum), `structopt` supports enum classes. You can use an enum classes to ask the user to provide a value given a choice of values, restricting the possible set of allowed input arguments.
```cpp
#includestruct StyleOptions {
enum class Color {red, green, blue};// e.g., `--color red`
std::optional color = Color::red;
};
STRUCTOPT(StyleOptions, color);int main(int argc, char *argv[]) {
try {
auto options = structopt::app("my_app").parse(argc, argv);// Use parsed argument `options.color`
if (options.color == StyleOptions::Color::red) {
std::cout << "#ff0000\n";
}
else if (options.color == StyleOptions::Color::blue) {
std::cout << "#0000ff\n";
}
else if (options.color == StyleOptions::Color::green) {
std::cout << "#00ff00\n";
}} catch (structopt::exception& e) {
std::cout << e.what() << "\n";
std::cout << e.help();
}
}
``````console
foo@bar:~$ ./main --color red
#ff0000foo@bar:~$ ./main -c blue
#0000fffoo@bar:~$ ./main --color green
#00ff00foo@bar:~$ ./main -c black
Error: unexpected input `black` provided for enum argument `color`. Allowed values are {red, green, blue}USAGE: ./my_app [OPTIONS]
OPTIONS:
-c, --color
```### Tuple Arguments
Now that we've looked at enum class support, let's build a simple calculator. In this sample, we will use an `std::tuple` to pack all the arguments to the calculator:
```cpp
#includestruct CalculatorOptions {
// types of operations supported
enum class operation { add, subtract, multiply, divide };// single tuple positional argument
std::tuple input;};
STRUCTOPT(CalculatorOptions, input);int main(int argc, char *argv[]) {
try {
auto options = structopt::app("my_app").parse(argc, argv);auto op = std::get<0>(options.input);
auto lhs = std::get<1>(options.input);
auto rhs = std::get<2>(options.input);
switch(op)
{
case CalculatorOptions::operation::add:
std::cout << lhs + rhs << "\n";
break;
case CalculatorOptions::operation::subtract:
std::cout << lhs - rhs << "\n";
break;
case CalculatorOptions::operation::multiply:
std::cout << lhs * rhs << "\n";
break;
case CalculatorOptions::operation::divide:
std::cout << lhs / rhs << "\n";
break;
}
}
catch (structopt::exception& e) {
std::cout << e.what();
std::cout << e.help();
}}
``````console
foo@bar:~$ ./main add 1 2
3foo@bar:~$ ./main subtract 5 9
-4foo@bar:~$ ./main multiply 16 5
80foo@bar:~$ ./main divide 1331 11
121foo@bar:~$ ./main add 5
Error: failed to correctly parse tuple `input`. Expected 3 arguments, 2 provided.USAGE: my_app input
ARGS:
input
```### Vector Arguments
`structopt` supports gathering "remaining" arguments at the end of the command, e.g., for use in a compiler:
```bash
$ compiler file1 file2 file3
```Do this by using an `std::vector` (or other STL containers with `.push_back()`, e.g, `std::deque` or `std::list`).
***NOTE*** Vector arguments have a cardinality of `0..*`, i.e., zero or more arguments. Unlike array types, you can provide zero arguments to a vector and `structopt` will (try to) not complain.
```cpp
#includestruct CompilerOptions {
// Language standard
// e.g., --std c++17
std::optional std;// remaining arguments
// e.g., ./compiler file1 file2 file3
std::vector files{};
};
STRUCTOPT(CompilerOptions, std, files);int main(int argc, char *argv[]) {
try {
auto options = structopt::app("my_app").parse(argc, argv);std::cout << "Standard : " << options.std.value_or("not provided") << "\n";
std::cout << "Files : { ";
std::copy(options.files.begin(), options.files.end(),
std::ostream_iterator(std::cout, " "));
std::cout << "}" << std::endl;
} catch (structopt::exception &e) {
std::cout << e.what() << "\n";
std::cout << e.help();
}
}
```***NOTE*** Notice below that the act of gathering remaining arguments is arrested as soon as an optional argument is detected. See the output of `./main file1.cpp file2.cpp --std c++17` below. Notice that `--std=c++17` is not part of the vector. This is because `--std` is a valid optional argument.
```console
foo@bar:~$ ./main
Standard : not provided
Files : { }foo@bar:~$ ./main file1.cpp file2.cpp
Standard : not provided
Files : { file1.cpp file2.cpp }foo@bar:~$ ./main file1.cpp file2.cpp --std=c++17
Standard : c++17
Files : { file1.cpp file2.cpp }foo@bar:~$ ./main --std:c++20 file1.cpp file2.cpp
Standard : c++20
Files : { file1.cpp file2.cpp }
```### Compound Arguments
Compound arguments are optional arguments that are combined and provided as a single argument. Example: `ps -aux`
```cpp
#includestruct Options {
// Flag arguments
std::optional a = false;
std::optional b = false;// Optional argument
// e.g., -c 1.1 2.2
std::optional> c = {};
};
STRUCTOPT(Options, a, b, c);int main(int argc, char *argv[]) {
try {
auto options = structopt::app("my_app").parse(argc, argv);// Print parsed arguments:
std::cout << std::boolalpha << "a = " << options.a.value()
<< ", b = " << options.b.value() << "\n";
if (options.c.has_value()) {
std::cout << "c = [" << options.c.value()[0] << ", " << options.c.value()[1]
<< "]\n";
}
} catch (structopt::exception &e) {
std::cout << e.what() << "\n";
std::cout << e.help();
}
}
``````console
foo@bar:~$ ./main -ac 3.14 2.718
a = true, b = false
c = [3.14, 2.718]foo@bar:~$ ./main -ba
a = true, b = truefoo@bar:~$ ./main -c 1.5 3.0 -ab
a = true, b = true
c = [1.5, 3]
```### Parsing Numbers
#### Integer Literals
`structopt` supports parsing integer literals including hexadecimal, octal, and binary notation.
```cpp
#includestruct IntegerLiterals {
std::vector numbers;
};
STRUCTOPT(IntegerLiterals, numbers);int main(int argc, char *argv[]) {
try {
auto options = structopt::app("my_app").parse(argc, argv);for (auto &n : options.numbers)
std::cout << n << "\n";
} catch (structopt::exception &e) {
std::cout << e.what() << "\n";
std::cout << e.help();
}
}
``````console
foo@bar:~$ ./main 1 0x5B 071 0b0101 -35 +98
1
91
57
5
-35
98
```#### Floating point Literals
As for floating point numbers, `structopt` supports parsing scientific notation (e/E-notation):
```cpp
#includestruct FloatLiterals {
std::vector numbers;
};
STRUCTOPT(FloatLiterals, numbers);int main(int argc, char *argv[]) {
try {
auto options = structopt::app("my_app").parse(argc, argv);for (auto &n : options.numbers)
std::cout << n << "\n";
} catch (structopt::exception &e) {
std::cout << e.what() << "\n";
std::cout << e.help();
}
}
``````console
foo@bar:~$ ./main -3.15 +2.717 2E-4 0.1e2 .5 -.3 +5.999
-3.15
2.717
0.0002
10
0.5
-0.3
5.999
```### Nested Structures
With `structopt`, you can define sub-commands, e.g., `git init args` or `git config [flags] args` using nested structures.
* Simply create a nested structure that inherits from `structopt::sub_command`
* You can use `.has_value()` to check if it has been invoked.The following program support two sub-commands: `config` and `init`:
```cpp
#includestruct Git {
// Subcommand: git config
struct Config : structopt::sub_command {
// flag argument `--global`
std::optional global = false;// key-value pair, e.g., `user.name "John Doe"`
std::array name_value_pair{};
};
Config config;// Subcommand: git init
struct Init : structopt::sub_command {// required argument
// repository name
std::string name;
};
Init init;
};
STRUCTOPT(Git::Config, global, name_value_pair);
STRUCTOPT(Git::Init, name);
STRUCTOPT(Git, config, init);int main(int argc, char *argv[]) {
try {
auto options = structopt::app("my_app").parse(argc, argv);if (options.config.has_value()) {
// config was invoked
std::cout << "You invoked `git config`:\n";
std::cout << "Global : " << std::boolalpha << options.config.global.value() << "\n";
std::cout << "Input : (" << options.config.name_value_pair[0] << ", " << options.config.name_value_pair[1] << ")\n";
}
else if (options.init.has_value()) {
// init was invoked
std::cout << "You invoked `git init`:\n";
std::cout << "Repository name : " << options.init.name << "\n";
}} catch (structopt::exception& e) {
std::cout << e.what() << "\n";
std::cout << e.help();
}
}
``````console
foo@bar:~$ ./main config user.email "[email protected]"
You invoked `git config`:
Global : false
Input : (user.email, [email protected])foo@bar:~$ ./main config user.name "John Doe" --global
You invoked `git config`:
Global : true
Input : (user.name, John Doe)foo@bar:~$ ./main init my_repo
You invoked `git init`:
Repository name : my_repofoo@bar:~$ ./main -h
USAGE: my_app [OPTIONS] [SUBCOMMANDS]
OPTIONS:
-h, --help
-v, --versionSUBCOMMANDS:
config
initfoo@bar:~$ ./main config -h
USAGE: config [FLAGS] [OPTIONS] name_value_pair
FLAGS:
-g, --globalOPTIONS:
-h, --help
-v, --versionARGS:
name_value_pairfoo@bar:~$ ./main init -h
USAGE: init [OPTIONS] name
OPTIONS:
-h, --help
-v, --versionARGS:
name
```***NOTE*** Notice in the above stdout that the `-h` help option supports printing help both at the top-level struct and at the sub-command level.
***NOTE*** `structopt` does not allow to invoke multiple sub-commands. If one has already been invoked, you will see the following error:
```console
foo@bar:~$ ./main config user.name "John Doe" init my_repo
Error: failed to invoke sub-command `init` because a different sub-command, `config`, has already been invoked.
```### Sub-Commands, Vector Arguments, and Delimited Positional Arguments
Here's a second example for nested structures with vector arguments and the double dash (`--`) delimiter
```cpp
#includestruct CommandOptions {
struct Sed : structopt::sub_command {
// --trace
std::optional trace = false;// remaining args
std::vector args;// pattern
std::string pattern;// file
std::string file;
};
Sed sed;
};
STRUCTOPT(CommandOptions::Sed, trace, args, pattern, file);
STRUCTOPT(CommandOptions, sed);int main(int argc, char *argv[]) {
auto app = structopt::app("my_app");
try {
auto options = app.parse(argc, argv);
if (options.sed.has_value()) {
// sed has been invokedif (options.sed.trace == true) {
std::cout << "Trace enabled!\n";
}std::cout << "Args : ";
for (auto& a : options.sed.args) std::cout << a << " ";
std::cout << "\n";
std::cout << "Pattern : " << options.sed.pattern << "\n";
std::cout << "File : " << options.sed.file << "\n";
}
else {
std::cout << app.help();
}} catch (structopt::exception &e) {
std::cout << e.what() << "\n";
std::cout << e.help();
}
}
``````console
foo@bar:~$ ./mainUSAGE: my_app [OPTIONS] [SUBCOMMANDS]
OPTIONS:
-h, --help
-v, --versionSUBCOMMANDS:
sedfoo@bar:~$ ./main sed --trace X=1 Y=2 Z=3 -- 's/foo/bar/g' foo.txt
Trace enabled!
Args : X=1 Y=2 Z=3
Pattern : s/foo/bar/g
File : foo.txt
```### Printing Help
`structopt` will insert two optional arguments for the user: `help` and `version`.
* Using `-h` or `--help` will print the help message and exit.
* Using `-v` or `--version` will print the program version and exit.```cpp
#includestruct Options {
// positional arguments
std::string input_file;
std::string output_file;// optional arguments
std::optional bind_address;// remaining arguments
std::vector files;
};
STRUCTOPT(Options, input_file, output_file, bind_address, files);int main(int argc, char *argv[]) {
auto options = structopt::app("my_app", "1.0.3").parse(argc, argv);
}
``````console
foo@bar:~$ ./main -hUSAGE: my_app [OPTIONS] input_file output_file files
OPTIONS:
-b, --bind-address
-h, --help
-v, --versionARGS:
input_file
output_file
filesfoo@bar:~$ ./main -v
1.0.3
```### Printing CUSTOM Help
`structopt` allows users to provide a custom help messages. Simply pass in your custom help as a string argument to `structopt::app`
```cpp
#includestruct Options {
// positional arguments
std::string input_file;
std::string output_file;// optional arguments
std::optional bind_address;// remaining arguments
std::vector files;
};
STRUCTOPT(Options, input_file, output_file, bind_address, files);int main(int argc, char *argv[]) {
try {
const std::string& custom_help = "Usage: ./my_app input_file output_file [--bind-address BIND_ADDRESS] [files...]\n";
auto options = structopt::app("my_app", "1.0.3", custom_help).parse(argc, argv);
} catch (structopt::exception &e) {
std::cout << e.what() << "\n";
std::cout << e.help();
}
}
``````console
foo@bar:~$ ./main -h
Usage: ./my_app input_file output_file [--bind-address BIND_ADDRESS] [files...]
```## Building Samples and Tests
```bash
git clone https://github.com/p-ranav/structopt
cd structopt
mkdir build && cd build
cmake -DSTRUCTOPT_SAMPLES=ON -DSTRUCTOPT_TESTS=ON ..
make
```### WinLibs + MinGW
For Windows, if you use [WinLibs](http://winlibs.com/) like I do, the cmake command would look like this:
```console
foo@bar:~$ mkdir build && cd build
foo@bar:~$ cmake -G "MinGW Makefiles" -DCMAKE_CXX_COMPILER="C:/WinLibs/mingw64/bin/g++.exe" -DSTRUCTOPT_SAMPLES=ON -DSTRUCTOPT_TESTS=ON ..
foo@bar:~$ makefoo@bar:~$ .\tests\structopt_tests.exe
[doctest] doctest version is "2.3.5"
[doctest] run with "--help" for options
===============================================================================
[doctest] test cases: 54 | 54 passed | 0 failed | 0 skipped
[doctest] assertions: 393 | 393 passed | 0 failed |
[doctest] Status: SUCCESS!
```## Compiler Compatibility
* Clang/LLVM >= 5
* MSVC++ >= 14.11 / Visual Studio >= 2017
* Xcode >= 10
* GCC >= 9## Generating Single Header
```bash
python3 utils/amalgamate/amalgamate.py -c single_include.json -s .
```## Contributing
Contributions are welcome, have a look at the [CONTRIBUTING.md](CONTRIBUTING.md) document for more information.## License
The project is available under the [MIT](https://opensource.org/licenses/MIT) license.