Quick Start

Installation

The best way to install ActiveConfigProgramOptions is to install it via pip:

Installation of ActiveConfigProgramOptions from a linux prompt
$ python3 -m pip install ActiveConfigProgramOptions

It can also be cloned from Gitlab and installed locally:

Cloning ActiveConfigProgramOptions from Gitlab
$ git clone https://gitlab.com/semantik-software/code/python/ActiveConfigProgramOptions.git
$ cd ActiveConfigProgramOptions
$ python3 -m pip install .

Running the Examples

Once installed, you can go to the Examples directory and run the examples:

$ cd examples
$ python3 ActiveConfigProgramOptions-example-01.py
...

Using ActiveConfigProgramOptions in Code

The first step is to include the ActiveConfigProgramOptions class:

1#!/usr/bin/env python3
2# -*- mode: python; py-indent-offset: 4; py-continuation-offset: 4 -*-
3from pathlib import Path
4import activeconfigprogramoptions
5

Examples

Example 1 - Command Line

In ActiveConfigProgramOptions-example-01 we have a .ini file that demostrates utilization of the use operation that is inherited from ActiveConfigParser ActiveConfigParser. In this example we are going to demonstrate how the opt-set operations in our .ini file can be used to generate a custom bash command with customizable argument sets added.

In this case, we will process the [MY_LS_COMMAND] section to generate a bash command that would generate a directory listing in list form by reverse time order of last modified with a custom timestamp format.

While this example is quite simple we can see how a complex environment in a DevOps setting might use this to bundle “common” operations to reduce the amount of copying and pasting that is used.

First, we need a .ini file:

ActiveConfigProgramOptions-example-01.ini
 1#
 2# ActiveConfigProgramOptions-example-01.ini
 3#
 4[LS_COMMAND]
 5opt-set ls
 6
 7[LS_LIST_TIME_REVERSED]
 8opt-set "-l -t -r"
 9
10[LS_CUSTOM_TIME_STYLE]
11opt-set --time-style : "+%%Y-%%m-%%d %%H:%%M:%%S"
12
13[MY_LS_COMMAND]
14use LS_COMMAND
15use LS_LIST_TIME_REVERSED
16use LS_CUSTOM_TIME_STYLE

This ini file demonstrates how we can use sections to set up a command that could be used to generate different options.

First, the LS_COMMAND section defines the executable application option that is to be called. In this case it’s just ls to get a directory listing.

The next two sections, LS_LIST_TIME_REVERSED and LS_CUSTOM_TIME_STYLE provides the command line options create a directory listing to sort the most recently modified files to the end and to make a custom date/time format, respectively.

Finally the last section MY_LS_COMMAND would be the section that we use to generate a full directory listing command that will combine the executable with the sorting and timestamp format options applied.

The following code shows how our application can use ActiveConfigProgramOptions to generate the full command line from this toy problem:

ActiveConfigProgramOptions-example-01.py
 1#!/usr/bin/env python3
 2# -*- mode: python; py-indent-offset: 4; py-continuation-offset: 4 -*-
 3from pathlib import Path
 4import activeconfigprogramoptions
 5
 6# print a banner
 7print(80 * "-")
 8print(f"- {Path(__file__).name}")
 9print(80 * "-")
10
11filename = "ActiveConfigProgramOptions-example-01.ini"
12
13# create a ActiveConfigProgramOptions object using the .ini file
14popts = activeconfigprogramoptions.ActiveConfigProgramOptions(filename)
15
16# Parse a section that generates a LS command
17section = "MY_LS_COMMAND"
18popts.parse_section(section)
19
20# Extract a list containing the options
21bash_options = popts.gen_option_list(section, generator="bash")
22
23# Print the option list
24print(" ".join(bash_options))

We see in this code the call to gen_option_list() parses the MY_LS_COMMAND section and generates a list containing the arguments for our command. We can join these together to create a single string containing our command or send the list directly to subprocess.run to execute the command.

In our example, we’re just printing out the results of our generator:

_ActiveConfigProgramOptions-example-01.log
1--------------------------------------------------------------------------------
2- ActiveConfigProgramOptions-example-01.py
3--------------------------------------------------------------------------------
4ls -l -t -r --time-style="+%Y-%m-%d %H:%M:%S"

