Developer Guide

Build Process

This section discusses how to run quire-gen and how to use quire as the part of your applications, using different build systems.

Note

The ABI for the library is not stable so the recommended way of using quire is by using it as git submodule of your application.

Raw Process

Whole configuration parser generation is based on single YAML file. By convention it’s called “config.yaml” and is put near the sources of the project (e.g. “src/” folder).

If you have installed quire to system, to make parser generator run:

quire-gen –source config.yaml –c-header config.h –c-header config.c

Then you may use the files as normal C sources. But be careful to update them when yaml file changes. If you add them as a part of build process you may need the mark as “generated” or equal, so that build system would not error if they are absent. See below for instructions for specific build systems. You do not need to bundle original yaml file with distribution of your application.

This is it. See tutorial for examples of the yaml itself and how to use it in your own code.

Using Make

To use make for configuration file generation you might write something along the lines of:

config.h config.c: config.yaml
    quire-gen --source $^ --c-header config.h --c-source config.c

Using CMake and Git Submodule

If you are using cmake for building your project, you are lucky, because the developers of quire use cmake too. So the whole process is easy.

Let’s add submodule first:

git submodule add git@github.com:tailhook/quire quire

Now we should add the following to the CMakeLists.txt:

# Assuming you have "exe_name" executable
# Add "config.c" that's will be generated to list of sources
ADD_EXECUTABLE(exe_name
    main.c
    other.c
    config.c)
# Builds quire itself
ADD_SUBDIRECTORY(quire)
# Get's the full path of quire-gen executable just built
GET_TARGET_PROPERTY(QUIRE_GEN quire-gen LOCATION)
# Adds target to build C files and headers
# You may need to adjust source and/or directory
ADD_CUSTOM_COMMAND(
    OUTPUT config.h config.c
    COMMAND ${QUIRE_GEN}
        --source ${CMAKE_CURRENT_SOURCE_DIR}/config.yaml
        --c-header config.h
        --c-source config.c
    DEPENDS config.yaml quire-gen
    )
# Marks files as generated so make/cmake doesn't complain they are absent
SET_SOURCE_FILES_PROPERTIES(
    config.h config.c
    PROPERTIES GENERATED 1
    )
# Add include search path for the files that include "config.h"
SET_SOURCE_FILES_PROPERTIES(
    main.c other.c  # These files need to be adjusted
    COMPILE_FLAGS "-I${CMAKE_CURRENT_BINARY_DIR}")
# Add include search path for quire.h (overriding system one if exists)
INCLUDE_DIRECTORIES(BEFORE SYSTEM quire/include)
# Add linkage, adjust "exe_name" to name of your executable
TARGET_LINK_LIBRARIES(exe_name quire)

Now just run cmake && make like you always do with cmake.

You also need to include folder quire to your source distributions, even if they have C files generated. You also need to add instructions to run git submodule update --init for building from git.

Variable Types

All variable declarations start with yaml tag (an string starting with exclamation mark). Almost any type can be declared in it’s short form, as a (tagged) scalar:

val1: !Int 0
val2: !String hello
val3: !Bool yes
val4: !Float 1.5
val5: !Type some_type

And any type can be written in equivalent long form as a mapping:

val1: !Int
  default: 0
val2: !String
  default: hello
val3: !Bool
  default: yes
val4: !Float
  default: 1.5
val5: !Type
  type: some_type

Using the latter form adds more features to the type definition. Next section describes properties that can be used in any type, and following sections describe each type in detail.

Common Properties

The following properties can be used for any type, given the it’s written in it’s long form (in form of mapping). Here is a list (string is for the sake of example, any type could be used):

val: !String
  description: This value is something that is set in config
  default: nothing-relevant
  example: something-cool
  only-command-line: no
  command-line:
    names: [-v, --val-set]
    group: Options
    metavar: STR
    descr: This option sets val

Let’s take a closer look.

val: !String
  description: This value is something that is set in config

The description is displayed in the output of --config-print and -PP command-line options. It’s reformatted to the 80 characters in width, on output. If set it’s also used in command-line option description (--help) if not overriden in command-line section.

val: !String
  default: nothing-relevant

