diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index f7cf9d48e796c8ccfaae137174aebc86f0f62521..a031ed1fc421ed420f91589d922661e61b2b7bb2 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,39 +1,54 @@
 image: eicweb.phy.anl.gov:4567/eic/juggler/juggler:$JUGGLER_TAG
 
 default:
-  tags:
-    - silicon
   artifacts:
     expire_in: 3 days
     paths:
       - results/
 
 stages:
-  - build
-  - config
+  - initialize
   - run
   - benchmarks1
   - benchmarks2
+  - process
+  - collect
   - finish
 
-configure:
-  stage: config
+detector:
+  stage: initialize
+  needs: []
+  timeout: 1 hours
+  cache:
+    key:
+      files:
+        - config/env.sh
+        - util/build_detector.sh
+      prefix: "$CI_COMMIT_REF_SLUG"
+    paths:
+      - .local/detector
+      - .local/lib
+  artifacts:
+    paths:
+      - .local/detector
+      - .local/lib
+      - results
+      - config
   script:
+    - ./util/print_env.sh
+    - ./util/build_detector.sh
     - mkdir -p results
     - mkdir -p config
 
-
 include: 
-  - local: 'ecal/ecal_config.yml'
-  - local: 'tracking/tracking_config.yml'
-  - local: 'clustering/clustering_config.yml'
-  - local: 'rich/rich_config.yml'
+  - local: 'benchmarks/ecal/config.yml'
+  - local: 'benchmarks/tracking/config.yml'
+  - local: 'benchmarks/clustering/config.yml'
+  - local: 'benchmarks/rich/config.yml'
 
 
 final_report:
   stage: finish
-  tags:
-    - silicon
   needs: ["ecal_1_emcal_electrons", "tracking_central_electrons"]
   script:
     - mkdir -p results/views && cd results/views && bash ../../bin/download_views
diff --git a/benchmarks/clustering/barrel_clusters.sh b/benchmarks/clustering/barrel_clusters.sh
new file mode 100644
index 0000000000000000000000000000000000000000..5e19e09eeb565f3a82c643e34ed9cf32f7adbed9
--- /dev/null
+++ b/benchmarks/clustering/barrel_clusters.sh
@@ -0,0 +1,75 @@
+#!/bin/bash
+
+./util/print_env.sh
+
+## To run the reconstruction, we need the following global variables:
+## - JUGGLER_INSTALL_PREFIX:   Install prefix for Juggler (simu/recon)
+## - JUGGLER_DETECTOR:         the detector package we want to use for this benchmark
+## - JUGGLER_DETECTOR_VERSION: the detector package we want to use for this benchmark
+## - DETECTOR_PATH:            full path to the detector definitions
+##
+## You can ready options/env.sh for more in-depth explanations of the variables
+## and how they can be controlled.
+source options/env.sh
+export JUGGLER_DETECTOR_PATH=${DETECTOR_PATH}
+
+if [[ ! -n  "${JUGGLER_N_EVENTS}" ]] ; then 
+  export JUGGLER_N_EVENTS=100
+fi
+
+export JUGGLER_FILE_NAME_TAG="barrel_clusters"
+export JUGGLER_GEN_FILE="${JUGGLER_FILE_NAME_TAG}.hepmc"
+
+export JUGGLER_SIM_FILE="sim_${JUGGLER_FILE_NAME_TAG}.root"
+export JUGGLER_REC_FILE="rec_${JUGGLER_FILE_NAME_TAG}.root"
+
+echo "JUGGLER_N_EVENTS = ${JUGGLER_N_EVENTS}"
+echo "JUGGLER_DETECTOR = ${JUGGLER_DETECTOR}"
+
+### Build the detector constructors.
+#git clone https://eicweb.phy.anl.gov/EIC/detectors/${JUGGLER_DETECTOR}.git
+#git clone https://eicweb.phy.anl.gov/EIC/detectors/accelerator.git
+#pushd ${JUGGLER_DETECTOR}
+#ln -s ../accelerator/eic
+#popd
+#mkdir ${JUGGLER_DETECTOR}/build
+#pushd ${JUGGLER_DETECTOR}/build
+#cmake ../. -DCMAKE_INSTALL_PREFIX=/usr/local && make -j30 install
+#popd
+
+root -b -q "benchmarks/clustering/scripts/gen_central_electrons.cxx(${JUGGLER_N_EVENTS}, \"${JUGGLER_FILE_NAME_TAG}.hepmc\")"
+
+#pushd ${JUGGLER_DETECTOR}
+#ls -l
+### run geant4 simulations
+npsim --runType batch \
+      --part.minimalKineticEnergy 1000*GeV  \
+      -v WARNING \
+      --numberOfEvents ${JUGGLER_N_EVENTS} \
+      --compactFile ${DETECTOR_PATH}/${JUGGLER_DETECTOR}.xml \
+      --inputFiles  ${JUGGLER_FILE_NAME_TAG}.hepmc \
+      --outputFile  ${JUGGLER_SIM_FILE}
+if [[ "$?" -ne "0" ]] ; then
+  echo "ERROR running npdet"
+  exit 1
+fi
+
+# Need to figure out how to pass file name to juggler from the commandline
+xenv -x ${JUGGLER_INSTALL_PREFIX}/Juggler.xenv gaudirun.py benchmarks/clustering/options/calorimeter_clustering.py
+if [[ "$?" -ne "0" ]] ; then
+  echo "ERROR running juggler"
+  exit 1
+fi
+
+pwd
+mkdir -p results/clustering
+
+root -b -q "benchmarks/clustering/scripts/barrel_clusters.cxx(\"${JUGGLER_REC_FILE}\")"
+
+root_filesize=$(stat --format=%s "${JUGGLER_DETECTOR}/${JUGGLER_REC_FILE}")
+if [[ "${JUGGLER_N_EVENTS}" -lt "500" ]] ; then 
+  # file must be less than 10 MB to upload
+  if [[ "${root_filesize}" -lt "10000000" ]] ; then 
+    cp ${JUGGLER_REC_FILE} results/clustering/.
+  fi
+fi
diff --git a/benchmarks/clustering/config.yml b/benchmarks/clustering/config.yml
new file mode 100644
index 0000000000000000000000000000000000000000..f4223df743f5ba3180fe9f960d0b7a0a492e9146
--- /dev/null
+++ b/benchmarks/clustering/config.yml
@@ -0,0 +1,22 @@
+clustering:process :
+  stage: process
+  timeout: 8 hour
+  needs: ["detector"]
+  artifacts:
+    expire_in: 20 weeks
+    paths:
+      - results/
+  script:
+    - bash benchmarks/clustering/barrel_clusters.sh
+
+clustering:results:
+  stage: collect
+  needs: ["clustering:process"]
+  script:
+    - ls -lrth
+      #- python dvcs/scripts/merge_results.py
+  artifacts:
+    paths:
+      - results
+        #  reports:
+
diff --git a/clustering/options/calorimeter_clustering.py b/benchmarks/clustering/options/calorimeter_clustering.py
similarity index 96%
rename from clustering/options/calorimeter_clustering.py
rename to benchmarks/clustering/options/calorimeter_clustering.py
index 95d585310a94ec56b070b69f565bb8648272d34a..d71d80f0594eab1ce9582f780c700131fb5e156d 100644
--- a/clustering/options/calorimeter_clustering.py
+++ b/benchmarks/clustering/options/calorimeter_clustering.py
@@ -9,6 +9,9 @@ detector_name = "topside"
 if "JUGGLER_DETECTOR" in os.environ :
   detector_name = str(os.environ["JUGGLER_DETECTOR"])
 
+if "JUGGLER_DETECTOR_PATH" in os.environ :
+  detector_name = str(os.environ["JUGGLER_DETECTOR_PATH"]) + "/" + detector_name
+
 # todo add checks
 input_sim_file  = str(os.environ["JUGGLER_SIM_FILE"])
 output_rec_file = str(os.environ["JUGGLER_REC_FILE"])
diff --git a/clustering/scripts/barrel_clusters.cxx b/benchmarks/clustering/scripts/barrel_clusters.cxx
similarity index 100%
rename from clustering/scripts/barrel_clusters.cxx
rename to benchmarks/clustering/scripts/barrel_clusters.cxx
diff --git a/clustering/scripts/gen_central_electrons.cxx b/benchmarks/clustering/scripts/gen_central_electrons.cxx
similarity index 100%
rename from clustering/scripts/gen_central_electrons.cxx
rename to benchmarks/clustering/scripts/gen_central_electrons.cxx
diff --git a/ecal/ecal_config.yml b/benchmarks/ecal/config.yml
similarity index 60%
rename from ecal/ecal_config.yml
rename to benchmarks/ecal/config.yml
index 7c993fefba126fce93014fb75fabbbde4491227d..a1902af77f513f2c90fbd837716b4ee9009aca61 100644
--- a/ecal/ecal_config.yml
+++ b/benchmarks/ecal/config.yml
@@ -1,27 +1,23 @@
 ecal_1_emcal_electrons:
   image: eicweb.phy.anl.gov:4567/eic/juggler/juggler:$JUGGLER_TAG
-  tags:
-    - silicon
-  needs: ["configure"]
-  timeout: 48 hours
+  stage: process
+  needs: ["detector"]
+  timeout: 8 hours
   artifacts:
     expire_in: 20 weeks
     paths:
       - results/
-  stage: run
   script:
-    - bash ecal/emcal_electrons.sh
+    - bash benchmarks/ecal/emcal_electrons.sh
 
 ecal_1_emcal_pi0s:
   image: eicweb.phy.anl.gov:4567/eic/juggler/juggler:$JUGGLER_TAG
-  tags:
-    - silicon
-  needs: ["configure"]
-  timeout: 12 hours 30 minutes
+  needs: ["detector"]
+  timeout: 12 hours
   artifacts:
     expire_in: 20 weeks
     paths:
       - results/
   stage: run
   script:
-    - bash ecal/emcal_pi0s.sh
+    - bash benchmarks/ecal/emcal_pi0s.sh
diff --git a/ecal/emcal_electrons.sh b/benchmarks/ecal/emcal_electrons.sh
similarity index 61%
rename from ecal/emcal_electrons.sh
rename to benchmarks/ecal/emcal_electrons.sh
index 2c6d3e74bd9d921eb836db2f0a259ec062e7f30b..717b68b56241c2e52da60bb2658b435d5b688d83 100644
--- a/ecal/emcal_electrons.sh
+++ b/benchmarks/ecal/emcal_electrons.sh
@@ -1,17 +1,22 @@
 #!/bin/bash
 
-if [[ ! -n  "${JUGGLER_DETECTOR}" ]] ; then 
-  export JUGGLER_DETECTOR="topside"
-fi
+./util/print_env.sh
+
+## To run the reconstruction, we need the following global variables:
+## - JUGGLER_INSTALL_PREFIX:   Install prefix for Juggler (simu/recon)
+## - JUGGLER_DETECTOR:         the detector package we want to use for this benchmark
+## - JUGGLER_DETECTOR_VERSION: the detector package we want to use for this benchmark
+## - DETECTOR_PATH:            full path to the detector definitions
+##
+## You can ready options/env.sh for more in-depth explanations of the variables
+## and how they can be controlled.
+source options/env.sh
+export JUGGLER_DETECTOR_PATH=${DETECTOR_PATH}
 
 if [[ ! -n  "${JUGGLER_N_EVENTS}" ]] ; then 
   export JUGGLER_N_EVENTS=100
 fi
 
-if [[ ! -n  "${JUGGLER_INSTALL_PREFIX}" ]] ; then 
-  export JUGGLER_INSTALL_PREFIX="/usr/local"
-fi
-
 if [[ ! -n  "${E_start}" ]] ; then
   export E_start=0.0
 fi
@@ -30,40 +35,28 @@ echo "JUGGLER_N_EVENTS = ${JUGGLER_N_EVENTS}"
 echo "JUGGLER_DETECTOR = ${JUGGLER_DETECTOR}"
 
 
-# Build the detector constructors.
-git clone https://eicweb.phy.anl.gov/EIC/detectors/${JUGGLER_DETECTOR}.git
-git clone https://eicweb.phy.anl.gov/EIC/detectors/accelerator.git
-pushd ${JUGGLER_DETECTOR}
-ln -s ../accelerator/eic
-popd
-mkdir ${JUGGLER_DETECTOR}/build
-pushd ${JUGGLER_DETECTOR}/build
-cmake ../. -DCMAKE_INSTALL_PREFIX=/usr/local && make -j30 install
-popd
 
 # generate the input events
 # note datasets is now only used to develop datasets.
 #git clone https://eicweb.phy.anl.gov/EIC/datasets.git datasets
-root -b -q "ecal/scripts/emcal_electrons.cxx(${JUGGLER_N_EVENTS}, ${E_start}, ${E_end}, \"${JUGGLER_FILE_NAME_TAG}.hepmc\")"
+root -b -q "benchmarks/ecal/scripts/emcal_electrons.cxx(${JUGGLER_N_EVENTS}, ${E_start}, ${E_end}, \"${JUGGLER_FILE_NAME_TAG}.hepmc\")"
 if [[ "$?" -ne "0" ]] ; then
   echo "ERROR running script"
   exit 1
 fi
-root -b -q "ecal/scripts/emcal_electrons_reader.cxx(${E_start}, ${E_end}, \"${JUGGLER_FILE_NAME_TAG}.hepmc\")"
+root -b -q "benchmarks/ecal/scripts/emcal_electrons_reader.cxx(${E_start}, ${E_end}, \"${JUGGLER_FILE_NAME_TAG}.hepmc\")"
 if [[ "$?" -ne "0" ]] ; then
   echo "ERROR running script"
   exit 1
 fi
 
-pushd ${JUGGLER_DETECTOR}
-ls -l
 # run geant4 simulations
 npsim --runType batch \
-      -v WARNING \
       --part.minimalKineticEnergy 1000*GeV  \
+      -v WARNING \
       --numberOfEvents ${JUGGLER_N_EVENTS} \
-      --compactFile ${JUGGLER_DETECTOR}.xml \
-      --inputFiles ../${JUGGLER_FILE_NAME_TAG}.hepmc \
+      --compactFile ${DETECTOR_PATH}/${JUGGLER_DETECTOR}.xml \
+      --inputFiles  ${JUGGLER_FILE_NAME_TAG}.hepmc \
       --outputFile  ${JUGGLER_SIM_FILE}
 # Need to figure out how to pass file name to juggler from the commandline
 if [[ "$?" -ne "0" ]] ; then
@@ -71,20 +64,17 @@ if [[ "$?" -ne "0" ]] ; then
   exit 1
 fi
 xenv -x ${JUGGLER_INSTALL_PREFIX}/Juggler.xenv \
-  gaudirun.py ../ecal/options/crystal_calorimeter_reco.py
+  gaudirun.py benchmarks/ecal/options/crystal_calorimeter_reco.py
 if [[ "$?" -ne "0" ]] ; then
   echo "ERROR running juggler"
   exit 1
 fi
-ls -l
-popd
 
-pwd
-mkdir -p results
 
+mkdir -p results
 
 #rootls ${JUGGLER_DETECTOR}/${JUGGLER_REC_FILE}
-root -b -q "ecal/scripts/rec_emcal_electrons_reader.C(${E_start}, ${E_end}, \"${JUGGLER_DETECTOR}/${JUGGLER_REC_FILE}\")"
+root -b -q "benchmarks/ecal/scripts/rec_emcal_electrons_reader.C(${E_start}, ${E_end}, \"${JUGGLER_REC_FILE}\")"
 if [[ "$?" -ne "0" ]] ; then
   echo "ERROR running analysis script"
   exit 1
@@ -92,20 +82,20 @@ fi
 #root -b -q "ecal/scripts/makeplot.C(${E_start}, ${E_end}, \"${JUGGLER_DETECTOR}/${JUGGLER_REC_FILE}\", \"results/rec_${JUGGLER_FILE_NAME_TAG}.txt\")"
 #root -b -q "ecal/scripts/makeplot_input.C(\"${JUGGLER_DETECTOR}/${JUGGLER_SIM_FILE}\", \"results/sim_${JUGGLER_FILE_NAME_TAG}.txt\")"
 
-root -b -q "ecal/scripts/crystal_cal_electrons.cxx(\"${JUGGLER_DETECTOR}/${JUGGLER_REC_FILE}\")"
+root -b -q "benchmarks/ecal/scripts/crystal_cal_electrons.cxx(\"${JUGGLER_REC_FILE}\")"
 if [[ "$?" -ne "0" ]] ; then
   echo "ERROR running analysis script"
   exit 1
 fi
 
-root -b -q "ecal/scripts/emcal_electrons_analysis.cxx(\"${JUGGLER_DETECTOR}/${JUGGLER_REC_FILE}\")"
+root -b -q "benchmarks/ecal/scripts/emcal_electrons_analysis.cxx(\"${JUGGLER_REC_FILE}\")"
 if [[ "$?" -ne "0" ]] ; then
   echo "ERROR running analysis script"
   exit 1
 fi
 
 # Script to generate energy resolution plots
-root -b -q "ecal/scripts/rec_emcal_resolution_analysis.cxx(\"${JUGGLER_DETECTOR}/${JUGGLER_REC_FILE}\")"
+root -b -q "benchmarks/ecal/scripts/rec_emcal_resolution_analysis.cxx(\"${JUGGLER_REC_FILE}\")"
 if [[ "$?" -ne "0" ]] ; then
   echo "ERROR running analysis script"
   exit 1
@@ -114,11 +104,11 @@ fi
 #paste results/sim_${JUGGLER_FILE_NAME_TAG}.txt results/rec_${JUGGLER_FILE_NAME_TAG}.txt > results/eng_${JUGGLER_FILE_NAME_TAG}.txt
 #root -b -q "ecal/scripts/read_eng.C(\"results/eng_${JUGGLER_FILE_NAME_TAG}.root\", \"results/eng_${JUGGLER_FILE_NAME_TAG}.txt\")"
 #root -b -q "ecal/scripts/cal_eng_res.C(\"results/eng_${JUGGLER_FILE_NAME_TAG}.root\")"
-root_filesize=$(stat --format=%s "${JUGGLER_DETECTOR}/${JUGGLER_REC_FILE}")
+root_filesize=$(stat --format=%s "${JUGGLER_REC_FILE}")
 if [[ "${JUGGLER_N_EVENTS}" -lt "500" ]] ; then 
   # file must be less than 10 MB to upload
   if [[ "${root_filesize}" -lt "10000000" ]] ; then 
-    cp ${JUGGLER_DETECTOR}/${JUGGLER_REC_FILE} results/.
+    cp ${JUGGLER_REC_FILE} results/.
   fi
 fi
 
diff --git a/ecal/emcal_pi0s.sh b/benchmarks/ecal/emcal_pi0s.sh
similarity index 57%
rename from ecal/emcal_pi0s.sh
rename to benchmarks/ecal/emcal_pi0s.sh
index 94c6ac08d541b8f4e0dcfbd46c79a475a11a03a7..cdb93bbd2332441f535161a29238cfea9b169a31 100644
--- a/ecal/emcal_pi0s.sh
+++ b/benchmarks/ecal/emcal_pi0s.sh
@@ -1,14 +1,23 @@
 #!/bin/bash
 # Based on emcal_electrons.sh script
 
-# Detector
-if [[ ! -n  "${JUGGLER_DETECTOR}" ]] ; then 
-  export JUGGLER_DETECTOR="topside"
-fi
-# Number of events
+./util/print_env.sh
+
+## To run the reconstruction, we need the following global variables:
+## - JUGGLER_INSTALL_PREFIX:   Install prefix for Juggler (simu/recon)
+## - JUGGLER_DETECTOR:         the detector package we want to use for this benchmark
+## - JUGGLER_DETECTOR_VERSION: the detector package we want to use for this benchmark
+## - DETECTOR_PATH:            full path to the detector definitions
+##
+## You can ready options/env.sh for more in-depth explanations of the variables
+## and how they can be controlled.
+source options/env.sh
+export JUGGLER_DETECTOR_PATH=${DETECTOR_PATH}
+
 if [[ ! -n  "${JUGGLER_N_EVENTS}" ]] ; then 
   export JUGGLER_N_EVENTS=100
 fi
+
 # File names
 export JUGGLER_FILE_NAME_TAG="emcal_uniform_pi0s"
 export JUGGLER_GEN_FILE="${JUGGLER_FILE_NAME_TAG}.hepmc"
@@ -20,44 +29,30 @@ echo "JUGGLER_N_EVENTS = ${JUGGLER_N_EVENTS}"
 echo "JUGGLER_DETECTOR = ${JUGGLER_DETECTOR}"
 # Datasets
 #git clone https://eicweb.phy.anl.gov/EIC/datasets.git datasets
-root -b -q "ecal/scripts/emcal_pi0.cxx(${JUGGLER_N_EVENTS}, \"${JUGGLER_FILE_NAME_TAG}.hepmc\")"
+root -b -q "benchmarks/ecal/scripts/emcal_pi0.cxx(${JUGGLER_N_EVENTS}, \"${JUGGLER_FILE_NAME_TAG}.hepmc\")"
 if [[ "$?" -ne "0" ]] ; then
   echo "ERROR running analysis script"
   exit 1
 fi
-root -b -q "ecal/scripts/emcal_pi0_reader.cxx(\"${JUGGLER_FILE_NAME_TAG}.hepmc\")"
+root -b -q "benchmarks/ecal/scripts/emcal_pi0_reader.cxx(\"${JUGGLER_FILE_NAME_TAG}.hepmc\")"
 if [[ "$?" -ne "0" ]] ; then
   echo "ERROR running analysis script"
   exit 1
 fi
 
-# Detector TOPSiDE
-git clone https://eicweb.phy.anl.gov/EIC/detectors/${JUGGLER_DETECTOR}.git
-git clone https://eicweb.phy.anl.gov/EIC/detectors/accelerator.git
-pushd ${JUGGLER_DETECTOR}
-ln -s ../accelerator/eic
-popd
-mkdir ${JUGGLER_DETECTOR}/build
-pushd ${JUGGLER_DETECTOR}/build
-cmake ../. -DCMAKE_INSTALL_PREFIX=/usr/local && make -j30 install
-popd
-
 echo "CHECK POINT FOR GEANT4 SIMULATION"
-pwd
-pushd ${JUGGLER_DETECTOR}
-pwd
-ls -l
+
 echo "JUGGLER_N_EVENTS = ${JUGGLER_N_EVENTS}"
 echo "JUGGLER_DETECTOR = ${JUGGLER_DETECTOR}"
 echo "JUGGLER_FILE_NAME_TAG = ${JUGGLER_FILE_NAME_TAG}"
 echo "JUGGLER_SIM_FILE = ${JUGGLER_SIM_FILE}"
 # run geant4 simulations
 npsim --runType batch \
-      -v WARNING \
       --part.minimalKineticEnergy 1000*GeV  \
+      -v WARNING \
       --numberOfEvents ${JUGGLER_N_EVENTS} \
-      --compactFile ${JUGGLER_DETECTOR}.xml \
-      --inputFiles ../${JUGGLER_FILE_NAME_TAG}.hepmc \
+      --compactFile ${DETECTOR_PATH}/${JUGGLER_DETECTOR}.xml \
+      --inputFiles  ${JUGGLER_FILE_NAME_TAG}.hepmc \
       --outputFile  ${JUGGLER_SIM_FILE}
 if [[ "$?" -ne "0" ]] ; then
   echo "ERROR running npdet"
@@ -65,30 +60,25 @@ if [[ "$?" -ne "0" ]] ; then
 fi
 
 # Need to figure out how to pass file name to juggler from the commandline
-xenv -x /usr/local/Juggler.xenv gaudirun.py ../ecal/options/crystal_calorimeter_reco.py
+xenv -x ${JUGGLER_INSTALL_PREFIX}/Juggler.xenv gaudirun.py benchmarks/ecal/options/crystal_calorimeter_reco.py
 if [[ "$?" -ne "0" ]] ; then
   echo "ERROR running juggler"
   exit 1
 fi
 
-ls -l
-
-popd
-ls -l
 
-pwd
 mkdir -p results
-root -b -q "ecal/scripts/makeplot_pi0.C(\"${JUGGLER_DETECTOR}/${JUGGLER_REC_FILE}\")"
+root -b -q "benchmarks/ecal/scripts/makeplot_pi0.C(\"${JUGGLER_REC_FILE}\")"
 if [[ "$?" -ne "0" ]] ; then
   echo "ERROR running analysis script"
   exit 1
 fi
 
-root_filesize=$(stat --format=%s "${JUGGLER_DETECTOR}/${JUGGLER_REC_FILE}")
+root_filesize=$(stat --format=%s "${JUGGLER_REC_FILE}")
 if [[ "${JUGGLER_N_EVENTS}" -lt "500" ]] ; then 
   # file must be less than 10 MB to upload
   if [[ "${root_filesize}" -lt "10000000" ]] ; then 
-    cp ${JUGGLER_DETECTOR}/${JUGGLER_REC_FILE} results/.
+    cp ${JUGGLER_REC_FILE} results/.
   fi
 fi
 
diff --git a/ecal/options/crystal_calorimeter_reco.py b/benchmarks/ecal/options/crystal_calorimeter_reco.py
similarity index 96%
rename from ecal/options/crystal_calorimeter_reco.py
rename to benchmarks/ecal/options/crystal_calorimeter_reco.py
index f42d78f91eea8d4fae998a060d5bc8b9d3f3bf2e..218ccc8479b563dbbd0ebd5445f751fe39ae225d 100644
--- a/ecal/options/crystal_calorimeter_reco.py
+++ b/benchmarks/ecal/options/crystal_calorimeter_reco.py
@@ -9,6 +9,9 @@ detector_name = "topside"
 if "JUGGLER_DETECTOR" in os.environ :
   detector_name = str(os.environ["JUGGLER_DETECTOR"])
 
+if "JUGGLER_DETECTOR_PATH" in os.environ :
+  detector_name = str(os.environ["JUGGLER_DETECTOR_PATH"])+"/"+detector_name 
+
 # todo add checks
 input_sim_file = "jug_input.root"
 if "JUGGLER_SIM_FILE" in os.environ :
diff --git a/ecal/options/example_crystal.py b/benchmarks/ecal/options/example_crystal.py
similarity index 100%
rename from ecal/options/example_crystal.py
rename to benchmarks/ecal/options/example_crystal.py
diff --git a/ecal/scripts/cal_eng_res.C b/benchmarks/ecal/scripts/cal_eng_res.C
similarity index 100%
rename from ecal/scripts/cal_eng_res.C
rename to benchmarks/ecal/scripts/cal_eng_res.C
diff --git a/ecal/scripts/crystal_cal_electrons.cxx b/benchmarks/ecal/scripts/crystal_cal_electrons.cxx
similarity index 100%
rename from ecal/scripts/crystal_cal_electrons.cxx
rename to benchmarks/ecal/scripts/crystal_cal_electrons.cxx
diff --git a/ecal/scripts/emcal_electrons.cxx b/benchmarks/ecal/scripts/emcal_electrons.cxx
similarity index 100%
rename from ecal/scripts/emcal_electrons.cxx
rename to benchmarks/ecal/scripts/emcal_electrons.cxx
diff --git a/ecal/scripts/emcal_electrons_analysis.cxx b/benchmarks/ecal/scripts/emcal_electrons_analysis.cxx
similarity index 100%
rename from ecal/scripts/emcal_electrons_analysis.cxx
rename to benchmarks/ecal/scripts/emcal_electrons_analysis.cxx
diff --git a/ecal/scripts/emcal_electrons_reader.cxx b/benchmarks/ecal/scripts/emcal_electrons_reader.cxx
similarity index 100%
rename from ecal/scripts/emcal_electrons_reader.cxx
rename to benchmarks/ecal/scripts/emcal_electrons_reader.cxx
diff --git a/ecal/scripts/emcal_pi0.cxx b/benchmarks/ecal/scripts/emcal_pi0.cxx
similarity index 100%
rename from ecal/scripts/emcal_pi0.cxx
rename to benchmarks/ecal/scripts/emcal_pi0.cxx
diff --git a/ecal/scripts/emcal_pi0_reader.cxx b/benchmarks/ecal/scripts/emcal_pi0_reader.cxx
similarity index 100%
rename from ecal/scripts/emcal_pi0_reader.cxx
rename to benchmarks/ecal/scripts/emcal_pi0_reader.cxx
diff --git a/ecal/scripts/makeplot.C b/benchmarks/ecal/scripts/makeplot.C
similarity index 100%
rename from ecal/scripts/makeplot.C
rename to benchmarks/ecal/scripts/makeplot.C
diff --git a/ecal/scripts/makeplot_input.C b/benchmarks/ecal/scripts/makeplot_input.C
similarity index 100%
rename from ecal/scripts/makeplot_input.C
rename to benchmarks/ecal/scripts/makeplot_input.C
diff --git a/ecal/scripts/makeplot_pi0.C b/benchmarks/ecal/scripts/makeplot_pi0.C
similarity index 100%
rename from ecal/scripts/makeplot_pi0.C
rename to benchmarks/ecal/scripts/makeplot_pi0.C
diff --git a/ecal/scripts/rdf_test.cxx b/benchmarks/ecal/scripts/rdf_test.cxx
similarity index 100%
rename from ecal/scripts/rdf_test.cxx
rename to benchmarks/ecal/scripts/rdf_test.cxx
diff --git a/ecal/scripts/read_eng.C b/benchmarks/ecal/scripts/read_eng.C
similarity index 100%
rename from ecal/scripts/read_eng.C
rename to benchmarks/ecal/scripts/read_eng.C
diff --git a/ecal/scripts/rec_emcal_electrons_reader.C b/benchmarks/ecal/scripts/rec_emcal_electrons_reader.C
similarity index 100%
rename from ecal/scripts/rec_emcal_electrons_reader.C
rename to benchmarks/ecal/scripts/rec_emcal_electrons_reader.C
diff --git a/ecal/scripts/rec_emcal_resolution_analysis.cxx b/benchmarks/ecal/scripts/rec_emcal_resolution_analysis.cxx
similarity index 100%
rename from ecal/scripts/rec_emcal_resolution_analysis.cxx
rename to benchmarks/ecal/scripts/rec_emcal_resolution_analysis.cxx
diff --git a/rich/rich_config.yml b/benchmarks/rich/config.yml
similarity index 67%
rename from rich/rich_config.yml
rename to benchmarks/rich/config.yml
index ca524c308b14f0d098ffc6c4aff77756682881e9..ff140147d9341588afa043de0dde62f82fc8f07f 100644
--- a/rich/rich_config.yml
+++ b/benchmarks/rich/config.yml
@@ -1,8 +1,6 @@
 rich_job_x:
   image: eicweb.phy.anl.gov:4567/eic/juggler/juggler:$JUGGLER_TAG
-  tags:
-    - silicon
-  needs: ["configure"]
+  needs: ["detector"]
   timeout: 24 hours
   artifacts:
     expire_in: 20 weeks
@@ -10,5 +8,7 @@ rich_job_x:
       - results/
   stage: run
   script:
-    - bash rich/forward_hadrons.sh
+    - bash benchmarks/rich/forward_hadrons.sh
+  allow_failure: true
+
 
diff --git a/benchmarks/rich/forward_hadrons.sh b/benchmarks/rich/forward_hadrons.sh
new file mode 100644
index 0000000000000000000000000000000000000000..c80add8e987eda9dbfa3c71ae1bd3d293161db8f
--- /dev/null
+++ b/benchmarks/rich/forward_hadrons.sh
@@ -0,0 +1,85 @@
+#!/bin/bash
+
+./util/print_env.sh
+
+## To run the reconstruction, we need the following global variables:
+## - JUGGLER_INSTALL_PREFIX:   Install prefix for Juggler (simu/recon)
+## - JUGGLER_DETECTOR:         the detector package we want to use for this benchmark
+## - JUGGLER_DETECTOR_VERSION: the detector package we want to use for this benchmark
+## - DETECTOR_PATH:            full path to the detector definitions
+##
+## You can ready options/env.sh for more in-depth explanations of the variables
+## and how they can be controlled.
+source options/env.sh
+export JUGGLER_DETECTOR_PATH=${DETECTOR_PATH}
+
+if [[ ! -n  "${JUGGLER_N_EVENTS}" ]] ; then 
+  export JUGGLER_N_EVENTS=100
+fi
+
+export JUGGLER_FILE_NAME_TAG="rich_forward_hadrons"
+export JUGGLER_GEN_FILE="${JUGGLER_FILE_NAME_TAG}.hepmc"
+
+export JUGGLER_SIM_FILE="sim_${JUGGLER_FILE_NAME_TAG}.root"
+export JUGGLER_REC_FILE="rec_${JUGGLER_FILE_NAME_TAG}.root"
+
+echo "JUGGLER_N_EVENTS = ${JUGGLER_N_EVENTS}"
+echo "JUGGLER_DETECTOR = ${JUGGLER_DETECTOR}"
+
+
+# generate the input events
+# note datasets is now only used to develop datasets.
+#git clone https://eicweb.phy.anl.gov/EIC/datasets.git datasets
+python benchmarks/rich/scripts/rich_data_gen.py \
+       ${JUGGLER_FILE_NAME_TAG}.hepmc \
+       -n ${JUGGLER_N_EVENTS} \
+       --pmin 5.0 \
+       --pmax 100.0 \
+       --angmin 3.0 \
+       --angmax 8.0
+if [[ "$?" -ne "0" ]] ; then
+  echo "ERROR running script"
+  exit 1
+fi
+
+# This script appears to be missing
+## run geant4 simulations
+#python benchmakrs/options/ForwardRICH/simu.py \
+#       --compact=${DETECTOR_PATH}/${JUGGLER_DETECTOR}.xml \
+#       -i ${JUGGLER_FILE_NAME_TAG}.hepmc \
+#       -o ${JUGGLER_SIM_FILE}
+#       -n ${JUGGLER_N_EVENTS}
+#if [[ "$?" -ne "0" ]] ; then
+#  echo "ERROR running script"
+#  exit 1
+#fi
+
+npsim --runType batch \
+      --part.minimalKineticEnergy 1000*GeV  \
+      -v WARNING \
+      --numberOfEvents ${JUGGLER_N_EVENTS} \
+      --compactFile ${DETECTOR_PATH}/${JUGGLER_DETECTOR}.xml \
+      --inputFiles  ${JUGGLER_FILE_NAME_TAG}.hepmc \
+      --outputFile  ${JUGGLER_SIM_FILE}
+if [[ "$?" -ne "0" ]] ; then
+  echo "ERROR running npdet"
+  exit 1
+fi
+
+# @TODO changeable simulation file name and detector xml file name
+xenv -x ${JUGGLER_INSTALL_PREFIX}/Juggler.xenv \
+  gaudirun.py benchmarks/rich/options/rich_reco.py
+if [[ "$?" -ne "0" ]] ; then
+  echo "ERROR running juggler"
+  exit 1
+fi
+
+# @TODO add analysis scripts
+#root_filesize=$(stat --format=%s "${JUGGLER_DETECTOR}/${JUGGLER_REC_FILE}")
+#if [[ "${JUGGLER_N_EVENTS}" -lt "500" ]] ; then 
+#  # file must be less than 10 MB to upload
+#  if [[ "${root_filesize}" -lt "10000000" ]] ; then 
+#    cp ${JUGGLER_DETECTOR}/${JUGGLER_REC_FILE} results/.
+#  fi
+#fi
+
diff --git a/rich/options/rich_reco.py b/benchmarks/rich/options/rich_reco.py
similarity index 95%
rename from rich/options/rich_reco.py
rename to benchmarks/rich/options/rich_reco.py
index d6faddc5cab1f8519099006c1fc7857c35759874..a230fae6981e884efa019fe6c264e386c4ade5b7 100644
--- a/rich/options/rich_reco.py
+++ b/benchmarks/rich/options/rich_reco.py
@@ -7,6 +7,8 @@ from Configurables import ApplicationMgr, EICDataSvc, PodioOutput, GeoSvc
 detector_name = "topside"
 if "JUGGLER_DETECTOR" in os.environ :
   detector_name = str(os.environ["JUGGLER_DETECTOR"])
