A C++ library that uses clever tricks to create super low-code, yet fully functional CLIs
Fire for C++ is a single header library that creates a command line interface from a function signature. Here's the whole program for adding two numbers with command line:
#include <iostream>
#include <fire-hpp/fire.hpp>
int fired_main(int x = fire::arg("-x"), int y = fire::arg("-y")) {
std::cout << x + y << std::endl;
return 0;
}
FIRE(fired_main)
That's all. Usage:
$ ./add -x=1 -y=2
3
As you likely expect,
--help
prints a meaningful message with required arguments and their types.std::string
with automatic error checking-abc <=> -a -b -c
and -x=1 <=> -x 1
With most libraries, creating a CLI roughly follows this pattern:
parse(argc, argv);
parse()
, print them and return (optional)-h
and --help
, print the help message and return (optional)That's a non-trivial amount of boilerplate, especially for simple scripts. Because of that, programmers (and a lot of library examples) tend to skip the optional parts, however this incurs a significant usability cost. Also, many libraries don't help at all with the conversion step.
With fire-hpp, you only call FIRE(fired_main)
and define arguments as function parameters. When fired_main()
scope begins, all steps have already been completed.
Steps to run examples:
git clone https://github.com/kongaskristjan/fire-hpp
cd fire-hpp && mkdir build && cd build
cmake .. && cmake --build .
(or substitute the latter command with appropriate build system invocation, eg. make -j8
or ninja
)cmake -D DISABLE_PEDANTIC= ..
(you are encouraged to open an issue)../examples/add --help
or ./examples/add -x=3 -y=5
Let's go through each part of the following example.
int fired_main(int x = fire::arg("-x"), int y = fire::arg("-y")) { // Define and convert arguments
std::cout << x + y << std::endl; // Use x and y, they're ints!
return 0;
}
FIRE(fired_main) // call fired_main()
FIRE(function name)
FIRE(fired_main)
expands into the actual main()
function that defines your program's entry point and fires off fired_main()
. fired_main
is called without arguments, thus compiler is forced to use the default fire::arg
values.
fire::arg(identifiers[, default value])
A constructor that accepts the name/shorthand/description/position of the argument. Use a brace enclosed list for several of them (eg. fire::arg({"-x", "--longer-name", "description of the argument"})
or fire::arg({0, "zeroth element"})
. The library expects a single dash for single-character shorthands, two dashes for multi-character names, and zero dashes for descriptions. fire::arg
objects should be used as default values for fired function parameters. See documentation for more info.
int fired_main(arguments)
This is what you perceive as the program entry point. All arguments must be bool
, integral, floating-point, fire::optional<T>
, std::string
or std::vector<T>
type and default initialized with fire::arg
objects (failing to initialize properly results in undefined behavior!). See conversions to learn how each of them affects the CLI.
FIRE(fired_main[, program_description])
creates the main function that parses arguments and calls fired_main
.FIRE_NO_EXCEPTIONS(...)
is similar, but can be used even if compiler has exceptions disabled. However, this imposes limitations on what the library can parse. Specifically, it disallows space assignment, eg. -x 1
must be written as -x=1
.FIRE_ALLOW_UNUSED(...)
is similar to FIRE(...)
, but allows unused arguments. This is useful when raw arguments are accessed (eg. for another library).Program description can be supplied as the second argument:
FIRE(fired_main, "Hello there")
Identifiers are used to find arguments from command line and provide a description. In general, it's a brace enclosed list of elements (braces can be omitted for a single element):
"-s"
shorthand name for argument"--multicharacter-name"
0
index of a positional argument"<name of the positional argument>"
"description of any argument"
fire::variadic()
Example: int fired_main(int x = fire::arg("-x"));
program -x=1
Example: int fired_main(int x = fire::arg({"-x", "--long-name"}));
program -x=1
program --long-name=1
Example: int fired_main(int x = fire::arg({0, "<name of argument>", "description"}));
program 1
<name of argument>
and description
appear in help messagesExample: int fired_main(vector<int> x = fire::arg(fire::variadic()));
program 1 2 3
Default value if no value is provided through command line. Can be either std::string
, integral or floating-point type and fire::arg
must be converted to that same type. This default is also displayed on the help page.
int fired_main(int x = fire::arg({"-x", "--long-name"}, 0));
program
-> x==0
program -x=1
-> x==1
For an optional argument without a default, see fire::optional.
Constraints can be applied to arguments by calling fire:arg
's constraint methods:
fire::arg().min(T minimum)
- specifies minimum valuefire::arg().max(T maximum)
- specifies maximum valuefire::arg().bounds(T minimum, T maximum)
- specifies both minimum and maximum valuesfire::arg().one_of({...})
- specifies possible valuesThese methods
Example: int fired_main(int x = fire::arg("-x").bounds(-1000, 1000));
program -x=100
-> x==100
program -x=-10000
-> Error: argument -x value -10000 must be at least -1000
program --help
-> -x=INTEGER description [-1000 <= x <= 1000]
(one of the lines)Example: int fired_main(std::string s = fire::arg("-s").one_of({"hi", "there"}));
program -s=hi
-> s==hi
program -s=hello
-> Error: argument -s value must be one of (hi, there), but given was 'hello'
To conveniently obtain arguments with the right type and automatically check the validity of input, fire::arg
class defines several implicit conversions.
Converts the argument value on command line to the respective type. Displays an error if the conversion is impossible or default value has wrong type.
Example: int fired_main(std::string name = fire::arg("--name"));
program --name=fire
-> name=="fire"
Example: int fired_main(double x = fire::arg("-x"));
program -x=2.5
-> x==2.5
program -x=blah
-> Error: value blah is not a real number
Used for optional arguments without a reasonable default value. This way the default value doesn't get printed in a help message. The underlying type can be std::string
, integral or floating-point.
fire::optional
is a tear-down version of std::optional
, with compatible implementations for has_value()
, value_or()
and value()
.
int fired_main(fire::optional<std::string> name = fire::arg("--name"));
program
-> name.has_value()==false
, name.value_or("default")=="default"
program --name="fire"
-> name.has_value()==true
and name.value_or("default")==name.value()=="fire"
For an optional argument with a sensible default value, see default value.
Boolean flags are true
when they exist on command line and false
when they don't. Multiple single-character flags can be packed on command line by prefixing with a single hyphen: -abc <=> -a -b -c
int fired_main(bool flag = fire::arg("--flag"));
program
-> flag==false
program --flag
-> flag==true
A method for getting all positional arguments as a vector. The fire::arg
object can be converted to std::vector<std::string>
, std::vector<integral type>
or std::vector<floating-point type>
. Using variadic argument forbids extracting positional arguments with fire::arg(index)
.
In this case, identifier should be fire::variadic()
. Description can be supplied in the usual way.
int fired_main(vector<std::string> params = fire::arg({fire::variadic(), "description"}));
program abc xyz
-> params=={"abc", "xyz"}
program
-> params=={}
fire::print_help()
- print the help messagefire::input_error(const string &msg)
- print error message and exit programfire::input_assert(bool pass, const std::string &msg)
- if pass
is not satisfied, print error message and exit programstd::string fire::helpful_name(const string &name)
- return user called name of the specified argument (given by one name) if it exists, otherwise return empty string.
int fired_main(optional<int> value = fire::arg({"-v", "--value"}));
program
-> fire::helpful_name("--value") == ""
program -v=2
-> fire::helpful_name("--value") == "-v"
program --value=2
-> fire::helpful_name("--value") == "--value"
fire::input_assert(value > 0, "Argument " + fire::helpful_name("--value") + " must be greater than 0")
std::string fire::helpful_name(int pos)
- return correctly formatted positional argument number
Some third party libraries require access to raw argc/argv. This is gained through fire::raw_args
(of type fire::c_args
), which has argc()
and argv()
methods for accessing the arguments.
Examples:
int argc = fire::raw_args.argc();
char ** argv = fire::raw_args.argv();
non_modifying_call(argc, argv);
fire::c_args raw_args_copy = fire::raw_args;
int& argc = raw_args_copy.argc();
char ** argv = raw_args_copy.argv();
modifying_call(argc, argv);
// Once out of scope, raw_args_copy releases argv strings
You also need FIRE_ALLOW_UNUSED(...)
if the third party library processes it's own arguments.
Other libraries you might find useful:
(int x, int y)
instead of (int x = fire::arg("-x"), int y = fire::arg("-y"))
). Also adds subcommands support. The downside is that it's not pure C++, and requires you to compile an llvm plugin to actually compile your code.main()
).