diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index a9d37e70..1e19898a 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -3,11 +3,11 @@ variables: GIT_SUBMODULE_STRATEGY: recursive - OSPRAY_VER: "2.7.1" - RKCOMMON_VER: "1.7.0" - PYTHON_VERSION: "3.7.10" - PYTHON_ROOT_MACOS: "/NAS/packages/apps/python/$PYTHON_VERSION/macos" - PYTHON_ROOT_WINDOWS: "N:/packages/apps/python/3.7.9/windows" ## python-3.7.10 currently not installed for windows + OSPRAY_VER: "2.8.0" + RKCOMMON_VER: "1.8.0" + PYTHON_VERSION: "3.9.7" + # PYTHON_ROOT_MACOS: "/NAS/packages/apps/python/$PYTHON_VERSION/macos" + PYTHON_ROOT_WINDOWS: "N:/packages/apps/python/$PYTHON_VERSION/windows" stages: - build @@ -17,7 +17,7 @@ stages: .centos7: tags: [ build, docker, modules ] - image: $DOCKER_REGISTRY/general:centos7-devel + image: $DOCKER_REGISTRY/centos:7 before_script: - module load python/$PYTHON_VERSION - echo $Python3_ROOT @@ -25,14 +25,11 @@ stages: .ubuntu18.04: tags: [ build, docker, modules ] - image: $DOCKER_REGISTRY/ospray/docker-images:ubuntu18.04-mod + image: $DOCKER_REGISTRY/ubuntu:18.04 before_script: - - apt install -y xorg-dev - # image: $DOCKER_REGISTRY/general:ubuntu18.04-devel - # before_script: - # - module load python/$PYTHON_VERSION - # - echo $Python3_ROOT - # - ls $Python3_ROOT + - module load python/$PYTHON_VERSION + - echo $Python3_ROOT + - ls $Python3_ROOT .ubuntu20.04: tags: [ build, docker ] @@ -40,17 +37,9 @@ stages: .macos: tags: [ osx, clang ] - before_script: - - export Python3_ROOT=$PYTHON_ROOT_MACOS - - echo $Python3_ROOT - - ls $Python3_ROOT .macos-sign: tags: [ osx, clang, sign ] - before_script: - - export Python3_ROOT=$PYTHON_ROOT_MACOS - - echo $Python3_ROOT - - ls $Python3_ROOT .windows: tags: [ win, msvc15 ] @@ -114,9 +103,6 @@ build-centos7: - .centos7 - .linux-build-artifacts script: - - source $SPACK_SETUP_ENV - - module load cmake/3.15.2 - - module load gcc/5.4.0 - module load intel/2020.1 - export CC=icc - export CXX=icpc @@ -274,9 +260,6 @@ release-centos7: - .linux-release-artifacts needs: [ test-run-centos7 ] script: - - source $SPACK_SETUP_ENV - - module load cmake/3.15.2 - - module load gcc/5.4.0 - module load intel/2020.1 - export CC=icc - export CXX=icpc @@ -304,6 +287,7 @@ release-macos: - master - /^release-.*$/ - devel + - web scan-bdba-bin: stage: scan diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e83070c..10bd52bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,30 @@ Version History --------------- +### Changes in OSPRay Studio v0.9.0 + +- Compatible with OSPRay release v2.8.0 + +- Features and Improvements + - Added initial EULUMDAT photometric light support to SpotLight + - Enabled alternate camera selection in glTF scenes + - Added ability for python scripts to define transfer functions and load Studio plugins + - Enabled .sg scene files can now contain an HDRI light + - Much improved command line parsing in all modes + - Added support for several new KHR_materials extentions + (`KHR_materials_volume`, `KHR_materials_specular`, `KHR_materials_ior`) + and `KHR_texture_transform` + - Improved UI controls for adjusting model transforms +
+ +- Bug Fixes: + - Fixed high-DPI display issues + - Fixed bug causing model textures to be flipped if loading HDRI first + - Fixed crash when selecting non-default cameras in glTF scenes + - Fixed bug with macOS ARM build. + - Fixed `KHR_lights_punctual` light direction bug + + ### Changes in OSPRay Studio v0.8.1 - Compatible with OSPRay release v2.7.1 @@ -26,6 +50,7 @@ Version History - Updated external 3rd party dependencies bringing in features and bug fixes - Modified OBJ importer to parse and use quads and triangles - Improved Arcball camera navigation with up-vector lock +
- Bug Fixes - Fixed materials after clearing scene @@ -33,7 +58,7 @@ Version History - Can no longer create a zero-dimension framebuffer - Fixed instancing node-naming to allow for for many identical instances - Fixed crash if optional glTF punctual light color was omitted - - Fixed crash if isosuface geometry is used as clipping geometry when no other + - Fixed crash if isosurface geometry is used as clipping geometry when no other geometries are in the scene ### Changes in OSPRay Studio v0.7.0 diff --git a/CMakeLists.txt b/CMakeLists.txt index 821c4403..c6d0adcf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,7 +3,7 @@ cmake_minimum_required(VERSION 3.15) -project(ospray_studio VERSION 0.8.1 LANGUAGES CXX) +project(ospray_studio VERSION 0.9.0 LANGUAGES CXX) include(GNUInstallDirs) include(ProcessorCount) diff --git a/README.md b/README.md index 55b2a0e9..a6840d06 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,10 @@ # OSPRay Studio -This is release v0.8.1 of Intel® OSPRay Studio. It is released under the +This is release v0.9.0 of Intel® OSPRay Studio. It is released under the Apache 2.0 license. -Visit http://www.ospray.org/ospray_studio for more information. +Visit [**OSPRay Studio**](http://www.ospray.org/ospray_studio) +(http://www.ospray.org/ospray_studio) for more information. See [what's new](https://github.com/ospray/ospray_studio/blob/master/CHANGELOG.md) @@ -12,10 +13,9 @@ in this release. ## Overview Intel OSPRay Studio is an open source and interactive visualization and -ray tracing application that leverages [Intel -OSPRay](https://www.ospray.org) as its core rendering engine. It can be -used to load complex scenes requiring high fidelity rendering or very -large scenes requiring supercomputing resources. +ray tracing application that leverages [Intel OSPRay](https://www.ospray.org) +as its core rendering engine. It can be used to load complex scenes requiring +high fidelity rendering or very large scenes requiring supercomputing resources. The main control structure is a *scene graph* which allows users to create an abstract scene in a *directed acyclical graph* manner. Scenes @@ -30,27 +30,26 @@ OSPRay Studio has the following required and optional dependencies. ### Required dependencies - [CMake](https://www.cmake.org) (v3.15+) and any C++14 compiler -- Intel [OSPRay](https://www.github.com/ospray/ospray) (v2.7.1) and +- Intel [OSPRay](https://www.github.com/ospray/ospray) (v2.8.0) and its dependencies - OSPRay Studio builds on top of OSPRay. Instructions on building OSPRay are provided - [here](http://www.ospray.org/downloads.html#building-and-finding-ospray) - - Intel [Open VKL](https://www.github.com/openvkl/openvkl) (v1.0.1) + [here](http://www.ospray.org/downloads.html#building-and-finding-ospray). + - Intel [Open VKL](https://www.github.com/openvkl/openvkl) (v1.0.1 or newer) - Intel [Embree](https://www.github.com/embree/embree) (v3.13.1 or newer) - Intel oneAPI Rendering Toolkit common library - [rkcommon](https://www.github.com/ospray/rkcommon) (v1.7.0) - - Intel [Threading Building - Blocks](https://www.threadingbuildingblocks.org/) -- OpenGL and [GLFW](https://www.glfw.org/) (v3.x) - for the windowing - environment + [rkcommon](https://www.github.com/ospray/rkcommon) (v1.8.0) + - Intel [Threading Building Blocks](https://www.threadingbuildingblocks.org/) +- OpenGL and [GLFW](https://www.glfw.org) (v3.3.4) - for the windowing environment ### Optional Dependencies -- Intel [Open Image Denoise](https://openimagedenoise.github.io/) - (v1.2.3 or +- Intel [Open Image Denoise](https://openimagedenoise.github.io) - (v1.2.3 or newer) for denoising frames. To use with OSPRay Studio, OSPRay must be built - with `-DBUILD_OIDN=ON` in CMake -- [OpenImageIO]() and [OpenEXR]() - to support images in a variety of - file formats. Set `OPENIMAGEIO_ROOT` and `OPENEXR_ROOT` to the - respective install directories to use these libraries + with `-DBUILD_OIDN=ON` in CMake. +- [OpenImageIO](http://openimageio.org/) and [OpenEXR](https://www.openexr.com/) + to support images in a variety of file formats. Set `OPENIMAGEIO_ROOT` + and `OPENEXR_ROOT` to the respective install directories to use these libraries. +- [Python] (3.9.7) (https://python.org) for python bindings ### Building on Linux and macOS @@ -66,6 +65,9 @@ OSPRay Studio has the following required and optional dependencies. export rkcommon_DIR = ${RKCOMMON_INSTALL_LOCATION} ``` + Alternatively, [CMAKE_PREFIX_PATH](https://cmake.org/cmake/help/latest/variable/CMAKE_PREFIX_PATH.html) + can be set to find the OSPRay install and other dependencies. + - Clone OSPRay Studio ``` bash diff --git a/app/Batch.cpp b/app/Batch.cpp index df2220a1..04ddaca1 100644 --- a/app/Batch.cpp +++ b/app/Batch.cpp @@ -16,6 +16,9 @@ #include "sg/JSONDefs.h" #include "PluginManager.h" +// CLI +#include + static bool resetFileId = false; BatchContext::BatchContext(StudioCommon &_common) @@ -42,7 +45,7 @@ void BatchContext::start() bool useCamera = refreshCamera(cameraDef); if (useCamera) { render(); - if (animate) { + if (fps) { std::cout << "..rendering animation!" << std::endl; renderAnimation(); } else if (!cameraStack.empty()) { @@ -62,7 +65,7 @@ void BatchContext::start() bool useCamera = refreshCamera(cameraIdx, true); if (useCamera) { render(); - if (animate) { + if (fps) { std::cout << "..rendering animation!" << std::endl; renderAnimation(); } else @@ -76,220 +79,160 @@ void BatchContext::start() } } -bool BatchContext::parseCommandLine() -{ - int argc = studioCommon.argc; - const char **argv = studioCommon.argv; - int argIndex = 1; - - volumeParams = std::make_shared(); - - auto argAvailability = [&](std::string switchArg, int nComp) { - if (argc >= argIndex + nComp) +void BatchContext::addToCommandLine(std::shared_ptr app) { + app->add_option( + "--cameraType", + optCameraTypeStr, + "Set the camera type" + )->check(CLI::IsMember({"perspective", "orthographic", "panoramic"})); + app->add_option( + "--position", + [&](const std::vector val) { + pos = vec3f(std::stof(val[0]), std::stof(val[1]), std::stof(val[2])); + cmdlCam = true; return true; - std::cout << "Missing argument value for : " << switchArg << std::endl; - return false; - }; - - while (argIndex < argc) { - std::string switchArg(argv[argIndex++]); - - if (switchArg == "--help") { - printHelp(); - return 0; - } else if (switchArg == "-r" || switchArg == "--renderer") { - if (argAvailability(switchArg, 1)) - optRendererTypeStr = argv[argIndex++]; - - } else if (switchArg == "-c" || switchArg == "--camera") { - if (argAvailability(switchArg, 1)) - optCameraTypeStr = argv[argIndex++]; - - } else if (switchArg == "-vp") { - if (argAvailability(switchArg, 3)) { - vec3f posVec; - posVec.x = atof(argv[argIndex++]); - posVec.y = atof(argv[argIndex++]); - posVec.z = atof(argv[argIndex++]); - pos = posVec; - cmdlCam = true; - } - - } else if (switchArg == "-vu") { - if (argAvailability(switchArg, 3)) { - vec3f upVec; - upVec.x = atof(argv[argIndex++]); - upVec.y = atof(argv[argIndex++]); - upVec.z = atof(argv[argIndex++]); - up = upVec; - cmdlCam = true; - } - - } else if (switchArg == "-f" || switchArg == "--format") { - if (argAvailability(switchArg, 1)) - optImageFormat = argv[argIndex++]; - - } else if (switchArg == "-i" || switchArg == "--image") { - if (argAvailability(switchArg, 1)) - optImageName = argv[argIndex++]; - - } else if (switchArg == "-vi") { - if (argAvailability(switchArg, 3)) { - vec3f gazeVec; - gazeVec.x = atof(argv[argIndex++]); - gazeVec.y = atof(argv[argIndex++]); - gazeVec.z = atof(argv[argIndex++]); - gaze = gazeVec; - cmdlCam = true; - } - - } else if (switchArg == "-id" || switchArg == "--interpupillaryDistance") { - if (argAvailability(switchArg, 1)) - optInterpupillaryDistance = max(0.0, atof(argv[argIndex++])); - - } else if (switchArg == "-sm" || switchArg == "--stereoMode") { - if (argAvailability(switchArg, 1)) - optStereoMode = max(0, atoi(argv[argIndex++])); - - } else if (switchArg == "-s" || switchArg == "--size") { - if (argAvailability(switchArg, 2)) { - auto x = max(0, atoi(argv[argIndex++])); - auto y = max(0, atoi(argv[argIndex++])); - optImageSize = vec2i(x, y); - } - - } else if (switchArg == "-spp" || switchArg == "--samples") { - if (argAvailability(switchArg, 1)) - optSPP = max(1, atoi(argv[argIndex++])); - - } else if (switchArg == "-vt" || switchArg == "--variance") { - if (argAvailability(switchArg, 1)) - optVariance = max(0.0, atof(argv[argIndex++])); - - } else if (switchArg == "-pf" || switchArg == "--pixelfilter") { - if (argAvailability(switchArg, 1)) - optPF = max(0, atoi(argv[argIndex++])); - - } else if (switchArg == "-oidn" || switchArg == "--denoiser") { + }, + "Set the camera position" + )->expected(3); + app->add_option( + "--view", + [&](const std::vector val) { + gaze = vec3f(std::stof(val[0]), std::stof(val[1]), std::stof(val[2])); + cmdlCam = true; + return true; + }, + "Set the camera view vector" + )->expected(3); + app->add_option( + "--up", + [&](const std::vector val) { + up = vec3f(std::stof(val[0]), std::stof(val[1]), std::stof(val[2])); + cmdlCam = true; + return true; + }, + "Set the camera up vector" + ); + app->add_option( + "--format", + optImageFormat, + "Set the image format" + )->check(CLI::IsMember({"png", "jpg", "ppm", "pfm", "exr", "hdr"})); + app->add_option( + "--image", + optImageName, + "Set the image name" + ); + app->add_option( + "--interpupillaryDistance", + optInterpupillaryDistance, + "Set the interpupillary distance" + )->check(CLI::PositiveNumber); + app->add_option( + "--stereoMode", + optStereoMode, + "Set the stereo mode" + )->check(CLI::PositiveNumber); + app->add_option( + "--size", + [&](const std::vector val) { + optImageSize = vec2i(std::stoi(val[0]), std::stoi(val[1])); + return true; + }, + "Set the image size" + )->expected(2)->check(CLI::PositiveNumber); + app->add_option_function( + "--denoiser", + [&](const int denoiser) { if (studioCommon.denoiserAvailable) { - if (argAvailability(switchArg, 1)) - optDenoiser = min(2, max(0, atoi(argv[argIndex++]))); - } else { - std::cout << " Denoiser not enabled. Check OSPRay module.\n"; - argIndex++; - } - } else if (switchArg == "-g" || switchArg == "--grid") { - if (argAvailability(switchArg, 3)) { - auto x = max(0, atoi(argv[argIndex++])); - auto y = max(0, atoi(argv[argIndex++])); - auto z = max(0, atoi(argv[argIndex++])); - optGridSize = vec3i(x, y, z); - optGridEnable = true; - } - } else if (switchArg == "-a" || switchArg == "--albedo") { - saveAlbedo = true; - } else if (switchArg == "-d" || switchArg == "--depth") { - saveDepth = true; - } else if (switchArg == "-n" || switchArg == "--normal") { - saveNormal = true; - } else if (switchArg == "-l" || switchArg == "--layers") { - saveLayers = true; - } else if (switchArg == "-m" || switchArg == "--metadata") { - saveMetaData = true; - } else if (switchArg == "-fps" || switchArg == "--speed") { - if (argAvailability(switchArg, 1)) - fps = atoi(argv[argIndex++]); - animate = true; - } else if (switchArg == "-fr" || switchArg == "--force") { - forceRewrite = true; - } else if (switchArg == "-cam" || switchArg == "--camera") { - if (argAvailability(switchArg, 1)) { - cameraDef = std::stoi(argv[argIndex++]); - if (cameraDef < 0) { - std::cout << "unsupported camera index specified " << std::endl; - return false; - } - } - if (!cameraDef) - std::cout - << "using default ospray camera, to use imported definition camera indices begins from 1" - << std::endl; - } else if (switchArg == "-cams" || switchArg == "--cameras") { - if (argAvailability(switchArg, 2)) { - auto x = atoi(argv[argIndex++]); - auto y = atoi(argv[argIndex++]); - cameraRange.lower = x; - cameraRange.upper = y; - useCameraRange = true; - } - if (!cameraRange.lower && !cameraRange.upper) - std::cout - << "using default ospray camera, to use imported definition camera indices begins from 1" - << std::endl; - } else if (switchArg == "-rn" || switchArg == "--range") { - if (argAvailability(switchArg, 2)) { - auto x = atoi(argv[argIndex++]); - auto y = atoi(argv[argIndex++]); - framesRange.lower = x; - framesRange.upper = y; - } - } - // volume parameters - else if (switchArg == "--dimensions" || switchArg == "-dim") { - if (argAvailability(switchArg, 3)) { - const std::string dimX(argv[argIndex++]); - const std::string dimY(argv[argIndex++]); - const std::string dimZ(argv[argIndex++]); - auto dimensions = vec3i(std::stoi(dimX), std::stoi(dimY), std::stoi(dimZ)); - volumeParams->createChild("dimensions", "vec3i", dimensions); - } - } else if (switchArg == "--gridSpacing" || switchArg == "-gs") { - if (argAvailability(switchArg, 3)) { - const std::string gridSpacingX(argv[argIndex++]); - const std::string gridSpacingY(argv[argIndex++]); - const std::string gridSpacingZ(argv[argIndex++]); - auto gridSpacing = - vec3f(stof(gridSpacingX), stof(gridSpacingY), stof(gridSpacingZ)); - volumeParams->createChild("gridSpacing", "vec3f", gridSpacing); - } - } else if (switchArg == "--gridOrigin" || switchArg == "-go") { - if (argAvailability(switchArg, 3)) { - const std::string gridOriginX(argv[argIndex++]); - const std::string gridOriginY(argv[argIndex++]); - const std::string gridOriginZ(argv[argIndex++]); - auto gridOrigin = - vec3f(stof(gridOriginX), stof(gridOriginY), stof(gridOriginZ)); - volumeParams->createChild("gridOrigin", "vec3f", gridOrigin); - } - } else if (switchArg == "--voxelType" || switchArg == "-vt") { - if (argAvailability(switchArg, 1)) { - auto voxelTypeStr = std::string(argv[argIndex++]); - auto it = sg::volumeVoxelType.find(voxelTypeStr); - if (it != sg::volumeVoxelType.end()) { - auto voxelType = it->second; - volumeParams->createChild("voxelType", "int", (int)voxelType); - } else { - throw std::runtime_error("improper -voxelType format requested"); - } - } - } else if (switchArg == "--sceneConfig" || switchArg == "-sc") { - // valid values are dynamic, compact and robust - if (argAvailability(switchArg, 1)) { - const std::string sc(argv[argIndex++]); - sceneConfig = sc; - } - } else if (switchArg == "--instanceConfig" || switchArg == "-ic") { - // valid values are dynamic, compact and robust - if (argAvailability(switchArg, 1)) { - const std::string ic(argv[argIndex++]); - instanceConfig = ic; + optDenoiser = denoiser; + return true; } - } else if (switchArg.front() == '-') { - std::cout << " Unknown option: " << switchArg << std::endl; - break; - } else { - filesToImport.push_back(switchArg); - } + return false; + }, + "Set the denoiser" + )->check(CLI::Range(0, 2+1)); + app->add_option( + "--grid", + [&](const std::vector val) { + optGridSize = vec3i(std::stoi(val[0]), std::stoi(val[1]), std::stoi(val[2])); + optGridEnable = true; + return true; + }, + "Set the camera position" + )->expected(3); + app->add_flag( + "--saveAlbedo", + saveAlbedo, + "Save albedo values" + ); + app->add_flag( + "--saveDepth", + saveDepth, + "Save depth values" + ); + app->add_flag( + "--saveNormal", + saveNormal, + "Save normal values" + ); + app->add_flag( + "--saveLayers", + saveLayers, + "Save all layers" + ); + app->add_flag( + "--saveMetadata", + saveMetaData, + "Save metadata" + ); + app->add_option( + "--framesPerSecond", + fps, + "Set the number of frames per second (integer)" + ); + app->add_flag( + "--forceRewrite", + forceRewrite, + "Force overwriting saved files if they exist" + ); + app->add_option( + "--camera", + cameraDef, + "Set the camera index to use" + )->check(CLI::PositiveNumber); + app->add_option( + "--cameras", + [&](const std::vector val) { + cameraRange.lower = std::stoi(val[0]); + cameraRange.upper = std::stoi(val[1]); + useCameraRange = true; + return true; + }, + "Set the camera range" + )->expected(2); + app->add_option( + "--frameRange", + [&](const std::vector val) { + framesRange.lower = std::stoi(val[0]); + framesRange.upper = std::stoi(val[1]); + return true; + }, + "Set the frames range" + )->expected(2); +} + +bool BatchContext::parseCommandLine() +{ + int ac = studioCommon.argc; + const char **av = studioCommon.argv; + + std::shared_ptr app = std::make_shared("OSPRay Studio Batch"); + StudioContext::addToCommandLine(app); + BatchContext::addToCommandLine(app); + try { + app->parse(ac, av); + } catch (const CLI::ParseError &e) { + exit(app->exit(e)); } if (filesToImport.size() == 0) { @@ -311,25 +254,59 @@ void BatchContext::refreshRenderer() renderer.child("varianceThreshold").setValue(optVariance); } -bool BatchContext::refreshCamera(int cameraIdx, bool resetArcball) +void BatchContext::reshape() { - if(frame->hasChild("camera")) - frame->remove("camera"); + auto fSize = frame->child("windowSize").valueAs(); + + if (lockAspectRatio) { + // Tell OSPRay to render the largest subset of the window that satisies the + // aspect ratio + float aspectCorrection = lockAspectRatio * static_cast(fSize.y) + / static_cast(fSize.x); + if (aspectCorrection > 1.f) { + fSize.y /= aspectCorrection; + } else { + fSize.x *= aspectCorrection; + } + if (frame->child("camera").hasChild("aspect")) + frame->child("camera")["aspect"] = static_cast(fSize.x) / fSize.y; + } else if (frame->child("camera").hasChild("aspect")) + frame->child("camera")["aspect"] = optImageSize.x / (float)optImageSize.y; + frame->child("windowSize") = fSize; + frame->currentAccum = 0; + + // update camera + arcballCamera->updateWindowSize(fSize); +} + +bool BatchContext::refreshCamera(int cameraIdx, bool resetArcball) +{ if (resetArcball) arcballCamera.reset( new ArcballCamera(frame->child("world").bounds(), optImageSize)); + int hasParents{0}; if (cameraIdx <= cameras.size() && cameraIdx > 0) { std::cout << "Loading camera from index: " << std::to_string(cameraIdx) << std::endl; selectedSceneCamera = cameras[cameraIdx - 1]; + hasParents = selectedSceneCamera->parents().size(); + frame->remove("camera"); + frame->add(selectedSceneCamera); - auto &camera = frame->createChildAs( - "camera", selectedSceneCamera->subType()); - - for (auto &c : selectedSceneCamera->children()) - camera.add(c.second); + // TODO: remove this Hack : for some reason the accumulated transform in + // transform node does not get updated for the BIT animation scene. + // Attempting to make transform modified so it picks up accumulated + // transform values made by renderScene + if (hasParents) { + auto cameraXfm = selectedSceneCamera->parents().front(); + if (cameraXfm->valueAs() == affine3f(one)) + cameraXfm->createChild("refresh", "bool"); + } + + if (selectedSceneCamera->hasChild("aspect")) + lockAspectRatio = selectedSceneCamera->child("aspect").valueAs(); // create unique cameraId for every camera auto &cameraParents = selectedSceneCamera->parents(); @@ -350,11 +327,14 @@ bool BatchContext::refreshCamera(int cameraIdx, bool resetArcball) } else { std::cout << "No cameras imported or invalid camera index specified" << std::endl; - selectedSceneCamera = createNode( - "camera", "camera_" + optCameraTypeStr); - frame->add(selectedSceneCamera); + std::cout << "using default camera..." << std::endl; } + reshape(); // resets aspect + + // if imported cameras don't have parent transform then use Arcball properties + if (!hasParents) + useArcball = true; updateCamera(); return true; @@ -362,10 +342,6 @@ bool BatchContext::refreshCamera(int cameraIdx, bool resetArcball) void BatchContext::render() { - - // Set the frame "windowSize", it will create the right sized framebuffer - frame->child("windowSize") = optImageSize; - auto &frameBuffer = frame->childAs("framebuffer"); frameBuffer["floatFormat"] = true; frameBuffer.commit(); @@ -414,14 +390,6 @@ void BatchContext::renderFrame() frame->immediatelyWait = true; frame->startNewFrame(); - if (selectedSceneCamera->nodeAs()->animate || cameraDef > 0) { - auto newCS = selectedSceneCamera->nodeAs()->getState(); - arcballCamera->setState(*newCS); - updateCamera(); - frame->cancelFrame(); - frame->startNewFrame(); - } - static int filenum; if (resetFileId) { filenum = framesRange.lower; @@ -474,11 +442,11 @@ void BatchContext::refreshScene(bool resetCam) // Check that the frame contains a world, if not create one auto world = frame->hasChild("world") ? frame->childNodeAs("world") : sg::createNode("world", "world"); - if (sceneConfig == "dynamic") + if (optSceneConfig == "dynamic") world->child("dynamicScene").setValue(true); - else if (sceneConfig == "compact") + else if (optSceneConfig == "compact") world->child("compactMode").setValue(true); - else if (sceneConfig == "robust") + else if (optSceneConfig == "robust") world->child("robustMode").setValue(true); world->createChild( "materialref", "reference_to_material", defaultMaterialIdx); @@ -509,12 +477,11 @@ void BatchContext::updateCamera() frame->currentAccum = 0; auto &camera = frame->child("camera"); - camera["position"] = arcballCamera->eyePos(); - camera["direction"] = arcballCamera->lookDir(); - camera["up"] = arcballCamera->upDir(); - - if (camera.hasChild("aspect")) - camera["aspect"] = optImageSize.x / (float)optImageSize.y; + if (useArcball) { + camera["position"] = arcballCamera->eyePos(); + camera["direction"] = arcballCamera->lookDir(); + camera["up"] = arcballCamera->upDir(); + } if (cmdlCam) { camera["position"] = pos; @@ -538,7 +505,7 @@ void BatchContext::importFiles(sg::NodePtr world) { importedModels = createNode("importXfm", "transform"); frame->child("world").add(importedModels); - if (animate) + if (fps) animationManager = std::shared_ptr(new AnimationManager); for (auto file : filesToImport) { @@ -558,6 +525,7 @@ void BatchContext::importFiles(sg::NodePtr world) importer->setMaterialRegistry(baseMaterialRegistry); importer->setCameraList(cameras); importer->setLightsManager(lightsManager); + importer->setArguments(studioCommon.argc, (char**)studioCommon.argv); if (volumeParams->children().size() > 0) { auto vp = importer->getVolumeParams(); for (auto &c : volumeParams->children()) { @@ -567,13 +535,13 @@ void BatchContext::importFiles(sg::NodePtr world) } if (animationManager) importer->setAnimationList(animationManager->getAnimations()); - if (instanceConfig == "dynamic") + if (optInstanceConfig == "dynamic") importer->setInstanceConfiguration( sg::InstanceConfiguration::DYNAMIC); - else if (instanceConfig == "compact") + else if (optInstanceConfig == "compact") importer->setInstanceConfiguration( sg::InstanceConfiguration::COMPACT); - else if (instanceConfig == "robust") + else if (optInstanceConfig == "robust") importer->setInstanceConfiguration( sg::InstanceConfiguration::ROBUST); importer->importScene(); @@ -592,65 +560,3 @@ void BatchContext::importFiles(sg::NodePtr world) if (animationManager) animationManager->init(); } - -void BatchContext::printHelp() -{ - std::cout << - R"text( -./ospStudio batch [parameters] [scene_files] - -ospStudio batch specific parameters: - -fps --speed - -fr --forceRewrite - force rewrite on existing saved files - -rn --range [start end] for eg : [10 20] - range of frames to be rendered - This should be determined by the user based on specified `fps` and total animation time. - -cam --camera - In case of mulitple imported cameras specify which camera definition to use, counting starts from 1 - 0 here would use default camera implementation - -cams --cameras - In case of mulitple imported cameras specify which camera-range to use, counting starts from 1 - for eg. a valid range would be [1 7] - -a --albedo - -d --depth - -n --normal - -m --metadata - -l --layers - -f --format (default png) - format for saving the image - (sg, exr, hdr, jpg, pfm,png, ppm) - -i --image [baseFilename] (default 'ospBatch') - base name of saved image - -s --size [x y] (default 1024x768) - image size - -spp --samples [int] (default 32) - samples per pixel - -pf --pixelfilter (default gauss) - (0=point, 1=box, 2=gauss, 3=mitchell, 4=blackman_harris) - -r --renderer [type] (default "pathtracer") - rendererType scivis, ao, or pathtracer - -c --camera [type] (default "perspective") - cameraType perspective or panoramic - -vp [x y z] camera position - -vu [x y z] camera up - -vi [x y z] camera look-at - -sm --stereoMode 0=none, 1=left, 2=right, 3=side-by-side, 4=top-bottom - -id --interpupillaryDistance - -g --grid [x y z] (default 1 1 1, single instance) - instace a grid of models - -sc --sceneConfig(default is the static BVH build of embree) - set global scene configuration params - valid values are dynamic, compact and robust - -ic --instanceConfig(default is the static BVH build of embree) - set instance scene configuration params - valid values are dynamic, compact and robust)text" - << std::endl; - if (studioCommon.denoiserAvailable) { - std::cout << - R"text( - -oidn --denoiser [0,1,2] (default 0) - image denoiser (0 = off, 1 = on, 2 = save both) -)text" << std::endl; - } -} diff --git a/app/Batch.h b/app/Batch.h index f0bb7097..c0273835 100644 --- a/app/Batch.h +++ b/app/Batch.h @@ -26,6 +26,7 @@ class BatchContext : public StudioContext ~BatchContext() {} void start() override; + void addToCommandLine(std::shared_ptr app) override; bool parseCommandLine() override; void importFiles(sg::NodePtr world) override; void refreshRenderer(); @@ -36,6 +37,7 @@ class BatchContext : public StudioContext void renderFrame(); void renderAnimation(); bool refreshCamera(int cameraIdx, bool resetArcball = false); + void reshape(); protected: NodePtr importedModels; @@ -47,11 +49,10 @@ class BatchContext : public StudioContext bool saveLayers{false}; bool saveMetaData{true}; std::string optImageFormat{"png"}; - bool animate{false}; - int fps{24}; + + int fps{0}; bool forceRewrite{false}; range1i framesRange{0, 0}; - void printHelp() override; int cameraDef{0}; range1i cameraRange{0, 0}; bool useCameraRange{false}; @@ -65,10 +66,6 @@ class BatchContext : public StudioContext //camera animation sg::NodePtr selectedSceneCamera; - //Volume parameters - sg::NodePtr volumeParams; - - // scene/instance configuration params - std::string sceneConfig{""}; - std::string instanceConfig{""}; + float lockAspectRatio = 0.0; + bool useArcball{false}; }; diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index 6d7a257b..58903265 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -59,6 +59,7 @@ target_link_libraries(ospStudio ospray_ui # Not all run modes should link against UI (ie batch). ospray_sg json + cli11 ) _target_strip_and_sign(ospStudio) diff --git a/app/MainWindow.cpp b/app/MainWindow.cpp index ec992a11..05962b0c 100644 --- a/app/MainWindow.cpp +++ b/app/MainWindow.cpp @@ -6,6 +6,7 @@ #include "imgui.h" #include "imgui_impl_glfw.h" #include "imgui_impl_opengl2.h" +#include "Proggy.h" // std #include #include @@ -23,6 +24,7 @@ #include "sg/visitors/PrintNodes.h" #include "sg/visitors/Search.h" #include "sg/visitors/SetParamByNode.h" +#include "sg/visitors/CollectTransferFunctions.h" #include "sg/scene/volume/Volume.h" // rkcommon #include "rkcommon/math/rkmath.h" @@ -43,6 +45,9 @@ #include "widgets/PieMenu.h" #include "widgets/Guizmo.h" +// CLI +#include + using namespace ospray_studio; using namespace ospray; @@ -51,7 +56,6 @@ static ImGuiWindowFlags g_imguiWindowFlags = ImGuiWindowFlags_AlwaysAutoResize; static bool g_quitNextFrame = false; static bool g_saveNextFrame = false; static bool g_animatingPath = false; -static bool g_animateCamera = false; static bool g_clearSceneConfirm = false; static const std::vector g_scenes = {"tutorial_scene", @@ -174,6 +178,10 @@ MainWindow::MainWindow(StudioCommon &_common) glfwWindowHint(GLFW_SRGB_CAPABLE, GLFW_TRUE); + // get primary monitor's display scaling + GLFWmonitor *primaryMonitor = glfwGetPrimaryMonitor(); + glfwGetMonitorContentScale(primaryMonitor, &contentScale.x, &contentScale.y); + // create GLFW window glfwWindow = glfwCreateWindow( windowSize.x, windowSize.y, "OSPRay Studio", nullptr, nullptr); @@ -382,6 +390,17 @@ MainWindow::MainWindow(StudioCommon &_common) // Disable active viewports until users enables toggled in view menu ImGui::GetIO().ConfigFlags &= ~ImGuiConfigFlags_ViewportsEnable; + // set ImGui font, scaled to display DPI + auto &io = ImGui::GetIO(); + auto scaleFactor = std::max(contentScale.x, contentScale.y); + auto scaledFontSize = fontSize * scaleFactor; + ImVec2 imScale(contentScale.x, contentScale.y); + + ImFont *font = io.Fonts->AddFontFromMemoryCompressedTTF( + ProggyClean_compressed_data, ProggyClean_compressed_size, scaledFontSize); + io.FontGlobalScale = 1.f / scaleFactor; + io.DisplayFramebufferScale = imScale; + // set initial OpenGL state glEnable(GL_TEXTURE_2D); glDisable(GL_LIGHTING); @@ -656,7 +675,7 @@ void MainWindow::motion(const vec2f &position) if (frame->pauseRendering) return; - const vec2f mouse(position.x, position.y); + const vec2f mouse = position * contentScale; if (previousMouse != vec2f(-1)) { const bool leftDown = glfwGetMouseButton(glfwWindow, GLFW_MOUSE_BUTTON_LEFT) == GLFW_PRESS; @@ -672,10 +691,12 @@ void MainWindow::motion(const vec2f &position) if (glfwGetKey(glfwWindow, GLFW_KEY_LEFT_CONTROL) == GLFW_PRESS) sensitivity *= fineControl; - const vec2f mouseFrom(clamp(prev.x * 2.f / windowSize.x - 1.f, -1.f, 1.f), - clamp(prev.y * 2.f / windowSize.y - 1.f, -1.f, 1.f)); - const vec2f mouseTo(clamp(mouse.x * 2.f / windowSize.x - 1.f, -1.f, 1.f), - clamp(mouse.y * 2.f / windowSize.y - 1.f, -1.f, 1.f)); + auto displaySize = windowSize * contentScale; + + const vec2f mouseFrom(clamp(prev.x * 2.f / displaySize.x - 1.f, -1.f, 1.f), + clamp(prev.y * 2.f / displaySize.y - 1.f, -1.f, 1.f)); + const vec2f mouseTo(clamp(mouse.x * 2.f / displaySize.x - 1.f, -1.f, 1.f), + clamp(mouse.y * 2.f / displaySize.y - 1.f, -1.f, 1.f)); if (leftDown) { arcballCamera->constrainedRotate(mouseFrom, @@ -704,7 +725,8 @@ void MainWindow::mouseButton(const vec2f &position) if (glfwGetKey(glfwWindow, GLFW_KEY_LEFT_SHIFT) == GLFW_PRESS && glfwGetMouseButton(glfwWindow, GLFW_MOUSE_BUTTON_LEFT) == GLFW_PRESS) { - pickCenterOfRotation(position.x, position.y); + vec2f scaledPosition = position * contentScale; + pickCenterOfRotation(scaledPosition.x, scaledPosition.y); } } @@ -737,7 +759,7 @@ void MainWindow::display() { static auto displayStart = std::chrono::high_resolution_clock::now(); - if (autorotate) { + if (optAutorotate) { vec2f from(0.f, 0.f); vec2f to(autorotateSpeed * 0.001f, 0.f); arcballCamera->rotate(from, to); @@ -763,17 +785,6 @@ void MainWindow::display() } } - // Add new camera params - if (g_animateCamera) { - auto selectedCamera = g_selectedSceneCamera->nodeAs(); - auto cs = selectedCamera->getState(); - - if (cs != nullptr) { - arcballCamera->setState(*cs); - updateCamera(); - } - } - keyboardMotion(); if (displayCallback) @@ -798,12 +809,12 @@ void MainWindow::display() waitOnOSPRayFrame(); // Only enabled if they exist - showAlbedo &= frameBuffer.hasAlbedoChannel(); - showDepth &= frameBuffer.hasDepthChannel(); + optShowAlbedo &= frameBuffer.hasAlbedoChannel(); + optShowDepth &= frameBuffer.hasDepthChannel(); - auto *mappedFB = (void *)frame->mapFrame(showDepth + auto *mappedFB = (void *)frame->mapFrame(optShowDepth ? OSP_FB_DEPTH - : (showAlbedo ? OSP_FB_ALBEDO : OSP_FB_COLOR)); + : (optShowAlbedo ? OSP_FB_ALBEDO : OSP_FB_COLOR)); // This needs to query the actual framebuffer format const GLenum glType = @@ -811,7 +822,7 @@ void MainWindow::display() // Only create the copy if it's needed float *pDepthCopy = nullptr; - if (showDepth) { + if (optShowDepth) { // Create a local copy and don't modify OSPRay buffer const auto *mappedDepth = static_cast(mappedFB); std::vector depthCopy( @@ -832,7 +843,7 @@ void MainWindow::display() const float rcpDepthRange = 1.f / (maxDepth - minDepth); // Inverted depth (1.0 -> 0.0) may be more meaningful - if (showDepthInvert) + if (optShowDepthInvert) std::transform(depthCopy.begin(), depthCopy.end(), depthCopy.begin(), @@ -849,13 +860,13 @@ void MainWindow::display() glBindTexture(GL_TEXTURE_2D, framebufferTexture); glTexImage2D(GL_TEXTURE_2D, 0, - showAlbedo ? gl_rgb_format : gl_rgba_format, + optShowAlbedo ? gl_rgb_format : gl_rgba_format, fbSize.x, fbSize.y, 0, - showDepth ? GL_LUMINANCE : (showAlbedo ? GL_RGB : GL_RGBA), + optShowDepth ? GL_LUMINANCE : (optShowAlbedo ? GL_RGB : GL_RGBA), glType, - showDepth ? pDepthCopy : mappedFB); + optShowDepth ? pDepthCopy : mappedFB); frame->unmapFrame(mappedFB); @@ -1032,11 +1043,11 @@ void MainWindow::refreshScene(bool resetCam) // Check that the frame contains a world, if not create one auto world = frame->hasChild("world") ? frame->childNodeAs("world") : sg::createNode("world", "world"); - if (sceneConfig == "dynamic") + if (optSceneConfig == "dynamic") world->child("dynamicScene").setValue(true); - else if (sceneConfig == "compact") + else if (optSceneConfig == "compact") world->child("compactMode").setValue(true); - else if (sceneConfig == "robust") + else if (optSceneConfig == "robust") world->child("robustMode").setValue(true); world->createChild( @@ -1072,76 +1083,36 @@ void MainWindow::refreshScene(bool resetCam) fb.resetAccumulation(); } +void MainWindow::addToCommandLine(std::shared_ptr app) { + app->add_flag( + "--animate", + optAnimate, + "enable loading glTF animations" + ); +} + bool MainWindow::parseCommandLine() { int ac = studioCommon.argc; const char **av = studioCommon.argv; - volumeParams = std::make_shared(); - for (int i = 1; i < ac; i++) { - const auto arg = std::string(av[i]); - if (arg.rfind("-", 0) != 0) { - filesToImport.push_back(arg); - } else if (arg == "-h" || arg == "--help") { - printHelp(); - return false; - } else if (arg == "-pf" || arg == "--pixelfilter") { - optPF = max(0, atoi(av[i + 1])); - rkcommon::removeArgs(ac, av, i, 2); - --i; - } else if (arg == "--animate" || arg == "-a") { - animate = true; - } else if (arg == "--dimensions" || arg == "-dim") { - const std::string dimX(av[++i]); - const std::string dimY(av[++i]); - const std::string dimZ(av[++i]); - auto dimensions = vec3i(std::stoi(dimX), std::stoi(dimY), std::stoi(dimZ)); - volumeParams->createChild("dimensions", "vec3i", dimensions); - } else if (arg == "--gridSpacing" || arg == "-gs") { - const std::string gridSpacingX(av[++i]); - const std::string gridSpacingY(av[++i]); - const std::string gridSpacingZ(av[++i]); - auto gridSpacing = - vec3f(stof(gridSpacingX), stof(gridSpacingY), stof(gridSpacingZ)); - volumeParams->createChild("gridSpacing", "vec3f", gridSpacing); - } else if (arg == "--gridOrigin" || arg == "-go") { - const std::string gridOriginX(av[++i]); - const std::string gridOriginY(av[++i]); - const std::string gridOriginZ(av[++i]); - auto gridOrigin = - vec3f(stof(gridOriginX), stof(gridOriginY), stof(gridOriginZ)); - volumeParams->createChild("gridOrigin", "vec3f", gridOrigin); - } else if (arg == "--voxelType" || arg == "-vt") { - auto voxelTypeStr = std::string(av[++i]); - auto it = sg::volumeVoxelType.find(voxelTypeStr); - if (it != sg::volumeVoxelType.end()) { - auto voxelType = it->second; - volumeParams->createChild("voxelType", "int", (int)voxelType); - } else { - throw std::runtime_error("improper -voxelType format requested"); - } - } else if (arg == "--sceneConfig" || arg == "-sc") { - // valid values are dynamic, compact and robust - const std::string sc(av[++i]); - sceneConfig = sc; - } else if (arg == "--instanceConfig" || arg == "-ic") { - // valid values are dynamic, compact and robust - const std::string ic(av[++i]); - instanceConfig = ic; - } else if (arg == "--2160p") - glfwSetWindowSize(glfwWindow, 3840, 2160); - else if (arg == "--1440p") - glfwSetWindowSize(glfwWindow, 2560, 1440); - else if (arg == "--1080p") - glfwSetWindowSize(glfwWindow, 1920, 1080); - else if (arg == "--720p") - glfwSetWindowSize(glfwWindow, 1280, 720); - else if (arg == "--540p") - glfwSetWindowSize(glfwWindow, 960, 540); - else if (arg == "--270p") - glfwSetWindowSize(glfwWindow, 480, 270); - else if (arg == "--pointSize" || arg == "-ps") - pointSize = std::stof(av[++i]); + + std::shared_ptr app = std::make_shared("OSPRay Studio GUI"); + StudioContext::addToCommandLine(app); + MainWindow::addToCommandLine(app); + try { + app->parse(ac, av); + } catch (const CLI::ParseError &e) { + exit(app->exit(e)); + } + + // XXX: changing windowSize here messes causes some display scaling issues + // because it desyncs window and framebuffer size with any scaling + if (optResolution.x != 0) { + windowSize = optResolution; + glfwSetWindowSize(glfwWindow, optResolution.x, optResolution.y); + reshape(windowSize); } + rendererTypeStr = optRendererTypeStr; if (!filesToImport.empty()) { std::cout << "Import files from cmd line" << std::endl; @@ -1155,7 +1126,7 @@ bool MainWindow::parseCommandLine() void MainWindow::importFiles(sg::NodePtr world) { std::vector cameras; - if (animate) + if (optAnimate) animationManager = std::shared_ptr(new AnimationManager); for (auto file : filesToImport) { @@ -1184,15 +1155,16 @@ void MainWindow::importFiles(sg::NodePtr world) importer->setMaterialRegistry(baseMaterialRegistry); importer->setCameraList(cameras); importer->setLightsManager(lightsManager); + importer->setArguments(studioCommon.argc, (char**)studioCommon.argv); if (animationManager) importer->setAnimationList(animationManager->getAnimations()); - if (instanceConfig == "dynamic") + if (optInstanceConfig == "dynamic") importer->setInstanceConfiguration( sg::InstanceConfiguration::DYNAMIC); - else if (instanceConfig == "compact") + else if (optInstanceConfig == "compact") importer->setInstanceConfiguration( sg::InstanceConfiguration::COMPACT); - else if (instanceConfig == "robust") + else if (optInstanceConfig == "robust") importer->setInstanceConfiguration( sg::InstanceConfiguration::ROBUST); @@ -1216,19 +1188,13 @@ void MainWindow::importFiles(sg::NodePtr world) } if (cameras.size() > 0) { - auto &mainCamera = frame->child("camera"); - auto defaultCamera = - sg::createNode("default_camera", frame->child("camera").subType()); - for (auto &c : mainCamera.children()) { - defaultCamera->createChild( - c.first, c.second->subType(), c.second->value()); - } - - g_sceneCameras["default camera"] = defaultCamera; + auto mainCamera = frame->child("camera").nodeAs(); + g_sceneCameras[mainCamera->child("uniqueCameraName") + .valueAs()] = mainCamera; // populate cameras in camera editor in View menu for (auto &c : cameras) - g_sceneCameras[c->name()] = c; + g_sceneCameras[c->child("uniqueCameraName").valueAs()] = c; } } @@ -1303,10 +1269,10 @@ void MainWindow::buildMainMenuFile() if (ImGui::BeginMenu("File")) { if (ImGui::MenuItem("Import ...", nullptr)) { showImportFileBrowser = true; - animate = false; + optAnimate = false; } else if (ImGui::MenuItem("Import and animate ...", nullptr)) { showImportFileBrowser = true; - animate = true; + optAnimate = true; } if (ImGui::BeginMenu("Demo Scene")) { for (size_t i = 0; i < g_scenes.size(); ++i) { @@ -1530,8 +1496,8 @@ void MainWindow::buildMainMenuView() ImGui::SetNextItemWidth(5 * ImGui::GetFontSize()); frame->childAs("renderer")["varianceThreshold"]. traverse(sg::TreeState::ROOTOPEN); - ImGui::Checkbox("Auto rotate", &autorotate); - if (autorotate) { + ImGui::Checkbox("Auto rotate", &optAutorotate); + if (optAutorotate) { ImGui::SameLine(); ImGui::SetNextItemWidth(5 * ImGui::GetFontSize()); ImGui::SliderInt(" speed", &autorotateSpeed, 1, 100); @@ -1796,21 +1762,21 @@ void MainWindow::buildWindowFrameBufferEditor() switch (whichBuffer) { case 0: - showColor = true; - showAlbedo = showDepth = showDepthInvert = false; + optShowColor = true; + optShowAlbedo = optShowDepth = optShowDepthInvert = false; break; case 1: - showAlbedo = true; - showColor = showDepth = showDepthInvert = false; + optShowAlbedo = true; + optShowColor = optShowDepth = optShowDepthInvert = false; break; case 2: - showDepth = true; - showColor = showAlbedo = showDepthInvert = false; + optShowDepth = true; + optShowColor = optShowAlbedo = optShowDepthInvert = false; break; case 3: - showDepth = true; - showDepthInvert = true; - showColor = showAlbedo = false; + optShowDepth = true; + optShowDepthInvert = true; + optShowColor = optShowAlbedo = false; break; } @@ -2172,9 +2138,7 @@ void MainWindow::buildWindowLightEditor() if (lightsManager->addLight(lightName, lightType)) { if (lightType == "hdri") { auto &hdri = lightsManager->child(lightName); - auto &hdriTex = hdri.createChild("map", "texture_2d"); - auto ast2d = hdriTex.nodeAs(); - ast2d->load(texFileName, false, false); + hdri["filename"] = texFileName.str(); } // Select newly added light whichLight = lights.size() - 1; @@ -2206,34 +2170,35 @@ void MainWindow::buildWindowCameraEditor() } // Only present selector UI if more than one camera - if (!g_sceneCameras.empty() && - ImGui::Combo("sceneCameras##whichCamera", + if (!g_sceneCameras.empty() + && ImGui::Combo("sceneCameras##whichCamera", &whichCamera, cameraUI_callback, nullptr, g_sceneCameras.size())) { - if (whichCamera > -1 && whichCamera < (int) g_sceneCameras.size()) { + if (whichCamera > -1 && whichCamera < (int)g_sceneCameras.size()) { auto &newCamera = g_sceneCameras.at_index(whichCamera); g_selectedSceneCamera = newCamera.second; - g_animateCamera = g_selectedSceneCamera->nodeAs()->animate; - - // Change the camera type, if the new camera is different. - if (frame->childAs("camera").subType() - != newCamera.second->subType()) { - frame->createChildAs("camera", - newCamera.second->subType()); + auto hasParents = g_selectedSceneCamera->parents().size(); + frame->remove("camera"); + frame->add(g_selectedSceneCamera); + + // TODO: remove this Hack : for some reason the accumulated transform in + // transform node does not get updated for the BIT animation scene. + // Attempting to make transform modified so it picks up accumulated + // transform values made by renderScene + if (hasParents) { + auto cameraXfm = g_selectedSceneCamera->parents().front(); + if (cameraXfm->valueAs() == affine3f(one)) + cameraXfm->createChild("refresh", "bool"); } - // Add new camera params - auto &camera = frame->childAs("camera"); - for (auto &c : g_selectedSceneCamera->children()) - camera.add(c.second); - - auto newCS = g_selectedSceneCamera->nodeAs()->getState(); - arcballCamera->setState(*newCS); - + if (g_selectedSceneCamera->hasChild("aspect")) + lockAspectRatio = + g_selectedSceneCamera->child("aspect").valueAs(); reshape(windowSize); // resets aspect - updateCamera(); + if (!hasParents) + updateCamera(); } } @@ -2286,17 +2251,9 @@ void MainWindow::buildWindowTransferFunctionEditor() } // Gather all transfer functions in the scene - std::map transferFunctions = {}; - for (auto &node : frame->child("world").children()) - if (node.second->type() == sg::NodeType::GENERATOR - || node.second->type() == sg::NodeType::IMPORTER - || node.second->type() == sg::NodeType::VOLUME) { - auto tfn = - findFirstNodeOfType(node.second, sg::NodeType::TRANSFER_FUNCTION); - // node.first is a unique name. tfn->name() is always "transferFunction" - if (tfn) - transferFunctions[node.first] = tfn; - } + sg::CollectTransferFunctions visitor; + frame->traverse(visitor); + auto &transferFunctions = visitor.transferFunctions; if (transferFunctions.empty()) { ImGui::Text("== empty == "); @@ -2605,23 +2562,3 @@ void MainWindow::buildWindowRenderingStats() ImGui::End(); } - -void MainWindow::printHelp() -{ - const char *help = R"help(ospStudio gui [options] [file1 [file2 ...]] - - OPTIONS - -h, --help this help message - -pf N, --pixelfilter N set default pixel filter: - 0 = point - 1 = box - 2 = Gaussian - 3 = Mitchell-Netravali - 4 = Blackman-Harris - -a, --animate enable loading glTF animations - --2160p, --1440p, set window/frame resolution - --1080p, --720p, - --540p, --270p -)help"; - std::cerr << help; -} diff --git a/app/MainWindow.h b/app/MainWindow.h index 56a3313f..b32a41e0 100644 --- a/app/MainWindow.h +++ b/app/MainWindow.h @@ -14,6 +14,7 @@ // std #include +#include #include "widgets/AnimationWidget.h" #include "PluginManager.h" #include "sg/importer/Importer.h" @@ -68,6 +69,7 @@ class MainWindow : public StudioContext void mainLoop(); + void addToCommandLine(std::shared_ptr app) override; bool parseCommandLine() override; void start() override; @@ -82,8 +84,6 @@ class MainWindow : public StudioContext void updateTitleBar(); - std::string rendererTypeStr{"pathtracer"}; - void refreshRenderer(); void updateCamera() override; @@ -92,6 +92,8 @@ class MainWindow : public StudioContext int whichLightType{-1}; int whichCamera{0}; std::string lightTypeStr{"ambient"}; + std::string scene; + std::string rendererTypeStr; protected: void buildPanel(); @@ -135,16 +137,10 @@ class MainWindow : public StudioContext void buildWindowRenderingStats(); void setCameraSnapshot(size_t snapshot); - void printHelp() override; std::vector cameraStack; sg::NodePtr g_selectedSceneCamera; - //Volume parameters - sg::NodePtr volumeParams; - - float pointSize{0.05f}; - // Plugins // std::vector> pluginPanels; @@ -174,20 +170,12 @@ class MainWindow : public StudioContext static MainWindow *activeWindow; + int fontSize{13}; // pixels + vec2f contentScale; vec2i windowSize; vec2i fbSize; vec2f previousMouse{-1.f}; - bool showColor{true}; - bool showAlbedo{false}; - bool showDepth{false}; - bool showDepthInvert{false}; - bool autorotate{false}; - bool linkNodes{false}; - bool animate{false}; - - std::string scene; - OSPRayRendererType rendererType{OSPRayRendererType::SCIVIS}; int optPF = -1; // optional pixel filter, -1 = use default @@ -231,6 +219,11 @@ class MainWindow : public StudioContext std::shared_ptr animationWidget{nullptr}; - std::string sceneConfig{""}; - std::string instanceConfig{""}; + // CLI + bool optShowColor{true}; + bool optShowAlbedo{false}; + bool optShowDepth{false}; + bool optShowDepthInvert{false}; + bool optAutorotate{false}; + bool optAnimate{false}; }; diff --git a/app/PluginManager.cpp b/app/PluginManager.cpp index 7cb3c971..f9d8d0a8 100644 --- a/app/PluginManager.cpp +++ b/app/PluginManager.cpp @@ -3,40 +3,17 @@ #include "PluginManager.h" +#include "sg/PluginCore.h" + #include void PluginManager::loadPlugin(const std::string &name) { - std::cout << "...attempting to load plugin '" << name << "'\n"; - std::string libName = "ospray_studio_plugin_" + name; - try { - rkcommon::loadLibrary(libName, false); - } catch (std::runtime_error &e) { - std::cout << "...failed to load plugin '" << name << "'!" - << " (plugin was not found). Please verify the name of the plugin" - << " is correct and that it is on your LD_LIBRARY_PATH." - << e.what() << std::endl; - return; - } - - std::string initSymName = "init_plugin_" + name; - void *initSym = rkcommon::getSymbol(initSymName); - if (!initSym) { - std::cout << "...failed to load plugin, could not find init function!\n"; - return; - } - - Plugin *(*initMethod)() = (Plugin * (*)()) initSym; - - try { + void *plugin = ospray::sg::loadPluginCore(name); + if (plugin != nullptr) { auto pluginInstance = - std::unique_ptr(static_cast(initMethod())); + std::unique_ptr(static_cast(plugin)); addPlugin(std::move(pluginInstance)); - } catch (const std::exception &e) { - std::cout << "...failed to initialize plugin '" << name << "'!\n"; - std::cout << " " << e.what() << std::endl; - } catch (...) { - std::cout << "...failed to initialize plugin '" << name << "'!\n"; } } diff --git a/app/TimeSeriesWindow.cpp b/app/TimeSeriesWindow.cpp index 3cb235a0..35868781 100644 --- a/app/TimeSeriesWindow.cpp +++ b/app/TimeSeriesWindow.cpp @@ -17,6 +17,9 @@ #include "sg/scene/volume/VolumeTimeStep.h" #include "sg/scene/volume/Volume.h" +// CLI +#include + using namespace ospray::sg; std::vector variablesLoaded; @@ -180,7 +183,7 @@ void TimeSeriesWindow::mainLoop() } auto tfn = std::static_pointer_cast( - sg::createNode("tfn_" + to_string(i), "transfer_function_cloud")); + sg::createNode("tfn_" + to_string(i), "transfer_function_turbo")); for (int j = 0; j < numInstances; j++) { auto newX = createNode("geomXfm" + to_string(j), "transform"); @@ -254,7 +257,7 @@ void TimeSeriesWindow::mainLoop() } auto tfn = std::static_pointer_cast( - sg::createNode("tfn_" + to_string(i), "transfer_function_cloud")); + sg::createNode("tfn_" + to_string(i), "transfer_function_turbo")); for (int j = 0; j < numInstances; j++) { auto newX = createNode("geomXfm" + to_string(j), "transform"); @@ -307,90 +310,39 @@ void TimeSeriesWindow::updateWindowTitle(std::string &updatedTitle) updatedTitle = windowTitle.str(); } -bool TimeSeriesWindow::parseCommandLine() -{ - int argc = studioCommon.argc; - const char **argv = studioCommon.argv; - - if (argc < 2) { - std::cout << "incomplete/wrong usage of command line params" << std::endl; - - std::cerr << "usage: " << argv[0] << " timeseries" - <<" [-renderer scivis | pathtracer ]" - <<" [-dimensions ] " - << "[-voxelType OSP_FLOAT | OSP_INT] " - << "[-gridOrigin ] " - "[-gridSpacing ] " - << "[-numInstances ] " - << "[-variable >...]" << std::endl; - return 1; - } - while (argIndex < argc) { - std::string switchArg(argv[argIndex++]); - - if (switchArg == "-renderer") { - // XXX This doesn't play nicely with the whichRenderer int in MainWindow - rendererTypeStr = argv[argIndex++]; - } - - else if (switchArg == "-light") { - // XXX This doesn't play nicely with the whichLight int in MainWindow - lightTypeStr = argv[argIndex++]; - } - - else if (switchArg == "-numInstances") { - numInstances = stoi(std::string(argv[argIndex++])); - } - - else if (switchArg == "-dimensions") { - - const std::string dimX(argv[argIndex++]); - const std::string dimY(argv[argIndex++]); - const std::string dimZ(argv[argIndex++]); - - dimensions = vec3i(stoi(dimX), stoi(dimY), stoi(dimZ)); - } - - else if (switchArg == "-voxelType") { - auto voxelTypeStr = std::string(argv[argIndex++]); - auto it = sg::volumeVoxelType.find(voxelTypeStr); - if (it != sg::volumeVoxelType.end()) { - voxelType = it->second; - } else { - throw std::runtime_error("improper -voxelType format requested"); - } - } - - else if (switchArg == "-gridSpacing") { - - const std::string gridSpacingX(argv[argIndex++]); - const std::string gridSpacingY(argv[argIndex++]); - const std::string gridSpacingZ(argv[argIndex++]); - - gridSpacing = - vec3f(stof(gridSpacingX), stof(gridSpacingY), stof(gridSpacingZ)); - } - - else if (switchArg == "-gridOrigin") { - - const std::string gridOriginX(argv[argIndex++]); - const std::string gridOriginY(argv[argIndex++]); - const std::string gridOriginZ(argv[argIndex++]); - - gridOrigin = - vec3f(stof(gridOriginX), stof(gridOriginY), stof(gridOriginZ)); - } - - else if (switchArg == "-separateTimeseries") { - importAsSeparateTimeseries = true; - } - - else if (switchArg == "-variable") { - if (argc < argIndex + 1) { - throw std::runtime_error("improper -variable arguments"); - } - - // fit it into a unit cube (if no other fetchnamerid spacing provided) +void TimeSeriesWindow::addToCommandLine(std::shared_ptr app) { + app->add_option( + "--lightType", + lightTypeStr, + "set the type of light" + )->check(CLI::IsMember({"ambient", "distant", "hdri", "sphere", "spot", "sunSky", "quad"})); + app->add_option( + "--numInstances", + numInstances, + "set the number of rendering instances" + )->check(CLI::PositiveNumber); + + app->add_flag( + "--separateTimeseries", + importAsSeparateTimeseries, + "Load volumes as separate timeseries objects" + ); + app->add_flag( + "--localLoading", + g_localLoading, + "Load volumes locally" + ); + app->add_flag( + "--separateFb", + setSeparateFramebuffers, + "Render into separate framebuffers" + ); + + app->remove_option(app->get_option_no_throw("files")); + app->add_option_function>( + "files", + [&](const std::vector val) { + // fit it into a unit cube (if no other gridSpacing provided) if (gridSpacing == vec3f(-1.f)) { const float normalizedGridSpacing = reduce_min(1.f / dimensions); @@ -398,32 +350,26 @@ bool TimeSeriesWindow::parseCommandLine() gridSpacing = vec3f(normalizedGridSpacing); } - std::vector singleVariableData; - - while (argIndex < argc && argv[argIndex][0] != '-') { - const std::string filename(argv[argIndex++]); - singleVariableData.push_back(filename); - } - - allVariablesData.push_back(singleVariableData); - } - - else if (switchArg == "-localLoading") { - g_localLoading = true; - } - - else if (switchArg == "-separateFb") { - setSeparateFramebuffers = true; - } + allVariablesData.push_back(val); + return true; + }, + "Load these volume files" + ); +} - else { - // Ignore "--osp:" ospray arguments - if (switchArg.rfind("--osp:") != std::string::npos) - continue; - printHelp(); - std::cerr << "switch arg: " << switchArg << std::endl; - throw std::runtime_error("unknown switch argument"); - } +bool TimeSeriesWindow::parseCommandLine() +{ + int ac = studioCommon.argc; + const char **av = studioCommon.argv; + + std::shared_ptr app = std::make_shared("OSPRay Studio Timeseries"); + StudioContext::addToCommandLine(app); + MainWindow::addToCommandLine(app); + TimeSeriesWindow::addToCommandLine(app); + try { + app->parse(ac, av); + } catch (const CLI::ParseError &e) { + exit(app->exit(e)); } std::cerr << "local loading for time steps (on each process): " @@ -656,25 +602,3 @@ void TimeSeriesWindow::setTimestep(int timestep) if (setSeparateFramebuffers) setTimestepFb(timestep); } - -void TimeSeriesWindow::printHelp() -{ - std::cout << - R"text( - ./ospStudio timeseries [parameters] [files] - - requirement for parameteres depend on the type of volume import. - - ospStudio timeseries specific parameters: - -separateTimeseries add volume variables as separate dropdown selectable timeseries - -separateFb configure separate Framebuffer per timestep - -localLoading to disable asynchronous loading of timesteps - -numInstances - -renderer pathtracer | scivis - -dimensions - -voxelType OSP_FLOAT | - -gridSpacing - -gridOrigin - -variable >... )text" - << std::endl; -} diff --git a/app/TimeSeriesWindow.h b/app/TimeSeriesWindow.h index f3038794..241d311f 100644 --- a/app/TimeSeriesWindow.h +++ b/app/TimeSeriesWindow.h @@ -1,4 +1,4 @@ -// Copyright 2019-2020 Intel Corporation +// Copyright 2019-2021 Intel Corporation // SPDX-License-Identifier: Apache-2.0 #pragma once @@ -27,6 +27,7 @@ class TimeSeriesWindow : public MainWindow void start() override; + void addToCommandLine(std::shared_ptr app) override; bool parseCommandLine() override; void mainLoop(); @@ -45,8 +46,6 @@ class TimeSeriesWindow : public MainWindow void setVariableTimeseries(int whichVariable, int timestep); - void printHelp() override; - std::vector g_volumeParameters; std::vector> g_allWorlds; std::vector>> diff --git a/app/ospStudio.cpp b/app/ospStudio.cpp index c68c8f61..eb7d6f4d 100644 --- a/app/ospStudio.cpp +++ b/app/ospStudio.cpp @@ -1,4 +1,4 @@ -// Copyright 2018-2020 Intel Corporation +// Copyright 2018-2021 Intel Corporation // SPDX-License-Identifier: Apache-2.0 #include "ospStudio.h" @@ -6,10 +6,142 @@ #include "MainWindow.h" #include "Batch.h" #include "TimeSeriesWindow.h" +// CLI +#include using namespace ospray; using rkcommon::removeArgs; +void StudioCommon::splitPluginArguments() { + int original_argc = argc; + const char **original_argv = argv; + for (int i=0; i app) { + volumeParams = std::make_shared(); + + app->add_option( + "files", + filesToImport, + "The list of files to import" + ); + app->add_option( + "--renderer", + optRendererTypeStr, + "set the renderer type" + )->check(CLI::IsMember({"scivis", "pathtracer", "ao", "debug"})); + app->add_option( + "--pixelfilter", + optPF, + "set default pixel filter (0=point, 1=box, 2=Gaussian, 3=Mitchell-Netravali, 4=Blackman-Harris)" + ); + app->add_option( + "--resolution", + [&](const std::vector val) { + std::string s = val[0]; + // s is one of: X"p", X"k", X"x"Y, X + // e.g. 720p, 4K, 1024x768, 1024 + + auto it = standardResolutionSizeMap.find(s); + int found = s.find('x'); + + if (it != standardResolutionSizeMap.end()) { + // standard size: 720p, 1080p, etc + optResolution = it->second; + } else if (found != std::string::npos) { + // exact resolution: 1024x768, 512x512, etc + std::string width = s.substr(0, found); + std::string height = s.substr(found + 1); + optResolution = vec2i(std::stoi(width), std::stoi(height)); + } else { + // exact square resolution: 1024, 512, etc + optResolution = vec2i(std::stoi(s)); + } + return true; + }, + "Set the initial framebuffer/window size (e.g. 720p, 4K, 1024x768, 1024)" + ); + app->add_option( + "--dimensions", + [&](const std::vector val) { + auto dimensions = vec3i(std::stoi(val[0]), std::stoi(val[1]), std::stoi(val[2])); + volumeParams->createChild("dimensions", "vec3i", dimensions); + return true; + }, + "Set the dimensions for imported volumes" + )->expected(3); + app->add_option( + "--gridSpacing", + [&](const std::vector val) { + auto gridSpacing = vec3f(std::stof(val[0]), std::stof(val[1]), std::stof(val[2])); + volumeParams->createChild("gridSpacing", "vec3f", gridSpacing); + return true; + }, + "Set the grid spacing for imported volumes" + )->expected(3); + app->add_option( + "--gridOrigin", + [&](const std::vector val) { + auto gridOrigin = vec3f(std::stof(val[0]), std::stof(val[1]), std::stof(val[2])); + volumeParams->createChild("gridSpacing", "vec3f", gridOrigin); + return true; + }, + "Set the grid origin for imported volumes" + )->expected(3); + app->add_option_function( + "--voxelType", + [&](const OSPDataType &voxelType) { + volumeParams->createChild("voxelType", "int", (int)voxelType); + }, + "Set the voxel type for imported volumes" + )->transform(CLI::CheckedTransformer(sg::volumeVoxelType)); + app->add_option( + "--sceneConfig", + optSceneConfig, + "Set the scene configuration (valid values: dynamic, compact, robust)" + )->check(CLI::IsMember({"dynamic", "compact", "robust"})); + app->add_option( + "--instanceConfig", + optInstanceConfig, + "Set the instance configuration (valid values: dynamic, compact, robust)" + )->check(CLI::IsMember({"dynamic", "compact", "robust"})); + app->add_option( + "--pointSize", + pointSize, + "Set the importer's point size" + ); +} + int main(int argc, const char *argv[]) { std::cout << "OSPRay Studio" << std::endl; @@ -82,6 +214,7 @@ int main(int argc, const char *argv[]) // Set parameters common to all modes StudioCommon studioCommon(pluginsToLoad, denoiser, argc, argv); + studioCommon.splitPluginArguments(); // This scope contains all OSPRay API calls. It enforces cleanly calling all // destructors before calling ospShutdown() diff --git a/app/ospStudio.h b/app/ospStudio.h index 53ed2117..d0ef7d3b 100644 --- a/app/ospStudio.h +++ b/app/ospStudio.h @@ -22,6 +22,12 @@ #include "version.h" +// Forward-declare CLI::App to prevent every file that imports this header from +// also having to load CLI11.hpp. +namespace CLI { +class App; +} + using namespace ospray; using namespace rkcommon::math; @@ -40,6 +46,20 @@ const static std::map StudioModeMap = { {"server", StudioMode::HEADLESS}, {"timeseries", StudioMode::TIMESERIES}}; +const static std::map standardResolutionSizeMap = { + {"144p", {256, 144}}, + {"240p", {426, 240}}, + {"360p", {640, 360}}, + {"480p", {640, 480}}, + {"720p", {1280, 720}}, + {"1080p", {1920, 1080}}, + {"1440p", {2560, 1440}}, + {"2160p", {3840, 2160}}, + {"4K", {3840, 2160}}, + {"4320p", {7680, 4320}}, + {"8K", {7680, 4320}}}; + + // Common across all modes class StudioCommon @@ -52,7 +72,9 @@ class StudioCommon : pluginsToLoad(_pluginsToLoad), denoiserAvailable(denoiser), argc(_argc), - argv(_argv){}; + argv(_argv) {} + + void splitPluginArguments(); std::vector pluginsToLoad; bool denoiserAvailable{false}; @@ -60,6 +82,8 @@ class StudioCommon int argc; const char **argv; + int plugin_argc{0}; + const char **plugin_argv{nullptr}; }; // abstract base class for all Studio modes @@ -82,6 +106,7 @@ class StudioContext : public std::enable_shared_from_this virtual void start() = 0; virtual bool parseCommandLine() = 0; + virtual void addToCommandLine(std::shared_ptr app); virtual void importFiles(sg::NodePtr world) = 0; virtual void refreshScene(bool resetCam) = 0; virtual void updateCamera() = 0; @@ -118,16 +143,21 @@ class StudioContext : public std::enable_shared_from_this // XXX should be OSPStereoMode, but for that we need 'uchar' Nodes int optStereoMode = 0; float optInterpupillaryDistance = 0.0635f; + sg::NodePtr volumeParams; + float pointSize{0.05f}; + vec2i optResolution{0, 0}; + std::string optSceneConfig{""}; + std::string optInstanceConfig{""}; - protected: - virtual void printHelp() - { - std::cerr << "common Studio help message" << std::endl; - } + StudioCommon &studioCommon; + bool showPoseBBox{false}; + bool showInstBBox{false}; + bool showInstBBoxFrame{false}; + + protected: bool sgScene{false}; // whether we are loading a scene file - StudioCommon &studioCommon; }; inline OSPError initializeOSPRay(int &argc, const char **argv) diff --git a/docs/api.md b/docs/api.md index 45971a08..067ac203 100644 --- a/docs/api.md +++ b/docs/api.md @@ -336,8 +336,8 @@ parameter name, and their value as the parameter's value. A basic example of a scenegraph contains a `Frame` node at the root. By default, this node will have a `FrameBuffer`, `Camera`, `Renderer`, and -`World` child. The `World` will contain a `Lights` node (lights manager) -which provides it a single ambient light `Light` node. +`World` child. The `Frame` will contain a `Lights` node (lights manager) +which provides it a single ambient light `Light` node by default. From here, importer nodes may be added to the world. `Transform`, `Geometry`, and `Volume` nodes are children of the importer node that diff --git a/external/CMakeLists.txt b/external/CMakeLists.txt index 07160212..02970c82 100644 --- a/external/CMakeLists.txt +++ b/external/CMakeLists.txt @@ -1,4 +1,4 @@ -## Copyright 2009-2020 Intel Corporation +## Copyright 2009-2021 Intel Corporation ## SPDX-License-Identifier: Apache-2.0 add_subdirectory(imgui) @@ -8,3 +8,4 @@ add_subdirectory(tiny_obj_loader) add_subdirectory(tiny_gltf) add_subdirectory(stb_image) add_subdirectory(json) +add_subdirectory(cli11) diff --git a/external/cli11/CLI11.hpp b/external/cli11/CLI11.hpp new file mode 100644 index 00000000..ce1d06e3 --- /dev/null +++ b/external/cli11/CLI11.hpp @@ -0,0 +1,8938 @@ +// CLI11: Version 2.0.0 +// Originally designed by Henry Schreiner +// https://github.com/CLIUtils/CLI11 +// +// This is a standalone header file generated by MakeSingleHeader.py in CLI11/scripts +// from: v2.0.0 (added include gaurd) +// +// CLI11 2.0.0 Copyright (c) 2017-2020 University of Cincinnati, developed by Henry +// Schreiner under NSF AWARD 1414736. All rights reserved. +// +// Redistribution and use in source and binary forms of CLI11, with or without +// modification, are permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this +// list of conditions and the following disclaimer. +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// 3. Neither the name of the copyright holder nor the names of its contributors +// may be used to endorse or promote products derived from this software without +// specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +// ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +// ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +// Standard combined includes: +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +#define CLI11_VERSION_MAJOR 2 +#define CLI11_VERSION_MINOR 0 +#define CLI11_VERSION_PATCH 0 +#define CLI11_VERSION "2.0.0" + + + + +// The following version macro is very similar to the one in pybind11 +#if !(defined(_MSC_VER) && __cplusplus == 199711L) && !defined(__INTEL_COMPILER) +#if __cplusplus >= 201402L +#define CLI11_CPP14 +#if __cplusplus >= 201703L +#define CLI11_CPP17 +#if __cplusplus > 201703L +#define CLI11_CPP20 +#endif +#endif +#endif +#elif defined(_MSC_VER) && __cplusplus == 199711L +// MSVC sets _MSVC_LANG rather than __cplusplus (supposedly until the standard is fully implemented) +// Unless you use the /Zc:__cplusplus flag on Visual Studio 2017 15.7 Preview 3 or newer +#if _MSVC_LANG >= 201402L +#define CLI11_CPP14 +#if _MSVC_LANG > 201402L && _MSC_VER >= 1910 +#define CLI11_CPP17 +#if __MSVC_LANG > 201703L && _MSC_VER >= 1910 +#define CLI11_CPP20 +#endif +#endif +#endif +#endif + +#if defined(CLI11_CPP14) +#define CLI11_DEPRECATED(reason) [[deprecated(reason)]] +#elif defined(_MSC_VER) +#define CLI11_DEPRECATED(reason) __declspec(deprecated(reason)) +#else +#define CLI11_DEPRECATED(reason) __attribute__((deprecated(reason))) +#endif + + + + +// C standard library +// Only needed for existence checking +#if defined CLI11_CPP17 && defined __has_include && !defined CLI11_HAS_FILESYSTEM +#if __has_include() +// Filesystem cannot be used if targeting macOS < 10.15 +#if defined __MAC_OS_X_VERSION_MIN_REQUIRED && __MAC_OS_X_VERSION_MIN_REQUIRED < 101500 +#define CLI11_HAS_FILESYSTEM 0 +#else +#include +#if defined __cpp_lib_filesystem && __cpp_lib_filesystem >= 201703 +#if defined _GLIBCXX_RELEASE && _GLIBCXX_RELEASE >= 9 +#define CLI11_HAS_FILESYSTEM 1 +#elif defined(__GLIBCXX__) +// if we are using gcc and Version <9 default to no filesystem +#define CLI11_HAS_FILESYSTEM 0 +#else +#define CLI11_HAS_FILESYSTEM 1 +#endif +#else +#define CLI11_HAS_FILESYSTEM 0 +#endif +#endif +#endif +#endif + +#if defined CLI11_HAS_FILESYSTEM && CLI11_HAS_FILESYSTEM > 0 +#include // NOLINT(build/include) +#else +#include +#include +#endif + + + +namespace CLI { + + +/// Include the items in this namespace to get free conversion of enums to/from streams. +/// (This is available inside CLI as well, so CLI11 will use this without a using statement). +namespace enums { + +/// output streaming for enumerations +template ::value>::type> +std::ostream &operator<<(std::ostream &in, const T &item) { + // make sure this is out of the detail namespace otherwise it won't be found when needed + return in << static_cast::type>(item); +} + +} // namespace enums + +/// Export to CLI namespace +using enums::operator<<; + +namespace detail { +/// a constant defining an expected max vector size defined to be a big number that could be multiplied by 4 and not +/// produce overflow for some expected uses +constexpr int expected_max_vector_size{1 << 29}; +// Based on http://stackoverflow.com/questions/236129/split-a-string-in-c +/// Split a string by a delim +inline std::vector split(const std::string &s, char delim) { + std::vector elems; + // Check to see if empty string, give consistent result + if(s.empty()) { + elems.emplace_back(); + } else { + std::stringstream ss; + ss.str(s); + std::string item; + while(std::getline(ss, item, delim)) { + elems.push_back(item); + } + } + return elems; +} + +/// Simple function to join a string +template std::string join(const T &v, std::string delim = ",") { + std::ostringstream s; + auto beg = std::begin(v); + auto end = std::end(v); + if(beg != end) + s << *beg++; + while(beg != end) { + s << delim << *beg++; + } + return s.str(); +} + +/// Simple function to join a string from processed elements +template ::value>::type> +std::string join(const T &v, Callable func, std::string delim = ",") { + std::ostringstream s; + auto beg = std::begin(v); + auto end = std::end(v); + auto loc = s.tellp(); + while(beg != end) { + auto nloc = s.tellp(); + if(nloc > loc) { + s << delim; + loc = nloc; + } + s << func(*beg++); + } + return s.str(); +} + +/// Join a string in reverse order +template std::string rjoin(const T &v, std::string delim = ",") { + std::ostringstream s; + for(std::size_t start = 0; start < v.size(); start++) { + if(start > 0) + s << delim; + s << v[v.size() - start - 1]; + } + return s.str(); +} + +// Based roughly on http://stackoverflow.com/questions/25829143/c-trim-whitespace-from-a-string + +/// Trim whitespace from left of string +inline std::string <rim(std::string &str) { + auto it = std::find_if(str.begin(), str.end(), [](char ch) { return !std::isspace(ch, std::locale()); }); + str.erase(str.begin(), it); + return str; +} + +/// Trim anything from left of string +inline std::string <rim(std::string &str, const std::string &filter) { + auto it = std::find_if(str.begin(), str.end(), [&filter](char ch) { return filter.find(ch) == std::string::npos; }); + str.erase(str.begin(), it); + return str; +} + +/// Trim whitespace from right of string +inline std::string &rtrim(std::string &str) { + auto it = std::find_if(str.rbegin(), str.rend(), [](char ch) { return !std::isspace(ch, std::locale()); }); + str.erase(it.base(), str.end()); + return str; +} + +/// Trim anything from right of string +inline std::string &rtrim(std::string &str, const std::string &filter) { + auto it = + std::find_if(str.rbegin(), str.rend(), [&filter](char ch) { return filter.find(ch) == std::string::npos; }); + str.erase(it.base(), str.end()); + return str; +} + +/// Trim whitespace from string +inline std::string &trim(std::string &str) { return ltrim(rtrim(str)); } + +/// Trim anything from string +inline std::string &trim(std::string &str, const std::string filter) { return ltrim(rtrim(str, filter), filter); } + +/// Make a copy of the string and then trim it +inline std::string trim_copy(const std::string &str) { + std::string s = str; + return trim(s); +} + +/// remove quotes at the front and back of a string either '"' or '\'' +inline std::string &remove_quotes(std::string &str) { + if(str.length() > 1 && (str.front() == '"' || str.front() == '\'')) { + if(str.front() == str.back()) { + str.pop_back(); + str.erase(str.begin(), str.begin() + 1); + } + } + return str; +} + +/// Make a copy of the string and then trim it, any filter string can be used (any char in string is filtered) +inline std::string trim_copy(const std::string &str, const std::string &filter) { + std::string s = str; + return trim(s, filter); +} +/// Print a two part "help" string +inline std::ostream &format_help(std::ostream &out, std::string name, const std::string &description, std::size_t wid) { + name = " " + name; + out << std::setw(static_cast(wid)) << std::left << name; + if(!description.empty()) { + if(name.length() >= wid) + out << "\n" << std::setw(static_cast(wid)) << ""; + for(const char c : description) { + out.put(c); + if(c == '\n') { + out << std::setw(static_cast(wid)) << ""; + } + } + } + out << "\n"; + return out; +} + +/// Print subcommand aliases +inline std::ostream &format_aliases(std::ostream &out, const std::vector &aliases, std::size_t wid) { + if(!aliases.empty()) { + out << std::setw(static_cast(wid)) << " aliases: "; + bool front = true; + for(const auto &alias : aliases) { + if(!front) { + out << ", "; + } else { + front = false; + } + out << alias; + } + out << "\n"; + } + return out; +} + +/// Verify the first character of an option +template bool valid_first_char(T c) { + return std::isalnum(c, std::locale()) || c == '_' || c == '?' || c == '@'; +} + +/// Verify following characters of an option +template bool valid_later_char(T c) { return valid_first_char(c) || c == '.' || c == '-'; } + +/// Verify an option name +inline bool valid_name_string(const std::string &str) { + if(str.empty() || !valid_first_char(str[0])) + return false; + for(auto c : str.substr(1)) + if(!valid_later_char(c)) + return false; + return true; +} + +/// check if a string is a container segment separator (empty or "%%") +inline bool is_separator(const std::string &str) { + static const std::string sep("%%"); + return (str.empty() || str == sep); +} + +/// Verify that str consists of letters only +inline bool isalpha(const std::string &str) { + return std::all_of(str.begin(), str.end(), [](char c) { return std::isalpha(c, std::locale()); }); +} + +/// Return a lower case version of a string +inline std::string to_lower(std::string str) { + std::transform(std::begin(str), std::end(str), std::begin(str), [](const std::string::value_type &x) { + return std::tolower(x, std::locale()); + }); + return str; +} + +/// remove underscores from a string +inline std::string remove_underscore(std::string str) { + str.erase(std::remove(std::begin(str), std::end(str), '_'), std::end(str)); + return str; +} + +/// Find and replace a substring with another substring +inline std::string find_and_replace(std::string str, std::string from, std::string to) { + + std::size_t start_pos = 0; + + while((start_pos = str.find(from, start_pos)) != std::string::npos) { + str.replace(start_pos, from.length(), to); + start_pos += to.length(); + } + + return str; +} + +/// check if the flag definitions has possible false flags +inline bool has_default_flag_values(const std::string &flags) { + return (flags.find_first_of("{!") != std::string::npos); +} + +inline void remove_default_flag_values(std::string &flags) { + auto loc = flags.find_first_of('{'); + while(loc != std::string::npos) { + auto finish = flags.find_first_of("},", loc + 1); + if((finish != std::string::npos) && (flags[finish] == '}')) { + flags.erase(flags.begin() + static_cast(loc), + flags.begin() + static_cast(finish) + 1); + } + loc = flags.find_first_of('{', loc + 1); + } + flags.erase(std::remove(flags.begin(), flags.end(), '!'), flags.end()); +} + +/// Check if a string is a member of a list of strings and optionally ignore case or ignore underscores +inline std::ptrdiff_t find_member(std::string name, + const std::vector names, + bool ignore_case = false, + bool ignore_underscore = false) { + auto it = std::end(names); + if(ignore_case) { + if(ignore_underscore) { + name = detail::to_lower(detail::remove_underscore(name)); + it = std::find_if(std::begin(names), std::end(names), [&name](std::string local_name) { + return detail::to_lower(detail::remove_underscore(local_name)) == name; + }); + } else { + name = detail::to_lower(name); + it = std::find_if(std::begin(names), std::end(names), [&name](std::string local_name) { + return detail::to_lower(local_name) == name; + }); + } + + } else if(ignore_underscore) { + name = detail::remove_underscore(name); + it = std::find_if(std::begin(names), std::end(names), [&name](std::string local_name) { + return detail::remove_underscore(local_name) == name; + }); + } else { + it = std::find(std::begin(names), std::end(names), name); + } + + return (it != std::end(names)) ? (it - std::begin(names)) : (-1); +} + +/// Find a trigger string and call a modify callable function that takes the current string and starting position of the +/// trigger and returns the position in the string to search for the next trigger string +template inline std::string find_and_modify(std::string str, std::string trigger, Callable modify) { + std::size_t start_pos = 0; + while((start_pos = str.find(trigger, start_pos)) != std::string::npos) { + start_pos = modify(str, start_pos); + } + return str; +} + +/// Split a string '"one two" "three"' into 'one two', 'three' +/// Quote characters can be ` ' or " +inline std::vector split_up(std::string str, char delimiter = '\0') { + + const std::string delims("\'\"`"); + auto find_ws = [delimiter](char ch) { + return (delimiter == '\0') ? (std::isspace(ch, std::locale()) != 0) : (ch == delimiter); + }; + trim(str); + + std::vector output; + bool embeddedQuote = false; + char keyChar = ' '; + while(!str.empty()) { + if(delims.find_first_of(str[0]) != std::string::npos) { + keyChar = str[0]; + auto end = str.find_first_of(keyChar, 1); + while((end != std::string::npos) && (str[end - 1] == '\\')) { // deal with escaped quotes + end = str.find_first_of(keyChar, end + 1); + embeddedQuote = true; + } + if(end != std::string::npos) { + output.push_back(str.substr(1, end - 1)); + if(end + 2 < str.size()) { + str = str.substr(end + 2); + } else { + str.clear(); + } + + } else { + output.push_back(str.substr(1)); + str = ""; + } + } else { + auto it = std::find_if(std::begin(str), std::end(str), find_ws); + if(it != std::end(str)) { + std::string value = std::string(str.begin(), it); + output.push_back(value); + str = std::string(it + 1, str.end()); + } else { + output.push_back(str); + str = ""; + } + } + // transform any embedded quotes into the regular character + if(embeddedQuote) { + output.back() = find_and_replace(output.back(), std::string("\\") + keyChar, std::string(1, keyChar)); + embeddedQuote = false; + } + trim(str); + } + return output; +} + +/// Add a leader to the beginning of all new lines (nothing is added +/// at the start of the first line). `"; "` would be for ini files +/// +/// Can't use Regex, or this would be a subs. +inline std::string fix_newlines(const std::string &leader, std::string input) { + std::string::size_type n = 0; + while(n != std::string::npos && n < input.size()) { + n = input.find('\n', n); + if(n != std::string::npos) { + input = input.substr(0, n + 1) + leader + input.substr(n + 1); + n += leader.size(); + } + } + return input; +} + +/// This function detects an equal or colon followed by an escaped quote after an argument +/// then modifies the string to replace the equality with a space. This is needed +/// to allow the split up function to work properly and is intended to be used with the find_and_modify function +/// the return value is the offset+1 which is required by the find_and_modify function. +inline std::size_t escape_detect(std::string &str, std::size_t offset) { + auto next = str[offset + 1]; + if((next == '\"') || (next == '\'') || (next == '`')) { + auto astart = str.find_last_of("-/ \"\'`", offset - 1); + if(astart != std::string::npos) { + if(str[astart] == ((str[offset] == '=') ? '-' : '/')) + str[offset] = ' '; // interpret this as a space so the split_up works properly + } + } + return offset + 1; +} + +/// Add quotes if the string contains spaces +inline std::string &add_quotes_if_needed(std::string &str) { + if((str.front() != '"' && str.front() != '\'') || str.front() != str.back()) { + char quote = str.find('"') < str.find('\'') ? '\'' : '"'; + if(str.find(' ') != std::string::npos) { + str.insert(0, 1, quote); + str.append(1, quote); + } + } + return str; +} + +} // namespace detail + + + + +// Use one of these on all error classes. +// These are temporary and are undef'd at the end of this file. +#define CLI11_ERROR_DEF(parent, name) \ + protected: \ + name(std::string ename, std::string msg, int exit_code) : parent(std::move(ename), std::move(msg), exit_code) {} \ + name(std::string ename, std::string msg, ExitCodes exit_code) \ + : parent(std::move(ename), std::move(msg), exit_code) {} \ + \ + public: \ + name(std::string msg, ExitCodes exit_code) : parent(#name, std::move(msg), exit_code) {} \ + name(std::string msg, int exit_code) : parent(#name, std::move(msg), exit_code) {} + +// This is added after the one above if a class is used directly and builds its own message +#define CLI11_ERROR_SIMPLE(name) \ + explicit name(std::string msg) : name(#name, msg, ExitCodes::name) {} + +/// These codes are part of every error in CLI. They can be obtained from e using e.exit_code or as a quick shortcut, +/// int values from e.get_error_code(). +enum class ExitCodes { + Success = 0, + IncorrectConstruction = 100, + BadNameString, + OptionAlreadyAdded, + FileError, + ConversionError, + ValidationError, + RequiredError, + RequiresError, + ExcludesError, + ExtrasError, + ConfigError, + InvalidError, + HorribleError, + OptionNotFound, + ArgumentMismatch, + BaseClass = 127 +}; + +// Error definitions + +/// @defgroup error_group Errors +/// @brief Errors thrown by CLI11 +/// +/// These are the errors that can be thrown. Some of them, like CLI::Success, are not really errors. +/// @{ + +/// All errors derive from this one +class Error : public std::runtime_error { + int actual_exit_code; + std::string error_name{"Error"}; + + public: + int get_exit_code() const { return actual_exit_code; } + + std::string get_name() const { return error_name; } + + Error(std::string name, std::string msg, int exit_code = static_cast(ExitCodes::BaseClass)) + : runtime_error(msg), actual_exit_code(exit_code), error_name(std::move(name)) {} + + Error(std::string name, std::string msg, ExitCodes exit_code) : Error(name, msg, static_cast(exit_code)) {} +}; + +// Note: Using Error::Error constructors does not work on GCC 4.7 + +/// Construction errors (not in parsing) +class ConstructionError : public Error { + CLI11_ERROR_DEF(Error, ConstructionError) +}; + +/// Thrown when an option is set to conflicting values (non-vector and multi args, for example) +class IncorrectConstruction : public ConstructionError { + CLI11_ERROR_DEF(ConstructionError, IncorrectConstruction) + CLI11_ERROR_SIMPLE(IncorrectConstruction) + static IncorrectConstruction PositionalFlag(std::string name) { + return IncorrectConstruction(name + ": Flags cannot be positional"); + } + static IncorrectConstruction Set0Opt(std::string name) { + return IncorrectConstruction(name + ": Cannot set 0 expected, use a flag instead"); + } + static IncorrectConstruction SetFlag(std::string name) { + return IncorrectConstruction(name + ": Cannot set an expected number for flags"); + } + static IncorrectConstruction ChangeNotVector(std::string name) { + return IncorrectConstruction(name + ": You can only change the expected arguments for vectors"); + } + static IncorrectConstruction AfterMultiOpt(std::string name) { + return IncorrectConstruction( + name + ": You can't change expected arguments after you've changed the multi option policy!"); + } + static IncorrectConstruction MissingOption(std::string name) { + return IncorrectConstruction("Option " + name + " is not defined"); + } + static IncorrectConstruction MultiOptionPolicy(std::string name) { + return IncorrectConstruction(name + ": multi_option_policy only works for flags and exact value options"); + } +}; + +/// Thrown on construction of a bad name +class BadNameString : public ConstructionError { + CLI11_ERROR_DEF(ConstructionError, BadNameString) + CLI11_ERROR_SIMPLE(BadNameString) + static BadNameString OneCharName(std::string name) { return BadNameString("Invalid one char name: " + name); } + static BadNameString BadLongName(std::string name) { return BadNameString("Bad long name: " + name); } + static BadNameString DashesOnly(std::string name) { + return BadNameString("Must have a name, not just dashes: " + name); + } + static BadNameString MultiPositionalNames(std::string name) { + return BadNameString("Only one positional name allowed, remove: " + name); + } +}; + +/// Thrown when an option already exists +class OptionAlreadyAdded : public ConstructionError { + CLI11_ERROR_DEF(ConstructionError, OptionAlreadyAdded) + explicit OptionAlreadyAdded(std::string name) + : OptionAlreadyAdded(name + " is already added", ExitCodes::OptionAlreadyAdded) {} + static OptionAlreadyAdded Requires(std::string name, std::string other) { + return OptionAlreadyAdded(name + " requires " + other, ExitCodes::OptionAlreadyAdded); + } + static OptionAlreadyAdded Excludes(std::string name, std::string other) { + return OptionAlreadyAdded(name + " excludes " + other, ExitCodes::OptionAlreadyAdded); + } +}; + +// Parsing errors + +/// Anything that can error in Parse +class ParseError : public Error { + CLI11_ERROR_DEF(Error, ParseError) +}; + +// Not really "errors" + +/// This is a successful completion on parsing, supposed to exit +class Success : public ParseError { + CLI11_ERROR_DEF(ParseError, Success) + Success() : Success("Successfully completed, should be caught and quit", ExitCodes::Success) {} +}; + +/// -h or --help on command line +class CallForHelp : public Success { + CLI11_ERROR_DEF(Success, CallForHelp) + CallForHelp() : CallForHelp("This should be caught in your main function, see examples", ExitCodes::Success) {} +}; + +/// Usually something like --help-all on command line +class CallForAllHelp : public Success { + CLI11_ERROR_DEF(Success, CallForAllHelp) + CallForAllHelp() + : CallForAllHelp("This should be caught in your main function, see examples", ExitCodes::Success) {} +}; + +/// -v or --version on command line +class CallForVersion : public Success { + CLI11_ERROR_DEF(Success, CallForVersion) + CallForVersion() + : CallForVersion("This should be caught in your main function, see examples", ExitCodes::Success) {} +}; + +/// Does not output a diagnostic in CLI11_PARSE, but allows main() to return with a specific error code. +class RuntimeError : public ParseError { + CLI11_ERROR_DEF(ParseError, RuntimeError) + explicit RuntimeError(int exit_code = 1) : RuntimeError("Runtime error", exit_code) {} +}; + +/// Thrown when parsing an INI file and it is missing +class FileError : public ParseError { + CLI11_ERROR_DEF(ParseError, FileError) + CLI11_ERROR_SIMPLE(FileError) + static FileError Missing(std::string name) { return FileError(name + " was not readable (missing?)"); } +}; + +/// Thrown when conversion call back fails, such as when an int fails to coerce to a string +class ConversionError : public ParseError { + CLI11_ERROR_DEF(ParseError, ConversionError) + CLI11_ERROR_SIMPLE(ConversionError) + ConversionError(std::string member, std::string name) + : ConversionError("The value " + member + " is not an allowed value for " + name) {} + ConversionError(std::string name, std::vector results) + : ConversionError("Could not convert: " + name + " = " + detail::join(results)) {} + static ConversionError TooManyInputsFlag(std::string name) { + return ConversionError(name + ": too many inputs for a flag"); + } + static ConversionError TrueFalse(std::string name) { + return ConversionError(name + ": Should be true/false or a number"); + } +}; + +/// Thrown when validation of results fails +class ValidationError : public ParseError { + CLI11_ERROR_DEF(ParseError, ValidationError) + CLI11_ERROR_SIMPLE(ValidationError) + explicit ValidationError(std::string name, std::string msg) : ValidationError(name + ": " + msg) {} +}; + +/// Thrown when a required option is missing +class RequiredError : public ParseError { + CLI11_ERROR_DEF(ParseError, RequiredError) + explicit RequiredError(std::string name) : RequiredError(name + " is required", ExitCodes::RequiredError) {} + static RequiredError Subcommand(std::size_t min_subcom) { + if(min_subcom == 1) { + return RequiredError("A subcommand"); + } + return RequiredError("Requires at least " + std::to_string(min_subcom) + " subcommands", + ExitCodes::RequiredError); + } + static RequiredError + Option(std::size_t min_option, std::size_t max_option, std::size_t used, const std::string &option_list) { + if((min_option == 1) && (max_option == 1) && (used == 0)) + return RequiredError("Exactly 1 option from [" + option_list + "]"); + if((min_option == 1) && (max_option == 1) && (used > 1)) { + return RequiredError("Exactly 1 option from [" + option_list + "] is required and " + std::to_string(used) + + " were given", + ExitCodes::RequiredError); + } + if((min_option == 1) && (used == 0)) + return RequiredError("At least 1 option from [" + option_list + "]"); + if(used < min_option) { + return RequiredError("Requires at least " + std::to_string(min_option) + " options used and only " + + std::to_string(used) + "were given from [" + option_list + "]", + ExitCodes::RequiredError); + } + if(max_option == 1) + return RequiredError("Requires at most 1 options be given from [" + option_list + "]", + ExitCodes::RequiredError); + + return RequiredError("Requires at most " + std::to_string(max_option) + " options be used and " + + std::to_string(used) + "were given from [" + option_list + "]", + ExitCodes::RequiredError); + } +}; + +/// Thrown when the wrong number of arguments has been received +class ArgumentMismatch : public ParseError { + CLI11_ERROR_DEF(ParseError, ArgumentMismatch) + CLI11_ERROR_SIMPLE(ArgumentMismatch) + ArgumentMismatch(std::string name, int expected, std::size_t received) + : ArgumentMismatch(expected > 0 ? ("Expected exactly " + std::to_string(expected) + " arguments to " + name + + ", got " + std::to_string(received)) + : ("Expected at least " + std::to_string(-expected) + " arguments to " + name + + ", got " + std::to_string(received)), + ExitCodes::ArgumentMismatch) {} + + static ArgumentMismatch AtLeast(std::string name, int num, std::size_t received) { + return ArgumentMismatch(name + ": At least " + std::to_string(num) + " required but received " + + std::to_string(received)); + } + static ArgumentMismatch AtMost(std::string name, int num, std::size_t received) { + return ArgumentMismatch(name + ": At Most " + std::to_string(num) + " required but received " + + std::to_string(received)); + } + static ArgumentMismatch TypedAtLeast(std::string name, int num, std::string type) { + return ArgumentMismatch(name + ": " + std::to_string(num) + " required " + type + " missing"); + } + static ArgumentMismatch FlagOverride(std::string name) { + return ArgumentMismatch(name + " was given a disallowed flag override"); + } +}; + +/// Thrown when a requires option is missing +class RequiresError : public ParseError { + CLI11_ERROR_DEF(ParseError, RequiresError) + RequiresError(std::string curname, std::string subname) + : RequiresError(curname + " requires " + subname, ExitCodes::RequiresError) {} +}; + +/// Thrown when an excludes option is present +class ExcludesError : public ParseError { + CLI11_ERROR_DEF(ParseError, ExcludesError) + ExcludesError(std::string curname, std::string subname) + : ExcludesError(curname + " excludes " + subname, ExitCodes::ExcludesError) {} +}; + +/// Thrown when too many positionals or options are found +class ExtrasError : public ParseError { + CLI11_ERROR_DEF(ParseError, ExtrasError) + explicit ExtrasError(std::vector args) + : ExtrasError((args.size() > 1 ? "The following arguments were not expected: " + : "The following argument was not expected: ") + + detail::rjoin(args, " "), + ExitCodes::ExtrasError) {} + ExtrasError(const std::string &name, std::vector args) + : ExtrasError(name, + (args.size() > 1 ? "The following arguments were not expected: " + : "The following argument was not expected: ") + + detail::rjoin(args, " "), + ExitCodes::ExtrasError) {} +}; + +/// Thrown when extra values are found in an INI file +class ConfigError : public ParseError { + CLI11_ERROR_DEF(ParseError, ConfigError) + CLI11_ERROR_SIMPLE(ConfigError) + static ConfigError Extras(std::string item) { return ConfigError("INI was not able to parse " + item); } + static ConfigError NotConfigurable(std::string item) { + return ConfigError(item + ": This option is not allowed in a configuration file"); + } +}; + +/// Thrown when validation fails before parsing +class InvalidError : public ParseError { + CLI11_ERROR_DEF(ParseError, InvalidError) + explicit InvalidError(std::string name) + : InvalidError(name + ": Too many positional arguments with unlimited expected args", ExitCodes::InvalidError) { + } +}; + +/// This is just a safety check to verify selection and parsing match - you should not ever see it +/// Strings are directly added to this error, but again, it should never be seen. +class HorribleError : public ParseError { + CLI11_ERROR_DEF(ParseError, HorribleError) + CLI11_ERROR_SIMPLE(HorribleError) +}; + +// After parsing + +/// Thrown when counting a non-existent option +class OptionNotFound : public Error { + CLI11_ERROR_DEF(Error, OptionNotFound) + explicit OptionNotFound(std::string name) : OptionNotFound(name + " not found", ExitCodes::OptionNotFound) {} +}; + +#undef CLI11_ERROR_DEF +#undef CLI11_ERROR_SIMPLE + +/// @} + + + + +// Type tools + +// Utilities for type enabling +namespace detail { +// Based generally on https://rmf.io/cxx11/almost-static-if +/// Simple empty scoped class +enum class enabler {}; + +/// An instance to use in EnableIf +constexpr enabler dummy = {}; +} // namespace detail + +/// A copy of enable_if_t from C++14, compatible with C++11. +/// +/// We could check to see if C++14 is being used, but it does not hurt to redefine this +/// (even Google does this: https://github.com/google/skia/blob/master/include/private/SkTLogic.h) +/// It is not in the std namespace anyway, so no harm done. +template using enable_if_t = typename std::enable_if::type; + +/// A copy of std::void_t from C++17 (helper for C++11 and C++14) +template struct make_void { using type = void; }; + +/// A copy of std::void_t from C++17 - same reasoning as enable_if_t, it does not hurt to redefine +template using void_t = typename make_void::type; + +/// A copy of std::conditional_t from C++14 - same reasoning as enable_if_t, it does not hurt to redefine +template using conditional_t = typename std::conditional::type; + +/// Check to see if something is bool (fail check by default) +template struct is_bool : std::false_type {}; + +/// Check to see if something is bool (true if actually a bool) +template <> struct is_bool : std::true_type {}; + +/// Check to see if something is a shared pointer +template struct is_shared_ptr : std::false_type {}; + +/// Check to see if something is a shared pointer (True if really a shared pointer) +template struct is_shared_ptr> : std::true_type {}; + +/// Check to see if something is a shared pointer (True if really a shared pointer) +template struct is_shared_ptr> : std::true_type {}; + +/// Check to see if something is copyable pointer +template struct is_copyable_ptr { + static bool const value = is_shared_ptr::value || std::is_pointer::value; +}; + +/// This can be specialized to override the type deduction for IsMember. +template struct IsMemberType { using type = T; }; + +/// The main custom type needed here is const char * should be a string. +template <> struct IsMemberType { using type = std::string; }; + +namespace detail { + +// These are utilities for IsMember and other transforming objects + +/// Handy helper to access the element_type generically. This is not part of is_copyable_ptr because it requires that +/// pointer_traits be valid. + +/// not a pointer +template struct element_type { using type = T; }; + +template struct element_type::value>::type> { + using type = typename std::pointer_traits::element_type; +}; + +/// Combination of the element type and value type - remove pointer (including smart pointers) and get the value_type of +/// the container +template struct element_value_type { using type = typename element_type::type::value_type; }; + +/// Adaptor for set-like structure: This just wraps a normal container in a few utilities that do almost nothing. +template struct pair_adaptor : std::false_type { + using value_type = typename T::value_type; + using first_type = typename std::remove_const::type; + using second_type = typename std::remove_const::type; + + /// Get the first value (really just the underlying value) + template static auto first(Q &&pair_value) -> decltype(std::forward(pair_value)) { + return std::forward(pair_value); + } + /// Get the second value (really just the underlying value) + template static auto second(Q &&pair_value) -> decltype(std::forward(pair_value)) { + return std::forward(pair_value); + } +}; + +/// Adaptor for map-like structure (true version, must have key_type and mapped_type). +/// This wraps a mapped container in a few utilities access it in a general way. +template +struct pair_adaptor< + T, + conditional_t, void>> + : std::true_type { + using value_type = typename T::value_type; + using first_type = typename std::remove_const::type; + using second_type = typename std::remove_const::type; + + /// Get the first value (really just the underlying value) + template static auto first(Q &&pair_value) -> decltype(std::get<0>(std::forward(pair_value))) { + return std::get<0>(std::forward(pair_value)); + } + /// Get the second value (really just the underlying value) + template static auto second(Q &&pair_value) -> decltype(std::get<1>(std::forward(pair_value))) { + return std::get<1>(std::forward(pair_value)); + } +}; + +// Warning is suppressed due to "bug" in gcc<5.0 and gcc 7.0 with c++17 enabled that generates a Wnarrowing warning +// in the unevaluated context even if the function that was using this wasn't used. The standard says narrowing in +// brace initialization shouldn't be allowed but for backwards compatibility gcc allows it in some contexts. It is a +// little fuzzy what happens in template constructs and I think that was something GCC took a little while to work out. +// But regardless some versions of gcc generate a warning when they shouldn't from the following code so that should be +// suppressed +#ifdef __GNUC__ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wnarrowing" +#endif +// check for constructibility from a specific type and copy assignable used in the parse detection +template class is_direct_constructible { + template + static auto test(int, std::true_type) -> decltype( +// NVCC warns about narrowing conversions here +#ifdef __CUDACC__ +#pragma diag_suppress 2361 +#endif + TT { std::declval() } +#ifdef __CUDACC__ +#pragma diag_default 2361 +#endif + , + std::is_move_assignable()); + + template static auto test(int, std::false_type) -> std::false_type; + + template static auto test(...) -> std::false_type; + + public: + static constexpr bool value = decltype(test(0, typename std::is_constructible::type()))::value; +}; +#ifdef __GNUC__ +#pragma GCC diagnostic pop +#endif + +// Check for output streamability +// Based on https://stackoverflow.com/questions/22758291/how-can-i-detect-if-a-type-can-be-streamed-to-an-stdostream + +template class is_ostreamable { + template + static auto test(int) -> decltype(std::declval() << std::declval(), std::true_type()); + + template static auto test(...) -> std::false_type; + + public: + static constexpr bool value = decltype(test(0))::value; +}; + +/// Check for input streamability +template class is_istreamable { + template + static auto test(int) -> decltype(std::declval() >> std::declval(), std::true_type()); + + template static auto test(...) -> std::false_type; + + public: + static constexpr bool value = decltype(test(0))::value; +}; + +/// Check for complex +template class is_complex { + template + static auto test(int) -> decltype(std::declval().real(), std::declval().imag(), std::true_type()); + + template static auto test(...) -> std::false_type; + + public: + static constexpr bool value = decltype(test(0))::value; +}; + +/// Templated operation to get a value from a stream +template ::value, detail::enabler> = detail::dummy> +bool from_stream(const std::string &istring, T &obj) { + std::istringstream is; + is.str(istring); + is >> obj; + return !is.fail() && !is.rdbuf()->in_avail(); +} + +template ::value, detail::enabler> = detail::dummy> +bool from_stream(const std::string & /*istring*/, T & /*obj*/) { + return false; +} + +// check to see if an object is a mutable container (fail by default) +template struct is_mutable_container : std::false_type {}; + +/// type trait to test if a type is a mutable container meaning it has a value_type, it has an iterator, a clear, and +/// end methods and an insert function. And for our purposes we exclude std::string and types that can be constructed +/// from a std::string +template +struct is_mutable_container< + T, + conditional_t().end()), + decltype(std::declval().clear()), + decltype(std::declval().insert(std::declval().end())>(), + std::declval()))>, + void>> + : public conditional_t::value, std::false_type, std::true_type> {}; + +// check to see if an object is a mutable container (fail by default) +template struct is_readable_container : std::false_type {}; + +/// type trait to test if a type is a container meaning it has a value_type, it has an iterator, a clear, and an end +/// methods and an insert function. And for our purposes we exclude std::string and types that can be constructed from +/// a std::string +template +struct is_readable_container< + T, + conditional_t().end()), decltype(std::declval().begin())>, void>> + : public std::true_type {}; + +// check to see if an object is a wrapper (fail by default) +template struct is_wrapper : std::false_type {}; + +// check if an object is a wrapper (it has a value_type defined) +template +struct is_wrapper, void>> : public std::true_type {}; + +// Check for tuple like types, as in classes with a tuple_size type trait +template class is_tuple_like { + template + // static auto test(int) + // -> decltype(std::conditional<(std::tuple_size::value > 0), std::true_type, std::false_type>::type()); + static auto test(int) -> decltype(std::tuple_size::type>::value, std::true_type{}); + template static auto test(...) -> std::false_type; + + public: + static constexpr bool value = decltype(test(0))::value; +}; + +/// Convert an object to a string (directly forward if this can become a string) +template ::value, detail::enabler> = detail::dummy> +auto to_string(T &&value) -> decltype(std::forward(value)) { + return std::forward(value); +} + +/// Construct a string from the object +template ::value && !std::is_convertible::value, + detail::enabler> = detail::dummy> +std::string to_string(const T &value) { + return std::string(value); +} + +/// Convert an object to a string (streaming must be supported for that type) +template ::value && !std::is_constructible::value && + is_ostreamable::value, + detail::enabler> = detail::dummy> +std::string to_string(T &&value) { + std::stringstream stream; + stream << value; + return stream.str(); +} + +/// If conversion is not supported, return an empty string (streaming is not supported for that type) +template ::value && !is_ostreamable::value && + !is_readable_container::type>::value, + detail::enabler> = detail::dummy> +std::string to_string(T &&) { + return std::string{}; +} + +/// convert a readable container to a string +template ::value && !is_ostreamable::value && + is_readable_container::value, + detail::enabler> = detail::dummy> +std::string to_string(T &&variable) { + std::vector defaults; + auto cval = variable.begin(); + auto end = variable.end(); + while(cval != end) { + defaults.emplace_back(CLI::detail::to_string(*cval)); + ++cval; + } + return std::string("[" + detail::join(defaults) + "]"); +} + +/// special template overload +template ::value, detail::enabler> = detail::dummy> +auto checked_to_string(T &&value) -> decltype(to_string(std::forward(value))) { + return to_string(std::forward(value)); +} + +/// special template overload +template ::value, detail::enabler> = detail::dummy> +std::string checked_to_string(T &&) { + return std::string{}; +} +/// get a string as a convertible value for arithmetic types +template ::value, detail::enabler> = detail::dummy> +std::string value_string(const T &value) { + return std::to_string(value); +} +/// get a string as a convertible value for enumerations +template ::value, detail::enabler> = detail::dummy> +std::string value_string(const T &value) { + return std::to_string(static_cast::type>(value)); +} +/// for other types just use the regular to_string function +template ::value && !std::is_arithmetic::value, detail::enabler> = detail::dummy> +auto value_string(const T &value) -> decltype(to_string(value)) { + return to_string(value); +} + +/// template to get the underlying value type if it exists or use a default +template struct wrapped_type { using type = def; }; + +/// Type size for regular object types that do not look like a tuple +template struct wrapped_type::value>::type> { + using type = typename T::value_type; +}; + +/// This will only trigger for actual void type +template struct type_count_base { static const int value{0}; }; + +/// Type size for regular object types that do not look like a tuple +template +struct type_count_base::value && !is_mutable_container::value && + !std::is_void::value>::type> { + static constexpr int value{1}; +}; + +/// the base tuple size +template +struct type_count_base::value && !is_mutable_container::value>::type> { + static constexpr int value{std::tuple_size::value}; +}; + +/// Type count base for containers is the type_count_base of the individual element +template struct type_count_base::value>::type> { + static constexpr int value{type_count_base::value}; +}; + +/// Set of overloads to get the type size of an object + +/// forward declare the subtype_count structure +template struct subtype_count; + +/// forward declare the subtype_count_min structure +template struct subtype_count_min; + +/// This will only trigger for actual void type +template struct type_count { static const int value{0}; }; + +/// Type size for regular object types that do not look like a tuple +template +struct type_count::value && !is_tuple_like::value && !is_complex::value && + !std::is_void::value>::type> { + static constexpr int value{1}; +}; + +/// Type size for complex since it sometimes looks like a wrapper +template struct type_count::value>::type> { + static constexpr int value{2}; +}; + +/// Type size of types that are wrappers,except complex and tuples(which can also be wrappers sometimes) +template struct type_count::value>::type> { + static constexpr int value{subtype_count::value}; +}; + +/// Type size of types that are wrappers,except containers complex and tuples(which can also be wrappers sometimes) +template +struct type_count::value && !is_complex::value && !is_tuple_like::value && + !is_mutable_container::value>::type> { + static constexpr int value{type_count::value}; +}; + +/// 0 if the index > tuple size +template +constexpr typename std::enable_if::value, int>::type tuple_type_size() { + return 0; +} + +/// Recursively generate the tuple type name +template + constexpr typename std::enable_if < I::value, int>::type tuple_type_size() { + return subtype_count::type>::value + tuple_type_size(); +} + +/// Get the type size of the sum of type sizes for all the individual tuple types +template struct type_count::value>::type> { + static constexpr int value{tuple_type_size()}; +}; + +/// definition of subtype count +template struct subtype_count { + static constexpr int value{is_mutable_container::value ? expected_max_vector_size : type_count::value}; +}; + +/// This will only trigger for actual void type +template struct type_count_min { static const int value{0}; }; + +/// Type size for regular object types that do not look like a tuple +template +struct type_count_min< + T, + typename std::enable_if::value && !is_tuple_like::value && !is_wrapper::value && + !is_complex::value && !std::is_void::value>::type> { + static constexpr int value{type_count::value}; +}; + +/// Type size for complex since it sometimes looks like a wrapper +template struct type_count_min::value>::type> { + static constexpr int value{1}; +}; + +/// Type size min of types that are wrappers,except complex and tuples(which can also be wrappers sometimes) +template +struct type_count_min< + T, + typename std::enable_if::value && !is_complex::value && !is_tuple_like::value>::type> { + static constexpr int value{subtype_count_min::value}; +}; + +/// 0 if the index > tuple size +template +constexpr typename std::enable_if::value, int>::type tuple_type_size_min() { + return 0; +} + +/// Recursively generate the tuple type name +template + constexpr typename std::enable_if < I::value, int>::type tuple_type_size_min() { + return subtype_count_min::type>::value + tuple_type_size_min(); +} + +/// Get the type size of the sum of type sizes for all the individual tuple types +template struct type_count_min::value>::type> { + static constexpr int value{tuple_type_size_min()}; +}; + +/// definition of subtype count +template struct subtype_count_min { + static constexpr int value{is_mutable_container::value + ? ((type_count::value < expected_max_vector_size) ? type_count::value : 0) + : type_count_min::value}; +}; + +/// This will only trigger for actual void type +template struct expected_count { static const int value{0}; }; + +/// For most types the number of expected items is 1 +template +struct expected_count::value && !is_wrapper::value && + !std::is_void::value>::type> { + static constexpr int value{1}; +}; +/// number of expected items in a vector +template struct expected_count::value>::type> { + static constexpr int value{expected_max_vector_size}; +}; + +/// number of expected items in a vector +template +struct expected_count::value && is_wrapper::value>::type> { + static constexpr int value{expected_count::value}; +}; + +// Enumeration of the different supported categorizations of objects +enum class object_category : int { + char_value = 1, + integral_value = 2, + unsigned_integral = 4, + enumeration = 6, + boolean_value = 8, + floating_point = 10, + number_constructible = 12, + double_constructible = 14, + integer_constructible = 16, + // string like types + string_assignable = 23, + string_constructible = 24, + other = 45, + // special wrapper or container types + wrapper_value = 50, + complex_number = 60, + tuple_value = 70, + container_value = 80, + +}; + +/// Set of overloads to classify an object according to type + +/// some type that is not otherwise recognized +template struct classify_object { + static constexpr object_category value{object_category::other}; +}; + +/// Signed integers +template +struct classify_object< + T, + typename std::enable_if::value && !std::is_same::value && std::is_signed::value && + !is_bool::value && !std::is_enum::value>::type> { + static constexpr object_category value{object_category::integral_value}; +}; + +/// Unsigned integers +template +struct classify_object::value && std::is_unsigned::value && + !std::is_same::value && !is_bool::value>::type> { + static constexpr object_category value{object_category::unsigned_integral}; +}; + +/// single character values +template +struct classify_object::value && !std::is_enum::value>::type> { + static constexpr object_category value{object_category::char_value}; +}; + +/// Boolean values +template struct classify_object::value>::type> { + static constexpr object_category value{object_category::boolean_value}; +}; + +/// Floats +template struct classify_object::value>::type> { + static constexpr object_category value{object_category::floating_point}; +}; + +/// String and similar direct assignment +template +struct classify_object::value && !std::is_integral::value && + std::is_assignable::value>::type> { + static constexpr object_category value{object_category::string_assignable}; +}; + +/// String and similar constructible and copy assignment +template +struct classify_object< + T, + typename std::enable_if::value && !std::is_integral::value && + !std::is_assignable::value && (type_count::value == 1) && + std::is_constructible::value>::type> { + static constexpr object_category value{object_category::string_constructible}; +}; + +/// Enumerations +template struct classify_object::value>::type> { + static constexpr object_category value{object_category::enumeration}; +}; + +template struct classify_object::value>::type> { + static constexpr object_category value{object_category::complex_number}; +}; + +/// Handy helper to contain a bunch of checks that rule out many common types (integers, string like, floating point, +/// vectors, and enumerations +template struct uncommon_type { + using type = typename std::conditional::value && !std::is_integral::value && + !std::is_assignable::value && + !std::is_constructible::value && !is_complex::value && + !is_mutable_container::value && !std::is_enum::value, + std::true_type, + std::false_type>::type; + static constexpr bool value = type::value; +}; + +/// wrapper type +template +struct classify_object::value && is_wrapper::value && + !is_tuple_like::value && uncommon_type::value)>::type> { + static constexpr object_category value{object_category::wrapper_value}; +}; + +/// Assignable from double or int +template +struct classify_object::value && type_count::value == 1 && + !is_wrapper::value && is_direct_constructible::value && + is_direct_constructible::value>::type> { + static constexpr object_category value{object_category::number_constructible}; +}; + +/// Assignable from int +template +struct classify_object::value && type_count::value == 1 && + !is_wrapper::value && !is_direct_constructible::value && + is_direct_constructible::value>::type> { + static constexpr object_category value{object_category::integer_constructible}; +}; + +/// Assignable from double +template +struct classify_object::value && type_count::value == 1 && + !is_wrapper::value && is_direct_constructible::value && + !is_direct_constructible::value>::type> { + static constexpr object_category value{object_category::double_constructible}; +}; + +/// Tuple type +template +struct classify_object< + T, + typename std::enable_if::value && + ((type_count::value >= 2 && !is_wrapper::value) || + (uncommon_type::value && !is_direct_constructible::value && + !is_direct_constructible::value))>::type> { + static constexpr object_category value{object_category::tuple_value}; + // the condition on this class requires it be like a tuple, but on some compilers (like Xcode) tuples can be + // constructed from just the first element so tuples of can be constructed from a string, which + // could lead to issues so there are two variants of the condition, the first isolates things with a type size >=2 + // mainly to get tuples on Xcode with the exception of wrappers, the second is the main one and just separating out + // those cases that are caught by other object classifications +}; + +/// container type +template struct classify_object::value>::type> { + static constexpr object_category value{object_category::container_value}; +}; + +// Type name print + +/// Was going to be based on +/// http://stackoverflow.com/questions/1055452/c-get-name-of-type-in-template +/// But this is cleaner and works better in this case + +template ::value == object_category::char_value, detail::enabler> = detail::dummy> +constexpr const char *type_name() { + return "CHAR"; +} + +template ::value == object_category::integral_value || + classify_object::value == object_category::integer_constructible, + detail::enabler> = detail::dummy> +constexpr const char *type_name() { + return "INT"; +} + +template ::value == object_category::unsigned_integral, detail::enabler> = detail::dummy> +constexpr const char *type_name() { + return "UINT"; +} + +template ::value == object_category::floating_point || + classify_object::value == object_category::number_constructible || + classify_object::value == object_category::double_constructible, + detail::enabler> = detail::dummy> +constexpr const char *type_name() { + return "FLOAT"; +} + +/// Print name for enumeration types +template ::value == object_category::enumeration, detail::enabler> = detail::dummy> +constexpr const char *type_name() { + return "ENUM"; +} + +/// Print name for enumeration types +template ::value == object_category::boolean_value, detail::enabler> = detail::dummy> +constexpr const char *type_name() { + return "BOOLEAN"; +} + +/// Print name for enumeration types +template ::value == object_category::complex_number, detail::enabler> = detail::dummy> +constexpr const char *type_name() { + return "COMPLEX"; +} + +/// Print for all other types +template ::value >= object_category::string_assignable && + classify_object::value <= object_category::other, + detail::enabler> = detail::dummy> +constexpr const char *type_name() { + return "TEXT"; +} +/// typename for tuple value +template ::value == object_category::tuple_value && type_count_base::value >= 2, + detail::enabler> = detail::dummy> +std::string type_name(); // forward declaration + +/// Generate type name for a wrapper or container value +template ::value == object_category::container_value || + classify_object::value == object_category::wrapper_value, + detail::enabler> = detail::dummy> +std::string type_name(); // forward declaration + +/// Print name for single element tuple types +template ::value == object_category::tuple_value && type_count_base::value == 1, + detail::enabler> = detail::dummy> +inline std::string type_name() { + return type_name::type>::type>(); +} + +/// Empty string if the index > tuple size +template +inline typename std::enable_if::value, std::string>::type tuple_name() { + return std::string{}; +} + +/// Recursively generate the tuple type name +template +inline typename std::enable_if<(I < type_count_base::value), std::string>::type tuple_name() { + std::string str = std::string(type_name::type>::type>()) + + ',' + tuple_name(); + if(str.back() == ',') + str.pop_back(); + return str; +} + +/// Print type name for tuples with 2 or more elements +template ::value == object_category::tuple_value && type_count_base::value >= 2, + detail::enabler>> +inline std::string type_name() { + auto tname = std::string(1, '[') + tuple_name(); + tname.push_back(']'); + return tname; +} + +/// get the type name for a type that has a value_type member +template ::value == object_category::container_value || + classify_object::value == object_category::wrapper_value, + detail::enabler>> +inline std::string type_name() { + return type_name(); +} + +// Lexical cast + +/// Convert to an unsigned integral +template ::value, detail::enabler> = detail::dummy> +bool integral_conversion(const std::string &input, T &output) noexcept { + if(input.empty()) { + return false; + } + char *val = nullptr; + std::uint64_t output_ll = std::strtoull(input.c_str(), &val, 0); + output = static_cast(output_ll); + return val == (input.c_str() + input.size()) && static_cast(output) == output_ll; +} + +/// Convert to a signed integral +template ::value, detail::enabler> = detail::dummy> +bool integral_conversion(const std::string &input, T &output) noexcept { + if(input.empty()) { + return false; + } + char *val = nullptr; + std::int64_t output_ll = std::strtoll(input.c_str(), &val, 0); + output = static_cast(output_ll); + return val == (input.c_str() + input.size()) && static_cast(output) == output_ll; +} + +/// Convert a flag into an integer value typically binary flags +inline std::int64_t to_flag_value(std::string val) { + static const std::string trueString("true"); + static const std::string falseString("false"); + if(val == trueString) { + return 1; + } + if(val == falseString) { + return -1; + } + val = detail::to_lower(val); + std::int64_t ret; + if(val.size() == 1) { + if(val[0] >= '1' && val[0] <= '9') { + return (static_cast(val[0]) - '0'); + } + switch(val[0]) { + case '0': + case 'f': + case 'n': + case '-': + ret = -1; + break; + case 't': + case 'y': + case '+': + ret = 1; + break; + default: + throw std::invalid_argument("unrecognized character"); + } + return ret; + } + if(val == trueString || val == "on" || val == "yes" || val == "enable") { + ret = 1; + } else if(val == falseString || val == "off" || val == "no" || val == "disable") { + ret = -1; + } else { + ret = std::stoll(val); + } + return ret; +} + +/// Integer conversion +template ::value == object_category::integral_value || + classify_object::value == object_category::unsigned_integral, + detail::enabler> = detail::dummy> +bool lexical_cast(const std::string &input, T &output) { + return integral_conversion(input, output); +} + +/// char values +template ::value == object_category::char_value, detail::enabler> = detail::dummy> +bool lexical_cast(const std::string &input, T &output) { + if(input.size() == 1) { + output = static_cast(input[0]); + return true; + } + return integral_conversion(input, output); +} + +/// Boolean values +template ::value == object_category::boolean_value, detail::enabler> = detail::dummy> +bool lexical_cast(const std::string &input, T &output) { + try { + auto out = to_flag_value(input); + output = (out > 0); + return true; + } catch(const std::invalid_argument &) { + return false; + } catch(const std::out_of_range &) { + // if the number is out of the range of a 64 bit value then it is still a number and for this purpose is still + // valid all we care about the sign + output = (input[0] != '-'); + return true; + } +} + +/// Floats +template ::value == object_category::floating_point, detail::enabler> = detail::dummy> +bool lexical_cast(const std::string &input, T &output) { + if(input.empty()) { + return false; + } + char *val = nullptr; + auto output_ld = std::strtold(input.c_str(), &val); + output = static_cast(output_ld); + return val == (input.c_str() + input.size()); +} + +/// complex +template ::value == object_category::complex_number, detail::enabler> = detail::dummy> +bool lexical_cast(const std::string &input, T &output) { + using XC = typename wrapped_type::type; + XC x{0.0}, y{0.0}; + auto str1 = input; + bool worked = false; + auto nloc = str1.find_last_of("+-"); + if(nloc != std::string::npos && nloc > 0) { + worked = detail::lexical_cast(str1.substr(0, nloc), x); + str1 = str1.substr(nloc); + if(str1.back() == 'i' || str1.back() == 'j') + str1.pop_back(); + worked = worked && detail::lexical_cast(str1, y); + } else { + if(str1.back() == 'i' || str1.back() == 'j') { + str1.pop_back(); + worked = detail::lexical_cast(str1, y); + x = XC{0}; + } else { + worked = detail::lexical_cast(str1, x); + y = XC{0}; + } + } + if(worked) { + output = T{x, y}; + return worked; + } + return from_stream(input, output); +} + +/// String and similar direct assignment +template ::value == object_category::string_assignable, detail::enabler> = detail::dummy> +bool lexical_cast(const std::string &input, T &output) { + output = input; + return true; +} + +/// String and similar constructible and copy assignment +template < + typename T, + enable_if_t::value == object_category::string_constructible, detail::enabler> = detail::dummy> +bool lexical_cast(const std::string &input, T &output) { + output = T(input); + return true; +} + +/// Enumerations +template ::value == object_category::enumeration, detail::enabler> = detail::dummy> +bool lexical_cast(const std::string &input, T &output) { + typename std::underlying_type::type val; + if(!integral_conversion(input, val)) { + return false; + } + output = static_cast(val); + return true; +} + +/// wrapper types +template ::value == object_category::wrapper_value && + std::is_assignable::value, + detail::enabler> = detail::dummy> +bool lexical_cast(const std::string &input, T &output) { + typename T::value_type val; + if(lexical_cast(input, val)) { + output = val; + return true; + } + return from_stream(input, output); +} + +template ::value == object_category::wrapper_value && + !std::is_assignable::value && std::is_assignable::value, + detail::enabler> = detail::dummy> +bool lexical_cast(const std::string &input, T &output) { + typename T::value_type val; + if(lexical_cast(input, val)) { + output = T{val}; + return true; + } + return from_stream(input, output); +} + +/// Assignable from double or int +template < + typename T, + enable_if_t::value == object_category::number_constructible, detail::enabler> = detail::dummy> +bool lexical_cast(const std::string &input, T &output) { + int val; + if(integral_conversion(input, val)) { + output = T(val); + return true; + } else { + double dval; + if(lexical_cast(input, dval)) { + output = T{dval}; + return true; + } + } + return from_stream(input, output); +} + +/// Assignable from int +template < + typename T, + enable_if_t::value == object_category::integer_constructible, detail::enabler> = detail::dummy> +bool lexical_cast(const std::string &input, T &output) { + int val; + if(integral_conversion(input, val)) { + output = T(val); + return true; + } + return from_stream(input, output); +} + +/// Assignable from double +template < + typename T, + enable_if_t::value == object_category::double_constructible, detail::enabler> = detail::dummy> +bool lexical_cast(const std::string &input, T &output) { + double val; + if(lexical_cast(input, val)) { + output = T{val}; + return true; + } + return from_stream(input, output); +} + +/// Non-string convertible from an int +template ::value == object_category::other && std::is_assignable::value, + detail::enabler> = detail::dummy> +bool lexical_cast(const std::string &input, T &output) { + int val; + if(integral_conversion(input, val)) { +#ifdef _MSC_VER +#pragma warning(push) +#pragma warning(disable : 4800) +#endif + // with Atomic this could produce a warning due to the conversion but if atomic gets here it is an old style + // so will most likely still work + output = val; +#ifdef _MSC_VER +#pragma warning(pop) +#endif + return true; + } + // LCOV_EXCL_START + // This version of cast is only used for odd cases in an older compilers the fail over + // from_stream is tested elsewhere an not relevant for coverage here + return from_stream(input, output); + // LCOV_EXCL_STOP +} + +/// Non-string parsable by a stream +template ::value == object_category::other && !std::is_assignable::value, + detail::enabler> = detail::dummy> +bool lexical_cast(const std::string &input, T &output) { + static_assert(is_istreamable::value, + "option object type must have a lexical cast overload or streaming input operator(>>) defined, if it " + "is convertible from another type use the add_option(...) with XC being the known type"); + return from_stream(input, output); +} + +/// Assign a value through lexical cast operations +/// Strings can be empty so we need to do a little different +template ::value && + (classify_object::value == object_category::string_assignable || + classify_object::value == object_category::string_constructible), + detail::enabler> = detail::dummy> +bool lexical_assign(const std::string &input, AssignTo &output) { + return lexical_cast(input, output); +} + +/// Assign a value through lexical cast operations +template ::value && std::is_assignable::value && + classify_object::value != object_category::string_assignable && + classify_object::value != object_category::string_constructible, + detail::enabler> = detail::dummy> +bool lexical_assign(const std::string &input, AssignTo &output) { + if(input.empty()) { + output = AssignTo{}; + return true; + } + + return lexical_cast(input, output); +} + +/// Assign a value through lexical cast operations +template ::value && !std::is_assignable::value && + classify_object::value == object_category::wrapper_value, + detail::enabler> = detail::dummy> +bool lexical_assign(const std::string &input, AssignTo &output) { + if(input.empty()) { + typename AssignTo::value_type emptyVal{}; + output = emptyVal; + return true; + } + return lexical_cast(input, output); +} + +/// Assign a value through lexical cast operations for int compatible values +/// mainly for atomic operations on some compilers +template ::value && !std::is_assignable::value && + classify_object::value != object_category::wrapper_value && + std::is_assignable::value, + detail::enabler> = detail::dummy> +bool lexical_assign(const std::string &input, AssignTo &output) { + if(input.empty()) { + output = 0; + return true; + } + int val; + if(lexical_cast(input, val)) { + output = val; + return true; + } + return false; +} + +/// Assign a value converted from a string in lexical cast to the output value directly +template ::value && std::is_assignable::value, + detail::enabler> = detail::dummy> +bool lexical_assign(const std::string &input, AssignTo &output) { + ConvertTo val{}; + bool parse_result = (!input.empty()) ? lexical_cast(input, val) : true; + if(parse_result) { + output = val; + } + return parse_result; +} + +/// Assign a value from a lexical cast through constructing a value and move assigning it +template < + typename AssignTo, + typename ConvertTo, + enable_if_t::value && !std::is_assignable::value && + std::is_move_assignable::value, + detail::enabler> = detail::dummy> +bool lexical_assign(const std::string &input, AssignTo &output) { + ConvertTo val{}; + bool parse_result = input.empty() ? true : lexical_cast(input, val); + if(parse_result) { + output = AssignTo(val); // use () form of constructor to allow some implicit conversions + } + return parse_result; +} + +/// primary lexical conversion operation, 1 string to 1 type of some kind +template ::value <= object_category::other && + classify_object::value <= object_category::wrapper_value, + detail::enabler> = detail::dummy> +bool lexical_conversion(const std::vector &strings, AssignTo &output) { + return lexical_assign(strings[0], output); +} + +/// Lexical conversion if there is only one element but the conversion type is for two, then call a two element +/// constructor +template ::value <= 2) && expected_count::value == 1 && + is_tuple_like::value && type_count_base::value == 2, + detail::enabler> = detail::dummy> +bool lexical_conversion(const std::vector &strings, AssignTo &output) { + // the remove const is to handle pair types coming from a container + typename std::remove_const::type>::type v1; + typename std::tuple_element<1, ConvertTo>::type v2; + bool retval = lexical_assign(strings[0], v1); + if(strings.size() > 1) { + retval = retval && lexical_assign(strings[1], v2); + } + if(retval) { + output = AssignTo{v1, v2}; + } + return retval; +} + +/// Lexical conversion of a container types of single elements +template ::value && is_mutable_container::value && + type_count::value == 1, + detail::enabler> = detail::dummy> +bool lexical_conversion(const std::vector &strings, AssignTo &output) { + output.erase(output.begin(), output.end()); + for(const auto &elem : strings) { + typename AssignTo::value_type out; + bool retval = lexical_assign(elem, out); + if(!retval) { + return false; + } + output.insert(output.end(), std::move(out)); + } + return (!output.empty()); +} + +/// Lexical conversion for complex types +template ::value, detail::enabler> = detail::dummy> +bool lexical_conversion(const std::vector &strings, AssignTo &output) { + + if(strings.size() >= 2 && !strings[1].empty()) { + using XC2 = typename wrapped_type::type; + XC2 x{0.0}, y{0.0}; + auto str1 = strings[1]; + if(str1.back() == 'i' || str1.back() == 'j') { + str1.pop_back(); + } + auto worked = detail::lexical_cast(strings[0], x) && detail::lexical_cast(str1, y); + if(worked) { + output = ConvertTo{x, y}; + } + return worked; + } else { + return lexical_assign(strings[0], output); + } +} + +/// Conversion to a vector type using a particular single type as the conversion type +template ::value && (expected_count::value == 1) && + (type_count::value == 1), + detail::enabler> = detail::dummy> +bool lexical_conversion(const std::vector &strings, AssignTo &output) { + bool retval = true; + output.clear(); + output.reserve(strings.size()); + for(const auto &elem : strings) { + + output.emplace_back(); + retval = retval && lexical_assign(elem, output.back()); + } + return (!output.empty()) && retval; +} + +// forward declaration + +/// Lexical conversion of a container types with conversion type of two elements +template ::value && is_mutable_container::value && + type_count_base::value == 2, + detail::enabler> = detail::dummy> +bool lexical_conversion(std::vector strings, AssignTo &output); + +/// Lexical conversion of a vector types with type_size >2 forward declaration +template ::value && is_mutable_container::value && + type_count_base::value != 2 && + ((type_count::value > 2) || + (type_count::value > type_count_base::value)), + detail::enabler> = detail::dummy> +bool lexical_conversion(const std::vector &strings, AssignTo &output); + +/// Conversion for tuples +template ::value && is_tuple_like::value && + (type_count_base::value != type_count::value || + type_count::value > 2), + detail::enabler> = detail::dummy> +bool lexical_conversion(const std::vector &strings, AssignTo &output); // forward declaration + +/// Conversion for operations where the assigned type is some class but the conversion is a mutable container or large +/// tuple +template ::value && !is_mutable_container::value && + classify_object::value != object_category::wrapper_value && + (is_mutable_container::value || type_count::value > 2), + detail::enabler> = detail::dummy> +bool lexical_conversion(const std::vector &strings, AssignTo &output) { + + if(strings.size() > 1 || (!strings.empty() && !(strings.front().empty()))) { + ConvertTo val; + auto retval = lexical_conversion(strings, val); + output = AssignTo{val}; + return retval; + } + output = AssignTo{}; + return true; +} + +/// function template for converting tuples if the static Index is greater than the tuple size +template +inline typename std::enable_if<(I >= type_count_base::value), bool>::type +tuple_conversion(const std::vector &, AssignTo &) { + return true; +} + +/// Conversion of a tuple element where the type size ==1 and not a mutable container +template +inline typename std::enable_if::value && type_count::value == 1, bool>::type +tuple_type_conversion(std::vector &strings, AssignTo &output) { + auto retval = lexical_assign(strings[0], output); + strings.erase(strings.begin()); + return retval; +} + +/// Conversion of a tuple element where the type size !=1 but the size is fixed and not a mutable container +template +inline typename std::enable_if::value && (type_count::value > 1) && + type_count::value == type_count_min::value, + bool>::type +tuple_type_conversion(std::vector &strings, AssignTo &output) { + auto retval = lexical_conversion(strings, output); + strings.erase(strings.begin(), strings.begin() + type_count::value); + return retval; +} + +/// Conversion of a tuple element where the type is a mutable container or a type with different min and max type sizes +template +inline typename std::enable_if::value || + type_count::value != type_count_min::value, + bool>::type +tuple_type_conversion(std::vector &strings, AssignTo &output) { + + std::size_t index{subtype_count_min::value}; + const std::size_t mx_count{subtype_count::value}; + const std::size_t mx{(std::max)(mx_count, strings.size())}; + + while(index < mx) { + if(is_separator(strings[index])) { + break; + } + ++index; + } + bool retval = lexical_conversion( + std::vector(strings.begin(), strings.begin() + static_cast(index)), output); + strings.erase(strings.begin(), strings.begin() + static_cast(index) + 1); + return retval; +} + +/// Tuple conversion operation +template +inline typename std::enable_if<(I < type_count_base::value), bool>::type +tuple_conversion(std::vector strings, AssignTo &output) { + bool retval = true; + using ConvertToElement = typename std:: + conditional::value, typename std::tuple_element::type, ConvertTo>::type; + if(!strings.empty()) { + retval = retval && tuple_type_conversion::type, ConvertToElement>( + strings, std::get(output)); + } + retval = retval && tuple_conversion(std::move(strings), output); + return retval; +} + +/// Lexical conversion of a container types with tuple elements of size 2 +template ::value && is_mutable_container::value && + type_count_base::value == 2, + detail::enabler>> +bool lexical_conversion(std::vector strings, AssignTo &output) { + output.clear(); + while(!strings.empty()) { + + typename std::remove_const::type>::type v1; + typename std::tuple_element<1, typename ConvertTo::value_type>::type v2; + bool retval = tuple_type_conversion(strings, v1); + if(!strings.empty()) { + retval = retval && tuple_type_conversion(strings, v2); + } + if(retval) { + output.insert(output.end(), typename AssignTo::value_type{v1, v2}); + } else { + return false; + } + } + return (!output.empty()); +} + +/// lexical conversion of tuples with type count>2 or tuples of types of some element with a type size>=2 +template ::value && is_tuple_like::value && + (type_count_base::value != type_count::value || + type_count::value > 2), + detail::enabler>> +bool lexical_conversion(const std::vector &strings, AssignTo &output) { + static_assert( + !is_tuple_like::value || type_count_base::value == type_count_base::value, + "if the conversion type is defined as a tuple it must be the same size as the type you are converting to"); + return tuple_conversion(strings, output); +} + +/// Lexical conversion of a vector types for everything but tuples of two elements and types of size 1 +template ::value && is_mutable_container::value && + type_count_base::value != 2 && + ((type_count::value > 2) || + (type_count::value > type_count_base::value)), + detail::enabler>> +bool lexical_conversion(const std::vector &strings, AssignTo &output) { + bool retval = true; + output.clear(); + std::vector temp; + std::size_t ii{0}; + std::size_t icount{0}; + std::size_t xcm{type_count::value}; + auto ii_max = strings.size(); + while(ii < ii_max) { + temp.push_back(strings[ii]); + ++ii; + ++icount; + if(icount == xcm || is_separator(temp.back()) || ii == ii_max) { + if(static_cast(xcm) > type_count_min::value && is_separator(temp.back())) { + temp.pop_back(); + } + typename AssignTo::value_type temp_out; + retval = retval && + lexical_conversion(temp, temp_out); + temp.clear(); + if(!retval) { + return false; + } + output.insert(output.end(), std::move(temp_out)); + icount = 0; + } + } + return retval; +} + +/// conversion for wrapper types +template ::value == object_category::wrapper_value && + std::is_assignable::value, + detail::enabler> = detail::dummy> +bool lexical_conversion(const std::vector &strings, AssignTo &output) { + if(strings.empty() || strings.front().empty()) { + output = ConvertTo{}; + return true; + } + typename ConvertTo::value_type val; + if(lexical_conversion(strings, val)) { + output = ConvertTo{val}; + return true; + } + return false; +} + +/// conversion for wrapper types +template ::value == object_category::wrapper_value && + !std::is_assignable::value, + detail::enabler> = detail::dummy> +bool lexical_conversion(const std::vector &strings, AssignTo &output) { + using ConvertType = typename ConvertTo::value_type; + if(strings.empty() || strings.front().empty()) { + output = ConvertType{}; + return true; + } + ConvertType val; + if(lexical_conversion(strings, val)) { + output = val; + return true; + } + return false; +} + +/// Sum a vector of flag representations +/// The flag vector produces a series of strings in a vector, simple true is represented by a "1", simple false is +/// by +/// "-1" an if numbers are passed by some fashion they are captured as well so the function just checks for the most +/// common true and false strings then uses stoll to convert the rest for summing +template ::value, detail::enabler> = detail::dummy> +void sum_flag_vector(const std::vector &flags, T &output) { + std::int64_t count{0}; + for(auto &flag : flags) { + count += detail::to_flag_value(flag); + } + output = (count > 0) ? static_cast(count) : T{0}; +} + +/// Sum a vector of flag representations +/// The flag vector produces a series of strings in a vector, simple true is represented by a "1", simple false is +/// by +/// "-1" an if numbers are passed by some fashion they are captured as well so the function just checks for the most +/// common true and false strings then uses stoll to convert the rest for summing +template ::value, detail::enabler> = detail::dummy> +void sum_flag_vector(const std::vector &flags, T &output) { + std::int64_t count{0}; + for(auto &flag : flags) { + count += detail::to_flag_value(flag); + } + output = static_cast(count); +} + +#ifdef _MSC_VER +#pragma warning(push) +#pragma warning(disable : 4800) +#endif +// with Atomic this could produce a warning due to the conversion but if atomic gets here it is an old style so will +// most likely still work + +/// Sum a vector of flag representations +/// The flag vector produces a series of strings in a vector, simple true is represented by a "1", simple false is +/// by +/// "-1" an if numbers are passed by some fashion they are captured as well so the function just checks for the most +/// common true and false strings then uses stoll to convert the rest for summing +template ::value && !std::is_unsigned::value, detail::enabler> = detail::dummy> +void sum_flag_vector(const std::vector &flags, T &output) { + std::int64_t count{0}; + for(auto &flag : flags) { + count += detail::to_flag_value(flag); + } + std::string out = detail::to_string(count); + lexical_cast(out, output); +} + +#ifdef _MSC_VER +#pragma warning(pop) +#endif + +} // namespace detail + + + +namespace detail { + +// Returns false if not a short option. Otherwise, sets opt name and rest and returns true +inline bool split_short(const std::string ¤t, std::string &name, std::string &rest) { + if(current.size() > 1 && current[0] == '-' && valid_first_char(current[1])) { + name = current.substr(1, 1); + rest = current.substr(2); + return true; + } + return false; +} + +// Returns false if not a long option. Otherwise, sets opt name and other side of = and returns true +inline bool split_long(const std::string ¤t, std::string &name, std::string &value) { + if(current.size() > 2 && current.substr(0, 2) == "--" && valid_first_char(current[2])) { + auto loc = current.find_first_of('='); + if(loc != std::string::npos) { + name = current.substr(2, loc - 2); + value = current.substr(loc + 1); + } else { + name = current.substr(2); + value = ""; + } + return true; + } + return false; +} + +// Returns false if not a windows style option. Otherwise, sets opt name and value and returns true +inline bool split_windows_style(const std::string ¤t, std::string &name, std::string &value) { + if(current.size() > 1 && current[0] == '/' && valid_first_char(current[1])) { + auto loc = current.find_first_of(':'); + if(loc != std::string::npos) { + name = current.substr(1, loc - 1); + value = current.substr(loc + 1); + } else { + name = current.substr(1); + value = ""; + } + return true; + } + return false; +} + +// Splits a string into multiple long and short names +inline std::vector split_names(std::string current) { + std::vector output; + std::size_t val; + while((val = current.find(",")) != std::string::npos) { + output.push_back(trim_copy(current.substr(0, val))); + current = current.substr(val + 1); + } + output.push_back(trim_copy(current)); + return output; +} + +/// extract default flag values either {def} or starting with a ! +inline std::vector> get_default_flag_values(const std::string &str) { + std::vector flags = split_names(str); + flags.erase(std::remove_if(flags.begin(), + flags.end(), + [](const std::string &name) { + return ((name.empty()) || (!(((name.find_first_of('{') != std::string::npos) && + (name.back() == '}')) || + (name[0] == '!')))); + }), + flags.end()); + std::vector> output; + output.reserve(flags.size()); + for(auto &flag : flags) { + auto def_start = flag.find_first_of('{'); + std::string defval = "false"; + if((def_start != std::string::npos) && (flag.back() == '}')) { + defval = flag.substr(def_start + 1); + defval.pop_back(); + flag.erase(def_start, std::string::npos); + } + flag.erase(0, flag.find_first_not_of("-!")); + output.emplace_back(flag, defval); + } + return output; +} + +/// Get a vector of short names, one of long names, and a single name +inline std::tuple, std::vector, std::string> +get_names(const std::vector &input) { + + std::vector short_names; + std::vector long_names; + std::string pos_name; + + for(std::string name : input) { + if(name.length() == 0) { + continue; + } + if(name.length() > 1 && name[0] == '-' && name[1] != '-') { + if(name.length() == 2 && valid_first_char(name[1])) + short_names.emplace_back(1, name[1]); + else + throw BadNameString::OneCharName(name); + } else if(name.length() > 2 && name.substr(0, 2) == "--") { + name = name.substr(2); + if(valid_name_string(name)) + long_names.push_back(name); + else + throw BadNameString::BadLongName(name); + } else if(name == "-" || name == "--") { + throw BadNameString::DashesOnly(name); + } else { + if(pos_name.length() > 0) + throw BadNameString::MultiPositionalNames(name); + pos_name = name; + } + } + + return std::tuple, std::vector, std::string>( + short_names, long_names, pos_name); +} + +} // namespace detail + + + +class App; + +/// Holds values to load into Options +struct ConfigItem { + /// This is the list of parents + std::vector parents{}; + + /// This is the name + std::string name{}; + + /// Listing of inputs + std::vector inputs{}; + + /// The list of parents and name joined by "." + std::string fullname() const { + std::vector tmp = parents; + tmp.emplace_back(name); + return detail::join(tmp, "."); + } +}; + +/// This class provides a converter for configuration files. +class Config { + protected: + std::vector items{}; + + public: + /// Convert an app into a configuration + virtual std::string to_config(const App *, bool, bool, std::string) const = 0; + + /// Convert a configuration into an app + virtual std::vector from_config(std::istream &) const = 0; + + /// Get a flag value + virtual std::string to_flag(const ConfigItem &item) const { + if(item.inputs.size() == 1) { + return item.inputs.at(0); + } + throw ConversionError::TooManyInputsFlag(item.fullname()); + } + + /// Parse a config file, throw an error (ParseError:ConfigParseError or FileError) on failure + std::vector from_file(const std::string &name) { + std::ifstream input{name}; + if(!input.good()) + throw FileError::Missing(name); + + return from_config(input); + } + + /// Virtual destructor + virtual ~Config() = default; +}; + +/// This converter works with INI/TOML files; to write INI files use ConfigINI +class ConfigBase : public Config { + protected: + /// the character used for comments + char commentChar = '#'; + /// the character used to start an array '\0' is a default to not use + char arrayStart = '['; + /// the character used to end an array '\0' is a default to not use + char arrayEnd = ']'; + /// the character used to separate elements in an array + char arraySeparator = ','; + /// the character used separate the name from the value + char valueDelimiter = '='; + /// the character to use around strings + char stringQuote = '"'; + /// the character to use around single characters + char characterQuote = '\''; + + public: + std::string + to_config(const App * /*app*/, bool default_also, bool write_description, std::string prefix) const override; + + std::vector from_config(std::istream &input) const override; + /// Specify the configuration for comment characters + ConfigBase *comment(char cchar) { + commentChar = cchar; + return this; + } + /// Specify the start and end characters for an array + ConfigBase *arrayBounds(char aStart, char aEnd) { + arrayStart = aStart; + arrayEnd = aEnd; + return this; + } + /// Specify the delimiter character for an array + ConfigBase *arrayDelimiter(char aSep) { + arraySeparator = aSep; + return this; + } + /// Specify the delimiter between a name and value + ConfigBase *valueSeparator(char vSep) { + valueDelimiter = vSep; + return this; + } + /// Specify the quote characters used around strings and characters + ConfigBase *quoteCharacter(char qString, char qChar) { + stringQuote = qString; + characterQuote = qChar; + return this; + } +}; + +/// the default Config is the TOML file format +using ConfigTOML = ConfigBase; + +/// ConfigINI generates a "standard" INI compliant output +class ConfigINI : public ConfigTOML { + + public: + ConfigINI() { + commentChar = ';'; + arrayStart = '\0'; + arrayEnd = '\0'; + arraySeparator = ' '; + valueDelimiter = '='; + } +}; + + + +class Option; + +/// @defgroup validator_group Validators + +/// @brief Some validators that are provided +/// +/// These are simple `std::string(const std::string&)` validators that are useful. They return +/// a string if the validation fails. A custom struct is provided, as well, with the same user +/// semantics, but with the ability to provide a new type name. +/// @{ + +/// +class Validator { + protected: + /// This is the description function, if empty the description_ will be used + std::function desc_function_{[]() { return std::string{}; }}; + + /// This is the base function that is to be called. + /// Returns a string error message if validation fails. + std::function func_{[](std::string &) { return std::string{}; }}; + /// The name for search purposes of the Validator + std::string name_{}; + /// A Validator will only apply to an indexed value (-1 is all elements) + int application_index_ = -1; + /// Enable for Validator to allow it to be disabled if need be + bool active_{true}; + /// specify that a validator should not modify the input + bool non_modifying_{false}; + + public: + Validator() = default; + /// Construct a Validator with just the description string + explicit Validator(std::string validator_desc) : desc_function_([validator_desc]() { return validator_desc; }) {} + /// Construct Validator from basic information + Validator(std::function op, std::string validator_desc, std::string validator_name = "") + : desc_function_([validator_desc]() { return validator_desc; }), func_(std::move(op)), + name_(std::move(validator_name)) {} + /// Set the Validator operation function + Validator &operation(std::function op) { + func_ = std::move(op); + return *this; + } + /// This is the required operator for a Validator - provided to help + /// users (CLI11 uses the member `func` directly) + std::string operator()(std::string &str) const { + std::string retstring; + if(active_) { + if(non_modifying_) { + std::string value = str; + retstring = func_(value); + } else { + retstring = func_(str); + } + } + return retstring; + } + + /// This is the required operator for a Validator - provided to help + /// users (CLI11 uses the member `func` directly) + std::string operator()(const std::string &str) const { + std::string value = str; + return (active_) ? func_(value) : std::string{}; + } + + /// Specify the type string + Validator &description(std::string validator_desc) { + desc_function_ = [validator_desc]() { return validator_desc; }; + return *this; + } + /// Specify the type string + Validator description(std::string validator_desc) const { + Validator newval(*this); + newval.desc_function_ = [validator_desc]() { return validator_desc; }; + return newval; + } + /// Generate type description information for the Validator + std::string get_description() const { + if(active_) { + return desc_function_(); + } + return std::string{}; + } + /// Specify the type string + Validator &name(std::string validator_name) { + name_ = std::move(validator_name); + return *this; + } + /// Specify the type string + Validator name(std::string validator_name) const { + Validator newval(*this); + newval.name_ = std::move(validator_name); + return newval; + } + /// Get the name of the Validator + const std::string &get_name() const { return name_; } + /// Specify whether the Validator is active or not + Validator &active(bool active_val = true) { + active_ = active_val; + return *this; + } + /// Specify whether the Validator is active or not + Validator active(bool active_val = true) const { + Validator newval(*this); + newval.active_ = active_val; + return newval; + } + + /// Specify whether the Validator can be modifying or not + Validator &non_modifying(bool no_modify = true) { + non_modifying_ = no_modify; + return *this; + } + /// Specify the application index of a validator + Validator &application_index(int app_index) { + application_index_ = app_index; + return *this; + } + /// Specify the application index of a validator + Validator application_index(int app_index) const { + Validator newval(*this); + newval.application_index_ = app_index; + return newval; + } + /// Get the current value of the application index + int get_application_index() const { return application_index_; } + /// Get a boolean if the validator is active + bool get_active() const { return active_; } + + /// Get a boolean if the validator is allowed to modify the input returns true if it can modify the input + bool get_modifying() const { return !non_modifying_; } + + /// Combining validators is a new validator. Type comes from left validator if function, otherwise only set if the + /// same. + Validator operator&(const Validator &other) const { + Validator newval; + + newval._merge_description(*this, other, " AND "); + + // Give references (will make a copy in lambda function) + const std::function &f1 = func_; + const std::function &f2 = other.func_; + + newval.func_ = [f1, f2](std::string &input) { + std::string s1 = f1(input); + std::string s2 = f2(input); + if(!s1.empty() && !s2.empty()) + return std::string("(") + s1 + ") AND (" + s2 + ")"; + else + return s1 + s2; + }; + + newval.active_ = (active_ & other.active_); + newval.application_index_ = application_index_; + return newval; + } + + /// Combining validators is a new validator. Type comes from left validator if function, otherwise only set if the + /// same. + Validator operator|(const Validator &other) const { + Validator newval; + + newval._merge_description(*this, other, " OR "); + + // Give references (will make a copy in lambda function) + const std::function &f1 = func_; + const std::function &f2 = other.func_; + + newval.func_ = [f1, f2](std::string &input) { + std::string s1 = f1(input); + std::string s2 = f2(input); + if(s1.empty() || s2.empty()) + return std::string(); + + return std::string("(") + s1 + ") OR (" + s2 + ")"; + }; + newval.active_ = (active_ & other.active_); + newval.application_index_ = application_index_; + return newval; + } + + /// Create a validator that fails when a given validator succeeds + Validator operator!() const { + Validator newval; + const std::function &dfunc1 = desc_function_; + newval.desc_function_ = [dfunc1]() { + auto str = dfunc1(); + return (!str.empty()) ? std::string("NOT ") + str : std::string{}; + }; + // Give references (will make a copy in lambda function) + const std::function &f1 = func_; + + newval.func_ = [f1, dfunc1](std::string &test) -> std::string { + std::string s1 = f1(test); + if(s1.empty()) { + return std::string("check ") + dfunc1() + " succeeded improperly"; + } + return std::string{}; + }; + newval.active_ = active_; + newval.application_index_ = application_index_; + return newval; + } + + private: + void _merge_description(const Validator &val1, const Validator &val2, const std::string &merger) { + + const std::function &dfunc1 = val1.desc_function_; + const std::function &dfunc2 = val2.desc_function_; + + desc_function_ = [=]() { + std::string f1 = dfunc1(); + std::string f2 = dfunc2(); + if((f1.empty()) || (f2.empty())) { + return f1 + f2; + } + return std::string(1, '(') + f1 + ')' + merger + '(' + f2 + ')'; + }; + } +}; // namespace CLI + +/// Class wrapping some of the accessors of Validator +class CustomValidator : public Validator { + public: +}; +// The implementation of the built in validators is using the Validator class; +// the user is only expected to use the const (static) versions (since there's no setup). +// Therefore, this is in detail. +namespace detail { + +/// CLI enumeration of different file types +enum class path_type { nonexistent, file, directory }; + +#if defined CLI11_HAS_FILESYSTEM && CLI11_HAS_FILESYSTEM > 0 +/// get the type of the path from a file name +inline path_type check_path(const char *file) noexcept { + std::error_code ec; + auto stat = std::filesystem::status(file, ec); + if(ec) { + return path_type::nonexistent; + } + switch(stat.type()) { + case std::filesystem::file_type::none: + case std::filesystem::file_type::not_found: + return path_type::nonexistent; + case std::filesystem::file_type::directory: + return path_type::directory; + case std::filesystem::file_type::symlink: + case std::filesystem::file_type::block: + case std::filesystem::file_type::character: + case std::filesystem::file_type::fifo: + case std::filesystem::file_type::socket: + case std::filesystem::file_type::regular: + case std::filesystem::file_type::unknown: + default: + return path_type::file; + } +} +#else +/// get the type of the path from a file name +inline path_type check_path(const char *file) noexcept { +#if defined(_MSC_VER) + struct __stat64 buffer; + if(_stat64(file, &buffer) == 0) { + return ((buffer.st_mode & S_IFDIR) != 0) ? path_type::directory : path_type::file; + } +#else + struct stat buffer; + if(stat(file, &buffer) == 0) { + return ((buffer.st_mode & S_IFDIR) != 0) ? path_type::directory : path_type::file; + } +#endif + return path_type::nonexistent; +} +#endif +/// Check for an existing file (returns error message if check fails) +class ExistingFileValidator : public Validator { + public: + ExistingFileValidator() : Validator("FILE") { + func_ = [](std::string &filename) { + auto path_result = check_path(filename.c_str()); + if(path_result == path_type::nonexistent) { + return "File does not exist: " + filename; + } + if(path_result == path_type::directory) { + return "File is actually a directory: " + filename; + } + return std::string(); + }; + } +}; + +/// Check for an existing directory (returns error message if check fails) +class ExistingDirectoryValidator : public Validator { + public: + ExistingDirectoryValidator() : Validator("DIR") { + func_ = [](std::string &filename) { + auto path_result = check_path(filename.c_str()); + if(path_result == path_type::nonexistent) { + return "Directory does not exist: " + filename; + } + if(path_result == path_type::file) { + return "Directory is actually a file: " + filename; + } + return std::string(); + }; + } +}; + +/// Check for an existing path +class ExistingPathValidator : public Validator { + public: + ExistingPathValidator() : Validator("PATH(existing)") { + func_ = [](std::string &filename) { + auto path_result = check_path(filename.c_str()); + if(path_result == path_type::nonexistent) { + return "Path does not exist: " + filename; + } + return std::string(); + }; + } +}; + +/// Check for an non-existing path +class NonexistentPathValidator : public Validator { + public: + NonexistentPathValidator() : Validator("PATH(non-existing)") { + func_ = [](std::string &filename) { + auto path_result = check_path(filename.c_str()); + if(path_result != path_type::nonexistent) { + return "Path already exists: " + filename; + } + return std::string(); + }; + } +}; + +/// Validate the given string is a legal ipv4 address +class IPV4Validator : public Validator { + public: + IPV4Validator() : Validator("IPV4") { + func_ = [](std::string &ip_addr) { + auto result = CLI::detail::split(ip_addr, '.'); + if(result.size() != 4) { + return std::string("Invalid IPV4 address must have four parts (") + ip_addr + ')'; + } + int num; + for(const auto &var : result) { + bool retval = detail::lexical_cast(var, num); + if(!retval) { + return std::string("Failed parsing number (") + var + ')'; + } + if(num < 0 || num > 255) { + return std::string("Each IP number must be between 0 and 255 ") + var; + } + } + return std::string(); + }; + } +}; + +} // namespace detail + +// Static is not needed here, because global const implies static. + +/// Check for existing file (returns error message if check fails) +const detail::ExistingFileValidator ExistingFile; + +/// Check for an existing directory (returns error message if check fails) +const detail::ExistingDirectoryValidator ExistingDirectory; + +/// Check for an existing path +const detail::ExistingPathValidator ExistingPath; + +/// Check for an non-existing path +const detail::NonexistentPathValidator NonexistentPath; + +/// Check for an IP4 address +const detail::IPV4Validator ValidIPV4; + +/// Validate the input as a particular type +template class TypeValidator : public Validator { + public: + explicit TypeValidator(const std::string &validator_name) : Validator(validator_name) { + func_ = [](std::string &input_string) { + auto val = DesiredType(); + if(!detail::lexical_cast(input_string, val)) { + return std::string("Failed parsing ") + input_string + " as a " + detail::type_name(); + } + return std::string(); + }; + } + TypeValidator() : TypeValidator(detail::type_name()) {} +}; + +/// Check for a number +const TypeValidator Number("NUMBER"); + +/// Produce a range (factory). Min and max are inclusive. +class Range : public Validator { + public: + /// This produces a range with min and max inclusive. + /// + /// Note that the constructor is templated, but the struct is not, so C++17 is not + /// needed to provide nice syntax for Range(a,b). + template + Range(T min, T max, const std::string &validator_name = std::string{}) : Validator(validator_name) { + if(validator_name.empty()) { + std::stringstream out; + out << detail::type_name() << " in [" << min << " - " << max << "]"; + description(out.str()); + } + + func_ = [min, max](std::string &input) { + T val; + bool converted = detail::lexical_cast(input, val); + if((!converted) || (val < min || val > max)) + return std::string("Value ") + input + " not in range " + std::to_string(min) + " to " + + std::to_string(max); + + return std::string(); + }; + } + + /// Range of one value is 0 to value + template + explicit Range(T max, const std::string &validator_name = std::string{}) + : Range(static_cast(0), max, validator_name) {} +}; + +/// Check for a non negative number +const Range NonNegativeNumber(std::numeric_limits::max(), "NONNEGATIVE"); + +/// Check for a positive valued number (val>0.0), min() her is the smallest positive number +const Range PositiveNumber(std::numeric_limits::min(), std::numeric_limits::max(), "POSITIVE"); + +/// Produce a bounded range (factory). Min and max are inclusive. +class Bound : public Validator { + public: + /// This bounds a value with min and max inclusive. + /// + /// Note that the constructor is templated, but the struct is not, so C++17 is not + /// needed to provide nice syntax for Range(a,b). + template Bound(T min, T max) { + std::stringstream out; + out << detail::type_name() << " bounded to [" << min << " - " << max << "]"; + description(out.str()); + + func_ = [min, max](std::string &input) { + T val; + bool converted = detail::lexical_cast(input, val); + if(!converted) { + return std::string("Value ") + input + " could not be converted"; + } + if(val < min) + input = detail::to_string(min); + else if(val > max) + input = detail::to_string(max); + + return std::string{}; + }; + } + + /// Range of one value is 0 to value + template explicit Bound(T max) : Bound(static_cast(0), max) {} +}; + +namespace detail { +template ::type>::value, detail::enabler> = detail::dummy> +auto smart_deref(T value) -> decltype(*value) { + return *value; +} + +template < + typename T, + enable_if_t::type>::value, detail::enabler> = detail::dummy> +typename std::remove_reference::type &smart_deref(T &value) { + return value; +} +/// Generate a string representation of a set +template std::string generate_set(const T &set) { + using element_t = typename detail::element_type::type; + using iteration_type_t = typename detail::pair_adaptor::value_type; // the type of the object pair + std::string out(1, '{'); + out.append(detail::join( + detail::smart_deref(set), + [](const iteration_type_t &v) { return detail::pair_adaptor::first(v); }, + ",")); + out.push_back('}'); + return out; +} + +/// Generate a string representation of a map +template std::string generate_map(const T &map, bool key_only = false) { + using element_t = typename detail::element_type::type; + using iteration_type_t = typename detail::pair_adaptor::value_type; // the type of the object pair + std::string out(1, '{'); + out.append(detail::join( + detail::smart_deref(map), + [key_only](const iteration_type_t &v) { + std::string res{detail::to_string(detail::pair_adaptor::first(v))}; + + if(!key_only) { + res.append("->"); + res += detail::to_string(detail::pair_adaptor::second(v)); + } + return res; + }, + ",")); + out.push_back('}'); + return out; +} + +template struct has_find { + template + static auto test(int) -> decltype(std::declval().find(std::declval()), std::true_type()); + template static auto test(...) -> decltype(std::false_type()); + + static const auto value = decltype(test(0))::value; + using type = std::integral_constant; +}; + +/// A search function +template ::value, detail::enabler> = detail::dummy> +auto search(const T &set, const V &val) -> std::pair { + using element_t = typename detail::element_type::type; + auto &setref = detail::smart_deref(set); + auto it = std::find_if(std::begin(setref), std::end(setref), [&val](decltype(*std::begin(setref)) v) { + return (detail::pair_adaptor::first(v) == val); + }); + return {(it != std::end(setref)), it}; +} + +/// A search function that uses the built in find function +template ::value, detail::enabler> = detail::dummy> +auto search(const T &set, const V &val) -> std::pair { + auto &setref = detail::smart_deref(set); + auto it = setref.find(val); + return {(it != std::end(setref)), it}; +} + +/// A search function with a filter function +template +auto search(const T &set, const V &val, const std::function &filter_function) + -> std::pair { + using element_t = typename detail::element_type::type; + // do the potentially faster first search + auto res = search(set, val); + if((res.first) || (!(filter_function))) { + return res; + } + // if we haven't found it do the longer linear search with all the element translations + auto &setref = detail::smart_deref(set); + auto it = std::find_if(std::begin(setref), std::end(setref), [&](decltype(*std::begin(setref)) v) { + V a{detail::pair_adaptor::first(v)}; + a = filter_function(a); + return (a == val); + }); + return {(it != std::end(setref)), it}; +} + +// the following suggestion was made by Nikita Ofitserov(@himikof) +// done in templates to prevent compiler warnings on negation of unsigned numbers + +/// Do a check for overflow on signed numbers +template +inline typename std::enable_if::value, T>::type overflowCheck(const T &a, const T &b) { + if((a > 0) == (b > 0)) { + return ((std::numeric_limits::max)() / (std::abs)(a) < (std::abs)(b)); + } else { + return ((std::numeric_limits::min)() / (std::abs)(a) > -(std::abs)(b)); + } +} +/// Do a check for overflow on unsigned numbers +template +inline typename std::enable_if::value, T>::type overflowCheck(const T &a, const T &b) { + return ((std::numeric_limits::max)() / a < b); +} + +/// Performs a *= b; if it doesn't cause integer overflow. Returns false otherwise. +template typename std::enable_if::value, bool>::type checked_multiply(T &a, T b) { + if(a == 0 || b == 0 || a == 1 || b == 1) { + a *= b; + return true; + } + if(a == (std::numeric_limits::min)() || b == (std::numeric_limits::min)()) { + return false; + } + if(overflowCheck(a, b)) { + return false; + } + a *= b; + return true; +} + +/// Performs a *= b; if it doesn't equal infinity. Returns false otherwise. +template +typename std::enable_if::value, bool>::type checked_multiply(T &a, T b) { + T c = a * b; + if(std::isinf(c) && !std::isinf(a) && !std::isinf(b)) { + return false; + } + a = c; + return true; +} + +} // namespace detail +/// Verify items are in a set +class IsMember : public Validator { + public: + using filter_fn_t = std::function; + + /// This allows in-place construction using an initializer list + template + IsMember(std::initializer_list values, Args &&... args) + : IsMember(std::vector(values), std::forward(args)...) {} + + /// This checks to see if an item is in a set (empty function) + template explicit IsMember(T &&set) : IsMember(std::forward(set), nullptr) {} + + /// This checks to see if an item is in a set: pointer or copy version. You can pass in a function that will filter + /// both sides of the comparison before computing the comparison. + template explicit IsMember(T set, F filter_function) { + + // Get the type of the contained item - requires a container have ::value_type + // if the type does not have first_type and second_type, these are both value_type + using element_t = typename detail::element_type::type; // Removes (smart) pointers if needed + using item_t = typename detail::pair_adaptor::first_type; // Is value_type if not a map + + using local_item_t = typename IsMemberType::type; // This will convert bad types to good ones + // (const char * to std::string) + + // Make a local copy of the filter function, using a std::function if not one already + std::function filter_fn = filter_function; + + // This is the type name for help, it will take the current version of the set contents + desc_function_ = [set]() { return detail::generate_set(detail::smart_deref(set)); }; + + // This is the function that validates + // It stores a copy of the set pointer-like, so shared_ptr will stay alive + func_ = [set, filter_fn](std::string &input) { + local_item_t b; + if(!detail::lexical_cast(input, b)) { + throw ValidationError(input); // name is added later + } + if(filter_fn) { + b = filter_fn(b); + } + auto res = detail::search(set, b, filter_fn); + if(res.first) { + // Make sure the version in the input string is identical to the one in the set + if(filter_fn) { + input = detail::value_string(detail::pair_adaptor::first(*(res.second))); + } + + // Return empty error string (success) + return std::string{}; + } + + // If you reach this point, the result was not found + return input + " not in " + detail::generate_set(detail::smart_deref(set)); + }; + } + + /// You can pass in as many filter functions as you like, they nest (string only currently) + template + IsMember(T &&set, filter_fn_t filter_fn_1, filter_fn_t filter_fn_2, Args &&... other) + : IsMember( + std::forward(set), + [filter_fn_1, filter_fn_2](std::string a) { return filter_fn_2(filter_fn_1(a)); }, + other...) {} +}; + +/// definition of the default transformation object +template using TransformPairs = std::vector>; + +/// Translate named items to other or a value set +class Transformer : public Validator { + public: + using filter_fn_t = std::function; + + /// This allows in-place construction + template + Transformer(std::initializer_list> values, Args &&... args) + : Transformer(TransformPairs(values), std::forward(args)...) {} + + /// direct map of std::string to std::string + template explicit Transformer(T &&mapping) : Transformer(std::forward(mapping), nullptr) {} + + /// This checks to see if an item is in a set: pointer or copy version. You can pass in a function that will filter + /// both sides of the comparison before computing the comparison. + template explicit Transformer(T mapping, F filter_function) { + + static_assert(detail::pair_adaptor::type>::value, + "mapping must produce value pairs"); + // Get the type of the contained item - requires a container have ::value_type + // if the type does not have first_type and second_type, these are both value_type + using element_t = typename detail::element_type::type; // Removes (smart) pointers if needed + using item_t = typename detail::pair_adaptor::first_type; // Is value_type if not a map + using local_item_t = typename IsMemberType::type; // Will convert bad types to good ones + // (const char * to std::string) + + // Make a local copy of the filter function, using a std::function if not one already + std::function filter_fn = filter_function; + + // This is the type name for help, it will take the current version of the set contents + desc_function_ = [mapping]() { return detail::generate_map(detail::smart_deref(mapping)); }; + + func_ = [mapping, filter_fn](std::string &input) { + local_item_t b; + if(!detail::lexical_cast(input, b)) { + return std::string(); + // there is no possible way we can match anything in the mapping if we can't convert so just return + } + if(filter_fn) { + b = filter_fn(b); + } + auto res = detail::search(mapping, b, filter_fn); + if(res.first) { + input = detail::value_string(detail::pair_adaptor::second(*res.second)); + } + return std::string{}; + }; + } + + /// You can pass in as many filter functions as you like, they nest + template + Transformer(T &&mapping, filter_fn_t filter_fn_1, filter_fn_t filter_fn_2, Args &&... other) + : Transformer( + std::forward(mapping), + [filter_fn_1, filter_fn_2](std::string a) { return filter_fn_2(filter_fn_1(a)); }, + other...) {} +}; + +/// translate named items to other or a value set +class CheckedTransformer : public Validator { + public: + using filter_fn_t = std::function; + + /// This allows in-place construction + template + CheckedTransformer(std::initializer_list> values, Args &&... args) + : CheckedTransformer(TransformPairs(values), std::forward(args)...) {} + + /// direct map of std::string to std::string + template explicit CheckedTransformer(T mapping) : CheckedTransformer(std::move(mapping), nullptr) {} + + /// This checks to see if an item is in a set: pointer or copy version. You can pass in a function that will filter + /// both sides of the comparison before computing the comparison. + template explicit CheckedTransformer(T mapping, F filter_function) { + + static_assert(detail::pair_adaptor::type>::value, + "mapping must produce value pairs"); + // Get the type of the contained item - requires a container have ::value_type + // if the type does not have first_type and second_type, these are both value_type + using element_t = typename detail::element_type::type; // Removes (smart) pointers if needed + using item_t = typename detail::pair_adaptor::first_type; // Is value_type if not a map + using local_item_t = typename IsMemberType::type; // Will convert bad types to good ones + // (const char * to std::string) + using iteration_type_t = typename detail::pair_adaptor::value_type; // the type of the object pair + + // Make a local copy of the filter function, using a std::function if not one already + std::function filter_fn = filter_function; + + auto tfunc = [mapping]() { + std::string out("value in "); + out += detail::generate_map(detail::smart_deref(mapping)) + " OR {"; + out += detail::join( + detail::smart_deref(mapping), + [](const iteration_type_t &v) { return detail::to_string(detail::pair_adaptor::second(v)); }, + ","); + out.push_back('}'); + return out; + }; + + desc_function_ = tfunc; + + func_ = [mapping, tfunc, filter_fn](std::string &input) { + local_item_t b; + bool converted = detail::lexical_cast(input, b); + if(converted) { + if(filter_fn) { + b = filter_fn(b); + } + auto res = detail::search(mapping, b, filter_fn); + if(res.first) { + input = detail::value_string(detail::pair_adaptor::second(*res.second)); + return std::string{}; + } + } + for(const auto &v : detail::smart_deref(mapping)) { + auto output_string = detail::value_string(detail::pair_adaptor::second(v)); + if(output_string == input) { + return std::string(); + } + } + + return "Check " + input + " " + tfunc() + " FAILED"; + }; + } + + /// You can pass in as many filter functions as you like, they nest + template + CheckedTransformer(T &&mapping, filter_fn_t filter_fn_1, filter_fn_t filter_fn_2, Args &&... other) + : CheckedTransformer( + std::forward(mapping), + [filter_fn_1, filter_fn_2](std::string a) { return filter_fn_2(filter_fn_1(a)); }, + other...) {} +}; + +/// Helper function to allow ignore_case to be passed to IsMember or Transform +inline std::string ignore_case(std::string item) { return detail::to_lower(item); } + +/// Helper function to allow ignore_underscore to be passed to IsMember or Transform +inline std::string ignore_underscore(std::string item) { return detail::remove_underscore(item); } + +/// Helper function to allow checks to ignore spaces to be passed to IsMember or Transform +inline std::string ignore_space(std::string item) { + item.erase(std::remove(std::begin(item), std::end(item), ' '), std::end(item)); + item.erase(std::remove(std::begin(item), std::end(item), '\t'), std::end(item)); + return item; +} + +/// Multiply a number by a factor using given mapping. +/// Can be used to write transforms for SIZE or DURATION inputs. +/// +/// Example: +/// With mapping = `{"b"->1, "kb"->1024, "mb"->1024*1024}` +/// one can recognize inputs like "100", "12kb", "100 MB", +/// that will be automatically transformed to 100, 14448, 104857600. +/// +/// Output number type matches the type in the provided mapping. +/// Therefore, if it is required to interpret real inputs like "0.42 s", +/// the mapping should be of a type or . +class AsNumberWithUnit : public Validator { + public: + /// Adjust AsNumberWithUnit behavior. + /// CASE_SENSITIVE/CASE_INSENSITIVE controls how units are matched. + /// UNIT_OPTIONAL/UNIT_REQUIRED throws ValidationError + /// if UNIT_REQUIRED is set and unit literal is not found. + enum Options { + CASE_SENSITIVE = 0, + CASE_INSENSITIVE = 1, + UNIT_OPTIONAL = 0, + UNIT_REQUIRED = 2, + DEFAULT = CASE_INSENSITIVE | UNIT_OPTIONAL + }; + + template + explicit AsNumberWithUnit(std::map mapping, + Options opts = DEFAULT, + const std::string &unit_name = "UNIT") { + description(generate_description(unit_name, opts)); + validate_mapping(mapping, opts); + + // transform function + func_ = [mapping, opts](std::string &input) -> std::string { + Number num; + + detail::rtrim(input); + if(input.empty()) { + throw ValidationError("Input is empty"); + } + + // Find split position between number and prefix + auto unit_begin = input.end(); + while(unit_begin > input.begin() && std::isalpha(*(unit_begin - 1), std::locale())) { + --unit_begin; + } + + std::string unit{unit_begin, input.end()}; + input.resize(static_cast(std::distance(input.begin(), unit_begin))); + detail::trim(input); + + if(opts & UNIT_REQUIRED && unit.empty()) { + throw ValidationError("Missing mandatory unit"); + } + if(opts & CASE_INSENSITIVE) { + unit = detail::to_lower(unit); + } + if(unit.empty()) { + if(!detail::lexical_cast(input, num)) { + throw ValidationError(std::string("Value ") + input + " could not be converted to " + + detail::type_name()); + } + // No need to modify input if no unit passed + return {}; + } + + // find corresponding factor + auto it = mapping.find(unit); + if(it == mapping.end()) { + throw ValidationError(unit + + " unit not recognized. " + "Allowed values: " + + detail::generate_map(mapping, true)); + } + + if(!input.empty()) { + bool converted = detail::lexical_cast(input, num); + if(!converted) { + throw ValidationError(std::string("Value ") + input + " could not be converted to " + + detail::type_name()); + } + // perform safe multiplication + bool ok = detail::checked_multiply(num, it->second); + if(!ok) { + throw ValidationError(detail::to_string(num) + " multiplied by " + unit + + " factor would cause number overflow. Use smaller value."); + } + } else { + num = static_cast(it->second); + } + + input = detail::to_string(num); + + return {}; + }; + } + + private: + /// Check that mapping contains valid units. + /// Update mapping for CASE_INSENSITIVE mode. + template static void validate_mapping(std::map &mapping, Options opts) { + for(auto &kv : mapping) { + if(kv.first.empty()) { + throw ValidationError("Unit must not be empty."); + } + if(!detail::isalpha(kv.first)) { + throw ValidationError("Unit must contain only letters."); + } + } + + // make all units lowercase if CASE_INSENSITIVE + if(opts & CASE_INSENSITIVE) { + std::map lower_mapping; + for(auto &kv : mapping) { + auto s = detail::to_lower(kv.first); + if(lower_mapping.count(s)) { + throw ValidationError(std::string("Several matching lowercase unit representations are found: ") + + s); + } + lower_mapping[detail::to_lower(kv.first)] = kv.second; + } + mapping = std::move(lower_mapping); + } + } + + /// Generate description like this: NUMBER [UNIT] + template static std::string generate_description(const std::string &name, Options opts) { + std::stringstream out; + out << detail::type_name() << ' '; + if(opts & UNIT_REQUIRED) { + out << name; + } else { + out << '[' << name << ']'; + } + return out.str(); + } +}; + +/// Converts a human-readable size string (with unit literal) to uin64_t size. +/// Example: +/// "100" => 100 +/// "1 b" => 100 +/// "10Kb" => 10240 // you can configure this to be interpreted as kilobyte (*1000) or kibibyte (*1024) +/// "10 KB" => 10240 +/// "10 kb" => 10240 +/// "10 kib" => 10240 // *i, *ib are always interpreted as *bibyte (*1024) +/// "10kb" => 10240 +/// "2 MB" => 2097152 +/// "2 EiB" => 2^61 // Units up to exibyte are supported +class AsSizeValue : public AsNumberWithUnit { + public: + using result_t = std::uint64_t; + + /// If kb_is_1000 is true, + /// interpret 'kb', 'k' as 1000 and 'kib', 'ki' as 1024 + /// (same applies to higher order units as well). + /// Otherwise, interpret all literals as factors of 1024. + /// The first option is formally correct, but + /// the second interpretation is more wide-spread + /// (see https://en.wikipedia.org/wiki/Binary_prefix). + explicit AsSizeValue(bool kb_is_1000) : AsNumberWithUnit(get_mapping(kb_is_1000)) { + if(kb_is_1000) { + description("SIZE [b, kb(=1000b), kib(=1024b), ...]"); + } else { + description("SIZE [b, kb(=1024b), ...]"); + } + } + + private: + /// Get mapping + static std::map init_mapping(bool kb_is_1000) { + std::map m; + result_t k_factor = kb_is_1000 ? 1000 : 1024; + result_t ki_factor = 1024; + result_t k = 1; + result_t ki = 1; + m["b"] = 1; + for(std::string p : {"k", "m", "g", "t", "p", "e"}) { + k *= k_factor; + ki *= ki_factor; + m[p] = k; + m[p + "b"] = k; + m[p + "i"] = ki; + m[p + "ib"] = ki; + } + return m; + } + + /// Cache calculated mapping + static std::map get_mapping(bool kb_is_1000) { + if(kb_is_1000) { + static auto m = init_mapping(true); + return m; + } else { + static auto m = init_mapping(false); + return m; + } + } +}; + +namespace detail { +/// Split a string into a program name and command line arguments +/// the string is assumed to contain a file name followed by other arguments +/// the return value contains is a pair with the first argument containing the program name and the second +/// everything else. +inline std::pair split_program_name(std::string commandline) { + // try to determine the programName + std::pair vals; + trim(commandline); + auto esp = commandline.find_first_of(' ', 1); + while(detail::check_path(commandline.substr(0, esp).c_str()) != path_type::file) { + esp = commandline.find_first_of(' ', esp + 1); + if(esp == std::string::npos) { + // if we have reached the end and haven't found a valid file just assume the first argument is the + // program name + if(commandline[0] == '"' || commandline[0] == '\'' || commandline[0] == '`') { + bool embeddedQuote = false; + auto keyChar = commandline[0]; + auto end = commandline.find_first_of(keyChar, 1); + while((end != std::string::npos) && (commandline[end - 1] == '\\')) { // deal with escaped quotes + end = commandline.find_first_of(keyChar, end + 1); + embeddedQuote = true; + } + if(end != std::string::npos) { + vals.first = commandline.substr(1, end - 1); + esp = end + 1; + if(embeddedQuote) { + vals.first = find_and_replace(vals.first, std::string("\\") + keyChar, std::string(1, keyChar)); + embeddedQuote = false; + } + } else { + esp = commandline.find_first_of(' ', 1); + } + } else { + esp = commandline.find_first_of(' ', 1); + } + + break; + } + } + if(vals.first.empty()) { + vals.first = commandline.substr(0, esp); + rtrim(vals.first); + } + + // strip the program name + vals.second = (esp != std::string::npos) ? commandline.substr(esp + 1) : std::string{}; + ltrim(vals.second); + return vals; +} + +} // namespace detail +/// @} + + + + +class Option; +class App; + +/// This enum signifies the type of help requested +/// +/// This is passed in by App; all user classes must accept this as +/// the second argument. + +enum class AppFormatMode { + Normal, ///< The normal, detailed help + All, ///< A fully expanded help + Sub, ///< Used when printed as part of expanded subcommand +}; + +/// This is the minimum requirements to run a formatter. +/// +/// A user can subclass this is if they do not care at all +/// about the structure in CLI::Formatter. +class FormatterBase { + protected: + /// @name Options + ///@{ + + /// The width of the first column + std::size_t column_width_{30}; + + /// @brief The required help printout labels (user changeable) + /// Values are Needs, Excludes, etc. + std::map labels_{}; + + ///@} + /// @name Basic + ///@{ + + public: + FormatterBase() = default; + FormatterBase(const FormatterBase &) = default; + FormatterBase(FormatterBase &&) = default; + + /// Adding a destructor in this form to work around bug in GCC 4.7 + virtual ~FormatterBase() noexcept {} // NOLINT(modernize-use-equals-default) + + /// This is the key method that puts together help + virtual std::string make_help(const App *, std::string, AppFormatMode) const = 0; + + ///@} + /// @name Setters + ///@{ + + /// Set the "REQUIRED" label + void label(std::string key, std::string val) { labels_[key] = val; } + + /// Set the column width + void column_width(std::size_t val) { column_width_ = val; } + + ///@} + /// @name Getters + ///@{ + + /// Get the current value of a name (REQUIRED, etc.) + std::string get_label(std::string key) const { + if(labels_.find(key) == labels_.end()) + return key; + else + return labels_.at(key); + } + + /// Get the current column width + std::size_t get_column_width() const { return column_width_; } + + ///@} +}; + +/// This is a specialty override for lambda functions +class FormatterLambda final : public FormatterBase { + using funct_t = std::function; + + /// The lambda to hold and run + funct_t lambda_; + + public: + /// Create a FormatterLambda with a lambda function + explicit FormatterLambda(funct_t funct) : lambda_(std::move(funct)) {} + + /// Adding a destructor (mostly to make GCC 4.7 happy) + ~FormatterLambda() noexcept override {} // NOLINT(modernize-use-equals-default) + + /// This will simply call the lambda function + std::string make_help(const App *app, std::string name, AppFormatMode mode) const override { + return lambda_(app, name, mode); + } +}; + +/// This is the default Formatter for CLI11. It pretty prints help output, and is broken into quite a few +/// overridable methods, to be highly customizable with minimal effort. +class Formatter : public FormatterBase { + public: + Formatter() = default; + Formatter(const Formatter &) = default; + Formatter(Formatter &&) = default; + + /// @name Overridables + ///@{ + + /// This prints out a group of options with title + /// + virtual std::string make_group(std::string group, bool is_positional, std::vector opts) const; + + /// This prints out just the positionals "group" + virtual std::string make_positionals(const App *app) const; + + /// This prints out all the groups of options + std::string make_groups(const App *app, AppFormatMode mode) const; + + /// This prints out all the subcommands + virtual std::string make_subcommands(const App *app, AppFormatMode mode) const; + + /// This prints out a subcommand + virtual std::string make_subcommand(const App *sub) const; + + /// This prints out a subcommand in help-all + virtual std::string make_expanded(const App *sub) const; + + /// This prints out all the groups of options + virtual std::string make_footer(const App *app) const; + + /// This displays the description line + virtual std::string make_description(const App *app) const; + + /// This displays the usage line + virtual std::string make_usage(const App *app, std::string name) const; + + /// This puts everything together + std::string make_help(const App * /*app*/, std::string, AppFormatMode) const override; + + ///@} + /// @name Options + ///@{ + + /// This prints out an option help line, either positional or optional form + virtual std::string make_option(const Option *opt, bool is_positional) const { + std::stringstream out; + detail::format_help( + out, make_option_name(opt, is_positional) + make_option_opts(opt), make_option_desc(opt), column_width_); + return out.str(); + } + + /// @brief This is the name part of an option, Default: left column + virtual std::string make_option_name(const Option *, bool) const; + + /// @brief This is the options part of the name, Default: combined into left column + virtual std::string make_option_opts(const Option *) const; + + /// @brief This is the description. Default: Right column, on new line if left column too large + virtual std::string make_option_desc(const Option *) const; + + /// @brief This is used to print the name on the USAGE line + virtual std::string make_option_usage(const Option *opt) const; + + ///@} +}; + + + + +using results_t = std::vector; +/// callback function definition +using callback_t = std::function; + +class Option; +class App; + +using Option_p = std::unique_ptr