diff --git a/.github/workflows/ccpp.yml b/.github/workflows/ccpp.yml deleted file mode 100644 index 5d151e6..0000000 --- a/.github/workflows/ccpp.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: C/C++ CI of Cli - -on: [push,pull_request] - -jobs: - build: - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - - name: setup dependencies - run: | - sudo apt-get -y update - sudo apt-get -y install libboost-all-dev libasio-dev - - name: run cmake - run: | - mkdir build - cd build - cmake .. -DCLI_BuildTests=ON -DCLI_BuildExamples=ON -DCLI_UseBoostAsio=ON - - name: make - run: | - cd /home/runner/work/cli/cli/build - make all - - name: run tests - run: | - cd /home/runner/work/cli/cli/build/test - ./test_suite diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..f44004b --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,79 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ "master" ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ "master" ] + schedule: + - cron: '20 0 * * 6' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'cpp' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] + # Use only 'java' to analyze code written in Java, Kotlin or both + # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both + # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + + # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + #- name: Autobuild + # uses: github/codeql-action/autobuild@v2 + + # â„šī¸ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + + # If the Autobuild fails above, remove it and uncomment the following three lines. + # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. + + - name: Build + run: | + mkdir build + cd build + cmake .. -DCLI_BuildTests=OFF -DCLI_BuildExamples=ON + make all + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 + with: + category: "/language:${{matrix.language}}" diff --git a/.github/workflows/macos_ci.yml b/.github/workflows/macos_ci.yml new file mode 100644 index 0000000..ee9801f --- /dev/null +++ b/.github/workflows/macos_ci.yml @@ -0,0 +1,66 @@ +name: CI macOS + +on: [push,pull_request] + +jobs: + Test: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + + matrix: + os: [macos-15, macos-26] + compiler: [apple-clang, llvm-18, llvm-19] + standard: [14, 26] + build_type: [Release, Debug] + asio_library: [boost, standalone, none] + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Cpp + uses: aminya/setup-cpp@v1 + with: + compiler: ${{ matrix.compiler }} + vcvarsall: false + + cmake: true + ninja: true + vcpkg: false + ccache: true + clangtidy: true + + cppcheck: true + + gcovr: true + opencppcoverage: true + + - name: setup dependencies + run: | + brew update + brew install boost + brew install asio + + - name: Configure CMake + run: | + cmake -S . -B ./build \ + -G Ninja \ + -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} \ + -DCMAKE_CXX_STANDARD=${{ matrix.standard }} \ + -DCMAKE_CXX_FLAGS="-DBOOST_ERROR_CODE_HEADER_ONLY -DBOOST_SYSTEM_NO_LIB" \ + -DCLI_BuildTests=ON \ + -DCLI_BuildExamples=ON \ + -DCLI_UseBoostAsio=$([ "${{ matrix.asio_library }}" == "boost" ] && echo "ON" || echo "OFF") \ + -DCLI_UseStandaloneAsio=$([ "${{ matrix.asio_library }}" == "standalone" ] && echo "ON" || echo "OFF") + + - name: Build + run: | + cmake --build ./build --config ${{matrix.build_type}} --parallel $(sysctl -n hw.logicalcpu) + + - name: run test + working-directory: ./build + run: | + # cd /home/runner/work/cli/cli/build/test/ + # ./test_suite + ctest -C ${{matrix.build_type}} --output-on-failure diff --git a/.github/workflows/ubuntu_ci.yml b/.github/workflows/ubuntu_ci.yml new file mode 100644 index 0000000..39ede18 --- /dev/null +++ b/.github/workflows/ubuntu_ci.yml @@ -0,0 +1,61 @@ +name: CI linux + +on: [push,pull_request] + +jobs: + Test: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + + matrix: + os: [ubuntu-22.04] + compiler: [llvm-17.0.2, clang++-15, gcc-11] + standard: [14, 23] + build_type: [Release, Debug] + asio_library: [boost, standalone, none] + + steps: + - uses: actions/checkout@v4 + + - name: Setup Cpp + uses: aminya/setup-cpp@v1 + with: + compiler: ${{ matrix.compiler }} + vcvarsall: false + + cmake: true + ninja: true + vcpkg: false + ccache: true + clangtidy: true + + cppcheck: true + + gcovr: true + opencppcoverage: true + + - name: setup dependencies + run: | + sudo apt-get -y update + sudo apt-get -y install -y ninja-build libboost-all-dev libasio-dev + + - name: Configure CMake + run: | + cmake -S . -B ./build \ + -G "Ninja Multi-Config" \ + -DCMAKE_BUILD_TYPE:STRING=${{matrix.build_type}} \ + -DCMAKE_CXX_STANDARD=${{matrix.standard}} \ + -DCLI_BuildTests=ON \ + -DCLI_BuildExamples=ON \ + -DCLI_UseBoostAsio=$([ "${{ matrix.asio_library }}" == "boost" ] && echo "ON" || echo "OFF") \ + -DCLI_UseStandaloneAsio=$([ "${{ matrix.asio_library }}" == "standalone" ] && echo "ON" || echo "OFF") + + - name: Build + run: | + cmake --build ./build --config ${{matrix.build_type}} + + - name: run tests + working-directory: ./build + run: | + ctest -C ${{matrix.build_type}} --output-on-failure diff --git a/.github/workflows/win_ci.yml b/.github/workflows/win_ci.yml new file mode 100644 index 0000000..a253435 --- /dev/null +++ b/.github/workflows/win_ci.yml @@ -0,0 +1,94 @@ +name: CI windows + +on: [push,pull_request] + +jobs: + Test: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + + matrix: + os: [windows-2022, windows-2025] + compiler: [clang++-15, msvc] + standard: [14, 23] + build_type: [Release, Debug] + asio_library: [boost, standalone, none] + + steps: + - uses: actions/checkout@v4 + + - name: Setup Cpp + uses: aminya/setup-cpp@v1 + with: + compiler: ${{ matrix.compiler }} + vcvarsall: true + + cmake: true + ninja: true + vcpkg: false + ccache: true + clangtidy: true + + cppcheck: true + + gcovr: true + opencppcoverage: true + + - name: setup dependencies + run: | + choco install boost-msvc-14.1 --no-progress --limit-output + + Invoke-WebRequest ` + -Uri https://github.com/chriskohlhoff/asio/archive/refs/tags/asio-1-18-2.zip ` + -OutFile asio.zip + + tar -xf asio.zip + move asio-asio-1-18-2 asio + + + - name: Configure CMake + run: | + # Correctly constructs the path for Windows + $AsioPath = Join-Path $env:GITHUB_WORKSPACE "asio/asio/include" + # Converts to forward slash (CMake handles them better) + $AsioPath = $AsioPath -replace "\\", "/" + + echo "ASIO path: $AsioPath" + dir $AsioPath + + $UseBoostAsio = if ("${{ matrix.asio_library }}" -eq "boost") { "ON" } else { "OFF" } + $UseStandaloneAsio = if ("${{ matrix.asio_library }}" -eq "standalone") { "ON" } else { "OFF" } + + cmake -S . -B ./build ` + -DCMAKE_BUILD_TYPE:STRING=${{ matrix.build_type }} ` + -DCMAKE_CXX_STANDARD=${{ matrix.standard }} ` + -DCLI_BuildTests=ON ` + -DCLI_BuildExamples=ON ` + -DCLI_UseBoostAsio=$UseBoostAsio ` + -DCLI_UseStandaloneAsio=$UseStandaloneAsio ` + -DASIO_INCLUDEDIR="$AsioPath" + + - name: Set Boost DLL Path + run: | + # Find the directory of Boost DLLs installed by choco (usually in 'lib' under the root) + $boostPath = "C:\ProgramData\chocolatey\lib\boost-msvc-14.1.*\lib" + $boostDllDir = Get-ChildItem -Path $boostPath -Directory | Select-Object -First 1 + + if ($null -ne $boostDllDir) { + echo "Adding the path of Boost DLLs: $($boostDllDir.FullName)" + # Adds the path to the PATH environment variable for subsequent steps + echo "$($boostDllDir.FullName)" | Out-File -FilePath $env:GITHUB_PATH -Encoding UTF8 -Append + } else { + echo "WARNING: Boost DLL path not found." + } + + - name: Build + run: | + cmake --build ./build --config ${{matrix.build_type}} + +# on windows test_suite throws an exception because it cannot find boost libraries path :-( +# - name: run tests +# working-directory: ./build +# run: | +# ctest -C ${{matrix.build_type}} --output-on-failure diff --git a/.gitignore b/.gitignore index ef6e87a..db31d3c 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ # Cmake Files CMakeLists.txt.user CMakeCache.txt +CMakeSettings.json CMakeFiles CMakeScripts Testing diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ee1fee..f8b7faa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,49 @@ # Changelog +All notable changes to this project will be documented in this file. + +This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## Unreleased + - Fix mutex initialization issue in cli library (pr [#249](https://github.com/daniele77/cli/pull/249)) + +## [2.2.0] - 2024-10-25 + + - Add clear screen command using ctrl + L (issue [#229](https://github.com/daniele77/cli/issues/229)) + - Add history view and execute an history command with bang + id (issue [#235](https://github.com/daniele77/cli/issues/235)) + - Custom handler for wrong commands (issue [#223](https://github.com/daniele77/cli/issues/223)) + - Parent menus does not chain (issue [#234](https://github.com/daniele77/cli/issues/234)) + - Parent menu shortcut (issue [#233](https://github.com/daniele77/cli/issues/233)) + - Integer overflow warning detected by clang++ (issue [#236](https://github.com/daniele77/cli/issues/236)) + - Enable Keyboard Handling in Command Handlers on Linux Platform (issue [#239](https://github.com/daniele77/cli/issues/239)) + - Fix issue with remote session on ARM platforms (issue [#220](https://github.com/daniele77/cli/issues/220)) + +## [2.1.0] - 2023-06-29 + + - Nest namespace rang (issue [#167](https://github.com/daniele77/cli/issues/167)) + - Add ascii value 8 for backspace (issue [#124](https://github.com/daniele77/cli/issues/124)) + - Add check for CMAKE_SKIP_INSTALL_RULES (issue [#160](https://github.com/daniele77/cli/issues/160)) + - Add enter action (issue [#177](https://github.com/daniele77/cli/issues/177) - PR [#180](https://github.com/daniele77/cli/pull/177)) + - Fix missing echo after paste of command (issue [#185](https://github.com/daniele77/cli/issues/185)) + - Fix asio::io_context::work has no member named reset in old asio lib (issue [#188](https://github.com/daniele77/cli/issues/188)) + - Shown in the `complete` and `pluginmanager` examples that the issue #96 was already fixed (issue [#96](https://github.com/daniele77/cli/issues/96)) + - Allow custom prompt for menu (issue [#101](https://github.com/daniele77/cli/issues/101)) + +## [2.0.2] - 2022-08-18 + + - Specify signed for char parameters (issue [#149](https://github.com/daniele77/cli/issues/149)) + - CliSession can call OutStream::UnRegister() when it's already destroyed (issue [#153](https://github.com/daniele77/cli/issues/153)) + +## [2.0.1] - 2022-04-19 + - Add a non-blocking exec method to schedulers (issue [#127](https://github.com/daniele77/cli/issues/127)) + - Add a Menu::Insert method working with free functions as handler + - Cli::cout() returns a class derived from std::ostream + - Fix address sanitizer issue with GenericAsioScheduler dtor + - Fix teardown problem with linux (issue [#132](https://github.com/daniele77/cli/issues/132)) + - Fix teardown problem with windows + - The prompt is no more shown after exit + - Telnet server now works on MobaXTerm ## [2.0.0] - 2021-08-25 @@ -17,7 +58,7 @@ ## [1.2.1] - 2020-08-27 - - With Boost >= 1.74 use TS exectuor by default (issue [#79](https://github.com/daniele77/cli/issues/79)) + - With Boost >= 1.74 use TS executor by default (issue [#79](https://github.com/daniele77/cli/issues/79)) - Standard and custom exception handler for cli commands (issue [#74](https://github.com/daniele77/cli/issues/74)) ## [1.2.0] - 2020-06-27 diff --git a/CMakeLists.txt b/CMakeLists.txt index 777943d..7afe32a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -27,123 +27,153 @@ # DEALINGS IN THE SOFTWARE. ################################################################################ -cmake_minimum_required(VERSION 3.8) +cmake_minimum_required(VERSION 3.8...3.27) -project(cli VERSION 2.0.0 - # DESCRIPTION "A library for interactive command line interfaces in modern C++" - LANGUAGES CXX) +project( + cli + VERSION 2.2.0 + # DESCRIPTION "A library for interactive command line interfaces in modern C++" + LANGUAGES CXX +) include(GNUInstallDirs) +# --------------------------------------------------------------- +# Options +# --------------------------------------------------------------- option(CLI_BuildExamples "Build the examples." OFF) option(CLI_BuildTests "Build the unit tests." OFF) -option(CLI_UseBoostAsio "Use the boost asio library." OFF) -option(CLI_UseStandaloneAsio "Use the standalone asio library." OFF) - - -if(WIN32) - macro(get_WIN32_WINNT version) - if(CMAKE_SYSTEM_VERSION) - set(ver ${CMAKE_SYSTEM_VERSION}) - string(REGEX MATCH "^([0-9]+).([0-9])" ver ${ver}) - string(REGEX MATCH "^([0-9]+)" verMajor ${ver}) - # Check for Windows 10, b/c we'll need to convert to hex 'A'. - if("${verMajor}" MATCHES "10") - set(verMajor "A") - string(REGEX REPLACE "^([0-9]+)" ${verMajor} ver ${ver}) - endif() - # Remove all remaining '.' characters. - string(REPLACE "." "" ver ${ver}) - # Prepend each digit with a zero. - string(REGEX REPLACE "([0-9A-Z])" "0\\1" ver ${ver}) - set(${version} "0x${ver}") - endif() - endmacro() - - get_WIN32_WINNT(ver) - add_definitions(-D_WIN32_WINNT=${ver}) -endif() - -if (CLI_UseBoostAsio) - set(Boost_NO_BOOST_CMAKE ON) - add_definitions( -DBOOST_ALL_NO_LIB ) # for windows - find_package(Boost 1.55 REQUIRED COMPONENTS system) -endif() - -if (CLI_UseStandaloneAsio) - find_path(STANDALONE_ASIO_INCLUDE_PATH NAMES "asio.hpp" HINTS ${ASIO_INCLUDEDIR}) - mark_as_advanced(STANDALONE_ASIO_INCLUDE_PATH) -endif() +option(CLI_UseBoostAsio "Use Boost.Asio library." OFF) +option(CLI_UseStandaloneAsio "Use standalone Asio library." OFF) -find_package(Threads REQUIRED) - -# Add Library +# --------------------------------------------------------------- +# Library +# --------------------------------------------------------------- add_library(cli INTERFACE) -# Add target alias add_library(cli::cli ALIAS cli) target_include_directories(cli INTERFACE - $ - $ + $ + $ ) +find_package(Threads REQUIRED) target_link_libraries(cli INTERFACE Threads::Threads) -if (CLI_UseBoostAsio) - target_link_libraries(cli INTERFACE Boost::system) + +# --------------------------------------------------------------- +# Boost.Asio support +# --------------------------------------------------------------- +if(CLI_UseBoostAsio) + if(POLICY CMP0074) + cmake_policy(SET CMP0074 NEW) # _ROOT + endif() + + if(POLICY CMP0144) + cmake_policy(SET CMP0144 NEW) # uppercase variables like BOOST_ROOT + endif() + + if(POLICY CMP0167) + cmake_policy(SET CMP0167 OLD) # re-enable FindBoost + endif() + + + set(Boost_NO_BOOST_CMAKE ON) + find_package(Boost 1.66 QUIET COMPONENTS system date_time) + add_definitions(-DBOOST_ALL_NO_LIB) # for windows + + if(Boost_FOUND) + message(STATUS "Using compiled Boost libraries on ${CMAKE_SYSTEM_NAME}") + target_link_libraries(cli INTERFACE Boost::system Boost::date_time) + else() + message(STATUS "Boost libraries not found. Falling back to header-only mode.") + target_compile_definitions(cli INTERFACE + BOOST_ERROR_CODE_HEADER_ONLY + BOOST_SYSTEM_NO_LIB + ) + + # find headers (default locations) + find_path(BOOST_INCLUDE_PATH "boost/version.hpp") + if(NOT BOOST_INCLUDE_PATH) + message(FATAL_ERROR "Boost headers not found for header-only fallback!") + endif() + target_include_directories(cli INTERFACE ${BOOST_INCLUDE_PATH}) + endif() + target_compile_definitions(cli INTERFACE BOOST_ASIO_NO_DEPRECATED=1) endif() -if (CLI_UseStandaloneAsio) - add_library(standalone_asio INTERFACE IMPORTED) - target_include_directories(standalone_asio SYSTEM INTERFACE ${STANDALONE_ASIO_INCLUDE_PATH}) - target_link_libraries(standalone_asio INTERFACE Threads::Threads) + +# --------------------------------------------------------------- +# Standalone Asio support +# --------------------------------------------------------------- +if(CLI_UseStandaloneAsio) + find_path(STANDALONE_ASIO_INCLUDE_PATH NAMES "asio.hpp" HINTS ${ASIO_INCLUDEDIR}) + mark_as_advanced(STANDALONE_ASIO_INCLUDE_PATH) + if(NOT STANDALONE_ASIO_INCLUDE_PATH) + message(FATAL_ERROR "Standalone Asio headers not found. Set ASIO_INCLUDEDIR or STANDALONE_ASIO_INCLUDE_PATH.") + endif() + + add_library(standalone_asio INTERFACE IMPORTED) + target_include_directories(standalone_asio SYSTEM INTERFACE ${STANDALONE_ASIO_INCLUDE_PATH}) + target_link_libraries(standalone_asio INTERFACE Threads::Threads) target_link_libraries(cli INTERFACE standalone_asio) +endif() - # alternative way: - # target_include_directories(cli SYSTEM INTERFACE ${STANDALONE_ASIO_INCLUDE_PATH}) +# --------------------------------------------------------------- +# C++ standard +# --------------------------------------------------------------- +if(NOT DEFINED CMAKE_CXX_STANDARD) + set(CMAKE_CXX_STANDARD 14) +endif() +target_compile_features(cli INTERFACE cxx_std_${CMAKE_CXX_STANDARD}) + +# --------------------------------------------------------------- +# Windows specific +# --------------------------------------------------------------- +if(WIN32) + # Set _WIN32_WINNT for Windows 10+ + if(NOT DEFINED _WIN32_WINNT) + add_definitions(-D_WIN32_WINNT=0x0A00) + endif() endif() -target_compile_features(cli INTERFACE cxx_std_14) +# --------------------------------------------------------------- # Examples -if (CLI_BuildExamples) +# --------------------------------------------------------------- +if(CLI_BuildExamples) add_subdirectory(examples) endif() +# --------------------------------------------------------------- # Tests -if (CLI_BuildTests) +# --------------------------------------------------------------- +if(CLI_BuildTests) enable_testing() add_subdirectory(test) endif() +# --------------------------------------------------------------- # Install -install(DIRECTORY include/cli DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) - -include(CMakePackageConfigHelpers) -configure_package_config_file( - "cliConfig.cmake.in" - "${CMAKE_CURRENT_BINARY_DIR}/cliConfig.cmake" - INSTALL_DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/cli" -) - -# Generate pkg-config .pc file -set(PKGCONFIG_INSTALL_DIR - ${CMAKE_INSTALL_DATAROOTDIR}/pkgconfig - CACHE PATH "Installation directory for pkg-config (cli.pc) file" -) -configure_file( - "cli.pc.in" - "cli.pc" - @ONLY -) - -install(TARGETS cli EXPORT cliTargets LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}) -install(EXPORT cliTargets FILE cliTargets.cmake NAMESPACE cli:: DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/cli) - -install( - FILES "${CMAKE_CURRENT_BINARY_DIR}/cliConfig.cmake" - DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/cli" -) -install( - FILES "${CMAKE_CURRENT_BINARY_DIR}/cli.pc" - DESTINATION ${PKGCONFIG_INSTALL_DIR} -) +# --------------------------------------------------------------- +if(NOT CMAKE_SKIP_INSTALL_RULES) + install(DIRECTORY include/cli DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) + + include(CMakePackageConfigHelpers) + configure_package_config_file( + "cliConfig.cmake.in" + "${CMAKE_CURRENT_BINARY_DIR}/cliConfig.cmake" + INSTALL_DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/cli" + ) + + # Generate pkg-config .pc file + set(PKGCONFIG_INSTALL_DIR + ${CMAKE_INSTALL_DATAROOTDIR}/pkgconfig + CACHE PATH "Installation directory for pkg-config (cli.pc) file" + ) + configure_file("cli.pc.in" "cli.pc" @ONLY) + + install(TARGETS cli EXPORT cliTargets LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}) + install(EXPORT cliTargets FILE cliTargets.cmake NAMESPACE cli:: DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/cli) + install(FILES "${CMAKE_CURRENT_BINARY_DIR}/cliConfig.cmake" DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/cli") + install(FILES "${CMAKE_CURRENT_BINARY_DIR}/cli.pc" DESTINATION ${PKGCONFIG_INSTALL_DIR}) +endif() diff --git a/README.md b/README.md index 2018aa2..113096c 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,9 @@ -![C/C++ CI of Cli](https://github.com/daniele77/cli/workflows/C/C++%20CI%20of%20Cli/badge.svg) +[![ubuntu CI of Cli](https://github.com/daniele77/cli/actions/workflows/ubuntu_ci.yml/badge.svg)](https://github.com/daniele77/cli/actions/workflows/ubuntu_ci.yml) +[![win CI of Cli](https://github.com/daniele77/cli/actions/workflows/win_ci.yml/badge.svg)](https://github.com/daniele77/cli/actions/workflows/win_ci.yml) +[![macos CI of Cli](https://github.com/daniele77/cli/actions/workflows/macos_ci.yml/badge.svg)](https://github.com/daniele77/cli/actions/workflows/macos_ci.yml) +[![CodeQL](https://github.com/daniele77/cli/actions/workflows/codeql.yml/badge.svg)](https://github.com/daniele77/cli/actions/workflows/codeql.yml) + +[:heart: Sponsor](https://github.com/sponsors/daniele77) # cli @@ -8,14 +13,6 @@ A cross-platform header only C++14 library for interactive command line interfac ![demo_telnet_session](https://user-images.githubusercontent.com/5451767/51046612-d1dadc00-15c6-11e9-83c2-beadb3593348.gif) -![C/C++ CI of Cli](https://github.com/daniele77/cli/workflows/C/C++%20CI%20of%20Cli/badge.svg) - -[:heart: Sponsor](https://github.com/sponsors/daniele77) - -**IMPORTANT: Breaking API changes!** Version 2.0 of `cli` has made breaking changes in order to add more functionality. -To migrate your application to new `cli` version, see the section -"Async programming and Schedulers" of this file, or the examples that come with the library. - ## Features * Header only @@ -31,12 +28,13 @@ To migrate your application to new `cli` version, see the section * From [GitHub](https://github.com/daniele77/cli/releases) * Using [Vcpkg](https://github.com/Microsoft/vcpkg) +* Using [Conan](https://conan.io/center/recipes/cli) ## Dependencies The library has no dependencies if you don't need remote sessions. -The library depends on asio (either the standalone version or the boost version) +The library depends on asio (either the standalone version or the boost version >= 1.66) *only* to provide telnet server (i.e., remote sessions). ## Installation @@ -61,12 +59,28 @@ or, if you want to specify the installation path: mkdir build && cd build cmake .. -DCMAKE_INSTALL_PREFIX:PATH= make install + +Alternatively, you can use CMake's `FetchContent` module to include CLI library in your project directly. +Add something like this in your `CMakeLists.txt` file: + + include(FetchContent) + FetchContent_Declare( + cli + GIT_REPOSITORY https://github.com/daniele77/cli.git + GIT_TAG v2.1.0 + ) + FetchContent_MakeAvailable(cli) + + add_executable(main-project) + target_link_libraries(main-project PRIVATE cli::cli) + ## Compilation of the examples You can find some examples in the directory "examples". -Each .cpp file corresponds to an executable. You can compile each example by including -cli (and optionally asio/boost header files) +Each .cpp file corresponds to an executable. +Each example can be compiled by including cli +(and optionally asio/boost header files) and linking pthread on linux (and optionally boost system). To compile the examples using cmake, use: @@ -92,7 +106,7 @@ In the same directory you can also find: * Windows nmake files (makefile.noasio.win, makefile.boostasio.win, makefile.standaloneasio.win) * a Visual Studio solution -You can specify boost/asio library path in the following ways: +If needed, you can specify asio library path in the following ways: ### GNU Make @@ -132,9 +146,7 @@ E.g., from a visual studio console, use one of the following commands: ### Visual Studio solution -Currently, the VS solution compiles the examples only with the BOOST dependency. - -Set the environment variable BOOST. Then, open the file +Set the environment variables BOOST and/or ASIO. Then, open the file `cli/examples/examples.sln` ## Compilation of the Doxygen documentation @@ -147,11 +159,43 @@ of the library in this way: ## CLI usage -The cli interpreter can manage correctly sentences using quote (') and double quote ("). -Any character (spaces too) comprises between quotes or double quotes are considered as a single parameter of a command. -The characters ' and " can be used inside a command parameter by escaping them with a backslash. +At the start of your application, the CLI presents a prompt with the +name you provided in the `Cli` constructor. +This indicates you're in the root menu. + +### Navigation + +- **Enter a submenu:** Type the submenu name to enter it. + The prompt will change to reflect the current submenu. +- **Go back to parent menu:** Type the name of the parent menu or `..` to return. +- **Navigate history:** Use up and down arrow keys to navigate through previously entered commands. +- **Exit:** Type `exit` to terminate the CLI application. + +### Commands in any menu + +- `help`: Prints a list of available commands with descriptions. +- `history`: Displays the list of previously entered commands. +- `exit`: Terminates the CLI application. +- **Command execution:** + - **Current menu:** Enter the name of a command available in the current menu to execute it. + - **Submenu (full path):** Specify the complete path (separated by spaces) to a command within a submenu to execute it. + - **History execution:** Use `!` followed by the identifier of a history item to execute that command again. -Some example: +### Autocompletion + +Use the Tab key to get suggestions for completing command or menu names as you type. + +### Screen Clearing + +Press `Ctrl-L` to clear the screen at any time. + +### Parameter parsing + +The CLI interpreter can handle sentences using single quotes (`'`) and double quotes (`"`). +Any character (including spaces) enclosed within quotes is considered a single parameter for a command. +You can use quotes within parameters by escaping them with a backslash (`\`). + +**Examples:** cli> echo "this is a single parameter" this is a single parameter @@ -194,7 +238,7 @@ because they internally use `boost::asio` and `asio`. You should use one of them also if your application uses `asio` in some way. After setting up your application, you must call `Scheduler::Run()` -to enter the scheduler loop. Each comamnd handler of the library +to enter the scheduler loop. Each command handler of the library will execute in the thread that called `Scheduler::Run()`. You can exit the scheduler loop by calling `Scheduler::Stop()` @@ -213,7 +257,7 @@ LoopScheduler scheduler; CliLocalTerminalSession localSession(cli, scheduler); ... // in another thread you can do: -scheduler.Post([](){ cout << "This will be executed in the scheduler thread" << endl; }); +scheduler.Post([](){ cout << "This will be executed in the scheduler thread\n"; }); ... // start the scheduler main loop // it will exit from this method only when scheduler.Stop() is called @@ -253,6 +297,230 @@ ioc.run(); ... ``` +## Adding menus and commands + +You must provide at least a root menu for your cli: + +```C++ +// create a menu (this is the root menu of our cli) +auto rootMenu = make_unique("myprompt"); + +... // fills rootMenu with commands + +// create the cli with the root menu +Cli cli(std::move(rootMenu)); + +``` + +You can add menus to existing menus, to get a hierarchy: + +```C++ +auto rootMenu = make_unique("myprompt"); +auto menuA = make_unique("a_prompt"); +auto menuAA = make_unique("aa_prompt"); +auto menuAB = make_unique("ab_prompt"); +auto menuAC = make_unique("ac_prompt"); +auto menuACA = make_unique("aca_prompt"); +auto menuB = make_unique("b_prompt"); +auto menuBA = make_unique("ba_prompt"); +auto menuBB = make_unique("bb_prompt"); + +menuAC->Insert( std::move(menuACA) ); +menuB->Insert( std::move(menuBA) ); +menuB->Insert( std::move(menuBB) ); +menuA->Insert( std::move(menuAA) ); +menuA->Insert( std::move(menuAB) ); +menuA->Insert( std::move(menuAC) ); +rootMenu->Insert( std::move(menuA) ); +rootMenu->Insert( std::move(menuB) ); +``` + +This results in this tree: + +``` +myprompt + | + +--- a_prompt + | | + | +--- aa_prompt + | | + | +--- ab_prompt + | | + | +--- ac_prompt + | | + | +--- aca_prompt + | + +--- b_prompt + | + +--- ba_prompt + | + +--- bb_prompt + +``` + +Finally, you can add commands to menus, using the `Menu::Insert` method. +The library supports adding commands handler via: +- free functions +- `std::function` +- lambdas + +```C++ + +static void foo(std::ostream& out, int x) { out << x << std::endl; } + +std::function fun(foo); + +... + +myMenu->Insert("free_function", foo); + +myMenu->Insert("std_function", fun); + +myMenu->Insert("lambda", [](std::ostream& out, int x){ out << x << std::endl; } ); + +``` + +There is no limit to the number of parameters that a command handler can take. +They can be basic types and `std::string`s + +```C++ +myMenu->Insert( + "mycmd", + [](std::ostream& out, int a, double b, const std::string& c, bool d, long e) + { + ... + } ); + +myMenu->Insert( + "complex", + [](std::ostream& out, std::complex c) + { + ... + } ); +``` + +Or they can be custom types by overloading the `std::istream::operator>>`: + +```C++ +struct Foo +{ + friend istream & operator >> (istream &in, Foo& p); + int value; +}; + +istream & operator >> (istream& in, Foo& p) +{ + in >> p.value; + return in; +} + +myMenu->Insert( + "foo", + [](std::ostream& out, Foo f) + { + ... + } ); + +``` + +If you need it, you can have a command handlers taking an arbitrary +number of `std::string` parameters: + +```C++ +myMenu->Insert( + "mycmd", + [](std::ostream& out, const std::vector& pars) + { + ... + } ); + +``` + +Please note that in this case your command handler must take *only one* +parameter of type `std::vector`. + +## Enter and exit actions + +You can add an enter action and/or an exit action (for example to print a welcome/goodbye message +every time a user opens/closes a session, even a remote one): + +```C++ +Cli cli(std::move(rootMenu)); +cli.EnterAction( + [&enterActionDone](std::ostream& out) { out << "Welcome\n"; }); +cli.ExitAction( + [&enterActionDone](std::ostream& out) { out << "Goodbye\n"; }); +``` + +## Custom Handler for Unknown Commands + +You can modify the default behavior of the library for cases where +the user enters an unknown command or its parameters do not match: + +```C++ +Cli cli(std::move(rootMenu)); +cli.WrongCommandHandler( + [](std::ostream& out, const std::string& cmd) + { + out << "Unknown command or incorrect parameters: " + << cmd + << ".\n"; + } +); +``` + +## Standard Exception Custom Handler + +You can handle cases where an exception is thrown inside a command handler +by registering a specific handler: + +```C++ +Cli cli(std::move(rootMenu)); +cli.StdExceptionHandler( + [](std::ostream& out, const std::string& cmd, const std::exception& e) + { + out << "Exception caught in CLI handler: " + << e.what() + << " while handling command: " + << cmd + << ".\n"; + } +); +``` + +## Unicode + +`cli` uses the input and output stream objects provided by the standard library (such as `std::cin` and `std::cout`) by default, so currently `cli` does not have effective support for unicode input and output. + +If you want to handle unicode input and output, you need to provide custom i/o unicode aware stream objects derived from `std::istream` or `std::ostream`. + +For example, you can use [boost::nowide](https://github.com/boostorg/nowide) as an alternative to implement UTF-8 aware programming in a out-of-box and cross-platform way: + +```c++ +#include // for boost::nowide::cin and boost::nowide::cout +// other headers... + +cli::Cli app{/*init code*/}; + +// FileSession session{app}; // default + +// now, all parameters is in a UTF-8 encoded std::string +// pass boost::nowide::cin and boost::nowide::cout as parameters(FileSession requires std::istream and std::ostream) +FileSession session{app, boost::nowide::cin, boost::nowide::cout}; + +/*....*/ + +// you can call this command funtion and get UTF-8 encoded input data (p), just use it. +// boost::noide helps us avoid the trouble +// caused by inconsistent default code page and encoding of the console under different platforms. +void a_command_function(std::ostream& os, std::string const& p) { + /* implements */ +} + +``` + +Of course, you can also pass stream objects with other capabilities to achieve more customized input and output functions. + ## License Distributed under the Boost Software License, Version 1.0. diff --git a/doc/doxy/Doxyfile b/doc/doxy/Doxyfile index 0b2a0ae..4ec97f1 100644 --- a/doc/doxy/Doxyfile +++ b/doc/doxy/Doxyfile @@ -29,7 +29,7 @@ #--------------------------------------------------------------------------- PROJECT_NAME = "Interactive CLI" -PROJECT_NUMBER = 2.0 +PROJECT_NUMBER = 2.2 #--------------------------------------------------------------------------- FULL_PATH_NAMES = NO #--------------------------------------------------------------------------- diff --git a/doc/uml/cli.uml b/doc/uml/cli.uml index 19d92ed..e7e7930 100644 --- a/doc/uml/cli.uml +++ b/doc/uml/cli.uml @@ -20,7 +20,7 @@ oEKMHKU6mE+m/a0pz8BhbAAA oEKMHKU6mE+m/a0pz8BhbAAA -63 +69 clMaroon $008CC7FF @@ -641,8 +641,8 @@ $00ADD39C 708 668 -118 -82 +140 +98 k0xzFwLELEuiW5O6+cJeKgAA @@ -663,7 +663,6 @@ k0xzFwLELEuiW5O6+cJeKgAA -False k0xzFwLELEuiW5O6+cJeKgAA @@ -671,14 +670,14 @@ clMaroon $00ADD39C 660 -800 -129 -46 +820 +153 +59 ZrKIbc2h9UiX3VckmuMUxAAA 1 -InputHandler +CommandProcessor False @@ -694,7 +693,6 @@ ZrKIbc2h9UiX3VckmuMUxAAA -False ZrKIbc2h9UiX3VckmuMUxAAA @@ -702,7 +700,7 @@ clMaroon $00B9FFFF lsRectilinear -680,800;680,553 +680,820;680,553 Lcf1JQtSikCLkJLFQhEojAAA cAYfTB+XvkKH0Itv3nkBcAAA 7Ef3BQms7EmzQhyYeH2k3gAA @@ -1161,7 +1159,7 @@ clMaroon $00B9FFFF lsRectilinear -788,812;872,812;872,708;984,708 +812,840;872,840;872,708;984,708 kBMx9okl1E+/6otAoCk92wAA RfCB3p//B06jtFIxJ3bt8QAA 7Ef3BQms7EmzQhyYeH2k3gAA @@ -1447,7 +1445,7 @@ clMaroon $00B9FFFF lsRectilinear -788,832;848,832;848,948;1132,948 +812,868;848,868;848,948;1132,948 PouNuHuoq0urswc/Z8Vz2wAA HxW4qmceQ0K97zXdfiP5HAAA 7Ef3BQms7EmzQhyYeH2k3gAA @@ -1532,7 +1530,7 @@ clMaroon $00B9FFFF lsRectilinear -756,749;756,800 +756,765;756,820 ivhaI9tpsEyvRvOTq9QsUQAA 7Ef3BQms7EmzQhyYeH2k3gAA wezzfHtUP0CYmR9LnpXbfgAA @@ -1760,7 +1758,7 @@ clMaroon $00B9FFFF lsRectilinear -680,845;680,948 +680,878;680,948 Xuoq/PcdGUi7vE2IM10R+AAA jpsEsVgK9k6jXu/jiH9jSgAA 7Ef3BQms7EmzQhyYeH2k3gAA @@ -2787,8 +2785,8 @@ clMaroon $00CEB694 -764 -488 +724 +504 96 56 s2Iwv+rItUqlsLKHfr2iuwAA @@ -2819,7 +2817,7 @@ clMaroon $00B9FFFF lsRectilinear -792,668;792,543 +764,668;764,559 IlzfnO06D0OvY+GUMXxxAQAA qtX+GLbeIEKZf8LW43rK8wAA wezzfHtUP0CYmR9LnpXbfgAA @@ -2904,7 +2902,7 @@ clMaroon $00B9FFFF lsRectilinear -764,524;692,524 +724,524;692,524 RgNqxlPBVEmE427D4nszbwAA cAYfTB+XvkKH0Itv3nkBcAAA qtX+GLbeIEKZf8LW43rK8wAA @@ -2985,6 +2983,180 @@ VEgY36xa4U2KMtQJCuBQUwAA + +clMaroon +$00ADD39C +936 +432 +80 +56 +J7ZlA8kB00KGuPg6YlBfzAAA + + +1 +LocalScreen + + +False + + +False + + + +J7ZlA8kB00KGuPg6YlBfzAAA + + +J7ZlA8kB00KGuPg6YlBfzAAA + + +False +J7ZlA8kB00KGuPg6YlBfzAAA + + + +clMaroon +$00ADD39C +936 +496 +86 +56 +5YW72IpFpkyfjBJV0jX9SAAA + + +1 +TelnetScreen + + +False + + +False + + + +5YW72IpFpkyfjBJV0jX9SAAA + + +5YW72IpFpkyfjBJV0jX9SAAA + + +False +5YW72IpFpkyfjBJV0jX9SAAA + + + +clMaroon +$00ADD39C +932 +560 +91 +56 +6fgV4QJsJk6Jjpc1CxIwIAAA + + +1 +WinScreen + + +False + + +False + + + +6fgV4QJsJk6Jjpc1CxIwIAAA + + +6fgV4QJsJk6Jjpc1CxIwIAAA + + +False +6fgV4QJsJk6Jjpc1CxIwIAAA + + + +clMaroon +$00B9FFFF +lsRectilinear +847,684;952,684;952,615 +NrNJq/Wiw0m42wrCXstKTAAA +/HuKWa4wMkqkSbZxvyRL/AAA +wezzfHtUP0CYmR9LnpXbfgAA + +False +1,5707963267949 +15 +NrNJq/Wiw0m42wrCXstKTAAA + + +False +1,5707963267949 +30 +NrNJq/Wiw0m42wrCXstKTAAA + + +False +-1,5707963267949 +15 +NrNJq/Wiw0m42wrCXstKTAAA + + + +clMaroon +$00B9FFFF +lsRectilinear +847,676;900,676;900,516;936,516 +7O5BCh3Sg0iP6Vt8/PxZ2AAA +640IYTOgRkuoUkMurgkBTwAA +wezzfHtUP0CYmR9LnpXbfgAA + +False +1,5707963267949 +15 +7O5BCh3Sg0iP6Vt8/PxZ2AAA + + +False +1,5707963267949 +30 +7O5BCh3Sg0iP6Vt8/PxZ2AAA + + +False +-1,5707963267949 +15 +7O5BCh3Sg0iP6Vt8/PxZ2AAA + + + +clMaroon +$00B9FFFF +lsRectilinear +847,668;884,668;884,456;936,456 +GUId6shvmUaYZUsbjBkRoQAA +O6idmn/2F0G2QGsbJviViwAA +wezzfHtUP0CYmR9LnpXbfgAA + +False +1,5707963267949 +15 +GUId6shvmUaYZUsbjBkRoQAA + + +False +1,5707963267949 +30 +GUId6shvmUaYZUsbjBkRoQAA + + +False +-1,5707963267949 +15 +GUId6shvmUaYZUsbjBkRoQAA + + @@ -3715,7 +3887,7 @@ -69 +75 CliSession NLtrxd0pREa5saxYXXatOgAA @@ -4035,36 +4207,45 @@ GSfST7YktkCZZRTCzfeZfQAA z3eJDl7ylUSYd2pkk49hEgAA C5aHiT2+QUigN7/IKHMmxwAA -3 - -Register -k0xzFwLELEuiW5O6+cJeKgAA -1 - -handler -PJCV2TnlnEO4f0j5mPwHqAAA - +3 +NrNJq/Wiw0m42wrCXstKTAAA +7O5BCh3Sg0iP6Vt8/PxZ2AAA +GUId6shvmUaYZUsbjBkRoQAA +1 + +SCREEN +k0xzFwLELEuiW5O6+cJeKgAA - +3 + SetLine k0xzFwLELEuiW5O6+cJeKgAA - + ResetCursor k0xzFwLELEuiW5O6+cJeKgAA + +KeyPressed +k0xzFwLELEuiW5O6+cJeKgAA + 2 odPh4KR73EOr2nyHtHHmdAAA 9b7b8YY8r0SlDrXweD49RQAA -InputHandler +CommandProcessor NLtrxd0pREa5saxYXXatOgAA 4 7Ef3BQms7EmzQhyYeH2k3gAA fun+n57M4Ei1awtQKPu8bQAA oKEtOL8xpUyEfcOk8B7e0wAA BR5RgDC9U0yt7g8GGX/PXQAA +1 + +SCREEN +ZrKIbc2h9UiX3VckmuMUxAAA + 5 qYli0bcUo06T8FGe7cOpYQAA K0X4B2lQtkSZs9vWQqU97gAA @@ -5121,8 +5302,8 @@ k0xzFwLELEuiW5O6+cJeKgAA 4 DInaM1xL3EKpPRbQpqSLNQAA -10N81H03+Uu5MvalmW/WHwAA -K+DZQTBi80Oh/lYi9qc2JQAA +K+DZQTBi80Oh/lYi9qc2JQAA +10N81H03+Uu5MvalmW/WHwAA KlDlNHGXVkG1+TiODEhk4gAA @@ -5130,8 +5311,8 @@ s2Iwv+rItUqlsLKHfr2iuwAA 4 0VSXW3LqnEKW9/1MVUT8vQAA -HFiqWFrizEinscWbyku6DgAA -BjRaQOgdbUK9NsIEvHAcFwAA +BjRaQOgdbUK9NsIEvHAcFwAA +HFiqWFrizEinscWbyku6DgAA zkzsJNWYnEu8UAGktaYq5gAA @@ -5148,8 +5329,8 @@ s2Iwv+rItUqlsLKHfr2iuwAA 4 EH7z3/T88E2Fsqc8iipgiQAA -yjxcYUIl9EGueWh803dXhwAA -PVxU6qCGYE2X6Fnqy8wCigAA +PVxU6qCGYE2X6Fnqy8wCigAA +yjxcYUIl9EGueWh803dXhwAA 1npp+CihGki5SxEbgnqASQAA @@ -5158,11 +5339,92 @@ TRhgwOteGUCFcmtcX/iZ1wAA 4 oa26Yq7qhUGpdSN9IpHFlAAA -MdhKVdpkVke/ou3lM9pa9wAA -5JLMNItIqkaYrfkV4QdFfgAA +5JLMNItIqkaYrfkV4QdFfgAA +MdhKVdpkVke/ou3lM9pa9wAA E2q7kEICT0qDkzrdEugyAAAA + +LocalScreen +NLtrxd0pREa5saxYXXatOgAA +4 +O6idmn/2F0G2QGsbJviViwAA +zl4Olroye0qEkPw75NCrLgAA +fZEWtYhB2EStzpWh1QhE2QAA +5luvnapvEkirk5WImWIK3AAA +1 +GUId6shvmUaYZUsbjBkRoQAA +1 + +Clear +skClassifier +J7ZlA8kB00KGuPg6YlBfzAAA + + + +TelnetScreen +NLtrxd0pREa5saxYXXatOgAA +4 +640IYTOgRkuoUkMurgkBTwAA +E0tXV9S+4UK5M+KlcmoRaAAA +Lhxar+Z2XUq+kueLH+1wKgAA +4TLEU5rgkUWUUk+8+91KSAAA +1 +7O5BCh3Sg0iP6Vt8/PxZ2AAA +1 + +Clear +skClassifier +5YW72IpFpkyfjBJV0jX9SAAA + + + +WinScreen +NLtrxd0pREa5saxYXXatOgAA +4 +/HuKWa4wMkqkSbZxvyRL/AAA +XlWLuiDsvkipj3u4XPwmHwAA +H8hCvopcOU6zLTTS5/1q0QAA +7rhlVhsQ5UmbyIFS1GB4rgAA +1 +NrNJq/Wiw0m42wrCXstKTAAA +1 + +Clear +skClassifier +6fgV4QJsJk6Jjpc1CxIwIAAA + + + +NLtrxd0pREa5saxYXXatOgAA +k0xzFwLELEuiW5O6+cJeKgAA +6fgV4QJsJk6Jjpc1CxIwIAAA +4 +1bvTXiZdm0mMYUVeJD91AAAA +cPbslDDB1068fB9MUgvgFAAA +WZwo7RJddU+sJZBPMgngKgAA +YENya15VHUuO45hKZyW5iAAA + + +NLtrxd0pREa5saxYXXatOgAA +k0xzFwLELEuiW5O6+cJeKgAA +5YW72IpFpkyfjBJV0jX9SAAA +4 +5DdHxHDe102ZYKiqOEaeLQAA +rij/j4fEG0GS4FYF2OylBAAA +Dc0ds2ZMKkOSD1p27gxOhAAA +TgFVhLrxvkm6FmRsLzZAvwAA + + +NLtrxd0pREa5saxYXXatOgAA +k0xzFwLELEuiW5O6+cJeKgAA +J7ZlA8kB00KGuPg6YlBfzAAA +4 +pAaqPcHMxk6i8jyHJpnWLAAA +NVc8m7iV10+ndca3IgwoDAAA +iop1AkA+40Od0vbXCR9sJwAA +A7CFufc2qEe97xcmCbrbHAAA + diff --git a/doc/uml/implementation.png b/doc/uml/implementation.png index 25edd63..a4105e9 100644 Binary files a/doc/uml/implementation.png and b/doc/uml/implementation.png differ diff --git a/examples/asyncsession.cpp b/examples/asyncsession.cpp index f082c49..181065f 100644 --- a/examples/asyncsession.cpp +++ b/examples/asyncsession.cpp @@ -125,11 +125,11 @@ int main() } catch (const std::exception& e) { - cerr << "Exception caugth in main: " << e.what() << endl; + cerr << "Exception caught in main: " << e.what() << '\n'; } catch (...) { - cerr << "Unknown exception caugth in main." << endl; + cerr << "Unknown exception caught in main.\n"; } return -1; } diff --git a/examples/boostasio_nonowner_iocontext.cpp b/examples/boostasio_nonowner_iocontext.cpp index 0d57466..2ae996b 100644 --- a/examples/boostasio_nonowner_iocontext.cpp +++ b/examples/boostasio_nonowner_iocontext.cpp @@ -114,11 +114,8 @@ int main() { // main application that creates an asio io_context and uses it IoContext iocontext; - boost::asio::deadline_timer timer( - iocontext, - boost::posix_time::seconds(5) - ); - timer.async_wait([](const boost::system::error_code&){ cout << "Timer expired!\n"; }); + boost::asio::steady_timer timer(iocontext, std::chrono::seconds(5)); + timer.async_wait([](const error_code&){ cout << "Timer expired!\n"; }); // cli setup UserInterface interface(iocontext); @@ -135,11 +132,11 @@ int main() } catch (const std::exception& e) { - cerr << "Exception caugth in main: " << e.what() << endl; + cerr << "Exception caught in main: " << e.what() << '\n'; } catch (...) { - cerr << "Unknown exception caugth in main." << endl; + cerr << "Unknown exception caught in main.\n"; } return -1; } diff --git a/examples/boostasio_nonowner_iocontext_vsprj/boostasio_nonowner_iocontext.vcxproj b/examples/boostasio_nonowner_iocontext_vsprj/boostasio_nonowner_iocontext.vcxproj index 6b3aa1b..e5ceeff 100644 --- a/examples/boostasio_nonowner_iocontext_vsprj/boostasio_nonowner_iocontext.vcxproj +++ b/examples/boostasio_nonowner_iocontext_vsprj/boostasio_nonowner_iocontext.vcxproj @@ -32,26 +32,26 @@ Application true - v142 + v143 Unicode Application false - v142 + v143 true Unicode Application true - v142 + v143 Unicode Application false - v142 + v143 true Unicode diff --git a/examples/complete.cpp b/examples/complete.cpp index 3bd04dd..4f31199 100644 --- a/examples/complete.cpp +++ b/examples/complete.cpp @@ -58,9 +58,35 @@ #include #include // std::copy +#include + using namespace cli; using namespace std; +// a free function to be used as handler +static void foo(std::ostream& out, int x) { out << x << std::endl; } + +// a custom struct to be used as a user-defined parameter type +struct Bar +{ + string to_string() const { return std::to_string(value); } + friend istream & operator >> (istream &in, Bar& p); + int value; +}; + +istream & operator >> (istream& in, Bar& p) +{ + in >> p.value; + return in; +} + +// needed only for generic help, you can omit this +namespace cli { template <> struct TypeDesc { static const char* Name() { return ""; } }; } + +// needed only for generic help, you can omit this +namespace cli { template <> struct TypeDesc> { static const char* Name() { return ""; } }; } + + int main() { try @@ -71,6 +97,11 @@ int main() // setup cli auto rootMenu = make_unique("cli"); + + rootMenu->Insert( + "free_function", + foo, + "Call a free function that echoes the parameter passed" ); rootMenu->Insert( "hello", [](std::ostream& out){ out << "Hello, world\n"; }, @@ -145,6 +176,14 @@ int main() out << "\n"; }, "Alphabetically sort a list of words" ); + rootMenu->Insert( + "bar", + [](std::ostream& out, Bar x){ out << "You entered bar: " << x.to_string() << "\n"; }, + "Custom type" ); + rootMenu->Insert( + "complex", + [](std::ostream& out, std::complex x){ out << "You entered complex : " << x << "\n"; }, + "Print a complex number" ); colorCmd = rootMenu->Insert( "color", [&](std::ostream& out) @@ -165,16 +204,21 @@ int main() nocolorCmd.Disable(); }, "Disable colors in the cli" ); + nocolorCmd.Disable(); // start w/o colors, so we disable this command rootMenu->Insert( "removecmds", [&](std::ostream&) { colorCmd.Remove(); nocolorCmd.Remove(); - } - ); + }, + "Remove both color and nocolor commands from the menu" ); - auto subMenu = make_unique("sub"); + // a submenu + // first parameter is the command to enter the submenu + // second parameter (optional) is the description of the menu in the help + // third parameter (optional) is the prompt of the menu (default is the name of the command) + auto subMenu = make_unique("sub", "Enter a submenu", "submenu"); subMenu->Insert( "hello", [](std::ostream& out){ out << "Hello, submenu world\n"; }, @@ -184,7 +228,7 @@ int main() [](std::ostream& out){ out << "This is a sample!\n"; }, "Print a demo string" ); - auto subSubMenu = make_unique("subsub"); + auto subSubMenu = make_unique("subsub", "Enter a submenu of second level"); subSubMenu->Insert( "hello", [](std::ostream& out){ out << "Hello, subsubmenu world\n"; }, @@ -204,9 +248,18 @@ int main() cli.StdExceptionHandler( [](std::ostream& out, const std::string& cmd, const std::exception& e) { - out << "Exception caught in cli handler: " + out << "Exception caught in CLI handler: " << e.what() - << " handling command: " + << " while handling command: " + << cmd + << ".\n"; + } + ); + // custom handler for unknown commands + cli.WrongCommandHandler( + [](std::ostream& out, const std::string& cmd) + { + out << "Unknown command or incorrect parameters: " << cmd << ".\n"; } @@ -235,11 +288,11 @@ int main() } catch (const std::exception& e) { - cerr << "Exception caugth in main: " << e.what() << endl; + cerr << "Exception caught in main: " << e.what() << '\n'; } catch (...) { - cerr << "Unknown exception caugth in main." << endl; + cerr << "Unknown exception caught in main.\n"; } return -1; } diff --git a/examples/complete_vsprj/complete.vcxproj b/examples/complete_vsprj/complete.vcxproj index 7009a87..a1c2ebd 100644 --- a/examples/complete_vsprj/complete.vcxproj +++ b/examples/complete_vsprj/complete.vcxproj @@ -28,26 +28,26 @@ Application true - v142 + v143 MultiByte Application false - v142 + v143 true MultiByte Application true - v142 + v143 MultiByte Application false - v142 + v143 true MultiByte diff --git a/examples/filesession_vsprj/filesession.vcxproj b/examples/filesession_vsprj/filesession.vcxproj index bd54902..77e11d8 100644 --- a/examples/filesession_vsprj/filesession.vcxproj +++ b/examples/filesession_vsprj/filesession.vcxproj @@ -28,26 +28,26 @@ Application true - v142 + v143 MultiByte Application false - v142 + v143 true MultiByte Application true - v142 + v143 MultiByte Application false - v142 + v143 true MultiByte diff --git a/examples/pluginmanager.cpp b/examples/pluginmanager.cpp index 7e5ff31..b7f6b11 100644 --- a/examples/pluginmanager.cpp +++ b/examples/pluginmanager.cpp @@ -98,7 +98,7 @@ class PluginContainer { auto p = PluginRegistry::Instance().Create(plugin, menu); if (p) - plugins.push_back(move(p)); + plugins.push_back(std::move(p)); } void Unload(const string& plugin) { @@ -164,7 +164,7 @@ class Arithmetic : public RegisteredPlugin public: explicit Arithmetic(Menu* menu) { - auto subMenu = make_unique(Name()); + auto subMenu = make_unique(Name(), "Enter the " + Name() + " plugin menu"); subMenu->Insert( "add", {"first_term", "second_term"}, [](std::ostream& out, int x, int y) @@ -187,7 +187,7 @@ class Arithmetic : public RegisteredPlugin }, "Print the result of a subtraction" ); - menuHandler = menu->Insert(move(subMenu)); + menuHandler = menu->Insert(std::move(subMenu)); } ~Arithmetic() override { @@ -208,7 +208,7 @@ class Strings : public RegisteredPlugin public: explicit Strings(Menu* menu) { - auto subMenu = make_unique(Name()); + auto subMenu = make_unique(Name(), "Enter the " + Name() + " plugin menu"); subMenu->Insert( "reverse", {"string_to_revert"}, [](std::ostream& out, const string& arg) @@ -237,7 +237,7 @@ class Strings : public RegisteredPlugin out << "\n"; }, "Alphabetically sort a list of words" ); - menuHandler = menu->Insert(move(subMenu)); + menuHandler = menu->Insert(std::move(subMenu)); } ~Strings() override { @@ -334,11 +334,11 @@ int main() } catch (const std::exception& e) { - cerr << "Exception caugth in main: " << e.what() << endl; + cerr << "Exception caught in main: " << e.what() << '\n'; } catch (...) { - cerr << "Unknown exception caugth in main." << endl; + cerr << "Unknown exception caught in main.\n"; } return -1; } diff --git a/examples/pluginmanager_vsprj/pluginmanager.vcxproj b/examples/pluginmanager_vsprj/pluginmanager.vcxproj index f226999..d645983 100644 --- a/examples/pluginmanager_vsprj/pluginmanager.vcxproj +++ b/examples/pluginmanager_vsprj/pluginmanager.vcxproj @@ -28,26 +28,26 @@ Application true - v142 + v143 MultiByte Application false - v142 + v143 true MultiByte Application true - v142 + v143 MultiByte Application false - v142 + v143 true MultiByte diff --git a/examples/simplelocalsession_vsprj/simplelocalsession.vcxproj b/examples/simplelocalsession_vsprj/simplelocalsession.vcxproj index aa5651c..1586dfc 100644 --- a/examples/simplelocalsession_vsprj/simplelocalsession.vcxproj +++ b/examples/simplelocalsession_vsprj/simplelocalsession.vcxproj @@ -28,26 +28,26 @@ Application true - v142 + v143 MultiByte Application false - v142 + v143 true MultiByte Application true - v142 + v143 MultiByte Application false - v142 + v143 true MultiByte diff --git a/examples/standaloneasio_nonowner_iocontext.cpp b/examples/standaloneasio_nonowner_iocontext.cpp index 5a51fb6..ae37c02 100644 --- a/examples/standaloneasio_nonowner_iocontext.cpp +++ b/examples/standaloneasio_nonowner_iocontext.cpp @@ -127,11 +127,11 @@ int main() } catch (const std::exception& e) { - cerr << "Exception caugth in main: " << e.what() << endl; + cerr << "Exception caught in main: " << e.what() << '\n'; } catch (...) { - cerr << "Unknown exception caugth in main." << endl; + cerr << "Unknown exception caught in main.\n"; } return -1; } diff --git a/examples/standaloneasio_nonowner_iocontext_vsprj/standaloneasio_nonowner_iocontext.vcxproj b/examples/standaloneasio_nonowner_iocontext_vsprj/standaloneasio_nonowner_iocontext.vcxproj index 4c58c4e..2695e29 100644 --- a/examples/standaloneasio_nonowner_iocontext_vsprj/standaloneasio_nonowner_iocontext.vcxproj +++ b/examples/standaloneasio_nonowner_iocontext_vsprj/standaloneasio_nonowner_iocontext.vcxproj @@ -32,26 +32,26 @@ Application true - v142 + v143 Unicode Application false - v142 + v143 true Unicode Application true - v142 + v143 Unicode Application false - v142 + v143 true Unicode diff --git a/include/cli/cli.h b/include/cli/cli.h index 95b3aa4..437e838 100644 --- a/include/cli/cli.h +++ b/include/cli/cli.h @@ -72,54 +72,48 @@ namespace cli // ******************************************************************** - // forward declarations - class Menu; - class CliSession; - - - class Cli + // this class provides a global output stream + class OutStream : public std::basic_ostream, public std::streambuf { - - // inner class to provide a global output stream - class OutStream + public: + OutStream() : std::basic_ostream(this) { - public: - template - OutStream& operator << (const T& msg) - { - for (auto out: ostreams) - *out << msg; - return *this; - } + } - // this is the type of std::cout - using CoutType = std::basic_ostream >; - // this is the function signature of std::endl - using StandardEndLine = CoutType &(*)(CoutType &); + // std::streambuf overrides + std::streamsize xsputn(const char* s, std::streamsize n) override + { + for (auto os: ostreams) + os->rdbuf()->sputn(s, n); + return n; + } + int overflow(int c) override + { + for (auto os: ostreams) + *os << static_cast(c); + return c; + } - // takes << std::endl - OutStream& operator << (StandardEndLine manip) - { - for (auto out: ostreams) - manip(*out); - return *this; - } + void Register(std::ostream& o) + { + ostreams.push_back(&o); + } + void UnRegister(std::ostream& o) + { + ostreams.erase(std::remove(ostreams.begin(), ostreams.end(), &o), ostreams.end()); + } - private: - friend class Cli; + private: - void Register(std::ostream& o) - { - ostreams.push_back(&o); - } - void UnRegister(std::ostream& o) - { - ostreams.erase(std::remove(ostreams.begin(), ostreams.end(), &o), ostreams.end()); - } + std::vector ostreams; + }; + + // forward declarations + class Menu; + class CliSession; - std::vector ostreams; - }; - // end inner class + class Cli + { public: ~Cli() = default; @@ -130,25 +124,11 @@ namespace cli Cli(Cli&&) = default; Cli& operator = (Cli&&) = default; - /// \deprecated Use the @c Cli::Cli(std::unique_ptr,std::unique_ptr) - /// overload version and the method @c Cli::ExitAction instead - [[deprecated("Use the other overload of Cli constructor and the method Cli::ExitAction instead")]] - explicit Cli( - std::unique_ptr&& _rootMenu, - std::function< void(std::ostream&)> _exitAction, - std::unique_ptr&& historyStorage = std::make_unique() - ) : - globalHistoryStorage(std::move(historyStorage)), - rootMenu(std::move(_rootMenu)), - exitAction(std::move(_exitAction)) - { - } - /** * @brief Construct a new Cli object having a given root menu that contains the first level commands available. * * @param _rootMenu is the @c Menu containing the first level commands available to the user. - * @param historyStorage is the policy for the storage of the cli commands history. You must pass an istance of + * @param historyStorage is the policy for the storage of the cli commands history. You must pass an instance of * a class derived from @c HistoryStorage. The library provides these policies: * - @c VolatileHistoryStorage * - @c FileHistoryStorage it's a persistent history. I.e., the command history is preserved after your application @@ -156,12 +136,14 @@ namespace cli * * However, you can develop your own, just derive a class from @c HistoryStorage . */ - Cli(std::unique_ptr _rootMenu, std::unique_ptr historyStorage = std::make_unique()) : - globalHistoryStorage(std::move(historyStorage)), - rootMenu(std::move(_rootMenu)), - exitAction{} - { - } + Cli(std::unique_ptr _rootMenu, std::unique_ptr historyStorage = std::make_unique()); + + /** + * @brief Add a global enter action that is called every time a session (local or remote) is established. + * + * @param action the function to be called when a session exits, taking a @c std::ostream& parameter to write on that session console. + */ + void EnterAction(const std::function< void(std::ostream&)>& action) { enterAction = action; } /** * @brief Add a global exit action that is called every time a session (local or remote) gets the "exit" command. @@ -174,14 +156,26 @@ namespace cli * @brief Add an handler that will be called when a @c std::exception (or derived) is thrown inside a command handler. * If an exception handler is not set, the exception will be logget on the session output stream. * - * @param handler the function to be called when an exception is thrown, taking a @c std::ostream& parameter to write on that session console - * and the exception thrown. + * @param handler the function to be called when an exception is thrown, taking a @c std::ostream& parameter to write on that session console, + * the command entered and the exception thrown. */ void StdExceptionHandler(const std::function< void(std::ostream&, const std::string& cmd, const std::exception&) >& handler) { exceptionHandler = handler; } + /** + * @brief Add an handler that will be called when the user enter a wrong command (not existing command or having wrong parameters). + * If an handler is not set, the library will print the message "wrong command" on the console. + * + * @param handler the function to be called when the user enter a wrong command, taking a @c std::ostream& parameter to write on that session console + * and the command entered. + */ + void WrongCommandHandler(const std::function< void(std::ostream&, const std::string& cmd) >& handler) + { + wrongCmdHandler = handler; + } + /** * @brief Get a global out stream object that can be used to print on every session currently connected (local and remote) * @@ -189,19 +183,30 @@ namespace cli */ static OutStream& cout() { - static OutStream s; - return s; + return *CoutPtr(); } private: friend class CliSession; + static std::shared_ptr CoutPtr() + { + static std::shared_ptr s = std::make_shared(); + return s; + } + Menu* RootMenu() { return rootMenu.get(); } - void ExitAction( std::ostream& out ) + void EnterAction(std::ostream& out) { - if ( exitAction ) - exitAction( out ); + if (enterAction) + enterAction(out); + } + + void ExitAction(std::ostream& out) + { + if (exitAction) + exitAction(out); } void StdExceptionHandler(std::ostream& out, const std::string& cmd, const std::exception& e) @@ -212,9 +217,13 @@ namespace cli out << e.what() << '\n'; } - static void Register(std::ostream& o) { cout().Register(o); } - - static void UnRegister(std::ostream& o) { cout().UnRegister(o); } + void WrongCommandHandler(std::ostream& out, const std::string& cmd) + { + if (wrongCmdHandler) + wrongCmdHandler(out, cmd); + else + out << "wrong command: " << cmd << '\n'; + } void StoreCommands(const std::vector& cmds) { @@ -229,8 +238,10 @@ namespace cli private: std::unique_ptr globalHistoryStorage; std::unique_ptr rootMenu; // just to keep it alive + std::function enterAction; std::function exitAction; std::function exceptionHandler; + std::function wrongCmdHandler; }; // ******************************************************************** @@ -297,7 +308,7 @@ namespace cli { public: CliSession(Cli& _cli, std::ostream& _out, std::size_t historySize = 100); - virtual ~CliSession() noexcept { Cli::UnRegister(out); } + virtual ~CliSession() noexcept; // disable value semantics CliSession(const CliSession&) = delete; @@ -306,7 +317,7 @@ namespace cli CliSession(CliSession&&) = delete; CliSession& operator = (CliSession&&) = delete; - void Feed( const std::string& cmd ); + void Feed(const std::string& cmd); void Prompt(); @@ -316,6 +327,14 @@ namespace cli void Help() const; + void Enter() + { + cli.EnterAction(out); + + if (enterAction) + enterAction(out); + } + void Exit() { exitAction(out); @@ -323,6 +342,13 @@ namespace cli auto cmds = history.GetCommands(); cli.StoreCommands(cmds); + + exit = true; // prevent the prompt to be shown + } + + void EnterAction(const std::function& action) + { + enterAction = action; } void ExitAction(const std::function& action) @@ -332,6 +358,13 @@ namespace cli void ShowHistory() const { history.Show(out); } + void ExecFromHistory(unsigned index) + { + history.ForgetLatest(); + const auto cmd = history.At(index); + Feed(cmd); + } + std::string PreviousCmd(const std::string& line) { return history.Previous(line); @@ -347,11 +380,14 @@ namespace cli private: Cli& cli; + std::shared_ptr coutPtr; Menu* current; std::unique_ptr globalScopeMenu; std::ostream& out; - std::function< void(std::ostream&)> exitAction = []( std::ostream& ){}; + std::function< void(std::ostream&)> enterAction = []( std::ostream& ) noexcept {}; + std::function< void(std::ostream&)> exitAction = []( std::ostream& ) noexcept {}; detail::History history; + bool exit{ false }; // to prevent the prompt after exit command }; // ******************************************************************** @@ -418,10 +454,17 @@ namespace cli Menu() : Command({}), parent(nullptr), description(), cmds(std::make_shared()) {} - explicit Menu(const std::string& _name, std::string desc = "(menu)") : - Command(_name), parent(nullptr), description(std::move(desc)), cmds(std::make_shared()) + explicit Menu(const std::string& _name, std::string desc = "(menu)", const std::string& _prompt="") : + Command(_name), + parent(nullptr), + description(std::move(desc)), + prompt(_prompt.empty() ? _name : _prompt), + cmds(std::make_shared()) {} + template + CmdHandler Insert(const std::string& cmdName, R (*f)(std::ostream&, Args...), const std::string& help, const std::vector& parDesc={}); + template CmdHandler Insert(const std::string& cmdName, F f, const std::string& help = "", const std::vector& parDesc={}) { @@ -455,24 +498,12 @@ namespace cli bool Exec(const std::vector& cmdLine, CliSession& session) override { - if (!IsEnabled()) - return false; - if (cmdLine[0] == Name()) - { - if (cmdLine.size() == 1) - { - session.Current(this); - return true; - } - else - { - // check also for subcommands - std::vector subCmdLine(cmdLine.begin()+1, cmdLine.end()); - for (auto& cmd: *cmds) - if (cmd->Exec( subCmdLine, session )) return true; - } - } - return false; + return HandleCommand({Name()}, cmdLine, session); + } + + bool ExecParent(const std::vector& cmdLine, CliSession& session) + { + return HandleCommand({Name(), ParentShortcut()}, cmdLine, session); } bool ScanCmds(const std::vector& cmdLine, CliSession& session) @@ -482,12 +513,13 @@ namespace cli for (auto& cmd: *cmds) if (cmd->Exec(cmdLine, session)) return true; - return (parent && parent->Exec(cmdLine, session)); + assert(!cmdLine.empty()); + return (parent && parent->ExecParent(cmdLine, session)); } std::string Prompt() const { - return Name(); + return prompt; } void MainHelp(std::ostream& out) @@ -514,37 +546,142 @@ namespace cli auto result = cli::GetCompletions(cmds, currentLine); if (parent != nullptr) { - auto c = parent->GetCompletionRecursive(currentLine); + auto c = parent->GetCompletionWithParent(currentLine); result.insert(result.end(), std::make_move_iterator(c.begin()), std::make_move_iterator(c.end())); } return result; } - // returns: - // - the completion of this menu command - // - the recursive completions of the subcommands + /** + * Retrieves completion suggestions for the user input recursively. + * + * This function checks if the user input starts with the current command's name. If it + * does, it extracts the remaining part of the input and retrieves suggestions: + * - From subcommands using their `GetCompletionRecursive` function. + * - From the parent command (if available) using its `GetCompletionRecursiveFull` function. + * - (Optional) You can customize the behavior for empty lines to provide top-level commands. + * + * If the input doesn't start with the command name, it delegates to the base class's + * `Command::GetCompletionRecursive` function for handling generic commands. + * + * @param line The user's input string (potentially incomplete command). + * @return A vector containing suggested completions for the user input. + */ std::vector GetCompletionRecursive(const std::string& line) const override { if (line.rfind(Name(), 0) == 0) // line starts_with Name() { - auto rest = line; - rest.erase(0, Name().size()); - // trim_left(rest); - rest.erase(rest.begin(), std::find_if(rest.begin(), rest.end(), [](int ch) { return !std::isspace(ch); })); - std::vector result; - for (const auto& cmd: *cmds) - { - auto cs = cmd->GetCompletionRecursive(rest); - for (const auto& c: cs) - result.push_back(Name() + ' ' + c); // concat submenu with command - } - return result; + return GetCompletionRecursiveHelper(line, Name()); } + return Command::GetCompletionRecursive(line); } private: + /** + * Retrieves completion suggestions for the user input recursively, including the parent command. + * + * This function is similar to `GetCompletionRecursive` but explicitly includes + * completions from the parent command (if available) using its `GetCompletionRecursiveFull` function. + * This allows navigation within the command hierarchy using "..". + * + * The rest of the functionality remains the same as `GetCompletionRecursive`. + * + * @param line The user's input string (potentially incomplete command). + * @return A vector containing suggested completions for the user input. + */ + std::vector GetCompletionWithParent(const std::string& line) const + { + if (line.rfind(Name(), 0) == 0) // line starts_with Name() + { + return GetCompletionRecursiveHelper(line, Name()); + } + + if (line.rfind(ParentShortcut(), 0) == 0) // line starts_with .. + { + return GetCompletionRecursiveHelper(line, ParentShortcut()); + } + + return Command::GetCompletionRecursive(line); + } + + std::vector GetCompletionRecursiveHelper(const std::string& line, const std::string& prefix) const + { + auto rest = line; + rest.erase(0, prefix.size()); + // trim_left(rest); + rest.erase(rest.begin(), std::find_if(rest.begin(), rest.end(), [](int ch) { return !std::isspace(ch); })); + std::vector result; + for (const auto& cmd: *cmds) + { + auto cs = cmd->GetCompletionRecursive(rest); + for (const auto& c: cs) + result.push_back(prefix + ' ' + c); // concat submenu with command + } + if (parent != nullptr) + { + auto cs = parent->GetCompletionWithParent(rest); + for (const auto& c: cs) + result.push_back(prefix + ' ' + c); // concat submenu with command + } + return result; + } + + /** + * Handles a command from the user input. + * + * This function checks if the first element of the `cmdLine` vector matches any of the + * valid commands listed in `cmdNames`. If it does, it performs the following actions: + * - If the `cmdLine` is of length 1 (only the command itself), it sets the current + * session to this object (`session.Current(this)`) and returns true. + * - If the `cmdLine` is longer (includes subcommands), it iterates through registered + * subcommands (`*cmds`) and calls their `Exec` function with the subcommand arguments + * (`subCmdLine`) and the session (`session`). If any subcommand successfully handles + * the command, it returns true. + * - If no subcommand handles the command and a parent object (`parent`) is set, it + * calls the parent's `ExecParent` function with the subcommand arguments and the + * session. + * + * The function returns false if the command is not found, not enabled, or no subcommand or + * parent can handle it. + * + * @param cmdNames - List of valid command names. + * @param cmdLine - User input divided into tokens (command and arguments). + * @param session - Reference to the current CliSession object. + * @return true if the command is handled successfully, false otherwise. + */ + bool HandleCommand(const std::vector& cmdNames, const std::vector& cmdLine, CliSession& session) + { + if (!IsEnabled()) + return false; + + assert(!cmdLine.empty()); + + if (std::find(cmdNames.begin(), cmdNames.end(), cmdLine[0]) != cmdNames.end()) + { + if (cmdLine.size() == 1) + { + session.Current(this); + return true; + } + else + { + // check also for subcommands + std::vector subCmdLine(cmdLine.begin()+1, cmdLine.end()); + for (auto& cmd: *cmds) + if (cmd->Exec( subCmdLine, session )) return true; + return (parent && parent->ExecParent(subCmdLine, session)); + } + } + return false; + } + + static std::string ParentShortcut() + { + return ".."; + } + template CmdHandler Insert(const std::string& name, const std::string& help, const std::vector& parDesc, F& f, R (F::*)(std::ostream& out, Args...) const); @@ -556,6 +693,7 @@ namespace cli Menu* parent{ nullptr }; const std::string description; + const std::string prompt; // using shared_ptr instead of unique_ptr to get a weak_ptr // for the CmdHandler::Descriptor using Cmds = std::vector>; @@ -564,27 +702,27 @@ namespace cli // ******************************************************************** - template + template struct Select; - template - struct Select + template + struct Select { - template + template static void Exec(const F& f, InputIt first, InputIt last) { assert( first != last ); assert( std::distance(first, last) == 1+sizeof...(Args) ); const P p = detail::from_string::type>(*first); auto g = [&](auto ... pars){ f(p, pars...); }; - Select::Exec(g, std::next(first), last); + Select::Exec(g, std::next(first), last); } }; - template - struct Select + template <> + struct Select<> { - template + template static void Exec(const F& f, InputIt first, InputIt last) { // silence the unused warning in release mode when assert is disabled @@ -646,7 +784,7 @@ namespace cli try { auto g = [&](auto ... pars){ func( session.OutStream(), pars... ); }; - Select::Exec(g, std::next(cmdLine.begin()), cmdLine.end()); + Select::Exec(g, std::next(cmdLine.begin()), cmdLine.end()); } catch (std::bad_cast&) { @@ -726,10 +864,21 @@ namespace cli // ******************************************************************** + // Cli implementation + + inline Cli::Cli(std::unique_ptr _rootMenu, std::unique_ptr historyStorage) : + globalHistoryStorage{std::move(historyStorage)}, + rootMenu{std::move(_rootMenu)}, + enterAction{}, + exitAction{} + { + } + // CliSession implementation inline CliSession::CliSession(Cli& _cli, std::ostream& _out, std::size_t historySize) : cli(_cli), + coutPtr(Cli::CoutPtr()), current(cli.RootMenu()), globalScopeMenu(std::make_unique< Menu >()), out(_out), @@ -737,7 +886,7 @@ namespace cli { history.LoadCommands(cli.GetCommands()); - Cli::Register(out); + coutPtr->Register(out); globalScopeMenu->Insert( "help", [this](std::ostream&){ Help(); }, @@ -748,13 +897,16 @@ namespace cli [this](std::ostream&){ Exit(); }, "Quit the session" ); -#ifdef CLI_HISTORY_CMD globalScopeMenu->Insert( "history", [this](std::ostream&){ ShowHistory(); }, "Show the history" ); -#endif + globalScopeMenu->Insert( + "!", {"history entry index"}, + [this](std::ostream&, unsigned cmdIndex){ ExecFromHistory(cmdIndex); }, + "Exec a command by index in the history" + ); } inline void CliSession::Feed(const std::string& cmd) @@ -767,15 +919,14 @@ namespace cli try { - // global cmds check bool found = globalScopeMenu->ScanCmds(strs, *this); // root menu recursive cmds check if (!found) found = current->ScanCmds(strs, *this); - if (!found) // error msg if not found - out << "wrong command: " << cmd << '\n'; + if (!found) // wrong command handler if not found + cli.WrongCommandHandler(out, cmd); } catch(const std::exception& e) { @@ -791,6 +942,7 @@ namespace cli inline void CliSession::Prompt() { + if (exit) return; out << beforePrompt << current->Prompt() << afterPrompt @@ -821,8 +973,20 @@ namespace cli return v1; } + inline CliSession::~CliSession() noexcept + { + coutPtr->UnRegister(out); + } + // Menu implementation + template + CmdHandler Menu::Insert(const std::string& cmdName, R (*f)(std::ostream&, Args...), const std::string& help, const std::vector& parDesc) + { + using F = R (*)(std::ostream&, Args...); + return Insert(std::make_unique>(cmdName, f, help, parDesc)); + } + template CmdHandler Menu::Insert(const std::string& cmdName, const std::string& help, const std::vector& parDesc, F& f, R (F::*)(std::ostream& out, Args...) const ) { diff --git a/include/cli/clifilesession.h b/include/cli/clifilesession.h index 2416487..68177e5 100644 --- a/include/cli/clifilesession.h +++ b/include/cli/clifilesession.h @@ -50,7 +50,7 @@ class CliFileSession : public CliSession if (!_in.good()) throw std::invalid_argument("istream invalid"); if (!_out.good()) throw std::invalid_argument("ostream invalid"); ExitAction( - [this](std::ostream&) + [this](std::ostream&) noexcept { exit = true; } @@ -58,6 +58,8 @@ class CliFileSession : public CliSession } void Start() { + Enter(); + while(!exit) { Prompt(); diff --git a/include/cli/clilocalsession.h b/include/cli/clilocalsession.h index 241a8b8..3265a19 100644 --- a/include/cli/clilocalsession.h +++ b/include/cli/clilocalsession.h @@ -31,9 +31,10 @@ #define CLI_CLILOCALSESSION_H #include // std::ostream -#include "detail/keyboard.h" -#include "detail/inputhandler.h" +#include "detail/commandprocessor.h" #include "cli.h" // CliSession +#include "detail/keyboard.h" +#include "detail/screen.h" namespace cli { @@ -64,12 +65,13 @@ class CliLocalTerminalSession : public CliSession kb(scheduler), ih(*this, kb) { + Enter(); Prompt(); } private: detail::Keyboard kb; - detail::InputHandler ih; + detail::CommandProcessor ih; }; using CliLocalSession = CliLocalTerminalSession; diff --git a/include/cli/colorprofile.h b/include/cli/colorprofile.h index 0d9714b..518ce57 100644 --- a/include/cli/colorprofile.h +++ b/include/cli/colorprofile.h @@ -47,25 +47,25 @@ enum AfterInput { afterInput }; inline std::ostream& operator<<(std::ostream& os, BeforePrompt) { - if ( Color() ) { os << rang::control::forceColor << rang::fg::green << rang::style::bold; } + if ( Color() ) { os << detail::rang::control::forceColor << detail::rang::fg::green << detail::rang::style::bold; } return os; } inline std::ostream& operator<<(std::ostream& os, AfterPrompt) { - os << rang::style::reset; + os << detail::rang::style::reset; return os; } inline std::ostream& operator<<(std::ostream& os, BeforeInput) { - if ( Color() ) { os << rang::control::forceColor << rang::fgB::gray; } + if ( Color() ) { os << detail::rang::control::forceColor << detail::rang::fgB::gray; } return os; } inline std::ostream& operator<<(std::ostream& os, AfterInput) { - os << rang::style::reset; + os << detail::rang::style::reset; return os; } diff --git a/include/cli/detail/inputhandler.h b/include/cli/detail/commandprocessor.h similarity index 72% rename from include/cli/detail/inputhandler.h rename to include/cli/detail/commandprocessor.h index ac9170e..ea8f6a0 100644 --- a/include/cli/detail/inputhandler.h +++ b/include/cli/detail/commandprocessor.h @@ -27,8 +27,8 @@ * DEALINGS IN THE SOFTWARE. ******************************************************************************/ -#ifndef CLI_DETAIL_INPUTHANDLER_H_ -#define CLI_DETAIL_INPUTHANDLER_H_ +#ifndef CLI_DETAIL_COMMANDPROCESSOR_H_ +#define CLI_DETAIL_COMMANDPROCESSOR_H_ #include #include @@ -42,24 +42,52 @@ namespace cli namespace detail { -class InputHandler +/** + * @class CommandProcessor + * @brief This class handles user input and processes commands in a CLI session. + * + * The CommandProcessor class is responsible for handling user input from an InputDevice, + * processing the input into commands, and executing these commands in a CLI session. + * It also provides functionality for command history navigation and command auto-completion. + * + * @tparam SCREEN The type of the terminal screen. + */ +template +class CommandProcessor { public: - InputHandler(CliSession& _session, InputDevice& kb) : + /** + * @brief Construct a new CommandProcessor object. + * + * @param _session The CLI session to be managed. + * @param _kb The input device to be used. + */ + CommandProcessor(CliSession& _session, InputDevice& _kb) : session(_session), - terminal(session.OutStream()) + terminal(session.OutStream()), + kb(_kb) { kb.Register( [this](auto key){ this->Keypressed(key); } ); } private: + /** + * @brief Handle a keypress event. + * + * @param k The key that was pressed. + */ void Keypressed(std::pair k) { const std::pair s = terminal.Keypressed(k); NewCommand(s); } + /** + * @brief Process a new command. + * + * @param s The symbol and string representing the command. + */ void NewCommand(const std::pair& s) { switch (s.first) @@ -75,8 +103,10 @@ class InputHandler } case Symbol::command: { + kb.DeactivateInput(); session.Feed(s.second); session.Prompt(); + kb.ActivateInput(); break; } case Symbol::down: @@ -118,16 +148,27 @@ class InputHandler terminal.SetLine( line ); break; } + case Symbol::clear: + { + const auto currentLine = terminal.GetLine(); + terminal.Clear(); + session.Prompt(); + terminal.ResetCursor(); + terminal.SetLine(currentLine); + break; + } } } CliSession& session; - Terminal terminal; + Terminal terminal; + InputDevice& kb; }; } // namespace detail } // namespace cli -#endif // CLI_DETAIL_INPUTHANDLER_H_ +#endif // CLI_DETAIL_COMMANDPROCESSOR_H_ + diff --git a/include/cli/detail/fromstring.h b/include/cli/detail/fromstring.h index e66fd85..606b3e2 100644 --- a/include/cli/detail/fromstring.h +++ b/include/cli/detail/fromstring.h @@ -131,7 +131,8 @@ inline T signed_from_string(std::string s) { s = s.substr(1); const U val = unsigned_digits_from_string(s); - if ( val > static_cast( - std::numeric_limits::min() ) ) + auto min = std::numeric_limits::min(); // this to avoid overflow warnings. Please NOTE: const auto produces warning! + if ( val > static_cast( - min ) ) throw bad_conversion(); return (- static_cast(val)); } diff --git a/include/cli/detail/genericasioremotecli.h b/include/cli/detail/genericasioremotecli.h index 5d6488c..f8f7760 100644 --- a/include/cli/detail/genericasioremotecli.h +++ b/include/cli/detail/genericasioremotecli.h @@ -32,10 +32,11 @@ #include #include "../cli.h" -#include "inputhandler.h" +#include "commandprocessor.h" #include "server.h" #include "inputdevice.h" #include "genericasioscheduler.h" +#include "screen.h" namespace cli { @@ -77,14 +78,15 @@ class TelnetSession : public Session // https://www.ibm.com/support/knowledgecenter/SSLTBW_1.13.0/com.ibm.zos.r13.hald001/telcmds.htm - std::string iacDoLineMode{ "\x0FF\x0FD\x022", 3 }; + static const std::string iacDoLineMode{ "\x0FF\x0FD\x022", 3 }; this -> OutStream() << iacDoLineMode << std::flush; - std::string iacSbLineMode0IacSe{ "\x0FF\x0FA\x022\x001\x000\x0FF\x0F0", 7 }; + static const std::string iacSbLineMode0IacSe{ "\x0FF\x0FA\x022\x001\x000\x0FF\x0F0", 7 }; this -> OutStream() << iacSbLineMode0IacSe << std::flush; - std::string iacWillEcho{ "\x0FF\x0FB\x001", 3 }; + static const std::string iacWillEcho{ "\x0FF\x0FB\x001", 3 }; this -> OutStream() << iacWillEcho << std::flush; + /* constexpr char IAC = '\x0FF'; // 255 constexpr char DO = '\x0FD'; // 253 @@ -166,6 +168,12 @@ class TelnetSession : public Session } #else + /* + See + https://www.iana.org/assignments/telnet-options/telnet-options.xhtml + for a list of telnet options + */ + enum { SE = '\x0F0', // End of subnegotiation parameters. @@ -200,10 +208,16 @@ class TelnetSession : public Session // or confirmation that you are no // longer expecting the other party // to perform, the indicated option. - IAC = '\x0FF' // Data Byte 255. + IAC = '\x0FF', // Data Byte 255. + + _ECHO = '\x001', + SUPPRESS_GO_AHEAD = '\x003', + TERMINAL_TYPE = '\x018', + NEGOTIATE_ABOUT_WIN_SIZE = '\x01F', + TERMINAL_SPEED = '\x020', + NEW_ENV_OPTION = '\x027' }; - void OnDataReceived(const std::string& _data) override { for (char c: _data) @@ -217,15 +231,10 @@ class TelnetSession : public Session if (escape) { if (c == IAC) - { Data(c); - escape = false; - } else - { Command(c); - escape = false; - } + escape = false; } else { @@ -244,22 +253,22 @@ class TelnetSession : public Session Output(c); break; case State::sub: - Sub(c); + RxSub(c); break; case State::wait_will: - Will(c); + RxWill(c); state = State::data; break; case State::wait_wont: - Wont(c); + RxWont(c); state = State::data; break; case State::wait_do: - Do(c); + RxDo(c); state = State::data; break; case State::wait_dont: - Dont(c); + RxDont(c); state = State::data; break; } @@ -295,7 +304,7 @@ class TelnetSession : public Session if (state == State::sub) state = State::data; else - std::cout << "ERROR: received SE when not in sub state" << std::endl; + std::cerr << "ERROR: received SE when not in sub state\n"; break; case DataMark: // ? case Break: // ? @@ -333,15 +342,24 @@ class TelnetSession : public Session } } - void Will(char c) + void RxWill(char c) { #ifdef CLI_TELNET_TRACE std::cout << "will " << static_cast(c) << std::endl; - #else - (void)c; #endif + switch(c) + { + case SUPPRESS_GO_AHEAD: + SendIacCmd(WILL, SUPPRESS_GO_AHEAD); + break; + case NEGOTIATE_ABOUT_WIN_SIZE: + SendIacCmd(DO, NEGOTIATE_ABOUT_WIN_SIZE); + break; + default: + SendIacCmd(DONT, c); + }; } - void Wont(char c) + void RxWont(char c) { #ifdef CLI_TELNET_TRACE std::cout << "wont " << static_cast(c) << std::endl; @@ -349,15 +367,24 @@ class TelnetSession : public Session (void)c; #endif } - void Do(char c) + void RxDo(char c) { #ifdef CLI_TELNET_TRACE std::cout << "do " << static_cast(c) << std::endl; - #else - (void)c; #endif + switch (c) + { + case _ECHO: + SendIacCmd(DO, _ECHO); + break; + case SUPPRESS_GO_AHEAD: + SendIacCmd(WILL, SUPPRESS_GO_AHEAD); + break; + default: + SendIacCmd(WONT, c); + }; } - void Dont(char c) + void RxDont(char c) { #ifdef CLI_TELNET_TRACE std::cout << "dont " << static_cast(c) << std::endl; @@ -365,7 +392,7 @@ class TelnetSession : public Session (void)c; #endif } - void Sub(char c) + void RxSub(char c) { #ifdef CLI_TELNET_TRACE std::cout << "sub: " << static_cast(c) << std::endl; @@ -373,6 +400,13 @@ class TelnetSession : public Session (void)c; #endif } + void SendIacCmd(char action, char op) + { + std::string answer("\x0FF\x000\x000", 3); + answer[1] = action; + answer[2] = op; + this -> OutStream() << answer << std::flush; + } protected: virtual void Output(char c) { @@ -394,36 +428,8 @@ class TelnetSession : public Session { if (std::isprint(c)) std::cout << c << std::endl; else std::cout << "0x" << std::hex << static_cast(c) << std::dec << std::endl; -/* - switch ( c ) - { - case 0: break; - case '\n': - case '\r': - { - // trim trailing spaces - std::size_t endpos = buffer.find_last_not_of(" \t\r\n"); - if( std::string::npos != endpos ) buffer = buffer.substr( 0, endpos+1 ); - if ( cliSession.Feed( buffer ) ) cliSession.Prompt(); - else Disconnect(); - - buffer.clear(); - break; - } - default: - Echo( c ); - buffer += c; - } -*/ } -/* - void Echo( char c ) - { - this -> OutStream() << c << std::flush; - } -*/ std::string buffer; - //bool waitAck = false; }; template @@ -458,23 +464,26 @@ class CliTelnetSession : public InputDevice, public TelnetSession, public CliSes void OnConnect() override { TelnetSession::OnConnect(); + Enter(); Prompt(); } - void Output(char c) override + void Output(char c) override // NB: C++ does not specify wether char is signed or unsigned { switch(step) { case Step::_1: switch( c ) { - case EOF: + case static_cast(EOF): case 4: // EOT Notify(std::make_pair(KeyType::eof,' ')); break; case 8: // Backspace case 127: // Backspace or Delete Notify(std::make_pair(KeyType::backspace, ' ')); break; //case 10: Notify(std::make_pair(KeyType::ret,' ')); break; + case 12: // ctrl+L + Notify(std::make_pair(KeyType::clear, ' ')); break; case 27: step = Step::_2; break; // symbol case 13: step = Step::wait_0; break; // wait for 0 (ENTER key) default: // ascii @@ -535,7 +544,7 @@ class CliTelnetSession : public InputDevice, public TelnetSession, public CliSes enum class Step { _1, _2, _3, _4, wait_0 }; Step step = Step::_1; - InputHandler poll; + CommandProcessor poll; }; template @@ -554,6 +563,12 @@ class CliGenericTelnetServer : public Server cli(_cli), historySize(_historySize) {} + + void EnterAction(std::function< void(std::ostream&)> action) + { + enterAction = action; + } + void ExitAction( std::function< void(std::ostream&)> action ) { exitAction = action; @@ -565,6 +580,7 @@ class CliGenericTelnetServer : public Server private: Scheduler& scheduler; Cli& cli; + std::function< void(std::ostream&)> enterAction; std::function< void(std::ostream&)> exitAction; std::size_t historySize; }; diff --git a/include/cli/detail/genericasioscheduler.h b/include/cli/detail/genericasioscheduler.h index 1cc2687..7fb2979 100644 --- a/include/cli/detail/genericasioscheduler.h +++ b/include/cli/detail/genericasioscheduler.h @@ -55,7 +55,14 @@ class GenericAsioScheduler : public Scheduler explicit GenericAsioScheduler(ContextType& _context) : context{&_context}, executor{*context} {} - ~GenericAsioScheduler() override { if (owned) delete context; } + ~GenericAsioScheduler() override + { + if (owned) + { + work.reset(); // work uses context, so it must be deleted before context + delete context; + } + } // non copyable GenericAsioScheduler(const GenericAsioScheduler&) = delete; @@ -64,7 +71,7 @@ class GenericAsioScheduler : public Scheduler void Stop() { if (work) - work->reset(); + ASIOLIB::Reset(*work); context->stop(); } diff --git a/include/cli/detail/history.h b/include/cli/detail/history.h index 528109b..e8eb401 100644 --- a/include/cli/detail/history.h +++ b/include/cli/detail/history.h @@ -111,9 +111,13 @@ class History // Show the whole history on the given ostream void Show(std::ostream& out) const { + const auto size = buffer.size(); out << '\n'; - for (auto& item: buffer) - out << item << '\n'; + for (std::size_t i = 0; i < size; ++i) + { + const auto j = size-1-i; + out << IndexToId(j) << '\t' << buffer[j] << '\n'; + } out << '\n' << std::flush; } @@ -142,21 +146,54 @@ class History return result; } + std::string At(std::size_t id) const + { + std::size_t index = IdToIndex(id); + assert(index < buffer.size()); + return buffer[index]; + } + + void ForgetLatest() + { + assert(!buffer.empty()); + buffer.pop_front(); + } + private: + // oldest has index = size-1 and id = idOfOldest + // newest has index = 0 and id = idOfOldest + size-1 + std::size_t IndexToId(std::size_t index) const + { + if (index > idOfOldest+buffer.size()-1) + throw std::out_of_range("Index not found in history"); + return idOfOldest + buffer.size() - 1 - index; + } + + std::size_t IdToIndex(std::size_t id) const + { + if (id < idOfOldest || id > idOfOldest+buffer.size()-1) + throw std::out_of_range("Index not found in history"); + return idOfOldest + buffer.size() - 1 - id; + } + void Insert(const std::string& item) { - buffer.push_front(item); + buffer.emplace_front(item); if (buffer.size() > maxSize) + { buffer.pop_back(); + ++idOfOldest; + } } const std::size_t maxSize; - std::deque buffer; + std::deque buffer; // buffer[0] is the newest element std::size_t current = 0; std::size_t commands = 0; // number of commands issued enum class Mode { inserting, browsing }; Mode mode = Mode::inserting; + std::size_t idOfOldest = 0; // the id of the oldest element kept in the history }; } // namespace detail diff --git a/include/cli/detail/inputdevice.h b/include/cli/detail/inputdevice.h index 5655ed8..62a369f 100644 --- a/include/cli/detail/inputdevice.h +++ b/include/cli/detail/inputdevice.h @@ -39,7 +39,7 @@ namespace cli namespace detail { -enum class KeyType { ascii, up, down, left, right, backspace, canc, home, end, ret, eof, ignored }; +enum class KeyType { ascii, up, down, left, right, backspace, canc, home, end, ret, eof, ignored, clear, }; class InputDevice { @@ -48,6 +48,8 @@ class InputDevice explicit InputDevice(Scheduler& _scheduler) : scheduler(_scheduler) {} virtual ~InputDevice() = default; + virtual void ActivateInput() {} + virtual void DeactivateInput() {} template void Register(H&& h) { handler = std::forward(h); } diff --git a/include/cli/detail/keyboard.h b/include/cli/detail/keyboard.h index e300d25..6d71be2 100644 --- a/include/cli/detail/keyboard.h +++ b/include/cli/detail/keyboard.h @@ -30,29 +30,17 @@ #ifndef CLI_DETAIL_KEYBOARD_H_ #define CLI_DETAIL_KEYBOARD_H_ -#if defined(__unix__) || defined(__unix) || defined(__linux__) - #define OS_LINUX -#elif defined(WIN32) || defined(_WIN32) || defined(_WIN64) - #define OS_WIN -#elif defined(__APPLE__) || defined(__MACH__) - #define OS_MAC -#else - #error "Platform not supported (yet)." -#endif +#include "platform.h" -#if defined(OS_LINUX) || defined(OS_MAC) +#if defined(CLI_OS_LINUX) || defined(CLI_OS_MAC) #include "linuxkeyboard.h" namespace cli { namespace detail { using Keyboard = LinuxKeyboard; } } -#elif defined(OS_WIN) +#elif defined(CLI_OS_WIN) #include "winkeyboard.h" namespace cli { namespace detail { using Keyboard = WinKeyboard; } } #else #error "Platform not supported (yet)." #endif -#undef OS_LINUX -#undef OS_WIN -#undef OS_MAC - #endif // CLI_DETAIL_KEYBOARD_H_ diff --git a/include/cli/detail/linuxkeyboard.h b/include/cli/detail/linuxkeyboard.h index ec9c568..17b40e1 100644 --- a/include/cli/detail/linuxkeyboard.h +++ b/include/cli/detail/linuxkeyboard.h @@ -32,12 +32,14 @@ #include #include -#include +#include #include #include #include - +#include +#include +#include #include "inputdevice.h" @@ -46,53 +48,150 @@ namespace cli namespace detail { +class InputSource +{ +public: + + InputSource() + { + int pipes[2]; + if (pipe(pipes) == 0) + { + shutdownPipe = pipes[1]; // we store the write end + readPipe = pipes[0]; // ... and the read end + } + } + + void WaitKbHit() + { + fd_set rfds; + FD_ZERO(&rfds); + FD_SET(STDIN_FILENO, &rfds); + FD_SET(readPipe, &rfds); + + while (select(readPipe + 1, &rfds, nullptr, nullptr, nullptr) == 0); + + if (FD_ISSET(readPipe, &rfds)) // stop called + { + close(readPipe); + throw std::runtime_error("InputSource stop"); + } + + if (FD_ISSET(STDIN_FILENO, &rfds)) // char from stdinput + { + return; + } + + // cannot reach this point + assert(false); + } + + void Stop() + { + auto unused = write(shutdownPipe, " ", 1); + unused = close(shutdownPipe); + static_cast(unused); // silence unused warn + shutdownPipe = -1; + } + +private: + int shutdownPipe; + int readPipe; +}; + +// + class LinuxKeyboard : public InputDevice { public: explicit LinuxKeyboard(Scheduler& _scheduler) : - InputDevice(_scheduler) + InputDevice(_scheduler), + enabled(false), + servant( [this]() noexcept { Read(); } ) { - ToManualMode(); - servant = std::make_unique( [this](){ Read(); } ); - servant->detach(); + ActivateInputImpl(); } ~LinuxKeyboard() override { - run = false; ToStandardMode(); + is.Stop(); + servant.join(); + } + void ActivateInput() override + { + ActivateInputImpl(); + } + void DeactivateInput() override + { + ToStandardMode(); + std::lock_guard lock(mtx); + enabled = false; } + + private: -private: + // we need a private non virtual method to call from the constructor + void ActivateInputImpl() + { + ToManualMode(); + std::lock_guard lock(mtx); + enabled = true; + cv.notify_one(); + } - void Read() + void Read() noexcept { - while ( run ) + try + { + while (true) + { + { + std::unique_lock lock(mtx); + cv.wait(lock, [this]{ return enabled; }); // release mtx, suspend thread execution until enabled becomes true + } + auto k = Get(); + Notify(k); + } + } + catch(const std::exception&) { - auto k = Get(); - Notify(k); + // nothing to do: just exit } } + char GetChar() + { + char buffer = 0; + auto unused = read(0, &buffer, 1); + static_cast(unused); // silence unused warn + return buffer; + } + std::pair Get() { - int ch = std::getchar(); - switch( ch ) + is.WaitKbHit(); + + auto ch = GetChar(); + switch(ch) { case EOF: case 4: // EOT return std::make_pair(KeyType::eof,' '); break; - case 127: return std::make_pair(KeyType::backspace,' '); break; + case 127: + case 8: + return std::make_pair(KeyType::backspace,' '); break; case 10: return std::make_pair(KeyType::ret,' '); break; + case 12: return std::make_pair(KeyType::clear, ' '); break; case 27: // symbol - ch = std::getchar(); + ch = GetChar(); if ( ch == 91 ) // arrow keys { - ch = std::getchar(); + ch = GetChar(); switch( ch ) { case 51: - ch = std::getchar(); + ch = GetChar(); if ( ch == 126 ) return std::make_pair(KeyType::canc,' '); else return std::make_pair(KeyType::ignored,' '); break; @@ -120,21 +219,24 @@ class LinuxKeyboard : public InputDevice constexpr tcflag_t ICANON_FLAG = ICANON; constexpr tcflag_t ECHO_FLAG = ECHO; - tcgetattr( STDIN_FILENO, &oldt ); + tcgetattr(STDIN_FILENO, &oldt); newt = oldt; newt.c_lflag &= ~( ICANON_FLAG | ECHO_FLAG ); - tcsetattr( STDIN_FILENO, TCSANOW, &newt ); + tcsetattr(STDIN_FILENO, TCSANOW, &newt); } void ToStandardMode() { - tcsetattr( STDIN_FILENO, TCSANOW, &oldt ); + tcsetattr(STDIN_FILENO, TCSANOW, &oldt); } + bool enabled; termios oldt; termios newt; - std::atomic run{ true }; - std::unique_ptr servant; + InputSource is; + std::mutex mtx; + std::condition_variable cv; + std::thread servant; }; } // namespace detail @@ -142,3 +244,4 @@ class LinuxKeyboard : public InputDevice #endif // CLI_DETAIL_LINUXKEYBOARD_H_ + diff --git a/include/cli/detail/newboostasiolib.h b/include/cli/detail/newboostasiolib.h index 1d1f3ee..2d7b4e6 100644 --- a/include/cli/detail/newboostasiolib.h +++ b/include/cli/detail/newboostasiolib.h @@ -75,6 +75,11 @@ class NewBoostAsioLib return boost::asio::make_work_guard(context); } + static void Reset(WorkGuard& wg) + { + wg.reset(); + } + }; } // namespace detail diff --git a/include/cli/detail/newstandaloneasiolib.h b/include/cli/detail/newstandaloneasiolib.h index 92f3d23..54d5aa7 100644 --- a/include/cli/detail/newstandaloneasiolib.h +++ b/include/cli/detail/newstandaloneasiolib.h @@ -74,6 +74,12 @@ class NewStandaloneAsioLib { return asio::make_work_guard(context); } + + static void Reset(WorkGuard& wg) + { + wg.reset(); + } + }; } // namespace detail diff --git a/include/cli/detail/oldboostasiolib.h b/include/cli/detail/oldboostasiolib.h index af5c704..a24173d 100644 --- a/include/cli/detail/oldboostasiolib.h +++ b/include/cli/detail/oldboostasiolib.h @@ -69,6 +69,10 @@ class OldBoostAsioLib return work; } + static void Reset(WorkGuard& /*wg*/) + { + } + }; } // namespace detail diff --git a/include/cli/detail/oldstandaloneasiolib.h b/include/cli/detail/oldstandaloneasiolib.h index a764fb3..6c582e6 100644 --- a/include/cli/detail/oldstandaloneasiolib.h +++ b/include/cli/detail/oldstandaloneasiolib.h @@ -72,6 +72,10 @@ class OldStandaloneAsioLib return work; } + static void Reset(WorkGuard& /*wg*/) + { + } + }; } // namespace detail diff --git a/include/cli/detail/platform.h b/include/cli/detail/platform.h new file mode 100644 index 0000000..1ed999c --- /dev/null +++ b/include/cli/detail/platform.h @@ -0,0 +1,44 @@ +/******************************************************************************* + * CLI - A simple command line interface. + * Copyright (C) 2016-2024 Daniele Pallastrelli + * + * Boost Software License - Version 1.0 - August 17th, 2003 + * + * Permission is hereby granted, free of charge, to any person or organization + * obtaining a copy of the software and accompanying documentation covered by + * this license (the "Software") to use, reproduce, display, distribute, + * execute, and transmit the Software, and to prepare derivative works of the + * Software, and to permit third-parties to whom the Software is furnished to + * do so, all subject to the following: + * + * The copyright notices in the Software and this entire statement, including + * the above license grant, this restriction and the following disclaimer, + * must be included in all copies of the Software, in whole or in part, and + * all derivative works of the Software, unless such copies or derivative + * works are solely in the form of machine-executable object code generated by + * a source language processor. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT + * SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE + * FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, + * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + ******************************************************************************/ + +#ifndef CLI_DETAIL_PLATFORM_H_ +#define CLI_DETAIL_PLATFORM_H_ + +#if defined(__unix__) || defined(__unix) || defined(__linux__) + #define CLI_OS_LINUX +#elif defined(WIN32) || defined(_WIN32) || defined(_WIN64) + #define CLI_OS_WIN +#elif defined(__APPLE__) || defined(__MACH__) + #define CLI_OS_MAC +#else + #error "Platform not supported (yet)." +#endif + +#endif // CLI_DETAIL_PLATFORM_H_ + diff --git a/include/cli/detail/rang.h b/include/cli/detail/rang.h index 708f443..79b5ae6 100644 --- a/include/cli/detail/rang.h +++ b/include/cli/detail/rang.h @@ -30,6 +30,8 @@ #include #include +namespace cli { +namespace detail { namespace rang { enum class style { @@ -289,7 +291,10 @@ inline rang_implementation::enableControl operator<<( } return os; } + } // namespace rang +} // namespace detail +} // namespace cli #undef OS_LINUX #undef OS_WIN diff --git a/include/cli/detail/screen.h b/include/cli/detail/screen.h new file mode 100644 index 0000000..df4936a --- /dev/null +++ b/include/cli/detail/screen.h @@ -0,0 +1,46 @@ +/******************************************************************************* + * CLI - A simple command line interface. + * Copyright (C) 2016-2024 Daniele Pallastrelli + * + * Boost Software License - Version 1.0 - August 17th, 2003 + * + * Permission is hereby granted, free of charge, to any person or organization + * obtaining a copy of the software and accompanying documentation covered by + * this license (the "Software") to use, reproduce, display, distribute, + * execute, and transmit the Software, and to prepare derivative works of the + * Software, and to permit third-parties to whom the Software is furnished to + * do so, all subject to the following: + * + * The copyright notices in the Software and this entire statement, including + * the above license grant, this restriction and the following disclaimer, + * must be included in all copies of the Software, in whole or in part, and + * all derivative works of the Software, unless such copies or derivative + * works are solely in the form of machine-executable object code generated by + * a source language processor. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT + * SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE + * FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, + * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + ******************************************************************************/ + +#ifndef CLI_DETAIL_SCREEN_H_ +#define CLI_DETAIL_SCREEN_H_ + +#include "platform.h" +#include "telnetscreen.h" + +#if defined(CLI_OS_LINUX) || defined(CLI_OS_MAC) + namespace cli { namespace detail { using LocalScreen = TelnetScreen; } } +#elif defined(CLI_OS_WIN) + #include "winscreen.h" + namespace cli { namespace detail { using LocalScreen = WinScreen; } } +#else + #error "Platform not supported (yet)." +#endif + +#endif // CLI_DETAIL_SCREEN_H_ + diff --git a/include/cli/detail/server.h b/include/cli/detail/server.h index 7d6eb41..8013057 100644 --- a/include/cli/detail/server.h +++ b/include/cli/detail/server.h @@ -125,14 +125,12 @@ class Server Server& operator = ( const Server& ) = delete; Server(typename ASIOLIB::ContextType& ios, unsigned short port) : - acceptor(ios, asiolib::ip::tcp::endpoint(asiolib::ip::tcp::v4(), port)), - socket(ios) + acceptor(ios, asiolib::ip::tcp::endpoint(asiolib::ip::tcp::v4(), port)) { Accept(); } Server(typename ASIOLIB::ContextType& ios, std::string address, unsigned short port) : - acceptor(ios, asiolib::ip::tcp::endpoint(ASIOLIB::IpAddressFromString(address), port)), - socket(ios) + acceptor(ios, asiolib::ip::tcp::endpoint(ASIOLIB::IpAddressFromString(address), port)) { Accept(); } @@ -142,16 +140,16 @@ class Server private: void Accept() { - acceptor.async_accept(socket, [this](asiolibec::error_code ec) + acceptor.async_accept([this](asiolibec::error_code ec, asiolib::ip::tcp::socket socket) { if (!ec) CreateSession(std::move(socket))->Start(); Accept(); }); } asiolib::ip::tcp::acceptor acceptor; - asiolib::ip::tcp::socket socket; }; + } // namespace detail } // namespace cli diff --git a/include/cli/detail/split.h b/include/cli/detail/split.h index fbd8686..cea4fe2 100644 --- a/include/cli/detail/split.h +++ b/include/cli/detail/split.h @@ -101,6 +101,10 @@ class Text state = State::escape; splitResult.emplace_back(""); } + else if (c == '!') + { + splitResult.emplace_back(1, c); + } else { state = State::word; @@ -114,6 +118,11 @@ class Text { state = State::space; } + else if (c == '!') + { + splitResult.emplace_back(1, c); + state = State::space; + } else if (c == '"' || c == '\'') { NewSentence(c); @@ -122,7 +131,7 @@ class Text { prev_state = state; state = State::escape; - } + } else { assert(!splitResult.empty()); diff --git a/include/cli/detail/telnetscreen.h b/include/cli/detail/telnetscreen.h new file mode 100644 index 0000000..71916b3 --- /dev/null +++ b/include/cli/detail/telnetscreen.h @@ -0,0 +1,48 @@ +/******************************************************************************* + * CLI - A simple command line interface. + * Copyright (C) 2016-2024 Daniele Pallastrelli + * + * Boost Software License - Version 1.0 - August 17th, 2003 + * + * Permission is hereby granted, free of charge, to any person or organization + * obtaining a copy of the software and accompanying documentation covered by + * this license (the "Software") to use, reproduce, display, distribute, + * execute, and transmit the Software, and to prepare derivative works of the + * Software, and to permit third-parties to whom the Software is furnished to + * do so, all subject to the following: + * + * The copyright notices in the Software and this entire statement, including + * the above license grant, this restriction and the following disclaimer, + * must be included in all copies of the Software, in whole or in part, and + * all derivative works of the Software, unless such copies or derivative + * works are solely in the form of machine-executable object code generated by + * a source language processor. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT + * SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE + * FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, + * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + ******************************************************************************/ + +#ifndef CLI_DETAIL_TELNETSCREEN_H_ +#define CLI_DETAIL_TELNETSCREEN_H_ + +#include + +namespace cli +{ +namespace detail +{ + +struct TelnetScreen +{ + static void Clear(std::ostream& out) { out << "\033[H\033[J" << std::flush; } +}; + +} // namespace detail +} // namespace cli + +#endif // CLI_DETAIL_TELNETSCREEN_H_ diff --git a/include/cli/detail/terminal.h b/include/cli/detail/terminal.h index 6db62f5..f720fd8 100644 --- a/include/cli/detail/terminal.h +++ b/include/cli/detail/terminal.h @@ -46,9 +46,11 @@ enum class Symbol up, down, tab, - eof + eof, + clear }; +template class Terminal { public: @@ -56,13 +58,15 @@ class Terminal void ResetCursor() { position = 0; } + void Clear() const { SCREEN::Clear(out); } + void SetLine(const std::string &newLine) { out << beforeInput << std::string(position, '\b') << newLine << afterInput << std::flush; - // if newLine is shorter then currentLine, we have + // if newLine is shorter than currentLine, we have // to clear the rest of the string if (newLine.size() < currentLine.size()) { @@ -193,6 +197,9 @@ class Terminal position = 0; break; } + case KeyType::clear: + return std::make_pair(Symbol::clear, std::string()); + break; case KeyType::ignored: // TODO break; diff --git a/include/cli/detail/winkeyboard.h b/include/cli/detail/winkeyboard.h index cea4d2c..b02b6b8 100644 --- a/include/cli/detail/winkeyboard.h +++ b/include/cli/detail/winkeyboard.h @@ -34,41 +34,104 @@ #include #include #include -#include #include +#include #include "inputdevice.h" +#if !defined(NOMINMAX) +#define NOMINMAX 1 // prevent windows from defining min and max macros +#endif // !defined(NOMINMAX) +#include + namespace cli { namespace detail { +class InputSource +{ +public: + + InputSource() + { + events[0] = CreateEvent(nullptr, FALSE, FALSE, nullptr); // Obtain a Windows handle to use to stop + events[1] = GetStdHandle(STD_INPUT_HANDLE); // Get a Windows handle to the keyboard input + } + + void WaitKbHit() + { + // Wait for either the timer to expire or a key press event + DWORD dwResult = WaitForMultipleObjects(2, events, false, INFINITE); + + if (dwResult == WAIT_FAILED) + { + // TODO + assert(false); + } + else + { + if (dwResult == WAIT_OBJECT_0) // WAIT_OBJECT_0 corresponds to the stop event + { + throw std::runtime_error("InputSource stop"); + } + else + { + return; + } + } + + // we can't reach this point + assert(false); + } + + void Stop() + { + SetEvent(events[0]); + } + +private: + HANDLE events[2]; +}; + +// + class WinKeyboard : public InputDevice { public: - explicit WinKeyboard(Scheduler& _scheduler) : InputDevice(_scheduler) + explicit WinKeyboard(Scheduler& _scheduler) : + InputDevice(_scheduler), + servant([this]() noexcept { Read(); }) { - servant = std::make_unique( [this](){ Read(); } ); - servant->detach(); } - ~WinKeyboard() + ~WinKeyboard() override { - run = false; + is.Stop(); + servant.join(); } private: - void Read() + + void Read() noexcept { - while (run) + try + { + while (true) + { + auto k = Get(); + Notify(k); + } + } + catch (const std::exception&) { - auto k = Get(); - Notify(k); + // nothing to do: just exit } } std::pair Get() { + is.WaitKbHit(); + int c = _getch(); switch (c) { @@ -97,6 +160,9 @@ class WinKeyboard : public InputDevice case 8: return std::make_pair(KeyType::backspace, c); break; + case 12: // CTRL-L + return std::make_pair(KeyType::clear, ' '); + break; case 13: return std::make_pair(KeyType::ret, c); break; @@ -109,8 +175,8 @@ class WinKeyboard : public InputDevice return std::make_pair(KeyType::ignored, ' '); } - std::atomic run{true}; - std::unique_ptr servant; + InputSource is; + std::thread servant; }; } // namespace detail diff --git a/include/cli/detail/winscreen.h b/include/cli/detail/winscreen.h new file mode 100644 index 0000000..9af3e8d --- /dev/null +++ b/include/cli/detail/winscreen.h @@ -0,0 +1,62 @@ +/******************************************************************************* + * CLI - A simple command line interface. + * Copyright (C) 2016-2024 Daniele Pallastrelli + * + * Boost Software License - Version 1.0 - August 17th, 2003 + * + * Permission is hereby granted, free of charge, to any person or organization + * obtaining a copy of the software and accompanying documentation covered by + * this license (the "Software") to use, reproduce, display, distribute, + * execute, and transmit the Software, and to prepare derivative works of the + * Software, and to permit third-parties to whom the Software is furnished to + * do so, all subject to the following: + * + * The copyright notices in the Software and this entire statement, including + * the above license grant, this restriction and the following disclaimer, + * must be included in all copies of the Software, in whole or in part, and + * all derivative works of the Software, unless such copies or derivative + * works are solely in the form of machine-executable object code generated by + * a source language processor. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT + * SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE + * FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, + * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + ******************************************************************************/ + +#ifndef CLI_DETAIL_WINSCREEN_H_ +#define CLI_DETAIL_WINSCREEN_H_ + +#if !defined(NOMINMAX) +#define NOMINMAX 1 // prevent windows from defining min and max macros +#endif // !defined(NOMINMAX) +#include + +namespace cli +{ +namespace detail +{ + +struct WinScreen +{ + static void Clear(std::ostream& /*out*/) + { + HANDLE hStdOut = GetStdHandle(STD_OUTPUT_HANDLE); + COORD coord = { 0, 0 }; + DWORD count; + + CONSOLE_SCREEN_BUFFER_INFO csbi; + GetConsoleScreenBufferInfo(hStdOut, &csbi); + + FillConsoleOutputCharacter(hStdOut, ' ', csbi.dwSize.X * csbi.dwSize.Y, coord, &count); + SetConsoleCursorPosition(hStdOut, coord); + } +}; + +} // namespace detail +} // namespace cli + +#endif // CLI_DETAIL_WINSCREEN_H_ diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index d0942e0..36e64a7 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -27,7 +27,7 @@ # DEALINGS IN THE SOFTWARE. ################################################################################ -cmake_minimum_required(VERSION 3.8) +cmake_minimum_required(VERSION 3.8...3.27) project(cli_test) enable_testing() @@ -36,8 +36,20 @@ set(Boost_ADDITIONAL_VERSIONS "1.66" "1.66.0") set(Boost_NO_BOOST_CMAKE ON) add_definitions( -DBOOST_ALL_NO_LIB ) # for windows -# finds boost, triggers an error otherwise -find_package(Boost 1.55 REQUIRED COMPONENTS unit_test_framework) +if(POLICY CMP0074) + cmake_policy(SET CMP0074 NEW) # _ROOT +endif() + +if(POLICY CMP0144) + cmake_policy(SET CMP0144 NEW) # uppercase variables like BOOST_ROOT +endif() + +if(POLICY CMP0167) + cmake_policy(SET CMP0167 OLD) # re-enable FindBoost +endif() + + +find_package(Boost 1.66 QUIET COMPONENTS unit_test_framework system) # finds standalone asio, triggers an error otherwise find_path(STANDALONE_ASIO_INCLUDE_PATH NAMES "asio.hpp" HINTS ${ASIO_INCLUDEDIR}) @@ -66,7 +78,23 @@ target_include_directories(test_suite SYSTEM PRIVATE ${Boost_INCLUDE_DIRS}) # indicates the shared library variant target_compile_definitions(test_suite PRIVATE "BOOST_TEST_DYN_LINK=1") # indicates the link paths -target_link_libraries(test_suite ${Boost_UNIT_TEST_FRAMEWORK_LIBRARY} standalone_asio_test cli::cli) +target_link_libraries(test_suite + ${Boost_UNIT_TEST_FRAMEWORK_LIBRARY} + standalone_asio_test + cli::cli +) + +# add it only if found +if (Boost_SYSTEM_FOUND) + message(STATUS "Using Boost.System library for tests") + target_link_libraries(test_suite Boost::system) +else() + message(STATUS "Boost.System missing: using header-only mode for tests") + target_compile_definitions(test_suite PRIVATE + BOOST_ERROR_CODE_HEADER_ONLY + BOOST_SYSTEM_NO_LIB + ) +endif() # declares a test with our executable add_test(NAME cli_test COMMAND test_suite) diff --git a/test/scheduler_test_templates.h b/test/scheduler_test_templates.h index 0c7d4d0..9792f36 100644 --- a/test/scheduler_test_templates.h +++ b/test/scheduler_test_templates.h @@ -38,7 +38,7 @@ void SchedulingTest() { S scheduler; bool done = false; - scheduler.Post( [&done](){ done = true; } ); + scheduler.Post( [&done]() noexcept { done = true; } ); scheduler.ExecOne(); BOOST_CHECK(done); } @@ -56,7 +56,7 @@ void SameThreadTest() { postThreadId = this_thread::get_id(); scheduler.Post( - [&runThreadId]() + [&runThreadId]() noexcept { runThreadId = this_thread::get_id(); } diff --git a/test/test_boostasioscheduler.cpp b/test/test_boostasioscheduler.cpp index dc90491..b3c478b 100644 --- a/test/test_boostasioscheduler.cpp +++ b/test/test_boostasioscheduler.cpp @@ -55,7 +55,7 @@ BOOST_AUTO_TEST_CASE(BoostAsioNonOwner) detail::BoostAsioLib::ContextType ioc; BoostAsioScheduler scheduler(ioc); bool done = false; - scheduler.Post( [&done](){ done = true; } ); + scheduler.Post( [&done]() noexcept { done = true; } ); ioc.run_one(); BOOST_CHECK(done); } diff --git a/test/test_cli.cpp b/test/test_cli.cpp index ae395a6..22cae5c 100644 --- a/test/test_cli.cpp +++ b/test/test_cli.cpp @@ -97,7 +97,7 @@ BOOST_AUTO_TEST_CASE(Basics) rootMenu->Insert("int_cmd", [](ostream& out, int par){ out << par << "\n"; }, "int_cmd help", {"int_par"} ); rootMenu->Insert("string_cmd", [](ostream& out, const string& par){ out << par << "\n"; }, "string_cmd help", {"string_par"} ); - Cli cli(move(rootMenu)); + Cli cli(std::move(rootMenu)); stringstream oss; @@ -154,7 +154,7 @@ BOOST_AUTO_TEST_CASE(parameters) rootMenu->Insert("long_double_cmd", [](ostream& out, long double par){ out << par << "\n"; }, "long_double_cmd help", {"long_double_par"} ); rootMenu->Insert("string_cmd", [](ostream& out, const string& par){ out << par << "\n"; }, "string_cmd help", {"string_par"} ); - Cli cli(move(rootMenu)); + Cli cli(std::move(rootMenu)); stringstream oss; @@ -280,7 +280,7 @@ BOOST_AUTO_TEST_CASE(freeform) out << "\n"; }, "cmd_printer help", {""} ); - Cli cli(move(rootMenu)); + Cli cli(std::move(rootMenu)); stringstream oss; UserInput(cli, oss, R"(cmd_printer_by_value a b 'c d e' f)"); @@ -303,7 +303,7 @@ BOOST_AUTO_TEST_CASE(freeform) BOOST_AUTO_TEST_CASE(borderLine) { auto rootMenu = make_unique("cli"); - Cli cli(move(rootMenu)); + Cli cli(std::move(rootMenu)); stringstream oss; @@ -323,10 +323,10 @@ BOOST_AUTO_TEST_CASE(Submenus) auto subSubMenu = make_unique("subsub"); subSubMenu->Insert("double_int_cmd", [](ostream& out, int par1, int par2){ out << par1 << par2 << "\n"; } ); subSubMenu->Insert("double_string_cmd", [](ostream& out, const string& par1, const string& par2){ out << par1 << par2 << "\n"; } ); - subMenu->Insert(move(subSubMenu)); - rootMenu->Insert(move(subMenu)); + subMenu->Insert(std::move(subSubMenu)); + rootMenu->Insert(std::move(subMenu)); - Cli cli(move(rootMenu)); + Cli cli(std::move(rootMenu)); stringstream oss; @@ -370,15 +370,38 @@ BOOST_AUTO_TEST_CASE(Submenus) BOOST_CHECK_EQUAL(ExtractContent(oss), "foo"); } +BOOST_AUTO_TEST_CASE(EnterActions) +{ + auto rootMenu = make_unique("cli"); + rootMenu->Insert("int_cmd", + [](ostream &out, int par) { out << par << "\n"; }, + "int_cmd help", {"int_par"}); + rootMenu->Insert( + "string_cmd", + [](ostream &out, const string &par) { out << par << "\n"; }, + "string_cmd help", {"string_par"}); + + Cli cli(std::move(rootMenu)); + bool enterActionDone = false; + + cli.EnterAction( + [&enterActionDone](std::ostream &) noexcept { enterActionDone = true; }); + + stringstream oss; + + UserInput(cli, oss, "exit"); + BOOST_CHECK(enterActionDone); +} + BOOST_AUTO_TEST_CASE(ExitActions) { auto rootMenu = make_unique("cli"); rootMenu->Insert("int_cmd", [](ostream& out, int par){ out << par << "\n"; }, "int_cmd help", {"int_par"} ); rootMenu->Insert("string_cmd", [](ostream& out, const string& par){ out << par << "\n"; }, "string_cmd help", {"string_par"} ); - Cli cli(move(rootMenu)); + Cli cli(std::move(rootMenu)); bool exitActionDone = false; - cli.ExitAction([&](std::ostream&){ exitActionDone=true; }); + cli.ExitAction([&](std::ostream&) noexcept { exitActionDone=true; }); stringstream oss; @@ -392,7 +415,7 @@ BOOST_AUTO_TEST_CASE(Exceptions) rootMenu->Insert("stdexception", [](ostream&){ throw std::logic_error("myerror"); } ); rootMenu->Insert("customexception", [](ostream&){ throw 42; } ); - Cli cli(move(rootMenu)); + Cli cli(std::move(rootMenu)); stringstream oss; @@ -402,7 +425,7 @@ BOOST_AUTO_TEST_CASE(Exceptions) // std exception type, custom handler bool excActionDone = false; - cli.StdExceptionHandler( [&](std::ostream&, const std::string&, const std::exception&){ excActionDone = true; } ); + cli.StdExceptionHandler( [&](std::ostream&, const std::string&, const std::exception&) noexcept { excActionDone = true; } ); BOOST_CHECK_NO_THROW( UserInput(cli, oss, "stdexception") ); BOOST_CHECK(excActionDone); diff --git a/test/test_history.cpp b/test/test_history.cpp index 61e4003..b14cf0f 100644 --- a/test/test_history.cpp +++ b/test/test_history.cpp @@ -80,6 +80,11 @@ BOOST_AUTO_TEST_CASE(Insertion) history.NewCommand("item3"); history.NewCommand("item4"); + BOOST_CHECK_EQUAL(history.At(0), "item1"); + BOOST_CHECK_EQUAL(history.At(1), "item2"); + BOOST_CHECK_EQUAL(history.At(2), "item3"); + BOOST_CHECK_EQUAL(history.At(3), "item4"); + BOOST_CHECK_EQUAL(history.Previous(""), "item4"); BOOST_CHECK_EQUAL(history.Previous("item4"), "item3"); BOOST_CHECK_EQUAL(history.Previous("foo"), "item2"); @@ -90,6 +95,12 @@ BOOST_AUTO_TEST_CASE(Insertion) history.NewCommand("item5"); + BOOST_CHECK_EQUAL(history.At(0), "item1"); + BOOST_CHECK_EQUAL(history.At(1), "item2"); + BOOST_CHECK_EQUAL(history.At(2), "foo"); + BOOST_CHECK_EQUAL(history.At(3), "item4"); + BOOST_CHECK_EQUAL(history.At(4), "item5"); + BOOST_CHECK_EQUAL(history.Previous(""), "item5"); BOOST_CHECK_EQUAL(history.Previous("item5"), "item4"); BOOST_CHECK_EQUAL(history.Next(), "item5"); @@ -150,6 +161,10 @@ BOOST_AUTO_TEST_CASE(Copies) const std::vector v = { "item1", "item2", "item3" }; history.LoadCommands(v); + BOOST_CHECK_EQUAL(history.At(0), "item1"); + BOOST_CHECK_EQUAL(history.At(1), "item2"); + BOOST_CHECK_EQUAL(history.At(2), "item3"); + BOOST_CHECK_EQUAL(history.Previous(""), "item3"); BOOST_CHECK_EQUAL(history.Previous("item3"), "item2"); BOOST_CHECK_EQUAL(history.Previous("item2"), "item1"); @@ -158,6 +173,12 @@ BOOST_AUTO_TEST_CASE(Copies) history.NewCommand("itemA"); history.NewCommand("itemB"); + BOOST_CHECK_EQUAL(history.At(0), "item1"); + BOOST_CHECK_EQUAL(history.At(1), "item2"); + BOOST_CHECK_EQUAL(history.At(2), "item3"); + BOOST_CHECK_EQUAL(history.At(3), "itemA"); + BOOST_CHECK_EQUAL(history.At(4), "itemB"); + BOOST_CHECK_EQUAL(history.Previous(""), "itemB"); BOOST_CHECK_EQUAL(history.Previous("itemB"), "itemA"); BOOST_CHECK_EQUAL(history.Previous("itemA"), "item3"); @@ -175,6 +196,10 @@ BOOST_AUTO_TEST_CASE(Copies) const std::vector v1 = { "item1", "item2", "item3" }; history1.LoadCommands(v1); + BOOST_CHECK_EQUAL(history1.At(0), "item1"); + BOOST_CHECK_EQUAL(history1.At(1), "item2"); + BOOST_CHECK_EQUAL(history1.At(2), "item3"); + BOOST_CHECK_EQUAL(history1.Previous(""), "item3"); BOOST_CHECK_EQUAL(history1.Previous("item3"), "item2"); BOOST_CHECK_EQUAL(history1.Previous("item2"), "item2"); @@ -182,6 +207,10 @@ BOOST_AUTO_TEST_CASE(Copies) history1.NewCommand("itemA"); history1.NewCommand("itemB"); + BOOST_CHECK_EQUAL(history1.At(2), "item3"); + BOOST_CHECK_EQUAL(history1.At(3), "itemA"); + BOOST_CHECK_EQUAL(history1.At(4), "itemB"); + BOOST_CHECK_EQUAL(history1.Previous(""), "itemB"); BOOST_CHECK_EQUAL(history1.Previous("itemB"), "itemA"); BOOST_CHECK_EQUAL(history1.Previous("itemA"), "itemA"); @@ -199,9 +228,13 @@ BOOST_AUTO_TEST_CASE(Copies) history2.NewCommand("itemD"); history2.NewCommand("itemE"); + BOOST_CHECK_EQUAL(history2.At(2), "itemC"); + BOOST_CHECK_EQUAL(history2.At(3), "itemD"); + BOOST_CHECK_EQUAL(history2.At(4), "itemE"); + auto cmds2 = history2.GetCommands(); const std::vector expected2 = { "itemC", "itemD", "itemE" }; BOOST_CHECK_EQUAL_COLLECTIONS(cmds2.begin(), cmds2.end(), expected2.begin(), expected2.end()); } -BOOST_AUTO_TEST_SUITE_END() \ No newline at end of file +BOOST_AUTO_TEST_SUITE_END() diff --git a/test/test_menu.cpp b/test/test_menu.cpp index a5bf488..a47e76a 100644 --- a/test/test_menu.cpp +++ b/test/test_menu.cpp @@ -47,9 +47,9 @@ BOOST_AUTO_TEST_CASE(Basics) auto subMenu = make_unique("submenu"); auto subSubMenu = make_unique("subsubmenu"); subSubMenu->Insert("bar", [](ostream&){}); - subMenu->Insert(move(subSubMenu)); + subMenu->Insert(std::move(subSubMenu)); subMenu->Insert("foo", [](ostream&){}); - menu.Insert(move(subMenu)); + menu.Insert(std::move(subMenu)); /* menu @@ -89,7 +89,7 @@ BOOST_AUTO_TEST_CASE(Basics) BOOST_CHECK_EQUAL_COLLECTIONS(completions.begin(), completions.end(), expected.begin(), expected.end()); completions = menu.GetCompletions("submenu"); - expected = {"submenu subsubmenu", "submenu foo"}; + expected = {"submenu subsubmenu", "submenu foo", "submenu menu"}; BOOST_CHECK_EQUAL_COLLECTIONS(completions.begin(), completions.end(), expected.begin(), expected.end()); /* @@ -117,7 +117,7 @@ BOOST_AUTO_TEST_CASE(Basics) BOOST_CHECK_EQUAL_COLLECTIONS(completions.begin(), completions.end(), expected.begin(), expected.end()); completions = menu.GetCompletionRecursive("menu submenu"); - expected = {"menu submenu subsubmenu", "menu submenu foo"}; + expected = {"menu submenu subsubmenu", "menu submenu foo", "menu submenu menu"}; BOOST_CHECK_EQUAL_COLLECTIONS(completions.begin(), completions.end(), expected.begin(), expected.end()); } diff --git a/test/test_split.cpp b/test/test_split.cpp index cb88398..167cb6d 100644 --- a/test/test_split.cpp +++ b/test/test_split.cpp @@ -241,4 +241,47 @@ BOOST_AUTO_TEST_CASE(EscapedCases) BOOST_CHECK_EQUAL(strs[0], R"(foo\"bar)"); } +BOOST_AUTO_TEST_CASE(Symbols) +{ + VS strs; + + split(strs, "!foo"); // two words! + BOOST_CHECK_EQUAL(strs.size(), 2); + BOOST_CHECK_EQUAL(strs[0], "!"); + BOOST_CHECK_EQUAL(strs[1], "foo"); + + split(strs, " ! foo "); // two words + BOOST_CHECK_EQUAL(strs.size(), 2); + BOOST_CHECK_EQUAL(strs[0], "!"); + BOOST_CHECK_EQUAL(strs[1], "foo"); + + split(strs, "!42!69!"); // 5 words + BOOST_CHECK_EQUAL(strs.size(), 5); + BOOST_CHECK_EQUAL(strs[0], "!"); + BOOST_CHECK_EQUAL(strs[1], "42"); + BOOST_CHECK_EQUAL(strs[2], "!"); + BOOST_CHECK_EQUAL(strs[3], "69"); + BOOST_CHECK_EQUAL(strs[4], "!"); + + split(strs, " 38!42!69! 72 ! 33"); // 9 words + BOOST_CHECK_EQUAL(strs.size(), 9); + BOOST_CHECK_EQUAL(strs[0], "38"); + BOOST_CHECK_EQUAL(strs[1], "!"); + BOOST_CHECK_EQUAL(strs[2], "42"); + BOOST_CHECK_EQUAL(strs[3], "!"); + BOOST_CHECK_EQUAL(strs[4], "69"); + BOOST_CHECK_EQUAL(strs[5], "!"); + BOOST_CHECK_EQUAL(strs[6], "72"); + BOOST_CHECK_EQUAL(strs[7], "!"); + BOOST_CHECK_EQUAL(strs[8], "33"); + + split(strs, R"( 38!"42!69"! "72 ! 33" )"); // 5 words + BOOST_CHECK_EQUAL(strs.size(), 5); + BOOST_CHECK_EQUAL(strs[0], "38"); + BOOST_CHECK_EQUAL(strs[1], "!"); + BOOST_CHECK_EQUAL(strs[2], "42!69"); + BOOST_CHECK_EQUAL(strs[3], "!"); + BOOST_CHECK_EQUAL(strs[4], "72 ! 33"); +} + BOOST_AUTO_TEST_SUITE_END() \ No newline at end of file diff --git a/test/test_standaloneasioscheduler.cpp b/test/test_standaloneasioscheduler.cpp index 8bdf68c..d8198e3 100644 --- a/test/test_standaloneasioscheduler.cpp +++ b/test/test_standaloneasioscheduler.cpp @@ -55,7 +55,7 @@ BOOST_AUTO_TEST_CASE(StandaloneAsioNonOwner) detail::StandaloneAsioLib::ContextType ioc; StandaloneAsioScheduler scheduler(ioc); bool done = false; - scheduler.Post( [&done](){ done = true; } ); + scheduler.Post( [&done]() noexcept { done = true; } ); ioc.run_one(); BOOST_CHECK(done); }