+if "JUGGLER_DETECTOR_PATH" in os.environ :
+  detector_name = str(os.environ["JUGGLER_DETECTOR_PATH"])+"/"+detector_name
 
 # todo add checks
 input_sim_file  = str(os.environ["JUGGLER_SIM_FILE"])
diff --git a/rich/scripts/rich_data_gen.py b/benchmarks/rich/scripts/rich_data_gen.py
similarity index 100%
rename from rich/scripts/rich_data_gen.py
rename to benchmarks/rich/scripts/rich_data_gen.py
diff --git a/rich/scripts/rich_samples.py b/benchmarks/rich/scripts/rich_samples.py
similarity index 100%
rename from rich/scripts/rich_samples.py
rename to benchmarks/rich/scripts/rich_samples.py
diff --git a/tracking/central_electrons.sh b/benchmarks/tracking/central_electrons.sh
similarity index 50%
rename from tracking/central_electrons.sh
rename to benchmarks/tracking/central_electrons.sh
index 75cc37c4e3c30c97f22c7daa55ad7e9e08c6be33..3d53ebe4c6fd0fd1c9cee44265e49a10a616fcdf 100644
--- a/tracking/central_electrons.sh
+++ b/benchmarks/tracking/central_electrons.sh
@@ -1,16 +1,21 @@
 #!/bin/bash
-
-if [[ ! -n  "${JUGGLER_DETECTOR}" ]] ; then 
-  export JUGGLER_DETECTOR="topside"
-fi
+./util/print_env.sh
+
+## To run the reconstruction, we need the following global variables:
+## - JUGGLER_INSTALL_PREFIX:   Install prefix for Juggler (simu/recon)
+## - JUGGLER_DETECTOR:         the detector package we want to use for this benchmark
+## - JUGGLER_DETECTOR_VERSION: the detector package we want to use for this benchmark
+## - DETECTOR_PATH:            full path to the detector definitions
+##
+## You can ready options/env.sh for more in-depth explanations of the variables
+## and how they can be controlled.
+source options/env.sh
+export JUGGLER_DETECTOR_PATH=${DETECTOR_PATH}
 
 if [[ ! -n  "${JUGGLER_N_EVENTS}" ]] ; then 
   export JUGGLER_N_EVENTS=100
 fi
 
-if [[ ! -n  "${JUGGLER_INSTALL_PREFIX}" ]] ; then 
-  export JUGGLER_INSTALL_PREFIX="/usr/local"
-fi
 
 export JUGGLER_FILE_NAME_TAG="central_electrons"
 export JUGGLER_GEN_FILE="${JUGGLER_FILE_NAME_TAG}.hepmc"
@@ -22,34 +27,21 @@ echo "JUGGLER_N_EVENTS = ${JUGGLER_N_EVENTS}"
 echo "JUGGLER_DETECTOR = ${JUGGLER_DETECTOR}"
 
 
-### Build the detector constructors.
-git clone -b acts_v4 https://eicweb.phy.anl.gov/EIC/detectors/${JUGGLER_DETECTOR}.git
-git clone https://eicweb.phy.anl.gov/EIC/detectors/accelerator.git
-pushd ${JUGGLER_DETECTOR}
-ln -s ../accelerator/eic
-popd
-mkdir ${JUGGLER_DETECTOR}/build
-pushd ${JUGGLER_DETECTOR}/build
-cmake ../. -DCMAKE_INSTALL_PREFIX=/usr/local && make -j30 install
-popd
-
 ## generate the input events
-root -b -q "tracking/scripts/gen_central_electrons.cxx(${JUGGLER_N_EVENTS}, \"${JUGGLER_FILE_NAME_TAG}.hepmc\")"
+root -b -q "benchmarks/tracking/scripts/gen_central_electrons.cxx(${JUGGLER_N_EVENTS}, \"${JUGGLER_FILE_NAME_TAG}.hepmc\")"
 if [[ "$?" -ne "0" ]] ; then
   echo "ERROR running script"
   exit 1
 fi
 
-#
-pushd ${JUGGLER_DETECTOR}
 
 ## run geant4 simulations
 npsim --runType batch \
       --part.minimalKineticEnergy 1000*GeV  \
       -v WARNING \
       --numberOfEvents ${JUGGLER_N_EVENTS} \
-      --compactFile ${JUGGLER_DETECTOR}.xml \
-      --inputFiles ../${JUGGLER_FILE_NAME_TAG}.hepmc \
+      --compactFile ${DETECTOR_PATH}/${JUGGLER_DETECTOR}.xml \
+      --inputFiles  ${JUGGLER_FILE_NAME_TAG}.hepmc \
       --outputFile  ${JUGGLER_SIM_FILE}
 if [[ "$?" -ne "0" ]] ; then
   echo "ERROR running script"
@@ -57,29 +49,26 @@ if [[ "$?" -ne "0" ]] ; then
 fi
 
 # Need to figure out how to pass file name to juggler from the commandline
-xenv -x ${JUGGLER_INSTALL_PREFIX}/Juggler.xenv gaudirun.py ../tracking/options/tracker_reconstruction.py
-
+xenv -x ${JUGGLER_INSTALL_PREFIX}/Juggler.xenv gaudirun.py benchmarks/tracking/options/tracker_reconstruction.py
 if [[ "$?" -ne "0" ]] ; then
   echo "ERROR running juggler"
   exit 1
 fi
-ls -l
-popd
 
-pwd
+
 mkdir -p results/tracking
 
-root -b -q "tracking/scripts/rec_central_electrons.cxx(\"${JUGGLER_DETECTOR}/${JUGGLER_REC_FILE}\")"
+root -b -q "benchmarks/tracking/scripts/rec_central_electrons.cxx(\"${JUGGLER_REC_FILE}\")"
 if [[ "$?" -ne "0" ]] ; then
   echo "ERROR running root script"
   exit 1
 fi
 
-root_filesize=$(stat --format=%s "${JUGGLER_DETECTOR}/${JUGGLER_REC_FILE}")
+root_filesize=$(stat --format=%s "${JUGGLER_REC_FILE}")
 if [[ "${JUGGLER_N_EVENTS}" -lt "500" ]] ; then 
   # file must be less than 10 MB to upload
   if [[ "${root_filesize}" -lt "10000000" ]] ; then 
-    cp ${JUGGLER_DETECTOR}/${JUGGLER_REC_FILE} results/.
+    cp ${JUGGLER_REC_FILE} results/.
   fi
 fi
 
diff --git a/tracking/tracking_config.yml b/benchmarks/tracking/config.yml
similarity index 70%
rename from tracking/tracking_config.yml
rename to benchmarks/tracking/config.yml
index 9e0772b4097850d88aa4b9591ff3d290d5f14ab2..f8abdc0e30f2a0b46e1c44f8868afedeea3ae0b4 100644
--- a/tracking/tracking_config.yml
+++ b/benchmarks/tracking/config.yml
@@ -1,8 +1,6 @@
 tracking_central_electrons:
   image: eicweb.phy.anl.gov:4567/eic/juggler/juggler:$JUGGLER_TAG
-  tags:
-    - silicon
-  needs: ["configure"]
+  needs: ["detector"]
   timeout: 24 hours
   artifacts:
     expire_in: 20 weeks
@@ -10,5 +8,5 @@ tracking_central_electrons:
       - results/
   stage: run
   script:
-    - bash tracking/central_electrons.sh
+    - bash benchmarks/tracking/central_electrons.sh
 
diff --git a/tracking/options/tracker_reconstruction.py b/benchmarks/tracking/options/tracker_reconstruction.py
similarity index 98%
rename from tracking/options/tracker_reconstruction.py
rename to benchmarks/tracking/options/tracker_reconstruction.py
index 2fa99cae37bec6bb10c1def824cecb99b168a703..4c7d2074488692aea38c16ba650de9b41782997d 100644
--- a/tracking/options/tracker_reconstruction.py
+++ b/benchmarks/tracking/options/tracker_reconstruction.py
@@ -7,6 +7,8 @@ from GaudiKernel import SystemOfUnits as units
 detector_name = "topside"
 if "JUGGLER_DETECTOR" in os.environ :
   detector_name = str(os.environ["JUGGLER_DETECTOR"])
+if "JUGGLER_DETECTOR_PATH" in os.environ :
+  detector_name = str(os.environ["JUGGLER_DETECTOR_PATH"])+"/"+detector_name
 
 # todo add checks
 input_sim_file  = str(os.environ["JUGGLER_SIM_FILE"])