Example 2 - CMake Use Case

In this example we show use of ActiveConfigProgramOptionsCMake which lets us generate a .ini file that provides some flexibility in what CMake configuration commands we could generate based on the contents of our .ini file with re-use of common sections and arguments.

The configuration file is:

ActiveConfigProgramOptions-example-02.ini
 1#
 2# ActiveConfigProgramOptions-example-02.ini
 3#
 4[CMAKE_COMMAND]
 5opt-set cmake
 6
 7[CMAKE_GENERATOR_NINJA]
 8opt-set -G : Ninja
 9
10[MYPROJ_OPTIONS]
11opt-set-cmake-var  MYPROJ_CXX_FLAGS       STRING       : "-O0 -fopenmp"
12opt-set-cmake-var  MYPROJ_ENABLE_OPTION_A BOOL   FORCE : ON
13opt-set-cmake-var  MYPROJ_ENABLE_OPTION_B BOOL         : ON
14
15[MYPROJ_SOURCE_DIR]
16opt-set /path/to/source/dir
17
18[MYPROJ_CONFIGURATION_NINJA]
19use CMAKE_COMMAND
20use CMAKE_GENERATOR_NINJA
21use MYPROJ_OPTIONS
22use MYPROJ_SOURCE_DIR

In this file we have the base CMake command in CMAKE_COMMAND followed by an optional argument to enable the ninja generator in CMAKE_GENERATOR_NINJA.

CMake flags are enabled in MYPROG_OPTIONS. These follow the ActiveConfigParser style of:

opt-set-cmake-var VARNAME [TYPE] [FORCE] [PARENT_SCOPE]: VALUE

and in our file we’re setting cmake flags: MYPROJ_CXX_FLAGS, MYPROJ_ENABLE_OPTION_A, and MYPROJ_ENABLE_OPTION_B. For explanation of what the optional values mean, please check CMake’s documentation on the set command and how it relates to the -D command line option for CMake.

We note that there are some differences between how CMake treates an argument sent at the command line using a -D option and how the set() function inside a .cmake file behave. We will get into this in Example 3.

We can process this in our sample python script:

ActiveConfigProgramOptions-example-02.py
 1#!/usr/bin/env python3
 2# -*- mode: python; py-indent-offset: 4; py-continuation-offset: 4 -*-
 3from pathlib import Path
 4import activeconfigprogramoptions
 5
 6
 7
 8def print_separator(label):
 9    print("")
10    print(f"{label}")
11    print("-" * len(label))
12    return
13
14
15
16print(80 * "-")
17print(f"- {Path(__file__).name}")
18print(80 * "-")
19
20filename = "ActiveConfigProgramOptions-example-02.ini"
21popts = activeconfigprogramoptions.ActiveConfigProgramOptionsCMake(filename)
22
23section = "MYPROJ_CONFIGURATION_NINJA"
24popts.parse_section(section)
25
26# Generate BASH output
27print_separator("Generate Bash Output")
28bash_options = popts.gen_option_list(section, generator="bash")
29print(" \\\n   ".join(bash_options))
30
31# Generate a CMake Fragment
32print_separator("Generate CMake Fragment")
33cmake_options = popts.gen_option_list(section, generator="cmake_fragment")
34print("\n".join(cmake_options))
35
36print("\nDone")

This code extends the gen_option_list() method that is found in the base class ActiveConfigProgramOptions to add a second generator type, cmake_fragment. We show the output from each in this code:

_ActiveConfigProgramOptions-example-02.log
 1--------------------------------------------------------------------------------
 2- ActiveConfigProgramOptions-example-02.py
 3--------------------------------------------------------------------------------
 4
 5Generate Bash Output
 6--------------------
 7cmake \
 8   -G=Ninja \
 9   -DMYPROJ_CXX_FLAGS:STRING="-O0 -fopenmp" \