Set’s default value for the property. It should be the same type as the target value.

val: !String
  example: something-cool

Set’s the example value for the configuration variable. It’s only output in --config-print=example and may be any piece of yaml. However it’s recommended to obey same structure as a target value, as it may be enforced in the future. See description of --config-print for more information.

val: !String
  only-command-line: yes

This flag marks an option to be accepted from the command-line only. It is neither parsed in yaml file, nor printed using --config-print, but otherwise it is placed in the same place in configuration structure and respect same rules. If there is no command-line (see below) for this option, then a member of the structure is generated and default is set anyway.

The command-line may be specified in several ways. The simplest is:

val: !String
  command-line: -v

This adds single command-line option. Several options can be used too, mostly useful for having short and long options, but may be used for aliases too:

val: !String
  command-line: [-v, --val]

And full command-line specification is a mapping. Each property in a mapping is described in detail below.

val1: !String
  command-line:
    name: -v
    names: [-v, --val]

Either name or names may be specified, for the single option and multiple options respectively.

val1: !String
  command-line:
    group: Options

The group of the options in the --help. Doesn’t have any semantic meaning just keeps list of options nice. By default all options are listed under group Options.

val1: !String
  command-line:
    metavar: STR

The metavar that’s used in command-line description, e.g. --val STR. By default reasonably good type-specific name is used.

val1: !String
  command-line:
    descr: This option sets val

The description used in --help. If not set, the description in the option definition is used, if the latter is absent, some text similar to Set "val" is used instead.

There are also type-specific command-line actions:

intval: !Int
  command-line-incr: --incr
  command-line-decr: --decr
boolval: !Bool
  command-line-enable: --enable
  command-line-disable: --disable

They all obey pattern command-line-ACTION. Every such option may be specified by any ways that command-line can. However, they have the following difference:

  • they inherit group from the command-line if specified
  • they often have metavar useless
  • they don’t inherit description as it’s usually misleading

String Type

String is most primitive data type. It accepts any YAML scalar and stores it’s value as const char * along with it’s length.

The simplest config:

val: !String

If you supply scalar, is stands for the default value:

val: !String default_value

Maximum specification for string is something like the following:

val: !String
  description: This value is something that is set in config
  default: default_value
  example: some example
  command-line:
    names: [-v, --val-set]
    group: Options
    metavar: STR
    descr: This option sets val

The fields in C structure look like the following:

const char *val;
int val_len;

Note that the string is both nul-terminated and has length in the structure.

Warning

Technically it’s possible that the string contain embedded nulls. In most cases this fact may be ignored. But do not rely on val_len be the length of the string after strdup or similar operation.

Integer Type

Unlike in C there is only one integer type in quire. And it’s represented by long value in C.

The simplest config:

val: !Int

If you supply scalar, is stands for the default value:

val: !Int 10

The comprehensive specification for integer is something like the following:

val: !Int
  default: 1
  min: 0
  max: 10
  description: This value is something that is set in config
  example: 100
  command-line:
    names: [-v, --val-set]
    group: Options
    metavar: NUM
    descr: This option sets val
  command-line-incr:
    name: --incr
    group: Options
    descr: This option increments val
  command-line-decr:
    name: --decr
    group: Options
    descr: This option decrements val

The field in C structure look like the following:

long val;

The additinal keys represent minimum and maximum value for the integer:

val: !Int
  min: 0
  max: 10

Both values are inclusive. If user specifies bigger or smaller value either in configuration file or on command-line, error is printed and configuration rejected. If value overflows by using increments by command-line arguments (see below), the value is simply adjusted to the maximum or minimum value as appropriate.

The additional command-line actions:

command-line-incr: --incr
command-line-decr: --decr

May be used to increment the value in the configuration. They are applied after parsing the configuration file, and set-style options (regardless of the order of the command-line options). Mostly useful for log-level or similar things. The value printed using --config-print option includes all incr/decr arguments applied.

All integer values support parsing different bases (e.g. 0xA1 for hexadecimal 161) and units (e.g. 1M for one million)

Boolean Type

The simplest boolean:

val: !Bool

If you supply scalar, is stands for the default value:

val: !Bool yes