diff --git a/tracking/scripts/gen_central_electrons.cxx b/benchmarks/tracking/scripts/gen_central_electrons.cxx
similarity index 100%
rename from tracking/scripts/gen_central_electrons.cxx
rename to benchmarks/tracking/scripts/gen_central_electrons.cxx
diff --git a/tracking/scripts/rec_central_electrons.cxx b/benchmarks/tracking/scripts/rec_central_electrons.cxx
similarity index 100%
rename from tracking/scripts/rec_central_electrons.cxx
rename to benchmarks/tracking/scripts/rec_central_electrons.cxx
diff --git a/clustering/barrel_clusters.sh b/clustering/barrel_clusters.sh
deleted file mode 100644
index e675ce6924714b3eb18fa13fd92a3608ce818bdc..0000000000000000000000000000000000000000
--- a/clustering/barrel_clusters.sh
+++ /dev/null
@@ -1,65 +0,0 @@
-#!/bin/bash
-
-if [[ ! -n  "${JUGGLER_DETECTOR}" ]] ; then 
-  export JUGGLER_DETECTOR="topside"
-fi
-
-if [[ ! -n  "${JUGGLER_N_EVENTS}" ]] ; then 
-  export JUGGLER_N_EVENTS=100
-fi
-
-if [[ ! -n  "${JUGGLER_INSTALL_PREFIX}" ]] ; then 
-  export JUGGLER_INSTALL_PREFIX="/usr/local"
-fi
-
-export JUGGLER_FILE_NAME_TAG="barrel_clusters"
-export JUGGLER_GEN_FILE="${JUGGLER_FILE_NAME_TAG}.hepmc"
-
-export JUGGLER_SIM_FILE="sim_${JUGGLER_FILE_NAME_TAG}.root"
-export JUGGLER_REC_FILE="rec_${JUGGLER_FILE_NAME_TAG}.root"
-
-echo "JUGGLER_N_EVENTS = ${JUGGLER_N_EVENTS}"
-echo "JUGGLER_DETECTOR = ${JUGGLER_DETECTOR}"
-
-
-## Build the detector constructors.
-git clone https://eicweb.phy.anl.gov/EIC/detectors/${JUGGLER_DETECTOR}.git
-git clone https://eicweb.phy.anl.gov/EIC/detectors/accelerator.git
-pushd ${JUGGLER_DETECTOR}
-ln -s ../accelerator/eic
-popd
-mkdir ${JUGGLER_DETECTOR}/build
-pushd ${JUGGLER_DETECTOR}/build
-cmake ../. -DCMAKE_INSTALL_PREFIX=/usr/local && make -j30 install
-popd
-
-root -b -q "clustering/scripts/gen_central_electrons.cxx(${JUGGLER_N_EVENTS}, \"${JUGGLER_FILE_NAME_TAG}.hepmc\")"
-
-pushd ${JUGGLER_DETECTOR}
-#ls -l
-### run geant4 simulations
-npsim --runType batch \
-      --part.minimalKineticEnergy 1000*GeV  \
-      -v WARNING \
-      --numberOfEvents ${JUGGLER_N_EVENTS} \
-      --compactFile ${JUGGLER_DETECTOR}.xml \
-      --inputFiles ../${JUGGLER_FILE_NAME_TAG}.hepmc \
-      --outputFile  ${JUGGLER_SIM_FILE}
-
-# Need to figure out how to pass file name to juggler from the commandline
-xenv -x ${JUGGLER_INSTALL_PREFIX}/Juggler.xenv gaudirun.py ../clustering/options/calorimeter_clustering.py
-ls -l
-popd
-
-pwd
-mkdir -p results/clustering
-
-root -b -q "clustering/scripts/barrel_clusters.cxx(\"${JUGGLER_DETECTOR}/${JUGGLER_REC_FILE}\")"
-
-root_filesize=$(stat --format=%s "${JUGGLER_DETECTOR}/${JUGGLER_REC_FILE}")
-if [[ "${JUGGLER_N_EVENTS}" -lt "500" ]] ; then 
-  # file must be less than 10 MB to upload
-  if [[ "${root_filesize}" -lt "10000000" ]] ; then 
-    cp ${JUGGLER_DETECTOR}/${JUGGLER_REC_FILE} results/clustering/.
-  fi
-fi
diff --git a/clustering/clustering_config.yml b/clustering/clustering_config.yml
deleted file mode 100644
index 14f16edfdbc6a2ce869b03e894f49a88aa7e55dd..0000000000000000000000000000000000000000
--- a/clustering/clustering_config.yml
+++ /dev/null
@@ -1,14 +0,0 @@
-clustering_barrel_electrons:
-  image: eicweb.phy.anl.gov:4567/eic/juggler/juggler:$JUGGLER_TAG
-  tags:
-    - silicon
-  needs: ["configure"]
-  timeout: 24 hours
-  artifacts:
-    expire_in: 20 weeks
-    paths:
-      - results/
-  stage: run
-  script:
-    - bash clustering/barrel_clusters.sh
-
diff --git a/include/benchmark.h b/include/benchmark.h
new file mode 100644
index 0000000000000000000000000000000000000000..0634be0a1494739c4e7337f99e35ffbac0be7f7f
--- /dev/null
+++ b/include/benchmark.h
@@ -0,0 +1,124 @@
+#ifndef BENCHMARK_H
+#define BENCHMARK_H
+
+#include "exception.h"
+#include <fmt/core.h>
+#include <fstream>
+#include <iomanip>
+#include <iostream>
+#include <nlohmann/json.hpp>
+#include <string>
+
+// Bookkeeping of test data to store data of one or more tests in a json file to
+// facilitate future accounting.
+//
+// Usage Example 1 (single test):
+// ==============================
+// 1. define our test
+//      eic::util::Test test1{
+//        {{"name", "example_test"},
+//        {"title", "Example Test"},
+//        {"description", "This is an example of a test definition"},
+//        {"quantity", "efficiency"},
+//        {"target", "1"}}};
+// 2. set pass/fail/error status and return value (in this case .99)
+//      test1.pass(0.99)
+// 3. write our test data to a json file
+//      eic::util::write_test(test1, "test1.json");
+//
+// Usage Example 2 (multiple tests):
+// =================================
+// 1. define our tests
+//      eic::util::Test test1{
+//        {{"name", "example_test"},
+//        {"title", "Example Test"},
+//        {"description", "This is an example of a test definition"},
+//        {"quantity", "efficiency"},
+//        {"target", "1"}}};
+//      eic::util::Test test2{
+//        {{"name", "another_test"},
+//        {"title", "Another example Test"},
+//        {"description", "This is a second example of a test definition"},
+//        {"quantity", "resolution"},
+//        {"target", "3."}}};
+// 2. set pass/fail/error status and return value (in this case .99)
+//      test1.fail(10)
+// 3. write our test data to a json file
+//      eic::util::write_test({test1, test2}, "test.json");
+
+// Namespace for utility scripts, FIXME this should be part of an independent
+// library
+namespace eic::util {
+
+  struct TestDefinitionError : Exception {
+    TestDefinitionError(std::string_view msg) : Exception(msg, "test_definition_error") {}
+  };
+
+  // Wrapper for our test data json, with three methods to set the status
+  // after test completion (to pass, fail or error). The default value
+  // is error.
+  // The following fields should be defined in the definitions json
+  // for the test to make sense:
+  //  - name: unique identifier for this test
+  //  - title: Slightly more verbose identifier for this test
+  //  - description: Concise description of what is tested
+  //  - quantity: What quantity is tested? Unites of value/target
+  //  - target: Target value of <quantity> that we want to reach
+  //  - value: Actual value of <quantity>
+  //  - weight: Weight for this test (this is defaulted to 1.0 if not specified)
+  //  - result: pass/fail/error
+  struct Test {
+    // note: round braces for the json constructor, as else it will pick the wrong
+    //       initializer-list constructur (it will put everything in an array)
+    Test(const std::map<std::string, std::string>& definition) : json(definition)
+    {
+      // std::cout << json.dump() << std::endl;
+      // initialize with error (as we don't have a value yet)
+      error();
+      // Check that all required fields are present
+      for (const auto& field :
+           {"name", "title", "description", "quantity", "target", "value", "result"}) {
+        if (json.find(field) == json.end()) {
+          throw TestDefinitionError{
+              fmt::format("Error in test definition: field '{}' missing", field)};
+        }
+      }
+      // Default "weight" to 1 if not set
+      if (json.find("weight") == json.end()) {
+        json["weight"] = 1.0;
+      }
+    }
+    // Set this test to pass/fail/error
+    void pass(double value) { update_result("pass", value); }
+    void fail(double value) { update_result("fail", value); }
+    void error(double value = 0) { update_result("error", value); }
+
+    nlohmann::json json;
+
+  private:
+    void update_result(std::string_view status, double value)
+    {
+      json["result"] = status;
+      json["value"]  = value;
+    }
+  };
+
+  void write_test(const std::vector<Test>& data, const std::string& fname)
+  {
+    nlohmann::json test;
+    for (auto& entry : data) {
+      test["tests"].push_back(entry.json);
+    }
+    std::cout << fmt::format("Writing test data to {}\n", fname);
+    std::ofstream output_file(fname);
+    output_file << std::setw(4) << test << "\n";
+  }
+  void write_test(const Test& data, const std::string& fname)
+  {
+    std::vector<Test> vtd{data};
+    write_test(vtd, fname);
+  }
+
+} // namespace eic::util
+
+#endif
diff --git a/include/clipp.h b/include/clipp.h
new file mode 100644
index 0000000000000000000000000000000000000000..a2a0cf8bcb160ad9d0ca5d5132864f4f184f66de
--- /dev/null
+++ b/include/clipp.h
@@ -0,0 +1,6248 @@
+/*****************************************************************************
+ *
+ * CLIPP - command line interfaces for modern C++
+ *
+ * released under MIT license
+ *
+ * (c) 2017 André Müller; foss@andremueller-online.de
+ *
+ *****************************************************************************/
+
+#ifndef AM_CLIPP_H__
+#define AM_CLIPP_H__
+
+#include <cstring>
+#include <string>
+#include <cstdlib>
+#include <cstring>
+#include <cctype>
+#include <cmath>
+#include <memory>
+#include <vector>
+#include <limits>
+#include <stack>
+#include <algorithm>
+#include <sstream>
+#include <utility>
+#include <iterator>
+#include <functional>
+
+
+/*************************************************************************//**
+ *
+ * @brief primary namespace
+ *
+ *****************************************************************************/
+namespace clipp {
+
+
+
+/*****************************************************************************
+ *
+ * basic constants and datatype definitions
+ *
+ *****************************************************************************/
+using arg_index = int;
+
+using arg_string = std::string;
+using doc_string = std::string;
+
+using arg_list  = std::vector<arg_string>;
+
+
+
+/*************************************************************************//**
+ *
+ * @brief tristate
+ *
+ *****************************************************************************/
+enum class tri : char { no, yes, either };
+
+inline constexpr bool operator == (tri t, bool b) noexcept {
+    return b ? t != tri::no : t != tri::yes;
+}
+inline constexpr bool operator == (bool b, tri t) noexcept { return  (t == b); }
+inline constexpr bool operator != (tri t, bool b) noexcept { return !(t == b); }
+inline constexpr bool operator != (bool b, tri t) noexcept { return !(t == b); }
+
+
+
+/*************************************************************************//**
+ *
+ * @brief (start,size) index range
+ *
+ *****************************************************************************/
+class subrange {
+public:
+    using size_type = arg_string::size_type;
+
+    /** @brief default: no match */
+    explicit constexpr
+    subrange() noexcept :
+        at_{arg_string::npos}, length_{0}
+    {}
+
+    /** @brief match length & position within subject string */
+    explicit constexpr
+    subrange(size_type pos, size_type len) noexcept :
+        at_{pos}, length_{len}
+    {}
+
+    /** @brief position of the match within the subject string */
+    constexpr size_type at()     const noexcept { return at_; }
+    /** @brief length of the matching subsequence */
+    constexpr size_type length() const noexcept { return length_; }
+
+    /** @brief returns true, if query string is a prefix of the subject string */
+    constexpr bool prefix() const noexcept {
+        return at_ == 0 && length_ > 0;
+    }
+
+    /** @brief returns true, if query is a substring of the query string */
+    constexpr explicit operator bool () const noexcept {
+        return at_ != arg_string::npos && length_ > 0;
+    }
+
+private:
+    size_type at_;
+    size_type length_;
+};
+
+
+
+/*************************************************************************//**
+ *
+ * @brief match predicates
+ *
+ *****************************************************************************/
+using match_predicate = std::function<bool(const arg_string&)>;
+using match_function  = std::function<subrange(const arg_string&)>;
+
+
+
+
+
+
+/*************************************************************************//**
+ *
+ * @brief type traits (NOT FOR DIRECT USE IN CLIENT CODE!)
+ *        no interface guarantees; might be changed or removed in the future
+ *
+ *****************************************************************************/
+namespace traits {
+
+/*************************************************************************//**
+ *
+ * @brief function (class) signature type trait
+ *
+ *****************************************************************************/
+template<class Fn, class Ret, class... Args>
+constexpr auto
+check_is_callable(int) -> decltype(
+    std::declval<Fn>()(std::declval<Args>()...),
+    std::integral_constant<bool,
+        std::is_same<Ret,typename std::result_of<Fn(Args...)>::type>::value>{} );
+
+template<class,class,class...>
+constexpr auto
+check_is_callable(long) -> std::false_type;
+
+template<class Fn, class Ret>
+constexpr auto
+check_is_callable_without_arg(int) -> decltype(
+    std::declval<Fn>()(),
+    std::integral_constant<bool,
+        std::is_same<Ret,typename std::result_of<Fn()>::type>::value>{} );
+
+template<class,class>
+constexpr auto
+check_is_callable_without_arg(long) -> std::false_type;
+
+
+
+template<class Fn, class... Args>
+constexpr auto
+check_is_void_callable(int) -> decltype(
+    std::declval<Fn>()(std::declval<Args>()...), std::true_type{});
+
+template<class,class,class...>
+constexpr auto
+check_is_void_callable(long) -> std::false_type;
+
+template<class Fn>
+constexpr auto
+check_is_void_callable_without_arg(int) -> decltype(
+    std::declval<Fn>()(), std::true_type{});
+
+template<class>
+constexpr auto
+check_is_void_callable_without_arg(long) -> std::false_type;
+
+
+
+template<class Fn, class Ret>
+struct is_callable;
+
+
+template<class Fn, class Ret, class... Args>
+struct is_callable<Fn, Ret(Args...)> :
+    decltype(check_is_callable<Fn,Ret,Args...>(0))
+{};
+
+template<class Fn, class Ret>
+struct is_callable<Fn,Ret()> :
+    decltype(check_is_callable_without_arg<Fn,Ret>(0))
+{};
+
+
+template<class Fn, class... Args>
+struct is_callable<Fn, void(Args...)> :
+    decltype(check_is_void_callable<Fn,Args...>(0))
+{};
+
+template<class Fn>
+struct is_callable<Fn,void()> :
+    decltype(check_is_void_callable_without_arg<Fn>(0))
+{};
+
+
+
+/*************************************************************************//**
+ *
+ * @brief input range type trait
+ *
+ *****************************************************************************/
+template<class T>
+constexpr auto
+check_is_input_range(int) -> decltype(
+    begin(std::declval<T>()), end(std::declval<T>()),
+    std::true_type{});
+
+template<class T>
+constexpr auto
+check_is_input_range(char) -> decltype(
+    std::begin(std::declval<T>()), std::end(std::declval<T>()),
+    std::true_type{});
+
+template<class>
+constexpr auto
+check_is_input_range(long) -> std::false_type;
+
+template<class T>
+struct is_input_range :
+    decltype(check_is_input_range<T>(0))
+{};
+
+
+
+/*************************************************************************//**
+ *
+ * @brief size() member type trait
+ *
+ *****************************************************************************/
+template<class T>
+constexpr auto
+check_has_size_getter(int) ->
+    decltype(std::declval<T>().size(), std::true_type{});
+
+template<class>
+constexpr auto
+check_has_size_getter(long) -> std::false_type;
+
+template<class T>
+struct has_size_getter :
+    decltype(check_has_size_getter<T>(0))
+{};
+
+} // namespace traits
+
+
+
+
+
+
+/*************************************************************************//**
+ *
+ * @brief helpers (NOT FOR DIRECT USE IN CLIENT CODE!)
+ *        no interface guarantees; might be changed or removed in the future
+ *
+ *****************************************************************************/
+namespace detail {
+
+
+/*************************************************************************//**
+ * @brief forwards string to first non-whitespace char;
+ *        std string -> unsigned conv yields max value, but we want 0;
+ *        also checks for nullptr
+ *****************************************************************************/
+inline bool
+fwd_to_unsigned_int(const char*& s)
+{
+    if(!s) return false;
+    for(; std::isspace(*s); ++s);
+    if(!s[0] || s[0] == '-') return false;
+    if(s[0] == '-') return false;
+    return true;
+}
+
+
+/*************************************************************************//**
+ *
+ * @brief value limits clamping
+ *
+ *****************************************************************************/
+template<class T, class V, bool = (sizeof(V) > sizeof(T))>
+struct limits_clamped {
+    static T from(const V& v) {
+        if(v > V(std::numeric_limits<T>::max())) {
+            return std::numeric_limits<T>::max();
+        }
+        if(v < V(std::numeric_limits<T>::lowest())) {
+            return std::numeric_limits<T>::lowest();
+        }
+        return T(v);
+    }
+};
+
+template<class T, class V>
+struct limits_clamped<T,V,false> {
+    static T from(const V& v) { return T(v); }
+};
+
+
+/*************************************************************************//**
+ *
+ * @brief returns value of v as a T, clamped at T's maximum
+ *
+ *****************************************************************************/
+template<class T, class V>
+inline T clamped_on_limits(const V& v) {
+    return limits_clamped<T,V>::from(v);
+}
+
+
+
+
+/*************************************************************************//**
+ *
+ * @brief type conversion helpers
+ *
+ *****************************************************************************/
+template<class T>
+struct make;
+
+template<>
+struct make<bool> {
+    static inline bool from(const char* s) {
+        if(!s) return false;
+        return static_cast<bool>(s);
+    }
+};
+
+template<>
+struct make<unsigned char> {
+    static inline unsigned char from(const char* s) {
+        if(!fwd_to_unsigned_int(s)) return (0);
+        return clamped_on_limits<unsigned char>(std::strtoull(s,nullptr,10));
+    }
+};
+
+template<>
+struct make<unsigned short int> {
+    static inline unsigned short int from(const char* s) {
+        if(!fwd_to_unsigned_int(s)) return (0);
+        return clamped_on_limits<unsigned short int>(std::strtoull(s,nullptr,10));
+    }
+};
+
+template<>
+struct make<unsigned int> {
+    static inline unsigned int from(const char* s) {
+        if(!fwd_to_unsigned_int(s)) return (0);
+        return clamped_on_limits<unsigned int>(std::strtoull(s,nullptr,10));
+    }
+};
+
+template<>
+struct make<unsigned long int> {
+    static inline unsigned long int from(const char* s) {
+        if(!fwd_to_unsigned_int(s)) return (0);
+        return clamped_on_limits<unsigned long int>(std::strtoull(s,nullptr,10));
+    }
+};
+
+template<>
+struct make<unsigned long long int> {
+    static inline unsigned long long int from(const char* s) {
+        if(!fwd_to_unsigned_int(s)) return (0);
+        return clamped_on_limits<unsigned long long int>(std::strtoull(s,nullptr,10));
+    }
+};
+
+template<>
+struct make<char> {
+    static inline char from(const char* s) {
+        //parse as single character?
+        const auto n = std::strlen(s);
+        if(n == 1) return s[0];
+        //parse as integer
+        return clamped_on_limits<char>(std::strtoll(s,nullptr,10));
+    }
+};
+
+template<>
+struct make<short int> {
+    static inline short int from(const char* s) {
+        return clamped_on_limits<short int>(std::strtoll(s,nullptr,10));
+    }
+};
+
+template<>
+struct make<int> {
+    static inline int from(const char* s) {
+        return clamped_on_limits<int>(std::strtoll(s,nullptr,10));
+    }
+};
+
+template<>
+struct make<long int> {
+    static inline long int from(const char* s) {
+        return clamped_on_limits<long int>(std::strtoll(s,nullptr,10));
+    }
+};
+
+template<>
+struct make<long long int> {
+    static inline long long int from(const char* s) {
+        return (std::strtoll(s,nullptr,10));
+    }
+};
+
+template<>
+struct make<float> {
+    static inline float from(const char* s) {
+        return (std::strtof(s,nullptr));
+    }
+};
+
+template<>
+struct make<double> {
+    static inline double from(const char* s) {
+        return (std::strtod(s,nullptr));
+    }
+};
+
+template<>
+struct make<long double> {
+    static inline long double from(const char* s) {
+        return (std::strtold(s,nullptr));
+    }
+};
+
+template<>
+struct make<std::string> {
+    static inline std::string from(const char* s) {
+        return std::string(s);
+    }
+};
+
+
+
+/*************************************************************************//**
+ *
+ * @brief assigns boolean constant to one or multiple target objects
+ *
+ *****************************************************************************/
+template<class T, class V = T>
+class assign_value
+{
+public:
+    template<class X>
+    explicit constexpr
+    assign_value(T& target, X&& value) noexcept :
+        t_{std::addressof(target)}, v_{std::forward<X>(value)}
+    {}
+
+    void operator () () const {
+        if(t_) *t_ = v_;
+    }
+
+private:
+    T* t_;
+    V v_;
+};
+
+
+
+/*************************************************************************//**
+ *
+ * @brief flips bools
+ *
+ *****************************************************************************/
+class flip_bool
+{
+public:
+    explicit constexpr
+    flip_bool(bool& target) noexcept :
+        b_{&target}
+    {}
+
+    void operator () () const {
+        if(b_) *b_ = !*b_;
+    }
+
+private:
+    bool* b_;
+};
+
+
+
+/*************************************************************************//**
+ *
+ * @brief increments using operator ++
+ *
+ *****************************************************************************/
+template<class T>
+class increment
+{
+public:
+    explicit constexpr
+    increment(T& target) noexcept : t_{std::addressof(target)} {}
+
+    void operator () () const {
+        if(t_) ++(*t_);
+    }
+
+private:
+    T* t_;
+};
+
+
+
+/*************************************************************************//**
+ *
+ * @brief decrements using operator --
+ *
+ *****************************************************************************/
+template<class T>
+class decrement
+{
+public:
+    explicit constexpr
+    decrement(T& target) noexcept : t_{std::addressof(target)} {}
+
+    void operator () () const {
+        if(t_) --(*t_);
+    }
+
+private:
+    T* t_;
+};
+
+
+
+/*************************************************************************//**
+ *
+ * @brief increments by a fixed amount using operator +=
+ *
+ *****************************************************************************/
+template<class T>
+class increment_by
+{
+public:
+    explicit constexpr
+    increment_by(T& target, T by) noexcept :
+        t_{std::addressof(target)}, by_{std::move(by)}
+    {}
+
+    void operator () () const {
+        if(t_) (*t_) += by_;
+    }
+
+private:
+    T* t_;
+    T by_;
+};
+
+
+
+
+/*************************************************************************//**
+ *
+ * @brief makes a value from a string and assigns it to an object
+ *
+ *****************************************************************************/
+template<class T>
+class map_arg_to
+{
+public:
+    explicit constexpr
+    map_arg_to(T& target) noexcept : t_{std::addressof(target)} {}
+
+    void operator () (const char* s) const {
+        if(t_ && s && (std::strlen(s) > 0))
+            *t_ = detail::make<T>::from(s);
+    }
+
+private:
+    T* t_;
+};
+
+
+//-------------------------------------------------------------------
+/**
+ * @brief specialization for vectors: append element
+ */
+template<class T>
+class map_arg_to<std::vector<T>>
+{
+public:
+    map_arg_to(std::vector<T>& target): t_{std::addressof(target)} {}
+
+    void operator () (const char* s) const {
+        if(t_ && s) t_->push_back(detail::make<T>::from(s));
+    }
+
+private:
+    std::vector<T>* t_;
+};
+
+
+//-------------------------------------------------------------------
+/**
+ * @brief specialization for bools:
+ *        set to true regardless of string content
+ */
+template<>
+class map_arg_to<bool>
+{
+public:
+    map_arg_to(bool& target): t_{&target} {}
+
+    void operator () (const char* s) const {
+        if(t_ && s) *t_ = true;
+    }
+
+private:
+    bool* t_;
+};
+
+
+} // namespace detail
+
+
+
+
+
+
+/*************************************************************************//**
+ *
+ * @brief string matching and processing tools
+ *
+ *****************************************************************************/
+
+namespace str {
+
+
+/*************************************************************************//**
+ *
+ * @brief converts string to value of target type 'T'
+ *
+ *****************************************************************************/
+template<class T>
+T make(const arg_string& s)
+{
+    return detail::make<T>::from(s);
+}
+
+
+
+/*************************************************************************//**
+ *
+ * @brief removes trailing whitespace from string
+ *
+ *****************************************************************************/
+template<class C, class T, class A>
+inline void
+trimr(std::basic_string<C,T,A>& s)
+{
+    if(s.empty()) return;
+
+    s.erase(
+        std::find_if_not(s.rbegin(), s.rend(),
+                         [](char c) { return std::isspace(c);} ).base(),
+        s.end() );
+}
+
+
+/*************************************************************************//**
+ *
+ * @brief removes leading whitespace from string
+ *
+ *****************************************************************************/
+template<class C, class T, class A>
+inline void
+triml(std::basic_string<C,T,A>& s)
+{
+    if(s.empty()) return;
+
+    s.erase(
+        s.begin(),
+        std::find_if_not(s.begin(), s.end(),
+                         [](char c) { return std::isspace(c);})
+    );
+}
+
+
+/*************************************************************************//**
+ *
+ * @brief removes leading and trailing whitespace from string
+ *
+ *****************************************************************************/
+template<class C, class T, class A>
+inline void
+trim(std::basic_string<C,T,A>& s)
+{
+    triml(s);
+    trimr(s);
+}
+
+
+/*************************************************************************//**
+ *
+ * @brief removes all whitespaces from string
+ *
+ *****************************************************************************/
+template<class C, class T, class A>
+inline void
+remove_ws(std::basic_string<C,T,A>& s)
+{
+    if(s.empty()) return;
+
+    s.erase(std::remove_if(s.begin(), s.end(),
+                           [](char c) { return std::isspace(c); }),
+            s.end() );
+}
+
+
+/*************************************************************************//**
+ *
+ * @brief returns true, if the 'prefix' argument
+ *        is a prefix of the 'subject' argument
+ *
+ *****************************************************************************/
+template<class C, class T, class A>
+inline bool
+has_prefix(const std::basic_string<C,T,A>& subject,
+           const std::basic_string<C,T,A>& prefix)
+{
+    if(prefix.size() > subject.size()) return false;
+    return subject.find(prefix) == 0;
+}
+
+
+/*************************************************************************//**
+ *
+ * @brief returns true, if the 'postfix' argument
+ *        is a postfix of the 'subject' argument
+ *
+ *****************************************************************************/
+template<class C, class T, class A>
+inline bool
+has_postfix(const std::basic_string<C,T,A>& subject,
+            const std::basic_string<C,T,A>& postfix)
+{
+    if(postfix.size() > subject.size()) return false;
+    return (subject.size() - postfix.size()) == subject.find(postfix);
+}
+
+
+
+/*************************************************************************//**
+*
+* @brief   returns longest common prefix of several
+*          sequential random access containers
+*
+* @details InputRange require begin and end (member functions or overloads)
+*          the elements of InputRange require a size() member
+*
+*****************************************************************************/
+template<class InputRange>
+auto
+longest_common_prefix(const InputRange& strs)
+    -> typename std::decay<decltype(*begin(strs))>::type
+{
+    static_assert(traits::is_input_range<InputRange>(),
+        "parameter must satisfy the InputRange concept");
+
+    static_assert(traits::has_size_getter<
+        typename std::decay<decltype(*begin(strs))>::type>(),
+        "elements of input range must have a ::size() member function");
+
+    using std::begin;
+    using std::end;
+
+    using item_t = typename std::decay<decltype(*begin(strs))>::type;
+    using str_size_t = typename std::decay<decltype(begin(strs)->size())>::type;
+
+    const auto n = size_t(distance(begin(strs), end(strs)));
+    if(n < 1) return item_t("");
+    if(n == 1) return *begin(strs);
+
+    //length of shortest string
+    auto m = std::min_element(begin(strs), end(strs),
+                [](const item_t& a, const item_t& b) {
+                    return a.size() < b.size(); })->size();
+
+    //check each character until we find a mismatch
+    for(str_size_t i = 0; i < m; ++i) {
+        for(str_size_t j = 1; j < n; ++j) {
+            if(strs[j][i] != strs[j-1][i])
+                return strs[0].substr(0, i);
+        }
+    }
+    return strs[0].substr(0, m);
+}
+
+
+
+/*************************************************************************//**
+ *
+ * @brief  returns longest substring range that could be found in 'arg'
+ *
+ * @param  arg         string to be searched in
+ * @param  substrings  range of candidate substrings
+ *
+ *****************************************************************************/
+template<class C, class T, class A, class InputRange>
+subrange
+longest_substring_match(const std::basic_string<C,T,A>& arg,
+                        const InputRange& substrings)
+{
+    using string_t = std::basic_string<C,T,A>;
+
+    static_assert(traits::is_input_range<InputRange>(),
+        "parameter must satisfy the InputRange concept");
+
+    static_assert(std::is_same<string_t,
+        typename std::decay<decltype(*begin(substrings))>::type>(),
+        "substrings must have same type as 'arg'");
+
+    auto i = string_t::npos;
+    auto n = string_t::size_type(0);
+    for(const auto& s : substrings) {
+        auto j = arg.find(s);
+        if(j != string_t::npos && s.size() > n) {
+            i = j;
+            n = s.size();
+        }
+    }
+    return subrange{i,n};
+}
+
+
+
+/*************************************************************************//**
+ *
+ * @brief  returns longest prefix range that could be found in 'arg'
+ *
+ * @param  arg       string to be searched in
+ * @param  prefixes  range of candidate prefix strings
+ *
+ *****************************************************************************/
+template<class C, class T, class A, class InputRange>
+subrange
+longest_prefix_match(const std::basic_string<C,T,A>& arg,
+                     const InputRange& prefixes)
+{
+    using string_t = std::basic_string<C,T,A>;
+    using s_size_t = typename string_t::size_type;
+
+    static_assert(traits::is_input_range<InputRange>(),
+        "parameter must satisfy the InputRange concept");
+
+    static_assert(std::is_same<string_t,
+        typename std::decay<decltype(*begin(prefixes))>::type>(),
+        "prefixes must have same type as 'arg'");
+
+    auto i = string_t::npos;
+    auto n = s_size_t(0);
+    for(const auto& s : prefixes) {
+        auto j = arg.find(s);
+        if(j == 0 && s.size() > n) {
+            i = 0;
+            n = s.size();
+        }
+    }
+    return subrange{i,n};
+}
+
+
+
+/*************************************************************************//**
+ *
+ * @brief returns the first occurrence of 'query' within 'subject'
+ *
+ *****************************************************************************/
+template<class C, class T, class A>
+inline subrange
+substring_match(const std::basic_string<C,T,A>& subject,
+                const std::basic_string<C,T,A>& query)
+{
+    if(subject.empty() || query.empty()) return subrange{};
+    auto i = subject.find(query);
+    if(i == std::basic_string<C,T,A>::npos) return subrange{};
+    return subrange{i,query.size()};
+}
+
+
+
+/*************************************************************************//**
+ *
+ * @brief returns first substring match (pos,len) within the input string
+ *        that represents a number
+ *        (with at maximum one decimal point and digit separators)
+ *
+ *****************************************************************************/
+template<class C, class T, class A>
+subrange
+first_number_match(std::basic_string<C,T,A> s,
+                   C digitSeparator = C(','),
+                   C decimalPoint = C('.'),
+                   C exponential = C('e'))
+{
+    using string_t = std::basic_string<C,T,A>;
+
+    str::trim(s);
+    if(s.empty()) return subrange{};
+
+    auto i = s.find_first_of("0123456789+-");
+    if(i == string_t::npos) {
+        i = s.find(decimalPoint);
+        if(i == string_t::npos) return subrange{};
+    }
+
+    bool point = false;
+    bool sep = false;
+    auto exp = string_t::npos;
+    auto j = i + 1;
+    for(; j < s.size(); ++j) {
+        if(s[j] == digitSeparator) {
+            if(!sep) sep = true; else break;
+        }
+        else {
+            sep = false;
+            if(s[j] == decimalPoint) {
+                //only one decimal point before exponent allowed
+                if(!point && exp == string_t::npos) point = true; else break;
+            }
+            else if(std::tolower(s[j]) == std::tolower(exponential)) {
+                //only one exponent separator allowed
+                if(exp == string_t::npos) exp = j; else break;
+            }
+            else if(exp != string_t::npos && (exp+1) == j) {
+                //only sign or digit after exponent separator
+                if(s[j] != '+' && s[j] != '-' && !std::isdigit(s[j])) break;
+            }
+            else if(!std::isdigit(s[j])) {
+                break;
+            }
+        }
+    }
+
+    //if length == 1 then must be a digit
+    if(j-i == 1 && !std::isdigit(s[i])) return subrange{};
+
+    return subrange{i,j-i};
+}
+
+
+
+/*************************************************************************//**
+ *
+ * @brief returns first substring match (pos,len)
+ *        that represents an integer (with optional digit separators)
+ *
+ *****************************************************************************/
+template<class C, class T, class A>
+subrange
+first_integer_match(std::basic_string<C,T,A> s,
+                    C digitSeparator = C(','))
+{
+    using string_t = std::basic_string<C,T,A>;
+
+    str::trim(s);
+    if(s.empty()) return subrange{};
+
+    auto i = s.find_first_of("0123456789+-");
+    if(i == string_t::npos) return subrange{};
+
+    bool sep = false;
+    auto j = i + 1;
+    for(; j < s.size(); ++j) {
+        if(s[j] == digitSeparator) {
+            if(!sep) sep = true; else break;
+        }
+        else {
+            sep = false;
+            if(!std::isdigit(s[j])) break;
+        }
+    }
+
+    //if length == 1 then must be a digit
+    if(j-i == 1 && !std::isdigit(s[i])) return subrange{};
+
+    return subrange{i,j-i};
+}
+
+
+
+/*************************************************************************//**
+ *
+ * @brief returns true if candidate string represents a number
+ *
+ *****************************************************************************/
+template<class C, class T, class A>
+bool represents_number(const std::basic_string<C,T,A>& candidate,
+                   C digitSeparator = C(','),
+                   C decimalPoint = C('.'),
+                   C exponential = C('e'))
+{
+    const auto match = str::first_number_match(candidate, digitSeparator,
+                                               decimalPoint, exponential);
+
+    return (match && match.length() == candidate.size());
+}
+
+
+
+/*************************************************************************//**
+ *
+ * @brief returns true if candidate string represents an integer
+ *
+ *****************************************************************************/
+template<class C, class T, class A>
+bool represents_integer(const std::basic_string<C,T,A>& candidate,
+                        C digitSeparator = C(','))
+{
+    const auto match = str::first_integer_match(candidate, digitSeparator);
+    return (match && match.length() == candidate.size());
+}
+
+} // namespace str
+
+
+
+
+
+
+/*************************************************************************//**
+ *
+ * @brief makes function object with a const char* parameter
+ *        that assigns a value to a ref-captured object
+ *
+ *****************************************************************************/
+template<class T, class V>
+inline detail::assign_value<T,V>
+set(T& target, V value) {
+    return detail::assign_value<T>{target, std::move(value)};
+}
+
+
+
+/*************************************************************************//**
+ *
+ * @brief makes parameter-less function object
+ *        that assigns value(s) to a ref-captured object;
+ *        value(s) are obtained by converting the const char* argument to
+ *        the captured object types;
+ *        bools are always set to true if the argument is not nullptr
+ *
+ *****************************************************************************/
+template<class T>
+inline detail::map_arg_to<T>
+set(T& target) {
+    return detail::map_arg_to<T>{target};
+}
+
+
+
+/*************************************************************************//**
+ *
+ * @brief makes function object that sets a bool to true
+ *
+ *****************************************************************************/
+inline detail::assign_value<bool>
+set(bool& target) {
+    return detail::assign_value<bool>{target,true};
+}
+
+/*************************************************************************//**
+ *
+ * @brief makes function object that sets a bool to false
+ *
+ *****************************************************************************/
+inline detail::assign_value<bool>
+unset(bool& target) {
+    return detail::assign_value<bool>{target,false};
+}
+
+/*************************************************************************//**
+ *
+ * @brief makes function object that flips the value of a ref-captured bool
+ *
+ *****************************************************************************/
+inline detail::flip_bool
+flip(bool& b) {
+    return detail::flip_bool(b);
+}
+
+
+
+
+
+/*************************************************************************//**
+ *
+ * @brief makes function object that increments using operator ++
+ *
+ *****************************************************************************/
+template<class T>
+inline detail::increment<T>
+increment(T& target) {
+    return detail::increment<T>{target};
+}
+
+/*************************************************************************//**
+ *
+ * @brief makes function object that decrements using operator --
+ *
+ *****************************************************************************/
+template<class T>
+inline detail::increment_by<T>
+increment(T& target, T by) {
+    return detail::increment_by<T>{target, std::move(by)};
+}
+
+/*************************************************************************//**
+ *
+ * @brief makes function object that increments by a fixed amount using operator +=
+ *
+ *****************************************************************************/
+template<class T>
+inline detail::decrement<T>
+decrement(T& target) {
+    return detail::decrement<T>{target};
+}
+
+
+
+
+
+
+/*************************************************************************//**
+ *
+ * @brief helpers (NOT FOR DIRECT USE IN CLIENT CODE!)
+ *
+ *****************************************************************************/
+namespace detail {
+
+
+/*************************************************************************//**
+ *
+ * @brief mixin that provides action definition and execution
+ *
+ *****************************************************************************/
+template<class Derived>
+class action_provider
+{
+private:
+    //---------------------------------------------------------------
+    using simple_action = std::function<void()>;
+    using arg_action    = std::function<void(const char*)>;
+    using index_action  = std::function<void(int)>;
+
+    //-----------------------------------------------------
+    class simple_action_adapter {
+    public:
+        simple_action_adapter() = default;
+        simple_action_adapter(const simple_action& a): action_(a) {}
+        simple_action_adapter(simple_action&& a): action_(std::move(a)) {}
+        void operator() (const char*) const { action_(); }
+        void operator() (int) const { action_(); }
+    private:
+        simple_action action_;
+    };
+
+
+public:
+    //---------------------------------------------------------------
+    /** @brief adds an action that has an operator() that is callable
+     *         with a 'const char*' argument */
+    Derived&
+    call(arg_action a) {
+        argActions_.push_back(std::move(a));
+        return *static_cast<Derived*>(this);
+    }
+
+    /** @brief adds an action that has an operator()() */
+    Derived&
+    call(simple_action a) {
+        argActions_.push_back(simple_action_adapter(std::move(a)));
+        return *static_cast<Derived*>(this);
+    }
+
+    /** @brief adds an action that has an operator() that is callable
+     *         with a 'const char*' argument */
+    Derived& operator () (arg_action a)    { return call(std::move(a)); }
+
+    /** @brief adds an action that has an operator()() */
+    Derived& operator () (simple_action a) { return call(std::move(a)); }
+
+
+    //---------------------------------------------------------------
+    /** @brief adds an action that will set the value of 't' from
+     *         a 'const char*' arg */
+    template<class Target>
+    Derived&
+    set(Target& t) {
+        return call(clipp::set(t));
+    }
+
+    /** @brief adds an action that will set the value of 't' to 'v' */
+    template<class Target, class Value>
+    Derived&
+    set(Target& t, Value&& v) {
+        return call(clipp::set(t, std::forward<Value>(v)));
+    }
+
+
+    //---------------------------------------------------------------
+    /** @brief adds an action that will be called if a parameter
+     *         matches an argument for the 2nd, 3rd, 4th, ... time
+     */
+    Derived&
+    if_repeated(simple_action a) {
+        repeatActions_.push_back(simple_action_adapter{std::move(a)});
+        return *static_cast<Derived*>(this);
+    }
+    /** @brief adds an action that will be called with the argument's
+     *         index if a parameter matches an argument for
+     *         the 2nd, 3rd, 4th, ... time
+     */
+    Derived&
+    if_repeated(index_action a) {
+        repeatActions_.push_back(std::move(a));
+        return *static_cast<Derived*>(this);
+    }
+
+
+    //---------------------------------------------------------------
+    /** @brief adds an action that will be called if a required parameter
+     *         is missing
+     */
+    Derived&
+    if_missing(simple_action a) {
+        missingActions_.push_back(simple_action_adapter{std::move(a)});
+        return *static_cast<Derived*>(this);
+    }
+    /** @brief adds an action that will be called if a required parameter
+     *         is missing; the action will get called with the index of
+     *         the command line argument where the missing event occured first
+     */
+    Derived&
+    if_missing(index_action a) {
+        missingActions_.push_back(std::move(a));
+        return *static_cast<Derived*>(this);
+    }
+
+
+    //---------------------------------------------------------------
+    /** @brief adds an action that will be called if a parameter
+     *         was matched, but was unreachable in the current scope
+     */
+    Derived&
+    if_blocked(simple_action a) {
+        blockedActions_.push_back(simple_action_adapter{std::move(a)});
+        return *static_cast<Derived*>(this);
+    }
+    /** @brief adds an action that will be called if a parameter
+     *         was matched, but was unreachable in the current scope;
+     *         the action will be called with the index of
+     *         the command line argument where the problem occured
+     */
+    Derived&
+    if_blocked(index_action a) {
+        blockedActions_.push_back(std::move(a));
+        return *static_cast<Derived*>(this);
+    }
+
+
+    //---------------------------------------------------------------
+    /** @brief adds an action that will be called if a parameter match
+     *         was in conflict with a different alternative parameter
+     */
+    Derived&
+    if_conflicted(simple_action a) {
+        conflictActions_.push_back(simple_action_adapter{std::move(a)});
+        return *static_cast<Derived*>(this);
+    }
+    /** @brief adds an action that will be called if a parameter match
+     *         was in conflict with a different alternative paramete;
+     *         the action will be called with the index of
+     *         the command line argument where the problem occuredr
+     */
+    Derived&
+    if_conflicted(index_action a) {
+        conflictActions_.push_back(std::move(a));
+        return *static_cast<Derived*>(this);
+    }
+
+
+    //---------------------------------------------------------------
+    /** @brief adds targets = either objects whose values should be
+     *         set by command line arguments or actions that should
+     *         be called in case of a match */
+    template<class T, class... Ts>
+    Derived&
+    target(T&& t, Ts&&... ts) {
+        target(std::forward<T>(t));
+        target(std::forward<Ts>(ts)...);
+        return *static_cast<Derived*>(this);
+    }
+
+    /** @brief adds action that should be called in case of a match */
+    template<class T, class = typename std::enable_if<
+            !std::is_fundamental<typename std::decay<T>::type>() &&
+            (traits::is_callable<T,void()>() ||
+             traits::is_callable<T,void(const char*)>() )
+        >::type>
+    Derived&
+    target(T&& t) {
+        call(std::forward<T>(t));
+        return *static_cast<Derived*>(this);
+    }
+
+    /** @brief adds object whose value should be set by command line arguments
+     */
+    template<class T, class = typename std::enable_if<
+            std::is_fundamental<typename std::decay<T>::type>() ||
+            (!traits::is_callable<T,void()>() &&
+             !traits::is_callable<T,void(const char*)>() )
+        >::type>
+    Derived&
+    target(T& t) {
+        set(t);
+        return *static_cast<Derived*>(this);
+    }
+
+    //TODO remove ugly empty param list overload
+    Derived&
+    target() {
+        return *static_cast<Derived*>(this);
+    }
+
+
+    //---------------------------------------------------------------
+    /** @brief adds target, see member function 'target' */
+    template<class Target>
+    inline friend Derived&
+    operator << (Target&& t, Derived& p) {
+        p.target(std::forward<Target>(t));
+        return p;
+    }
+    /** @brief adds target, see member function 'target' */
+    template<class Target>
+    inline friend Derived&&
+    operator << (Target&& t, Derived&& p) {
+        p.target(std::forward<Target>(t));
+        return std::move(p);
+    }
+
+    //-----------------------------------------------------
+    /** @brief adds target, see member function 'target' */
+    template<class Target>
+    inline friend Derived&
+    operator >> (Derived& p, Target&& t) {
+        p.target(std::forward<Target>(t));
+        return p;
+    }
+    /** @brief adds target, see member function 'target' */
+    template<class Target>
+    inline friend Derived&&
+    operator >> (Derived&& p, Target&& t) {
+        p.target(std::forward<Target>(t));
+        return std::move(p);
+    }
+
+
+    //---------------------------------------------------------------
+    /** @brief executes all argument actions */
+    void execute_actions(const arg_string& arg) const {
+        int i = 0;
+        for(const auto& a : argActions_) {
+            ++i;
+            a(arg.c_str());
+        }
+    }
+
+    /** @brief executes repeat actions */
+    void notify_repeated(arg_index idx) const {
+        for(const auto& a : repeatActions_) a(idx);
+    }
+    /** @brief executes missing error actions */
+    void notify_missing(arg_index idx) const {
+        for(const auto& a : missingActions_) a(idx);
+    }
+    /** @brief executes blocked error actions */
+    void notify_blocked(arg_index idx) const {
+        for(const auto& a : blockedActions_) a(idx);
+    }
+    /** @brief executes conflict error actions */
+    void notify_conflict(arg_index idx) const {
+        for(const auto& a : conflictActions_) a(idx);
+    }
+
+private:
+    //---------------------------------------------------------------
+    std::vector<arg_action> argActions_;
+    std::vector<index_action> repeatActions_;
+    std::vector<index_action> missingActions_;
+    std::vector<index_action> blockedActions_;
+    std::vector<index_action> conflictActions_;
+};
+
+
+
+
+
+
+/*************************************************************************//**
+ *
+ * @brief mixin that provides basic common settings of parameters and groups
+ *
+ *****************************************************************************/
+template<class Derived>
+class token
+{
+public:
+    //---------------------------------------------------------------
+    using doc_string = clipp::doc_string;
+
+
+    //---------------------------------------------------------------
+    /** @brief returns documentation string */
+    const doc_string& doc() const noexcept {
+        return doc_;
+    }
+
+    /** @brief sets documentations string */
+    Derived& doc(const doc_string& txt) {
+        doc_ = txt;
+        return *static_cast<Derived*>(this);
+    }
+
+    /** @brief sets documentations string */
+    Derived& doc(doc_string&& txt) {
+        doc_ = std::move(txt);
+        return *static_cast<Derived*>(this);
+    }
+
+
+    //---------------------------------------------------------------
+    /** @brief returns if a group/parameter is repeatable */
+    bool repeatable() const noexcept {
+        return repeatable_;
+    }
+
+    /** @brief sets repeatability of group/parameter */
+    Derived& repeatable(bool yes) noexcept {
+        repeatable_ = yes;
+        return *static_cast<Derived*>(this);
+    }
+
+
+    //---------------------------------------------------------------
+    /** @brief returns if a group/parameter is blocking/positional */
+    bool blocking() const noexcept {
+        return blocking_;
+    }
+
+    /** @brief determines, if a group/parameter is blocking/positional */
+    Derived& blocking(bool yes) noexcept {
+        blocking_ = yes;
+        return *static_cast<Derived*>(this);
+    }
+
+
+private:
+    //---------------------------------------------------------------
+    doc_string doc_;
+    bool repeatable_ = false;
+    bool blocking_ = false;
+};
+
+
+
+
+/*************************************************************************//**
+ *
+ * @brief sets documentation strings on a token
+ *
+ *****************************************************************************/
+template<class T>
+inline T&
+operator % (doc_string docstr, token<T>& p)
+{
+    return p.doc(std::move(docstr));
+}
+//---------------------------------------------------------
+template<class T>
+inline T&&
+operator % (doc_string docstr, token<T>&& p)
+{
+    return std::move(p.doc(std::move(docstr)));
+}
+
+//---------------------------------------------------------
+template<class T>
+inline T&
+operator % (token<T>& p, doc_string docstr)
+{
+    return p.doc(std::move(docstr));
+}
+//---------------------------------------------------------
+template<class T>
+inline T&&
+operator % (token<T>&& p, doc_string docstr)
+{
+    return std::move(p.doc(std::move(docstr)));
+}
+
+
+
+
+/*************************************************************************//**
+ *
+ * @brief sets documentation strings on a token
+ *
+ *****************************************************************************/
+template<class T>
+inline T&
+doc(doc_string docstr, token<T>& p)
+{
+    return p.doc(std::move(docstr));
+}
+//---------------------------------------------------------
+template<class T>
+inline T&&
+doc(doc_string docstr, token<T>&& p)
+{
+    return std::move(p.doc(std::move(docstr)));
+}
+
+
+
+} // namespace detail
+
+
+
+/*************************************************************************//**
+ *
+ * @brief contains parameter matching functions and function classes
+ *
+ *****************************************************************************/
+namespace match {
+
+
+/*************************************************************************//**
+ *
+ * @brief predicate that is always true
+ *
+ *****************************************************************************/
+inline bool
+any(const arg_string&) { return true; }
+
+/*************************************************************************//**
+ *
+ * @brief predicate that is always false
+ *
+ *****************************************************************************/
+inline bool
+none(const arg_string&) { return false; }
+
+
+
+/*************************************************************************//**
+ *
+ * @brief predicate that returns true if the argument string is non-empty string
+ *
+ *****************************************************************************/
+inline bool
+nonempty(const arg_string& s) {
+    return !s.empty();
+}
+
+
+
+/*************************************************************************//**
+ *
+ * @brief predicate that returns true if the argument is a non-empty
+ *        string that consists only of alphanumeric characters
+ *
+ *****************************************************************************/
+inline bool
+alphanumeric(const arg_string& s) {
+    if(s.empty()) return false;
+    return std::all_of(s.begin(), s.end(), [](char c) {return std::isalnum(c); });
+}
+
+
+
+/*************************************************************************//**
+ *
+ * @brief predicate that returns true if the argument is a non-empty
+ *        string that consists only of alphabetic characters
+ *
+ *****************************************************************************/
+inline bool
+alphabetic(const arg_string& s) {
+    return std::all_of(s.begin(), s.end(), [](char c) {return std::isalpha(c); });
+}
+
+
+
+/*************************************************************************//**
+ *
+ * @brief predicate that returns the first substring match within the input
+ *        string that rmeepresents a number
+ *        (with at maximum one decimal point and digit separators)
+ *
+ *****************************************************************************/
+class numbers
+{
+public:
+    explicit
+    numbers(char decimalPoint = '.',
+            char digitSeparator = ' ',
+            char exponentSeparator = 'e')
+    :
+        decpoint_{decimalPoint}, separator_{digitSeparator},
+        exp_{exponentSeparator}
+    {}
+
+    subrange operator () (const arg_string& s) const {
+        return str::first_number_match(s, separator_, decpoint_, exp_);
+    }
+
+private:
+    char decpoint_;
+    char separator_;
+    char exp_;
+};
+
+
+
+/*************************************************************************//**
+ *
+ * @brief predicate that returns true if the input string represents an integer
+ *        (with optional digit separators)
+ *
+ *****************************************************************************/
+class integers {
+public:
+    explicit
+    integers(char digitSeparator = ' '): separator_{digitSeparator} {}
+
+    subrange operator () (const arg_string& s) const {
+        return str::first_integer_match(s, separator_);
+    }
+
+private:
+    char separator_;
+};
+
+
+
+/*************************************************************************//**
+ *
+ * @brief predicate that returns true if the input string represents
+ *        a non-negative integer (with optional digit separators)
+ *
+ *****************************************************************************/
+class positive_integers {
+public:
+    explicit
+    positive_integers(char digitSeparator = ' '): separator_{digitSeparator} {}
+
+    subrange operator () (const arg_string& s) const {
+        auto match = str::first_integer_match(s, separator_);
+        if(!match) return subrange{};
+        if(s[match.at()] == '-') return subrange{};
+        return match;
+    }
+
+private:
+    char separator_;
+};
+
+
+
+/*************************************************************************//**
+ *
+ * @brief predicate that returns true if the input string
+ *        contains a given substring
+ *
+ *****************************************************************************/
+class substring
+{
+public:
+    explicit
+    substring(arg_string str): str_{std::move(str)} {}
+
+    subrange operator () (const arg_string& s) const {
+        return str::substring_match(s, str_);
+    }
+
+private:
+    arg_string str_;
+};
+
+
+
+/*************************************************************************//**
+ *
+ * @brief predicate that returns true if the input string starts
+ *        with a given prefix
+ *
+ *****************************************************************************/
+class prefix {
+public:
+    explicit
+    prefix(arg_string p): prefix_{std::move(p)} {}
+
+    bool operator () (const arg_string& s) const {
+        return s.find(prefix_) == 0;
+    }
+
+private:
+    arg_string prefix_;
+};
+
+
+
+/*************************************************************************//**
+ *
+ * @brief predicate that returns true if the input string does not start
+ *        with a given prefix
+ *
+ *****************************************************************************/
+class prefix_not {
+public:
+    explicit
+    prefix_not(arg_string p): prefix_{std::move(p)} {}
+
+    bool operator () (const arg_string& s) const {
+        return s.find(prefix_) != 0;
+    }
+
+private:
+    arg_string prefix_;
+};
+
+
+/** @brief alias for prefix_not */
+using noprefix = prefix_not;
+
+
+
+/*************************************************************************//**
+ *
+ * @brief predicate that returns true if the length of the input string
+ *        is wihtin a given interval
+ *
+ *****************************************************************************/
+class length {
+public:
+    explicit
+    length(std::size_t exact):
+        min_{exact}, max_{exact}
+    {}
+
+    explicit
+    length(std::size_t min, std::size_t max):
+        min_{min}, max_{max}
+    {}
+
+    bool operator () (const arg_string& s) const {
+        return s.size() >= min_ && s.size() <= max_;
+    }
+
+private:
+    std::size_t min_;
+    std::size_t max_;
+};
+
+
+/*************************************************************************//**
+ *
+ * @brief makes function object that returns true if the input string has a
+ *        given minimum length
+ *
+ *****************************************************************************/
+inline length min_length(std::size_t min)
+{
+    return length{min, arg_string::npos-1};
+}
+
+/*************************************************************************//**
+ *
+ * @brief makes function object that returns true if the input string is
+ *        not longer than a given maximum length
+ *
+ *****************************************************************************/
+inline length max_length(std::size_t max)
+{
+    return length{0, max};
+}
+
+
+} // namespace match
+
+
+
+
+
+/*************************************************************************//**
+ *
+ * @brief command line parameter that can match one or many arguments.
+ *
+ *****************************************************************************/
+class parameter :
+    public detail::token<parameter>,
+    public detail::action_provider<parameter>
+{
+    class predicate_adapter {
+    public:
+        explicit
+        predicate_adapter(match_predicate pred): match_{std::move(pred)} {}
+
+        subrange operator () (const arg_string& arg) const {
+            return match_(arg) ? subrange{0,arg.size()} : subrange{};
+        }
+
+    private:
+        match_predicate match_;
+    };
+
+public:
+    //---------------------------------------------------------------
+    /** @brief makes default parameter, that will match nothing */
+    parameter():
+        flags_{},
+        matcher_{predicate_adapter{match::none}},
+        label_{}, required_{false}
+    {}
+
+    /** @brief makes "flag" parameter */
+    template<class... Strings>
+    explicit
+    parameter(arg_string str, Strings&&... strs):
+        flags_{},
+        matcher_{predicate_adapter{match::none}},
+        label_{}, required_{false}
+    {
+        add_flags(std::move(str), std::forward<Strings>(strs)...);
+    }
+
+    /** @brief makes "flag" parameter from range of strings */
+    explicit
+    parameter(const arg_list& flaglist):
+        flags_{},
+        matcher_{predicate_adapter{match::none}},
+        label_{}, required_{false}
+    {
+        add_flags(flaglist);
+    }
+
+    //-----------------------------------------------------
+    /** @brief makes "value" parameter with custom match predicate
+     *         (= yes/no matcher)
+     */
+    explicit
+    parameter(match_predicate filter):
+        flags_{},
+        matcher_{predicate_adapter{std::move(filter)}},
+        label_{}, required_{false}
+    {}
+
+    /** @brief makes "value" parameter with custom match function
+     *         (= partial matcher)
+     */
+    explicit
+    parameter(match_function filter):
+        flags_{},
+        matcher_{std::move(filter)},
+        label_{}, required_{false}
+    {}
+
+
+    //---------------------------------------------------------------
+    /** @brief returns if a parameter is required */
+    bool
+    required() const noexcept {
+        return required_;
+    }
+
+    /** @brief determines if a parameter is required */
+    parameter&
+    required(bool yes) noexcept {
+        required_ = yes;
+        return *this;
+    }
+
+
+    //---------------------------------------------------------------
+    /** @brief returns parameter label;
+     *         will be used for documentation, if flags are empty
+     */
+    const doc_string&
+    label() const {
+        return label_;
+    }
+
+    /** @brief sets parameter label;
+     *         will be used for documentation, if flags are empty
+     */
+    parameter&
+    label(const doc_string& lbl) {
+        label_ = lbl;
+        return *this;
+    }
+
+    /** @brief sets parameter label;
+     *         will be used for documentation, if flags are empty
+     */
+    parameter&
+    label(doc_string&& lbl) {
+        label_ = lbl;
+        return *this;
+    }
+
+
+    //---------------------------------------------------------------
+    /** @brief returns either longest matching prefix of 'arg' in any
+     *         of the flags or the result of the custom match operation
+     */
+    subrange
+    match(const arg_string& arg) const
+    {
+        if(arg.empty()) return subrange{};
+
+        if(flags_.empty()) {
+            return matcher_(arg);
+        }
+        else {
+            if(std::find(flags_.begin(), flags_.end(), arg) != flags_.end()) {
+                return subrange{0,arg.size()};
+            }
+            return str::longest_prefix_match(arg, flags_);
+        }
+    }
+
+
+    //---------------------------------------------------------------
+    /** @brief access range of flag strings */
+    const arg_list&
+    flags() const noexcept {
+        return flags_;
+    }
+
+    /** @brief access custom match operation */
+    const match_function&
+    matcher() const noexcept {
+        return matcher_;
+    }
+
+
+    //---------------------------------------------------------------
+    /** @brief prepend prefix to each flag */
+    inline friend parameter&
+    with_prefix(const arg_string& prefix, parameter& p)
+    {
+        if(prefix.empty() || p.flags().empty()) return p;
+
+        for(auto& f : p.flags_) {
+            if(f.find(prefix) != 0) f.insert(0, prefix);
+        }
+        return p;
+    }
+
+
+    /** @brief prepend prefix to each flag
+     */
+    inline friend parameter&
+    with_prefixes_short_long(
+        const arg_string& shortpfx, const arg_string& longpfx,
+        parameter& p)
+    {
+        if(shortpfx.empty() && longpfx.empty()) return p;
+        if(p.flags().empty()) return p;
+
+        for(auto& f : p.flags_) {
+            if(f.size() == 1) {
+                if(f.find(shortpfx) != 0) f.insert(0, shortpfx);
+            } else {
+                if(f.find(longpfx) != 0) f.insert(0, longpfx);
+            }
+        }
+        return p;
+    }
+
+private:
+    //---------------------------------------------------------------
+    void add_flags(arg_string str) {
+        //empty flags are not allowed
+        str::remove_ws(str);
+        if(!str.empty()) flags_.push_back(std::move(str));
+    }
+
+    //---------------------------------------------------------------
+    void add_flags(const arg_list& strs) {
+        if(strs.empty()) return;
+        flags_.reserve(flags_.size() + strs.size());
+        for(const auto& s : strs) add_flags(s);
+    }
+
+    template<class String1, class String2, class... Strings>
+    void
+    add_flags(String1&& s1, String2&& s2, Strings&&... ss) {
+        flags_.reserve(2 + sizeof...(ss));
+        add_flags(std::forward<String1>(s1));
+        add_flags(std::forward<String2>(s2), std::forward<Strings>(ss)...);
+    }
+
+    arg_list flags_;
+    match_function matcher_;
+    doc_string label_;
+    bool required_ = false;
+};
+
+
+
+
+/*************************************************************************//**
+ *
+ * @brief makes required non-blocking exact match parameter
+ *
+ *****************************************************************************/
+template<class String, class... Strings>
+inline parameter
+command(String&& flag, Strings&&... flags)
+{
+    return parameter{std::forward<String>(flag), std::forward<Strings>(flags)...}
+        .required(true).blocking(true).repeatable(false);
+}
+
+
+
+/*************************************************************************//**
+ *
+ * @brief makes required non-blocking exact match parameter
+ *
+ *****************************************************************************/
+template<class String, class... Strings>
+inline parameter
+required(String&& flag, Strings&&... flags)
+{
+    return parameter{std::forward<String>(flag), std::forward<Strings>(flags)...}
+        .required(true).blocking(false).repeatable(false);
+}
+
+
+
+/*************************************************************************//**
+ *
+ * @brief makes optional, non-blocking exact match parameter
+ *
+ *****************************************************************************/
+template<class String, class... Strings>
+inline parameter
+option(String&& flag, Strings&&... flags)
+{
+    return parameter{std::forward<String>(flag), std::forward<Strings>(flags)...}
+        .required(false).blocking(false).repeatable(false);
+}
+
+
+
+/*************************************************************************//**
+ *
+ * @brief makes required, blocking, repeatable value parameter;
+ *        matches any non-empty string
+ *
+ *****************************************************************************/
+template<class... Targets>
+inline parameter
+value(const doc_string& label, Targets&&... tgts)
+{
+    return parameter{match::nonempty}
+        .label(label)
+        .target(std::forward<Targets>(tgts)...)
+        .required(true).blocking(true).repeatable(false);
+}
+
+template<class Filter, class... Targets, class = typename std::enable_if<
+    traits::is_callable<Filter,bool(const char*)>::value ||
+    traits::is_callable<Filter,subrange(const char*)>::value>::type>
+inline parameter
+value(Filter&& filter, doc_string label, Targets&&... tgts)
+{
+    return parameter{std::forward<Filter>(filter)}
+        .label(label)
+        .target(std::forward<Targets>(tgts)...)
+        .required(true).blocking(true).repeatable(false);
+}
+
+
+
+/*************************************************************************//**
+ *
+ * @brief makes required, blocking, repeatable value parameter;
+ *        matches any non-empty string
+ *
+ *****************************************************************************/
+template<class... Targets>
+inline parameter
+values(const doc_string& label, Targets&&... tgts)
+{
+    return parameter{match::nonempty}
+        .label(label)
+        .target(std::forward<Targets>(tgts)...)
+        .required(true).blocking(true).repeatable(true);
+}
+
+template<class Filter, class... Targets, class = typename std::enable_if<
+    traits::is_callable<Filter,bool(const char*)>::value ||
+    traits::is_callable<Filter,subrange(const char*)>::value>::type>
+inline parameter
+values(Filter&& filter, doc_string label, Targets&&... tgts)
+{
+    return parameter{std::forward<Filter>(filter)}
+        .label(label)
+        .target(std::forward<Targets>(tgts)...)
+        .required(true).blocking(true).repeatable(true);
+}
+
+
+
+/*************************************************************************//**
+ *
+ * @brief makes optional, blocking value parameter;
+ *        matches any non-empty string
+ *
+ *****************************************************************************/
+template<class... Targets>
+inline parameter
+opt_value(const doc_string& label, Targets&&... tgts)
+{
+    return parameter{match::nonempty}
+        .label(label)
+        .target(std::forward<Targets>(tgts)...)
+        .required(false).blocking(false).repeatable(false);
+}
+
+template<class Filter, class... Targets, class = typename std::enable_if<
+    traits::is_callable<Filter,bool(const char*)>::value ||
+    traits::is_callable<Filter,subrange(const char*)>::value>::type>
+inline parameter
+opt_value(Filter&& filter, doc_string label, Targets&&... tgts)
+{
+    return parameter{std::forward<Filter>(filter)}
+        .label(label)
+        .target(std::forward<Targets>(tgts)...)
+        .required(false).blocking(false).repeatable(false);
+}
+
+
+
+/*************************************************************************//**
+ *
+ * @brief makes optional, blocking, repeatable value parameter;
+ *        matches any non-empty string
+ *
+ *****************************************************************************/
+template<class... Targets>
+inline parameter
+opt_values(const doc_string& label, Targets&&... tgts)
+{
+    return parameter{match::nonempty}
+        .label(label)
+        .target(std::forward<Targets>(tgts)...)
+        .required(false).blocking(false).repeatable(true);
+}
+
+template<class Filter, class... Targets, class = typename std::enable_if<
+    traits::is_callable<Filter,bool(const char*)>::value ||
+    traits::is_callable<Filter,subrange(const char*)>::value>::type>
+inline parameter
+opt_values(Filter&& filter, doc_string label, Targets&&... tgts)
+{
+    return parameter{std::forward<Filter>(filter)}
+        .label(label)
+        .target(std::forward<Targets>(tgts)...)
+        .required(false).blocking(false).repeatable(true);
+}
+
+
+
+/*************************************************************************//**
+ *
+ * @brief makes required, blocking value parameter;
+ *        matches any string consisting of alphanumeric characters
+ *
+ *****************************************************************************/
+template<class... Targets>
+inline parameter
+word(const doc_string& label, Targets&&... tgts)
+{
+    return parameter{match::alphanumeric}
+        .label(label)
+        .target(std::forward<Targets>(tgts)...)
+        .required(true).blocking(true).repeatable(false);
+}
+
+
+
+/*************************************************************************//**
+ *
+ * @brief makes required, blocking, repeatable value parameter;
+ *        matches any string consisting of alphanumeric characters
+ *
+ *****************************************************************************/
+template<class... Targets>
+inline parameter
+words(const doc_string& label, Targets&&... tgts)
+{
+    return parameter{match::alphanumeric}
+        .label(label)
+        .target(std::forward<Targets>(tgts)...)
+        .required(true).blocking(true).repeatable(true);
+}
+
+
+
+/*************************************************************************//**
+ *
+ * @brief makes optional, blocking value parameter;
+ *        matches any string consisting of alphanumeric characters
+ *
+ *****************************************************************************/
+template<class... Targets>
+inline parameter
+opt_word(const doc_string& label, Targets&&... tgts)
+{
+    return parameter{match::alphanumeric}
+        .label(label)
+        .target(std::forward<Targets>(tgts)...)
+        .required(false).blocking(false).repeatable(false);
+}
+
+
+
+/*************************************************************************//**
+ *
+ * @brief makes optional, blocking, repeatable value parameter;
+ *        matches any string consisting of alphanumeric characters
+ *
+ *****************************************************************************/
+template<class... Targets>
+inline parameter
+opt_words(const doc_string& label, Targets&&... tgts)
+{
+    return parameter{match::alphanumeric}
+        .label(label)
+        .target(std::forward<Targets>(tgts)...)
+        .required(false).blocking(false).repeatable(true);
+}
+
+
+
+/*************************************************************************//**
+ *
+ * @brief makes required, blocking value parameter;
+ *        matches any string that represents a number
+ *
+ *****************************************************************************/
+template<class... Targets>
+inline parameter
+number(const doc_string& label, Targets&&... tgts)
+{
+    return parameter{match::numbers{}}
+        .label(label)
+        .target(std::forward<Targets>(tgts)...)
+        .required(true).blocking(true).repeatable(false);
+}
+
+
+
+/*************************************************************************//**
+ *
+ * @brief makes required, blocking, repeatable value parameter;
+ *        matches any string that represents a number
+ *
+ *****************************************************************************/
+template<class... Targets>
+inline parameter
+numbers(const doc_string& label, Targets&&... tgts)
+{
+    return parameter{match::numbers{}}
+        .label(label)
+        .target(std::forward<Targets>(tgts)...)
+        .required(true).blocking(true).repeatable(true);
+}
+
+
+
+/*************************************************************************//**
+ *
+ * @brief makes optional, blocking value parameter;
+ *        matches any string that represents a number
+ *
+ *****************************************************************************/
+template<class... Targets>
+inline parameter
+opt_number(const doc_string& label, Targets&&... tgts)
+{
+    return parameter{match::numbers{}}
+        .label(label)
+        .target(std::forward<Targets>(tgts)...)
+        .required(false).blocking(false).repeatable(false);
+}
+
+
+
+/*************************************************************************//**
+ *
+ * @brief makes optional, blocking, repeatable value parameter;
+ *        matches any string that represents a number
+ *
+ *****************************************************************************/
+template<class... Targets>
+inline parameter
+opt_numbers(const doc_string& label, Targets&&... tgts)
+{
+    return parameter{match::numbers{}}
+        .label(label)
+        .target(std::forward<Targets>(tgts)...)
+        .required(false).blocking(false).repeatable(true);
+}
+
+
+
+/*************************************************************************//**
+ *
+ * @brief makes required, blocking value parameter;
+ *        matches any string that represents an integer
+ *
+ *****************************************************************************/
+template<class... Targets>
+inline parameter
+integer(const doc_string& label, Targets&&... tgts)
+{
+    return parameter{match::integers{}}
+        .label(label)
+        .target(std::forward<Targets>(tgts)...)
+        .required(true).blocking(true).repeatable(false);
+}
+
+
+
+/*************************************************************************//**
+ *
+ * @brief makes required, blocking, repeatable value parameter;
+ *        matches any string that represents an integer
+ *
+ *****************************************************************************/
+template<class... Targets>
+inline parameter
+integers(const doc_string& label, Targets&&... tgts)
+{
+    return parameter{match::integers{}}
+        .label(label)
+        .target(std::forward<Targets>(tgts)...)
+        .required(true).blocking(true).repeatable(true);
+}
+
+
+
+/*************************************************************************//**
+ *
+ * @brief makes optional, blocking value parameter;
+ *        matches any string that represents an integer
+ *
+ *****************************************************************************/
+template<class... Targets>
+inline parameter
+opt_integer(const doc_string& label, Targets&&... tgts)
+{
+    return parameter{match::integers{}}
+        .label(label)
+        .target(std::forward<Targets>(tgts)...)
+        .required(false).blocking(false).repeatable(false);
+}
+
+
+
+/*************************************************************************//**
+ *
+ * @brief makes optional, blocking, repeatable value parameter;
+ *        matches any string that represents an integer
+ *
+ *****************************************************************************/
+template<class... Targets>
+inline parameter
+opt_integers(const doc_string& label, Targets&&... tgts)
+{
+    return parameter{match::integers{}}
+        .label(label)
+        .target(std::forward<Targets>(tgts)...)
+        .required(false).blocking(false).repeatable(true);
+}
+
+
+
+/*************************************************************************//**
+ *
+ * @brief makes catch-all value parameter
+ *
+ *****************************************************************************/
+template<class... Targets>
+inline parameter
+any_other(Targets&&... tgts)
+{
+    return parameter{match::any}
+        .target(std::forward<Targets>(tgts)...)
+        .required(false).blocking(false).repeatable(true);
+}
+
+
+
+
+/*************************************************************************//**
+ *
+ * @brief group of parameters and/or other groups;
+ *        can be configured to act as a group of alternatives (exclusive match)
+ *
+ *****************************************************************************/
+class group :
+    public detail::token<group>
+{
+    //---------------------------------------------------------------
+    /**
+        * @brief tagged union type that either stores a parameter or a group
+        *        and provides a common interface to them
+        *        could be replaced by std::variant in the future
+        *
+        *        Note to future self: do NOT try again to do this with
+        *        dynamic polymorphism; there are a couple of
+        *        nasty problems associated with it and the implementation
+        *        becomes bloated and needlessly complicated.
+        */
+    template<class Param, class Group>
+    struct child_t {
+        enum class type : char {param, group};
+    public:
+
+        explicit
+        child_t(const Param&  v)          : m_{v},            type_{type::param} {}
+        child_t(      Param&& v) noexcept : m_{std::move(v)}, type_{type::param} {}
+
+        explicit
+        child_t(const Group&  g)          : m_{g},            type_{type::group} {}
+        child_t(      Group&& g) noexcept : m_{std::move(g)}, type_{type::group} {}
+
+        child_t(const child_t& src): type_{src.type_} {
+            switch(type_) {
+                default:
+                case type::param: new(&m_)data{src.m_.param}; break;
+                case type::group: new(&m_)data{src.m_.group}; break;
+            }
+        }
+
+        child_t(child_t&& src) noexcept : type_{src.type_} {
+            switch(type_) {
+                default:
+                case type::param: new(&m_)data{std::move(src.m_.param)}; break;
+                case type::group: new(&m_)data{std::move(src.m_.group)}; break;
+            }
+        }
+
+        child_t& operator = (const child_t& src) {
+            destroy_content();
+            type_ = src.type_;
+            switch(type_) {
+                default:
+                case type::param: new(&m_)data{src.m_.param}; break;
+                case type::group: new(&m_)data{src.m_.group}; break;
+            }
+            return *this;
+        }
+
+        child_t& operator = (child_t&& src) noexcept {
+            destroy_content();
+            type_ = src.type_;
+            switch(type_) {
+                default:
+                case type::param: new(&m_)data{std::move(src.m_.param)}; break;
+                case type::group: new(&m_)data{std::move(src.m_.group)}; break;
+            }
+            return *this;
+        }
+
+        ~child_t() {
+            destroy_content();
+        }
+
+        const doc_string&
+        doc() const noexcept {
+            switch(type_) {
+                default:
+                case type::param: return m_.param.doc();
+                case type::group: return m_.group.doc();
+            }
+        }
+
+        bool blocking() const noexcept {
+            switch(type_) {
+                case type::param: return m_.param.blocking();
+                case type::group: return m_.group.blocking();
+                default: return false;
+            }
+        }
+        bool repeatable() const noexcept {
+            switch(type_) {
+                case type::param: return m_.param.repeatable();
+                case type::group: return m_.group.repeatable();
+                default: return false;
+            }
+        }
+        bool required() const noexcept {
+            switch(type_) {
+                case type::param: return m_.param.required();
+                case type::group:
+                    return (m_.group.exclusive() && m_.group.all_required() ) ||
+                          (!m_.group.exclusive() && m_.group.any_required()  );
+                default: return false;
+            }
+        }
+        bool exclusive() const noexcept {
+            switch(type_) {
+                case type::group: return m_.group.exclusive();
+                case type::param:
+                default: return false;
+            }
+        }
+        std::size_t param_count() const noexcept {
+            switch(type_) {
+                case type::group: return m_.group.param_count();
+                case type::param:
+                default: return std::size_t(1);
+            }
+        }
+        std::size_t depth() const noexcept {
+            switch(type_) {
+                case type::group: return m_.group.depth();
+                case type::param:
+                default: return std::size_t(0);
+            }
+        }
+
+        void execute_actions(const arg_string& arg) const {
+            switch(type_) {
+                default:
+                case type::group: return;
+                case type::param: m_.param.execute_actions(arg); break;
+            }
+
+        }
+
+        void notify_repeated(arg_index idx) const {
+            switch(type_) {
+                default:
+                case type::group: return;
+                case type::param: m_.param.notify_repeated(idx); break;
+            }
+        }
+        void notify_missing(arg_index idx) const {
+            switch(type_) {
+                default:
+                case type::group: return;
+                case type::param: m_.param.notify_missing(idx); break;
+            }
+        }
+        void notify_blocked(arg_index idx) const {
+            switch(type_) {
+                default:
+                case type::group: return;
+                case type::param: m_.param.notify_blocked(idx); break;
+            }
+        }
+        void notify_conflict(arg_index idx) const {
+            switch(type_) {
+                default:
+                case type::group: return;
+                case type::param: m_.param.notify_conflict(idx); break;
+            }
+        }
+
+        bool is_param() const noexcept { return type_ == type::param; }
+        bool is_group() const noexcept { return type_ == type::group; }
+
+        Param& as_param() noexcept { return m_.param; }
+        Group& as_group() noexcept { return m_.group; }
+
+        const Param& as_param() const noexcept { return m_.param; }
+        const Group& as_group() const noexcept { return m_.group; }
+
+    private:
+        void destroy_content() {
+            switch(type_) {
+                default:
+                case type::param: m_.param.~Param(); break;
+                case type::group: m_.group.~Group(); break;
+            }
+        }
+
+        union data {
+            data() {}
+
+            data(const Param&  v)          : param{v} {}
+            data(      Param&& v) noexcept : param{std::move(v)} {}
+
+            data(const Group&  g)          : group{g} {}
+            data(      Group&& g) noexcept : group{std::move(g)} {}
+            ~data() {}
+
+            Param param;
+            Group group;
+        };
+
+        data m_;
+        type type_;
+    };
+
+
+public:
+    //---------------------------------------------------------------
+    using child      = child_t<parameter,group>;
+    using value_type = child;
+
+private:
+    using children_store = std::vector<child>;
+
+public:
+    using const_iterator = children_store::const_iterator;
+    using iterator       = children_store::iterator;
+    using size_type      = children_store::size_type;
+
+
+    //---------------------------------------------------------------
+    /**
+     * @brief recursively iterates over all nodes
+     */
+    class depth_first_traverser
+    {
+    public:
+        //-----------------------------------------------------
+        struct context {
+            context() = default;
+            context(const group& p):
+                parent{&p}, cur{p.begin()}, end{p.end()}
+            {}
+            const group* parent = nullptr;
+            const_iterator cur;
+            const_iterator end;
+        };
+        using context_list = std::vector<context>;
+
+        //-----------------------------------------------------
+        class memento {
+            friend class depth_first_traverser;
+            int level_;
+            context context_;
+        public:
+            int level() const noexcept { return level_; }
+            const child* param() const noexcept { return &(*context_.cur); }
+        };
+
+        depth_first_traverser() = default;
+
+        explicit
+        depth_first_traverser(const group& cur): stack_{} {
+            if(!cur.empty()) stack_.emplace_back(cur);
+        }
+
+        explicit operator bool() const noexcept {
+            return !stack_.empty();
+        }
+
+        int level() const noexcept {
+            return int(stack_.size());
+        }
+
+        bool is_first_in_group() const noexcept {
+            if(stack_.empty()) return false;
+            return (stack_.back().cur == stack_.back().parent->begin());
+        }
+
+        bool is_last_in_group() const noexcept {
+            if(stack_.empty()) return false;
+            return (stack_.back().cur+1 == stack_.back().end);
+        }
+
+        bool is_last_in_path() const noexcept {
+            if(stack_.empty()) return false;
+            for(const auto& t : stack_) {
+                if(t.cur+1 != t.end) return false;
+            }
+            const auto& top = stack_.back();
+            //if we have to descend into group on next ++ => not last in path
+            if(top.cur->is_group()) return false;
+            return true;
+        }
+
+        /** @brief inside a group of alternatives >= minlevel */
+        bool is_alternative(int minlevel = 0) const noexcept {
+            if(stack_.empty()) return false;
+            if(minlevel > 0) minlevel -= 1;
+            if(minlevel >= int(stack_.size())) return false;
+            return std::any_of(stack_.begin() + minlevel, stack_.end(),
+                [](const context& c) { return c.parent->exclusive(); });
+        }
+
+        /** @brief repeatable or inside a repeatable group >= minlevel */
+        bool is_repeatable(int minlevel = 0) const noexcept {
+            if(stack_.empty()) return false;
+            if(stack_.back().cur->repeatable()) return true;
+            if(minlevel > 0) minlevel -= 1;
+            if(minlevel >= int(stack_.size())) return false;
+            return std::any_of(stack_.begin() + minlevel, stack_.end(),
+                [](const context& c) { return c.parent->repeatable(); });
+        }
+        /** @brief inside group with joinable flags */
+        bool joinable() const noexcept {
+            if(stack_.empty()) return false;
+            return std::any_of(stack_.begin(), stack_.end(),
+                [](const context& c) { return c.parent->joinable(); });
+        }
+
+        const context_list&
+        stack() const {
+            return stack_;
+        }
+
+        /** @brief innermost repeat group */
+        const group*
+        repeat_group() const noexcept {
+            auto i = std::find_if(stack_.rbegin(), stack_.rend(),
+                [](const context& c) { return c.parent->repeatable(); });
+
+            return i != stack_.rend() ? i->parent : nullptr;
+        }
+
+        /** @brief outermost join group */
+        const group*
+        join_group() const noexcept {
+            auto i = std::find_if(stack_.begin(), stack_.end(),
+                [](const context& c) { return c.parent->joinable(); });
+            return i != stack_.end() ? i->parent : nullptr;
+        }
+
+        const group* root() const noexcept {
+            return stack_.empty() ? nullptr : stack_.front().parent;
+        }
+
+        /** @brief common flag prefix of all flags in current group */
+        arg_string common_flag_prefix() const noexcept {
+            if(stack_.empty()) return "";
+            auto g = join_group();
+            return g ? g->common_flag_prefix() : arg_string("");
+        }
+
+        const child&
+        operator * () const noexcept {
+            return *stack_.back().cur;
+        }
+
+        const child*
+        operator -> () const noexcept {
+            return &(*stack_.back().cur);
+        }
+
+        const group&
+        parent() const noexcept {
+            return *(stack_.back().parent);
+        }
+
+
+        /** @brief go to next element of depth first search */
+        depth_first_traverser&
+        operator ++ () {
+            if(stack_.empty()) return *this;
+            //at group -> decend into group
+            if(stack_.back().cur->is_group()) {
+                stack_.emplace_back(stack_.back().cur->as_group());
+            }
+            else {
+                next_sibling();
+            }
+            return *this;
+        }
+
+        /** @brief go to next sibling of current */
+        depth_first_traverser&
+        next_sibling() {
+            if(stack_.empty()) return *this;
+            ++stack_.back().cur;
+            //at the end of current group?
+            while(stack_.back().cur == stack_.back().end) {
+                //go to parent
+                stack_.pop_back();
+                if(stack_.empty()) return *this;
+                //go to next sibling in parent
+                ++stack_.back().cur;
+            }
+            return *this;
+        }
+
+        /** @brief go to next position after siblings of current */
+        depth_first_traverser&
+        next_after_siblings() {
+            if(stack_.empty()) return *this;
+            stack_.back().cur = stack_.back().end-1;
+            next_sibling();
+            return *this;
+        }
+
+        /** @brief skips to next alternative in innermost group
+        */
+        depth_first_traverser&
+        next_alternative() {
+            if(stack_.empty()) return *this;
+
+            //find first exclusive group (from the top of the stack!)
+            auto i = std::find_if(stack_.rbegin(), stack_.rend(),
+                [](const context& c) { return c.parent->exclusive(); });
+            if(i == stack_.rend()) return *this;
+
+            stack_.erase(i.base(), stack_.end());
+            next_sibling();
+            return *this;
+        }
+
+        /**
+         * @brief
+         */
+        depth_first_traverser&
+        back_to_parent() {
+            if(stack_.empty()) return *this;
+            stack_.pop_back();
+            return *this;
+        }
+
+        /** @brief don't visit next siblings, go back to parent on next ++
+         *         note: renders siblings unreachable for *this
+         **/
+        depth_first_traverser&
+        skip_siblings() {
+            if(stack_.empty()) return *this;
+            //future increments won't visit subsequent siblings:
+            stack_.back().end = stack_.back().cur+1;
+            return *this;
+        }
+
+        /** @brief skips all other alternatives in surrounding exclusive groups
+         *         on next ++
+         *         note: renders alternatives unreachable for *this
+        */
+        depth_first_traverser&
+        skip_alternatives() {
+            if(stack_.empty()) return *this;
+
+            //exclude all other alternatives in surrounding groups
+            //by making their current position the last one
+            for(auto& c : stack_) {
+                if(c.parent && c.parent->exclusive() && c.cur < c.end)
+                    c.end = c.cur+1;
+            }
+
+            return *this;
+        }
+
+        void invalidate() {
+            stack_.clear();
+        }
+
+        inline friend bool operator == (const depth_first_traverser& a,
+                                        const depth_first_traverser& b)
+        {
+            if(a.stack_.empty() || b.stack_.empty()) return false;
+
+            //parents not the same -> different position
+            if(a.stack_.back().parent != b.stack_.back().parent) return false;
+
+            bool aEnd = a.stack_.back().cur == a.stack_.back().end;
+            bool bEnd = b.stack_.back().cur == b.stack_.back().end;
+            //either both at the end of the same parent => same position
+            if(aEnd && bEnd) return true;
+            //or only one at the end => not at the same position
+            if(aEnd || bEnd) return false;
+            return std::addressof(*a.stack_.back().cur) ==
+                   std::addressof(*b.stack_.back().cur);
+        }
+        inline friend bool operator != (const depth_first_traverser& a,
+                                        const depth_first_traverser& b)
+        {
+            return !(a == b);
+        }
+
+        memento
+        undo_point() const {
+            memento m;
+            m.level_ = int(stack_.size());
+            if(!stack_.empty()) m.context_ = stack_.back();
+            return m;
+        }
+
+        void undo(const memento& m) {
+            if(m.level_ < 1) return;
+            if(m.level_ <= int(stack_.size())) {
+                stack_.erase(stack_.begin() + m.level_, stack_.end());
+                stack_.back() = m.context_;
+            }
+            else if(stack_.empty() && m.level_ == 1) {
+                stack_.push_back(m.context_);
+            }
+        }
+
+    private:
+        context_list stack_;
+    };
+
+
+    //---------------------------------------------------------------
+    group() = default;
+
+    template<class Param, class... Params>
+    explicit
+    group(doc_string docstr, Param param, Params... params):
+        children_{}, exclusive_{false}, joinable_{false}, scoped_{true}
+    {
+        doc(std::move(docstr));
+        push_back(std::move(param), std::move(params)...);
+    }
+
+    template<class... Params>
+    explicit
+    group(parameter param, Params... params):
+        children_{}, exclusive_{false}, joinable_{false}, scoped_{true}
+    {
+        push_back(std::move(param), std::move(params)...);
+    }
+
+    template<class P2, class... Ps>
+    explicit
+    group(group p1, P2 p2, Ps... ps):
+        children_{}, exclusive_{false}, joinable_{false}, scoped_{true}
+    {
+        push_back(std::move(p1), std::move(p2), std::move(ps)...);
+    }
+
+
+    //-----------------------------------------------------
+    group(const group&) = default;
+    group(group&&) = default;
+
+
+    //---------------------------------------------------------------
+    group& operator = (const group&) = default;
+    group& operator = (group&&) = default;
+
+
+    //---------------------------------------------------------------
+    /** @brief determines if a command line argument can be matched by a
+     *         combination of (partial) matches through any number of children
+     */
+    group& joinable(bool yes) {
+        joinable_ = yes;
+        return *this;
+    }
+
+    /** @brief returns if a command line argument can be matched by a
+     *         combination of (partial) matches through any number of children
+     */
+    bool joinable() const noexcept {
+        return joinable_;
+    }
+
+
+    //---------------------------------------------------------------
+    /** @brief turns explicit scoping on or off
+     *         operators , & | and other combinating functions will
+     *         not merge groups that are marked as scoped
+     */
+    group& scoped(bool yes) {
+        scoped_ = yes;
+        return *this;
+    }
+
+    /** @brief returns true if operators , & | and other combinating functions
+     *         will merge groups and false otherwise
+     */
+    bool scoped() const noexcept
+    {
+        return scoped_;
+    }
+
+
+    //---------------------------------------------------------------
+    /** @brief determines if children are mutually exclusive alternatives */
+    group& exclusive(bool yes) {
+        exclusive_ = yes;
+        return *this;
+    }
+    /** @brief returns if children are mutually exclusive alternatives */
+    bool exclusive() const noexcept {
+        return exclusive_;
+    }
+
+
+    //---------------------------------------------------------------
+    /** @brief returns true, if any child is required to match */
+    bool any_required() const
+    {
+        return std::any_of(children_.begin(), children_.end(),
+            [](const child& n){ return n.required(); });
+    }
+    /** @brief returns true, if all children are required to match */
+    bool all_required() const
+    {
+        return std::all_of(children_.begin(), children_.end(),
+            [](const child& n){ return n.required(); });
+    }
+
+
+    //---------------------------------------------------------------
+    /** @brief returns true if any child is optional (=non-required) */
+    bool any_optional() const {
+        return !all_required();
+    }
+    /** @brief returns true if all children are optional (=non-required) */
+    bool all_optional() const {
+        return !any_required();
+    }
+
+
+    //---------------------------------------------------------------
+    /** @brief returns if the entire group is blocking / positional */
+    bool blocking() const noexcept {
+        return token<group>::blocking() || (exclusive() && all_blocking());
+    }
+    //-----------------------------------------------------
+    /** @brief determines if the entire group is blocking / positional */
+    group& blocking(bool yes) {
+        return token<group>::blocking(yes);
+    }
+
+    //---------------------------------------------------------------
+    /** @brief returns true if any child is blocking */
+    bool any_blocking() const
+    {
+        return std::any_of(children_.begin(), children_.end(),
+            [](const child& n){ return n.blocking(); });
+    }
+    //---------------------------------------------------------------
+    /** @brief returns true if all children is blocking */
+    bool all_blocking() const
+    {
+        return std::all_of(children_.begin(), children_.end(),
+            [](const child& n){ return n.blocking(); });
+    }
+
+
+    //---------------------------------------------------------------
+    /** @brief returns if any child is a value parameter (recursive) */
+    bool any_flagless() const
+    {
+        return std::any_of(children_.begin(), children_.end(),
+            [](const child& p){
+                return p.is_param() && p.as_param().flags().empty();
+            });
+    }
+    /** @brief returns if all children are value parameters (recursive) */
+    bool all_flagless() const
+    {
+        return std::all_of(children_.begin(), children_.end(),
+            [](const child& p){
+                return p.is_param() && p.as_param().flags().empty();
+            });
+    }
+
+
+    //---------------------------------------------------------------
+    /** @brief adds child parameter at the end */
+    group&
+    push_back(const parameter& v) {
+        children_.emplace_back(v);
+        return *this;
+    }
+    //-----------------------------------------------------
+    /** @brief adds child parameter at the end */
+    group&
+    push_back(parameter&& v) {
+        children_.emplace_back(std::move(v));
+        return *this;
+    }
+    //-----------------------------------------------------
+    /** @brief adds child group at the end */
+    group&
+    push_back(const group& g) {
+        children_.emplace_back(g);
+        return *this;
+    }
+    //-----------------------------------------------------
+    /** @brief adds child group at the end */
+    group&
+    push_back(group&& g) {
+        children_.emplace_back(std::move(g));
+        return *this;
+    }
+
+
+    //-----------------------------------------------------
+    /** @brief adds children (groups and/or parameters) */
+    template<class Param1, class Param2, class... Params>
+    group&
+    push_back(Param1&& param1, Param2&& param2, Params&&... params)
+    {
+        children_.reserve(children_.size() + 2 + sizeof...(params));
+        push_back(std::forward<Param1>(param1));
+        push_back(std::forward<Param2>(param2), std::forward<Params>(params)...);
+        return *this;
+    }
+
+
+    //---------------------------------------------------------------
+    /** @brief adds child parameter at the beginning */
+    group&
+    push_front(const parameter& v) {
+        children_.emplace(children_.begin(), v);
+        return *this;
+    }
+    //-----------------------------------------------------
+    /** @brief adds child parameter at the beginning */
+    group&
+    push_front(parameter&& v) {
+        children_.emplace(children_.begin(), std::move(v));
+        return *this;
+    }
+    //-----------------------------------------------------
+    /** @brief adds child group at the beginning */
+    group&
+    push_front(const group& g) {
+        children_.emplace(children_.begin(), g);
+        return *this;
+    }
+    //-----------------------------------------------------
+    /** @brief adds child group at the beginning */
+    group&
+    push_front(group&& g) {
+        children_.emplace(children_.begin(), std::move(g));
+        return *this;
+    }
+
+
+    //---------------------------------------------------------------
+    /** @brief adds all children of other group at the end */
+    group&
+    merge(group&& g)
+    {
+        children_.insert(children_.end(),
+                      std::make_move_iterator(g.begin()),
+                      std::make_move_iterator(g.end()));
+        return *this;
+    }
+    //-----------------------------------------------------
+    /** @brief adds all children of several other groups at the end */
+    template<class... Groups>
+    group&
+    merge(group&& g1, group&& g2, Groups&&... gs)
+    {
+        merge(std::move(g1));
+        merge(std::move(g2), std::forward<Groups>(gs)...);
+        return *this;
+    }
+
+
+    //---------------------------------------------------------------
+    /** @brief indexed, nutable access to child */
+    child& operator [] (size_type index) noexcept {
+        return children_[index];
+    }
+    /** @brief indexed, non-nutable access to child */
+    const child& operator [] (size_type index) const noexcept {
+        return children_[index];
+    }
+
+    //---------------------------------------------------------------
+    /** @brief mutable access to first child */
+          child& front()       noexcept { return children_.front(); }
+    /** @brief non-mutable access to first child */
+    const child& front() const noexcept { return children_.front(); }
+    //-----------------------------------------------------
+    /** @brief mutable access to last child */
+          child& back()       noexcept { return children_.back(); }
+    /** @brief non-mutable access to last child */
+    const child& back() const noexcept { return children_.back(); }
+
+
+    //---------------------------------------------------------------
+    /** @brief returns true, if group has no children, false otherwise */
+    bool empty() const noexcept     { return children_.empty(); }
+
+    /** @brief returns number of children */
+    size_type size() const noexcept { return children_.size(); }
+
+    /** @brief returns number of nested levels; 1 for a flat group */
+    size_type depth() const {
+        size_type n = 0;
+        for(const auto& c : children_) {
+            auto l = 1 + c.depth();
+            if(l > n) n = l;
+        }
+        return n;
+    }
+
+
+    //---------------------------------------------------------------
+    /** @brief returns mutating iterator to position of first element */
+          iterator  begin()       noexcept { return children_.begin(); }
+    /** @brief returns non-mutating iterator to position of first element */
+    const_iterator  begin() const noexcept { return children_.begin(); }
+    /** @brief returns non-mutating iterator to position of first element */
+    const_iterator cbegin() const noexcept { return children_.begin(); }
+
+    /** @brief returns mutating iterator to position one past the last element */
+          iterator  end()         noexcept { return children_.end(); }
+    /** @brief returns non-mutating iterator to position one past the last element */
+    const_iterator  end()   const noexcept { return children_.end(); }
+    /** @brief returns non-mutating iterator to position one past the last element */
+    const_iterator cend()   const noexcept { return children_.end(); }
+
+
+    //---------------------------------------------------------------
+    /** @brief returns augmented iterator for depth first searches
+     *  @details taverser knows end of iteration and can skip over children
+     */
+    depth_first_traverser
+    begin_dfs() const noexcept {
+        return depth_first_traverser{*this};
+    }
+
+
+    //---------------------------------------------------------------
+    /** @brief returns recursive parameter count */
+    size_type param_count() const {
+        size_type c = 0;
+        for(const auto& n : children_) {
+            c += n.param_count();
+        }
+        return c;
+    }
+
+
+    //---------------------------------------------------------------
+    /** @brief returns range of all flags (recursive) */
+    arg_list all_flags() const
+    {
+        std::vector<arg_string> all;
+        gather_flags(children_, all);
+        return all;
+    }
+
+    /** @brief returns true, if no flag occurs as true
+     *         prefix of any other flag (identical flags will be ignored) */
+    bool flags_are_prefix_free() const
+    {
+        const auto fs = all_flags();
+
+        using std::begin; using std::end;
+        for(auto i = begin(fs), e = end(fs); i != e; ++i) {
+            if(!i->empty()) {
+                for(auto j = i+1; j != e; ++j) {
+                    if(!j->empty() && *i != *j) {
+                        if(i->find(*j) == 0) return false;
+                        if(j->find(*i) == 0) return false;
+                    }
+                }
+            }
+        }
+
+        return true;
+    }
+
+
+    //---------------------------------------------------------------
+    /** @brief returns longest common prefix of all flags */
+    arg_string common_flag_prefix() const
+    {
+        arg_list prefixes;
+        gather_prefixes(children_, prefixes);
+        return str::longest_common_prefix(prefixes);
+    }
+
+
+private:
+    //---------------------------------------------------------------
+    static void
+    gather_flags(const children_store& nodes, arg_list& all)
+    {
+        for(const auto& p : nodes) {
+            if(p.is_group()) {
+                gather_flags(p.as_group().children_, all);
+            }
+            else {
+                const auto& pf = p.as_param().flags();
+                using std::begin;
+                using std::end;
+                if(!pf.empty()) all.insert(end(all), begin(pf), end(pf));
+            }
+        }
+    }
+    //---------------------------------------------------------------
+    static void
+    gather_prefixes(const children_store& nodes, arg_list& all)
+    {
+        for(const auto& p : nodes) {
+            if(p.is_group()) {
+                gather_prefixes(p.as_group().children_, all);
+            }
+            else if(!p.as_param().flags().empty()) {
+                auto pfx = str::longest_common_prefix(p.as_param().flags());
+                if(!pfx.empty()) all.push_back(std::move(pfx));
+            }
+        }
+    }
+
+    //---------------------------------------------------------------
+    children_store children_;
+    bool exclusive_ = false;
+    bool joinable_ = false;
+    bool scoped_ = false;
+};
+
+
+
+/*************************************************************************//**
+ *
+ * @brief group or parameter
+ *
+ *****************************************************************************/
+using pattern = group::child;
+
+
+
+/*************************************************************************//**
+ *
+ * @brief makes a group of parameters and/or groups
+ *
+ *****************************************************************************/
+inline group
+operator , (parameter a, parameter b)
+{
+    return group{std::move(a), std::move(b)}.scoped(false);
+}
+
+//---------------------------------------------------------
+inline group
+operator , (parameter a, group b)
+{
+    return !b.scoped() && !b.blocking() && !b.exclusive() && !b.repeatable()
+        && !b.joinable() && (b.doc().empty() || b.doc() == a.doc())
+       ? b.push_front(std::move(a))
+       : group{std::move(a), std::move(b)}.scoped(false);
+}
+
+//---------------------------------------------------------
+inline group
+operator , (group a, parameter b)
+{
+    return !a.scoped() && !a.blocking() && !a.exclusive() && !a.repeatable()
+        && !a.joinable() && (a.doc().empty() || a.doc() == b.doc())
+       ? a.push_back(std::move(b))
+       : group{std::move(a), std::move(b)}.scoped(false);
+}
+
+//---------------------------------------------------------
+inline group
+operator , (group a, group b)
+{
+    return !a.scoped() && !a.blocking() && !a.exclusive() && !a.repeatable()
+        && !a.joinable() && (a.doc().empty() || a.doc() == b.doc())
+       ? a.push_back(std::move(b))
+       : group{std::move(a), std::move(b)}.scoped(false);
+}
+
+
+
+/*************************************************************************//**
+ *
+ * @brief makes a group of alternative parameters or groups
+ *
+ *****************************************************************************/
+template<class Param, class... Params>
+inline group
+one_of(Param param, Params... params)
+{
+    return group{std::move(param), std::move(params)...}.exclusive(true);
+}
+
+
+/*************************************************************************//**
+ *
+ * @brief makes a group of alternative parameters or groups
+ *
+ *****************************************************************************/
+inline group
+operator | (parameter a, parameter b)
+{
+    return group{std::move(a), std::move(b)}.scoped(false).exclusive(true);
+}
+
+//-------------------------------------------------------------------
+inline group
+operator | (parameter a, group b)
+{
+    return !b.scoped() && !b.blocking() && b.exclusive() && !b.repeatable()
+        && !b.joinable()
+        && (b.doc().empty() || b.doc() == a.doc())
+        ? b.push_front(std::move(a))
+        : group{std::move(a), std::move(b)}.scoped(false).exclusive(true);
+}
+
+//-------------------------------------------------------------------
+inline group
+operator | (group a, parameter b)
+{
+    return !a.scoped() && a.exclusive() && !a.repeatable() && !a.joinable()
+        && a.blocking() == b.blocking()
+        && (a.doc().empty() || a.doc() == b.doc())
+        ? a.push_back(std::move(b))
+        : group{std::move(a), std::move(b)}.scoped(false).exclusive(true);
+}
+
+inline group
+operator | (group a, group b)
+{
+    return !a.scoped() && a.exclusive() &&!a.repeatable() && !a.joinable()
+        && a.blocking() == b.blocking()
+        && (a.doc().empty() || a.doc() == b.doc())
+        ? a.push_back(std::move(b))
+        : group{std::move(a), std::move(b)}.scoped(false).exclusive(true);
+}
+
+
+
+namespace detail {
+
+inline void set_blocking(bool) {}
+
+template<class P, class... Ps>
+void set_blocking(bool yes, P& p, Ps&... ps) {
+    p.blocking(yes);
+    set_blocking(yes, ps...);
+}
+
+} // namespace detail
+
+
+/*************************************************************************//**
+ *
+ * @brief makes a parameter/group sequence by making all input objects blocking
+ *
+ *****************************************************************************/
+template<class Param, class... Params>
+inline group
+in_sequence(Param param, Params... params)
+{
+    detail::set_blocking(true, param, params...);
+    return group{std::move(param), std::move(params)...}.scoped(true);
+}
+
+
+/*************************************************************************//**
+ *
+ * @brief makes a parameter/group sequence by making all input objects blocking
+ *
+ *****************************************************************************/
+inline group
+operator & (parameter a, parameter b)
+{
+    a.blocking(true);
+    b.blocking(true);
+    return group{std::move(a), std::move(b)}.scoped(true);
+}
+
+//---------------------------------------------------------
+inline group
+operator & (parameter a, group b)
+{
+    a.blocking(true);
+    return group{std::move(a), std::move(b)}.scoped(true);
+}
+
+//---------------------------------------------------------
+inline group
+operator & (group a, parameter b)
+{
+    b.blocking(true);
+    if(a.all_blocking() && !a.exclusive() && !a.repeatable() && !a.joinable()
+        && (a.doc().empty() || a.doc() == b.doc()))
+    {
+        return a.push_back(std::move(b));
+    }
+    else {
+        if(!a.all_blocking()) a.blocking(true);
+        return group{std::move(a), std::move(b)}.scoped(true);
+    }
+}
+
+inline group
+operator & (group a, group b)
+{
+    if(!b.all_blocking()) b.blocking(true);
+    if(a.all_blocking() && !a.exclusive() && !a.repeatable()
+        && !a.joinable() && (a.doc().empty() || a.doc() == b.doc()))
+    {
+        return a.push_back(std::move(b));
+    }
+    else {
+        if(!a.all_blocking()) a.blocking(true);
+        return group{std::move(a), std::move(b)}.scoped(true);
+    }
+}
+
+
+
+/*************************************************************************//**
+ *
+ * @brief makes a group of parameters and/or groups
+ *        where all single char flag params ("-a", "b", ...) are joinable
+ *
+ *****************************************************************************/
+inline group&
+joinable(group& param) {
+    return param.joinable(true);
+}
+
+inline group&&
+joinable(group&& param) {
+    return std::move(param.joinable(true));
+}
+
+//-------------------------------------------------------------------
+template<class... Params>
+inline group
+joinable(parameter param, Params... params)
+{
+    return group{std::move(param), std::move(params)...}.joinable(true);
+}
+
+template<class P2, class... Ps>
+inline group
+joinable(group p1, P2 p2, Ps... ps)
+{
+    return group{std::move(p1), std::move(p2), std::move(ps)...}.joinable(true);
+}
+
+template<class Param, class... Params>
+inline group
+joinable(doc_string docstr, Param param, Params... params)
+{
+    return group{std::move(param), std::move(params)...}
+                .joinable(true).doc(std::move(docstr));
+}
+
+
+
+/*************************************************************************//**
+ *
+ * @brief makes a repeatable copy of a parameter
+ *
+ *****************************************************************************/
+inline parameter
+repeatable(parameter p) {
+    return p.repeatable(true);
+}
+
+/*************************************************************************//**
+ *
+ * @brief makes a repeatable copy of a group
+ *
+ *****************************************************************************/
+inline group
+repeatable(group g) {
+    return g.repeatable(true);
+}
+
+
+
+/*************************************************************************//**
+ *
+ * @brief makes a group of parameters and/or groups
+ *        that is repeatable as a whole
+ *        Note that a repeatable group consisting entirely of non-blocking
+ *        children is equivalent to a non-repeatable group of
+ *        repeatable children.
+ *
+ *****************************************************************************/
+template<class P2, class... Ps>
+inline group
+repeatable(parameter p1, P2 p2, Ps... ps)
+{
+    return group{std::move(p1), std::move(p2),
+                 std::move(ps)...}.repeatable(true);
+}
+
+template<class P2, class... Ps>
+inline group
+repeatable(group p1, P2 p2, Ps... ps)
+{
+    return group{std::move(p1), std::move(p2),
+                 std::move(ps)...}.repeatable(true);
+}
+
+
+
+/*************************************************************************//**
+ *
+ * @brief recursively prepends a prefix to all flags
+ *
+ *****************************************************************************/
+inline parameter&&
+with_prefix(const arg_string& prefix, parameter&& p) {
+    return std::move(with_prefix(prefix, p));
+}
+
+
+//-------------------------------------------------------------------
+inline group&
+with_prefix(const arg_string& prefix, group& params)
+{
+    for(auto& p : params) {
+        if(p.is_group()) {
+            with_prefix(prefix, p.as_group());
+        } else {
+            with_prefix(prefix, p.as_param());
+        }
+    }
+    return params;
+}
+
+
+inline group&&
+with_prefix(const arg_string& prefix, group&& params)
+{
+    return std::move(with_prefix(prefix, params));
+}
+
+
+template<class Param, class... Params>
+inline group
+with_prefix(arg_string prefix, Param&& param, Params&&... params)
+{
+    return with_prefix(prefix, group{std::forward<Param>(param),
+                                     std::forward<Params>(params)...});
+}
+
+
+
+/*************************************************************************//**
+ *
+ * @brief recursively prepends a prefix to all flags
+ *
+ * @param shortpfx : used for single-letter flags
+ * @param longpfx  : used for flags with length > 1
+ *
+ *****************************************************************************/
+inline parameter&&
+with_prefixes_short_long(const arg_string& shortpfx, const arg_string& longpfx,
+                         parameter&& p)
+{
+    return std::move(with_prefixes_short_long(shortpfx, longpfx, p));
+}
+
+
+//-------------------------------------------------------------------
+inline group&
+with_prefixes_short_long(const arg_string& shortFlagPrefix,
+                         const arg_string& longFlagPrefix,
+                         group& params)
+{
+    for(auto& p : params) {
+        if(p.is_group()) {
+            with_prefixes_short_long(shortFlagPrefix, longFlagPrefix, p.as_group());
+        } else {
+            with_prefixes_short_long(shortFlagPrefix, longFlagPrefix, p.as_param());
+        }
+    }
+    return params;
+}
+
+
+inline group&&
+with_prefixes_short_long(const arg_string& shortFlagPrefix,
+                         const arg_string& longFlagPrefix,
+                         group&& params)
+{
+    return std::move(with_prefixes_short_long(shortFlagPrefix, longFlagPrefix,
+                                              params));
+}
+
+
+template<class Param, class... Params>
+inline group
+with_prefixes_short_long(const arg_string& shortFlagPrefix,
+                         const arg_string& longFlagPrefix,
+                         Param&& param, Params&&... params)
+{
+    return with_prefixes_short_long(shortFlagPrefix, longFlagPrefix,
+                                    group{std::forward<Param>(param),
+                                          std::forward<Params>(params)...});
+}
+
+
+
+
+
+
+
+
+/*************************************************************************//**
+ *
+ * @brief parsing implementation details
+ *
+ *****************************************************************************/
+namespace detail {
+
+
+/*************************************************************************//**
+ *
+ * @brief DFS traverser that keeps track of 'scopes'
+ *        scope = all parameters that are either bounded by
+ *        two blocking parameters on the same depth level
+ *        or the beginning/end of the outermost group
+ *
+ *****************************************************************************/
+class scoped_dfs_traverser
+{
+public:
+    using dfs_traverser = group::depth_first_traverser;
+
+    scoped_dfs_traverser() = default;
+
+    explicit
+    scoped_dfs_traverser(const group& g):
+        pos_{g}, lastMatch_{}, posAfterLastMatch_{}, scopes_{},
+        curMatched_{false}, ignoreBlocks_{false},
+        repeatGroupStarted_{false}, repeatGroupContinues_{false}
+    {}
+
+    const dfs_traverser& base() const noexcept { return pos_; }
+    const dfs_traverser& last_match() const noexcept { return lastMatch_; }
+
+    const group& parent() const noexcept { return pos_.parent(); }
+    const group* repeat_group() const noexcept { return pos_.repeat_group(); }
+    const group* join_group() const noexcept { return pos_.join_group(); }
+
+    const pattern* operator ->() const noexcept { return pos_.operator->(); }
+    const pattern& operator *() const noexcept { return *pos_; }
+
+    const pattern* ptr() const noexcept { return pos_.operator->(); }
+
+    explicit operator bool() const noexcept { return bool(pos_); }
+
+    bool joinable() const noexcept { return pos_.joinable(); }
+    arg_string common_flag_prefix() const { return pos_.common_flag_prefix(); }
+
+    void ignore_blocking(bool yes) { ignoreBlocks_ = yes; }
+
+    void invalidate() { pos_.invalidate(); curMatched_ = false; }
+    bool matched() const noexcept { return curMatched_; }
+
+    bool start_of_repeat_group() const noexcept { return repeatGroupStarted_; }
+
+    //-----------------------------------------------------
+    scoped_dfs_traverser&
+    next_sibling() { pos_.next_sibling(); return *this; }
+
+    scoped_dfs_traverser&
+    next_alternative() { pos_.next_alternative(); return *this; }
+
+    scoped_dfs_traverser&
+    next_after_siblings() { pos_.next_after_siblings(); return *this; }
+
+    //-----------------------------------------------------
+    scoped_dfs_traverser&
+    operator ++ ()
+    {
+        if(!pos_) return *this;
+
+        if(pos_.is_last_in_path()) {
+            return_to_outermost_scope();
+            return *this;
+        }
+
+        //current pattern can block if it didn't match already
+        if(!ignoreBlocks_ && !matched()) {
+            //current group can block if we didn't have any match in it
+            if(pos_.is_last_in_group() && pos_.parent().blocking()
+                && (!posAfterLastMatch_ || &(posAfterLastMatch_.parent()) != &(pos_.parent())))
+            {
+                //ascend to parent's level
+                ++pos_;
+                //skip all siblings of parent group
+                pos_.next_after_siblings();
+                if(!pos_) return_to_outermost_scope();
+            }
+            else if(pos_->blocking() && !pos_->is_group()) {
+                if(pos_.parent().exclusive()) { //is_alternative(pos_.level())) {
+                    pos_.next_alternative();
+                } else {
+                    //no match => skip siblings of blocking param
+                    pos_.next_after_siblings();
+                }
+                if(!pos_) return_to_outermost_scope();
+            } else {
+                ++pos_;
+            }
+        } else {
+            ++pos_;
+        }
+        check_left_scope();
+        return *this;
+    }
+
+    //-----------------------------------------------------
+    void next_after_match(scoped_dfs_traverser match)
+    {
+        if(!match || ignoreBlocks_) return;
+
+        check_repeat_group_start(match);
+
+        lastMatch_ = match.base();
+
+        if(!match->blocking() && match.base().parent().blocking()) {
+            match.pos_.back_to_parent();
+        }
+
+        //if match is not in current position & current position is blocking
+        //=> current position has to be advanced by one so that it is
+        //no longer reachable within current scope
+        //(can happen for repeatable, blocking parameters)
+        if(match.base() != pos_ && pos_->blocking()) pos_.next_sibling();
+
+        if(match->blocking()) {
+            if(match.pos_.is_alternative()) {
+                //discard other alternatives
+                match.pos_.skip_alternatives();
+            }
+
+            if(is_last_in_current_scope(match.pos_)) {
+                //if current param is not repeatable -> back to previous scope
+                if(!match->repeatable() && !match->is_group()) {
+                    curMatched_ = false;
+                    pos_ = std::move(match.pos_);
+                    if(!scopes_.empty()) pos_.undo(scopes_.top());
+                }
+                else { //stay at match position
+                    curMatched_ = true;
+                    pos_ = std::move(match.pos_);
+                }
+            }
+            else { //not last in current group
+                //if current param is not repeatable, go directly to next
+                if(!match->repeatable() && !match->is_group()) {
+                    curMatched_ = false;
+                    ++match.pos_;
+                } else {
+                    curMatched_ = true;
+                }
+
+                if(match.pos_.level() > pos_.level()) {
+                    scopes_.push(pos_.undo_point());
+                    pos_ = std::move(match.pos_);
+                }
+                else if(match.pos_.level() < pos_.level()) {
+                    return_to_level(match.pos_.level());
+                }
+                else {
+                    pos_ = std::move(match.pos_);
+                }
+            }
+            posAfterLastMatch_ = pos_;
+        }
+        else {
+            if(match.pos_.level() < pos_.level()) {
+                return_to_level(match.pos_.level());
+            }
+            posAfterLastMatch_ = pos_;
+        }
+        repeatGroupContinues_ = repeat_group_continues();
+    }
+
+private:
+    //-----------------------------------------------------
+    bool is_last_in_current_scope(const dfs_traverser& pos)
+    {
+        if(scopes_.empty()) return pos.is_last_in_path();
+        //check if we would leave the current scope on ++
+        auto p = pos;
+        ++p;
+        return p.level() < scopes_.top().level();
+    }
+
+    //-----------------------------------------------------
+    void check_repeat_group_start(const scoped_dfs_traverser& newMatch)
+    {
+        const auto newrg = newMatch.repeat_group();
+        if(!newrg) {
+            repeatGroupStarted_ = false;
+        }
+        else if(lastMatch_.repeat_group() != newrg) {
+            repeatGroupStarted_ = true;
+        }
+        else if(!repeatGroupContinues_ || !newMatch.repeatGroupContinues_) {
+            repeatGroupStarted_ = true;
+        }
+        else {
+            //special case: repeat group is outermost group
+            //=> we can never really 'leave' and 'reenter' it
+            //but if the current scope is the first element, then we are
+            //conceptually at a position 'before' the group
+            repeatGroupStarted_ = scopes_.empty() || (
+                    newrg == pos_.root() &&
+                    scopes_.top().param() == &(*pos_.root()->begin()) );
+        }
+        repeatGroupContinues_ = repeatGroupStarted_;
+    }
+
+    //-----------------------------------------------------
+    bool repeat_group_continues()
+    {
+        if(!repeatGroupContinues_) return false;
+        const auto curRepGroup = pos_.repeat_group();
+        if(!curRepGroup) return false;
+        if(curRepGroup != lastMatch_.repeat_group()) return false;
+        if(!posAfterLastMatch_) return false;
+        return true;
+    }
+
+    //-----------------------------------------------------
+    void check_left_scope()
+    {
+        if(posAfterLastMatch_) {
+            if(pos_.level() < posAfterLastMatch_.level()) {
+                while(!scopes_.empty() && scopes_.top().level() >= pos_.level()) {
+                    pos_.undo(scopes_.top());
+                    scopes_.pop();
+                }
+                posAfterLastMatch_.invalidate();
+            }
+        }
+        while(!scopes_.empty() && scopes_.top().level() > pos_.level()) {
+            pos_.undo(scopes_.top());
+            scopes_.pop();
+        }
+        repeatGroupContinues_ = repeat_group_continues();
+    }
+
+    //-----------------------------------------------------
+    void return_to_outermost_scope()
+    {
+        posAfterLastMatch_.invalidate();
+
+        if(scopes_.empty()) {
+            pos_.invalidate();
+            repeatGroupContinues_ = false;
+            return;
+        }
+
+        while(!scopes_.empty() && (!pos_ || pos_.level() >= 1)) {
+            pos_.undo(scopes_.top());
+            scopes_.pop();
+        }
+        while(!scopes_.empty()) scopes_.pop();
+
+        repeatGroupContinues_ = repeat_group_continues();
+    }
+
+    //-----------------------------------------------------
+    void return_to_level(int level)
+    {
+        if(pos_.level() <= level) return;
+        while(!scopes_.empty() && pos_.level() > level) {
+            pos_.undo(scopes_.top());
+            scopes_.pop();
+        }
+    };
+
+    dfs_traverser pos_;
+    dfs_traverser lastMatch_;
+    dfs_traverser posAfterLastMatch_;
+    std::stack<dfs_traverser::memento> scopes_;
+    bool curMatched_ = false;
+    bool ignoreBlocks_ = false;
+    bool repeatGroupStarted_ = false;
+    bool repeatGroupContinues_ = false;
+};
+
+
+
+
+/*****************************************************************************
+ *
+ * some parameter property predicates
+ *
+ *****************************************************************************/
+struct select_all {
+    bool operator () (const parameter&) const noexcept { return true; }
+};
+
+struct select_flags {
+    bool operator () (const parameter& p) const noexcept {
+        return !p.flags().empty();
+    }
+};
+
+struct select_values {
+    bool operator () (const parameter& p) const noexcept {
+        return p.flags().empty();
+    }
+};
+
+
+
+/*************************************************************************//**
+ *
+ * @brief result of a matching operation
+ *
+ *****************************************************************************/
+class match_t {
+public:
+    match_t() = default;
+    match_t(arg_string s, scoped_dfs_traverser p):
+        str_{std::move(s)}, pos_{std::move(p)}
+    {}
+
+    const arg_string& str() const noexcept { return str_; }
+    const scoped_dfs_traverser& pos() const noexcept { return pos_; }
+
+    explicit operator bool() const noexcept { return !str_.empty(); }
+
+private:
+    arg_string str_;
+    scoped_dfs_traverser pos_;
+};
+
+
+
+/*************************************************************************//**
+ *
+ * @brief finds the first parameter that matches a given string
+ *        candidate parameters are traversed using a scoped DFS traverser
+ *
+ *****************************************************************************/
+template<class Predicate>
+match_t
+full_match(scoped_dfs_traverser pos, const arg_string& arg,
+           const Predicate& select)
+{
+    if(arg.empty()) return match_t{};
+
+    while(pos) {
+        if(pos->is_param()) {
+            const auto& param = pos->as_param();
+            if(select(param)) {
+                const auto match = param.match(arg);
+                if(match && match.length() == arg.size()) {
+                    return match_t{arg, std::move(pos)};
+                }
+            }
+        }
+        ++pos;
+    }
+    return match_t{};
+}
+
+
+
+/*************************************************************************//**
+ *
+ * @brief finds the first parameter that matches any (non-empty) prefix
+ *        of a given string;
+ *        candidate parameters are traversed using a scoped DFS traverser
+ *
+ *****************************************************************************/
+template<class Predicate>
+match_t
+prefix_match(scoped_dfs_traverser pos, const arg_string& arg,
+             const Predicate& select)
+{
+    if(arg.empty()) return match_t{};
+
+    while(pos) {
+        if(pos->is_param()) {
+            const auto& param = pos->as_param();
+            if(select(param)) {
+                const auto match = param.match(arg);
+                if(match.prefix()) {
+                    if(match.length() == arg.size()) {
+                        return match_t{arg, std::move(pos)};
+                    }
+                    else {
+                        return match_t{arg.substr(match.at(), match.length()),
+                                       std::move(pos)};
+                    }
+                }
+            }
+        }
+        ++pos;
+    }
+    return match_t{};
+}
+
+
+
+/*************************************************************************//**
+ *
+ * @brief finds the first parameter that partially matches a given string;
+ *        candidate parameters are traversed using a scoped DFS traverser
+ *
+ *****************************************************************************/
+template<class Predicate>
+match_t
+partial_match(scoped_dfs_traverser pos, const arg_string& arg,
+              const Predicate& select)
+{
+    if(arg.empty()) return match_t{};
+
+    while(pos) {
+        if(pos->is_param()) {
+            const auto& param = pos->as_param();
+            if(select(param)) {
+                const auto match = param.match(arg);
+                if(match) {
+                    return match_t{arg.substr(match.at(), match.length()),
+                                   std::move(pos)};
+                }
+            }
+        }
+        ++pos;
+    }
+    return match_t{};
+}
+
+} //namespace detail
+
+
+
+
+
+
+/***************************************************************//**
+ *
+ * @brief default command line arguments parser
+ *
+ *******************************************************************/
+class parser
+{
+public:
+    using dfs_traverser = group::depth_first_traverser;
+    using scoped_dfs_traverser = detail::scoped_dfs_traverser;
+
+
+    /*****************************************************//**
+     * @brief arg -> parameter mapping
+     *********************************************************/
+    class arg_mapping {
+    public:
+        friend class parser;
+
+        explicit
+        arg_mapping(arg_index idx, arg_string s,
+                    const dfs_traverser& match)
+        :
+            index_{idx}, arg_{std::move(s)}, match_{match},
+            repeat_{0}, startsRepeatGroup_{false},
+            blocked_{false}, conflict_{false}
+        {}
+
+        explicit
+        arg_mapping(arg_index idx, arg_string s) :
+            index_{idx}, arg_{std::move(s)}, match_{},
+            repeat_{0}, startsRepeatGroup_{false},
+            blocked_{false}, conflict_{false}
+        {}
+
+        arg_index index() const noexcept { return index_; }
+        const arg_string& arg() const noexcept { return arg_; }
+
+        const parameter* param() const noexcept {
+            return match_ && match_->is_param()
+                ? &(match_->as_param()) : nullptr;
+        }
+
+        std::size_t repeat() const noexcept { return repeat_; }
+
+        bool blocked() const noexcept { return blocked_; }
+        bool conflict() const noexcept { return conflict_; }
+
+        bool bad_repeat() const noexcept {
+            if(!param()) return false;
+            return repeat_ > 0 && !param()->repeatable()
+                && !match_.repeat_group();
+        }
+
+        bool any_error() const noexcept {
+            return !match_ || blocked() || conflict() || bad_repeat();
+        }
+
+    private:
+        arg_index index_;
+        arg_string arg_;
+        dfs_traverser match_;
+        std::size_t repeat_;
+        bool startsRepeatGroup_;
+        bool blocked_;
+        bool conflict_;
+    };
+
+    /*****************************************************//**
+     * @brief references a non-matched, required parameter
+     *********************************************************/
+    class missing_event {
+    public:
+        explicit
+        missing_event(const parameter* p, arg_index after):
+            param_{p}, aftIndex_{after}
+        {}
+
+        const parameter* param() const noexcept { return param_; }
+
+        arg_index after_index() const noexcept { return aftIndex_; }
+
+    private:
+        const parameter* param_;
+        arg_index aftIndex_;
+    };
+
+    //-----------------------------------------------------
+    using missing_events = std::vector<missing_event>;
+    using arg_mappings   = std::vector<arg_mapping>;
+
+
+private:
+    struct miss_candidate {
+        miss_candidate(dfs_traverser p, arg_index idx,
+                       bool firstInRepeatGroup = false):
+            pos{std::move(p)}, index{idx},
+            startsRepeatGroup{firstInRepeatGroup}
+        {}
+
+        dfs_traverser pos;
+        arg_index index;
+        bool startsRepeatGroup;
+    };
+    using miss_candidates = std::vector<miss_candidate>;
+
+
+public:
+    //---------------------------------------------------------------
+    /** @brief initializes parser with a command line interface
+     *  @param offset = argument index offset used for reports
+     * */
+    explicit
+    parser(const group& root, arg_index offset = 0):
+        root_{&root}, pos_{root},
+        index_{offset-1}, eaten_{0},
+        args_{}, missCand_{}, blocked_{false}
+    {
+        for_each_potential_miss(dfs_traverser{root},
+            [this](const dfs_traverser& p){
+                missCand_.emplace_back(p, index_);
+            });
+    }
+
+
+    //---------------------------------------------------------------
+    /** @brief processes one command line argument */
+    bool operator() (const arg_string& arg)
+    {
+        ++eaten_;
+        ++index_;
+
+        if(!valid() || arg.empty()) return false;
+
+        if(!blocked_ && try_match(arg)) return true;
+
+        if(try_match_blocked(arg)) return false;
+
+        //skipping of blocking & required patterns is not allowed
+        if(!blocked_ && !pos_.matched() && pos_->required() && pos_->blocking()) {
+            blocked_ = true;
+            return false;
+        }
+
+        add_nomatch(arg);
+        return false;
+    }
+
+
+    //---------------------------------------------------------------
+    /** @brief returns range of argument -> parameter mappings */
+    const arg_mappings& args() const {
+        return args_;
+    }
+
+    /** @brief returns list of missing events */
+    missing_events missed() const {
+        missing_events misses;
+        misses.reserve(missCand_.size());
+        for(auto i = missCand_.begin(); i != missCand_.end(); ++i) {
+            misses.emplace_back(&(i->pos->as_param()), i->index);
+        }
+        return misses;
+    }
+
+    /** @brief returns number of processed command line arguments */
+    arg_index parse_count() const noexcept { return eaten_; }
+
+    /** @brief returns false if previously processed command line arguments
+     *         lead to an invalid / inconsistent parsing result
+     */
+    bool valid() const noexcept { return bool(pos_); }
+
+    /** @brief returns false if previously processed command line arguments
+     *         lead to an invalid / inconsistent parsing result
+     */
+    explicit operator bool() const noexcept { return valid(); }
+
+
+private:
+    //---------------------------------------------------------------
+    using match_t = detail::match_t;
+
+
+    //---------------------------------------------------------------
+    /** @brief try to match argument with unreachable parameter */
+    bool try_match_blocked(const arg_string& arg)
+    {
+        //try to match ahead (using temporary parser)
+        if(pos_) {
+            auto ahead = *this;
+            if(try_match_blocked(std::move(ahead), arg)) return true;
+        }
+
+        //try to match from the beginning (using temporary parser)
+        if(root_) {
+            parser all{*root_, index_+1};
+            if(try_match_blocked(std::move(all), arg)) return true;
+        }
+
+        return false;
+    }
+
+    //---------------------------------------------------------------
+    bool try_match_blocked(parser&& parse, const arg_string& arg)
+    {
+        const auto nold = int(parse.args_.size());
+
+        parse.pos_.ignore_blocking(true);
+
+        if(!parse.try_match(arg)) return false;
+
+        for(auto i = parse.args_.begin() + nold; i != parse.args_.end(); ++i) {
+            args_.push_back(*i);
+            args_.back().blocked_ = true;
+        }
+        return true;
+    }
+
+    //---------------------------------------------------------------
+    /** @brief try to find a parameter/pattern that matches 'arg' */
+    bool try_match(const arg_string& arg)
+    {
+        //Note: flag-params will always take precedence over value-params
+        if(try_match_full(arg, detail::select_flags{})) return true;
+        if(try_match_joined_flags(arg)) return true;
+        if(try_match_joined_sequence(arg, detail::select_flags{})) return true;
+        if(try_match_full(arg, detail::select_values{})) return true;
+        if(try_match_joined_sequence(arg, detail::select_all{})) return true;
+        if(try_match_joined_params(arg)) return true;
+        return false;
+    }
+
+    //---------------------------------------------------------------
+    template<class Predicate>
+    bool try_match_full(const arg_string& arg, const Predicate& select)
+    {
+        auto match = detail::full_match(pos_, arg, select);
+
+        if(!match) return false;
+
+        add_match(match);
+        return true;
+    }
+
+    //---------------------------------------------------------------
+    template<class Predicate>
+    bool try_match_joined_sequence(arg_string arg, const Predicate& acceptFirst)
+    {
+        auto fstMatch = detail::prefix_match(pos_, arg, acceptFirst);
+
+        if(!fstMatch) return false;
+
+        if(fstMatch.str().size() == arg.size()) {
+            add_match(fstMatch);
+            return true;
+        }
+
+        if(!fstMatch.pos()->blocking()) return false;
+
+        auto pos = fstMatch.pos();
+        pos.ignore_blocking(true);
+        const auto parent = &pos.parent();
+        if(!pos->repeatable()) ++pos;
+
+        arg.erase(0, fstMatch.str().size());
+        std::vector<match_t> matches { std::move(fstMatch) };
+
+        while(!arg.empty() && pos &&
+              pos->blocking() && pos->is_param() &&
+              (&pos.parent() == parent))
+        {
+            auto match = pos->as_param().match(arg);
+
+            if(match.prefix()) {
+                matches.emplace_back(arg.substr(0,match.length()), pos);
+                arg.erase(0, match.length());
+                if(!pos->repeatable()) ++pos;
+            }
+            else {
+                if(!pos->repeatable()) return false;
+                ++pos;
+            }
+
+        }
+
+        if(!arg.empty() || matches.empty()) return false;
+
+        for(const auto& m : matches) add_match(m);
+        return true;
+    }
+
+    //-----------------------------------------------------
+    bool try_match_joined_flags(const arg_string& arg)
+    {
+        return try_match_joined([&](const group& g) {
+            if(try_match_joined(g, arg, detail::select_flags{},
+                                g.common_flag_prefix()) )
+            {
+                return true;
+            }
+            return false;
+        });
+    }
+
+    //---------------------------------------------------------------
+    bool try_match_joined_params(const arg_string& arg)
+    {
+        return try_match_joined([&](const group& g) {
+            if(try_match_joined(g, arg, detail::select_all{}) ) {
+                return true;
+            }
+            return false;
+        });
+    }
+
+    //-----------------------------------------------------
+    template<class Predicate>
+    bool try_match_joined(const group& joinGroup, arg_string arg,
+                          const Predicate& pred,
+                          const arg_string& prefix = "")
+    {
+        parser parse {joinGroup};
+        std::vector<match_t> matches;
+
+        while(!arg.empty()) {
+            auto match = detail::prefix_match(parse.pos_, arg, pred);
+
+            if(!match) return false;
+
+            arg.erase(0, match.str().size());
+            //make sure prefix is always present after the first match
+            //ensures that, e.g., flags "-a" and "-b" will be found in "-ab"
+            if(!arg.empty() && !prefix.empty() && arg.find(prefix) != 0 &&
+                prefix != match.str())
+            {
+                arg.insert(0,prefix);
+            }
+
+            parse.add_match(match);
+            matches.push_back(std::move(match));
+        }
+
+        if(!arg.empty() || matches.empty()) return false;
+
+        if(!parse.missCand_.empty()) return false;
+        for(const auto& a : parse.args_) if(a.any_error()) return false;
+
+        //replay matches onto *this
+        for(const auto& m : matches) add_match(m);
+        return true;
+    }
+
+    //-----------------------------------------------------
+    template<class Predicate>
+    bool try_match_joined(const Predicate& pred)
+    {
+        if(pos_ && pos_.parent().joinable()) {
+            const auto& g = pos_.parent();
+            if(pred(g)) return true;
+            return false;
+        }
+
+        auto pos = pos_;
+        while(pos) {
+            if(pos->is_group() && pos->as_group().joinable()) {
+                const auto& g = pos->as_group();
+                if(pred(g)) return true;
+                pos.next_sibling();
+            }
+            else {
+                ++pos;
+            }
+        }
+        return false;
+    }
+
+
+    //---------------------------------------------------------------
+    void add_nomatch(const arg_string& arg) {
+        args_.emplace_back(index_, arg);
+    }
+
+
+    //---------------------------------------------------------------
+    void add_match(const match_t& match)
+    {
+        const auto& pos = match.pos();
+        if(!pos || !pos->is_param() || match.str().empty()) return;
+
+        pos_.next_after_match(pos);
+
+        arg_mapping newArg{index_, match.str(), pos.base()};
+        newArg.repeat_ = occurrences_of(&pos->as_param());
+        newArg.conflict_ = check_conflicts(pos.base());
+        newArg.startsRepeatGroup_ = pos_.start_of_repeat_group();
+        args_.push_back(std::move(newArg));
+
+        add_miss_candidates_after(pos);
+        clean_miss_candidates_for(pos.base());
+        discard_alternative_miss_candidates(pos.base());
+
+    }
+
+    //-----------------------------------------------------
+    bool check_conflicts(const dfs_traverser& match)
+    {
+        if(pos_.start_of_repeat_group()) return false;
+        bool conflict = false;
+        for(const auto& m : match.stack()) {
+            if(m.parent->exclusive()) {
+                for(auto i = args_.rbegin(); i != args_.rend(); ++i) {
+                    if(!i->blocked()) {
+                        for(const auto& c : i->match_.stack()) {
+                            //sibling within same exclusive group => conflict
+                            if(c.parent == m.parent && c.cur != m.cur) {
+                                conflict = true;
+                                i->conflict_ = true;
+                            }
+                        }
+                    }
+                    //check for conflicts only within current repeat cycle
+                    if(i->startsRepeatGroup_) break;
+                }
+            }
+        }
+        return conflict;
+    }
+
+    //-----------------------------------------------------
+    void clean_miss_candidates_for(const dfs_traverser& match)
+    {
+        auto i = std::find_if(missCand_.rbegin(), missCand_.rend(),
+            [&](const miss_candidate& m) {
+                return &(*m.pos) == &(*match);
+            });
+
+        if(i != missCand_.rend()) {
+            missCand_.erase(prev(i.base()));
+        }
+    }
+
+    //-----------------------------------------------------
+    void discard_alternative_miss_candidates(const dfs_traverser& match)
+    {
+        if(missCand_.empty()) return;
+        //find out, if miss candidate is sibling of one of the same
+        //alternative groups that the current match is a member of
+        //if so, we can discard the miss
+
+        //go through all exclusive groups of matching pattern
+        for(const auto& m : match.stack()) {
+            if(m.parent->exclusive()) {
+                for(auto i = int(missCand_.size())-1; i >= 0; --i) {
+                    bool removed = false;
+                    for(const auto& c : missCand_[i].pos.stack()) {
+                        //sibling within same exclusive group => discard
+                        if(c.parent == m.parent && c.cur != m.cur) {
+                            missCand_.erase(missCand_.begin() + i);
+                            if(missCand_.empty()) return;
+                            removed = true;
+                            break;
+                        }
+                    }
+                    //remove miss candidates only within current repeat cycle
+                    if(i > 0 && removed) {
+                        if(missCand_[i-1].startsRepeatGroup) break;
+                    } else {
+                        if(missCand_[i].startsRepeatGroup) break;
+                    }
+                }
+            }
+        }
+    }
+
+    //-----------------------------------------------------
+    void add_miss_candidates_after(const scoped_dfs_traverser& match)
+    {
+        auto npos = match.base();
+        if(npos.is_alternative()) npos.skip_alternatives();
+        ++npos;
+        //need to add potential misses if:
+        //either new repeat group was started
+        const auto newRepGroup = match.repeat_group();
+        if(newRepGroup) {
+            if(pos_.start_of_repeat_group()) {
+                for_each_potential_miss(std::move(npos),
+                    [&,this](const dfs_traverser& pos) {
+                        //only add candidates within repeat group
+                        if(newRepGroup == pos.repeat_group()) {
+                            missCand_.emplace_back(pos, index_, true);
+                        }
+                    });
+            }
+        }
+        //... or an optional blocking param was hit
+        else if(match->blocking() && !match->required() &&
+            npos.level() >= match.base().level())
+        {
+            for_each_potential_miss(std::move(npos),
+                [&,this](const dfs_traverser& pos) {
+                    //only add new candidates
+                    if(std::find_if(missCand_.begin(), missCand_.end(),
+                        [&](const miss_candidate& c){
+                            return &(*c.pos) == &(*pos);
+                        }) == missCand_.end())
+                    {
+                        missCand_.emplace_back(pos, index_);
+                    }
+                });
+        }
+
+    }
+
+    //-----------------------------------------------------
+    template<class Action>
+    static void
+    for_each_potential_miss(dfs_traverser pos, Action&& action)
+    {
+        const auto level = pos.level();
+        while(pos && pos.level() >= level) {
+            if(pos->is_group() ) {
+                const auto& g = pos->as_group();
+                if(g.all_optional() || (g.exclusive() && g.any_optional())) {
+                    pos.next_sibling();
+                } else {
+                    ++pos;
+                }
+            } else {  //param
+                if(pos->required()) {
+                    action(pos);
+                    ++pos;
+                } else if(pos->blocking()) { //optional + blocking
+                    pos.next_after_siblings();
+                } else {
+                    ++pos;
+                }
+            }
+        }
+    }
+
+
+    //---------------------------------------------------------------
+    std::size_t occurrences_of(const parameter* p) const
+    {
+        auto i = std::find_if(args_.rbegin(), args_.rend(),
+            [p](const arg_mapping& a){ return a.param() == p; });
+
+        if(i != args_.rend()) return i->repeat() + 1;
+        return 0;
+    }
+
+
+    //---------------------------------------------------------------
+    const group* root_;
+    scoped_dfs_traverser pos_;
+    arg_index index_;
+    arg_index eaten_;
+    arg_mappings args_;
+    miss_candidates missCand_;
+    bool blocked_;
+};
+
+
+
+
+/*************************************************************************//**
+ *
+ * @brief contains argument -> parameter mappings
+ *        and missing parameters
+ *
+ *****************************************************************************/
+class parsing_result
+{
+public:
+    using arg_mapping    = parser::arg_mapping;
+    using arg_mappings   = parser::arg_mappings;
+    using missing_event  = parser::missing_event;
+    using missing_events = parser::missing_events;
+    using iterator       = arg_mappings::const_iterator;
+
+    //-----------------------------------------------------
+    /** @brief default: empty redult */
+    parsing_result() = default;
+
+    parsing_result(arg_mappings arg2param, missing_events misses):
+        arg2param_{std::move(arg2param)}, missing_{std::move(misses)}
+    {}
+
+    //-----------------------------------------------------
+    /** @brief returns number of arguments that could not be mapped to
+     *         a parameter
+     */
+    arg_mappings::size_type
+    unmapped_args_count() const noexcept {
+        return std::count_if(arg2param_.begin(), arg2param_.end(),
+            [](const arg_mapping& a){ return !a.param(); });
+    }
+
+    /** @brief returns if any argument could only be matched by an
+     *         unreachable parameter
+     */
+    bool any_blocked() const noexcept {
+        return std::any_of(arg2param_.begin(), arg2param_.end(),
+            [](const arg_mapping& a){ return a.blocked(); });
+    }
+
+    /** @brief returns if any argument matched more than one parameter
+     *         that were mutually exclusive */
+    bool any_conflict() const noexcept {
+        return std::any_of(arg2param_.begin(), arg2param_.end(),
+            [](const arg_mapping& a){ return a.conflict(); });
+    }
+
+    /** @brief returns if any parameter matched repeatedly although
+     *         it was not allowed to */
+    bool any_bad_repeat() const noexcept {
+        return std::any_of(arg2param_.begin(), arg2param_.end(),
+            [](const arg_mapping& a){ return a.bad_repeat(); });
+    }
+
+    /** @brief returns true if any parsing error / violation of the
+     *         command line interface definition occured */
+    bool any_error() const noexcept {
+        return unmapped_args_count() > 0 || !missing().empty() ||
+               any_blocked() || any_conflict() || any_bad_repeat();
+    }
+
+    /** @brief returns true if no parsing error / violation of the
+     *         command line interface definition occured */
+    explicit operator bool() const noexcept { return !any_error(); }
+
+    /** @brief access to range of missing parameter match events */
+    const missing_events& missing() const noexcept { return missing_; }
+
+    /** @brief returns non-mutating iterator to position of
+     *         first argument -> parameter mapping  */
+    iterator begin() const noexcept { return arg2param_.begin(); }
+    /** @brief returns non-mutating iterator to position one past the
+     *         last argument -> parameter mapping  */
+    iterator end()   const noexcept { return arg2param_.end(); }
+
+private:
+    //-----------------------------------------------------
+    arg_mappings arg2param_;
+    missing_events missing_;
+};
+
+
+
+
+namespace detail {
+namespace {
+
+/*************************************************************************//**
+ *
+ * @brief correct some common problems
+ *        does not - and MUST NOT - change the number of arguments
+ *        (no insertion, no deletion)
+ *
+ *****************************************************************************/
+void sanitize_args(arg_list& args)
+{
+    //e.g. {"-o12", ".34"} -> {"-o", "12.34"}
+
+    if(args.empty()) return;
+
+    for(auto i = begin(args)+1; i != end(args); ++i) {
+        if(i != begin(args) && i->size() > 1 &&
+            i->find('.') == 0 && std::isdigit((*i)[1]) )
+        {
+            //find trailing digits in previous arg
+            using std::prev;
+            auto& prv = *prev(i);
+            auto fstDigit = std::find_if_not(prv.rbegin(), prv.rend(),
+                [](arg_string::value_type c){
+                    return std::isdigit(c);
+                }).base();
+
+            //handle leading sign
+            if(fstDigit > prv.begin() &&
+                (*prev(fstDigit) == '+' || *prev(fstDigit) == '-'))
+            {
+                --fstDigit;
+            }
+
+            //prepend digits from previous arg
+            i->insert(begin(*i), fstDigit, end(prv));
+
+            //erase digits in previous arg
+            prv.erase(fstDigit, end(prv));
+        }
+    }
+}
+
+
+
+/*************************************************************************//**
+ *
+ * @brief executes actions based on a parsing result
+ *
+ *****************************************************************************/
+void execute_actions(const parsing_result& res)
+{
+    for(const auto& m : res) {
+        if(m.param()) {
+            const auto& param = *(m.param());
+
+            if(m.repeat() > 0) param.notify_repeated(m.index());
+            if(m.blocked())    param.notify_blocked(m.index());
+            if(m.conflict())   param.notify_conflict(m.index());
+            //main action
+            if(!m.any_error()) param.execute_actions(m.arg());
+        }
+    }
+
+    for(auto m : res.missing()) {
+        if(m.param()) m.param()->notify_missing(m.after_index());
+    }
+}
+
+
+
+/*************************************************************************//**
+ *
+ * @brief parses input args
+ *
+ *****************************************************************************/
+static parsing_result
+parse_args(const arg_list& args, const group& cli,
+           arg_index offset = 0)
+{
+    //parse args and store unrecognized arg indices
+    parser parse{cli, offset};
+    for(const auto& arg : args) {
+        parse(arg);
+        if(!parse.valid()) break;
+    }
+
+    return parsing_result{parse.args(), parse.missed()};
+}
+
+/*************************************************************************//**
+ *
+ * @brief parses input args & executes actions
+ *
+ *****************************************************************************/
+static parsing_result
+parse_and_execute(const arg_list& args, const group& cli,
+                  arg_index offset = 0)
+{
+    auto result = parse_args(args, cli, offset);
+
+    execute_actions(result);
+
+    return result;
+}
+
+} //anonymous namespace
+} // namespace detail
+
+
+
+
+/*************************************************************************//**
+ *
+ * @brief parses vector of arg strings and executes actions
+ *
+ *****************************************************************************/
+inline parsing_result
+parse(arg_list args, const group& cli)
+{
+    detail::sanitize_args(args);
+    return detail::parse_and_execute(args, cli);
+}
+
+
+/*************************************************************************//**
+ *
+ * @brief parses initializer_list of C-style arg strings and executes actions
+ *
+ *****************************************************************************/
+inline parsing_result
+parse(std::initializer_list<const char*> arglist, const group& cli)
+{
+    arg_list args;
+    args.reserve(arglist.size());
+    for(auto a : arglist) {
+        if(std::strlen(a) > 0) args.push_back(a);
+    }
+
+    return parse(std::move(args), cli);
+}
+
+
+/*************************************************************************//**
+ *
+ * @brief parses range of arg strings and executes actions
+ *
+ *****************************************************************************/
+template<class InputIterator>
+inline parsing_result
+parse(InputIterator first, InputIterator last, const group& cli)
+{
+    return parse(arg_list(first,last), cli);
+}
+
+
+/*************************************************************************//**
+ *
+ * @brief parses the standard array of command line arguments; omits argv[0]
+ *
+ *****************************************************************************/
+inline parsing_result
+parse(const int argc, char* argv[], const group& cli, arg_index offset = 1)
+{
+    arg_list args;
+    if(offset < argc) args.assign(argv+offset, argv+argc);
+    detail::sanitize_args(args);
+    return detail::parse_and_execute(args, cli, offset);
+}
+
+
+
+
+
+
+/*************************************************************************//**
+ *
+ * @brief filter predicate for parameters and groups;
+ *        Can be used to limit documentation generation to parameter subsets.
+ *
+ *****************************************************************************/
+class param_filter
+{
+public:
+    /** @brief only allow parameters with given prefix */
+    param_filter& prefix(const arg_string& p) noexcept {
+        prefix_ = p; return *this;
+    }
+    /** @brief only allow parameters with given prefix */
+    param_filter& prefix(arg_string&& p) noexcept {
+        prefix_ = std::move(p); return *this;
+    }
+    const arg_string& prefix()  const noexcept { return prefix_; }
+
+    /** @brief only allow parameters with given requirement status */
+    param_filter& required(tri t)  noexcept { required_ = t; return *this; }
+    tri           required() const noexcept { return required_; }
+
+    /** @brief only allow parameters with given blocking status */
+    param_filter& blocking(tri t)  noexcept { blocking_ = t; return *this; }
+    tri           blocking() const noexcept { return blocking_; }
+
+    /** @brief only allow parameters with given repeatable status */
+    param_filter& repeatable(tri t)  noexcept { repeatable_ = t; return *this; }
+    tri           repeatable() const noexcept { return repeatable_; }
+
+    /** @brief only allow parameters with given docstring status */
+    param_filter& has_doc(tri t)  noexcept { hasDoc_ = t; return *this; }
+    tri           has_doc() const noexcept { return hasDoc_; }
+
+
+    /** @brief returns true, if parameter satisfies all filters */
+    bool operator() (const parameter& p) const noexcept {
+        if(!prefix_.empty()) {
+            if(!std::any_of(p.flags().begin(), p.flags().end(),
+                [&](const arg_string& flag){
+                    return str::has_prefix(flag, prefix_);
+                })) return false;
+        }
+        if(required()   != p.required())     return false;
+        if(blocking()   != p.blocking())     return false;
+        if(repeatable() != p.repeatable())   return false;
+        if(has_doc()    != !p.doc().empty()) return false;
+        return true;
+    }
+
+private:
+    arg_string prefix_;
+    tri required_   = tri::either;
+    tri blocking_   = tri::either;
+    tri repeatable_ = tri::either;
+    tri exclusive_  = tri::either;
+    tri hasDoc_     = tri::yes;
+};
+
+
+
+
+
+
+/*************************************************************************//**
+ *
+ * @brief documentation formatting options
+ *
+ *****************************************************************************/
+class doc_formatting
+{
+public:
+    using string = doc_string;
+
+    /** @brief determines column where documentation printing starts */
+    doc_formatting& start_column(int col) { startCol_ = col; return *this; }
+    int             start_column() const noexcept { return startCol_; }
+
+    /** @brief determines column where docstrings start */
+    doc_formatting& doc_column(int col) { docCol_ = col; return *this; }
+    int             doc_column() const noexcept  { return docCol_; }
+
+    /** @brief determines indent of documentation lines
+     *         for children of a documented group */
+    doc_formatting& indent_size(int indent) { indentSize_ = indent; return *this; }
+    int             indent_size() const noexcept  { return indentSize_; }
+
+    /** @brief determines string to be used
+     *         if a parameter has no flags and no label  */
+    doc_formatting& empty_label(const string& label) {
+        emptyLabel_ = label;
+        return *this;
+    }
+    const string& empty_label() const noexcept { return emptyLabel_; }
+
+    /** @brief determines string for separating parameters */
+    doc_formatting& param_separator(const string& sep) {
+        paramSep_ = sep;
+        return *this;
+    }
+    const string& param_separator() const noexcept { return paramSep_; }
+
+    /** @brief determines string for separating groups (in usage lines) */
+    doc_formatting& group_separator(const string& sep) {
+        groupSep_ = sep;
+        return *this;
+    }
+    const string& group_separator() const noexcept { return groupSep_; }
+
+    /** @brief determines string for separating alternative parameters */
+    doc_formatting& alternative_param_separator(const string& sep) {
+        altParamSep_ = sep;
+        return *this;
+    }
+    const string& alternative_param_separator() const noexcept { return altParamSep_; }
+
+    /** @brief determines string for separating alternative groups */
+    doc_formatting& alternative_group_separator(const string& sep) {
+        altGroupSep_ = sep;
+        return *this;
+    }
+    const string& alternative_group_separator() const noexcept { return altGroupSep_; }
+
+    /** @brief determines string for separating flags of the same parameter */
+    doc_formatting& flag_separator(const string& sep) {
+        flagSep_ = sep;
+        return *this;
+    }
+    const string& flag_separator() const noexcept { return flagSep_; }
+
+    /** @brief determnines strings surrounding parameter labels */
+    doc_formatting&
+    surround_labels(const string& prefix, const string& postfix) {
+        labelPre_ = prefix;
+        labelPst_ = postfix;
+        return *this;
+    }
+    const string& label_prefix()  const noexcept { return labelPre_; }
+    const string& label_postfix() const noexcept { return labelPst_; }
+
+    /** @brief determnines strings surrounding optional parameters/groups */
+    doc_formatting&
+    surround_optional(const string& prefix, const string& postfix) {
+        optionPre_ = prefix;
+        optionPst_ = postfix;
+        return *this;
+    }
+    const string& optional_prefix()  const noexcept { return optionPre_; }
+    const string& optional_postfix() const noexcept { return optionPst_; }
+
+    /** @brief determnines strings surrounding repeatable parameters/groups */
+    doc_formatting&
+    surround_repeat(const string& prefix, const string& postfix) {
+        repeatPre_ = prefix;
+        repeatPst_ = postfix;
+        return *this;
+    }
+    const string& repeat_prefix()  const noexcept { return repeatPre_; }
+    const string& repeat_postfix() const noexcept { return repeatPst_; }
+
+    /** @brief determnines strings surrounding exclusive groups */
+    doc_formatting&
+    surround_alternatives(const string& prefix, const string& postfix) {
+        alternPre_ = prefix;
+        alternPst_ = postfix;
+        return *this;
+    }
+    const string& alternatives_prefix()  const noexcept { return alternPre_; }
+    const string& alternatives_postfix() const noexcept { return alternPst_; }
+
+    /** @brief determnines strings surrounding alternative flags */
+    doc_formatting&
+    surround_alternative_flags(const string& prefix, const string& postfix) {
+        alternFlagPre_ = prefix;
+        alternFlagPst_ = postfix;
+        return *this;
+    }
+    const string& alternative_flags_prefix()  const noexcept { return alternFlagPre_; }
+    const string& alternative_flags_postfix() const noexcept { return alternFlagPst_; }
+
+    /** @brief determnines strings surrounding non-exclusive groups */
+    doc_formatting&
+    surround_group(const string& prefix, const string& postfix) {
+        groupPre_ = prefix;
+        groupPst_ = postfix;
+        return *this;
+    }
+    const string& group_prefix()  const noexcept { return groupPre_; }
+    const string& group_postfix() const noexcept { return groupPst_; }
+
+    /** @brief determnines strings surrounding joinable groups */
+    doc_formatting&
+    surround_joinable(const string& prefix, const string& postfix) {
+        joinablePre_ = prefix;
+        joinablePst_ = postfix;
+        return *this;
+    }
+    const string& joinable_prefix()  const noexcept { return joinablePre_; }
+    const string& joinable_postfix() const noexcept { return joinablePst_; }
+
+    /** @brief determines maximum number of flags per parameter to be printed
+     *         in detailed parameter documentation lines */
+    doc_formatting& max_flags_per_param_in_doc(int max) {
+        maxAltInDocs_ = max > 0 ? max : 0;
+        return *this;
+    }
+    int max_flags_per_param_in_doc() const noexcept { return maxAltInDocs_; }
+
+    /** @brief determines maximum number of flags per parameter to be printed
+     *         in usage lines */
+    doc_formatting& max_flags_per_param_in_usage(int max) {
+        maxAltInUsage_ = max > 0 ? max : 0;
+        return *this;
+    }
+    int max_flags_per_param_in_usage() const noexcept { return maxAltInUsage_; }
+
+    /** @brief determines number of empty rows after one single-line
+     *         documentation entry */
+    doc_formatting& line_spacing(int lines) {
+        lineSpc_ = lines > 0 ? lines : 0;
+        return *this;
+    }
+    int line_spacing() const noexcept { return lineSpc_; }
+
+    /** @brief determines number of empty rows before and after a paragraph;
+     *         a paragraph is defined by a documented group or if
+     *         a parameter documentation entry used more than one line */
+    doc_formatting& paragraph_spacing(int lines) {
+        paragraphSpc_ = lines > 0 ? lines : 0;
+        return *this;
+    }
+    int paragraph_spacing() const noexcept { return paragraphSpc_; }
+
+    /** @brief determines if alternative flags with a common prefix should
+     *         be printed in a merged fashion */
+    doc_formatting& merge_alternative_flags_with_common_prefix(bool yes = true) {
+        mergeAltCommonPfx_ = yes;
+        return *this;
+    }
+    bool merge_alternative_flags_with_common_prefix() const noexcept {
+        return mergeAltCommonPfx_;
+    }
+
+    /** @brief determines if joinable flags with a common prefix should
+     *         be printed in a merged fashion */
+    doc_formatting& merge_joinable_with_common_prefix(bool yes = true) {
+        mergeJoinableCommonPfx_ = yes;
+        return *this;
+    }
+    bool merge_joinable_with_common_prefix() const noexcept {
+        return mergeJoinableCommonPfx_;
+    }
+
+    /** @brief determines if children of exclusive groups should be printed
+     *         on individual lines if the exceed 'alternatives_min_split_size'
+     */
+    doc_formatting& split_alternatives(bool yes = true) {
+        splitTopAlt_ = yes;
+        return *this;
+    }
+    bool split_alternatives() const noexcept {
+        return splitTopAlt_;
+    }
+
+    /** @brief determines how many children exclusive groups can have before
+     *         their children are printed on individual usage lines */
+    doc_formatting& alternatives_min_split_size(int size) {
+        groupSplitSize_ = size > 0 ? size : 0;
+        return *this;
+    }
+    int alternatives_min_split_size() const noexcept { return groupSplitSize_; }
+
+private:
+    string paramSep_      = string(" ");
+    string groupSep_      = string(" ");
+    string altParamSep_   = string("|");
+    string altGroupSep_   = string(" | ");
+    string flagSep_       = string(", ");
+    string labelPre_      = string("<");
+    string labelPst_      = string(">");
+    string optionPre_     = string("[");
+    string optionPst_     = string("]");
+    string repeatPre_     = string("");
+    string repeatPst_     = string("...");
+    string groupPre_      = string("(");
+    string groupPst_      = string(")");
+    string alternPre_     = string("(");
+    string alternPst_     = string(")");
+    string alternFlagPre_ = string("");
+    string alternFlagPst_ = string("");
+    string joinablePre_   = string("(");
+    string joinablePst_   = string(")");
+    string emptyLabel_    = string("");
+    int startCol_ = 8;
+    int docCol_ = 20;
+    int indentSize_ = 4;
+    int maxAltInUsage_ = 1;
+    int maxAltInDocs_ = 32;
+    int lineSpc_ = 0;
+    int paragraphSpc_ = 1;
+    int groupSplitSize_ = 3;
+    bool splitTopAlt_ = true;
+    bool mergeAltCommonPfx_ = false;
+    bool mergeJoinableCommonPfx_ = true;
+};
+
+
+
+
+/*************************************************************************//**
+ *
+ * @brief   generates usage lines
+ *
+ * @details lazily evaluated
+ *
+ *****************************************************************************/
+class usage_lines
+{
+public:
+    using string = doc_string;
+
+    usage_lines(const group& params, string prefix = "",
+                const doc_formatting& fmt = doc_formatting{})
+    :
+        params_(params), fmt_(fmt), prefix_(std::move(prefix))
+    {
+        if(!prefix_.empty()) prefix_ += ' ';
+        if(fmt_.start_column() > 0) prefix_.insert(0, fmt.start_column(), ' ');
+    }
+
+    usage_lines(const group& params, const doc_formatting& fmt):
+        usage_lines(params, "", fmt)
+    {}
+
+    usage_lines& ommit_outermost_group_surrounders(bool yes) {
+        ommitOutermostSurrounders_ = yes;
+        return *this;
+    }
+    bool ommit_outermost_group_surrounders() const {
+        return ommitOutermostSurrounders_;
+    }
+
+    template<class OStream>
+    inline friend OStream& operator << (OStream& os, const usage_lines& p) {
+        p.print_usage(os);
+        return os;
+    }
+
+    string str() const {
+        std::ostringstream os; os << *this; return os.str();
+    }
+
+
+private:
+    const group& params_;
+    doc_formatting fmt_;
+    string prefix_;
+    bool ommitOutermostSurrounders_ = false;
+
+
+    //-----------------------------------------------------
+    struct context {
+        group::depth_first_traverser pos;
+        std::stack<string> separators;
+        std::stack<string> postfixes;
+        int level = 0;
+        const group* outermost = nullptr;
+        bool linestart = false;
+        bool useOutermost = true;
+        int line = 0;
+
+        bool is_singleton() const noexcept {
+            return linestart && pos.is_last_in_path();
+        }
+        bool is_alternative() const noexcept {
+            return pos.parent().exclusive();
+        }
+    };
+
+
+    /***************************************************************//**
+     *
+     * @brief writes usage text for command line parameters
+     *
+     *******************************************************************/
+    template<class OStream>
+    void print_usage(OStream& os) const
+    {
+        context cur;
+        cur.pos = params_.begin_dfs();
+        cur.linestart = true;
+        cur.level = cur.pos.level();
+        cur.outermost = &params_;
+
+        print_usage(os, cur, prefix_);
+    }
+
+
+    /***************************************************************//**
+     *
+     * @brief writes usage text for command line parameters
+     *
+     * @param prefix   all that goes in front of current things to print
+     *
+     *******************************************************************/
+    template<class OStream>
+    void print_usage(OStream& os, context cur, string prefix) const
+    {
+        if(!cur.pos) return;
+
+        std::ostringstream buf;
+        if(cur.linestart) buf << prefix;
+        const auto initPos = buf.tellp();
+
+        cur.level = cur.pos.level();
+
+        if(cur.useOutermost) {
+            //we cannot start outside of the outermost group
+            //so we have to treat it separately
+            start_group(buf, cur.pos.parent(), cur);
+            if(!cur.pos) {
+                os << buf.str();
+                return;
+            }
+        }
+        else {
+            //don't visit siblings of starter node
+            cur.pos.skip_siblings();
+        }
+        check_end_group(buf, cur);
+
+        do {
+            if(buf.tellp() > initPos) cur.linestart = false;
+            if(!cur.linestart && !cur.pos.is_first_in_group()) {
+                buf << cur.separators.top();
+            }
+            if(cur.pos->is_group()) {
+                start_group(buf, cur.pos->as_group(), cur);
+                if(!cur.pos) {
+                    os << buf.str();
+                    return;
+                }
+            }
+            else {
+                buf << param_label(cur.pos->as_param(), cur);
+                ++cur.pos;
+            }
+            check_end_group(buf, cur);
+        } while(cur.pos);
+
+        os << buf.str();
+    }
+
+
+    /***************************************************************//**
+     *
+     * @brief handles pattern group surrounders and separators
+     *        and alternative splitting
+     *
+     *******************************************************************/
+    void start_group(std::ostringstream& os,
+                     const group& group, context& cur) const
+    {
+        //does cur.pos already point to a member or to group itself?
+        //needed for special treatment of outermost group
+        const bool alreadyInside = &(cur.pos.parent()) == &group;
+
+        auto lbl = joined_label(group, cur);
+        if(!lbl.empty()) {
+            os << lbl;
+            cur.linestart = false;
+            //skip over entire group as its label has already been created
+            if(alreadyInside) {
+                cur.pos.next_after_siblings();
+            } else {
+                cur.pos.next_sibling();
+            }
+        }
+        else {
+            const bool splitAlternatives = group.exclusive() &&
+                fmt_.split_alternatives() &&
+                std::any_of(group.begin(), group.end(),
+                    [this](const pattern& p) {
+                        return int(p.param_count()) >= fmt_.alternatives_min_split_size();
+                    });
+
+            if(splitAlternatives) {
+                cur.postfixes.push("");
+                cur.separators.push("");
+                //recursively print alternative paths in decision-DAG
+                //enter group?
+                if(!alreadyInside) ++cur.pos;
+                cur.linestart = true;
+                cur.useOutermost = false;
+                auto pfx = os.str();
+                os.str("");
+                //print paths in DAG starting at each group member
+                for(std::size_t i = 0; i < group.size(); ++i) {
+                    std::stringstream buf;
+                    cur.outermost = cur.pos->is_group() ? &(cur.pos->as_group()) : nullptr;
+                    print_usage(buf, cur, pfx);
+                    if(buf.tellp() > int(pfx.size())) {
+                        os << buf.str();
+                        if(i < group.size()-1) {
+                            if(cur.line > 0) {
+                                os << string(fmt_.line_spacing(), '\n');
+                            }
+                            ++cur.line;
+                            os << '\n';
+                        }
+                    }
+                    cur.pos.next_sibling(); //do not descend into memebers
+                }
+                cur.pos.invalidate(); //signal end-of-path
+                return;
+            }
+            else {
+                //pre & postfixes, separators
+                auto surround = group_surrounders(group, cur);
+                os << surround.first;
+                cur.postfixes.push(std::move(surround.second));
+                cur.separators.push(group_separator(group, fmt_));
+                //descend into group?
+                if(!alreadyInside) ++cur.pos;
+            }
+        }
+        cur.level = cur.pos.level();
+    }
+
+
+    /***************************************************************//**
+     *
+     *******************************************************************/
+    void check_end_group(std::ostringstream& os, context& cur) const
+    {
+        for(; cur.level > cur.pos.level(); --cur.level) {
+            os << cur.postfixes.top();
+            cur.postfixes.pop();
+            cur.separators.pop();
+        }
+        cur.level = cur.pos.level();
+    }
+
+
+    /***************************************************************//**
+     *
+     * @brief makes usage label for one command line parameter
+     *
+     *******************************************************************/
+    string param_label(const parameter& p, const context& cur) const
+    {
+        const auto& parent = cur.pos.parent();
+
+        const bool startsOptionalSequence =
+            parent.size() > 1 && p.blocking() && cur.pos.is_first_in_group();
+
+        const bool outermost =
+            ommitOutermostSurrounders_ && cur.outermost == &parent;
+
+        const bool showopt = !cur.is_alternative() && !p.required()
+            && !startsOptionalSequence && !outermost;
+
+        const bool showrep = p.repeatable() && !outermost;
+
+        string lbl;
+
+        if(showrep) lbl += fmt_.repeat_prefix();
+        if(showopt) lbl += fmt_.optional_prefix();
+
+        const auto& flags = p.flags();
+        if(!flags.empty()) {
+            const int n = std::min(fmt_.max_flags_per_param_in_usage(),
+                                   int(flags.size()));
+
+            const bool surrAlt = n > 1 && !showopt && !cur.is_singleton();
+
+            if(surrAlt) lbl += fmt_.alternative_flags_prefix();
+            bool sep = false;
+            for(int i = 0; i < n; ++i) {
+                if(sep) {
+                    if(cur.is_singleton())
+                        lbl += fmt_.alternative_group_separator();
+                    else
+                        lbl += fmt_.flag_separator();
+                }
+                lbl += flags[i];
+                sep = true;
+            }
+            if(surrAlt) lbl += fmt_.alternative_flags_postfix();
+        }
+        else {
+             if(!p.label().empty()) {
+                 lbl += fmt_.label_prefix()
+                     + p.label()
+                     + fmt_.label_postfix();
+             } else if(!fmt_.empty_label().empty()) {
+                 lbl += fmt_.label_prefix()
+                     + fmt_.empty_label()
+                     + fmt_.label_postfix();
+             } else {
+                 return "";
+             }
+        }
+
+        if(showopt) lbl += fmt_.optional_postfix();
+        if(showrep) lbl += fmt_.repeat_postfix();
+
+        return lbl;
+    }
+
+
+    /***************************************************************//**
+     *
+     * @brief prints flags in one group in a merged fashion
+     *
+     *******************************************************************/
+    string joined_label(const group& params, const context& cur) const
+    {
+        if(!fmt_.merge_alternative_flags_with_common_prefix() &&
+           !fmt_.merge_joinable_with_common_prefix()) return "";
+
+        const bool flagsonly = std::all_of(params.begin(), params.end(),
+            [](const pattern& p){
+                return p.is_param() && !p.as_param().flags().empty();
+            });
+
+        if(!flagsonly) return "";
+
+        const bool showOpt = params.all_optional() &&
+            !(ommitOutermostSurrounders_ && cur.outermost == &params);
+
+        auto pfx = params.common_flag_prefix();
+        if(pfx.empty()) return "";
+
+        const auto n = pfx.size();
+        if(params.exclusive() &&
+           fmt_.merge_alternative_flags_with_common_prefix())
+        {
+            string lbl;
+            if(showOpt) lbl += fmt_.optional_prefix();
+            lbl += pfx + fmt_.alternatives_prefix();
+            bool first = true;
+            for(const auto& p : params) {
+                if(p.is_param()) {
+                    if(first)
+                        first = false;
+                    else
+                        lbl += fmt_.alternative_param_separator();
+                    lbl += p.as_param().flags().front().substr(n);
+                }
+            }
+            lbl += fmt_.alternatives_postfix();
+            if(showOpt) lbl += fmt_.optional_postfix();
+            return lbl;
+        }
+        //no alternatives, but joinable flags
+        else if(params.joinable() &&
+            fmt_.merge_joinable_with_common_prefix())
+        {
+            const bool allSingleChar = std::all_of(params.begin(), params.end(),
+                [&](const pattern& p){
+                    return p.is_param() &&
+                        p.as_param().flags().front().substr(n).size() == 1;
+                });
+
+            if(allSingleChar) {
+                string lbl;
+                if(showOpt) lbl += fmt_.optional_prefix();
+                lbl += pfx;
+                for(const auto& p : params) {
+                    if(p.is_param())
+                        lbl += p.as_param().flags().front().substr(n);
+                }
+                if(showOpt) lbl += fmt_.optional_postfix();
+                return lbl;
+            }
+        }
+
+        return "";
+    }
+
+
+    /***************************************************************//**
+     *
+     * @return symbols with which to surround a group
+     *
+     *******************************************************************/
+    std::pair<string,string>
+    group_surrounders(const group& group, const context& cur) const
+    {
+        string prefix;
+        string postfix;
+
+        const bool isOutermost = &group == cur.outermost;
+        if(isOutermost && ommitOutermostSurrounders_)
+            return {string{}, string{}};
+
+        if(group.exclusive()) {
+            if(group.all_optional()) {
+                prefix  = fmt_.optional_prefix();
+                postfix = fmt_.optional_postfix();
+                if(group.all_flagless()) {
+                    prefix  += fmt_.label_prefix();
+                    postfix = fmt_.label_prefix() + postfix;
+                }
+            } else if(group.all_flagless()) {
+                prefix  = fmt_.label_prefix();
+                postfix = fmt_.label_postfix();
+            } else if(!cur.is_singleton() || !isOutermost) {
+                prefix  = fmt_.alternatives_prefix();
+                postfix = fmt_.alternatives_postfix();
+            }
+        }
+        else if(group.size() > 1 &&
+                group.front().blocking() && !group.front().required())
+        {
+            prefix  = fmt_.optional_prefix();
+            postfix = fmt_.optional_postfix();
+        }
+        else if(group.size() > 1 && cur.is_alternative() &&
+                &group != cur.outermost)
+        {
+            prefix  = fmt_.group_prefix();
+            postfix = fmt_.group_postfix();
+        }
+        else if(!group.exclusive() &&
+            group.joinable() && !cur.linestart)
+        {
+            prefix  = fmt_.joinable_prefix();
+            postfix = fmt_.joinable_postfix();
+        }
+
+        if(group.repeatable()) {
+            if(prefix.empty()) prefix = fmt_.group_prefix();
+            prefix = fmt_.repeat_prefix() + prefix;
+            if(postfix.empty()) postfix = fmt_.group_postfix();
+            postfix += fmt_.repeat_postfix();
+        }
+
+        return {std::move(prefix), std::move(postfix)};
+    }
+
+
+    /***************************************************************//**
+     *
+     * @return symbol that separates members of a group
+     *
+     *******************************************************************/
+    static string
+    group_separator(const group& group, const doc_formatting& fmt)
+    {
+        const bool only1ParamPerMember = std::all_of(group.begin(), group.end(),
+            [](const pattern& p) { return p.param_count() < 2; });
+
+        if(only1ParamPerMember) {
+            if(group.exclusive()) {
+                return fmt.alternative_param_separator();
+            } else {
+                return fmt.param_separator();
+            }
+        }
+        else { //there is at least one large group inside
+            if(group.exclusive()) {
+                return fmt.alternative_group_separator();
+            } else {
+                return fmt.group_separator();
+            }
+        }
+    }
+};
+
+
+
+
+/*************************************************************************//**
+ *
+ * @brief   generates parameter and group documentation from docstrings
+ *
+ * @details lazily evaluated
+ *
+ *****************************************************************************/
+class documentation
+{
+public:
+    using string = doc_string;
+
+    documentation(const group& cli,
+                  const doc_formatting& fmt = doc_formatting{},
+                  const param_filter& filter = param_filter{})
+    :
+        cli_(cli), fmt_{fmt}, usgFmt_{fmt}, filter_{filter}
+    {
+        //necessary, because we re-use "usage_lines" to generate
+        //labels for documented groups
+        usgFmt_.max_flags_per_param_in_usage(
+            usgFmt_.max_flags_per_param_in_doc());
+    }
+
+    documentation(const group& params,
+                  const param_filter& filter,
+                  const doc_formatting& fmt = doc_formatting{})
+    :
+        documentation(params, fmt, filter)
+    {}
+
+    template<class OStream>
+    inline friend OStream& operator << (OStream& os, const documentation& p) {
+        printed prn = printed::nothing;
+        p.print_doc(os, p.cli_, prn);
+        return os;
+    }
+
+    string str() const {
+        std::ostringstream os; os << *this; return os.str();
+    }
+
+
+private:
+    using dfs_traverser = group::depth_first_traverser;
+    enum class printed { nothing, line, paragraph };
+
+    const group& cli_;
+    doc_formatting fmt_;
+    doc_formatting usgFmt_;
+    param_filter filter_;
+
+
+    /***************************************************************//**
+     *
+     * @brief writes full documentation text for command line parameters
+     *
+     *******************************************************************/
+    template<class OStream>
+    void print_doc(OStream& os, const group& params,
+                   printed& sofar,
+                   int indentLvl = 0) const
+    {
+        if(params.empty()) return;
+
+        //if group itself doesn't have docstring
+        if(params.doc().empty()) {
+            for(const auto& p : params) {
+                print_doc(os, p, sofar, indentLvl);
+            }
+        }
+        else { //group itself does have docstring
+            bool anyDocInside = std::any_of(params.begin(), params.end(),
+                [](const pattern& p){ return !p.doc().empty(); });
+
+            if(anyDocInside) { //group docstring as title, then child entries
+                if(sofar != printed::nothing) {
+                    os << string(fmt_.paragraph_spacing() + 1, '\n');
+                }
+                auto indent = string(fmt_.start_column(), ' ');
+                if(indentLvl > 0) indent += string(fmt_.indent_size() * indentLvl, ' ');
+                os << indent << params.doc() << '\n';
+                sofar = printed::nothing;
+                for(const auto& p : params) {
+                    print_doc(os, p, sofar, indentLvl + 1);
+                }
+                sofar = printed::paragraph;
+            }
+            else { //group label first then group docstring
+                auto lbl = usage_lines(params, usgFmt_)
+                           .ommit_outermost_group_surrounders(true).str();
+
+                str::trim(lbl);
+                print_entry(os, lbl, params.doc(), fmt_, sofar, indentLvl);
+            }
+        }
+    }
+
+
+    /***************************************************************//**
+     *
+     * @brief writes documentation text for one group or parameter
+     *
+     *******************************************************************/
+    template<class OStream>
+    void print_doc(OStream& os, const pattern& ptrn,
+                   printed& sofar, int indentLvl) const
+    {
+        if(ptrn.is_group()) {
+            print_doc(os, ptrn.as_group(), sofar, indentLvl);
+        }
+        else {
+            const auto& p = ptrn.as_param();
+            if(!filter_(p)) return;
+            print_entry(os, param_label(p, fmt_), p.doc(), fmt_, sofar, indentLvl);
+        }
+    }
+
+
+    /*********************************************************************//**
+     *
+     * @brief prints one entry = label + docstring
+     *
+     ************************************************************************/
+    template<class OStream>
+    static void
+    print_entry(OStream& os,
+                const string& label, const string& docstr,
+                const doc_formatting& fmt, printed& sofar, int indentLvl)
+    {
+        if(label.empty()) return;
+
+        auto indent = string(fmt.start_column(), ' ');
+        if(indentLvl > 0) indent += string(fmt.indent_size() * indentLvl, ' ');
+
+        const auto len = int(indent.size() + label.size());
+        const bool oneline = len < fmt.doc_column();
+
+        if(oneline) {
+            if(sofar == printed::line)
+                os << string(fmt.line_spacing() + 1, '\n');
+            else if(sofar == printed::paragraph)
+                os << string(fmt.paragraph_spacing() + 1, '\n');
+        }
+        else if(sofar != printed::nothing) {
+            os << string(fmt.paragraph_spacing() + 1, '\n');
+        }
+
+        sofar = oneline ? printed::line : printed::paragraph;
+
+        os << indent << label;
+
+        if(!docstr.empty()) {
+            if(oneline) {
+                os << string(fmt.doc_column() - len, ' ');
+            } else {
+                os << '\n' << string(fmt.doc_column(), ' ');
+            }
+            os << docstr;
+        }
+    }
+
+
+    /*********************************************************************//**
+     *
+     * @brief makes label for one parameter
+     *
+     ************************************************************************/
+    static doc_string
+    param_label(const parameter& param, const doc_formatting& fmt)
+    {
+        doc_string lbl;
+
+        if(param.repeatable()) lbl += fmt.repeat_prefix();
+
+        const auto& flags = param.flags();
+        if(!flags.empty()) {
+            lbl += flags[0];
+            const int n = std::min(fmt.max_flags_per_param_in_doc(),
+                                   int(flags.size()));
+            for(int i = 1; i < n; ++i) {
+                lbl += fmt.flag_separator() + flags[i];
+            }
+        }
+        else if(!param.label().empty() || !fmt.empty_label().empty()) {
+            lbl += fmt.label_prefix();
+            if(!param.label().empty()) {
+                lbl += param.label();
+            } else {
+                lbl += fmt.empty_label();
+            }
+            lbl += fmt.label_postfix();
+        }
+
+        if(param.repeatable()) lbl += fmt.repeat_postfix();
+
+        return lbl;
+    }
+
+};
+
+
+
+
+/*************************************************************************//**
+ *
+ * @brief stores strings for man page sections
+ *
+ *****************************************************************************/
+class man_page
+{
+public:
+    //---------------------------------------------------------------
+    using string = doc_string;
+
+    //---------------------------------------------------------------
+    /** @brief man page section */
+    class section {
+    public:
+        using string = doc_string;
+
+        section(string stitle, string scontent):
+            title_{std::move(stitle)}, content_{std::move(scontent)}
+        {}
+
+        const string& title()   const noexcept { return title_; }
+        const string& content() const noexcept { return content_; }
+
+    private:
+        string title_;
+        string content_;
+    };
+
+private:
+    using section_store = std::vector<section>;
+
+public:
+    //---------------------------------------------------------------
+    using value_type     = section;
+    using const_iterator = section_store::const_iterator;
+    using size_type      = section_store::size_type;
+
+
+    //---------------------------------------------------------------
+    man_page&
+    append_section(string title, string content)
+    {
+        sections_.emplace_back(std::move(title), std::move(content));
+        return *this;
+    }
+    //-----------------------------------------------------
+    man_page&
+    prepend_section(string title, string content)
+    {
+        sections_.emplace(sections_.begin(),
+                          std::move(title), std::move(content));
+        return *this;
+    }
+
+
+    //---------------------------------------------------------------
+    const section& operator [] (size_type index) const noexcept {
+        return sections_[index];
+    }
+
+    //---------------------------------------------------------------
+    size_type size() const noexcept { return sections_.size(); }
+
+    bool empty() const noexcept { return sections_.empty(); }
+
+
+    //---------------------------------------------------------------
+    const_iterator begin() const noexcept { return sections_.begin(); }
+    const_iterator end()   const noexcept { return sections_.end(); }
+
+
+    //---------------------------------------------------------------
+    man_page& program_name(const string& n) {
+        progName_ = n;
+        return *this;
+    }
+    man_page& program_name(string&& n) {
+        progName_ = std::move(n);
+        return *this;
+    }
+    const string& program_name() const noexcept {
+        return progName_;
+    }
+
+
+    //---------------------------------------------------------------
+    man_page& section_row_spacing(int rows) {
+        sectionSpc_ = rows > 0 ? rows : 0;
+        return *this;
+    }
+    int section_row_spacing() const noexcept { return sectionSpc_; }
+
+
+private:
+    int sectionSpc_ = 1;
+    section_store sections_;
+    string progName_;
+};
+
+
+
+/*************************************************************************//**
+ *
+ * @brief generates man sections from command line parameters
+ *        with sections "synopsis" and "options"
+ *
+ *****************************************************************************/
+inline man_page
+make_man_page(const group& params,
+              doc_string progname = "",
+              const doc_formatting& fmt = doc_formatting{})
+{
+    man_page man;
+    man.append_section("SYNOPSIS", usage_lines(params,progname,fmt).str());
+    man.append_section("OPTIONS", documentation(params,fmt).str());
+    return man;
+}
+
+
+
+/*************************************************************************//**
+ *
+ * @brief   generates man page based on command line parameters
+ *
+ *****************************************************************************/
+template<class OStream>
+OStream&
+operator << (OStream& os, const man_page& man)
+{
+    bool first = true;
+    const auto secSpc = doc_string(man.section_row_spacing() + 1, '\n');
+    for(const auto& section : man) {
+        if(!section.content().empty()) {
+            if(first) first = false; else os << secSpc;
+            if(!section.title().empty()) os << section.title() << '\n';
+            os << section.content();
+        }
+    }
+    os << '\n';
+    return os;
+}
+
+
+
+
+
+/*************************************************************************//**
+ *
+ * @brief printing methods for debugging command line interfaces
+ *
+ *****************************************************************************/
+namespace debug {
+
+
+/*************************************************************************//**
+ *
+ * @brief prints first flag or value label of a parameter
+ *
+ *****************************************************************************/
+inline doc_string doc_label(const parameter& p)
+{
+    if(!p.flags().empty()) return p.flags().front();
+    if(!p.label().empty()) return p.label();
+    return doc_string{"<?>"};
+}
+
+inline doc_string doc_label(const group&)
+{
+    return "<group>";
+}
+
+inline doc_string doc_label(const pattern& p)
+{
+    return p.is_group() ? doc_label(p.as_group()) : doc_label(p.as_param());
+}
+
+
+/*************************************************************************//**
+ *
+ * @brief prints parsing result
+ *
+ *****************************************************************************/
+template<class OStream>
+void print(OStream& os, const parsing_result& result)
+{
+    for(const auto& m : result) {
+        os << "#" << m.index() << " " << m.arg() << " -> ";
+        auto p = m.param();
+        if(p) {
+            os << doc_label(*p) << " \t";
+            if(m.repeat() > 0) {
+                os << (m.bad_repeat() ? "[bad repeat " : "[repeat ")
+                   <<  m.repeat() << "]";
+            }
+            if(m.blocked())  os << " [blocked]";
+            if(m.conflict()) os << " [conflict]";
+            os << '\n';
+        }
+        else {
+            os << " [unmapped]\n";
+        }
+    }
+
+    for(const auto& m : result.missing()) {
+        auto p = m.param();
+        if(p) {
+            os << doc_label(*p) << " \t";
+            os << " [missing after " << m.after_index() << "]\n";
+        }
+    }
+}
+
+
+/*************************************************************************//**
+ *
+ * @brief prints parameter label and some properties
+ *
+ *****************************************************************************/
+template<class OStream>
+void print(OStream& os, const parameter& p)
+{
+    if(p.blocking()) os << '!';
+    if(!p.required()) os << '[';
+    os << doc_label(p);
+    if(p.repeatable()) os << "...";
+    if(!p.required()) os << "]";
+}
+
+
+//-------------------------------------------------------------------
+template<class OStream>
+void print(OStream& os, const group& g, int level = 0);
+
+
+/*************************************************************************//**
+ *
+ * @brief prints group or parameter; uses indentation
+ *
+ *****************************************************************************/
+template<class OStream>
+void print(OStream& os, const pattern& param, int level = 0)
+{
+    if(param.is_group()) {
+        print(os, param.as_group(), level);
+    }
+    else {
+        os << doc_string(4*level, ' ');
+        print(os, param.as_param());
+    }
+}
+
+
+/*************************************************************************//**
+ *
+ * @brief prints group and its contents; uses indentation
+ *
+ *****************************************************************************/
+template<class OStream>
+void print(OStream& os, const group& g, int level)
+{
+    auto indent = doc_string(4*level, ' ');
+    os << indent;
+    if(g.blocking()) os << '!';
+    if(g.joinable()) os << 'J';
+    os << (g.exclusive() ? "(|\n" : "(\n");
+    for(const auto& p : g) {
+        print(os, p, level+1);
+    }
+    os << '\n' << indent << (g.exclusive() ? "|)" : ")");
+    if(g.repeatable()) os << "...";
+    os << '\n';
+}
+
+
+} // namespace debug
+} //namespace clipp
+
+#endif
+
diff --git a/include/exception.h b/include/exception.h
new file mode 100644
index 0000000000000000000000000000000000000000..b630bd008a0d8855211eca5ee0af60650aec1314
--- /dev/null
+++ b/include/exception.h
@@ -0,0 +1,22 @@
+#ifndef UTIL_EXCEPTION_H
+#define UTIL_EXCEPTION_H
+
+#include <exception>
+#include <string>
+
+namespace eic::util {
+  class Exception : public std::exception {
+  public:
+    Exception(std::string_view msg, std::string_view type = "exception") : msg_{msg}, type_{type} {}
+
+    virtual const char* what() const throw() { return msg_.c_str(); }
+    virtual const char* type() const throw() { return type_.c_str(); }
+    virtual ~Exception() throw() {}
+
+  private:
+    std::string msg_;
+    std::string type_;
+  };
+} // namespace eic::util
+
+#endif
diff --git a/include/mt.h b/include/mt.h
new file mode 100644
index 0000000000000000000000000000000000000000..198050c6ddc37e68518761b8ccc410e0d71ea123
--- /dev/null
+++ b/include/mt.h
@@ -0,0 +1,11 @@
+#ifndef MT_H
+#define MT_H
+
+// Defines the number of threads to run within the ROOT analysis scripts.
+// TODO: make this a file configured by the CI scripts so we can specify
+//       the number of threads (and the number of processes) at a global
+//       level
+
+constexpr const int kNumThreads = 8;
+
+#endif
diff --git a/include/plot.h b/include/plot.h
new file mode 100644
index 0000000000000000000000000000000000000000..c198616325d54d73ae5e4c08f5ba7113ae77f817
--- /dev/null
+++ b/include/plot.h
@@ -0,0 +1,42 @@
+#ifndef PLOT_H
+#define PLOT_H
+
+#include <TCanvas.h>
+#include <TColor.h>
+#include <TPad.h>
+#include <TPaveText.h>
+#include <TStyle.h>
+#include <fmt/core.h>
+#include <vector>
+
+namespace plot {
+
+  const int kMpBlue   = TColor::GetColor(0x1f, 0x77, 0xb4);
+  const int kMpOrange = TColor::GetColor(0xff, 0x7f, 0x0e);
+  const int kMpGreen  = TColor::GetColor(0x2c, 0xa0, 0x2c);
+  const int kMpRed    = TColor::GetColor(0xd6, 0x27, 0x28);
+  const int kMpPurple = TColor::GetColor(0x94, 0x67, 0xbd);
+  const int kMpBrown  = TColor::GetColor(0x8c, 0x56, 0x4b);
+  const int kMpPink   = TColor::GetColor(0xe3, 0x77, 0xc2);
+  const int kMpGrey   = TColor::GetColor(0x7f, 0x7f, 0x7f);
+  const int kMpMoss   = TColor::GetColor(0xbc, 0xbd, 0x22);
+  const int kMpCyan   = TColor::GetColor(0x17, 0xbe, 0xcf);
+
+  const std::vector<int> kPalette = {kMpBlue,  kMpOrange, kMpGreen, kMpRed,  kMpPurple,
+                                     kMpBrown, kMpPink,   kMpGrey,  kMpMoss, kMpCyan};
+
+  void draw_label(int ebeam, int pbeam, const std::string_view detector)
+  {
+    auto t = new TPaveText(.15, 0.800, .7, .925, "NB NDC");
+    t->SetFillColorAlpha(kWhite, 0.4);
+    t->SetTextFont(43);
+    t->SetTextSize(25);
+    t->AddText(fmt::format("#bf{{{} }}SIMULATION", detector).c_str());
+    t->AddText(fmt::format("{} GeV on {} GeV", ebeam, pbeam).c_str());
+    t->SetTextAlign(12);
+    t->Draw();
+  }
+
+} // namespace plot
+
+#endif
diff --git a/include/util.h b/include/util.h
new file mode 100644
index 0000000000000000000000000000000000000000..6a24b293c26963b4999fc63df821ab3418c694a8
--- /dev/null
+++ b/include/util.h
@@ -0,0 +1,161 @@
+#ifndef UTIL_H
+#define UTIL_H
+
+// TODO: should probably be moved to a global benchmark utility library
+
+#include <algorithm>
+#include <cmath>
+#include <exception>
+#include <fmt/core.h>
+#include <limits>
+#include <string>
+#include <vector>
+
+#include <Math/Vector4D.h>
+
+#include "dd4pod/Geant4ParticleCollection.h"
+#include "eicd/TrackParametersCollection.h"
+
+namespace util {
+
+  // Exception definition for unknown particle errors
+  // FIXME: A utility exception base class should be included in the analysis
+  //        utility library, so we can skip most of this boilerplate
+  class unknown_particle_error : public std::exception {
+  public:
+    unknown_particle_error(std::string_view particle) : m_particle{particle} {}
+    virtual const char* what() const throw()
+    {
+      return fmt::format("Unknown particle type: {}", m_particle).c_str();
+    }
+    virtual const char* type() const throw() { return "unknown_particle_error"; }
+
+  private:
+    const std::string m_particle;
+  };
+
+  // Simple function to return the appropriate PDG mass for the particles
+  // we care about for this process.
+  // FIXME: consider something more robust (maybe based on hepPDT) to the
+  //        analysis utility library
+  inline double get_pdg_mass(std::string_view part)
+  {
+    if (part == "electron") {
+      return 0.0005109989461;
+    } else if (part == "muon") {
+      return .1056583745;
+    } else if (part == "jpsi") {
+      return 3.0969;
+    } else if (part == "upsilon") {
+      return 9.49630;
+    } else if (part == "proton"){
+      return 0.938272;
+    } else {
+      throw unknown_particle_error{part};
+    }
+  }
+
+  // Get a vector of 4-momenta from raw tracking info, using an externally
+  // provided particle mass assumption.
+  inline auto momenta_from_tracking(const std::vector<eic::TrackParametersData>& tracks,
+                                    const double                                 mass)
+  {
+    std::vector<ROOT::Math::PxPyPzMVector> momenta{tracks.size()};
+    // transform our raw tracker info into proper 4-momenta
+    std::transform(tracks.begin(), tracks.end(), momenta.begin(), [mass](const auto& track) {
+      // make sure we don't divide by zero
+      if (fabs(track.qOverP) < 1e-9) {
+        return ROOT::Math::PxPyPzMVector{};
+      }
+      const double p  = fabs(1. / track.qOverP);
+      const double px = p * cos(track.phi) * sin(track.theta);
+      const double py = p * sin(track.phi) * sin(track.theta);
+      const double pz = p * cos(track.theta);
+      return ROOT::Math::PxPyPzMVector{px, py, pz, mass};
+    });
+    return momenta;
+  }
+
+  // Get a vector of 4-momenta from the simulation data.
+  // TODO: Add PID selector (maybe using ranges?)
+  inline auto momenta_from_simulation(const std::vector<dd4pod::Geant4ParticleData>& parts)
+  {
+    std::vector<ROOT::Math::PxPyPzMVector> momenta{parts.size()};
+    // transform our simulation particle data into 4-momenta
+    std::transform(parts.begin(), parts.end(), momenta.begin(), [](const auto& part) {
+      return ROOT::Math::PxPyPzMVector{part.psx, part.psy, part.psz, part.mass};
+    });
+    return momenta;
+  }
+
+  // Find the decay pair candidates from a vector of particles (parts),
+  // with invariant mass closest to a desired value (pdg_mass)
+  inline std::pair<ROOT::Math::PxPyPzMVector, ROOT::Math::PxPyPzMVector>
+  find_decay_pair(const std::vector<ROOT::Math::PxPyPzMVector>& parts, const double pdg_mass)
+  {
+    int    first     = -1;
+    int    second    = -1;
+    double best_mass = -1;
+
+    // go through all particle combinatorics, calculate the invariant mass
+    // for each combination, and remember which combination is the closest
+    // to the desired pdg_mass
+    for (size_t i = 0; i < parts.size(); ++i) {
+      for (size_t j = i + 1; j < parts.size(); ++j) {
+        const double new_mass{(parts[i] + parts[j]).mass()};
+        if (fabs(new_mass - pdg_mass) < fabs(best_mass - pdg_mass)) {
+          first     = i;
+          second    = j;
+          best_mass = new_mass;
+        }
+      }
+    }
+    if (first < 0) {
+      return {{}, {}};
+    }
+    return {parts[first], parts[second]};
+  }
+
+  // Calculate the magnitude of the momentum of a vector of 4-vectors
+  inline auto mom(const std::vector<ROOT::Math::PxPyPzMVector>& momenta)
+  {
+    std::vector<double> P(momenta.size());
+    // transform our raw tracker info into proper 4-momenta
+    std::transform(momenta.begin(), momenta.end(), P.begin(),
+                   [](const auto& mom) { return mom.P(); });
+    return P;
+  }
+  // Calculate the transverse momentum of a vector of 4-vectors
+  inline auto pt(const std::vector<ROOT::Math::PxPyPzMVector>& momenta)
+  {
+    std::vector<double> pt(momenta.size());
+    // transform our raw tracker info into proper 4-momenta
+    std::transform(momenta.begin(), momenta.end(), pt.begin(),
+                   [](const auto& mom) { return mom.pt(); });
+    return pt;
+  }
+
+  // Calculate the azimuthal angle phi of a vector of 4-vectors
+  inline auto phi(const std::vector<ROOT::Math::PxPyPzMVector>& momenta)
+  {
+    std::vector<double> phi(momenta.size());
+    // transform our raw tracker info into proper 4-momenta
+    std::transform(momenta.begin(), momenta.end(), phi.begin(),
+                   [](const auto& mom) { return mom.phi(); });
+    return phi;
+  }
+  // Calculate the pseudo-rapidity of a vector of particles
+  inline auto eta(const std::vector<ROOT::Math::PxPyPzMVector>& momenta)
+  {
+    std::vector<double> eta(momenta.size());
+    // transform our raw tracker info into proper 4-momenta
+    std::transform(momenta.begin(), momenta.end(), eta.begin(),
+                   [](const auto& mom) { return mom.eta(); });
+    return eta;
+  }
+
+  //=========================================================================================================
+
+} // namespace util
+
+#endif
diff --git a/options/env.sh b/options/env.sh
new file mode 100755
index 0000000000000000000000000000000000000000..bf20ba41020c4b9d91f9d8e6d87a7d1a7ff6f52a
--- /dev/null
+++ b/options/env.sh
@@ -0,0 +1,110 @@
+#!/bin/bash
+
+## =============================================================================
+## Global configuration variables for the benchmark scripts
+## The script defines the following environment variables that are meant to
+## be overriden by the Gitlab continuous integration (CI)
+##
+##  - JUGGLER_DETECTOR:       detector package to be used for the benchmark
+##  - JUGGLER_N_EVENTS:       #events processed by simulation/reconstruction
+##  - JUGGLER_INSTALL_PREFIX: location where Juggler (digi/recon) is installed
+##  - JUGGLER_N_THREADS:      Number of threads/processes to spawn in parallel
+##  - JUGGLER_RNG_SEED:       Random seed for the RNG
+##
+## It also defines the following additional variables for internal usage
+##  - LOCAL_PREFIX:           prefix for packages installed during the benchmark
+##  - DETECTOR_PREFIX:        prefix for the detector definitions
+##  - DETECTOR_PATH:          actual path with the detector definitions
+##
+## Finally, it makes sure LOCAL_PREFIX and JUGGLER_PREFIX are added to PATH
+## and LD_LIBRARY_PATH
+## =============================================================================
+
+echo "Setting up the Physics Benchmarks environment"
+
+## =============================================================================
+## Default variable definitions, normally these should be set
+## by the CI. In case of local development you may want to change these
+## in case you would like to modify the detector package or
+## number of events to be analyzed during the benchmark
+
+## Detector package to be used during the benchmark process
+if [ ! -n  "${JUGGLER_DETECTOR}" ] ; then 
+  export JUGGLER_DETECTOR="topside"
+fi
+
+if [ ! -n  "${JUGGLER_DETECTOR_VERSION}" ] ; then 
+  export JUGGLER_DETECTOR_VERSION="v0.0.1"
+fi
+
+
+## Number of events that will be processed by the reconstruction
+if [ ! -n  "${JUGGLER_N_EVENTS}" ] ; then 
+  export JUGGLER_N_EVENTS=100
+fi
+
+## Maximum number of threads or processes a single pipeline should use
+## (this is not enforced, but the different pipeline scripts should use
+##  this to guide the number of parallel processes or threads they 
+##  spawn).
+if [ ! -n "${JUGGLER_N_THREADS}" ]; then
+  export JUGGLER_N_THREADS=10
+fi
+
+## Random seed for event generation, should typically not be changed for
+## reproductability.
+if [ ! -n "${JUGGLER_RNG_SEED}" ]; then
+  export JUGGLER_RNG_SEED=1
+fi
+
+## Install prefix for juggler, needed to locate the Juggler xenv files.
+## Also used by the CI as install prefix for other packages where needed.
+## You should not have to touch this. Note that for local usage a different 
+## prefix structure is automatically used.
+if [ ! -n  "${JUGGLER_INSTALL_PREFIX}" ] ; then 
+  export JUGGLER_INSTALL_PREFIX="/usr/local"
+fi
+## Ensure the juggler prefix is an absolute path
+export JUGGLER_INSTALL_PREFIX=`realpath ${JUGGLER_INSTALL_PREFIX}`
+
+
+## =============================================================================
+## Other utility variables that govern how some of the dependent packages
+## are built and installed. You should not have to change these.
+
+## local prefix to be used for local storage of packages
+## downloaded/installed during the benchmark process
+LOCAL_PREFIX=".local"
+mkdir -p ${LOCAL_PREFIX}
+export LOCAL_PREFIX=`realpath ${LOCAL_PREFIX}`
+
+## detector prefix: prefix for the detector definitions
+export DETECTOR_PREFIX="${LOCAL_PREFIX}/detector"
+mkdir -p ${DETECTOR_PREFIX}
+
+## detector path: actual detector definition path
+export DETECTOR_PATH="${DETECTOR_PREFIX}/${JUGGLER_DETECTOR}"
+
+## build dir for ROOT to put its binaries etc.
+export ROOT_BUILD_DIR=$LOCAL_PREFIX/root_build
+
+echo "JUGGLER_DETECTOR:           ${JUGGLER_DETECTOR}"
+echo "JUGGLER_DETECTOR_VERSION:   ${JUGGLER_DETECTOR_VERSION}"
+echo "JUGGLER_N_EVENTS:           ${JUGGLER_N_EVENTS}"
+echo "JUGGLER_N_THREADS:          ${JUGGLER_N_THREADS}"
+echo "JUGGLER_RNG_SEED:           ${JUGGLER_RNG_SEED}"
+echo "JUGGLER_INSTALL_PREFIX:     ${JUGGLER_INSTALL_PREFIX}"
+echo "LOCAL_PREFIX:               ${LOCAL_PREFIX}"
+echo "DETECTOR_PREFIX:            ${DETECTOR_PREFIX}"
+echo "DETECTOR_PATH:              ${DETECTOR_PATH}"
+echo "ROOT_BUILD_DIR:             ${ROOT_BUILD_DIR}"
+
+## =============================================================================
+## Setup PATH and LD_LIBRARY_PATH to include our prefixes
+echo "Adding JUGGLER_INSTALL_PREFIX and LOCAL_PREFIX to PATH and LD_LIBRARY_PATH"
+export PATH=${JUGGLER_INSTALL_PREFIX}/bin:${LOCAL_PREFIX}/bin:${PATH}
+export LD_LIBRARY_PATH=${JUGGLER_INSTALL_PREFIX}/lib:${LOCAL_PREFIX}/lib:${LD_LIBRARY_PATH}
+
+## =============================================================================
+## That's all!
+echo "Environment setup complete."
diff --git a/options/tracker_reconstruction.py b/options/tracker_reconstruction.py
new file mode 100644
index 0000000000000000000000000000000000000000..3101824c52cada8cfe524da2dfb8e9c3b027af99
--- /dev/null
+++ b/options/tracker_reconstruction.py
@@ -0,0 +1,237 @@
+from Gaudi.Configuration import *
+
+from GaudiKernel.DataObjectHandleBase import DataObjectHandleBase
+from Configurables import ApplicationMgr, EICDataSvc, PodioOutput, GeoSvc
+from GaudiKernel import SystemOfUnits as units
+
+detector_name = "topside"
+if "JUGGLER_DETECTOR" in os.environ :
+  detector_name = str(os.environ["JUGGLER_DETECTOR"])
+
+# todo add checks
+input_sim_file  = str(os.environ["JUGGLER_SIM_FILE"])
+output_rec_file = str(os.environ["JUGGLER_REC_FILE"])
+n_events = str(os.environ["JUGGLER_N_EVENTS"])
+
+detector_path = detector_name
+if "DETECTOR_PATH" in os.environ :
+    detector_path = str(os.environ["DETECTOR_PATH"])
+
+geo_service  = GeoSvc("GeoSvc",
+        detectors=["{}/{}.xml".format(detector_path, detector_name)])
+podioevent   = EICDataSvc("EventDataSvc", inputs=[input_sim_file], OutputLevel=DEBUG)
+
+from Configurables import PodioInput
+from Configurables import Jug__Base__InputCopier_dd4pod__Geant4ParticleCollection_dd4pod__Geant4ParticleCollection_ as MCCopier
+from Configurables import Jug__Base__InputCopier_dd4pod__CalorimeterHitCollection_dd4pod__CalorimeterHitCollection_ as CalCopier
+from Configurables import Jug__Base__InputCopier_dd4pod__TrackerHitCollection_dd4pod__TrackerHitCollection_ as TrkCopier
+
+from Configurables import Jug__Digi__ExampleCaloDigi as ExampleCaloDigi
+from Configurables import Jug__Digi__UFSDTrackerDigi as UFSDTrackerDigi
+from Configurables import Jug__Digi__EMCalorimeterDigi as EMCalorimeterDigi
+
+from Configurables import Jug__Base__MC2DummyParticle as MC2DummyParticle
+
+from Configurables import Jug__Reco__TrackerHitReconstruction as TrackerHitReconstruction
+
+from Configurables import Jug__Reco__TrackerSourceLinker as TrackerSourceLinker
+from Configurables import Jug__Reco__Tracker2SourceLinker as Tracker2SourceLinker
+#from Configurables import Jug__Reco__TrackerSourcesLinker as TrackerSourcesLinker
+#from Configurables import Jug__Reco__TrackingHitsSourceLinker as TrackingHitsSourceLinker
+from Configurables import Jug__Reco__TrackParamTruthInit as TrackParamTruthInit
+from Configurables import Jug__Reco__TrackParamClusterInit as TrackParamClusterInit
+from Configurables import Jug__Reco__TrackParamVertexClusterInit as TrackParamVertexClusterInit
+
+from Configurables import Jug__Reco__TrackFindingAlgorithm as TrackFindingAlgorithm
+from Configurables import Jug__Reco__ParticlesFromTrackFit as ParticlesFromTrackFit
+from Configurables import Jug__Reco__EMCalReconstruction as EMCalReconstruction
+
+from Configurables import Jug__Reco__SimpleClustering as SimpleClustering
+
+
+
+podioinput = PodioInput("PodioReader", 
+                        collections=["mcparticles","SiTrackerEndcapHits","SiTrackerBarrelHits","EcalBarrelHits"])#, OutputLevel=DEBUG)
+#"SiVertexBarrelHits",
+
+dummy = MC2DummyParticle("MC2Dummy",
+        inputCollection="mcparticles",
+        outputCollection="DummyReconstructedParticles")
+
+## copiers to get around input --> output copy bug. Note the "2" appended to the output collection.
+copier = MCCopier("MCCopier", 
+        inputCollection="mcparticles", 
+        outputCollection="mcparticles2") 
+trkcopier = TrkCopier("TrkCopier", 
+        inputCollection="SiTrackerBarrelHits", 
+        outputCollection="SiTrackerBarrelHits2") 
+
+ecal_digi = EMCalorimeterDigi("ecal_digi", 
+        inputHitCollection="EcalBarrelHits", 
+        outputHitCollection="RawEcalBarrelHits")
+
+ufsd_digi = UFSDTrackerDigi("ufsd_digi", 
+        inputHitCollection="SiTrackerBarrelHits",
+        outputHitCollection="SiTrackerBarrelRawHits",
+        timeResolution=8)
+ufsd_digi2 = UFSDTrackerDigi("ufsd_digi2", 
+        inputHitCollection="SiTrackerEndcapHits",
+        outputHitCollection="SiTrackerEndcapRawHits",
+        timeResolution=8)
+
+#vtx_digi = UFSDTrackerDigi("vtx_digi", 
+#        inputHitCollection="SiVertexBarrelHits",
+#        outputHitCollection="SiVertexBarrelRawHits",
+#        timeResolution=8)
+
+
+ecal_reco = EMCalReconstruction("ecal_reco", 
+        inputHitCollection="RawEcalBarrelHits", 
+        outputHitCollection="RecEcalBarrelHits",
+        minModuleEdep=0.0*units.MeV,
+        OutputLevel=DEBUG)
+
+simple_cluster = SimpleClustering("simple_cluster", 
+        inputHitCollection="RecEcalBarrelHits", 
+        outputClusters="SimpleClusters",
+        minModuleEdep=1.0*units.MeV,
+        maxDistance=50.0*units.cm,
+        OutputLevel=DEBUG)
+
+trk_barrel_reco = TrackerHitReconstruction("trk_barrel_reco",
+        inputHitCollection="SiTrackerBarrelRawHits",
+        outputHitCollection="TrackerBarrelRecHits")
+
+trk_endcap_reco = TrackerHitReconstruction("trk_endcap_reco",
+        inputHitCollection="SiTrackerEndcapRawHits",
+        outputHitCollection="TrackerEndcapRecHits")
+
+#vtx_barrel_reco = TrackerHitReconstruction("vtx_barrel_reco",
+#        inputHitCollection = vtx_digi.outputHitCollection,
+#        outputHitCollection="VertexBarrelRecHits")
+
+# Source linker 
+sourcelinker = TrackerSourceLinker("trk_srclinker",
+        inputHitCollection="TrackerBarrelRecHits",
+        outputSourceLinks="BarrelTrackSourceLinks",
+        OutputLevel=DEBUG)
+
+trk_hits_srclnkr = Tracker2SourceLinker("trk_hits_srclnkr",
+        TrackerBarrelHits="TrackerBarrelRecHits",
+        TrackerEndcapHits="TrackerEndcapRecHits",
+        outputMeasurements="lnker2Measurements",
+        outputSourceLinks="lnker2Links",
+        allTrackerHits="linker2AllHits",
+        OutputLevel=DEBUG)
+
+## Track param init
+truth_trk_init = TrackParamTruthInit("truth_trk_init",
+        inputMCParticles="mcparticles",
+        outputInitialTrackParameters="InitTrackParams",
+        OutputLevel=DEBUG)
+
+clust_trk_init = TrackParamClusterInit("clust_trk_init",
+        inputClusters="SimpleClusters",
+        outputInitialTrackParameters="InitTrackParamsFromClusters",
+        OutputLevel=DEBUG)
+
+#vtxcluster_trk_init = TrackParamVertexClusterInit("vtxcluster_trk_init",
+#        inputVertexHits="VertexBarrelRecHits",
+#        inputClusters="SimpleClusters",
+#        outputInitialTrackParameters="InitTrackParamsFromVtxClusters",
+#        maxHitRadius=40.0*units.mm,
+#        OutputLevel=DEBUG)
+
+# Tracking algorithms
+trk_find_alg = TrackFindingAlgorithm("trk_find_alg",
+        inputSourceLinks = sourcelinker.outputSourceLinks,
+        inputMeasurements = sourcelinker.outputMeasurements,
+        inputInitialTrackParameters= "InitTrackParams",#"InitTrackParamsFromClusters", 
+        outputTrajectories="trajectories",
+        OutputLevel=DEBUG)
+parts_from_fit = ParticlesFromTrackFit("parts_from_fit",
+        inputTrajectories="trajectories",
+        outputParticles="ReconstructedParticles",
+        outputTrackParameters="outputTrackParameters",
+        OutputLevel=DEBUG)
+
+trk_find_alg1 = TrackFindingAlgorithm("trk_find_alg1",
+        inputSourceLinks = trk_hits_srclnkr.outputSourceLinks,
+        inputMeasurements = trk_hits_srclnkr.outputMeasurements,
+        inputInitialTrackParameters= "InitTrackParamsFromClusters", 
+        outputTrajectories="trajectories1",
+        OutputLevel=DEBUG)
+parts_from_fit1 = ParticlesFromTrackFit("parts_from_fit1",
+        inputTrajectories="trajectories1",
+        outputParticles="ReconstructedParticles1",
+        outputTrackParameters="outputTrackParameters1",
+        OutputLevel=DEBUG)
+
+trk_find_alg2 = TrackFindingAlgorithm("trk_find_alg2",
+        inputSourceLinks = trk_hits_srclnkr.outputSourceLinks,
+        inputMeasurements = trk_hits_srclnkr.outputMeasurements,
+        inputInitialTrackParameters= "InitTrackParams",#"InitTrackParamsFromClusters", 
+        #inputInitialTrackParameters= "InitTrackParamsFromVtxClusters", 
+        outputTrajectories="trajectories2",
+        OutputLevel=DEBUG)
+parts_from_fit2 = ParticlesFromTrackFit("parts_from_fit2",
+        inputTrajectories="trajectories2",
+        outputParticles="ReconstructedParticles2",
+        outputTrackParameters="outputTrackParameters2",
+        OutputLevel=DEBUG)
+
+
+#types = []
+## this printout is useful to check that the type information is passed to python correctly
+#print("---------------------------------------\n")
+#print("---\n# List of input and output types by class")
+#for configurable in sorted([ PodioInput, EICDataSvc, PodioOutput,
+#                             TrackerHitReconstruction,ExampleCaloDigi, 
+#                             UFSDTrackerDigi, TrackerSourceLinker,
+#                             PodioOutput],
+#                           key=lambda c: c.getType()):
+#    print("\"{}\":".format(configurable.getType()))
+#    props = configurable.getDefaultProperties()
+#    for propname, prop in sorted(props.items()):
+#        print(" prop name: {}".format(propname))
+#        if isinstance(prop, DataObjectHandleBase):
+#            types.append(prop.type())
+#            print("  {}: \"{}\"".format(propname, prop.type()))
+#print("---")
+
+out = PodioOutput("out", filename=output_rec_file)
+out.outputCommands = ["keep *", 
+        "drop BarrelTrackSourceLinks", 
+        "drop InitTrackParams",
+        "drop trajectories",
+        "drop outputSourceLinks",
+        "drop outputInitialTrackParameters",
+        "drop mcparticles"
+        ]
+
+ApplicationMgr(
+    TopAlg = [podioinput, 
+              dummy,
+              copier, trkcopier,
+              ecal_digi, ufsd_digi2,ufsd_digi, #vtx_digi, 
+              ecal_reco, 
+              simple_cluster,
+              trk_barrel_reco, 
+              trk_endcap_reco, 
+              #vtx_barrel_reco, 
+              sourcelinker, trk_hits_srclnkr,
+              clust_trk_init, 
+              truth_trk_init, 
+              #vtxcluster_trk_init, 
+              trk_find_alg, parts_from_fit,
+              trk_find_alg1, parts_from_fit1,
+              trk_find_alg2, parts_from_fit2,
+              out
+              ],
+    EvtSel = 'NONE',
+    EvtMax   = n_events,
+    ExtSvc = [podioevent,geo_service],
+    OutputLevel=DEBUG
+ )
+
+
diff --git a/rich/forward_hadrons.sh b/rich/forward_hadrons.sh
deleted file mode 100644
index c4248ff0746cf8dcfc94d4588426b11c22a02105..0000000000000000000000000000000000000000
--- a/rich/forward_hadrons.sh
+++ /dev/null
@@ -1,61 +0,0 @@
-#!/bin/bash
-
-if [[ ! -n  "${JUGGLER_DETECTOR}" ]] ; then
-  export JUGGLER_DETECTOR="topside"
-fi
-
-if [[ ! -n  "${JUGGLER_N_EVENTS}" ]] ; then
-  export JUGGLER_N_EVENTS=100
-fi
-
-export JUGGLER_FILE_NAME_TAG="rich_forward_hadrons"
-export JUGGLER_GEN_FILE="${JUGGLER_FILE_NAME_TAG}.hepmc"
-
-export JUGGLER_SIM_FILE="sim_${JUGGLER_FILE_NAME_TAG}.root"
-export JUGGLER_REC_FILE="rec_${JUGGLER_FILE_NAME_TAG}.root"
-
-echo "JUGGLER_N_EVENTS = ${JUGGLER_N_EVENTS}"
-echo "JUGGLER_DETECTOR = ${JUGGLER_DETECTOR}"
-
-
-# Build the detector constructors.
-git clone https://eicweb.phy.anl.gov/EIC/detectors/${JUGGLER_DETECTOR}.git
-mkdir ${JUGGLER_DETECTOR}/build
-pushd ${JUGGLER_DETECTOR}/build
-cmake ../. -DCMAKE_INSTALL_PREFIX=/usr/local && make -j30 install
-popd
-
-# generate the input events
-# note datasets is now only used to develop datasets.
-#git clone https://eicweb.phy.anl.gov/EIC/datasets.git datasets
-python rich/scripts/rich_data_gen.py \
-       ${JUGGLER_FILE_NAME_TAG}.hepmc \
-       -n ${JUGGLER_N_EVENTS} \
-       --pmin 5.0 \
-       --pmax 100.0 \
-       --angmin 3.0 \
-       --angmax 8.0
-
-pushd ${JUGGLER_DETECTOR}
-ls -l
-# run geant4 simulations
-python options/ForwardRICH/simu.py \
-       --compact=${JUGGLER_DETECTOR}.xml \
-       -i ../${JUGGLER_FILE_NAME_TAG}.hepmc \
-       -o ${JUGGLER_SIM_FILE}
-       -n ${JUGGLER_N_EVENTS}
-
-# @TODO changeable simulation file name and detector xml file name
-xenv -x /usr/local/Juggler.xenv gaudirun.py ../rich/options/rich_reco.py
-ls -l
-popd
-
-# @TODO add analysis scripts
-#root_filesize=$(stat --format=%s "${JUGGLER_DETECTOR}/${JUGGLER_REC_FILE}")
-#if [[ "${JUGGLER_N_EVENTS}" -lt "500" ]] ; then 
-#  # file must be less than 10 MB to upload
-#  if [[ "${root_filesize}" -lt "10000000" ]] ; then 
-#    cp ${JUGGLER_DETECTOR}/${JUGGLER_REC_FILE} results/.
-#  fi
-#fi
-
diff --git a/util/build_detector.sh b/util/build_detector.sh
new file mode 100755
index 0000000000000000000000000000000000000000..bccf765b24b79d9aa00d5075a1c20f321abfb1fd
--- /dev/null
+++ b/util/build_detector.sh
@@ -0,0 +1,64 @@
+#!/bin/bash
+
+## =============================================================================
+## Build and install the JUGGLER_DETECTOR detector package into our local prefix
+## =============================================================================
+
+## make sure we launch this script from the project root directory
+PROJECT_ROOT="$( cd "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )"/..
+pushd ${PROJECT_ROOT}
+
+## =============================================================================
+## Load the environment variables. To build the detector we need the following
+## variables:
+##
+## - JUGGLER_DETECTOR: the detector package we want to use for this benchmark
+## - LOCAL_PREFIX:     location where local packages should be installed
+## - DETECTOR_PREFIX:  prefix for the detector definitions 
+## - DETECTOR_PATH:    full path for the detector definitions
+##                     this is the same as ${DETECTOR_PREFIX}/${JUGGLER_DETECTOR}
+##
+## You can read options/env.sh for more in-depth explanations of the variables
+## and how they can be controlled.
+source options/env.sh
+
+## =============================================================================
+## Step 1: download/update the detector definitions (if needed)
+pushd ${DETECTOR_PREFIX}
+
+## We need an up-to-date copy of the detector
+if [ ! -d ${JUGGLER_DETECTOR} ]; then
+  echo "Fetching ${JUGGLER_DETECTOR}"
+  git clone -b ${JUGGLER_DETECTOR_VERSION} https://eicweb.phy.anl.gov/EIC/detectors/${JUGGLER_DETECTOR}.git
+else
+  echo "Updating ${JUGGLER_DETECTOR}"
+  pushd ${JUGGLER_DETECTOR}
+  git pull --ff-only
+  popd
+fi
+## We also need an up-to-date copy of the accelerator. For now this is done
+## manually. Down the road we could maybe automize this with cmake
+if [ ! -d accelerator ]; then
+  echo "Fetching accelerator"
+  git clone https://eicweb.phy.anl.gov/EIC/detectors/accelerator.git
+else
+  echo "Updating accelerator"
+  pushd accelerator
+  git pull --ff-only
+  popd
+fi
+## Now symlink the accelerator definition into the detector definition
+echo "Linking accelerator definition into detector definition"
+ln -s -f ${DETECTOR_PREFIX}/accelerator/eic ${DETECTOR_PATH}/eic
+
+## =============================================================================
+## Step 2: Compile and install the detector definition
+echo "Building and installing the ${JUGGLER_DETECTOR} package"
+
+mkdir -p ${DETECTOR_PREFIX}/build
+pushd ${DETECTOR_PREFIX}/build
+cmake ${DETECTOR_PATH} -DCMAKE_INSTALL_PREFIX=${LOCAL_PREFIX} && make -j30 install
+
+## =============================================================================
+## Step 3: That's all!
+echo "Detector build/install complete!"
diff --git a/util/collect_benchmarks.py b/util/collect_benchmarks.py
new file mode 100755
index 0000000000000000000000000000000000000000..0af7e9a12b37eb7616be3400d4046f0088bcf223
--- /dev/null
+++ b/util/collect_benchmarks.py
@@ -0,0 +1,181 @@
+#!/usr/bin/env python3
+
+"""
+Combine the json files from the individual benchmark tests into
+a final master json file combining all benchmarks.
+
+Benchmark results are expected to be all json files in the results
+directory.
+"""
+
+## Our master definition file, the benchmark project directory
+MASTER_FILE=r'benchmarks/benchmarks.json'
+
+## Our results directory
+RESULTS_PATH=r'results'
+
+## Output json file with all benchmark results
+OUTPUT_FILE=r'results/summary.json'
+
+import argparse
+import json
+from pathlib import Path
+
+## Exceptions for this module
+class Error(Exception):
+    '''Base class for exceptions in this module.'''
+    pass
+class FileNotFoundError(Error):
+    '''File does not exist.
+
+    Attributes:
+        file: the file name
+        message: error message
+    '''
+    def __init__(self, file):
+        self.file = file
+        self.message = 'No such file or directory: {}'.format(file)
+
+class InvalidDefinitionError(Error):
+    '''Raised for missing keys in the definitions.
+
+    Attributes:
+        key: the missing key
+        file: the definition file
+        message: error message
+    '''
+    def __init__(self, key, file):
+        self.key = key
+        self.file = file
+        self.message = "key '{}' not found in '{}'".format(key, file)
+
+class InvalidResultError(Error):
+    '''Raised for invalid benchmark result value.
+
+    Attributes:
+        key: the missing key
+        value: the invalid value
+        file: the benchmark definition file
+        message: error message
+    '''
+    def __init__(self, key, value, file):
+        self.key = key
+        self.value = value
+        self.file = file
+        self.message = "value '{}' for key '{}' invalid in benchmark file '{}'".format(
+                value, key, file)
+    
+def collect_benchmarks():
+    '''Collect all benchmark results and write results to a single file.'''
+    print("Collecting all benchmark results")
+
+    ## load the test definition for this benchmark
+    results = _load_master()
+
+    ## collect the test results
+    results['benchmarks'] = _load_benchmarks()
+    
+    ## calculate aggregate test statistics
+    results = _aggregate_results(results)
+
+    ## save results to output file
+    _save(results)
+
+    ## Summarize results
+    for bm in results['benchmarks']:
+        _print_benchmark(bm)
+    _print_summary(results)
+
+def _load_master():
+    '''Load master definition.'''
+    master_file = Path(MASTER_FILE)
+    if not master_file.exists():
+        raise FileNotFoundError(master_file)
+    print('  --> Loading master definition from:', master_file)
+    results = None
+    with master_file.open() as f:
+        results = json.load(f)
+    ## ensure this is a valid benchmark file
+    for key in ('name', 'title', 'description'):
+        if not key in results:
+            raise InvalidDefinitionError('target', master_file)
+    return results
+
+def _load_benchmarks():
+    '''Load all benchmark results from the results folder.'''
+    print('  --> Collecting all benchmarks')
+    rootdir = Path(RESULTS_PATH)
+    results = []
+    for file in rootdir.glob('*.json'):
+        print('    --> Loading file:', file, '... ', end='')
+        with open(file) as f:
+            bm = json.load(f)
+            ## skip files that don't include test results
+            if not 'tests' in bm:
+                print('skipped (does not contain benchmark results).')
+                continue
+            ## check if these are valid benchmark results,
+            ## raise exception otherwise
+            for key in ('name', 'title', 'description', 'target', 'n_tests',
+                    'n_pass', 'n_fail', 'n_error', 'maximum', 'sum', 'value',
+                    'result'):
+                if not key in bm:
+                    raise InvalidDefinitionError(key, file)
+            if bm['result'] not in ('pass', 'fail', 'error'):
+                raise InvalidResultError('result', bm['result'], file)
+            ## Append to our test results
+            results.append(bm)
+            print('done')
+    return results
+
+def _aggregate_results(results):
+    '''Aggregate benchmark results.'''
+    print('  --> Aggregating benchmark statistics')
+    results['n_benchmarks'] = len(results['benchmarks'])
+    results['n_pass'] = len([1 for t in results['benchmarks'] if t['result'] == 'pass'])
+    results['n_fail'] = len([1 for t in results['benchmarks'] if t['result'] == 'fail'])
+    results['n_error'] = len([1 for t in results['benchmarks'] if t['result'] == 'error'])
+    if results['n_error'] > 0:
+        results['result'] = 'error'
+    elif results['n_fail'] == 0:
+        results['result'] = 'pass'
+    else:
+        results['result'] = 'fail'
+    return results
+
+def _save(results):
+    '''Save aggregated benchmark results'''
+    ofile = Path(OUTPUT_FILE)
+    print('  --> Saving results to:', ofile)
+    with ofile.open('w') as f:
+        json.dump(results, f, indent=4)
+
+def _print_benchmark(bm):
+    '''Print benchmark summary to the terminal.'''
+    print('====================================================================')
+    print('  Summary for:', bm['title'])
+    print('  Pass: {}, Fail: {}, Error: {} out of {} total tests'.format(
+        bm['n_pass'], bm['n_fail'], bm['n_error'],
+        bm['n_tests']))
+    print('  Weighted sum: {} / {}'.format(bm['sum'], bm['maximum']))
+    print('  kBenchmark value: {} (target: {})'.format(
+        bm['value'], bm['target']))
+    print('  ===> status:', bm['result'])
+
+def _print_summary(results):
+    '''Print master benchmark summary to the terminal.'''
+    print('====================================================================')
+    print('MASTER BENCHMARK SUMMARY FOR:', results['title'].upper())
+    print('Pass: {}, Fail: {}, Error: {} out of {} total benchmarks'.format(
+        results['n_pass'], results['n_fail'], results['n_error'],
+        results['n_benchmarks']))
+    print('===> status:', results['result'])
+    print('====================================================================')
+
+
+if __name__ == "__main__":
+    try:
+        collect_benchmarks()
+    except Error as e:
+        print()
+        print('ERROR', e.message)
diff --git a/util/collect_tests.py b/util/collect_tests.py
new file mode 100755
index 0000000000000000000000000000000000000000..4d860ca79d9f996204a5ca9dc447fa10ed8ec4f4
--- /dev/null
+++ b/util/collect_tests.py
@@ -0,0 +1,204 @@
+#!/usr/bin/env python3
+
+"""
+Collect the json files from individual benchmark tests into
+a larger json file that combines all benchmark information,
+and do additional accounting for the benchmark.
+
+Tests results are expected to have the following file name and directory
+structure:
+   results/<BENCHMARK_NAME>/**/<SOME_NAME>.json
+where ** implies we check recursively check all sub-directories of <BENCHMARK_NAME>
+
+Internally, we will look for the "tests" keyword in each of these
+files to identify them as benchmark components.
+"""
+
+## Our benchmark definition file, stored in the benchmark root directory
+BENCHMARK_FILE=r'benchmarks/{}/benchmark.json'
+
+## Our benchmark results directory
+RESULTS_PATH=r'results/{}'
+
+## Output json file with benchmark results
+OUTPUT_FILE=r'results/{}.json'
+
+import argparse
+import json
+from pathlib import Path
+
+## Exceptions for this module
+class Error(Exception):
+    '''Base class for exceptions in this module.'''
+    pass
+class FileNotFoundError(Exception):
+    '''File does not exist.
+
+    Attributes:
+        file: the file name
+        message: error message
+    '''
+    def __init__(self, file):
+        self.file = file
+        self.message = 'No such file or directory: {}'.format(file)
+
+class InvalidBenchmarkDefinitionError(Exception):
+    '''Raised for missing keys in the benchmark definition.
+
+    Attributes:
+        key: the missing key
+        file: the benchmark definition file
+        message: error message
+    '''
+    def __init__(self, key, file):
+        self.key = key
+        self.file = file
+        self.message = "key '{}' not found in benchmark file '{}'".format(key, file)
+
+class InvalidTestDefinitionError(Exception):
+    '''Raised for missing keys in the test result.
+
+    Attributes:
+        key: the missing key
+        file: the test result file
+        message: error message
+    '''
+    def __init__(self, key, file):
+        self.key = key
+        self.file = file
+        self.message = "key '{}' not found in test file '{}'".format(key, file)
+class InvalidTestResultError(Exception):
+    '''Raised for invalid test result value.
+
+    Attributes:
+        key: the missing key
+        value: the invalid value
+        file: the benchmark definition file
+        message: error message
+    '''
+    def __init__(self, key, value, file):
+        self.key = key
+        self.value = value
+        self.file = file
+        self.message = "value '{}' for key '{}' invalid in test file '{}'".format(
+                value, key, file)
+    
+    
+parser = argparse.ArgumentParser()
+parser.add_argument(
+        'benchmark',
+        action='append',
+        help='One or more benchmarks for which to collect test results.')
+
+def collect_results(benchmark):
+    '''Collect benchmark tests and write results to file.'''
+    print("Collecting results for benchmark '{}'".format(benchmark))
+
+    ## load the test definition for this benchmark
+    results = _load_benchmark(benchmark)
+
+    ## collect the test results
+    results['tests'] = _load_tests(benchmark)
+    
+    ## calculate aggregate test statistics
+    results = _aggregate_results(results)
+
+    ## save results to output file
+    _save(benchmark, results)
+
+    ## Summarize results
+    _print_summary(results)
+
+def _load_benchmark(benchmark):
+    '''Load benchmark definition.'''
+    benchfile = Path(BENCHMARK_FILE.format(benchmark))
+    if not benchfile.exists():
+        raise FileNotFoundError(benchfile)
+    print('  --> Loading benchmark definition from:', benchfile)
+    results = None
+    with benchfile.open() as f:
+        results = json.load(f)
+    ## ensure this is a valid benchmark file
+    for key in ('name', 'title', 'description', 'target'):
+        if not key in results:
+            raise InvalidBenchmarkDefinitionError('target', benchfile)
+    return results
+
+def _load_tests(benchmark):
+    '''Loop over all test results in benchmark folder and return results.'''
+    print('  --> Collecting all test results')
+    rootdir = Path(RESULTS_PATH.format(benchmark))
+    results = []
+    for file in rootdir.glob('**/*.json'):
+        print('    --> Loading file:', file, '... ', end='')
+        with open(file) as f:
+            new_results = json.load(f)
+            ## skip files that don't include test results
+            if not 'tests' in new_results:
+                print('not a test result')
+                continue
+            ## check if these are valid test results,
+            ## raise exception otherwise
+            for test in new_results['tests']:
+                for key in ('name', 'title', 'description', 'quantity', 'target',
+                        'value', 'result'):
+                    if not key in test:
+                        raise InvalidTestDefinitionError(key, file)
+                if test['result'] not in ('pass', 'fail', 'error'):
+                    raise InvalidTestResultError('result', test['result'], file)
+                ## ensure 'weight' key present, defaulting to 1 in needed
+                if not 'weight' in test:
+                    test['weight'] = 1.
+                ## Append to our test results
+                results.append(test)
+            print('done')
+    return results
+
+def _aggregate_results(results):
+    '''Aggregate test results for our benchmark.'''
+    print('  --> Aggregating benchmark statistics')
+    results['target'] = float(results['target'])
+    results['n_tests'] = len(results['tests'])
+    results['n_pass'] = len([1 for t in results['tests'] if t['result'] == 'pass'])
+    results['n_fail'] = len([1 for t in results['tests'] if t['result'] == 'fail'])
+    results['n_error'] = len([1 for t in results['tests'] if t['result'] == 'error'])
+    results['maximum'] = sum([t['weight'] for t in results['tests']])
+    results['sum'] = sum([t['weight'] for t in results['tests'] if t['result'] == 'pass'])
+    if (results['n_tests'] > 0):
+        results['value'] = results['sum'] / results['maximum']
+        if results['n_error'] > 0:
+            results['result'] = 'error'
+        elif results['value'] >= results['target']:
+            results['result'] = 'pass'
+        else:
+            results['result'] = 'fail'
+    else:
+        results['value'] = -1
+        results['result'] = 'error'
+    return results
+
+def _save(benchmark, results):
+    '''Save benchmark results'''
+    ofile = Path(OUTPUT_FILE.format(benchmark))
+    print('  --> Saving benchmark results to:', ofile)
+    with ofile.open('w') as f:
+        json.dump(results, f, indent=4)
+
+def _print_summary(results):
+    '''Print benchmark summary to the terminal.'''
+    print('====================================================================')
+    print('Summary for:', results['title'])
+    print('Pass: {}, Fail: {}, Error: {} out of {} total tests'.format(
+        results['n_pass'], results['n_fail'], results['n_error'],
+        results['n_tests']))
+    print('Weighted sum: {} / {}'.format(results['sum'], results['maximum']))
+    print('Benchmark value: {} (target: {})'.format(
+        results['value'], results['target']))
+    print('===> status:', results['result'])
+    print('====================================================================')
+
+
+if __name__ == "__main__":
+    args = parser.parse_args()
+    for benchmark in args.benchmark:
+        collect_results(benchmark)
diff --git a/util/compile_analyses.py b/util/compile_analyses.py
new file mode 100755
index 0000000000000000000000000000000000000000..153f2ea2f61429b6864a21b3ad625e6b53373ba3
--- /dev/null
+++ b/util/compile_analyses.py
@@ -0,0 +1,119 @@
+#!/usr/bin/env python3
+
+"""
+Compile all root analysis scripts under
+benchmarks/<BENCHMARK>/analysis/*.cxx
+
+Doing this step here rather than during the main benchmark script has
+multiple advantages:
+    1. Get feedback on syntax errors early on, without wasting compute resources
+    2. Avoid race conditions for large benchmarks run in parallel
+    3. Make it easier to properly handle the root build directory, as
+       this has to exist prior to our attempt to compile, else all will
+       fail (this is probably an old bug in root...)
+
+Analysis scripts are expected to have extension 'cxx' and be located in the analysis
+subdirectory
+"""
+
+## Our analysis path and file extension for glob
+ANALYSIS_PATH=r'benchmarks/{}/analysis'
+ANALYSIS_EXT = r'cxx'
+
+import argparse
+import os
+from pathlib import Path
+
+## Exceptions for this module
+class Error(Exception):
+    '''Base class for exceptions in this module.'''
+    pass
+
+class PathNotFoundError(Exception):
+    '''Path does not exist.
+
+    Attributes:
+        path: the path name
+        message: error message
+    '''
+    def __init__(self, path):
+        self.file = file
+        self.message = 'No such directory: {}'.format(file)
+class NoAnalysesFoundError(Exception):
+    '''Did not find any analysis scripts to complile
+
+    Attributes:
+        path: the analysis path
+        message: error message
+    '''
+    def __init__(self, path):
+        self.file = file
+        self.message = 'No analysis found (extension \'{}\' in path: {}'.format(file,
+                ANALYSIS_EXT)
+
+class CompilationError(Exception):
+    '''Raised when we failed to compile an analysis script
+
+    Attributes:
+        file: analysis file name
+        path: analysis path
+        message: error message
+    '''
+    def __init__(self, file):
+        self.file = file
+        self.message = "Analysis '{}' failed to compile".format(file)
+
+parser = argparse.ArgumentParser()
+parser.add_argument(
+        'benchmark',
+        help='A benchmarks for which to compile the analysis scripts.')
+
+def compile_analyses(benchmark):
+    '''Compile all analysis scripts for a benchmark.'''
+    print("Compiling all analyis scripts for '{}'".format(benchmark))
+
+    ## Ensure our build directory exists
+    _init_build_dir(benchmark)
+
+    ## Get a list of all analysis scripts
+    _compile_all(benchmark)
+
+    ## All done!
+    print('All analyses for', benchmark, 'compiled successfully')
+
+def _init_build_dir(benchmark):
+    '''Initialize our ROOT build directory (if using one).'''
+    print(' --> Initializing ROOT build directory ...')
+    build_prefix = os.getenv('ROOT_BUILD_DIR')
+    if build_prefix is None:
+        print('    --> ROOT_BUILD_DIR not set, no action needed.')
+        return
+    ## deduce the root build directory
+    pwd = os.getenv('PWD')
+    build_dir = '{}/{}/{}'.format(build_prefix, pwd, ANALYSIS_PATH.format(benchmark))
+    print("    --> Ensuring directory '{}' exists".format(build_dir))
+    os.system('mkdir -p {}'.format(build_dir))
+
+def _compile_all(benchmark):
+    '''Compile all analysis for this benchmark.'''
+    print(' --> Compiling analysis scripts')
+    anadir = Path(ANALYSIS_PATH.format(benchmark))
+    if not anadir.exists():
+        raise PathNotFoundError(anadir)
+    ana_list = []
+    for file in anadir.glob('*.{}'.format(ANALYSIS_EXT)):
+        ana_list.append(file)
+        print('    --> Compiling:', file, flush=True)
+        err = os.system(_compile_cmd(file))
+        if err:
+            raise CompilationError(file)
+    if len(ana_list) == 0:
+        raise NoAnalysesFoundError(anadir)
+
+def _compile_cmd(file):
+    '''Return a one-line shell command to compile an analysis script.'''
+    return r'bash -c "root -q -b -e \".L {}+\""'.format(file)
+
+if __name__ == "__main__":
+    args = parser.parse_args()
+    compile_analyses(args.benchmark)
diff --git a/util/parse_cmd.sh b/util/parse_cmd.sh
new file mode 100755
index 0000000000000000000000000000000000000000..04028d8958d03df22f7b2d964a75acf2d9b47317
--- /dev/null
+++ b/util/parse_cmd.sh
@@ -0,0 +1,127 @@
+#!/bin/bash
+
+## =============================================================================
+## Generic utility script to parse command line arguments for the various
+## bash scripts that control the CI. This script should be source'd with
+## command line arguments from a bash-like (non-POSIX) shell such as
+## bash or zsh.
+##
+## To control some of the functionality of the script, you can set the following
+## environment variables prior to calling the script:
+##   - REQUIRE_DECAY:     require the --decay flag to be set
+## =============================================================================
+
+## Commented out because this should be taken care of by the 
+## calling script to not enforce a fixed directory structure.
+## make sure we launch this script from the project root directory
+#PROJECT_ROOT="$( cd "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )"/..
+#pushd ${PROJECT_ROOT}
+
+## =============================================================================
+## Step 1: Process the command line arguments
+
+function print_the_help {
+  echo "USAGE:    --ebeam E --pbeam E --config C1 --decay D2"
+  echo "          [--config C2 --decay D2 --decay D3 ...]"
+  echo "REQUIRED ARGUMENTS:"
+  echo "          --ebeam       Electron beam energy"
+  echo "          --pbeam       Ion beam energy"
+  echo "          --config      Generator configuration identifiers (at least one)"
+  if [ ! -z ${REQUIRE_DECAY} ]; then
+    echo "        --decay       Specific decay particle (e.g. muon)."
+  fi
+  if [ ! -z ${REQUIRE_LEADING} ]; then
+    echo "        --leading     Leading particle of interest (e.g. jpsi)."
+  fi
+  echo "          -h,--help     Print this message"
+  echo ""
+  echo "  Generate multiple monte carlo samples for a desired process." 
+  exit
+}
+
+## Required variables
+EBEAM=
+PBEAM=
+DECAYS=
+CONFIG=
+
+while [ $# -gt 0 ]
+do
+  key="$1"
+  case $key in
+    --config)
+      CONFIG="$2"
+      shift # past argument
+      shift # past value
+      ;;
+    --ebeam)
+      EBEAM="$2"
+      shift # past argument
+      shift # past value
+      ;;
+    --pbeam)
+      PBEAM="$2"
+      shift # past argument
+      shift # past value
+      ;;
+    --leading)
+      LEADING="$2"
+      shift # past argument
+      shift # past value
+      ;;
+    --decay)
+      DECAY="$2"
+      shift # past argument
+      shift # past value
+      ;;
+    -h|--help)
+      print_the_help
+      exit 0
+      ;;
+    *)    # unknown option
+      echo "unknown option"
+      exit 1
+      ;;
+  esac
+done
+
+if [ -z $CONFIG ]; then
+  echo "ERROR: CONFIG not defined: --config <config>"
+  print_the_help
+  exit 1
+elif [ -z $EBEAM ]; then
+  echo "ERROR: EBEAM not defined: --ebeam <energy>"
+  print_the_help
+  exit 1
+elif [ -z $PBEAM ]; then
+  echo "ERROR: PBEAM not defined: --pbeam <energy>"
+  print_the_help
+  exit 1
+elif [ -z $LEADING ] && [ ! -z $REQUIRE_LEADING ]; then
+  echo "ERROR: LEADING not defined: --leading <channel>"
+  print_the_help
+  exit 1
+elif [ ! -z $LEADING ] && [ -z $REQUIRE_LEADING ]; then
+  echo "ERROR: LEADING flag specified but not required"
+  print_the_help
+  exit 1
+elif [ -z $DECAY ] && [ ! -z $REQUIRE_DECAY ]; then
+  echo "ERROR: DECAY not defined: --decay <channel>"
+  print_the_help
+  exit 1
+elif [ ! -z $DECAY ] && [ -z $REQUIRE_DECAY ]; then
+  echo "ERROR: DECAY flag specified but not required"
+  print_the_help
+  exit 1
+fi
+
+## Export the configured variables
+export CONFIG
+export EBEAM
+export PBEAM
+if [ ! -z $REQUIRE_LEADING ]; then
+  export LEADING
+fi
+if [ ! -z $REQUIRE_DECAY ]; then
+  export DECAY
+fi
diff --git a/util/print_env.sh b/util/print_env.sh
new file mode 100755
index 0000000000000000000000000000000000000000..ce4010509e8763b3dba0fdc93bf0b6584f172e27
--- /dev/null
+++ b/util/print_env.sh
@@ -0,0 +1,12 @@
+#!/bin/bash
+
+echo "JUGGLER_TAG:                ${JUGGLER_TAG}"
+echo "JUGGLER_DETECTOR:           ${JUGGLER_DETECTOR}"
+echo "JUGGLER_DETECTOR_VERSION:   ${JUGGLER_DETECTOR_VERSION}"
+echo "JUGGLER_N_EVENTS:           ${JUGGLER_N_EVENTS}"
+echo "JUGGLER_N_THREADS:          ${JUGGLER_N_THREADS}"
+echo "JUGGLER_RNG_SEED:           ${JUGGLER_RNG_SEED}"
+echo "JUGGLER_INSTALL_PREFIX:     ${JUGGLER_INSTALL_PREFIX}"
+echo "LOCAL_PREFIX:               ${LOCAL_PREFIX}"
+echo "DETECTOR_PREFIX:            ${DETECTOR_PREFIX}"
+echo "DETECTOR_PATH:              ${DETECTOR_PATH}"
diff --git a/util/run_many.py b/util/run_many.py
new file mode 100755
index 0000000000000000000000000000000000000000..ccb7e83a7f81d1bc502fcddb32900e5a31eebdb1
--- /dev/null
+++ b/util/run_many.py
@@ -0,0 +1,124 @@
+#!/usr/bin/env python3
+
+""" 
+This script will run a CI generator or processing script for multiple configurations.
+
+Author: Sylvester Joosten <sjoosten@anl.gov>
+"""
+
+import os
+import argparse
+from multiprocessing import Pool, get_context
+from tempfile import NamedTemporaryFile
+
+class InvalidArgumentError(Exception):
+    pass
+
+parser = argparse.ArgumentParser()
+parser.add_argument(
+        'command',
+        help="Script to be launched in parallel")
+parser.add_argument(
+        '--energy', '-e',
+        dest='energies',
+        action='append',
+        help='One or more beam energy pairs (e.g. 10x100)',
+        required=True)
+parser.add_argument(
+        '--config', '-c',
+        dest='configs',
+        action='append',
+        help='One or more configurations',
+        required=True)
+parser.add_argument(
+        '--leading',
+        dest='leads',
+        action='append',
+        help='One or more leading particles(opt.)',
+        required=False)
+parser.add_argument(
+        '--decay',
+        dest='decays',
+        action='append',
+        help='One or more decay channels (opt.)',
+        required=False)
+parser.add_argument(
+        '--nproc',
+        dest='nproc',
+        default=5,
+        type=int,
+        help='Number of processes to launch in parallel',
+        required=False)
+
+def worker(command):
+    '''Execute the command in a system call, with the supplied argument string.'''
+    ## use a temporary file to capture the terminal output, and then
+    ## print the terminal output once the command finishes
+    with NamedTemporaryFile() as f:
+        cmd = [command, ' 2>&1 >', f.name]
+        cmd = ' '.join(cmd)
+        print("Executing '{}'".format(cmd))
+        ret = os.system(cmd)
+        with open(f.name) as log:
+            print(log.read())
+        return ret
+
+if __name__ == '__main__':
+    args = parser.parse_args()
+    print('Launching CI script in parallel for multiple settings')
+    for e in args.energies:
+        beam_setting = e.split('x')
+        if not beam_setting[0].isnumeric() or not beam_setting[1].isnumeric():
+            print("Error: invalid beam energy setting:", e)
+            raise InvalidArgumentError
+
+    if not os.path.exists(args.command):
+        print("Error: Script not found:", args.command)
+        raise InvalidArgumentError
+
+    if args.nproc < 1 or args.nproc > 50:
+        print("Error: Invalid process limit (should be 1-50):", args.nproc)
+        raise InvalidArgumentError
+
+    print(' - command: {}'.format(args.command))
+    print(' - energies: {}'.format(args.energies))
+    print(' - config: {}'.format(args.configs))
+    print(' - nproc: {}'.format(args.nproc))
+    if (args.leads):
+        print(' - leading: {}'.format(args.leads))
+    if (args.decays):
+        print(' - decay: {}'.format(args.decays))
+
+    ## Expand our command and argument list for all combinatorics
+    cmds = []
+    decays = args.decays if args.decays else [None]
+    leads = args.leads if args.leads else [None]
+    for e in args.energies:
+        for c in args.configs:
+            for l in leads:
+                for d in decays:
+                    beam_setting = e.split('x')
+                    cmd = [args.command,
+                           '--ebeam', beam_setting[0],
+                           '--pbeam', beam_setting[1],
+                           '--config', c]
+                    if l is not None:
+                        cmd += ['--leading', l]
+                    if d is not None:
+                        cmd += ['--decay', d]
+                    cmds.append(' '.join(cmd))
+
+    ## create a process pool
+    ## note that I'm using themultiprocessing.get_context function to setup
+    ## a context where subprocesses are created using the new "spawn" process
+    ## which avoids deadlocks that sometimes happen in the default dispatch
+    with get_context('spawn').Pool(processes=args.nproc) as pool:
+        return_values = pool.map(worker, cmds)
+        ## check if we all exited nicely, else exit with status 1
+        if not all(ret == 0 for ret in return_values):
+            n_fail = sum([1 for ret in return_values if ret != 0])
+            print('ERROR, {} of {} jobs failed'.format(n_fail, len(cmds)))
+            print('Return values:', [ret for ret in return_values if ret != 0])
+            exit(1)
+
+    ## That's all!