10   -DMYPROJ_ENABLE_OPTION_A:BOOL=ON \
11   -DMYPROJ_ENABLE_OPTION_B:BOOL=ON \
12   /path/to/source/dir
13
14Generate CMake Fragment
15-----------------------
16set(MYPROJ_CXX_FLAGS "-O0 -fopenmp" CACHE STRING "from .ini configuration")
17set(MYPROJ_ENABLE_OPTION_A ON CACHE BOOL "from .ini configuration" FORCE)
18set(MYPROJ_ENABLE_OPTION_B ON CACHE BOOL "from .ini configuration")
19
20Done

The general idea behind creating multiple generators is to enable this tool to be used to generate either a command line outptut that could be run directly or appended to a script that is constructed from some template or generate a .cmake fragment file that can be added to a CMake script.

As mentioned earlier, the goal of this tool is that the generated behaviour from CMake is the same for both the command-line method and for .cmake fragment files. Our next example provides additional details on this:

Example 3 - CMake Options and FORCE

This example is a bit more complicated and gets into some of the differences in how CMake treats a command line variable being set via a -D option versus a set() operation within a CMakeLists.txt file.

The script prints out a notice explaining the nuance but the general idea is that CMake treats options provided by a -D option at the command line as though they are CACHE variables with the FORCE option enabled. This is designed to allow command-line parameters to generally take precedence over what a CMakeLists.txt file might set as though it’s a user-override, but if the same option is provided multiple times on a command line then the last one wins.

This is different from how it works inside a .cmake file where the first time a CACHE varaible is set it is considered immutable unless the FORCE option is given. This effectively means that a CACHE var inside .cmake files are treated as first one wins unless the FORCE option is provided.

This can have a subtle yet profound effect on how we must process our opt-set-cmake-var operations within a .ini file if our goal is to ensure that the resulting CMakeCache.txt file generated by a CMake run would be the same for both bash and cmake fragment generators.

In this case, in order to have a variable set by the bash generator it must be a CACHE variable – which can be accomplished by either adding a TYPE or a FORCE option. We will note here that if FORCE is given without a TYPE then we use the default type of STRING.

If the same CMake variable is being assigned, such as in a case where we have a section that is updating a flag, then the FORCE option must be present on the second and all subsequent occurrences of opt-set-cmake-var or the bash generator will skip over that assignment since a non-forced set() operation in CMake would not overwrite an existing cache var. This situation can occur frequently if our .ini file(s) are structured to have some common configuration option set and then a specialization which updates one of the arguments. One example of this kind of situation might be where we have a specialization that adds OpenMP and we would want to add the -fopenmp flags to our linker flags.

ActiveConfigProgramOptions-example-03.ini
 1#
 2# ActiveConfigProgramOptions-example-03.ini
 3#
 4[TEST_VAR_EXPANSION_COMMON]
 5opt-set-cmake-var CMAKE_CXX_FLAGS STRING : "${LDFLAGS|ENV} -foo"
 6# note: STRING type implies this is a CACHE var.
 7
 8
 9[TEST_VAR_EXPANSION_UPDATE_01]
10opt-set cmake
11use TEST_VAR_EXPANSION_COMMON
12
13opt-set-cmake-var CMAKE_CXX_FLAGS STRING: "${CMAKE_CXX_FLAGS|CMAKE} -bar"
14# This will be skipped by the BASH generator without a FORCE option added
15# because .cmake files treate CACHE vars as immutable and won't overwrite
16# the value unless forced, but all bash `-D` options are CACHE and FORCE
17# so without FORCE the only way we can ensure the bash and cmake_fragment
18# generators create a result that generates the same CMakeCache.txt file
19# upon generation is to remove this option from the bash generated result.
ActiveConfigProgramOptions-example-03.py
 1#!/usr/bin/env python3
 2# -*- mode: python; py-indent-offset: 4; py-continuation-offset: 4 -*-
 3from pathlib import Path
 4from pprint import pprint
 5import activeconfigprogramoptions
 6
 7
 8
 9def print_separator(label):