The comprehensive specification for boolean is something like the following:

val: !Bool
  default: no
  description: This value is something that is set in config
  example: true
  command-line:
    names: [-v, --val-set]
    group: Options
    metavar: BOOL
    descr: This option sets val
  command-line-enable:
    name: --yes
    group: Options
    descr: This option sets val to true
  command-line-disable:
    name: --no
    group: Options
    descr: This option sets val to false

The field in C structure look like the following:

int val;

The value of val is always either 0 or 1 which stands for boolean false and true respectively.

The additional command-line actions:

command-line-enable: --yes
command-line-disable: --no

May be used to enable/disable the value in the configuration. They are applied after parsing the configuration file, and after set-style options. If multiple enable/disable options used, the last one wins. The value printed using --config-print option includes all enable/disable arguments applied.

The following values may be used as booleans, both on the command-line and in configuration file. The values are case insensitive:

False True
false true
no yes
n y
~  
empty string  

Floating Point Type

The simplest config:

val: !Float

If you supply scalar, is stands for the default value:

val: !Float 1.5

The comprehensive specification for floating point is something like the following:

val: !Float
  default: 1.5
  description: This value is something that is set in config
  example: 2.5
  command-line:
    names: [-v, --val-set]
    group: Options
    metavar: FLOAT
    descr: This option sets val

The field in C structure look like the following:

double val;

All floating point values support parsing decimal numbers, optionally followed by e and a decimal exponent. Floating point values also support units (e.g. 1M for one million). Note that fractional units are not supported yet.

Array Type

The array type has no short form, and is always written as a mapping. The only key required in the mapping is an element which denotes the type of item in each array element.

arr: !Array
  element: !Int

Any quire type may be the element of the array. Including array itself. More comprehensive example below:

arr: !Array
  description: Array of strings
  element: !String hello
  example: [hello, world]

Note

Command-line argument parsing is not supported neither for the array itself nor for any child of it. This may be improved in future. But look at variables, if you need some command-line customization.

The C structure for the array is a linked list:

struct cfg_a_str {
    struct cfg_a_str *next;
    const char *val;
    int val_len;
};

struct cfg_main {
    qu_config_head head;
    struct cfg_a_str *arr;
    struct cfg_a_str **arr_tail;
    int arr_len;
};

The example of array usage is given in tutorial.

Mapping Type

The mapping type has no short form, and is always written as a mapping. The two properties required in the mapping are key-element and value-element which denote the type of key and value for the mapping.

arr: !Mapping
  key-element: !Int
  value-element: !String

Any quire type may be the value element of the array. Including array itself. A key may be any scalar type. More comprehensive example below:

map: !Mappings
  description: A mapping of string to structure
  key-element: !String
  value-element: !String
  example:
     apple: fruit
     carrot vegetable

Note

Command-line argument parsing is not supported neither for the mapping itself nor for any child of it. This may be improved in future. But look at variables, if you need some command-line customization.

The C structure for the mapping is a linked list:

struct cfg_m_str_str {
    struct cfg_m_str_str *next;
    const char *key;
    int key_len;
    const char *val;
    int val_len;
};

struct cfg_main {
    qu_config_head head;
    struct cfg_m_str_str *map;
    struct cfg_m_str_str **map_tail;
    int map_len;
};

The example of mapping usage is given in tutorial.

Custom Type

Sometimes you want to reuse a part of the config in multiple places. You can do this with yaml aliases. But it’s better to be done by declaring a custom type. Here we will describe only how to refer to a custom type. See custom types for a way to declare a type.

The simplest type reference is:

val: !Type type_name

As with most types, declaration may be expanded to a mapping:

val: !Type
  description: My Value
  type: type_name
  example: some data

Note

Neither command-line, nor default are supported for type reference for now. But this is expected to be improved in future

Special Keys

Types

The __types__ defines the custom types that can be used in multiple places inside the configuration. It can also be used to define recursive types. Any type defined inside __types__ can be referred by !Type name_of_the_type. See custom types for more info.

Conditionals

There is a common use case where you have several utilities sharing mostly same config with some deviations. The most typical use case is a daemon process and a command-line interface to it, with a different set of command-line argumemnts. Here is how it looks like:

