From be1b75431d6ffab07798fb8e15c4167f74bd9ad3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A4r=20Winzell?= Date: Mon, 17 Dec 2018 16:13:53 -0800 Subject: [PATCH] Switch from CXXOPTS to CLI11. (#148) We want to move to auto-formatting all our code, and it just seemed impossible to make cxxopts usage tidy under clang-format's dominion. While trying to work out its quirks, I realised that CLI11 did everything I wanted much better, and so we've switched. We're also going to chuck the usage of ExternalProject_Add(), at least for the simplest use cases such as single-header include files. We'll just commit them directly; that's kind of the whole point. The one discipline we'll maintain is that commits that involve third_party/ should be as self-contained as possible (without breaking the app). --- .clang-format | 87 + CMakeLists.txt | 15 +- src/FBX2glTF.cpp | 640 +++--- src/FBX2glTF.h | 14 +- src/gltf/Raw2Gltf.cpp | 2 +- third_party/CLI11/CLI11.hpp | 4113 +++++++++++++++++++++++++++++++++++ 6 files changed, 4571 insertions(+), 300 deletions(-) create mode 100644 .clang-format create mode 100644 third_party/CLI11/CLI11.hpp diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..456643f --- /dev/null +++ b/.clang-format @@ -0,0 +1,87 @@ +--- +AccessModifierOffset: -1 +AlignAfterOpenBracket: AlwaysBreak +AlignConsecutiveAssignments: false +AlignConsecutiveDeclarations: false +AlignEscapedNewlinesLeft: true +AlignOperands: false +AlignTrailingComments: false +AllowAllParametersOfDeclarationOnNextLine: false +AllowShortBlocksOnASingleLine: false +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: Empty +AllowShortIfStatementsOnASingleLine: false +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: true +AlwaysBreakTemplateDeclarations: true +BinPackArguments: false +BinPackParameters: false +BraceWrapping: + AfterClass: false + AfterControlStatement: false + AfterEnum: false + AfterFunction: false + AfterNamespace: false + AfterObjCDeclaration: false + AfterStruct: false + AfterUnion: false + BeforeCatch: false + BeforeElse: false + IndentBraces: false +BreakBeforeBinaryOperators: None +BreakBeforeBraces: Attach +BreakBeforeTernaryOperators: true +BreakConstructorInitializersBeforeComma: false +BreakAfterJavaFieldAnnotations: false +BreakStringLiterals: false +ColumnLimit: 80 +CommentPragmas: '^ IWYU pragma:' +ConstructorInitializerAllOnOneLineOrOnePerLine: true +ConstructorInitializerIndentWidth: 4 +ContinuationIndentWidth: 4 +Cpp11BracedListStyle: true +DerivePointerAlignment: false +DisableFormat: false +ForEachMacros: [ FOR_EACH, FOR_EACH_ENUMERATE, FOR_EACH_KV, FOR_EACH_R, FOR_EACH_RANGE, FOR_EACH_RANGE_R, ] +IncludeCategories: + - Regex: '^<.*\.h(pp)?>' + Priority: 1 + - Regex: '^<.*' + Priority: 2 + - Regex: '.*' + Priority: 3 +IndentCaseLabels: true +IndentWidth: 2 +IndentWrappedFunctionNames: false +KeepEmptyLinesAtTheStartOfBlocks: false +MacroBlockBegin: '' +MacroBlockEnd: '' +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: None +ObjCBlockIndentWidth: 2 +ObjCSpaceAfterProperty: false +ObjCSpaceBeforeProtocolList: false +PenaltyBreakBeforeFirstCallParameter: 1 +PenaltyBreakComment: 300 +PenaltyBreakFirstLessLess: 120 +PenaltyBreakString: 1000 +PenaltyExcessCharacter: 1000000 +PenaltyReturnTypeOnItsOwnLine: 200 +PointerAlignment: Left +ReflowComments: true +SortIncludes: true +SpaceAfterCStyleCast: false +SpaceBeforeAssignmentOperators: true +SpaceBeforeParens: ControlStatements +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 1 +SpacesInAngles: false +SpacesInContainerLiterals: true +SpacesInCStyleCastParentheses: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +Standard: Cpp11 +TabWidth: 8 +UseTab: Never +... diff --git a/CMakeLists.txt b/CMakeLists.txt index 60d1efe..2642c31 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -130,17 +130,6 @@ ExternalProject_Add(CPPCodec ) set(CPPCODEC_INCLUDE_DIR "${CMAKE_BINARY_DIR}/cppcodec/src/CPPCodec") -# CXXOPTS -ExternalProject_Add(CxxOpts - GIT_REPOSITORY https://github.com/jarro2783/cxxopts - GIT_TAG v1.4.4 - PREFIX cxxopts - CONFIGURE_COMMAND ${CMAKE_COMMAND} -E echo "Skipping cxxopts configure step." - BUILD_COMMAND ${CMAKE_COMMAND} -E echo "Skipping cxxopts build step." - INSTALL_COMMAND ${CMAKE_COMMAND} -E echo "Skipping cxxopts install step." -) -set(CXXOPTS_INCLUDE_DIR "${CMAKE_BINARY_DIR}/cxxopts/src/CxxOpts/include") - # FMT ExternalProject_Add(Fmt PREFIX fmt @@ -223,6 +212,7 @@ set(LIB_SOURCE_FILES src/utils/Image_Utils.hpp src/utils/String_Utils.cpp src/utils/String_Utils.hpp + third_party/CLI11/CLI11.hpp ) add_library(libFBX2glTF STATIC ${LIB_SOURCE_FILES}) @@ -236,7 +226,6 @@ add_dependencies(libFBX2glTF FiFoMap Json STB - CxxOpts CPPCodec Fmt ) @@ -300,7 +289,7 @@ if (Iconv_FOUND) endif() target_include_directories(appFBX2glTF PUBLIC - ${CXXOPTS_INCLUDE_DIR} + "third_party/CLI11" ) target_link_libraries(appFBX2glTF libFBX2glTF) diff --git a/src/FBX2glTF.cpp b/src/FBX2glTF.cpp index 3b0cbd1..c4ba952 100644 --- a/src/FBX2glTF.cpp +++ b/src/FBX2glTF.cpp @@ -7,310 +7,392 @@ * of patent rights can be found in the PATENTS file in the same directory. */ -#include -#include -#include -#include #include +#include +#include +#include +#include -#if defined( __unix__ ) || defined( __APPLE__ ) +#if defined(__unix__) || defined(__APPLE__) #include #define _stricmp strcasecmp #endif -#include +#include #include "FBX2glTF.h" -#include "utils/String_Utils.hpp" -#include "utils/File_Utils.hpp" #include "fbx/Fbx2Raw.hpp" #include "gltf/Raw2Gltf.hpp" +#include "utils/File_Utils.hpp" +#include "utils/String_Utils.hpp" bool verboseOutput = false; -int main(int argc, char *argv[]) -{ - cxxopts::Options options( - "FBX2glTF", - fmt::sprintf("FBX2glTF %s: Generate a glTF 2.0 representation of an FBX model.", FBX2GLTF_VERSION) - ); +int main(int argc, char* argv[]) { + GltfOptions gltfOptions; - std::string inputPath; - std::string outputPath; + CLI::App app{ + fmt::sprintf( + "FBX2glTF %s: Generate a glTF 2.0 representation of an FBX model.", + FBX2GLTF_VERSION), + "FBX2glTF"}; - std::vector> texturesTransforms; - - GltfOptions gltfOptions; - - options.positional_help("[]"); - options.add_options() - ( - "i,input", "The FBX model to convert.", - cxxopts::value(inputPath)) - ( - "o,output", "Where to generate the output, without suffix.", - cxxopts::value(outputPath)) - ( - "e,embed", "Inline buffers as data:// URIs within generated non-binary glTF.", - cxxopts::value(gltfOptions.embedResources)) - ( - "b,binary", "Output a single binary format .glb file.", - cxxopts::value(gltfOptions.outputBinary)) - ( - "long-indices", "Whether to use 32-bit indices (never|auto|always).", - cxxopts::value>()) - ( - "d,draco", "Apply Draco mesh compression to geometries.", - cxxopts::value(gltfOptions.draco.enabled)) - ( - "draco-compression-level", "The compression level to tune Draco to, from 0 to 10. (default: 7)", - cxxopts::value(gltfOptions.draco.compressionLevel)) - ( - "draco-bits-for-positions", "How many bits to quantize position to. (default: 14)", - cxxopts::value(gltfOptions.draco.quantBitsPosition)) - ( - "draco-bits-for-uv", "How many bits to quantize UV coordinates to. (default: 10)", - cxxopts::value(gltfOptions.draco.quantBitsTexCoord)) - ( - "draco-bits-for-normals", "How many bits to quantize normals to. (default: 10)", - cxxopts::value(gltfOptions.draco.quantBitsNormal)) - ( - "draco-bits-for-colors", "How many bits to quantize color to. (default: 8)", - cxxopts::value(gltfOptions.draco.quantBitsColor)) - ( - "draco-bits-for-other", "How many bits to quantize other vertex attributes to to. (default: 8)", - cxxopts::value(gltfOptions.draco.quantBitsGeneric)) - ( - "compute-normals", "When to compute normals for vertices (never|broken|missing|always).", - cxxopts::value>()) - ("flip-u", "Flip all U texture coordinates.") - ("flip-v", "Flip all V texture coordinates (default behaviour!)") - ("no-flip-v", "Suppress the default flipping of V texture coordinates") - ( - "pbr-metallic-roughness", "Try to glean glTF 2.0 native PBR attributes from the FBX.", - cxxopts::value(gltfOptions.usePBRMetRough)) - ( - "khr-materials-unlit", "Use KHR_materials_unlit extension to specify Unlit shader.", - cxxopts::value(gltfOptions.useKHRMatUnlit)) - ( - "no-khr-punctual-lights", "Don't use KHR_punctual_lights extension to export lights.", - cxxopts::value(gltfOptions.useKHRPunctualLights)) - ( - "user-properties", "Transcribe FBX User Properties into glTF node and material 'extras'.", - cxxopts::value(gltfOptions.enableUserProperties)) - ( - "blend-shape-normals", "Include blend shape normals, if reported present by the FBX SDK.", - cxxopts::value(gltfOptions.useBlendShapeNormals)) - ( - "blend-shape-tangents", "Include blend shape tangents, if reported present by the FBX SDK.", - cxxopts::value(gltfOptions.useBlendShapeTangents)) - ( - "k,keep-attribute", "Used repeatedly to build a limiting set of vertex attributes to keep.", - cxxopts::value>()) - ("v,verbose", "Enable verbose output.") - ("h,help", "Show this help.") - ("V,version", "Display the current program version."); - - try { - options.parse_positional("input"); - options.parse(argc, argv); - - } catch (const cxxopts::OptionException &e) { - fmt::printf(options.help()); - return 1; - } - - if (options.count("version")) { - fmt::printf("FBX2glTF version %s\nCopyright (c) 2016-2018 Oculus VR, LLC.\n", FBX2GLTF_VERSION); - return 0; - } - - if (options.count("help")) { - fmt::printf(options.help()); - return 0; - } - - if (!options.count("input")) { - fmt::printf("You must supply a FBX file to convert.\n"); - fmt::printf(options.help()); - return 1; - } - - if (options.count("verbose")) { - verboseOutput = true; - } - - if (!gltfOptions.useKHRMatUnlit && !gltfOptions.usePBRMetRough) { - if (verboseOutput) { - fmt::printf("Defaulting to --pbr-metallic-roughness material support.\n"); - } - gltfOptions.usePBRMetRough = true; - } - - if (gltfOptions.draco.compressionLevel != -1 && - (gltfOptions.draco.compressionLevel < 1 || gltfOptions.draco.compressionLevel > 10)) { - fmt::printf("Draco compression level must lie in [1, 10].\n"); - return 0; - } - - if (options.count("flip-u") > 0) { - texturesTransforms.emplace_back([](Vec2f uv) { return Vec2f(1.0f - uv[0], uv[1]); }); - } - if (options.count("flip-v") > 0) { - fmt::printf("Note: The --flip-v command switch is now default behaviour.\n"); - } - if (options.count("no-flip-v") == 0) { - texturesTransforms.emplace_back([](Vec2f uv) { return Vec2f(uv[0], 1.0f - uv[1]); }); - } else if (verboseOutput) { - fmt::printf("Suppressing --flip-v transformation of texture coordinates.\n"); - } - - for (const std::string &choice : options["long-indices"].as>()) { - if (choice == "never") { - gltfOptions.useLongIndices = UseLongIndicesOptions::NEVER; - } else if (choice == "auto") { - gltfOptions.useLongIndices = UseLongIndicesOptions::AUTO; - } else if (choice == "always") { - gltfOptions.useLongIndices = UseLongIndicesOptions::ALWAYS; - } else { - fmt::printf("Unknown --long-indices: %s\n", choice); - fmt::printf(options.help()); - return 1; - } - } - - if (options.count("compute-normals") > 0) { - for (const std::string &choice : options["compute-normals"].as>()) { - if (choice == "never") { - gltfOptions.computeNormals = ComputeNormalsOption::NEVER; - } else if (choice == "broken") { - gltfOptions.computeNormals = ComputeNormalsOption::BROKEN; - } else if (choice == "missing") { - gltfOptions.computeNormals = ComputeNormalsOption::MISSING; - } else if (choice == "always") { - gltfOptions.computeNormals = ComputeNormalsOption::ALWAYS; - } else { - fmt::printf("Unknown --compute-normals: %s\n", choice); - fmt::printf(options.help()); - return 1; - } - } - } - - if (options.count("keep-attribute") > 0) { - gltfOptions.keepAttribs = RAW_VERTEX_ATTRIBUTE_JOINT_INDICES | RAW_VERTEX_ATTRIBUTE_JOINT_WEIGHTS; - for (std::string attribute : options["keep-attribute"].as>()) { - if (attribute == "position") { gltfOptions.keepAttribs |= RAW_VERTEX_ATTRIBUTE_POSITION; } - else if (attribute == "normal") { gltfOptions.keepAttribs |= RAW_VERTEX_ATTRIBUTE_NORMAL; } - else if (attribute == "tangent") { gltfOptions.keepAttribs |= RAW_VERTEX_ATTRIBUTE_TANGENT; } - else if (attribute == "binormal") { gltfOptions.keepAttribs |= RAW_VERTEX_ATTRIBUTE_BINORMAL; } - else if (attribute == "color") { gltfOptions.keepAttribs |= RAW_VERTEX_ATTRIBUTE_COLOR; } - else if (attribute == "uv0") { gltfOptions.keepAttribs |= RAW_VERTEX_ATTRIBUTE_UV0; } - else if (attribute == "uv1") { gltfOptions.keepAttribs |= RAW_VERTEX_ATTRIBUTE_UV1; } - else if (attribute == "auto") { gltfOptions.keepAttribs |= RAW_VERTEX_ATTRIBUTE_AUTO; } - else { - fmt::printf("Unknown --keep-attribute: %s\n", attribute); - fmt::printf("Valid choices are: position, normal, tangent, binormial, color, uv0, uv1, auto,\n"); - return 1; - } - } - } - - if (gltfOptions.embedResources && gltfOptions.outputBinary) { - fmt::printf("Note: Ignoring --embed; it's meaningless with --binary.\n"); - } - - if (options.count("output") == 0) { - // if -o is not given, default to the basename of the .fbx - outputPath = fmt::format(".{}{}", (const char)StringUtils::GetPathSeparator(), StringUtils::GetFileBaseString(inputPath)); - - fmt::printf("outputPath = %s\n", outputPath); - } - std::string outputFolder; // the output folder in .gltf mode, not used for .glb - std::string modelPath; // the path of the actual .glb or .gltf file - if (gltfOptions.outputBinary) { - // in binary mode, we write precisely where we're asked - modelPath = outputPath + ".glb"; - - } else { - // in gltf mode, we create a folder and write into that - outputFolder = fmt::format("{}_out{}", outputPath.c_str(), (const char)StringUtils::GetPathSeparator()); - modelPath = outputFolder + StringUtils::GetFileNameString(outputPath) + ".gltf"; - } - if (!FileUtils::CreatePath(modelPath.c_str())) { - fmt::fprintf(stderr, "ERROR: Failed to create folder: %s'\n", outputFolder.c_str()); - return 1; - } - - ModelData *data_render_model = nullptr; - RawModel raw; - - if (verboseOutput) { - fmt::printf("Loading FBX File: %s\n", inputPath); - } - if (!LoadFBXFile(raw, inputPath.c_str(), "png;jpg;jpeg")) { - fmt::fprintf(stderr, "ERROR:: Failed to parse FBX: %s\n", inputPath); - return 1; - } - - if (!texturesTransforms.empty()) { - raw.TransformTextures(texturesTransforms); - } - raw.Condense(); - raw.TransformGeometry(gltfOptions.computeNormals); - - std::ofstream outStream; // note: auto-flushes in destructor - const auto streamStart = outStream.tellp(); - - outStream.open(modelPath, std::ios::trunc | std::ios::ate | std::ios::out | std::ios::binary); - if (outStream.fail()) { - fmt::fprintf(stderr, "ERROR:: Couldn't open file for writing: %s\n", modelPath.c_str()); - return 1; - } - data_render_model = Raw2Gltf(outStream, outputFolder, raw, gltfOptions); - - if (gltfOptions.outputBinary) { - fmt::printf( - "Wrote %lu bytes of binary glTF to %s.\n", - (unsigned long) (outStream.tellp() - streamStart), modelPath); - delete data_render_model; - return 0; - } + app.add_flag( + "-v,--verbose", + verboseOutput, + "Include blend shape tangents, if reported present by the FBX SDK."); + app.add_flag_function("-V,--version", [&](size_t count) { fmt::printf( - "Wrote %lu bytes of glTF to %s.\n", - (unsigned long) (outStream.tellp() - streamStart), modelPath); + "FBX2glTF version %s\nCopyright (c) 2016-2018 Oculus VR, LLC.\n", + FBX2GLTF_VERSION); + exit(0); + }); - if (gltfOptions.embedResources) { - // we're done: everything was inlined into the glTF JSON - delete data_render_model; - return 0; - } + std::string inputPath; + app.add_option("FBX Model", inputPath, "The FBX model to convert.") + ->check(CLI::ExistingFile); + app.add_option("-i,--input", inputPath, "The FBX model to convert.") + ->check(CLI::ExistingFile); - assert(!outputFolder.empty()); + std::string outputPath; + app.add_option( + "-o,--output", + outputPath, + "Where to generate the output, without suffix."); - const std::string binaryPath = outputFolder + extBufferFilename; - FILE *fp = fopen(binaryPath.c_str(), "wb"); - if (fp == nullptr) { - fmt::fprintf(stderr, "ERROR:: Couldn't open file '%s' for writing.\n", binaryPath); - return 1; - } + app.add_flag( + "-e,--embed", + gltfOptions.embedResources, + "Inline buffers as data:// URIs within generated non-binary glTF."); + app.add_flag( + "-b,--binary", + gltfOptions.outputBinary, + "Output a single binary format .glb file."); - if (data_render_model->binary->empty() == false) - { - const unsigned char *binaryData = &(*data_render_model->binary)[0]; - unsigned long binarySize = data_render_model->binary->size(); - if (fwrite(binaryData, binarySize, 1, fp) != 1) { - fmt::fprintf(stderr, "ERROR: Failed to write %lu bytes to file '%s'.\n", binarySize, binaryPath); - fclose(fp); - return 1; + app.add_option( + "--long-indices", + [&](std::vector choices) -> bool { + for (const std::string choice : choices) { + if (choice == "never") { + gltfOptions.useLongIndices = UseLongIndicesOptions::NEVER; + } else if (choice == "auto") { + gltfOptions.useLongIndices = UseLongIndicesOptions::AUTO; + } else if (choice == "always") { + gltfOptions.useLongIndices = UseLongIndicesOptions::ALWAYS; + } else { + fmt::printf("Unknown --long-indices: %s\n", choice); + throw CLI::RuntimeError(1); + } + } + return true; + }, + "Whether to use 32-bit indices.") + ->type_name("(never|auto|always)"); + + app.add_option( + "--compute-normals", + [&](std::vector choices) -> bool { + for (const std::string choice : choices) { + if (choice == "never") { + gltfOptions.computeNormals = ComputeNormalsOption::NEVER; + } else if (choice == "broken") { + gltfOptions.computeNormals = ComputeNormalsOption::BROKEN; + } else if (choice == "missing") { + gltfOptions.computeNormals = ComputeNormalsOption::MISSING; + } else if (choice == "always") { + gltfOptions.computeNormals = ComputeNormalsOption::ALWAYS; + } else { + fmt::printf("Unknown --compute-normals option: %s\n", choice); + throw CLI::RuntimeError(1); + } + } + return true; + }, + "When to compute vertex normals from mesh geometry.") + ->type_name("(never|broken|missing|always)"); + + std::vector> texturesTransforms; + app.add_flag_function( + "--flip-u", + [&](size_t count) { + if (count > 0) { + texturesTransforms.emplace_back( + [](Vec2f uv) { return Vec2f(1.0f - uv[0], uv[1]); }); + if (verboseOutput) { + fmt::printf("Flipping texture coordinates in the 'U' dimension.\n"); + } } - fclose(fp); - fmt::printf("Wrote %lu bytes of binary data to %s.\n", binarySize, binaryPath); - } + }, + "Flip all U texture coordinates."); + app.add_flag("--no-flip-u", "Don't flip U texture coordinates.") + ->excludes("--flip-u"); + + app.add_flag_function( + "--no-flip-v", + [&](size_t count) { + if (count > 0) { + texturesTransforms.emplace_back( + [](Vec2f uv) { return Vec2f(uv[0], 1.0f - uv[1]); }); + if (verboseOutput) { + fmt::printf("NOT flipping texture coordinates in the 'V' dimension.\n"); + } + } + }, + "Flip all V texture coordinates."); + app.add_flag("--flip-v", "Don't flip U texture coordinates.") + ->excludes("--no-flip-v"); + + app.add_flag( + "--pbr-metallic-rougnness", + gltfOptions.usePBRMetRough, + "Try to glean glTF 2.0 native PBR attributes from the FBX.") + ->group("Materials"); + + app.add_flag( + "--khr-materials-unlit", + gltfOptions.useKHRMatUnlit, + "Use KHR_materials_unlit extension to request an unlit shader.") + ->group("Materials"); + + app.add_flag( + "--khr-lights-punctual", + gltfOptions.useKHRLightsPunctual, + "Use KHR_lights_punctual extension to request an unlit shader."); + + app.add_flag( + "--user-properties", + gltfOptions.enableUserProperties, + "Transcribe FBX User Properties into glTF node and material 'extras'."); + + app.add_flag( + "--blend-shape-normals", + gltfOptions.useBlendShapeNormals, + "Include blend shape normals, if reported present by the FBX SDK."); + + app.add_flag( + "--blend-shape-tangents", + gltfOptions.useBlendShapeTangents, + "Include blend shape tangents, if reported present by the FBX SDK."); + + app.add_option( + "-k,--keep-attribute", + [&](std::vector attributes) -> bool { + gltfOptions.keepAttribs = RAW_VERTEX_ATTRIBUTE_JOINT_INDICES | + RAW_VERTEX_ATTRIBUTE_JOINT_WEIGHTS; + for (std::string attribute : attributes) { + if (attribute == "position") { + gltfOptions.keepAttribs |= RAW_VERTEX_ATTRIBUTE_POSITION; + } else if (attribute == "normal") { + gltfOptions.keepAttribs |= RAW_VERTEX_ATTRIBUTE_NORMAL; + } else if (attribute == "tangent") { + gltfOptions.keepAttribs |= RAW_VERTEX_ATTRIBUTE_TANGENT; + } else if (attribute == "binormal") { + gltfOptions.keepAttribs |= RAW_VERTEX_ATTRIBUTE_BINORMAL; + } else if (attribute == "color") { + gltfOptions.keepAttribs |= RAW_VERTEX_ATTRIBUTE_COLOR; + } else if (attribute == "uv0") { + gltfOptions.keepAttribs |= RAW_VERTEX_ATTRIBUTE_UV0; + } else if (attribute == "uv1") { + gltfOptions.keepAttribs |= RAW_VERTEX_ATTRIBUTE_UV1; + } else if (attribute == "auto") { + gltfOptions.keepAttribs |= RAW_VERTEX_ATTRIBUTE_AUTO; + } else { + fmt::printf("Unknown --keep-attribute option: %s\n", attribute); + throw CLI::RuntimeError(1); + } + } + return true; + }, + "Used repeatedly to build a limiting set of vertex attributes to keep.") + ->type_size(-1) + ->type_name("(position|normal|tangent|binormial|color|uv0|uv1|auto)"); + + app.add_flag( + "-d,--draco", + gltfOptions.draco.enabled, + "Apply Draco mesh compression to geometries.") + ->group("Draco"); + + app.add_option( + "--draco-compression-level", + gltfOptions.draco.compressionLevel, + "The compression level to tune Draco to.", + true) + ->check(CLI::Range(0, 10)) + ->group("Draco"); + + app.add_option( + "--draco-bits-for-position", + gltfOptions.draco.quantBitsPosition, + "How many bits to quantize position to.", + true) + ->check(CLI::Range(1, 32)) + ->group("Draco"); + + app.add_option( + "--draco-bits-for-uv", + gltfOptions.draco.quantBitsTexCoord, + "How many bits to quantize UV coordinates to.", + true) + ->check(CLI::Range(1, 32)) + ->group("Draco"); + + app.add_option( + "--draco-bits-for-normals", + gltfOptions.draco.quantBitsNormal, + "How many bits to quantize nornals to.", + true) + ->check(CLI::Range(1, 32)) + ->group("Draco"); + + app.add_option( + "--draco-bits-for-colors", + gltfOptions.draco.quantBitsColor, + "How many bits to quantize colors to.", + true) + ->check(CLI::Range(1, 32)) + ->group("Draco"); + + app.add_option( + "--draco-bits-for-other", + gltfOptions.draco.quantBitsGeneric, + "How many bits to quantize all other vertex attributes to.", + true) + ->check(CLI::Range(1, 32)) + ->group("Draco"); + + CLI11_PARSE(app, argc, argv); + + if (inputPath.empty()) { + fmt::printf("You must supply a FBX file to convert.\n"); + exit(1); + } + + if (!gltfOptions.useKHRMatUnlit && !gltfOptions.usePBRMetRough) { + if (verboseOutput) { + fmt::printf("Defaulting to --pbr-metallic-roughness material support.\n"); + } + gltfOptions.usePBRMetRough = true; + } + + if (gltfOptions.embedResources && gltfOptions.outputBinary) { + fmt::printf("Note: Ignoring --embed; it's meaningless with --binary.\n"); + } + + if (outputPath.empty()) { + // if -o is not given, default to the basename of the .fbx + outputPath = fmt::format( + ".{}{}", + (const char)StringUtils::GetPathSeparator(), + StringUtils::GetFileBaseString(inputPath)); + } + // the output folder in .gltf mode, not used for .glb + std::string outputFolder; + + // the path of the actual .glb or .gltf file + std::string modelPath; + if (gltfOptions.outputBinary) { + // in binary mode, we write precisely where we're asked + modelPath = outputPath + ".glb"; + + } else { + // in gltf mode, we create a folder and write into that + outputFolder = fmt::format( + "{}_out{}", + outputPath.c_str(), + (const char)StringUtils::GetPathSeparator()); + modelPath = + outputFolder + StringUtils::GetFileNameString(outputPath) + ".gltf"; + } + if (!FileUtils::CreatePath(modelPath.c_str())) { + fmt::fprintf( + stderr, "ERROR: Failed to create folder: %s'\n", outputFolder.c_str()); + return 1; + } + + ModelData* data_render_model = nullptr; + RawModel raw; + + if (verboseOutput) { + fmt::printf("Loading FBX File: %s\n", inputPath); + } + if (!LoadFBXFile(raw, inputPath.c_str(), "png;jpg;jpeg")) { + fmt::fprintf(stderr, "ERROR:: Failed to parse FBX: %s\n", inputPath); + return 1; + } + + if (!texturesTransforms.empty()) { + raw.TransformTextures(texturesTransforms); + } + raw.Condense(); + raw.TransformGeometry(gltfOptions.computeNormals); + + std::ofstream outStream; // note: auto-flushes in destructor + const auto streamStart = outStream.tellp(); + + outStream.open( + modelPath, + std::ios::trunc | std::ios::ate | std::ios::out | std::ios::binary); + if (outStream.fail()) { + fmt::fprintf( + stderr, + "ERROR:: Couldn't open file for writing: %s\n", + modelPath.c_str()); + return 1; + } + data_render_model = Raw2Gltf(outStream, outputFolder, raw, gltfOptions); + + if (gltfOptions.outputBinary) { + fmt::printf( + "Wrote %lu bytes of binary glTF to %s.\n", + (unsigned long)(outStream.tellp() - streamStart), + modelPath); delete data_render_model; return 0; + } + + fmt::printf( + "Wrote %lu bytes of glTF to %s.\n", + (unsigned long)(outStream.tellp() - streamStart), + modelPath); + + if (gltfOptions.embedResources) { + // we're done: everything was inlined into the glTF JSON + delete data_render_model; + return 0; + } + + assert(!outputFolder.empty()); + + const std::string binaryPath = outputFolder + extBufferFilename; + FILE* fp = fopen(binaryPath.c_str(), "wb"); + if (fp == nullptr) { + fmt::fprintf( + stderr, "ERROR:: Couldn't open file '%s' for writing.\n", binaryPath); + return 1; + } + + if (data_render_model->binary->empty() == false) { + const unsigned char* binaryData = &(*data_render_model->binary)[0]; + unsigned long binarySize = data_render_model->binary->size(); + if (fwrite(binaryData, binarySize, 1, fp) != 1) { + fmt::fprintf( + stderr, + "ERROR: Failed to write %lu bytes to file '%s'.\n", + binarySize, + binaryPath); + fclose(fp); + return 1; + } + fclose(fp); + fmt::printf( + "Wrote %lu bytes of binary data to %s.\n", binarySize, binaryPath); + } + + delete data_render_model; + return 0; } diff --git a/src/FBX2glTF.h b/src/FBX2glTF.h index 97a5bd6..5d3f07a 100644 --- a/src/FBX2glTF.h +++ b/src/FBX2glTF.h @@ -76,12 +76,12 @@ struct GltfOptions /** Whether and how to use KHR_draco_mesh_compression to minimize static geometry size. */ struct { bool enabled = false; - int compressionLevel = -1; - int quantBitsPosition = -1; - int quantBitsTexCoord = -1; - int quantBitsNormal = -1; - int quantBitsColor = -1; - int quantBitsGeneric = -1; + int compressionLevel = 7; + int quantBitsPosition = 14; + int quantBitsTexCoord = 10; + int quantBitsNormal = 10; + int quantBitsColor = 8; + int quantBitsGeneric = 8; } draco; /** Whether to include FBX User Properties as 'extras' metadata in glTF nodes. */ @@ -93,7 +93,7 @@ struct GltfOptions bool usePBRMetRough { false }; /** Whether to include lights through the KHR_punctual_lights extension. */ - bool useKHRPunctualLights { true }; + bool useKHRLightsPunctual { true }; /** Whether to include blend shape normals, if present according to the SDK. */ bool useBlendShapeNormals { false }; diff --git a/src/gltf/Raw2Gltf.cpp b/src/gltf/Raw2Gltf.cpp index 612144f..55988e9 100644 --- a/src/gltf/Raw2Gltf.cpp +++ b/src/gltf/Raw2Gltf.cpp @@ -631,7 +631,7 @@ ModelData *Raw2Gltf( // lights // std::vector khrPunctualLights; - if (options.useKHRPunctualLights) { + if (options.useKHRLightsPunctual) { for (int i = 0; i < raw.GetLightCount(); i ++) { const RawLight &light = raw.GetLight(i); LightData::Type type; diff --git a/third_party/CLI11/CLI11.hpp b/third_party/CLI11/CLI11.hpp new file mode 100644 index 0000000..51a23dc --- /dev/null +++ b/third_party/CLI11/CLI11.hpp @@ -0,0 +1,4113 @@ +#pragma once + +// CLI11: Version 1.6.2 +// Originally designed by Henry Schreiner +// https://github.com/CLIUtils/CLI11 +// +// This is a standalone header file generated by MakeSingleHeader.py in CLI11/scripts +// from: v1.6.2 +// +// From LICENSE: +// +// CLI11 1.6 Copyright (c) 2017-2018 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. + + +// 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 +#include + + +// Verbatim copy from CLI/Version.hpp: + + +#define CLI11_VERSION_MAJOR 1 +#define CLI11_VERSION_MINOR 6 +#define CLI11_VERSION_PATCH 2 +#define CLI11_VERSION "1.6.2" + + + + +// Verbatim copy from CLI/Macros.hpp: + + +// 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 + + + + +// Verbatim copy from CLI/Optional.hpp: + +#ifdef __has_include + +// You can explicitly enable or disable support +// by defining these to 1 or 0. +#if defined(CLI11_CPP17) && __has_include() && \ + !defined(CLI11_STD_OPTIONAL) +#define CLI11_STD_OPTIONAL 1 +#endif + +#if defined(CLI11_CPP14) && __has_include() && \ + !defined(CLI11_EXPERIMENTAL_OPTIONAL) \ + && (!defined(CLI11_STD_OPTIONAL) || CLI11_STD_OPTIONAL == 0) +#define CLI11_EXPERIMENTAL_OPTIONAL 1 +#endif + +#if __has_include() && !defined(CLI11_BOOST_OPTIONAL) +#include +#if BOOST_VERSION >= 105800 +#define CLI11_BOOST_OPTIONAL 1 +#endif +#endif + +#endif + +#if CLI11_STD_OPTIONAL +#include +#endif +#if CLI11_EXPERIMENTAL_OPTIONAL +#include +#endif +#if CLI11_BOOST_OPTIONAL +#include +#endif + + +// From CLI/Version.hpp: + + + +// From CLI/Macros.hpp: + + + +// From CLI/Optional.hpp: + +namespace CLI { + +#if CLI11_STD_OPTIONAL +template std::istream &operator>>(std::istream &in, std::optional &val) { + T v; + in >> v; + val = v; + return in; +} +#endif + +#if CLI11_EXPERIMENTAL_OPTIONAL +template std::istream &operator>>(std::istream &in, std::experimental::optional &val) { + T v; + in >> v; + val = v; + return in; +} +#endif + +#if CLI11_BOOST_OPTIONAL +template std::istream &operator>>(std::istream &in, boost::optional &val) { + T v; + in >> v; + val = v; + return in; +} +#endif + +// Export the best optional to the CLI namespace +#if CLI11_STD_OPTIONAL +using std::optional; +#elif CLI11_EXPERIMENTAL_OPTIONAL +using std::experimental::optional; +#elif CLI11_BOOST_OPTIONAL +using boost::optional; +#endif + +// This is true if any optional is found +#if CLI11_STD_OPTIONAL || CLI11_EXPERIMENTAL_OPTIONAL || CLI11_BOOST_OPTIONAL +#define CLI11_OPTIONAL 1 +#endif + +} // namespace CLI + +// From CLI/StringTools.hpp: + +namespace CLI { +namespace detail { + +// 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; + size_t start = 0; + for(const auto &i : v) { + if(start++ > 0) + s << delim; + s << i; + } + return s.str(); +} + +/// Join a string in reverse order +template std::string rjoin(const T &v, std::string delim = ",") { + std::ostringstream s; + for(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); +} + +/// 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, std::string description, 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)) << ""; + out << description; + } + out << "\n"; + return out; +} + +/// Verify the first character of an option +template bool valid_first_char(T c) { return std::isalpha(c, std::locale()) || c == '_'; } + +/// Verify following characters of an option +template bool valid_later_char(T c) { + return std::isalnum(c, std::locale()) || 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; +} + +/// 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; +} + +/// Split a string '"one two" "three"' into 'one two', 'three' +inline std::vector split_up(std::string str) { + + std::vector delims = {'\'', '\"'}; + auto find_ws = [](char ch) { return std::isspace(ch, std::locale()); }; + trim(str); + + std::vector output; + + while(!str.empty()) { + if(str[0] == '\'') { + auto end = str.find('\'', 1); + if(end != std::string::npos) { + output.push_back(str.substr(1, end - 1)); + str = str.substr(end + 1); + } else { + output.push_back(str.substr(1)); + str = ""; + } + } else if(str[0] == '\"') { + auto end = str.find('\"', 1); + if(end != std::string::npos) { + output.push_back(str.substr(1, end - 1)); + str = str.substr(end + 1); + } 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, str.end()); + } else { + output.push_back(str); + str = ""; + } + } + 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(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; +} + +/// Find and replace a subtring with another substring +inline std::string find_and_replace(std::string str, std::string from, std::string to) { + + 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; +} + +} // namespace detail +} // namespace CLI + +// From CLI/Error.hpp: + +namespace CLI { + +// 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 name, std::string msg, int exit_code) : parent(std::move(name), std::move(msg), exit_code) {} \ + name(std::string name, std::string msg, ExitCodes exit_code) \ + : parent(std::move(name), 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 exit_code; + std::string name{"Error"}; + + public: + int get_exit_code() const { return exit_code; } + + std::string get_name() const { return name; } + + Error(std::string name, std::string msg, int exit_code = static_cast(ExitCodes::BaseClass)) + : runtime_error(msg), exit_code(exit_code), 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 ParseError { + CLI11_ERROR_DEF(ParseError, CallForHelp) + CallForHelp() : CallForHelp("This should be caught in your main function, see examples", ExitCodes::Success) {} +}; + +/// Usually somethign like --help-all on command line +class CallForAllHelp : public ParseError { + CLI11_ERROR_DEF(ParseError, CallForAllHelp) + CallForAllHelp() + : CallForAllHelp("This should be caught in your main function, see examples", ExitCodes::Success) {} +}; + +/// Does not output a diagnostic in CLI11_PARSE, but allows to return from main() 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(size_t min_subcom) { + if(min_subcom == 1) + return RequiredError("A subcommand"); + else + return RequiredError("Requires at least " + std::to_string(min_subcom) + " subcommands", + 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, size_t recieved) + : ArgumentMismatch(expected > 0 ? ("Expected exactly " + std::to_string(expected) + " arguments to " + name + + ", got " + std::to_string(recieved)) + : ("Expected at least " + std::to_string(-expected) + " arguments to " + name + + ", got " + std::to_string(recieved)), + ExitCodes::ArgumentMismatch) {} + + static ArgumentMismatch AtLeast(std::string name, int num) { + return ArgumentMismatch(name + ": At least " + std::to_string(num) + " required"); + } + static ArgumentMismatch TypedAtLeast(std::string name, int num, std::string type) { + return ArgumentMismatch(name + ": " + std::to_string(num) + " required " + type + " missing"); + } +}; + +/// 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) {} +}; + +/// 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 + +/// @} + +} // namespace CLI + +// From CLI/TypeTools.hpp: + +namespace CLI { + +// Type tools + +/// 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; + +/// Check to see if something is a vector (fail check by default) +template struct is_vector { static const bool value = false; }; + +/// Check to see if something is a vector (true if actually a vector) +template struct is_vector> { static bool const value = true; }; + +/// Check to see if something is bool (fail check by default) +template struct is_bool { static const bool value = false; }; + +/// Check to see if something is bool (true if actually a bool) +template <> struct is_bool { static bool const value = true; }; + +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 = {}; + +// 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 && std::is_signed::value, detail::enabler> = detail::dummy> +constexpr const char *type_name() { + return "INT"; +} + +template ::value && std::is_unsigned::value, detail::enabler> = detail::dummy> +constexpr const char *type_name() { + return "UINT"; +} + +template ::value, detail::enabler> = detail::dummy> +constexpr const char *type_name() { + return "FLOAT"; +} + +/// This one should not be used, since vector types print the internal type +template ::value, detail::enabler> = detail::dummy> +constexpr const char *type_name() { + return "VECTOR"; +} + +template ::value && !std::is_integral::value && !is_vector::value, + detail::enabler> = detail::dummy> +constexpr const char *type_name() { + return "TEXT"; +} + +// Lexical cast + +/// Signed integers / enums +template ::value && std::is_signed::value), detail::enabler> = detail::dummy> +bool lexical_cast(std::string input, T &output) { + try { + size_t n = 0; + long long output_ll = std::stoll(input, &n, 0); + output = static_cast(output_ll); + return n == input.size() && static_cast(output) == output_ll; + } catch(const std::invalid_argument &) { + return false; + } catch(const std::out_of_range &) { + return false; + } +} + +/// Unsigned integers +template ::value && std::is_unsigned::value, detail::enabler> = detail::dummy> +bool lexical_cast(std::string input, T &output) { + if(!input.empty() && input.front() == '-') + return false; // std::stoull happily converts negative values to junk without any errors. + + try { + size_t n = 0; + unsigned long long output_ll = std::stoull(input, &n, 0); + output = static_cast(output_ll); + return n == input.size() && static_cast(output) == output_ll; + } catch(const std::invalid_argument &) { + return false; + } catch(const std::out_of_range &) { + return false; + } +} + +/// Floats +template ::value, detail::enabler> = detail::dummy> +bool lexical_cast(std::string input, T &output) { + try { + size_t n = 0; + output = static_cast(std::stold(input, &n)); + return n == input.size(); + } catch(const std::invalid_argument &) { + return false; + } catch(const std::out_of_range &) { + return false; + } +} + +/// String and similar +template ::value && !std::is_integral::value && + std::is_assignable::value, + detail::enabler> = detail::dummy> +bool lexical_cast(std::string input, T &output) { + output = input; + return true; +} + +/// Non-string parsable +template ::value && !std::is_integral::value && + !std::is_assignable::value, + detail::enabler> = detail::dummy> +bool lexical_cast(std::string input, T &output) { + std::istringstream is; + + is.str(input); + is >> output; + return !is.fail() && !is.rdbuf()->in_avail(); +} + +} // namespace detail +} // namespace CLI + +// From CLI/Split.hpp: + +namespace CLI { +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; + } else + 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("="); + if(loc != std::string::npos) { + name = current.substr(2, loc - 2); + value = current.substr(loc + 1); + } else { + name = current.substr(2); + value = ""; + } + return true; + } else + return false; +} + +// Splits a string into multiple long and short names +inline std::vector split_names(std::string current) { + std::vector output; + 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; +} + +/// 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; + else 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 +} // namespace CLI + +// From CLI/ConfigFwd.hpp: + +namespace CLI { + +class App; + +namespace detail { + +/// Comma separated join, adds quotes if needed +inline std::string ini_join(std::vector args) { + std::ostringstream s; + size_t start = 0; + for(const auto &arg : args) { + if(start++ > 0) + s << " "; + + auto it = std::find_if(arg.begin(), arg.end(), [](char ch) { return std::isspace(ch, std::locale()); }); + if(it == arg.end()) + s << arg; + else if(arg.find(R"(")") == std::string::npos) + s << R"(")" << arg << R"(")"; + else + s << R"(')" << arg << R"(')"; + } + + return s.str(); +} + +} // namespace detail + +/// 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; + + /// Convert a flag to a bool + virtual std::vector to_flag(const ConfigItem &item) const { + if(item.inputs.size() == 1) { + std::string val = item.inputs.at(0); + val = detail::to_lower(val); + + if(val == "true" || val == "on" || val == "yes") { + return std::vector(1); + } else if(val == "false" || val == "off" || val == "no") { + return std::vector(); + } else { + try { + size_t ui = std::stoul(val); + return std::vector(ui); + } catch(const std::invalid_argument &) { + throw ConversionError::TrueFalse(item.fullname()); + } + } + } else { + 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 files +class ConfigINI : public Config { + public: + std::string to_config(const App *, bool default_also, bool write_description, std::string prefix) const override; + + std::vector from_config(std::istream &input) const override { + std::string line; + std::string section = "default"; + + std::vector output; + + while(getline(input, line)) { + std::vector items_buffer; + + detail::trim(line); + size_t len = line.length(); + if(len > 1 && line[0] == '[' && line[len - 1] == ']') { + section = line.substr(1, len - 2); + } else if(len > 0 && line[0] != ';') { + output.emplace_back(); + ConfigItem &out = output.back(); + + // Find = in string, split and recombine + auto pos = line.find('='); + if(pos != std::string::npos) { + out.name = detail::trim_copy(line.substr(0, pos)); + std::string item = detail::trim_copy(line.substr(pos + 1)); + items_buffer = detail::split_up(item); + } else { + out.name = detail::trim_copy(line); + items_buffer = {"ON"}; + } + + if(detail::to_lower(section) != "default") { + out.parents = {section}; + } + + if(out.name.find('.') != std::string::npos) { + std::vector plist = detail::split(out.name, '.'); + out.name = plist.back(); + plist.pop_back(); + out.parents.insert(out.parents.end(), plist.begin(), plist.end()); + } + + out.inputs.insert(std::end(out.inputs), std::begin(items_buffer), std::end(items_buffer)); + } + } + return output; + } +}; + +} // namespace CLI + +// From CLI/Validators.hpp: + +namespace CLI { + +/// @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. +/// @{ + +/// +struct Validator { + /// This is the type name, if empty the type name will not be changed + std::string tname; + + /// This it the base function that is to be called. + /// Returns a string error message if validation fails. + std::function func; + + /// 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 { return func(str); }; + + /// Combining validators is a new validator + Validator operator&(const Validator &other) const { + Validator newval; + newval.tname = (tname == other.tname ? tname : ""); + + // Give references (will make a copy in lambda function) + const std::function &f1 = func; + const std::function &f2 = other.func; + + newval.func = [f1, f2](const std::string &filename) { + std::string s1 = f1(filename); + std::string s2 = f2(filename); + if(!s1.empty() && !s2.empty()) + return s1 + " & " + s2; + else + return s1 + s2; + }; + return newval; + } + + /// Combining validators is a new validator + Validator operator|(const Validator &other) const { + Validator newval; + newval.tname = (tname == other.tname ? tname : ""); + + // Give references (will make a copy in lambda function) + const std::function &f1 = func; + const std::function &f2 = other.func; + + newval.func = [f1, f2](const std::string &filename) { + std::string s1 = f1(filename); + std::string s2 = f2(filename); + if(s1.empty() || s2.empty()) + return std::string(); + else + return s1 + " & " + s2; + }; + return newval; + } +}; + +// 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 { + +/// Check for an existing file (returns error message if check fails) +struct ExistingFileValidator : public Validator { + ExistingFileValidator() { + tname = "FILE"; + func = [](const std::string &filename) { + struct stat buffer; + bool exist = stat(filename.c_str(), &buffer) == 0; + bool is_dir = (buffer.st_mode & S_IFDIR) != 0; + if(!exist) { + return "File does not exist: " + filename; + } else if(is_dir) { + return "File is actually a directory: " + filename; + } + return std::string(); + }; + } +}; + +/// Check for an existing directory (returns error message if check fails) +struct ExistingDirectoryValidator : public Validator { + ExistingDirectoryValidator() { + tname = "DIR"; + func = [](const std::string &filename) { + struct stat buffer; + bool exist = stat(filename.c_str(), &buffer) == 0; + bool is_dir = (buffer.st_mode & S_IFDIR) != 0; + if(!exist) { + return "Directory does not exist: " + filename; + } else if(!is_dir) { + return "Directory is actually a file: " + filename; + } + return std::string(); + }; + } +}; + +/// Check for an existing path +struct ExistingPathValidator : public Validator { + ExistingPathValidator() { + tname = "PATH"; + func = [](const std::string &filename) { + struct stat buffer; + bool const exist = stat(filename.c_str(), &buffer) == 0; + if(!exist) { + return "Path does not exist: " + filename; + } + return std::string(); + }; + } +}; + +/// Check for an non-existing path +struct NonexistentPathValidator : public Validator { + NonexistentPathValidator() { + tname = "PATH"; + func = [](const std::string &filename) { + struct stat buffer; + bool exist = stat(filename.c_str(), &buffer) == 0; + if(exist) { + return "Path already exists: " + filename; + } + 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; + +/// Produce a range (factory). Min and max are inclusive. +struct Range : public Validator { + /// 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) { + std::stringstream out; + out << detail::type_name() << " in [" << min << " - " << max << "]"; + + tname = out.str(); + func = [min, max](std::string input) { + T val; + detail::lexical_cast(input, val); + if(val < min || val > max) + return "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) : Range(static_cast(0), max) {} +}; + +/// @} + +} // namespace CLI + +// From CLI/FormatterFwd.hpp: + +namespace CLI { + +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 + 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; + virtual ~FormatterBase() = 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(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 + 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)) {} + + /// 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 *, 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; + + ///@} +}; + +} // namespace CLI + +// From CLI/Option.hpp: + +namespace CLI { + +using results_t = std::vector; +using callback_t = std::function; + +class Option; +class App; + +using Option_p = std::unique_ptr