Applications built with Scarab¶
An application essentially encapsulates some behavior or capability with a user interface. Scarab provides tools to create an interface that allows the user to easily and repeatably configure and run an application.
The main set of classes that a developer will encounter are:
main_app: the top-level application class
app: application base class; used for subcommandsThe set of
paramclasses: data structure classes that hold the configuration information
Configuring an Application¶
The user tells the application how to run using its configuration. The primary configuration can be put together using several stages of input:
Default configuration: the developer of the application can provide a default hard-coded configuration via
main_app::default_config()Configuration file: supplied via the command line (
-cor--config)Non-option command-line arguments (see below)
Option arguments setup to modify the configuration
Starting from the top down, successive stages of configuration will add to or overwrite (for duplicated configuration keys) the primary configuration. The configuration can be accessed with main_app::primary_config().
Using the Command Line¶
Components of a command:
> my_exe [subcommand] [options] [non-option arguments]
- Subcommands
An executable using subcommands typically has multiple main modes of operation, each of which is identified by a subcommand.
- Options
An option modifies the behavior of the executable in some predefined way (unknown options are not allowed). Options are indicated by a hyphen and a single character (e.g.
-c) or two hyphens and a string name (e.g.--config). Options can either be flags, indicating some meaning just by their presence, or they can have some value attached to them. Options can be given as:-a(flag)-abc(flags can be combined)-f filename(option)-ffilename(no space required)-abcf filename(flags and option can be combined)--long(long flag)--file filename(space)--file=filename(equals)
Several options are available with any scarab-based application:
-hor--helpwill print the usage message and exit-Vor--versionwill print the version information and exit-cor--configallows the user to provide a configuration file--verbosityallows the user to set the global maximum verbosity on a scale of 0 (trace) to 6 (fatal)
- Non-Option Arguments
Arguments that do not start with one or two hyphens are called non-option arguments. They are parsed in two ways, and made available through the
main-appclass:Keyword arguments have a key and a value separated by a
'='. They’re merged into the global configuration, and they are also made available in a map-like structure,main-app.nonoption_kw_args(). The syntax options are:key=(NULL value)key={true|false}(boolean value)key=string(string value)key=[numeric](numeric value, either integer, unsigned integer, or float; scientific notation, e.g.1e7, is allowed for floats)
The key can be a simple string, which indicates a key/value pair in a flat map, or it can indicate a value within a nested hierarchical structure of maps and arrays. The only requirement on the structure is that the top level is a map with a string key. For example, the keyword argument
key_one.2.key_two=truewould indicate the following structure (using YAML notation for convenience):--- key_one: - NULL - NULL - key_two: true
Ordered arguments have a value (NULL, boolean, string, or numeric) but no key. They are not merged with the global configuration, and are made available in an array-like structure:
main_app::nonoption_ord_args().
Authentication¶
The main_app class incorporates an authentication object to collect and give access to authentication
information. The authentication specification is configurable under the configuration key auth-spec, e.g.:
auth-spec:
a_group:
username:
default: some_user
env: GROUP_USER
password:
default: ""
env: GROUP_PWORD
Alternatively, or in addition to the above example, the authentication specification can be loaded from
an authentication file. The filename is configured with the key auth-key.
See Authentication for more information about authentication specifications.
The authentication specification will be processed after all configuration steps are complete.
Note that authentication _data_ is intentionally not included in the main_app configuration.
We recommend that authentication values, especially passwords, be supplied as an environment variable or authentication file.
Creating an Application¶
Several examples are included below to demonstrate how one can create an application using Scarab.
CLI Library¶
Scarab uses the CLI11 library to do command-line argument parsing. The CLI::App class has been typedef’d as scarab::app for convenience, and there is a derived class scarab::main_app that takes care of parsing the non-option arguments.
Here are some useful resource for learning how to use the CLI11 library:
A Note about Logging¶
Due to issues with static-object deletion in the logger setup, you must manually stop logging at the end of your executable.
The recommended way to do this is to use the STOP_LOGGING macro at the end of your main() function and at any exit point.
The simplest example¶
#include "application.hh"
using namespace scarab;
int main( int argc, char **argv )
{
main_app the_main( false );
CLI11_PARSE( the_main, argc, argv );
return 0;
}
In this example we create the app (main_app), parse the CL input, and then return. Note that the main_app constructor
includes a single argument, passing false to disable the use of a config file.
Example with a callback¶
This example captures the behavior of the application in a class, and then runs the execute() function with a callback.
#include "application.hh"
#include "logger.hh"
LOGGER( testlog, "test_app_with_callback" );
namespace scarab
{
struct do_a_thing
{
do_a_thing() : f_value( 5 ) {}
void execute( const main_app& an_app )
{
// configure to run
f_value = an_app.primary_config().get_value( "value", f_value );
// do a thing!
LPROG( testlog, "My value is: " << f_value );
return;
}
int f_value;
};
}
using namespace scarab;
int main( int argc, char **argv )
{
main_app the_main;
auto t_dat_callback = [&](){
do_a_thing t_dat;
t_dat.execute( the_main );
};
the_main.callback( t_dat_callback );
CLI11_PARSE( the_main, argc, argv );
return 0;
}
Note that in the main_app constructor we did not pass an argument as we did in the previous example.
In this case we use the default value, which enables the use of a config file.
Example with subcommands¶
This example uses a class with two functions that are implemented as subcommands called by callback.
This can be run from the command line as test_app_with_subcommands get and test_app_with_subcommands set.
Note that app.fallthrough() is used in the main function to allow non-option arguments to be collected by the main app.
#include "application.hh"
#include "logger.hh"
LOGGER( testlog, "test_app_with_subcommands" );
namespace scarab
{
struct get_or_set
{
get_or_set() : f_value( 5 ) {}
void setup_subcommands( main_app& an_app )
{
app* t_sc_get = an_app.add_subcommand( "get", "Get the value" );
t_sc_get->callback([this]() { this->get(); } );
app* t_sc_set = an_app.add_subcommand( "set", "Set the value" );
t_sc_set->callback([&an_app, this]() { this->set( an_app ); } );
return;
}
void get()
{
LPROG( testlog, "Value is: " << f_value );
return;
}
void set( const main_app& an_app )
{
f_value = an_app.primary_config().get_value( "value", f_value );
LPROG( testlog, "Just to check: " << f_value );
return;
}
int f_value;
};
}
using namespace scarab;
int main( int argc, char **argv )
{
main_app the_main;
the_main.require_subcommand();
the_main.fallthrough();
get_or_set t_gos;
t_gos.setup_subcommands( the_main );
CLI11_PARSE( the_main, argc, argv );
return 0;
}
Example with authentication¶
This application example defines some default authentication specification that it uses for some purpose called backend.
The user could modify the information therein at runtime either via environment variables or an authentication file.
It also uses a callback function for execution of the application.
- ::
#include “application.hh”
#include “logger.hh” #include “param_helpers_impl.hh”
using namespace scarab;
LOGGER( testlog, “test_app_with_authentication” );
class test_app : public main_app {
- public:
- test_app(bool a_use_config = true) :
main_app(a_use_config)
{
- f_default_config.add( “auth-spec”, scarab::param_node(
- “backend”_a=scarab::param_node(
- “user”_a=scarab::param_node(
“default”_a=”a_backend_user”, “env”_a=”SCARAB_AUTH_TEST_BACKEND_USER”
), “password”_a=scarab::param_node(
“default”_a=”security_hole”, “env”_a=”SCARAB_AUTH_TEST_BACKEND_PASSWORD”
)
)
) );
} virtual ~test_app() {}
void execute() {
// Print the authentication information, both specification and data LPROG( testlog, “Authentication specification: “ << f_auth.spec() ); LPROG( testlog, “Authentication data: “ << f_auth.data() ); return;
}
};
int main( int argc, char **argv ) {
test_app the_main( true ); auto t_executor = [&](){
the_main.execute();
}; the_main.callback( t_executor );
CLI11_PARSE( the_main, argc, argv );
return 0;
}