__if__:defined CLIENT:
 query: !String
   only-command-line: yes
   command-line: --query

When compiling utility you should define the CLIENT macro:

gcc ... -DCLIENT

And you will get additional command-line arguments for this binary. In code it looks like:

struct cfg_main_t {
    int val1;
#if defined CLIENT
    const char *query;
    int query_len;
#endif  /* defined CLIENT */
}

The rule is: if expression is evaluated to true, you get the configuration with all the contents of conditional merged inside the mapping (i.e. conditional replaced by <<:). In case expression is evaluated to false, you should get the all the configuration structures and semantics as the key and all its contents doesn’t exist at all.

You can use any expression that C preprocessor is able to evaluate instead of defined CLIENT

Warning

You must define the macro consistently across all C files that use configuration header (config.h). In particular you can’t share config.o generated for the two executables having different definitions. CMake handles this case automatically but some other build systems don’t.

Include

There is __include__ special key, which allows to add #include directive to the generated configuration file header. This key can be present at any place and will add the preprocessor directive at the top of the file.

For example:

__include__: "types.h"

Will result into the following line in the config.h file:

#include "types.h"

Note

There is no way to include a system header (#include <filename>), you can include some intermediate file, which includes the system header, if you really need the functionality. But most of the time double-quoted name will be searched for in system folders if not found in the project itself.

Set Flags

The flag __set_flags__ can be used to generate xx_set field for each of the structure field. This flag may be used to find out whether field is set by user or the default value is provided. For example:

data:
  ? __set_flags__
  a: !Int 1
  b: !Int 2

Note

The syntax ? __set_flags__ is YAML shortcut to __set_flags__: null. We use and recommend this syntax for structure flags as it’s not only shorter, but also stand out from structure field definitions.

Will turn into the following structure:

struct cfg_main {
  qu_config_head head;
  struct {
     unsigned int a_set:1;
     unsigned int b_set:1;
     long a;
     long b;
  } data;
};

Note

The syntax int yy:1; is a syntax for bit field. I.e. the field that is only one bit in width. Given it is unsigned it can have one of the two values 0 and 1.

The values of a and b fields will always be intitialized (to 1 and 2 respectively), but the a_set and b_set will be non-zero only when user specified them in configuration file.

The __set_flags__ property can be specified in any structure, including the root structure and !Struct custom type or its descendent. The flag is propagated to the nested structures but not to the !Type fields.

Structure Name

Usually nested mappings that do not denoted by !Type are represented by anonymous structures. But you can set __name__ for the structure to have a name.

data:
  __name__: data
  a: !Int 1
  b: !Int 2

Will name the internal structure:

struct cfg_main {
  qu_config_head head;
  struct cfg_data {
     unsigned int a_set:1;
     unsigned int b_set:1;
     long a;
     long b;
  } data;
};

This is occasionally useful to use the structures in code.

Note

Author of config is responsible to set unique name of the structure otherwise the C compiler will throw an error.

Custom Types

Structure Type

Choice Type

Enumeration Type

Tagged Scalar Type

Field Type

Field type allows to wrap any other type into yet another C structure. It is sometimes useful, especially with non-scalar types. For example:

__types__:
  string_list: !Field
    field: !Array
      element: !String

Results into the following C definitions:

struct cfg_string_list {
    struct cfg_a_str *val;
    struct cfg_a_str **val_tail;
    int val_len;
};
struct cfg_main {
    qu_config_head head;
    struct cfg_string_list arr1;
};

C Fields

Warning

The functionality described in this section is currently discouraged and is subject to removing/adjusting at any time.

Ocasionally there is a need to put custom C field into generated structure. You can do that with the following syntax:

_field-name: !CDecl struct some_c_struct

Where _field-name may be arbitrary but must start with underscore. And at the right of the !CDecl may be any C type that compiler is able to understand. It’s written as is, so may potentially produce broken header if some garbage is written instead of the type name.

If you need to add some header for type to be known to the compiler use __include__ special key:

__include__: "types.h"
_field-name: !CDecl struct some_c_struct

Note all files are added with #include "filename" syntax, not the #include <filename>.