10    print("")
11    print(f"{label}")
12    print("-" * len(label))
13    return
14
15
16
17filename = "ActiveConfigProgramOptions-example-03.ini"
18print(f"filename: {filename}")
19
20section_name = "TEST_VAR_EXPANSION_UPDATE_01"
21print(f"section_name = {section_name}")
22
23parser = activeconfigprogramoptions.ActiveConfigProgramOptionsCMake(filename=filename)
24parser.debug_level = 0
25parser.exception_control_level = 4
26parser.exception_control_compact_warnings = True
27
28data = parser.activeconfigparserdata[section_name]
29print_separator(f"parser.activeconfigparserdata[{section_name}]")
30pprint(data, width=120)
31
32print_separator("Show parser.options")
33pprint(parser.options, width=200, sort_dicts=False)
34
35print_separator("Bash Output")
36print("Note: The _second_ assignment to `CMAKE_CXX_FLAGS` is skipped by a BASH generator")
37print("      without a `FORCE` option since by definition all CMake `-D` options on a ")
38print("      BASH command line are both CACHE and FORCE. Within a CMake source fragment")
39print("      changing an existing CACHE var requires a FORCE option to be set so we should")
40print("      skip the second assignment to maintain consistency between the bash and cmake")
41print("      fragment generators with respect to the CMakeCache.txt file that would be")
42print("      generated.")
43print("      The `WARNING` message below is terse since it's in compact form -- disable")
44print("      the `exception_control_compact_warnings` flag to get the full warning message.")
45print("")
46option_list = parser.gen_option_list(section_name, generator="bash")
47print("")
48print(" \\\n    ".join(option_list))
49
50print_separator("CMake Fragment")
51option_list = parser.gen_option_list(section_name, generator="cmake_fragment")
52if len(option_list) > 0:
53    print("\n".join(option_list))
54else:
55    print("-")
56print("")

We will see in the generated output that the assignment to CMAKE_CXX_FLAGS in section TEST_VAR_EXPANSION_UPDATE_01 is omitted from the bash output because it is not FORCEd:

_ActiveConfigProgramOptions-example-03.log
 1filename: ActiveConfigProgramOptions-example-03.ini
 2section_name = TEST_VAR_EXPANSION_UPDATE_01
 3
 4parser.activeconfigparserdata[TEST_VAR_EXPANSION_UPDATE_01]
 5-----------------------------------------------------------
 6{}
 7
 8Show parser.options
 9-------------------
10{'TEST_VAR_EXPANSION_UPDATE_01': [{'type': ['opt_set'], 'value': None, 'params': ['cmake']},
11                                  {'type': ['opt_set_cmake_var'], 'value': '${LDFLAGS|ENV} -foo', 'params': ['CMAKE_CXX_FLAGS', 'STRING']},
12                                  {'type': ['opt_set_cmake_var'], 'value': '${CMAKE_CXX_FLAGS|CMAKE} -bar', 'params': ['CMAKE_CXX_FLAGS', 'STRING']}]}
13
14Bash Output
15-----------
16Note: The _second_ assignment to `CMAKE_CXX_FLAGS` is skipped by a BASH generator
17      without a `FORCE` option since by definition all CMake `-D` options on a 
18      BASH command line are both CACHE and FORCE. Within a CMake source fragment
19      changing an existing CACHE var requires a FORCE option to be set so we should
20      skip the second assignment to maintain consistency between the bash and cmake
21      fragment generators with respect to the CMakeCache.txt file that would be
22      generated.
23      The `WARNING` message below is terse since it's in compact form -- disable
24      the `exception_control_compact_warnings` flag to get the full warning message.
25
26!! EXCEPTION SKIPPED (WARNING : ValueError) @ File "/builds/semantik-software/code/python/ActiveConfigProgramOptions/venv-examples/lib/python3.14/site-packages/activeconfigprogramoptions/ActiveConfigProgramOptionsCMake.py", line 306, in _program_option_handler_opt_set_cmake_var_bash
27
28cmake \
29    -DCMAKE_CXX_FLAGS:STRING="${LDFLAGS} -foo"
30
31CMake Fragment
32--------------
33set(CMAKE_CXX_FLAGS "$ENV{LDFLAGS} -foo" CACHE STRING "from .ini configuration")
34set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -bar" CACHE STRING "from .ini configuration")

We will note here that the CMake fragment generator will still generate all of the commands. In this case the second set() command would be ignored by CMake since it’s not FORCEd but the main take-away here is that the bash generator omitted the second -D operation since that would be a FORCE operation by default which is not representative of what was specified in the .ini file.

Additional Examples

See the Examples section for additional usage examples and the full code.