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.- an error message is displayed for incorrect usage.
- the program runs on Linux, Windows and Mac OS.
- flags; named and positional parameters; variadic parameters
- optional parameters/default values
- conversions to integer, floating-point and
std::string
with automatic error checking - program/parameter descriptions
- standard constructs, such as
-abc <=> -a -b -c
and-x=1 <=> -x 1
With most libraries, creating a CLI roughly follows this pattern:
- define arguments
- call
parse(argc, argv);
- check whether errors are detected by
parse()
, print them and return (optional) - check for
-h
and--help
, print the help message and return (optional) - for each argument:
- get argument from the map and if necessary convert to the right type
- try/catch for errors in conversion 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.
- Using fire.hpp: compiler compatible with C version 11, 14, 17, 20, or 23.
- Compiling examples: CMake 3.5 .
- Compiling/running tests: CMake 3.14 and Python 3.5-3.12. GTest is downloaded, compiled and linked automatically.
Steps to run examples:
- Clone repo:
git clone https://github.com/kongaskristjan/fire-hpp
- Create build and change directory:
cd fire-hpp && mkdir build && cd build
- Configure/build:
cmake .. && cmake --build .
(or substitute the latter command with appropriate build system invocation, eg.make -j8
orninja
) - If errors are encountered, clear the build directory and disable pedantic warnings as errors with
cmake -D DISABLE_PEDANTIC= ..
(you are encouraged to open an issue). - Run:
./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 actualmain()
function that defines your program's entry point and fires offfired_main()
.fired_main
is called without arguments, thus compiler is forced to use the defaultfire::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"})
orfire::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
orstd::vector<T>
type and default initialized withfire::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 callsfired_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 toFIRE(...)
, 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>"
- any other string:
"description of any argument"
- variadic arguments:
fire::variadic()
-
Example:
int fired_main(int x = fire::arg("-x"));
- CLI usage:
program -x=1
- CLI usage:
-
Example:
int fired_main(int x = fire::arg({"-x", "--long-name"}));
- CLI usage:
program -x=1
- CLI usage:
program --long-name=1
- CLI usage:
-
Example:
int fired_main(int x = fire::arg({0, "<name of argument>", "description"}));
- CLI usage:
program 1
<name of argument>
anddescription
appear in help messages
- CLI usage:
-
Example:
int fired_main(vector<int> x = fire::arg(fire::variadic()));
- CLI usage:
program 1 2 3
- CLI usage:
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.
- Example:
int fired_main(int x = fire::arg({"-x", "--long-name"}, 0));
- CLI usage:
program
->x==0
- CLI usage:
program -x=1
->x==1
- CLI usage:
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 values
These methods
- check whether user supplied value fits the constraint and emit a proper error message if condition is not met
- append the constraint to argument's help message
-
Example:
int fired_main(int x = fire::arg("-x").bounds(-1000, 1000));
- CLI usage:
program -x=100
->x==100
- CLI usage:
program -x=-10000
->Error: argument -x value -10000 must be at least -1000
- CLI usage:
program --help
->-x=INTEGER description [-1000 <= x <= 1000]
(one of the lines)
- CLI usage:
-
Example:
int fired_main(std::string s = fire::arg("-s").one_of({"hi", "there"}));
- CLI usage:
program -s=hi
->s==hi
- CLI usage:
program -s=hello
->Error: argument -s value must be one of (hi, there), but given was 'hello'
- CLI usage:
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"));
- CLI usage:
program --name=fire
->name=="fire"
- CLI usage:
-
Example:
int fired_main(double x = fire::arg("-x"));
- CLI usage:
program -x=2.5
->x==2.5
- CLI usage:
program -x=blah
->Error: value blah is not a real number
- CLI usage:
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()
.
- Example:
int fired_main(fire::optional<std::string> name = fire::arg("--name"));
- CLI usage:
program
->name.has_value()==false
,name.value_or("default")=="default"
- CLI usage:
program --name="fire"
->name.has_value()==true
andname.value_or("default")==name.value()=="fire"
- CLI usage:
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
- Example:
int fired_main(bool flag = fire::arg("--flag"));
- CLI usage:
program
->flag==false
- CLI usage:
program --flag
->flag==true
- CLI usage:
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.
- Example:
int fired_main(vector<std::string> params = fire::arg({fire::variadic(), "description"}));
- CLI usage:
program abc xyz
->params=={"abc", "xyz"}
- CLI usage:
program
->params=={}
- CLI usage:
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)
- ifpass
is not satisfied, print error message and exit program
std::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.
- Example:
int fired_main(optional<int> value = fire::arg({"-v", "--value"}));
- CLI usage:
program
->fire::helpful_name("--value") == ""
- CLI usage:
program -v=2
->fire::helpful_name("--value") == "-v"
- CLI usage:
program --value=2
->fire::helpful_name("--value") == "--value"
- CLI usage:
- Typical usage:
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:
- Usage without modification:
int argc = fire::raw_args.argc();
char ** argv = fire::raw_args.argv();
non_modifying_call(argc, argv);
- Usage with modification:
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:
- python-fire: something similar in Python, I got my inspiration from there
- fire-llvm: an even neater interface (you can write parameters as
(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. - CLI11: A somewhat more conventional C CLI library. IMHO A very good interface while avoiding unconventional trickery (like hijacking
main()
).