diff --git a/.gitignore b/.gitignore index f1efbe4..6d7eb1c 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ # Compiled Dynamic libraries *.so *.dylib +*.dll # Compiled Static libraries *.lai @@ -16,3 +17,16 @@ example naval_fate run_testcase +run_testcase.exe + +# CMake temporary files +CMakeCache.txt +CMakeFiles +CPackConfig.cmake +CPackSourceConfig.cmake +Makefile +cmake_install.cmake +docopt-config-version.cmake + +# Files configured by CMake +run_tests diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..90e7c68 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,103 @@ +language: cpp +sudo: false # Use the new container infrastructure + +matrix: + include: + - os: linux + env: + - COMPILER=g++-7 + addons: + apt: + sources: ['ubuntu-toolchain-r-test'] + packages: ["g++-7", "cmake-data", "cmake"] + - os: linux + env: + - COMPILER=g++-8 + addons: + apt: + sources: ['ubuntu-toolchain-r-test'] + packages: ["g++-8", "cmake-data", "cmake"] + - os: linux + env: + - COMPILER=g++-9 + addons: + apt: + sources: ['ubuntu-toolchain-r-test'] + packages: ["g++-9", "cmake-data", "cmake"] + - os: linux + env: + - COMPILER=g++-9 USE_BOOST_REGEX=ON + addons: + apt: + sources: ['ubuntu-toolchain-r-test'] + packages: ["g++-9", "cmake-data", "cmake", "libboost-regex-dev"] + + - os: linux + env: + - COMPILER=clang++-3.6 STDLIB=libc++ + addons: + apt: + sources: ['ubuntu-toolchain-r-test', 'llvm-toolchain-precise-3.6', 'george-edison55-precise-backports'] + packages: ["clang-3.6", "cmake-data", "cmake"] + + - os: linux + env: + - COMPILER=clang++-8 STDLIB=libc++ + addons: + apt: + sources: ['ubuntu-toolchain-r-test', 'llvm-toolchain-trusty-8'] + packages: ["clang-8", "cmake-data", "cmake"] + + - os: osx + osx_image: xcode9.4 + env: + - COMPILER=clang++ V='Apple LLVM 9.1' + - COMPILER=clang++ V='Apple LLVM 9.1' WITH_CPP14=true + + - os: osx + osx_image: xcode10.3 + env: + - COMPILER=clang++ V='Apple LLVM 10.0' + - COMPILER=clang++ V='Apple LLVM 10.0' WITH_CPP14=true + - os: osx + osx_image: xcode11.2 + env: + - COMPILER=clang++ V='Apple LLVM 11.0' + - COMPILER=clang++ V='Apple LLVM 11.0' WITH_CPP14=true + - os: osx + osx_image: xcode11.2 + env: + - COMPILER=clang++ V='Apple LLVM 11.0' + - COMPILER=clang++ V='Apple LLVM 11.0' WITH_CPP17=true + +before_install: + - CMAKE_CXX_FLAGS+=" -Wall" + - | + if [[ "${WITH_CPP14}" == "true" ]]; then + CMAKE_OPTIONS+=" -DCMAKE_CXX_STANDARD=14" + fi + - | + if [[ "${WITH_CPP17}" == "true" ]]; then + CMAKE_OPTIONS+=" -DCMAKE_CXX_STANDARD=17" + fi + - | + if [[ "${USE_BOOST_REGEX}" == "ON" ]]; then + CMAKE_OPTIONS+=" -DUSE_BOOST_REGEX=ON" + CMAKE_OPTIONS+=" -DBoost_REGEX_LIBRARY_DEBUG=/usr/lib/x86_64-linux-gnu/libboost_regex.so" + CMAKE_OPTIONS+=" -DBoost_REGEX_LIBRARY_RELEASE=/usr/lib/x86_64-linux-gnu/libboost_regex.so" + fi + - | + if [[ "${STDLIB}" == "libc++" ]]; then + CMAKE_CXX_FLAGS+=" -stdlib=libc++" + fi + - ${COMPILER} --version + +before_script: + - rm -rf build/ + - mkdir build + - cd build + - cmake -DCMAKE_CXX_COMPILER=${COMPILER} -DCMAKE_CXX_FLAGS=${CMAKE_CXX_FLAGS} -DWITH_TESTS=1 -DWITH_EXAMPLE=1 ${CMAKE_OPTIONS} .. + +script: + - cmake --build . + - python run_tests diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..246e90e --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,129 @@ +cmake_minimum_required(VERSION 3.5) +project(docopt.cpp VERSION 0.6.2) + +include(GNUInstallDirs) + +#============================================================================ +# Settable options +#============================================================================ +option(WITH_TESTS "Build tests." OFF) +option(WITH_EXAMPLE "Build example." OFF) +option(USE_BOOST_REGEX "Replace std::regex with Boost.Regex" OFF) + +#============================================================================ +# Internal compiler options +#============================================================================ +# C++ standard +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) +if(NOT CMAKE_CXX_STANDARD OR CMAKE_CXX_STANDARD LESS 11) + set(CMAKE_CXX_STANDARD 11) +endif() + +#============================================================================ +# Sources & headers +#============================================================================ +set(docopt_SOURCES docopt.cpp) +set(docopt_HEADERS + docopt.h + docopt_private.h + docopt_util.h + docopt_value.h + ) + +#============================================================================ +# Compile targets +#============================================================================ +add_library(docopt ${docopt_SOURCES} ${docopt_HEADERS}) +set_target_properties(docopt PROPERTIES + VERSION ${PROJECT_VERSION} + SOVERSION ${PROJECT_VERSION_MAJOR} +) + +target_include_directories(docopt PUBLIC $ $) + +if(MSVC AND BUILD_SHARED_LIBS) + # DOCOPT_DLL: Must be specified when building *and* when using the DLL. + # That's what the "PUBLIC" means. + # DOCOPT_EXPORTS: Must use __declspec(dllexport) when building the DLL. + # "PRIVATE" means it's only defined when building the DLL. + target_compile_definitions(docopt PUBLIC DOCOPT_DLL + PRIVATE DOCOPT_EXPORTS) +endif() + +if(USE_BOOST_REGEX) + add_definitions("-DDOCTOPT_USE_BOOST_REGEX") + # This is needed on Linux, where linking a static library into docopt.so + # fails because boost static libs are not compiled with -fPIC + set(Boost_USE_STATIC_LIBS OFF) + find_package(Boost 1.53 REQUIRED COMPONENTS regex) + include_directories(${Boost_INCLUDE_DIRS}) + target_link_libraries(docopt ${Boost_LIBRARIES}) +endif() + +#============================================================================ +# Examples +#============================================================================ +if(WITH_EXAMPLE) + add_executable(docopt_example examples/naval_fate.cpp) + target_link_libraries(docopt_example docopt) +endif() + +#============================================================================ +# Tests +#============================================================================ +if(WITH_TESTS) + set(TESTPROG "${CMAKE_CURRENT_BINARY_DIR}/run_testcase") + set(TESTCASES "${PROJECT_SOURCE_DIR}/testcases.docopt") + add_executable(run_testcase run_testcase.cpp) + target_link_libraries(run_testcase docopt) + configure_file( + "${PROJECT_SOURCE_DIR}/run_tests.py" + "${CMAKE_CURRENT_BINARY_DIR}/run_tests" + ESCAPE_QUOTES + ) + add_test("Testcases docopt" ${TESTPROG}) +endif() + +#============================================================================ +# Install +#============================================================================ +set(export_name "docopt-targets") + +# Runtime package +install(TARGETS docopt EXPORT ${export_name} + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}) + +# Development package +install(FILES ${docopt_HEADERS} DESTINATION include/docopt) + +# CMake Package +include(CMakePackageConfigHelpers) +write_basic_package_version_file("${PROJECT_BINARY_DIR}/docopt-config-version.cmake" COMPATIBILITY SameMajorVersion) +install(FILES docopt-config.cmake ${PROJECT_BINARY_DIR}/docopt-config-version.cmake DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/docopt") +install(EXPORT ${export_name} DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/docopt") + +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/docopt.pc.in ${CMAKE_CURRENT_BINARY_DIR}/docopt.pc @ONLY) +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/docopt.pc DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig) + +#============================================================================ +# CPack +#============================================================================ +set(CPACK_PACKAGE_NAME "docopt") +set(CPACK_DEBIAN_PACKAGE_DEPENDS "") +set(CPACK_RPM_PACKAGE_REQUIRES "") +set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "Beautiful command line interfaces") +set(CPACK_PACKAGE_VENDOR "Jared Grubb") +set(CPACK_PACKAGE_CONTACT ${CPACK_PACKAGE_VENDOR}) +set(CPACK_PACKAGE_DESCRIPTION_FILE "${PROJECT_SOURCE_DIR}/README.rst") +set(CPACK_RESOURCE_FILE_LICENSE "${PROJECT_SOURCE_DIR}/LICENSE-MIT") +set(CPACK_PACKAGE_VERSION_MAJOR ${PROJECT_VERSION_MAJOR}) +set(CPACK_PACKAGE_VERSION_MINOR ${PROJECT_VERSION_MINOR}) +set(CPACK_PACKAGE_VERSION_PATCH ${PROJECT_VERSION_PATCH}) +set(CPACK_DEBIAN_PACKAGE_SECTION "Development") +set(CPACK_RPM_PACKAGE_GROUP "Development/Libraries") +set(CPACK_RPM_PACKAGE_LICENSE "MIT") +set(CPACK_STRIP_FILES TRUE) +include(CPack) diff --git a/README.rst b/README.rst index c3979ff..6d7012d 100644 --- a/README.rst +++ b/README.rst @@ -1,7 +1,15 @@ ``docopt.cpp``: A C++11 Port ============================ -doctopt creates *beautiful* command-line interfaces ---------------------------------------------------- + +Contents +-------- + +.. contents:: + :local: + :depth: 3 + +docopt creates *beautiful* command-line interfaces +-------------------------------------------------- Isn't it awesome how ``getopt`` (and ``boost::program_options`` for you fancy folk!) generate help messages based on your code?! These timeless functions @@ -42,8 +50,8 @@ and instead can write only the help message--*the way you want it*. int main(int argc, const char** argv) { - std::map args - = docopt::docopt(USAGE, + std::map args + = docopt::docopt(USAGE, { argv + 1, argv + argc }, true, // show help if requested "Naval Fate 2.0"); // version string @@ -58,13 +66,40 @@ and instead can write only the help message--*the way you want it*. Beat that! The option parser is generated based on the docstring above that is passed to ``docopt::docopt`` function. ``docopt`` parses the usage pattern (``"Usage: ..."``) and option descriptions (lines starting -with dash "``-``") and ensures that the program invocation matches the +with a dash "``-``") and ensures that the program invocation matches the usage pattern; it parses options, arguments and commands based on that. The basic idea is that *a good help message has all necessary information in it to make a parser*. +Getting and using +----------------- + +To get *docopt.cpp*, the simplest is to use `Conda `_:: + + conda install -c conda-forge docopt.cpp + +Alternatively manual installation is done using (unix):: + + git clone + cmake . + make install + +To link *docopt.cpp*, the simplest is to use CMake. The general structure of your +``CMakeLists.txt`` would be as follows:: + + cmake_minimum_required(VERSION 3.5) + + project(example) + + find_package(docopt COMPONENTS CXX REQUIRED) + include_directories(${DOCOPT_INCLUDE_DIRS}) + + add_executable(${PROJECT_NAME} ...) + + target_link_libraries(${PROJECT_NAME} docopt) + C++11 port details ---------------------------------------------------- +------------------ This is a port of the ``docopt.py`` module (https://github.com/docopt/docopt), and we have tried to maintain full feature parity (and code structure) as the @@ -74,12 +109,14 @@ This port is written in C++11 and also requires a good C++11 standard library (in particular, one with ``regex`` support). The following compilers are known to work with docopt: -- clang 3.3 and later -- gcc 4.9 +- Clang 3.3 and later +- GCC 4.9 +- Visual C++ 2015 RC -Note that gcc-4.8 will not work due to its missing the ``regex`` module. -Note that Visual C++ 2013 will not compile this code, as its C++11 is not -quite good enough. If a later VC++ works, please let me know! +GCC-4.8 can work, but the std::regex module needs to be replaced with ``Boost.Regex``. +In that case, you will need to define ``DOCTOPT_USE_BOOST_REGEX`` when compiling +docopt, and link your code with the appropriated Boost libraries. A relatively +recent version of Boost is needed: 1.55 works, but 1.46 does not for example. This port is licensed under the MIT license, just like the original module. However, we are also dual-licensing this code under the Boost License, version 1.0, @@ -93,13 +130,13 @@ The differences from the Python port are: * a ``docopt::value`` type to hold the various value types that can be parsed. We considered using boost::variant, but it seems better to have no external dependencies (beyond a good STL). -* because C++ is statically-typed and Python is not, we had to make some - changes to the interfaces of the internal parse tree types. +* because C++ is statically-typed and Python is not, we had to make some + changes to the interfaces of the internal parse tree types. * because ``std::regex`` does not have an equivalent to Python's regex.split, some of the regex's had to be restructured and additional loops used. API ---------------------------------------------------- +--- .. code:: c++ @@ -127,7 +164,7 @@ API - ``argv`` is a vector of strings representing the args passed. Although main usually takes a ``(int argc, const char** argv)`` pair, you can pass the value ``{argv+1, argv+argc}`` to generate the vector automatically. - (Note we skip the argv[0] argument!) Alternatively you can supply a list of + (Note we skip the argv[0] argument!) Alternatively you can supply a list of strings like ``{ "--verbose", "-o", "hai.txt" }``. - ``help``, by default ``true``, specifies whether the parser should @@ -156,8 +193,8 @@ API compatibility with POSIX, or if you want to dispatch your arguments to other programs. -The **return** value is a ``map`` with options, -arguments and commands as keys, spelled exactly like in your help message. +The **return** value is a ``map`` with options, +arguments and commands as keys, spelled exactly like in your help message. Long versions of options are given priority. For example, if you invoke the top example as:: @@ -180,16 +217,15 @@ If any parsing error (in either the usage, or due to incorrect user inputs) is encountered, the program will exit with exit code -1. Note that there is another function that does not exit on error, and instead will -propogate an exception that you can catch and process as you like. See the docopt.h file +propagate an exception that you can catch and process as you like. See the docopt.h file for information on the exceptions and usage: .. code:: c++ docopt::docopt_parse(doc, argv, help /* =true */, version /* =true */, options_first /* =false) - Help message format ---------------------------------------------------- +------------------- Help message consists of 2 parts: @@ -208,7 +244,7 @@ Help message consists of 2 parts: Their format is described below; other text is ignored. Usage pattern format ----------------------------------------------------------------------- +-------------------- **Usage pattern** is a substring of ``doc`` that starts with ``usage:`` (case *insensitive*) and ends with a *visibly* empty line. @@ -261,7 +297,7 @@ Use the following constructs to specify patterns: - **|** (pipe) **mutually exclusive** elements. Group them using **( )** if one of the mutually exclusive elements is required: ``my_program (--clockwise | --counter-clockwise) TIME``. Group - them using **[ ]** if none of the mutually-exclusive elements are + them using **[ ]** if none of the mutually exclusive elements are required: ``my_program [--left | --right]``. - **...** (ellipsis) **one or more** elements. To specify that arbitrary number of repeating elements could be accepted, use @@ -289,7 +325,7 @@ then number of occurrences of the option will be counted. I.e. ``args['-v']`` will be ``2`` if program was invoked as ``my_program -vv``. Same works for commands. -If your usage patterns allows to match same-named option with argument +If your usage pattern allows to match same-named option with argument or positional argument several times, the matched arguments will be collected into a list:: @@ -299,9 +335,8 @@ I.e. invoked with ``my_program file1 file2 --path=./here --path=./there`` the returned dict will contain ``args[''] == ['file1', 'file2']`` and ``args['--path'] == ['./here', './there']``. - Option descriptions format ----------------------------------------------------------------------- +-------------------------- **Option descriptions** consist of a list of options that you put below your usage patterns. @@ -326,7 +361,7 @@ The rules are as follows: argument after space (or equals "``=``" sign) as shown below. Follow either or UPPER-CASE convention for options' arguments. You can use comma if you want to separate options. In - the example below, both lines are valid, however you are recommended + the example below, both lines are valid. However you are recommended to stick to a single style.:: -o FILE --output=FILE # without comma, with "=" sign @@ -350,7 +385,7 @@ The rules are as follows: - If the option is not repeatable, the value inside ``[default: ...]`` will be interpreted as string. If it *is* repeatable, it will be - splited into a list on whitespace:: + split into a list on whitespace:: Usage: my_program [--repeatable= --repeatable=] [--another-repeatable=]... @@ -366,18 +401,18 @@ The rules are as follows: --not-repeatable= [default: ./here ./there] Examples ----------------------------------------------------------------------- +-------- We have an extensive list of `examples `_ which cover every aspect of functionality of **docopt**. Try them out, read the source if in doubt. -There are also very intersting applications and ideas at that page. +There are also very interesting applications and ideas at that page. Check out the sister project for more information! Subparsers, multi-level help and *huge* applications (like git) ----------------------------------------------------------------------- +--------------------------------------------------------------- If you want to split your usage-pattern into several, implement multi-level help (with separate help-screen for each subcommand), @@ -389,7 +424,8 @@ we implemented a subset of git command-line interface as an example: `_ Compiling the example / Running the tests ----------------------------------------------------------------------- +----------------------------------------- + The original Python module includes some language-agnostic unit tests, and these can be run with this port as well. @@ -398,7 +434,7 @@ a C++ test case runner (run_testcase.cpp):: $ clang++ --std=c++11 --stdlib=libc++ docopt.cpp run_testcase.cpp -o run_testcase $ python run_tests.py - PASS (175) + PASS (175) You can also compile the example shown at the start (included as example.cpp):: @@ -423,21 +459,21 @@ You can also compile the example shown at the start (included as example.cpp):: shoot: false Development ---------------------------------------------------- +----------- -Comments and suggestions are *very* welcome! If you find issues, please +Comments and suggestions are *very* welcome! If you find issues, please file them and help improve our code! -Please note, however, that we have tried to stay true to the original +Please note, however, that we have tried to stay true to the original Python code. If you have any major patches, structural changes, or new features, we might want to first negotiate these changes into the Python code first. However, bring it up! Let's hear it! Changelog ---------------------------------------------------- +--------- **docopt** follows `semantic versioning `_. The first release with stable API will be 1.0.0 (soon). -- 0.6.1 The initial C++ port of docopt.py - +- 0.6.2 Bugfix release (still based on docopt 0.6.1) +- 0.6.1 The initial C++ port of docopt.py (based on docopt 0.6.1) diff --git a/docopt-config.cmake b/docopt-config.cmake new file mode 100644 index 0000000..33c36c0 --- /dev/null +++ b/docopt-config.cmake @@ -0,0 +1 @@ +include("${CMAKE_CURRENT_LIST_DIR}/docopt-targets.cmake") diff --git a/docopt.cpp b/docopt.cpp index 25589c1..c6ee9b9 100644 --- a/docopt.cpp +++ b/docopt.cpp @@ -17,431 +17,47 @@ #include #include #include -#include #include #include +#include using namespace docopt; -DocoptExitHelp::DocoptExitHelp() -: std::runtime_error("Docopt --help argument encountered") -{} - -DocoptExitVersion::DocoptExitVersion() -: std::runtime_error("Docopt --version argument encountered") -{} - -const char* value::kindAsString(Kind kind) -{ - switch (kind) { - case Kind::Empty: return "empty"; - case Kind::Bool: return "bool"; - case Kind::Long: return "long"; - case Kind::String: return "string"; - case Kind::StringList: return "string-list"; - } - return "unknown"; -} - -void value::throwIfNotKind(Kind expected) const -{ - if (kind == expected) - return; - - std::string error = "Illegal cast to "; - error += kindAsString(expected); - error += "; type is actually "; - error += kindAsString(kind); - throw std::runtime_error(std::move(error)); -} - +DOCOPT_INLINE std::ostream& docopt::operator<<(std::ostream& os, value const& val) { if (val.isBool()) { bool b = val.asBool(); - std::cout << (b ? "true" : "false"); + os << (b ? "true" : "false"); } else if (val.isLong()) { long v = val.asLong(); - std::cout << v; + os << v; } else if (val.isString()) { std::string const& str = val.asString(); - std::cout << '"' << str << '"'; + os << '"' << str << '"'; } else if (val.isStringList()) { auto const& list = val.asStringList(); - std::cout << "["; + os << "["; bool first = true; for(auto const& el : list) { if (first) { first = false; } else { - std::cout << ", "; + os << ", "; } - std::cout << '"' << el << '"'; + os << '"' << el << '"'; } - std::cout << "]"; + os << "]"; } else { - std::cout << "null"; + os << "null"; } return os; } -#pragma mark - -#pragma mark Pattern types - -std::vector Pattern::leaves() { - std::vector ret; - collect_leaves(ret); - return ret; -} - -bool Required::match(PatternList& left, std::vector>& collected) const -{ - auto l = left; - auto c = collected; - - for(auto const& pattern : fChildren) { - bool ret = pattern->match(l, c); - if (!ret) { - // leave (left, collected) untouched - return false; - } - } - - left = std::move(l); - collected = std::move(c); - return true; -} - -bool LeafPattern::match(PatternList& left, std::vector>& collected) const -{ - auto match = single_match(left); - if (!match.second) { - return false; - } - - left.erase(left.begin()+match.first); - - auto same_name = std::find_if(collected.begin(), collected.end(), [&](std::shared_ptr const& p) { - return p->name()==name(); - }); - if (getValue().isLong()) { - long val = 1; - if (same_name == collected.end()) { - collected.push_back(match.second); - match.second->setValue(value{val}); - } else if ((**same_name).getValue().isLong()) { - val += (**same_name).getValue().asLong(); - (**same_name).setValue(value{val}); - } else { - (**same_name).setValue(value{val}); - } - } else if (getValue().isStringList()) { - std::vector val; - if (match.second->getValue().isString()) { - val.push_back(match.second->getValue().asString()); - } else if (match.second->getValue().isStringList()) { - val = match.second->getValue().asStringList(); - } else { - /// cant be!? - } - - if (same_name == collected.end()) { - collected.push_back(match.second); - match.second->setValue(value{val}); - } else if ((**same_name).getValue().isStringList()) { - std::vector const& list = (**same_name).getValue().asStringList(); - val.insert(val.begin(), list.begin(), list.end()); - (**same_name).setValue(value{val}); - } else { - (**same_name).setValue(value{val}); - } - } else { - collected.push_back(match.second); - } - return true; -} - -Option Option::parse(std::string const& option_description) -{ - std::string shortOption, longOption; - int argcount = 0; - value val { false }; - - auto double_space = option_description.find(" "); - auto options_end = option_description.end(); - if (double_space != std::string::npos) { - options_end = option_description.begin() + double_space; - } - - static const std::regex pattern {"(--|-)?(.*?)([,= ]|$)"}; - for(std::sregex_iterator i {option_description.begin(), options_end, pattern, std::regex_constants::match_not_null}, - e{}; - i != e; - ++i) - { - std::smatch const& match = *i; - if (match[1].matched) { // [1] is optional. - if (match[1].length()==1) { - shortOption = "-" + match[2].str(); - } else { - longOption = "--" + match[2].str(); - } - } else if (match[2].length() > 0) { // [2] always matches. - std::string m = match[2]; - argcount = 1; - } else { - // delimeter - } - - if (match[3].length() == 0) { // [3] always matches. - // Hit end of string. For some reason 'match_not_null' will let us match empty - // at the end, and then we'll spin in an infinite loop. So, if we hit an empty - // match, we know we must be at the end. - break; - } - } - - if (argcount) { - std::smatch match; - if (std::regex_search(options_end, option_description.end(), - match, - std::regex{"\\[default: (.*)\\]", std::regex::icase})) - { - val = match[1].str(); - } - } - - return {std::move(shortOption), - std::move(longOption), - argcount, - std::move(val)}; -} - -bool OneOrMore::match(PatternList& left, std::vector>& collected) const -{ - assert(fChildren.size() == 1); - - auto l = left; - auto c = collected; - - bool matched = true; - size_t times = 0; - - decltype(l) l_; - bool firstLoop = true; - - while (matched) { - // could it be that something didn't match but changed l or c? - matched = fChildren[0]->match(l, c); - - if (matched) - ++times; - - if (firstLoop) { - firstLoop = false; - } else if (l == l_) { - break; - } - - l_ = l; - } - - if (times == 0) { - return false; - } - - left = std::move(l); - collected = std::move(c); - return true; -} - -bool Either::match(PatternList& left, std::vector>& collected) const -{ - using Outcome = std::pair>>; - - std::vector outcomes; - - for(auto const& pattern : fChildren) { - // need a copy so we apply the same one for every iteration - auto l = left; - auto c = collected; - bool matched = pattern->match(l, c); - if (matched) { - outcomes.emplace_back(std::move(l), std::move(c)); - } - } - - auto min = std::min_element(outcomes.begin(), outcomes.end(), [](Outcome const& o1, Outcome const& o2) { - return o1.first.size() < o2.first.size(); - }); - - if (min == outcomes.end()) { - // (left, collected) unchanged - return false; - } - - std::tie(left, collected) = std::move(*min); - return true; -} - -std::pair> Argument::single_match(PatternList const& left) const -{ - std::pair> ret {}; - - for(size_t i = 0, size = left.size(); i < size; ++i) - { - auto arg = dynamic_cast(left[i].get()); - if (arg) { - ret.first = i; - ret.second = std::make_shared(name(), arg->getValue()); - break; - } - } - - return ret; -} - -std::pair> Command::single_match(PatternList const& left) const -{ - std::pair> ret {}; - - for(size_t i = 0, size = left.size(); i < size; ++i) - { - auto arg = dynamic_cast(left[i].get()); - if (arg) { - if (name() == arg->getValue()) { - ret.first = i; - ret.second = std::make_shared(name(), value{true}); - } - break; - } - } - - return ret; -} - -std::pair> Option::single_match(PatternList const& left) const -{ - std::pair> ret {}; - - for(size_t i = 0, size = left.size(); i < size; ++i) - { - auto leaf = std::dynamic_pointer_cast(left[i]); - if (leaf && name() == leaf->name()) { - ret.first = i; - ret.second = leaf; - break; - } - } - - return ret; -} - +#if 0 #pragma mark - #pragma mark Parsing stuff - -std::vector transform(PatternList pattern); - -void BranchPattern::fix_repeating_arguments() -{ - std::vector either = transform(children()); - for(auto const& group : either) { - // use multiset to help identify duplicate entries - std::unordered_multiset, PatternHasher> group_set {group.begin(), group.end()}; - for(auto const& e : group_set) { - if (group_set.count(e) == 1) - continue; - - LeafPattern* leaf = dynamic_cast(e.get()); - if (!leaf) continue; - - bool ensureList = false; - bool ensureInt = false; - - if (dynamic_cast(leaf)) { - ensureInt = true; - } else if (dynamic_cast(leaf)) { - ensureList = true; - } else if (Option* o = dynamic_cast(leaf)) { - if (o->argCount()) { - ensureList = true; - } else { - ensureInt = true; - } - } - - if (ensureList) { - std::vector newValue; - if (leaf->getValue().isString()) { - newValue = split(leaf->getValue().asString()); - } - if (!leaf->getValue().isStringList()) { - leaf->setValue(value{newValue}); - } - } else if (ensureInt) { - leaf->setValue(value{0}); - } - } - } -} - -std::vector transform(PatternList pattern) -{ - std::vector result; - - std::vector groups; - groups.emplace_back(std::move(pattern)); - - while(!groups.empty()) { - // pop off the first element - auto children = std::move(groups[0]); - groups.erase(groups.begin()); - - // find the first branch node in the list - auto child_iter = std::find_if(children.begin(), children.end(), [](std::shared_ptr const& p) { - return dynamic_cast(p.get()); - }); - - // no branch nodes left : expansion is complete for this grouping - if (child_iter == children.end()) { - result.emplace_back(std::move(children)); - continue; - } - - // pop the child from the list - auto child = std::move(*child_iter); - children.erase(child_iter); - - // expand the branch in the appropriate way - if (Either* either = dynamic_cast(child.get())) { - // "[e] + children" for each child 'e' in Either - for(auto const& eitherChild : either->children()) { - PatternList group = { eitherChild }; - group.insert(group.end(), children.begin(), children.end()); - - groups.emplace_back(std::move(group)); - } - } else if (OneOrMore* oneOrMore = dynamic_cast(child.get())) { - // child.children * 2 + children - auto const& subchildren = oneOrMore->children(); - PatternList group = subchildren; - group.insert(group.end(), subchildren.begin(), subchildren.end()); - group.insert(group.end(), children.begin(), children.end()); - - groups.emplace_back(std::move(group)); - } else { // Required, Optional, OptionsShortcut - BranchPattern* branch = dynamic_cast(child.get()); - - // child.children + children - PatternList group = branch->children(); - group.insert(group.end(), children.begin(), children.end()); - - groups.emplace_back(std::move(group)); - } - } - - return result; -} +#endif class Tokens { public: @@ -449,11 +65,11 @@ class Tokens { : fTokens(std::move(tokens)), fIsParsingArgv(isParsingArgv) {} - + explicit operator bool() const { return fIndex < fTokens.size(); } - + static Tokens from_pattern(std::string const& source) { static const std::regex re_separators { "(?:\\s*)" // any spaces (non-matching subgroup) @@ -462,21 +78,21 @@ class Tokens { "|" "\\.\\.\\." // elipsis ")" }; - + static const std::regex re_strings { "(?:\\s*)" // any spaces (non-matching subgroup) "(" "\\S*<.*?>" // strings, but make sure to keep "< >" strings together "|" - "\\S+" // string without <> + "[^<>\\s]+" // string without <> ")" }; - + // We do two stages of regex matching. The '[]()' and '...' are strong delimeters // and need to be split out anywhere they occur (even at the end of a token). We // first split on those, and then parse the stuff between them to find the string // tokens. This is a little harder than the python version, since they have regex.split // and we dont have anything like that. - + std::vector tokens; std::for_each(std::sregex_iterator{ source.begin(), source.end(), re_separators }, std::sregex_iterator{}, @@ -491,53 +107,53 @@ class Tokens { tokens.push_back(m[1].str()); }); } - + // handle the delimter token itself if (match[1].matched) { tokens.push_back(match[1].str()); } }); - + return Tokens(tokens, false); } - + std::string const& current() const { if (*this) return fTokens[fIndex]; - + static std::string const empty; return empty; } - + std::string the_rest() const { if (!*this) return {}; - return join(fTokens.begin()+fIndex, + return join(fTokens.begin()+static_cast(fIndex), fTokens.end(), " "); } - + std::string pop() { return std::move(fTokens.at(fIndex++)); } - + bool isParsingArgv() const { return fIsParsingArgv; } - + struct OptionError : std::runtime_error { using runtime_error::runtime_error; }; - + private: std::vector fTokens; size_t fIndex = 0; bool fIsParsingArgv; }; - + // Get all instances of 'T' from the pattern template std::vector flat_filter(Pattern& pattern) { std::vector flattened = pattern.flat([](Pattern const* p) -> bool { - return dynamic_cast(p); + return dynamic_cast(p) != nullptr; }); - + // now, we're guaranteed to have T*'s, so just use static_cast std::vector ret; std::transform(flattened.begin(), flattened.end(), std::back_inserter(ret), [](Pattern* p) { @@ -546,7 +162,7 @@ std::vector flat_filter(Pattern& pattern) { return ret; } -std::vector parse_section(std::string const& name, std::string const& source) { +static std::vector parse_section(std::string const& name, std::string const& source) { // ECMAScript regex only has "?=" for a non-matching lookahead. In order to make sure we always have // a newline to anchor our matching, we have to avoid matching the final newline of each grouping. // Therefore, our regex is adjusted from the docopt Python one to use ?= to match the newlines before @@ -559,7 +175,7 @@ std::vector parse_section(std::string const& name, std::string cons ")", std::regex::icase }; - + std::vector ret; std::for_each(std::sregex_iterator(source.begin(), source.end(), re_section_pattern), std::sregex_iterator(), @@ -567,20 +183,20 @@ std::vector parse_section(std::string const& name, std::string cons { ret.push_back(trim(match[1].str())); }); - + return ret; } -bool is_argument_spec(std::string const& token) { +static bool is_argument_spec(std::string const& token) { if (token.empty()) return false; - + if (token[0]=='<' && token[token.size()-1]=='>') return true; - + if (std::all_of(token.begin(), token.end(), &::isupper)) return true; - + return false; } @@ -593,26 +209,26 @@ std::vector longOptions(I iter, I end) { return ret; } -PatternList parse_long(Tokens& tokens, std::vector