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 = ¶ms_; + + 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 == ¶ms); + + 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!