├── src ├── kilo │ ├── CREDITS │ ├── kilo.h │ └── kilo.cpp ├── ESP32Console │ ├── Commands │ │ ├── NetworkCommands.h │ │ ├── SystemCommands.h │ │ ├── GPIOCommands.h │ │ ├── CoreCommands.h │ │ ├── VFSCommands.h │ │ ├── CoreCommands.cpp │ │ ├── GPIOCommands.cpp │ │ ├── SystemCommands.cpp │ │ ├── NetworkCommands.cpp │ │ └── VFSCommands.cpp │ ├── Helpers │ │ ├── InputParser.h │ │ ├── PWDHelpers.h │ │ ├── PWDHelpers.cpp │ │ └── InputParser.cpp │ ├── ConsoleCommandBase.h │ ├── ConsoleCommandD.cpp │ ├── ConsoleCommand.h │ ├── OptionsConsoleCommand.cpp │ ├── ConsoleCommandD.h │ ├── OptionsConsoleCommand.h │ ├── Console.h │ └── Console.cpp ├── ESP32Console.h └── cxxopts │ └── cxxopts.hpp ├── extras └── screenshot.png ├── .github └── workflows │ ├── arduino-lint.yaml │ └── arduino-ci.yaml ├── library.properties ├── examples ├── network │ └── network.ino ├── other_uart_channel │ └── other_uart_channel.ino ├── gpio │ └── gpio.ino ├── vfs │ └── vfs.ino ├── argparser │ └── argparser.ino └── basic │ └── basic.ino ├── library.json ├── LICENSE ├── commands.md └── README.md /src/kilo/CREDITS: -------------------------------------------------------------------------------- 1 | Kilo editor from: 2 | https://github.com/antirez/kilo -------------------------------------------------------------------------------- /extras/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbtronics/ESP32Console/HEAD/extras/screenshot.png -------------------------------------------------------------------------------- /src/ESP32Console/Commands/NetworkCommands.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "ESP32Console/ConsoleCommand.h" 4 | 5 | namespace ESP32Console::Commands 6 | { 7 | const ConsoleCommand getPingCommand(); 8 | 9 | 10 | const ConsoleCommand getIpconfigCommand(); 11 | } -------------------------------------------------------------------------------- /.github/workflows/arduino-lint.yaml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | jobs: 3 | lint: 4 | runs-on: ubuntu-latest 5 | steps: 6 | - uses: actions/checkout@v4 7 | - uses: arduino/arduino-lint-action@v1 8 | with: 9 | library-manager: update 10 | compliance: strict -------------------------------------------------------------------------------- /src/ESP32Console.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #if !defined(ESP32) 4 | #error This library depends on ESP-IDF and only works on ESP32! 5 | #endif 6 | 7 | #define ESP32CONSOLE_VERSION "1.2.2" 8 | 9 | #include "ESP32Console/Console.h" 10 | #include "ESP32Console/ConsoleCommand.h" 11 | #include "ESP32Console/ConsoleCommandD.h" 12 | #include "ESP32Console/OptionsConsoleCommand.h" 13 | -------------------------------------------------------------------------------- /src/ESP32Console/Commands/SystemCommands.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "ESP32Console/ConsoleCommand.h" 4 | 5 | namespace ESP32Console::Commands 6 | { 7 | const ConsoleCommand getSysInfoCommand(); 8 | 9 | const ConsoleCommand getRestartCommand(); 10 | 11 | const ConsoleCommand getMemInfoCommand(); 12 | 13 | const ConsoleCommand getDateCommand(); 14 | }; -------------------------------------------------------------------------------- /src/ESP32Console/Commands/GPIOCommands.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "ESP32Console/ConsoleCommand.h" 4 | 5 | namespace ESP32Console::Commands 6 | { 7 | const ConsoleCommand getPinModeCommand(); 8 | 9 | const ConsoleCommand getDigitalWriteCommand(); 10 | 11 | const ConsoleCommand getDigitalReadCommand(); 12 | 13 | const ConsoleCommand getAnalogReadCommand(); 14 | } -------------------------------------------------------------------------------- /src/ESP32Console/Helpers/InputParser.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | namespace ESP32Console 5 | { 6 | /** 7 | * @brief Interpolate the given line using environment variables. $VAR and ${ENV} are replaced by the representive values of the environment variables. 8 | * 9 | * @param in 10 | * @return String 11 | */ 12 | String interpolateLine(const char *in); 13 | } -------------------------------------------------------------------------------- /library.properties: -------------------------------------------------------------------------------- 1 | name=ESP32Console 2 | version=1.2.2 3 | author=Jan Böhmer 4 | maintainer=Jan Böhmer 5 | sentence=Extensible UART console for ESP32 with useful included commands. 6 | paragraph=This library encapsules the console component of ESP-IDF and make them easy to use in an Arduino environment 7 | category=Communication 8 | url=https://github.com/jbtronics/ESP32Console 9 | architectures=esp32 10 | -------------------------------------------------------------------------------- /src/ESP32Console/Commands/CoreCommands.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "ESP32Console/ConsoleCommand.h" 4 | 5 | namespace ESP32Console::Commands 6 | { 7 | const ConsoleCommand getClearCommand(); 8 | 9 | const ConsoleCommand getEchoCommand(); 10 | 11 | const ConsoleCommand getSetMultilineCommand(); 12 | 13 | const ConsoleCommand getHistoryCommand(int uart_channel=0); 14 | 15 | const ConsoleCommand getEnvCommand(); 16 | 17 | const ConsoleCommand getDeclareCommand(); 18 | } -------------------------------------------------------------------------------- /src/kilo/kilo.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "ESP32Console/Helpers/PWDHelpers.h" 4 | #include 5 | #include "linenoise/linenoise.h" 6 | 7 | namespace ESP32Console::Kilo 8 | { 9 | int kilo(int argc, char **argv); 10 | 11 | struct KiloException : public std::exception 12 | { 13 | private: 14 | int code_; 15 | 16 | public: 17 | KiloException(int code) : code_(code){}; 18 | 19 | const char *what() const throw() 20 | { 21 | return "Kilo exited"; 22 | } 23 | }; 24 | } -------------------------------------------------------------------------------- /src/ESP32Console/Helpers/PWDHelpers.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace ESP32Console 4 | { 5 | 6 | /** 7 | * @brief Returns the current console process working dir 8 | * 9 | * @return const char* 10 | */ 11 | const char *console_getpwd(); 12 | 13 | /** 14 | * @brief Resolves the given path using the console process working dir 15 | * 16 | * @return const char* 17 | */ 18 | const char *console_realpath(const char *path, char *resolvedPath); 19 | 20 | int console_chdir(const char *path); 21 | 22 | } -------------------------------------------------------------------------------- /src/ESP32Console/Commands/VFSCommands.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "ESP32Console/ConsoleCommand.h" 4 | 5 | namespace ESP32Console::Commands 6 | { 7 | const ConsoleCommand getCatCommand(); 8 | 9 | const ConsoleCommand getPWDCommand(); 10 | 11 | const ConsoleCommand getCDCommand(); 12 | 13 | const ConsoleCommand getLsCommand(); 14 | 15 | const ConsoleCommand getMvCommand(); 16 | 17 | const ConsoleCommand getCPCommand(); 18 | 19 | const ConsoleCommand getRMCommand(); 20 | 21 | const ConsoleCommand getRMDirCommand(); 22 | 23 | const ConsoleCommand getEditCommand(); 24 | } -------------------------------------------------------------------------------- /src/ESP32Console/ConsoleCommandBase.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "esp_console.h" 4 | 5 | namespace ESP32Console { 6 | class ConsoleCommandBase 7 | { 8 | protected: 9 | const char *command_; 10 | const char *help_; 11 | esp_console_cmd_func_t func_; 12 | 13 | public: 14 | /** 15 | * @brief Get the command name 16 | * 17 | * @return const char* 18 | */ 19 | const char* getCommand() { return command_; }; 20 | 21 | const char* getHelp() { return help_; }; 22 | 23 | virtual const esp_console_cmd_t toCommandStruct() const = 0; 24 | }; 25 | }; -------------------------------------------------------------------------------- /examples/network/network.ino: -------------------------------------------------------------------------------- 1 | /** 2 | * This example demonstrates the usage of the builtin network functions 3 | */ 4 | 5 | #include 6 | #include "ESP32Console.h" 7 | #include 8 | 9 | #include "ESP32Console/Helpers/PWDHelpers.h" 10 | 11 | using namespace ESP32Console; 12 | 13 | Console console; 14 | 15 | const char *ssid = "yourSSID"; 16 | const char *password = "yourWLAN"; 17 | 18 | void setup() 19 | { 20 | //Connect to WiFi 21 | WiFi.begin(ssid, password); 22 | 23 | console.begin(115200); 24 | console.registerSystemCommands(); 25 | 26 | //Register network commands 27 | console.registerNetworkCommands(); 28 | } 29 | 30 | void loop() 31 | { 32 | 33 | } -------------------------------------------------------------------------------- /examples/other_uart_channel/other_uart_channel.ino: -------------------------------------------------------------------------------- 1 | /** 2 | * This example demonstrates the usage of ESP32Console on a seperate UART channel, using its own Pins, which allows you to use 3 | * the console besides normal Serial.* commands 4 | */ 5 | 6 | #include 7 | #include "ESP32Console.h" 8 | 9 | using namespace ESP32Console; 10 | 11 | Console console; 12 | 13 | void setup() 14 | { 15 | //Initialize Serial port on UART0 (the onboard USB Serial Converter) 16 | Serial.begin(115200); 17 | 18 | //Enable ESP32Console on Pin 12 & 14 on UART1 19 | console.begin(115200, 12, 14, 1); 20 | 21 | 22 | console.registerSystemCommands(); 23 | console.registerNetworkCommands(); 24 | } 25 | 26 | void loop() 27 | { 28 | //We can use Serial on UART0 as usual, while ESP32Console is available on the Pins above. 29 | Serial.println("Test"); 30 | delay(1000); 31 | } -------------------------------------------------------------------------------- /.github/workflows/arduino-ci.yaml: -------------------------------------------------------------------------------- 1 | name: Arduino Library CI 2 | 3 | on: [pull_request, push, repository_dispatch] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/setup-python@v5 11 | with: 12 | python-version: '3.x' 13 | - uses: actions/checkout@v4 14 | 15 | - uses: actions/checkout@v4 16 | with: 17 | repository: adafruit/ci-arduino 18 | path: ci 19 | 20 | - name: pre-install 21 | run: bash ci/actions_install.sh 22 | 23 | - name: test platforms 24 | run: python3 ci/build_platform.py --no_warn esp32 25 | 26 | #- name: clang 27 | # run: python3 ci/run-clang-format.py -e "ci/*" -e "bin/*" -r . 28 | 29 | - name: doxygen 30 | env: 31 | GH_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} 32 | PRETTYNAME : "ESP32Console Library" 33 | run: bash ci/doxy_gen_and_deploy.sh -------------------------------------------------------------------------------- /library.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ESP32Console", 3 | "version": "1.2.2", 4 | "description": "Extensible UART console for ESP32 with useful included commands. This library encapsules the console component of ESP-IDF and make them easy to use in an Arduino environment.", 5 | "keywords": "esp32, console, ping, ifconfig, sysinfo, editor, file", 6 | "repository": 7 | { 8 | "type": "git", 9 | "url": "https://github.com/jbtronics/ESP32Console" 10 | }, 11 | "authors": 12 | [ 13 | { 14 | "name": "Jan Böhmer", 15 | "email": "mail@jan-boehmer.de", 16 | "url": "https://github.com/jbtronics", 17 | "maintainer": true 18 | } 19 | ], 20 | "license": "MIT", 21 | "homepage": "https://github.com/jbtronics/ESP32Console", 22 | "dependencies": { 23 | }, 24 | "exclude": [ 25 | ".github", 26 | "extras" 27 | ], 28 | "frameworks": "arduino", 29 | "platforms": "espressif32" 30 | } 31 | -------------------------------------------------------------------------------- /src/ESP32Console/ConsoleCommandD.cpp: -------------------------------------------------------------------------------- 1 | #include "./ConsoleCommandD.h" 2 | #include "Arduino.h" 3 | 4 | const static char *TAG = "ConsoleCommandD"; 5 | 6 | namespace ESP32Console 7 | { 8 | std::unordered_map ConsoleCommandD::registry_ = std::unordered_map(); 9 | 10 | int ConsoleCommandD::delegateResolver(int argc, char **argv) 11 | { 12 | // Retrieve ConsoleCommandD from registry 13 | auto it = registry_.find(argv[0]); 14 | if (it == registry_.end()) 15 | { 16 | log_e("Could not resolve the delegated function call!"); 17 | return 1; 18 | } 19 | 20 | delegateFunc command = it->second; 21 | 22 | int code = 0; 23 | 24 | try 25 | { 26 | return command(argc, argv); 27 | } 28 | catch (const std::exception &err) 29 | { 30 | printf("%s", err.what()); 31 | printf("\n"); 32 | return 1; 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /examples/gpio/gpio.ino: -------------------------------------------------------------------------------- 1 | /** 2 | * This example demonstrates the usage of the internal GPIO functions and the env variables interpolation 3 | */ 4 | 5 | #include 6 | #include "ESP32Console.h" 7 | 8 | #include "ESP32Console/Helpers/PWDHelpers.h" 9 | 10 | using namespace ESP32Console; 11 | 12 | Console console; 13 | 14 | void setup() 15 | { 16 | 17 | console.begin(115200); 18 | console.registerSystemCommands(); 19 | 20 | /** 21 | * Set environment variables, so users can access pin numbers easily in console. 22 | * Users can do commands like `digitalWrite $LED HIGH` which will then be expanded to `digitalWrite 10 HIGH`. 23 | * Users can change these env variables or create new ones dynamically in the console by `declare LED 10`. 24 | */ 25 | setenv("LED", "10", 1); 26 | setenv("BUTTON", "4", 1); 27 | 28 | /** 29 | * Users can list all defined env variables and their values by `env` command 30 | */ 31 | 32 | //Register GPIO commands 33 | console.registerGPIOCommands(); 34 | } 35 | 36 | void loop() 37 | { 38 | 39 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Jan Böhmer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /examples/vfs/vfs.ino: -------------------------------------------------------------------------------- 1 | /** 2 | * This example demonstrates the usage of the builtin VFS commands and persistent history 3 | */ 4 | 5 | #include 6 | #include "ESP32Console.h" 7 | #include 8 | #include "SPIFFS.h" 9 | #include "ESP32Console/Helpers/PWDHelpers.h" 10 | 11 | using namespace ESP32Console; 12 | 13 | Console console; 14 | 15 | void setup() 16 | { 17 | //Initalize SPIFFS and mount it on /spiffs 18 | SPIFFS.begin(true, "/spiffs"); 19 | 20 | //Modify prompt to show current file path (%pwd% get replaced by the filepath) 21 | console.setPrompt("ESP32 %pwd%> "); 22 | 23 | //Set HOME env for easier navigating (type cd to jump to home) 24 | setenv("HOME", "/spiffs", 1); 25 | //Set PWD to env 26 | console_chdir("/spiffs"); 27 | 28 | console.begin(115200); 29 | 30 | //Enable the saving of our command history to SPIFFS. You will be able to see it, when you type ls in your console. 31 | console.enablePersistentHistory("/spiffs/.history.txt"); 32 | 33 | console.registerSystemCommands(); 34 | 35 | //Register the VFS specific commands 36 | console.registerVFSCommands(); 37 | } 38 | 39 | void loop() 40 | { 41 | } -------------------------------------------------------------------------------- /src/ESP32Console/ConsoleCommand.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "./ConsoleCommandBase.h" 4 | 5 | namespace ESP32Console 6 | { 7 | 8 | /** 9 | * @brief A class for registering custom console commands via a passed function pointer. 10 | * 11 | */ 12 | class ConsoleCommand : public ConsoleCommandBase 13 | { 14 | protected: 15 | const char *hint_; 16 | 17 | public: 18 | /** 19 | * @brief Creates a new ConsoleCommand object with the given parameters. 20 | * 21 | * @param command The name under which the command will be called (e.g. "ls"). Must not contain spaces. 22 | * @param func The pointer to the function which is run if this function is called. Takes two paramaters argc and argv, similar to a classical C program. 23 | * @param help The text shown in "help" output for this command. If set to empty string, then the command is not shown in help. 24 | * @param hint A hint explaining the parameters in help output 25 | */ 26 | ConsoleCommand(const char *command, esp_console_cmd_func_t func, const char* help, const char* hint = "") { command_ = command; func_ = func; help_= help; hint_ = hint;}; 27 | 28 | const esp_console_cmd_t toCommandStruct() const override 29 | { 30 | const esp_console_cmd_t cmd = { 31 | .command = command_, 32 | .help = help_, 33 | .hint = hint_, 34 | .func = func_, 35 | .argtable = nullptr 36 | }; 37 | 38 | return cmd; 39 | } 40 | }; 41 | }; -------------------------------------------------------------------------------- /src/ESP32Console/OptionsConsoleCommand.cpp: -------------------------------------------------------------------------------- 1 | #include "./OptionsConsoleCommand.h" 2 | #include "Arduino.h" 3 | 4 | const static char *TAG = "ArgParseCommand"; 5 | 6 | namespace ESP32Console 7 | { 8 | std::unordered_map OptionsConsoleCommand::registry_ = std::unordered_map(); 9 | 10 | int OptionsConsoleCommand::delegateResolver(int argc, char **argv) 11 | { 12 | // Retrieve ArgParserCommand from registry 13 | auto it = registry_.find(argv[0]); 14 | if (it == registry_.end()) 15 | { 16 | log_e("Could not resolve the delegated function call!"); 17 | return 1; 18 | } 19 | 20 | auto command = it->second; 21 | 22 | int code = 0; 23 | 24 | try 25 | { 26 | auto options = command.options; 27 | auto result = options.parse(argc, argv); 28 | 29 | //Print help on --help argument 30 | if (result.count("help")) { 31 | printf(options.help().c_str()); 32 | printf("\n"); 33 | return EXIT_SUCCESS; 34 | } 35 | 36 | if (command.getVersion() && result.count("version")) { 37 | printf("Version: %s\n", command.getVersion()); 38 | return EXIT_SUCCESS; 39 | } 40 | 41 | return command.delegateFn_(argc, argv, result, options); 42 | } 43 | catch (const std::exception &err) 44 | { 45 | printf(err.what()); 46 | printf("\n"); 47 | return EXIT_FAILURE; 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/ESP32Console/Helpers/PWDHelpers.cpp: -------------------------------------------------------------------------------- 1 | #include "ESP32Console/Helpers/PWDHelpers.h" 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | namespace ESP32Console 8 | { 9 | constexpr char *PWD_DEFAULT = (char*) "/"; 10 | 11 | const char *console_getpwd() 12 | { 13 | char *pwd = getenv("PWD"); 14 | if (pwd) 15 | { // If we have defined a PWD value, return it 16 | return pwd; 17 | } 18 | 19 | // Otherwise set a default one 20 | setenv("PWD", PWD_DEFAULT, 1); 21 | return PWD_DEFAULT; 22 | } 23 | 24 | const char *console_realpath(const char *path, char *resolvedPath) 25 | { 26 | String in = String(path); 27 | String pwd = String(console_getpwd()); 28 | String result; 29 | // If path is not absolute we prepend our pwd 30 | if (!in.startsWith("/")) 31 | { 32 | result = pwd + "/" + in; 33 | } 34 | else 35 | { 36 | result = in; 37 | } 38 | 39 | realpath(result.c_str(), resolvedPath); 40 | return resolvedPath; 41 | } 42 | 43 | int console_chdir(const char *path) 44 | { 45 | char buffer[PATH_MAX + 2]; 46 | console_realpath(path, buffer); 47 | 48 | size_t buffer_len = strlen(buffer); 49 | //If path does not end with slash, add it. 50 | if(buffer[buffer_len - 1] != '/') 51 | { 52 | buffer[buffer_len] = '/'; 53 | buffer[buffer_len + 1] = '\0'; 54 | } 55 | 56 | setenv("PWD", buffer, 1); 57 | 58 | return 0; 59 | } 60 | 61 | } -------------------------------------------------------------------------------- /examples/argparser/argparser.ino: -------------------------------------------------------------------------------- 1 | /** 2 | * This example demonstrates the usage of OptionsConsoleCommand, which allows you to easily build commands with complex options and argument parsing. 3 | */ 4 | 5 | #include 6 | 7 | #include "ESP32Console.h" 8 | 9 | using namespace ESP32Console; 10 | 11 | Console console; 12 | 13 | void setup() 14 | { 15 | console.begin(115200); 16 | 17 | // Define a new command. Every command has an --option argument which shows the possible arguments and (if defined) a --version option which shows the version of this command. 18 | OptionsConsoleCommand test( 19 | "test", // The command name 20 | [](int argc, char **argv, ParseResult result, Options options) -> int { // The function which is called when command is called. You get the parsed arguments and the options object 21 | 22 | printf("Lambda function test\n"); 23 | 24 | // Check if argument was passed, then print it: 25 | if (result.count("i")) 26 | { 27 | int i = result["i"].as(); 28 | printf("Selected mode: %d\n", i); 29 | } 30 | 31 | return EXIT_SUCCESS; 32 | }, 33 | "Just a test command!", 34 | "v1.0.0"); //You can define a version number which is shown when the command is called with --version 35 | 36 | //Customize the options of the console object. See https://github.com/jarro2783/cxxopts for explaination 37 | test.options.add_options()("i,integer", "Int param", cxxopts::value()); 38 | 39 | //Register it like any other command 40 | console.registerCommand(test); 41 | } 42 | 43 | void loop() 44 | { 45 | // Console works async in its own task, so you can do whatever you want in your loop() function. 46 | } -------------------------------------------------------------------------------- /src/ESP32Console/ConsoleCommandD.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "./ConsoleCommand.h" 4 | #include "esp_console.h" 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | 11 | namespace ESP32Console 12 | { 13 | using delegateFunc = std::function; 14 | 15 | /** 16 | * @brief A class for registering custom console commands via delegate function element. The difference to ConsoleCommand is that you can pass a std::function object instead of a function pointer directly. 17 | * This allows for use of lambda functions. The disadvantage is that we need more heap, as we have to save the delegate function objects in a map. 18 | * 19 | */ 20 | class ConsoleCommandD : public ConsoleCommand 21 | { 22 | protected: 23 | delegateFunc delegateFn_; 24 | 25 | static int delegateResolver(int argc, char **argv); 26 | 27 | public: 28 | static std::unordered_map registry_; 29 | 30 | ConsoleCommandD(const char *command, delegateFunc func, const char* help, const char* hint = ""): ConsoleCommand(command, &delegateResolver, help, hint), delegateFn_(func) {}; 31 | 32 | const esp_console_cmd_t toCommandStruct() const override 33 | { 34 | const esp_console_cmd_t cmd = { 35 | .command = command_, 36 | .help = help_, 37 | .hint = hint_, 38 | .func = func_, 39 | .argtable = nullptr 40 | }; 41 | 42 | // When the command gets registered add it to our map, so we can access it later to resolve the delegated function call 43 | registry_.insert({std::string(command_), std::move(delegateFn_)}); 44 | 45 | return cmd; 46 | } 47 | 48 | delegateFunc &getDelegateFunction() { return delegateFn_; } 49 | }; 50 | 51 | } -------------------------------------------------------------------------------- /src/ESP32Console/Helpers/InputParser.cpp: -------------------------------------------------------------------------------- 1 | #include "./InputParser.h" 2 | 3 | #include 4 | 5 | namespace ESP32Console 6 | { 7 | String interpolateLine(const char *in_) 8 | { 9 | String in(in_); 10 | String out = in; 11 | 12 | // Add a space at end of line, this does not change anything for our consoleLine and makes parsing easier 13 | in = in + " "; 14 | 15 | // Interpolate each $ with the env variable if existing. If $ is the first character in a line it is not interpolated 16 | int var_index = 1; 17 | while ((var_index = in.indexOf("$", var_index + 1)) > 0) 18 | { 19 | /** 20 | * Extract the possible env variable 21 | */ 22 | int variable_start = var_index + 1; 23 | // If the char after $ is a { we look for an closing }. Otherwise we just look for an space 24 | char delimiter = ' '; 25 | if (in.charAt(variable_start) == '{') 26 | { 27 | // Our variable starts then at the character after ${ 28 | variable_start++; 29 | } 30 | 31 | int variable_end = in.indexOf(delimiter, variable_start + 1); 32 | // If delimiter not found look for next possible env variable 33 | if (variable_end == -1) 34 | { 35 | continue; 36 | } 37 | 38 | String env_var = in.substring(variable_start, variable_end); 39 | env_var.trim(); 40 | // Depending on whether this is an variable string, we have to include the next character 41 | String replace_target = in.substring(var_index, delimiter == '}' ? variable_end + 1 : variable_end); 42 | 43 | // Check if we have an env with this name, then replace it 44 | const char *value = getenv(env_var.c_str()); 45 | if (value) 46 | { 47 | out.replace(replace_target.c_str(), value); 48 | } 49 | } 50 | 51 | return out.c_str(); 52 | } 53 | } -------------------------------------------------------------------------------- /examples/basic/basic.ino: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "ESP32Console.h" 4 | 5 | using namespace ESP32Console; 6 | 7 | Console console; 8 | 9 | constexpr int LED = 2; 10 | 11 | int led(int argc, char **argv) 12 | { 13 | //Ensure that we have an argument to parse 14 | if (argc != 2) 15 | { 16 | printf("You have to give 'on' or 'off' as a argument (e.g. 'led on')\n"); 17 | 18 | //Return EXIT_FAILURE if something did not worked. 19 | return EXIT_FAILURE; 20 | } 21 | 22 | //Take the first argument... 23 | auto arg = String(argv[1]); 24 | 25 | //and use it to decide what to do with the LED 26 | if (arg == "on") { 27 | digitalWrite(LED, HIGH); 28 | printf("LED is now on\n"); 29 | } else if(arg == "off") { 30 | digitalWrite(LED, LOW); 31 | printf("LED is now off\n"); 32 | } else { 33 | printf("Unknown argument!\n"); 34 | return EXIT_FAILURE; 35 | } 36 | 37 | //Return EXIT_SUCCESS if everything worked as intended. 38 | return EXIT_SUCCESS; 39 | } 40 | 41 | void setup() 42 | { 43 | pinMode(LED, OUTPUT); 44 | 45 | //You can change the console prompt before calling begin(). By default it is "ESP32>" 46 | console.setPrompt("MyConsole> "); 47 | 48 | //You can change the baud rate and pin numbers similar to Serial.begin() here. 49 | console.begin(115200); 50 | 51 | //Register builtin commands like 'reboot', 'version', or 'meminfo' 52 | console.registerSystemCommands(); 53 | 54 | //Register our own command 55 | //First argument is the name with which the command can be executed, second argument is the function to execute and third one is the description shown in help command. 56 | console.registerCommand(ConsoleCommand("led", &led, "Turn the LED on or off")); 57 | 58 | //With ConsoleCommandD you can use lambda functions (and anything else that can be cast to std::function). This needs a bit more memory and CPU time than the normal ConsoleCommand. 59 | console.registerCommand(ConsoleCommandD("test", [](int argc, char **argv) -> int { 60 | printf("Lambda function test\n"); 61 | return EXIT_SUCCESS; 62 | }, "Just a test command!")); 63 | 64 | //When console is in use, we can not use Serial.print but you can use printf to output text 65 | printf("\n\nWelcome to ESP32Console example. Try out typing 'led off' and 'led on' (without quotes) or see 'help' for all commands."); 66 | } 67 | 68 | void loop() 69 | { 70 | //Console works async in its own task, so you can do whatever you want in your loop() function. 71 | } -------------------------------------------------------------------------------- /src/ESP32Console/OptionsConsoleCommand.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "./ConsoleCommandBase.h" 4 | 5 | #include "esp_console.h" 6 | 7 | //This define is important, otherwise we get very high memory usage from regex 8 | #define CXXOPTS_NO_REGEX 1 9 | #define CXXOPTS_NO_RTTI 1 10 | #include "cxxopts/cxxopts.hpp" 11 | #include 12 | #include 13 | #include 14 | 15 | namespace ESP32Console 16 | { 17 | using cxxopts::Options; 18 | using cxxopts::ParseResult; 19 | using argParseFunc = std::function; 20 | 21 | class OptionsConsoleCommand : public ConsoleCommandBase 22 | { 23 | protected: 24 | argParseFunc delegateFn_; 25 | const char *hint_; 26 | const char *version_; 27 | 28 | static int delegateResolver(int argc, char **argv); 29 | 30 | public: 31 | Options options; 32 | static std::unordered_map registry_; 33 | 34 | OptionsConsoleCommand(const char *command, argParseFunc func, const char *help, const char* version = nullptr, const char *hint = nullptr): options(command, help) 35 | { 36 | command_ = command; 37 | help_ = help; 38 | version_ = version; 39 | 40 | if (hint) 41 | { 42 | hint_ = hint; 43 | } 44 | else 45 | { 46 | hint_ = "Use --help option of command for more info"; 47 | } 48 | 49 | //Add an option 50 | options.add_options() 51 | ("help", "Show help", cxxopts::value()->default_value("false")) 52 | ; 53 | 54 | if (version_) 55 | { 56 | options.add_options() 57 | ("version", "Show version number of this command", cxxopts::value()->default_value("false")) 58 | ; 59 | } 60 | 61 | 62 | 63 | delegateFn_ = func; 64 | func_ = &delegateResolver; 65 | } 66 | 67 | const esp_console_cmd_t toCommandStruct() const override 68 | { 69 | const esp_console_cmd_t cmd = { 70 | .command = command_, 71 | .help = help_, 72 | .hint = hint_, 73 | .func = func_, 74 | .argtable = nullptr 75 | }; 76 | 77 | // When the command gets registered add it to our map, so we can access it later to resolve the delegated function call 78 | registry_.insert({std::string(command_), std::move(*this)}); 79 | 80 | return cmd; 81 | } 82 | 83 | argParseFunc &getDelegateFunction() { return delegateFn_; } 84 | 85 | const char* getVersion() const {return version_;}; 86 | }; 87 | } -------------------------------------------------------------------------------- /commands.md: -------------------------------------------------------------------------------- 1 | # Included command reference 2 | 3 | ## Core commands (automatically loaded, when begin() is called) 4 | 5 | * `help`: Show a list of all possible commands with descriptions 6 | * `clear`: Clear terminal screen using ANSI commands (only working when using an ANSI compatible terminal) 7 | * `echo`: Echo the parameter strings 8 | * `history`: Show the recent command history 9 | * `multiline_mode`: Switch multiline mode on or off. When it is on, lines get break into a second line, if it gets too long. 10 | * `env`: List all environment variables 11 | * `declare`: Change the value of an environment variable (syntax: `declare VARIABLE Value` or `declare VARIABLE "Long Value"`) 12 | 13 | ## System commands (loaded by calling registerSystemCommands()) 14 | 15 | * `sysinfo`: Prints info about the chip model, revision, ESP-IDF version, EFuse MAC, flash and PSRAM 16 | * `restart`: Restarts the system 17 | * `meminfo`: Show informations about used and free Heap 18 | * `date`: Shows and set the current system time. Change time with `date -s "2022-07-13 22:47:00"`. Timezone can be set by changing the `TZ` env variable (e.g. `declare TZ CET-1`). 19 | 20 | ## Network commands (loaded by calling registerNetworkCommands()) 21 | 22 | * `ipconfig`: Shows information about WiFi connection 23 | * `ping [HOST]`: Pings a hostname. You can change the number of pings done with `-n`parameter. Use `-n 0` for infinite pinging. You can stop the ping by `Strg + C` or `Strg + D`. 24 | 25 | ## VFS commands (loaded by calling registerVFSCommands()) 26 | 27 | This functions allows you to navigate through and edit files in ESP-IDFs Virtual Filesystem. Things likes SPIFF, SDCards, some hardware and more are getting mounted into VFS with different prefixes. 28 | The following commands allow an unified access on it. 29 | 30 | * `pwd`: Show the current working directory (the directory we are currently in) 31 | * `cd [PATH]`: Change the current directory 32 | * `ls [PATH]`: List the contents of the current or given directory 33 | * `cat [FILES...]`: Show the content of the given files 34 | * `rm [FILE]`: Delete the given file 35 | * `rmdir [DIR]`: Delete the given director 36 | * `mv [ORIGIN] [TARGET]`: Moves/Rename the file to new name/posiion 37 | * `cp [ORIGIN] [TARGET]`: Copies the contents of origin to target file 38 | * `edit [FILE]`: Opens a file editor with a visual editor. Use `Strg + S` to save, `Strg + Q` to quit and `Strg + F` to search in file. 39 | 40 | ## GPIO commands (loaded by calling registerGPIOCommands()) 41 | The commands allow you to read and change the states of the ESPs GPIO pins. They are similar to the arduino included functions: 42 | 43 | * `pinMode [PIN] [MODE]`: Change the pinMode of an GPIO 44 | * `digitalRead [PIN]`: Reads the state of an digital GPIO 45 | * `digitalWrite [PIN] [LEVEL]`: Changes the state of an digital GPIO 46 | * `analogRead [PIN]`: Reads the voltage applied to an analog GPIO in millivolts -------------------------------------------------------------------------------- /src/ESP32Console/Commands/CoreCommands.cpp: -------------------------------------------------------------------------------- 1 | #include "./CoreCommands.h" 2 | #include "linenoise/linenoise.h" 3 | #include "Arduino.h" 4 | #include "soc/soc_caps.h" 5 | //#include "argparse/argparse.hpp" 6 | 7 | static int clear(int argc, char **argv) 8 | { 9 | // If we are on a dumb erminal clearing does not work 10 | if (linenoiseProbe()) 11 | { 12 | printf("\nYour terminal does not support escape sequences. Clearing screen does not work!\n"); 13 | return EXIT_FAILURE; 14 | } 15 | 16 | linenoiseClearScreen(); 17 | return EXIT_SUCCESS; 18 | } 19 | 20 | static int echo(int argc, char **argv) 21 | { 22 | for (int n = 1; n 1) 69 | { 70 | if (strcasecmp(argv[1], "-c")) 71 | { // When -c option was detected clear history. 72 | linenoiseHistorySetMaxLen(0); 73 | printf("History cleared!\n"); 74 | linenoiseHistorySetMaxLen(10); 75 | return EXIT_SUCCESS; 76 | } 77 | else 78 | { 79 | printf("Invalid argument. Use -c to clear history.\n"); 80 | 81 | return EXIT_FAILURE; 82 | } 83 | } 84 | else*/ 85 | { // Without arguments we just output the history 86 | // We use the ESP-IDF VFS to directly output the file to an UART. UART channel 0 has the path /dev/uart/0 and so on. 87 | char path[12] = {0}; 88 | snprintf(path, 12, "/dev/uart/%d", history_channel); 89 | 90 | // If we found the correct one, let linoise save (output) them. 91 | linenoiseHistorySave(path); 92 | return EXIT_SUCCESS; 93 | } 94 | 95 | return EXIT_FAILURE; 96 | } 97 | 98 | extern char **environ; 99 | 100 | static int env(int argc, char **argv) 101 | { 102 | char **s = environ; 103 | 104 | for (; *s; s++) 105 | { 106 | printf("%s\n", *s); 107 | } 108 | return EXIT_SUCCESS; 109 | } 110 | 111 | static int declare(int argc, char **argv) 112 | { 113 | if (argc != 3) { 114 | fprintf(stderr, "Syntax: declare VAR short OR declare VARIABLE \"Long Value\"\n"); 115 | return EXIT_FAILURE; 116 | } 117 | 118 | setenv(argv[1], argv[2], 1); 119 | 120 | return EXIT_SUCCESS; 121 | } 122 | 123 | namespace ESP32Console::Commands 124 | { 125 | const ConsoleCommand getClearCommand() 126 | { 127 | return ConsoleCommand("clear", &clear, "Clears the screen using ANSI codes"); 128 | } 129 | 130 | const ConsoleCommand getEchoCommand() 131 | { 132 | return ConsoleCommand("echo", &echo, "Echos the text supplied as argument"); 133 | } 134 | 135 | const ConsoleCommand getSetMultilineCommand() 136 | { 137 | return ConsoleCommand("multiline_mode", &set_multiline_mode, "Sets the multiline mode of the console"); 138 | } 139 | 140 | const ConsoleCommand getHistoryCommand(int uart_channel) 141 | { 142 | history_channel = uart_channel; 143 | return ConsoleCommand("history", &history, "Shows and clear command history (using -c parameter)"); 144 | } 145 | 146 | const ConsoleCommand getEnvCommand() 147 | { 148 | return ConsoleCommand("env", &env, "List all environment variables."); 149 | } 150 | 151 | const ConsoleCommand getDeclareCommand() 152 | { 153 | return ConsoleCommand("declare", &declare, "Change enviroment variables"); 154 | } 155 | } -------------------------------------------------------------------------------- /src/ESP32Console/Console.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #if !defined(ESP32) 4 | #error This library depends on ESP-IDF and only works on ESP32! 5 | #endif 6 | 7 | #include "esp_console.h" 8 | #include "Arduino.h" 9 | #include "./ConsoleCommandBase.h" 10 | #include "freertos/task.h" 11 | #include "linenoise/linenoise.h" 12 | 13 | namespace ESP32Console 14 | { 15 | class Console 16 | { 17 | private: 18 | const char *prompt_ = "ESP32> "; 19 | const uint32_t task_priority_; 20 | const BaseType_t task_stack_size_; 21 | 22 | uint16_t max_history_len_ = 40; 23 | const char* history_save_path_ = nullptr; 24 | 25 | const size_t max_cmdline_len_; 26 | const size_t max_cmdline_args_; 27 | 28 | uint8_t uart_channel_; 29 | 30 | TaskHandle_t task_; 31 | 32 | static void repl_task(void *args); 33 | 34 | void beginCommon(); 35 | 36 | public: 37 | /** 38 | * @brief Create a new ESP32Console with the default parameters 39 | */ 40 | Console(const uint32_t task_stack_size = 4096, const BaseType_t task_priority = 2, int max_cmdline_len = 256, int max_cmdline_args = 8) : task_priority_(task_priority), task_stack_size_(task_stack_size), max_cmdline_len_(max_cmdline_len), max_cmdline_args_(max_cmdline_args){}; 41 | 42 | ~Console() 43 | { 44 | vTaskDelete(task_); 45 | end(); 46 | } 47 | 48 | /** 49 | * @brief Register the given command, using the raw ESP-IDF structure. 50 | * 51 | * @param cmd The command that should be registered 52 | * @return Return true, if the registration was successfull, false if not. 53 | */ 54 | bool registerCommand(const esp_console_cmd_t *cmd) 55 | { 56 | log_v("Registering new command %s", cmd->command); 57 | 58 | auto code = esp_console_cmd_register(cmd); 59 | if (code != ESP_OK) 60 | { 61 | log_e("Error registering command (Reason %s)", esp_err_to_name(code)); 62 | return false; 63 | } 64 | 65 | return true; 66 | } 67 | 68 | /** 69 | * @brief Register the given command 70 | * 71 | * @param cmd The command that should be registered 72 | * @return true If the command was registered successful. 73 | * @return false If the command was not registered because of an error. 74 | */ 75 | bool registerCommand(const ConsoleCommandBase &cmd) 76 | { 77 | auto c = cmd.toCommandStruct(); 78 | return registerCommand(&c); 79 | } 80 | 81 | /** 82 | * @brief Registers the given command 83 | * 84 | * @param command The name under which the command can be called (e.g. "ls"). Must not contain spaces. 85 | * @param func A pointer to the function which should be run, when this command is called 86 | * @param help A text shown in output of "help" command describing this command. When empty it is not shown in help. 87 | * @param hint A text describing the usage of the command in help output 88 | * @return true If the command was registered successful. 89 | * @return false If the command was not registered because of an error. 90 | */ 91 | bool registerCommand(const char *command, esp_console_cmd_func_t func, const char *help, const char *hint = "") 92 | { 93 | const esp_console_cmd_t cmd = { 94 | .command = command, 95 | .help = help, 96 | .hint = hint, 97 | .func = func, 98 | .argtable = nullptr 99 | }; 100 | 101 | return registerCommand(&cmd); 102 | }; 103 | 104 | void registerCoreCommands(); 105 | 106 | void registerSystemCommands(); 107 | 108 | void registerNetworkCommands(); 109 | 110 | void registerVFSCommands(); 111 | 112 | void registerGPIOCommands(); 113 | 114 | /** 115 | * @brief Set the command prompt. Default is "ESP32>". 116 | * 117 | * @param prompt 118 | */ 119 | void setPrompt(const char *prompt) { prompt_ = prompt; }; 120 | 121 | /** 122 | * @brief Set the History Max Length object 123 | * 124 | * @param max_length 125 | */ 126 | void setHistoryMaxLength(uint16_t max_length) 127 | { 128 | max_history_len_ = max_length; 129 | linenoiseHistorySetMaxLen(max_length); 130 | } 131 | 132 | /** 133 | * @brief Enable saving of command history, which makes history persistent over resets. SPIFF need to be enabled, or you need to pass the filename to use. 134 | * 135 | * @param history_save_path The file which will be used to save command history. Set to nullptr to disable persistent saving 136 | */ 137 | void enablePersistentHistory(const char *history_save_path = "/spiffs/.history.txt") { history_save_path_ = history_save_path; }; 138 | 139 | /** 140 | * @brief Starts the console. Similar to the Serial.begin() function 141 | * 142 | * @param baud The baud rate with which the console should work. Recommended: 115200 143 | * @param rxPin The pin to use for RX 144 | * @param txPin The pin to use for TX 145 | * @param channel The number of the UART to use 146 | */ 147 | void begin(int baud, int rxPin = -1, int txPin = -1, uint8_t channel = 0); 148 | 149 | void end(); 150 | }; 151 | }; -------------------------------------------------------------------------------- /src/ESP32Console/Commands/GPIOCommands.cpp: -------------------------------------------------------------------------------- 1 | #include "./GPIOCommands.h" 2 | #include "Arduino.h" 3 | 4 | static int _pinmode(int argc, char **argv) 5 | { 6 | if (argc != 3) 7 | { 8 | printf("You have to pass a pin number and mode. Syntax: pinMode [GPIO] [MODE]\n"); 9 | return 1; 10 | } 11 | 12 | char *pin_str = argv[1]; 13 | String mode_str = String(argv[2]); 14 | 15 | unsigned long pin = 0; 16 | 17 | try 18 | { 19 | pin = std::stoul(pin_str); 20 | } 21 | catch (std::invalid_argument ex) 22 | { 23 | fprintf(stderr, "Invalid argument for pin: %s\n", ex.what()); 24 | return 1; 25 | } 26 | 27 | if (pin > 255 || !digitalPinIsValid(pin)) { 28 | fprintf(stderr, "%d is not a GPIO pin\n", pin); 29 | return 1; 30 | } 31 | 32 | int mode = INPUT; 33 | 34 | if (mode_str.equalsIgnoreCase("INPUT")) 35 | { 36 | mode = INPUT; 37 | } 38 | else if (mode_str.equalsIgnoreCase("OUTPUT")) 39 | { 40 | mode = OUTPUT; 41 | } 42 | else if (mode_str.equalsIgnoreCase("INPUT_PULLUP")) 43 | { 44 | mode = INPUT_PULLUP; 45 | } 46 | else if (mode_str.equalsIgnoreCase("INPUT_PULLDOWN")) 47 | { 48 | mode = INPUT_PULLDOWN; 49 | } 50 | else if (mode_str.equalsIgnoreCase("OUTPUT_OPEN_DRAIN")) 51 | { 52 | mode = OUTPUT_OPEN_DRAIN; 53 | } 54 | else 55 | { 56 | fprintf(stderr, "Invalid mode: Allowed modes are INPUT, OUTPUT, INPUT_PULLUP, INPUT_PULLDOWN, OUTPUT_OPEN_DRAIN\n"); 57 | } 58 | 59 | pinMode(pin, mode); 60 | printf("Mode set successful.\n"); 61 | 62 | return 0; 63 | } 64 | 65 | static int _digitalWrite(int argc, char** argv) 66 | { 67 | if (argc != 3) 68 | { 69 | printf("You have to pass an pin number and level. Syntax: digitalWrite [GPIO] [Level]\n"); 70 | return 1; 71 | } 72 | 73 | char *pin_str = argv[1]; 74 | String mode_str = String(argv[2]); 75 | 76 | unsigned long pin = 0; 77 | 78 | try 79 | { 80 | pin = std::stoul(pin_str); 81 | } 82 | catch (std::invalid_argument ex) 83 | { 84 | fprintf(stderr, "Invalid argument for pin: %s\n", ex.what()); 85 | return 1; 86 | } 87 | 88 | if (pin > 255 || !digitalPinCanOutput(pin)) { 89 | fprintf(stderr, "%d is not a GPIO pin\n", pin); 90 | return 1; 91 | } 92 | 93 | int mode = LOW; 94 | 95 | if (mode_str.equalsIgnoreCase("HIGH") || mode_str.equalsIgnoreCase("1")) 96 | { 97 | mode = HIGH; 98 | } 99 | else if (mode_str.equalsIgnoreCase("LOW") || mode_str.equalsIgnoreCase("0")) 100 | { 101 | mode = LOW; 102 | } else 103 | { 104 | fprintf(stderr, "Invalid mode: Allowed levels are HIGH, LOW, 0 and 1\n"); 105 | } 106 | 107 | pinMode(pin, mode); 108 | printf("Output set successful.\n"); 109 | 110 | return 0; 111 | } 112 | 113 | static int _digitalRead(int argc, char** argv) 114 | { 115 | if (argc != 2) 116 | { 117 | printf("You have to pass an pin number to read\n"); 118 | return 1; 119 | } 120 | 121 | char *pin_str = argv[1]; 122 | 123 | unsigned long pin = 0; 124 | 125 | try 126 | { 127 | pin = std::stoul(pin_str); 128 | } 129 | catch (std::invalid_argument ex) 130 | { 131 | fprintf(stderr, "Invalid argument for pin: %s\n", ex.what()); 132 | return 1; 133 | } 134 | 135 | if (pin > 255 || !digitalPinCanOutput(pin)) { 136 | fprintf(stderr, "%d is not a GPIO pin\n", pin); 137 | return 1; 138 | } 139 | 140 | auto level = digitalRead(pin); 141 | 142 | if(level == HIGH) { 143 | printf("HIGH\n"); 144 | } else if(level == LOW) { 145 | printf("LOW\n"); 146 | } else { 147 | fprintf(stderr, "Unknown state (%u) of pin %u!\n", level, pin); 148 | return 1; 149 | } 150 | 151 | return 0; 152 | } 153 | 154 | static int _analogRead(int argc, char** argv) 155 | { 156 | if (argc != 2) 157 | { 158 | printf("You have to pass an pin number to read\n"); 159 | return 1; 160 | } 161 | 162 | char *pin_str = argv[1]; 163 | 164 | unsigned long pin = 0; 165 | 166 | try 167 | { 168 | pin = std::stoul(pin_str); 169 | } 170 | catch (std::invalid_argument ex) 171 | { 172 | fprintf(stderr, "Invalid argument for pin: %s\n", ex.what()); 173 | return 1; 174 | } 175 | 176 | if (pin > 255 || digitalPinToAnalogChannel(pin) == -1) { 177 | fprintf(stderr, "%d is not a ADC pin\n", pin); 178 | return 1; 179 | } 180 | 181 | auto value = analogReadMilliVolts(pin); 182 | 183 | printf("%u mV\n", value); 184 | 185 | return 0; 186 | } 187 | 188 | 189 | 190 | namespace ESP32Console::Commands 191 | { 192 | const ConsoleCommand getPinModeCommand() 193 | { 194 | return ConsoleCommand("pinMode", &_pinmode, "Changes the pinmode of an GPIO pin (similar to Arduino function)"); 195 | } 196 | 197 | const ConsoleCommand getDigitalWriteCommand() 198 | { 199 | return ConsoleCommand("digitalWrite", &_digitalWrite, "Writes the state of an ouput pin (similar to Arduino function)"); 200 | } 201 | 202 | const ConsoleCommand getDigitalReadCommand() 203 | { 204 | return ConsoleCommand("digitalRead", &_digitalRead, "Reads the state of an input pin (similar to Arduino function)"); 205 | } 206 | 207 | const ConsoleCommand getAnalogReadCommand() 208 | { 209 | return ConsoleCommand("analogRead", &_analogRead, "Show the voltage at an analog pin in millivollts."); 210 | } 211 | } -------------------------------------------------------------------------------- /src/ESP32Console/Commands/SystemCommands.cpp: -------------------------------------------------------------------------------- 1 | #include "./SystemCommands.h" 2 | #include "ESP32Console.h" 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | // For XSTR macros 11 | #include 12 | 13 | static String mac2String(uint64_t mac) 14 | { 15 | byte *ar = (byte *)&mac; 16 | String s; 17 | for (byte i = 0; i < 6; ++i) 18 | { 19 | char buf[3]; 20 | sprintf(buf, "%02X", ar[i]); // J-M-L: slight modification, added the 0 in the format for padding 21 | s += buf; 22 | if (i < 5) 23 | s += ':'; 24 | } 25 | return s; 26 | } 27 | 28 | static const char *getFlashModeStr() 29 | { 30 | switch (ESP.getFlashChipMode()) 31 | { 32 | case FM_DIO: 33 | return "DIO"; 34 | case FM_DOUT: 35 | return "DOUT"; 36 | case FM_FAST_READ: 37 | return "FAST READ"; 38 | case FM_QIO: 39 | return "QIO"; 40 | case FM_QOUT: 41 | return "QOUT"; 42 | case FM_SLOW_READ: 43 | return "SLOW READ"; 44 | case FM_UNKNOWN: 45 | default: 46 | return "UNKNOWN"; 47 | } 48 | } 49 | 50 | static const char *getResetReasonStr() 51 | { 52 | switch (esp_reset_reason()) 53 | { 54 | case ESP_RST_BROWNOUT: 55 | return "Brownout reset (software or hardware)"; 56 | case ESP_RST_DEEPSLEEP: 57 | return "Reset after exiting deep sleep mode"; 58 | case ESP_RST_EXT: 59 | return "Reset by external pin (not applicable for ESP32)"; 60 | case ESP_RST_INT_WDT: 61 | return "Reset (software or hardware) due to interrupt watchdog"; 62 | case ESP_RST_PANIC: 63 | return "Software reset due to exception/panic"; 64 | case ESP_RST_POWERON: 65 | return "Reset due to power-on event"; 66 | case ESP_RST_SDIO: 67 | return "Reset over SDIO"; 68 | case ESP_RST_SW: 69 | return "Software reset via esp_restart"; 70 | case ESP_RST_TASK_WDT: 71 | return "Reset due to task watchdog"; 72 | case ESP_RST_WDT: 73 | return "ESP_RST_WDT"; 74 | 75 | case ESP_RST_UNKNOWN: 76 | default: 77 | return "Unknown"; 78 | } 79 | } 80 | 81 | static int sysInfo(int argc, char **argv) 82 | { 83 | esp_chip_info_t info; 84 | esp_chip_info(&info); 85 | 86 | printf("ESP32Console version: %s\n", ESP32CONSOLE_VERSION); 87 | printf("Arduino Core version: %s (%x)\n", XTSTR(ARDUINO_ESP32_GIT_DESC), ARDUINO_ESP32_GIT_VER); 88 | printf("ESP-IDF Version: %s\n", ESP.getSdkVersion()); 89 | 90 | printf("\n"); 91 | printf("Chip info:\n"); 92 | printf("\tModel: %s\n", ESP.getChipModel()); 93 | printf("\tRevison number: %d\n", ESP.getChipRevision()); 94 | printf("\tCores: %d\n", ESP.getChipCores()); 95 | printf("\tClock: %d MHz\n", ESP.getCpuFreqMHz()); 96 | printf("\tFeatures:%s%s%s%s%s\r\n", 97 | info.features & CHIP_FEATURE_WIFI_BGN ? " 802.11bgn " : "", 98 | info.features & CHIP_FEATURE_BLE ? " BLE " : "", 99 | info.features & CHIP_FEATURE_BT ? " BT " : "", 100 | info.features & CHIP_FEATURE_EMB_FLASH ? " Embedded-Flash " : " External-Flash ", 101 | info.features & CHIP_FEATURE_EMB_PSRAM ? " Embedded-PSRAM" : ""); 102 | 103 | printf("EFuse MAC: %s\n", mac2String(ESP.getEfuseMac()).c_str()); 104 | 105 | printf("Flash size: %d MB (mode: %s, speed: %d MHz)\n", ESP.getFlashChipSize() / (1024 * 1024), getFlashModeStr(), ESP.getFlashChipSpeed() / (1024 * 1024)); 106 | printf("PSRAM size: %d MB\n", ESP.getPsramSize() / (1024 * 1024)); 107 | 108 | printf("Sketch size: %d KB\n", ESP.getSketchSize() / (1024)); 109 | printf("Sketch MD5: %s\n", ESP.getSketchMD5().c_str()); 110 | 111 | #ifndef CONFIG_APP_REPRODUCIBLE_BUILD 112 | printf("Compilation datetime: " __DATE__ " " __TIME__ "\n"); 113 | #endif 114 | 115 | printf("\nReset reason: %s\n", getResetReasonStr()); 116 | 117 | printf("\n"); 118 | printf("CPU temperature: %.01f °C\n", temperatureRead()); 119 | 120 | return EXIT_SUCCESS; 121 | } 122 | 123 | static int restart(int argc, char **argv) 124 | { 125 | printf("Restarting..."); 126 | ESP.restart(); 127 | return EXIT_SUCCESS; 128 | } 129 | 130 | static int meminfo(int argc, char **argv) 131 | { 132 | uint32_t free = ESP.getFreeHeap() / 1024; 133 | uint32_t total = ESP.getHeapSize() / 1024; 134 | uint32_t used = total - free; 135 | uint32_t min = ESP.getMinFreeHeap() / 1024; 136 | 137 | printf("Heap: %u KB free, %u KB used, (%u KB total)\n", free, used, total); 138 | printf("Minimum free heap size during uptime was: %u KB\n", min); 139 | return EXIT_SUCCESS; 140 | } 141 | 142 | static int date(int argc, char **argv) 143 | { 144 | bool set_time = false; 145 | char *target = nullptr; 146 | 147 | int c; 148 | opterr = 0; 149 | 150 | // Set timezone from env variable 151 | tzset(); 152 | 153 | while ((c = getopt(argc, argv, "s")) != -1) 154 | switch (c) 155 | { 156 | case 's': 157 | set_time = true; 158 | break; 159 | case '?': 160 | printf("Unknown option: %c\n", optopt); 161 | return 1; 162 | case ':': 163 | printf("Missing arg for %c\n", optopt); 164 | return 1; 165 | } 166 | 167 | if (optind < argc) 168 | { 169 | target = argv[optind]; 170 | } 171 | 172 | if (set_time) 173 | { 174 | if (!target) 175 | { 176 | fprintf(stderr, "Set option requires an datetime as argument in format '%Y-%m-%d %H:%M:%S' (e.g. 'date -s \"2022-07-13 22:47:00\"'\n"); 177 | return 1; 178 | } 179 | 180 | tm t; 181 | 182 | if (!strptime(target, "%Y-%m-%d %H:%M:%S", &t)) 183 | { 184 | fprintf(stderr, "Set option requires an datetime as argument in format '%Y-%m-%d %H:%M:%S' (e.g. 'date -s \"2022-07-13 22:47:00\"'\n"); 185 | return 1; 186 | } 187 | 188 | timeval tv = { 189 | .tv_sec = mktime(&t), 190 | .tv_usec = 0}; 191 | 192 | if (settimeofday(&tv, nullptr)) 193 | { 194 | fprintf(stderr, "Could not set system time: %s", strerror(errno)); 195 | return 1; 196 | } 197 | 198 | time_t tmp = time(nullptr); 199 | 200 | constexpr int buffer_size = 100; 201 | char buffer[buffer_size]; 202 | strftime(buffer, buffer_size, "%a %b %e %H:%M:%S %Z %Y", localtime(&tmp)); 203 | printf("Time set: %s\n", buffer); 204 | 205 | return 0; 206 | } 207 | 208 | // If no target was supplied put a default one (similar to coreutils date) 209 | if (!target) 210 | { 211 | target = (char*) "+%a %b %e %H:%M:%S %Z %Y"; 212 | } 213 | 214 | // Ensure the format string is correct 215 | if (target[0] != '+') 216 | { 217 | fprintf(stderr, "Format string must start with an +!\n"); 218 | return 1; 219 | } 220 | 221 | // Ignore + by moving pointer one step forward 222 | target++; 223 | 224 | constexpr int buffer_size = 100; 225 | char buffer[buffer_size]; 226 | time_t t = time(nullptr); 227 | strftime(buffer, buffer_size, target, localtime(&t)); 228 | printf("%s\n", buffer); 229 | return 0; 230 | 231 | return EXIT_SUCCESS; 232 | } 233 | 234 | namespace ESP32Console::Commands 235 | { 236 | const ConsoleCommand getRestartCommand() 237 | { 238 | return ConsoleCommand("restart", &restart, "Restart / Reboot the system"); 239 | } 240 | 241 | const ConsoleCommand getSysInfoCommand() 242 | { 243 | return ConsoleCommand("sysinfo", &sysInfo, "Shows informations about the system like chip model and ESP-IDF version"); 244 | } 245 | 246 | const ConsoleCommand getMemInfoCommand() 247 | { 248 | return ConsoleCommand("meminfo", &meminfo, "Shows information about heap usage"); 249 | } 250 | 251 | const ConsoleCommand getDateCommand() 252 | { 253 | return ConsoleCommand("date", &date, "Shows and modify the system time"); 254 | } 255 | } -------------------------------------------------------------------------------- /src/ESP32Console/Commands/NetworkCommands.cpp: -------------------------------------------------------------------------------- 1 | #include "./NetworkCommands.h" 2 | #include 3 | 4 | #include 5 | #include 6 | #include 7 | #include "lwip/inet.h" 8 | #include "lwip/netdb.h" 9 | #include "lwip/sockets.h" 10 | 11 | #include 12 | 13 | #include "WiFi.h" 14 | 15 | static const char *wlstatus2string(wl_status_t status) 16 | { 17 | switch (status) 18 | { 19 | case WL_NO_SHIELD: 20 | return "Not initialized"; 21 | case WL_CONNECT_FAILED: 22 | return "Connection failed"; 23 | case WL_CONNECTED: 24 | return "Connected"; 25 | case WL_CONNECTION_LOST: 26 | return "Connection lost"; 27 | case WL_DISCONNECTED: 28 | return "Disconnected"; 29 | case WL_IDLE_STATUS: 30 | return "Idle status"; 31 | case WL_NO_SSID_AVAIL: 32 | return "No SSID available"; 33 | case WL_SCAN_COMPLETED: 34 | return "Scan completed"; 35 | default: 36 | return "Unknown"; 37 | } 38 | } 39 | 40 | const char* wlmode2string(wifi_mode_t mode) 41 | { 42 | switch(mode) { 43 | case WIFI_MODE_NULL: 44 | return "Not initialized"; 45 | case WIFI_MODE_AP: 46 | return "Accesspoint"; 47 | case WIFI_MODE_STA: 48 | return "Station"; 49 | case WIFI_MODE_APSTA: 50 | return "Station + Accesspoint"; 51 | default: 52 | return "Unknown"; 53 | } 54 | } 55 | 56 | static void on_ping_success(esp_ping_handle_t hdl, void *args) 57 | { 58 | uint8_t ttl; 59 | uint16_t seqno; 60 | uint32_t elapsed_time, recv_len; 61 | ip_addr_t target_addr; 62 | esp_ping_get_profile(hdl, ESP_PING_PROF_SEQNO, &seqno, sizeof(seqno)); 63 | esp_ping_get_profile(hdl, ESP_PING_PROF_TTL, &ttl, sizeof(ttl)); 64 | esp_ping_get_profile(hdl, ESP_PING_PROF_IPADDR, &target_addr, sizeof(target_addr)); 65 | esp_ping_get_profile(hdl, ESP_PING_PROF_SIZE, &recv_len, sizeof(recv_len)); 66 | esp_ping_get_profile(hdl, ESP_PING_PROF_TIMEGAP, &elapsed_time, sizeof(elapsed_time)); 67 | printf("%u bytes from %s icmp_seq=%d ttl=%d time=%u ms\n", 68 | recv_len, inet_ntoa(target_addr.u_addr.ip4), seqno, ttl, elapsed_time); 69 | } 70 | 71 | static void on_ping_timeout(esp_ping_handle_t hdl, void *args) 72 | { 73 | uint16_t seqno; 74 | ip_addr_t target_addr; 75 | esp_ping_get_profile(hdl, ESP_PING_PROF_SEQNO, &seqno, sizeof(seqno)); 76 | esp_ping_get_profile(hdl, ESP_PING_PROF_IPADDR, &target_addr, sizeof(target_addr)); 77 | printf("From %s icmp_seq=%u timeout\n", inet_ntoa(target_addr.u_addr.ip4), seqno); 78 | } 79 | 80 | static void on_ping_end(esp_ping_handle_t hdl, void *args) 81 | { 82 | } 83 | 84 | static int ping(int argc, char **argv) 85 | { 86 | //By default do 5 pings 87 | int number_of_pings = 5; 88 | 89 | int opt; 90 | while ((opt = getopt(argc, argv, "n:")) != -1) { 91 | switch(opt) { 92 | case 'n': 93 | number_of_pings = atoi(optarg); 94 | break; 95 | case '?': 96 | printf("Unknown option: %c\n", optopt); 97 | break; 98 | case ':': 99 | printf("Missing arg for %c\n", optopt); 100 | break; 101 | 102 | default: 103 | fprintf(stderr, "Usage: ping -n 5 [HOSTNAME]\n"); 104 | fprintf(stderr, "-n: The number of pings. 0 means infinite. Can be aborted with Ctrl+D or Ctrl+C."); 105 | return 1; 106 | } 107 | } 108 | 109 | int argind = optind; 110 | 111 | //Get hostname 112 | if (argind >= argc) { 113 | fprintf(stderr, "You need to pass an hostname!\n"); 114 | return EXIT_FAILURE; 115 | } 116 | 117 | char* hostname = argv[argind]; 118 | 119 | /* convert hostname to IP address */ 120 | ip_addr_t target_addr; 121 | struct addrinfo hint; 122 | struct addrinfo *res = NULL; 123 | memset(&hint, 0, sizeof(hint)); 124 | memset(&target_addr, 0, sizeof(target_addr)); 125 | auto result = getaddrinfo(hostname, NULL, &hint, &res); 126 | 127 | if (result) { 128 | fprintf(stderr, "Could not resolve hostname! (getaddrinfo returned %d)\n", result); 129 | return 1; 130 | } 131 | 132 | struct in_addr addr4 = ((struct sockaddr_in *) (res->ai_addr))->sin_addr; 133 | inet_addr_to_ip4addr(ip_2_ip4(&target_addr), &addr4); 134 | freeaddrinfo(res); 135 | 136 | //Configure ping session 137 | esp_ping_config_t ping_config = ESP_PING_DEFAULT_CONFIG(); 138 | ping_config.task_stack_size = 4096; 139 | ping_config.target_addr = target_addr; // target IP address 140 | ping_config.count = number_of_pings; // 0 means infinite ping 141 | 142 | /* set callback functions */ 143 | esp_ping_callbacks_t cbs; 144 | cbs.on_ping_success = on_ping_success; 145 | cbs.on_ping_timeout = on_ping_timeout; 146 | cbs.on_ping_end = on_ping_end; 147 | //Pass a variable as pointer so the sub tasks can decrease it 148 | //cbs.cb_args = &number_of_pings_remaining; 149 | 150 | esp_ping_handle_t ping; 151 | esp_ping_new_session(&ping_config, &cbs, &ping); 152 | 153 | esp_ping_start(ping); 154 | 155 | char c = 0; 156 | 157 | uint16_t seqno; 158 | esp_ping_get_profile(ping, ESP_PING_PROF_SEQNO, &seqno, sizeof(seqno)); 159 | 160 | //Make stdin input non blocking so we can query for input AND check ping seqno 161 | int flags = fcntl(fileno(stdin), F_GETFL, 0); 162 | fcntl(fileno(stdin), F_SETFL, flags | O_NONBLOCK); 163 | 164 | //Wait for Ctrl+D or Ctr+C or that our task finishes 165 | //The async tasks decrease number_of_pings, so wait for it to get to 0 166 | while((number_of_pings == 0 || seqno < number_of_pings) && c != 4 && c != 3) { 167 | esp_ping_get_profile(ping, ESP_PING_PROF_SEQNO, &seqno, sizeof(seqno)); 168 | c = getc(stdin); 169 | delay(50); 170 | } 171 | 172 | //Reset flags, so we dont end up destroying our terminal env later, when linenoise takes over again 173 | fcntl(fileno(stdin), F_SETFL, flags); 174 | 175 | esp_ping_stop(ping); 176 | //Print total statistics 177 | uint32_t transmitted; 178 | uint32_t received; 179 | uint32_t total_time_ms; 180 | esp_ping_get_profile(ping, ESP_PING_PROF_REQUEST, &transmitted, sizeof(transmitted)); 181 | esp_ping_get_profile(ping, ESP_PING_PROF_REPLY, &received, sizeof(received)); 182 | esp_ping_get_profile(ping, ESP_PING_PROF_DURATION, &total_time_ms, sizeof(total_time_ms)); 183 | printf("%u packets transmitted, %u received, time %u ms\n", transmitted, received, total_time_ms); 184 | 185 | 186 | 187 | esp_ping_delete_session(ping); 188 | 189 | return EXIT_SUCCESS; 190 | } 191 | 192 | static void ipconfig_wlan() 193 | { 194 | printf("==== WLAN ====\n"); 195 | auto status = WiFi.status(); 196 | printf("Mode: %s\n", wlmode2string(WiFi.getMode())); 197 | printf("Status: %s\n", wlstatus2string(status)); 198 | 199 | if (status == WL_NO_SHIELD) { 200 | return; 201 | } 202 | 203 | printf("\n"); 204 | printf("SSID: %s\n", WiFi.SSID().c_str()); 205 | printf("BSSID: %s\n", WiFi.BSSIDstr().c_str()); 206 | printf("Channel: %d\n", WiFi.channel()); 207 | 208 | printf("\n"); 209 | printf("IP: %s\n", WiFi.localIP().toString().c_str()); 210 | printf("Subnet Mask: %s (/%d)\n", WiFi.subnetMask().toString().c_str(), WiFi.subnetCIDR()); 211 | printf("Gateway: %s\n", WiFi.gatewayIP().toString().c_str()); 212 | printf("IPv6: %s\n", WiFi.localIPv6().toString().c_str()); 213 | 214 | printf("\n"); 215 | printf("Hostname: %s\n", WiFi.getHostname()); 216 | printf("DNS1: %s\n", WiFi.dnsIP(0).toString().c_str()); 217 | printf("DNS2: %s\n", WiFi.dnsIP(0).toString().c_str()); 218 | } 219 | 220 | static int ipconfig(int argc, char **argv) 221 | { 222 | ipconfig_wlan(); 223 | return EXIT_SUCCESS; 224 | } 225 | 226 | namespace ESP32Console::Commands 227 | { 228 | const ConsoleCommand getPingCommand() 229 | { 230 | return ConsoleCommand("ping", &ping, "Ping host"); 231 | } 232 | 233 | const ConsoleCommand getIpconfigCommand() 234 | { 235 | return ConsoleCommand("ipconfig", &ipconfig, "Show IP and connection informations"); 236 | } 237 | } -------------------------------------------------------------------------------- /src/ESP32Console/Commands/VFSCommands.cpp: -------------------------------------------------------------------------------- 1 | #include "./VFSCommands.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include "ESP32Console/Helpers/PWDHelpers.h" 10 | 11 | #include "kilo/kilo.h" 12 | 13 | char *canonicalize_file_name(const char *path); 14 | 15 | int cat(int argc, char **argv) 16 | { 17 | if (argc == 1) 18 | { 19 | fprintf(stderr, "You have to pass at least one file path!\n"); 20 | return EXIT_SUCCESS; 21 | } 22 | 23 | for (int n = 1; n < argc; n++) 24 | { 25 | char filename[PATH_MAX]; 26 | // We have manually do resolving of . and .., as VFS does not do it 27 | ESP32Console::console_realpath(argv[n], filename); 28 | 29 | FILE *file = fopen(filename, "r"); 30 | if (file == nullptr) 31 | { 32 | fprintf(stderr, "%s %s : %s\n", argv[0], filename, 33 | strerror(errno)); 34 | return errno; 35 | } 36 | 37 | int chr; 38 | while ((chr = getc(file)) != EOF) 39 | fprintf(stdout, "%c", chr); 40 | fclose(file); 41 | } 42 | 43 | return EXIT_SUCCESS; 44 | } 45 | 46 | int pwd(int argc, char **argv) 47 | { 48 | printf("%s\n", ESP32Console::console_getpwd()); 49 | return EXIT_SUCCESS; 50 | } 51 | 52 | int cd(int argc, char **argv) 53 | { 54 | const char *path; 55 | 56 | if (argc != 2) 57 | { 58 | path = getenv("HOME"); 59 | if (!path) 60 | { 61 | fprintf(stderr, "No HOME env variable set!\n"); 62 | return EXIT_FAILURE; 63 | } 64 | } 65 | else 66 | { 67 | path = argv[1]; 68 | } 69 | 70 | // Check if target path is a file 71 | char resolved[PATH_MAX]; 72 | ESP32Console::console_realpath(path, resolved); 73 | FILE *file = fopen(resolved, "r"); 74 | 75 | // If we can open it, then we can not chdir into it. 76 | if (file) 77 | { 78 | fclose(file); 79 | fprintf(stderr, "Can not cd into a file!\n"); 80 | return 1; 81 | } 82 | 83 | if (ESP32Console::console_chdir(path)) 84 | { 85 | fprintf(stderr, "Error: %s\n", strerror(errno)); 86 | return 1; 87 | } 88 | 89 | const char *pwd = ESP32Console::console_getpwd(); 90 | 91 | // Check if the new PWD exists, and show a warning if not 92 | DIR *dir = opendir(pwd); 93 | if (dir) 94 | { 95 | closedir(dir); 96 | } 97 | else if (ENOENT == errno) 98 | { 99 | fprintf(stderr, "Choosen directory maybe does not exists!\n"); 100 | } 101 | 102 | return EXIT_SUCCESS; 103 | } 104 | 105 | int ls(int argc, char **argv) 106 | { 107 | char *inpath; 108 | if (argc == 1) 109 | { 110 | inpath = (char *)"."; 111 | } 112 | else if (argc == 2) 113 | { 114 | inpath = argv[1]; 115 | } 116 | else 117 | { 118 | printf("You can pass only one filename!\n"); 119 | return 1; 120 | } 121 | 122 | char path[PATH_MAX]; 123 | ESP32Console::console_realpath(inpath, path); 124 | 125 | DIR *dir = opendir(path); 126 | if (!dir) 127 | { 128 | fprintf(stderr, "Could not open filepath: %s\n", strerror(errno)); 129 | return 1; 130 | } 131 | 132 | struct dirent *d; 133 | while ((d = readdir(dir)) != NULL) 134 | { 135 | printf("%s\n", d->d_name); 136 | } 137 | 138 | closedir(dir); 139 | return EXIT_SUCCESS; 140 | } 141 | 142 | int mv(int argc, char **argv) 143 | { 144 | if (argc != 3) 145 | { 146 | fprintf(stderr, "Syntax is mv [ORIGIN] [TARGET]\n"); 147 | return 1; 148 | } 149 | 150 | char old_name[PATH_MAX], new_name[PATH_MAX]; 151 | 152 | // Resolve arguments to full path 153 | ESP32Console::console_realpath(argv[1], old_name); 154 | ESP32Console::console_realpath(argv[2], new_name); 155 | 156 | // Do rename 157 | if (rename(old_name, new_name)) 158 | { 159 | printf("Error moving: %s\n", strerror(errno)); 160 | return EXIT_FAILURE; 161 | } 162 | 163 | return EXIT_SUCCESS; 164 | } 165 | 166 | int cp(int argc, char **argv) 167 | { 168 | //TODO: Shows weird error message 169 | if (argc != 3) 170 | { 171 | fprintf(stderr, "Syntax is cp [ORIGIN] [TARGET]\n"); 172 | return 1; 173 | } 174 | 175 | char old_name[PATH_MAX], new_name[PATH_MAX]; 176 | 177 | // Resolve arguments to full path 178 | ESP32Console::console_realpath(argv[1], old_name); 179 | ESP32Console::console_realpath(argv[2], new_name); 180 | 181 | // Do copy 182 | FILE *origin = fopen(old_name, "r"); 183 | if (!origin) 184 | { 185 | fprintf(stderr, "Error opening origin file: %s\n", strerror(errno)); 186 | return 1; 187 | } 188 | 189 | FILE *target = fopen(new_name, "w"); 190 | if (!target) 191 | { 192 | fclose(origin); 193 | fprintf(stderr, "Error opening target file: %s\n", strerror(errno)); 194 | return 1; 195 | } 196 | 197 | int buffer; 198 | 199 | // Clear existing errors 200 | auto error = errno; 201 | 202 | while ((buffer = getc(origin)) != EOF) 203 | { 204 | if(fputc(buffer, target) == EOF) { 205 | fprintf(stderr, "Error writing: %s\n", strerror(errno)); 206 | fclose(origin); fclose(target); 207 | return 1; 208 | } 209 | } 210 | 211 | error = errno; 212 | if (error && !feof(origin)) 213 | { 214 | fprintf(stderr, "Error copying: %s\n", strerror(error)); 215 | fclose(origin); 216 | fclose(target); 217 | return 1; 218 | } 219 | 220 | fclose(origin); 221 | fclose(target); 222 | 223 | return EXIT_SUCCESS; 224 | } 225 | 226 | int rm(int argc, char **argv) 227 | { 228 | if (argc != 2) 229 | { 230 | fprintf(stderr, "You have to pass exactly one file. Syntax rm [FIILE]\n"); 231 | return EXIT_SUCCESS; 232 | } 233 | 234 | char filename[PATH_MAX]; 235 | ESP32Console::console_realpath(argv[1], filename); 236 | 237 | if(unlink(filename)) { 238 | fprintf(stderr, "Error deleting %s: %s\n", filename, strerror(errno)); 239 | } 240 | 241 | return EXIT_SUCCESS; 242 | } 243 | 244 | int rmdir(int argc, char **argv) 245 | { 246 | if (argc != 2) 247 | { 248 | fprintf(stderr, "You have to pass exactly one file. Syntax rmdir [FIILE]\n"); 249 | return EXIT_SUCCESS; 250 | } 251 | 252 | char filename[PATH_MAX]; 253 | ESP32Console::console_realpath(argv[1], filename); 254 | 255 | if(rmdir(filename)) { 256 | fprintf(stderr, "Error deleting %s: %s\n", filename, strerror(errno)); 257 | } 258 | 259 | return EXIT_SUCCESS; 260 | } 261 | 262 | namespace ESP32Console::Commands 263 | { 264 | const ConsoleCommand getCatCommand() 265 | { 266 | return ConsoleCommand("cat", &cat, "Show the content of one or more files."); 267 | } 268 | 269 | const ConsoleCommand getPWDCommand() 270 | { 271 | return ConsoleCommand("pwd", &pwd, "Show the current working dir"); 272 | } 273 | 274 | const ConsoleCommand getCDCommand() 275 | { 276 | return ConsoleCommand("cd", &cd, "Change the working directory"); 277 | } 278 | 279 | const ConsoleCommand getLsCommand() 280 | { 281 | return ConsoleCommand("ls", &ls, "List the contents of the given path"); 282 | } 283 | 284 | const ConsoleCommand getMvCommand() 285 | { 286 | return ConsoleCommand("mv", &mv, "Move the given file to another place or name"); 287 | } 288 | 289 | const ConsoleCommand getCPCommand() 290 | { 291 | return ConsoleCommand("cp", &cp, "Copy the given file to another place or name"); 292 | } 293 | 294 | const ConsoleCommand getRMCommand() 295 | { 296 | return ConsoleCommand("rm", &rm, "Permanenty deletes the given file."); 297 | } 298 | 299 | const ConsoleCommand getRMDirCommand() 300 | { 301 | return ConsoleCommand("rmdir", &rmdir, "Permanenty deletes the given folder. Folder must be empty!"); 302 | } 303 | 304 | const ConsoleCommand getEditCommand() 305 | { 306 | return ConsoleCommand("edit", &ESP32Console::Kilo::kilo, "Edit files"); 307 | } 308 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ESP32Console 2 | 3 | [![PlatformIO Registry](https://badges.registry.platformio.org/packages/jbtronics/library/ESP32Console.svg)](https://registry.platformio.org/libraries/jbtronics/ESP32Console) 4 | 5 | Arduino library to add a serial console to your ESP32 console. It includes some useful commands and allows to easily add your own custom commands. 6 | 7 | This easily allows you to control and configure your control via serial console in a comfortable and easy way. 8 | 9 | This library encapsulates the Console component included in the ESP-IDF and allows to use it in an easy "Arduino-way". 10 | 11 | ![Screenshot of console output](extras/screenshot.png) 12 | 13 | ## Features 14 | * Simple to use 15 | * Navigatable history, autocompletion with tab for commands (when using an ANSI terminal) 16 | * Persistent history if wished (history gets saved across reboots) 17 | * Many useful commands included like showing infos about system, memory, network and more (see [commands.md](commands.md) for more info) 18 | * Console works in its own asynchronous task, so you can use your arduino loop() function as you like 19 | * Support for environment variables and variable interpolation in commands 20 | * Easy to implement own commands 21 | * Easy to use argument parsing using cxxopts (see `argparser` example) 22 | * Customizable prompt 23 | * Ships a simple file editor to modify and create files locally on system if wanted 24 | 25 | 26 | ## Usage 27 | 28 | ### Installation 29 | This library is available via Arduino Library Manager and [PlatformIO registry](https://registry.platformio.org/libraries/jbtronics/ESP32Console). So just install it via your preferred IDE's library manager and you are ready to start. 30 | 31 | ### Basic 32 | 33 | To use this library you have to do an `#include ` to import all needed files into your project. This library uses namespaces, so you have to do an `using namespace ESP32Console` to not having to prefix all classes (see example below). 34 | 35 | You instantiate an `Console` object and initialize it with `Console.begin(BAUD)`. You can specifiy the baud rate and rx/tx pins similar to `Serial.begin`. Please note that you can use EITHER `Serial` OR `Console`. If you try to start ESP32Console after Serial was inited you will get an error. 36 | 37 | Using `Console.registerCommand()` you can register your own custom commands. You have to pass a command object (see example below), which contains the name of the command, a little help text and the function which should be executed, when this command is executed. There are different types of commands: 38 | * `ConsoleCommand`: The "default" console command, which takes a pointer to a function which is executed on call (similar to arduinos `attachInterrupt` function). The function receives an `int argc` which contains the number of arguments and `char** argv` which contains the arguments itself. The function MUST return an integer. Return 0 if everything was successfull, return something else (e.g. 1) if an error happened. This command type has the lowest memory usage. 39 | * `ConsoleCommandD`: Similar to `ConsoleCommand` but allows to pass an `std::function` object as handler. This allows you to use lambda-functions and `std::bind` 40 | * `OptionsConsoleCommand`: Allows you to define and parse command options and arguments in an easy way. Uses [cxxopts](https://github.com/jarro2783/cxxopts) for arguments parsing. Every command of this type has an `--help` and `--version` option. See `examples/argparser` for usage. 41 | 42 | ### Included commands 43 | ESP32Console includes many useful commands, which can be registered using their respective `registerXXX()` functions. See [commands.md](commands.md) for a detailed list of the commands. 44 | 45 | ### Environment variables 46 | The ESP32Console supports environment variables and string interpolation in the console. You can use `env` command to list all existing environment variables and `declare [VAR] [VALUE]` to change one. In the console prompt `$ENV` and `${ENV}` will get replaced by the value of the defined env value. With that you can for example define a variable with `declare HOST www.github.com` and access it in other commands: `ping $HOST`. 47 | You can change and predefine env variables from your code. See `examples/gpio` for more info. 48 | 49 | ### Computer side 50 | You can use almost any terminal software on PC for connecting with ESP32Console. You can use a simple terminal like the one included in Arduino but it is highly recommended to use a VT100 compatible terminal (e.g. PuTTY on windows). 51 | This kind of terminal is needed for more complex functions like auto-complete with TAB, history scrolling, colors and more. 52 | 53 | If you use a VT100 compatible terminal you can use the keybinds, when in prompt: 54 | * `Ctrl + L`: Clear screen 55 | * `Ctrl + A`: Jump cursor to begin of line 56 | * `Ctrl + E`: Jump cursor to end of line 57 | * `Ctrl + U`: Delete whole line 58 | * `Ctrl + K`: Delete from current position to end of line 59 | * `Ctrl + W`: Delete previous word 60 | * `Ctrl + T`: Swap current character with previous one 61 | 62 | ## Examples 63 | A simple usage can be seen here (see `examples/simple.ino`): 64 | More advanced usages can be found in `examples/` folder. 65 | 66 | ``` 67 | #include 68 | 69 | #include "ESP32Console.h" 70 | 71 | using namespace ESP32Console; 72 | 73 | Console console; 74 | 75 | constexpr int LED = 2; 76 | 77 | int led(int argc, char **argv) 78 | { 79 | //Ensure that we have an argument to parse 80 | if (argc != 2) 81 | { 82 | printf("You have to give 'on' or 'off' as a argument (e.g. 'led on')\n"); 83 | 84 | //Return EXIT_FAILURE if something did not worked. 85 | return EXIT_FAILURE; 86 | } 87 | 88 | //Take the first argument... 89 | auto arg = String(argv[1]); 90 | 91 | //and use it to decide what to do with the LED 92 | if (arg == "on") { 93 | digitalWrite(LED, HIGH); 94 | printf("LED is now on\n"); 95 | } else if(arg == "off") { 96 | digitalWrite(LED, LOW); 97 | printf("LED is now off\n"); 98 | } else { 99 | printf("Unknown argument!\n"); 100 | return EXIT_FAILURE; 101 | } 102 | 103 | //Return EXIT_SUCCESS if everything worked as intended. 104 | return EXIT_SUCCESS; 105 | } 106 | 107 | void setup() 108 | { 109 | pinMode(LED, OUTPUT); 110 | 111 | //You can change the console prompt before calling begin(). By default it is "ESP32>" 112 | console.setPrompt("MyConsole> "); 113 | 114 | //You can change the baud rate and pin numbers similar to Serial.begin() here. 115 | console.begin(115200); 116 | 117 | //Register builtin commands like 'reboot', 'sysinfo', or 'meminfo' 118 | console.registerSystemCommands(); 119 | 120 | //Register our own command 121 | //First argument is the name with which the command can be executed, second argument is the function to execute and third one is the description shown in help command. 122 | console.registerCommand(ConsoleCommand("led", &led, "Turn the LED on or off")); 123 | 124 | //With ConsoleCommandD you can use lambda functions (and anything else that can be cast to std::function). This needs a bit more memory and CPU time than the normal ConsoleCommand. 125 | console.registerCommand(ConsoleCommandD("test", [](int argc, char **argv) -> int { 126 | printf("Lambda function test\n"); 127 | return EXIT_SUCCESS; 128 | }, "Just a test command!")); 129 | 130 | //When console is in use, we can not use Serial.print but you can use printf to output text 131 | printf("\n\nWelcome to ESP32Console example. Try out typing 'led off' and 'led on' (without quotes) or see 'help' for all commands."); 132 | } 133 | 134 | void loop() 135 | { 136 | //Console works async in its own task, so you can do whatever you want in your loop() function. 137 | } 138 | ``` 139 | 140 | ## Credits 141 | * This library utilizes the console component of ESP-IDF written by Espressif at core. 142 | * Argument parsing is done by [cxxopts](https://github.com/jarro2783/cxxopts). 143 | * As editor a modified version of [kilo](https://github.com/antirez/kilo) is used. 144 | 145 | ## LICENSE 146 | ESP32Console is licensed under MIT LICENSE. See [LICENSE file](LICENSE) for more info. 147 | 148 | kilo shipped with ESP32Console is licensed under BSD-2 clause license. See the respective file for more info. 149 | 150 | ## TODO 151 | * Add more useful commands 152 | * Easy integration of colors and console styles 153 | * Support of command batch files 154 | * Add support for ESP8266 (this should be possible in theory as the old RTOS-SDK already ships the console parts) 155 | * Check if more complex terminal stuff, like pipes, output redirection and similar is possible (difficult due only having one global stdout) 156 | -------------------------------------------------------------------------------- /src/ESP32Console/Console.cpp: -------------------------------------------------------------------------------- 1 | #include "./Console.h" 2 | #include "soc/soc_caps.h" 3 | #include "esp_err.h" 4 | #include "ESP32Console/Commands/CoreCommands.h" 5 | #include "ESP32Console/Commands/SystemCommands.h" 6 | #include "ESP32Console/Commands/NetworkCommands.h" 7 | #include "ESP32Console/Commands/VFSCommands.h" 8 | #include "ESP32Console/Commands/GPIOCommands.h" 9 | #include "driver/uart.h" 10 | #include "esp_vfs_dev.h" 11 | #include "linenoise/linenoise.h" 12 | #include "ESP32Console/Helpers/PWDHelpers.h" 13 | #include "ESP32Console/Helpers/InputParser.h" 14 | 15 | static const char *TAG = "ESP32Console"; 16 | 17 | using namespace ESP32Console::Commands; 18 | 19 | namespace ESP32Console 20 | { 21 | void Console::registerCoreCommands() 22 | { 23 | registerCommand(getClearCommand()); 24 | registerCommand(getHistoryCommand()); 25 | registerCommand(getEchoCommand()); 26 | registerCommand(getSetMultilineCommand()); 27 | registerCommand(getEnvCommand()); 28 | registerCommand(getDeclareCommand()); 29 | } 30 | 31 | void Console::registerSystemCommands() 32 | { 33 | registerCommand(getSysInfoCommand()); 34 | registerCommand(getRestartCommand()); 35 | registerCommand(getMemInfoCommand()); 36 | registerCommand(getDateCommand()); 37 | } 38 | 39 | void ESP32Console::Console::registerNetworkCommands() 40 | { 41 | registerCommand(getPingCommand()); 42 | registerCommand(getIpconfigCommand()); 43 | } 44 | 45 | void Console::registerVFSCommands() 46 | { 47 | registerCommand(getCatCommand()); 48 | registerCommand(getCDCommand()); 49 | registerCommand(getPWDCommand()); 50 | registerCommand(getLsCommand()); 51 | registerCommand(getMvCommand()); 52 | registerCommand(getCPCommand()); 53 | registerCommand(getRMCommand()); 54 | registerCommand(getRMDirCommand()); 55 | registerCommand(getEditCommand()); 56 | } 57 | 58 | void Console::registerGPIOCommands() 59 | { 60 | registerCommand(getPinModeCommand()); 61 | registerCommand(getDigitalReadCommand()); 62 | registerCommand(getDigitalWriteCommand()); 63 | registerCommand(getAnalogReadCommand()); 64 | } 65 | 66 | void Console::beginCommon() 67 | { 68 | /* Tell linenoise where to get command completions and hints */ 69 | linenoiseSetCompletionCallback(&esp_console_get_completion); 70 | linenoiseSetHintsCallback((linenoiseHintsCallback *)&esp_console_get_hint); 71 | 72 | /* Set command history size */ 73 | linenoiseHistorySetMaxLen(max_history_len_); 74 | 75 | /* Set command maximum length */ 76 | linenoiseSetMaxLineLen(max_cmdline_len_); 77 | 78 | // Load history if defined 79 | if (history_save_path_) 80 | { 81 | linenoiseHistoryLoad(history_save_path_); 82 | } 83 | 84 | // Register core commands like echo 85 | esp_console_register_help_command(); 86 | registerCoreCommands(); 87 | } 88 | 89 | void Console::begin(int baud, int rxPin, int txPin, uint8_t channel) 90 | { 91 | log_d("Initialize console"); 92 | 93 | if (channel >= SOC_UART_NUM) 94 | { 95 | log_e("Serial number is invalid, please use numers from 0 to %u", SOC_UART_NUM - 1); 96 | return; 97 | } 98 | 99 | this->uart_channel_ = channel; 100 | 101 | //Reinit the UART driver if the channel was already in use 102 | if (uart_is_driver_installed(channel)) { 103 | uart_driver_delete(channel); 104 | } 105 | 106 | /* Drain stdout before reconfiguring it */ 107 | fflush(stdout); 108 | fsync(fileno(stdout)); 109 | 110 | /* Disable buffering on stdin */ 111 | setvbuf(stdin, NULL, _IONBF, 0); 112 | 113 | /* Minicom, screen, idf_monitor send CR when ENTER key is pressed */ 114 | esp_vfs_dev_uart_port_set_rx_line_endings(channel, ESP_LINE_ENDINGS_CR); 115 | /* Move the caret to the beginning of the next line on '\n' */ 116 | esp_vfs_dev_uart_port_set_tx_line_endings(channel, ESP_LINE_ENDINGS_CRLF); 117 | 118 | /* Configure UART. Note that REF_TICK is used so that the baud rate remains 119 | * correct while APB frequency is changing in light sleep mode. 120 | */ 121 | const uart_config_t uart_config = { 122 | .baud_rate = baud, 123 | .data_bits = UART_DATA_8_BITS, 124 | .parity = UART_PARITY_DISABLE, 125 | .stop_bits = UART_STOP_BITS_1, 126 | #if SOC_UART_SUPPORT_REF_TICK 127 | .source_clk = UART_SCLK_REF_TICK, 128 | #elif SOC_UART_SUPPORT_XTAL_CLK 129 | .source_clk = UART_SCLK_XTAL, 130 | #endif 131 | }; 132 | 133 | 134 | ESP_ERROR_CHECK(uart_param_config(channel, &uart_config)); 135 | 136 | // Set the correct pins for the UART of needed 137 | if (rxPin > 0 || txPin > 0) { 138 | if (rxPin < 0 || txPin < 0) { 139 | log_e("Both rxPin and txPin has to be passed!"); 140 | } 141 | uart_set_pin(channel, txPin, rxPin, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE); 142 | } 143 | 144 | /* Install UART driver for interrupt-driven reads and writes */ 145 | ESP_ERROR_CHECK(uart_driver_install(channel, 256, 0, 0, NULL, 0)); 146 | 147 | /* Tell VFS to use UART driver */ 148 | esp_vfs_dev_uart_use_driver(channel); 149 | 150 | esp_console_config_t console_config = { 151 | .max_cmdline_length = max_cmdline_len_, 152 | .max_cmdline_args = max_cmdline_args_, 153 | .hint_color = 333333}; 154 | 155 | ESP_ERROR_CHECK(esp_console_init(&console_config)); 156 | 157 | beginCommon(); 158 | 159 | // Start REPL task 160 | if (xTaskCreate(&Console::repl_task, "console_repl", 4096, this, 2, &task_) != pdTRUE) 161 | { 162 | log_e("Could not start REPL task!"); 163 | } 164 | } 165 | 166 | static void resetAfterCommands() 167 | { 168 | //Reset all global states a command could change 169 | 170 | //Reset getopt parameters 171 | optind = 0; 172 | } 173 | 174 | void Console::repl_task(void *args) 175 | { 176 | Console const &console = *(static_cast(args)); 177 | 178 | /* Change standard input and output of the task if the requested UART is 179 | * NOT the default one. This block will replace stdin, stdout and stderr. 180 | * We have to do this in the repl task (not in the begin, as these settings are only valid for the current task) 181 | */ 182 | if (console.uart_channel_ != CONFIG_ESP_CONSOLE_UART_NUM) 183 | { 184 | char path[13] = {0}; 185 | snprintf(path, 13, "/dev/uart/%d", console.uart_channel_); 186 | 187 | stdin = fopen(path, "r"); 188 | stdout = fopen(path, "w"); 189 | stderr = stdout; 190 | } 191 | 192 | setvbuf(stdin, NULL, _IONBF, 0); 193 | 194 | /* This message shall be printed here and not earlier as the stdout 195 | * has just been set above. */ 196 | printf("\r\n" 197 | "Type 'help' to get the list of commands.\r\n" 198 | "Use UP/DOWN arrows to navigate through command history.\r\n" 199 | "Press TAB when typing command name to auto-complete.\r\n"); 200 | 201 | // Probe terminal status 202 | int probe_status = linenoiseProbe(); 203 | if (probe_status) 204 | { 205 | linenoiseSetDumbMode(1); 206 | } 207 | 208 | if (linenoiseIsDumbMode()) 209 | { 210 | printf("\r\n" 211 | "Your terminal application does not support escape sequences.\n\n" 212 | "Line editing and history features are disabled.\n\n" 213 | "On Windows, try using Putty instead.\r\n"); 214 | } 215 | 216 | linenoiseSetMaxLineLen(console.max_cmdline_len_); 217 | while (1) 218 | { 219 | String prompt = console.prompt_; 220 | 221 | // Insert current PWD into prompt if needed 222 | prompt.replace("%pwd%", console_getpwd()); 223 | 224 | char *line = linenoise(prompt.c_str()); 225 | if (line == NULL) 226 | { 227 | ESP_LOGD(TAG, "empty line"); 228 | /* Ignore empty lines */ 229 | continue; 230 | } 231 | 232 | log_v("Line received from linenoise: %s\n", line); 233 | 234 | /* Add the command to the history */ 235 | linenoiseHistoryAdd(line); 236 | 237 | /* Save command history to filesystem */ 238 | if (console.history_save_path_) 239 | { 240 | linenoiseHistorySave(console.history_save_path_); 241 | } 242 | 243 | //Interpolate the input line 244 | String interpolated_line = interpolateLine(line); 245 | log_v("Interpolated line: %s\n", interpolated_line.c_str()); 246 | 247 | /* Try to run the command */ 248 | int ret; 249 | esp_err_t err = esp_console_run(interpolated_line.c_str(), &ret); 250 | 251 | //Reset global state 252 | resetAfterCommands(); 253 | 254 | if (err == ESP_ERR_NOT_FOUND) 255 | { 256 | printf("Unrecognized command\n"); 257 | } 258 | else if (err == ESP_ERR_INVALID_ARG) 259 | { 260 | // command was empty 261 | } 262 | else if (err == ESP_OK && ret != ESP_OK) 263 | { 264 | printf("Command returned non-zero error code: 0x%x (%s)\n", ret, esp_err_to_name(ret)); 265 | } 266 | else if (err != ESP_OK) 267 | { 268 | printf("Internal error: %s\n", esp_err_to_name(err)); 269 | } 270 | /* linenoise allocates line buffer on the heap, so need to free it */ 271 | linenoiseFree(line); 272 | } 273 | ESP_LOGD(TAG, "REPL task ended"); 274 | vTaskDelete(NULL); 275 | } 276 | 277 | void Console::end() 278 | { 279 | } 280 | }; -------------------------------------------------------------------------------- /src/kilo/kilo.cpp: -------------------------------------------------------------------------------- 1 | /* Kilo -- A very simple editor in less than 1-kilo lines of code (as counted 2 | * by "cloc"). Does not depend on libcurses, directly emits VT100 3 | * escapes on the terminal. 4 | * 5 | * ----------------------------------------------------------------------- 6 | * 7 | * Copyright (C) 2016 Salvatore Sanfilippo 8 | * 9 | * All rights reserved. 10 | * 11 | * Redistribution and use in source and binary forms, with or without 12 | * modification, are permitted provided that the following conditions are 13 | * met: 14 | * 15 | * * Redistributions of source code must retain the above copyright 16 | * notice, this list of conditions and the following disclaimer. 17 | * 18 | * * Redistributions in binary form must reproduce the above copyright 19 | * notice, this list of conditions and the following disclaimer in the 20 | * documentation and/or other materials provided with the distribution. 21 | * 22 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 23 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 24 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 25 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 26 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 27 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 28 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 29 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 30 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 31 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 32 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 33 | */ 34 | 35 | /** 36 | * Modified by Jan Boehmer 2022, to match the requirements of ESP32Console 37 | * 38 | */ 39 | 40 | #pragma GCC diagnostic push 41 | #pragma GCC diagnostic ignored "-Wwrite-strings" 42 | 43 | #define KILO_VERSION "0.0.1" 44 | 45 | #ifdef __linux__ 46 | #define _POSIX_C_SOURCE 200809L 47 | #endif 48 | 49 | #include 50 | 51 | #include 52 | #include 53 | #include 54 | #include 55 | #include 56 | #include 57 | #include 58 | #include 59 | #include 60 | #include 61 | #include 62 | #include 63 | #include 64 | #include 65 | #include 66 | 67 | /* Syntax highlight types */ 68 | #define HL_NORMAL 0 69 | #define HL_NONPRINT 1 70 | #define HL_COMMENT 2 /* Single line comment. */ 71 | #define HL_MLCOMMENT 3 /* Multi-line comment. */ 72 | #define HL_KEYWORD1 4 73 | #define HL_KEYWORD2 5 74 | #define HL_STRING 6 75 | #define HL_NUMBER 7 76 | #define HL_MATCH 8 /* Search match. */ 77 | 78 | #define HL_HIGHLIGHT_STRINGS (1 << 0) 79 | #define HL_HIGHLIGHT_NUMBERS (1 << 1) 80 | 81 | namespace ESP32Console::Kilo 82 | { 83 | struct editorSyntax 84 | { 85 | char **filematch; 86 | char **keywords; 87 | char singleline_comment_start[3]; 88 | char multiline_comment_start[3]; 89 | char multiline_comment_end[3]; 90 | int flags; 91 | }; 92 | 93 | /* This structure represents a single line of the file we are editing. */ 94 | typedef struct erow 95 | { 96 | int idx; /* Row index in the file, zero-based. */ 97 | int size; /* Size of the row, excluding the null term. */ 98 | int rsize; /* Size of the rendered row. */ 99 | char *chars; /* Row content. */ 100 | char *render; /* Row content "rendered" for screen (for TABs). */ 101 | unsigned char *hl; /* Syntax highlight type for each character in render.*/ 102 | int hl_oc; /* Row had open comment at end in last syntax highlight 103 | check. */ 104 | } erow; 105 | 106 | typedef struct hlcolor 107 | { 108 | int r, g, b; 109 | } hlcolor; 110 | 111 | struct editorConfig 112 | { 113 | int cx, cy; /* Cursor x and y position in characters */ 114 | int rowoff; /* Offset of row displayed. */ 115 | int coloff; /* Offset of column displayed. */ 116 | int screenrows; /* Number of rows that we can show */ 117 | int screencols; /* Number of cols that we can show */ 118 | int numrows; /* Number of rows */ 119 | int rawmode; /* Is terminal raw mode enabled? */ 120 | erow *row; /* Rows */ 121 | int dirty; /* File modified but not saved. */ 122 | char *filename; /* Currently open filename */ 123 | char statusmsg[80]; 124 | time_t statusmsg_time; 125 | struct editorSyntax *syntax; /* Current syntax highlight, or NULL. */ 126 | }; 127 | 128 | static struct editorConfig E; 129 | 130 | enum KEY_ACTION 131 | { 132 | KEY_NULL = 0, /* NULL */ 133 | CTRL_C = 3, /* Ctrl-c */ 134 | CTRL_D = 4, /* Ctrl-d */ 135 | CTRL_F = 6, /* Ctrl-f */ 136 | CTRL_H = 8, /* Ctrl-h */ 137 | TAB = 9, /* Tab */ 138 | CTRL_L = 12, /* Ctrl+l */ 139 | ENTER = 13, /* Enter */ 140 | CTRL_Q = 17, /* Ctrl-q */ 141 | CTRL_S = 19, /* Ctrl-s */ 142 | CTRL_U = 21, /* Ctrl-u */ 143 | ESC = 27, /* Escape */ 144 | BACKSPACE = 127, /* Backspace */ 145 | /* The following are just soft codes, not really reported by the 146 | * terminal directly. */ 147 | ARROW_LEFT = 1000, 148 | ARROW_RIGHT, 149 | ARROW_UP, 150 | ARROW_DOWN, 151 | DEL_KEY, 152 | HOME_KEY, 153 | END_KEY, 154 | PAGE_UP, 155 | PAGE_DOWN 156 | }; 157 | 158 | void editorSetStatusMessage(const char *fmt, ...); 159 | 160 | /* =========================== Syntax highlights DB ========================= 161 | * 162 | * In order to add a new syntax, define two arrays with a list of file name 163 | * matches and keywords. The file name matches are used in order to match 164 | * a given syntax with a given file name: if a match pattern starts with a 165 | * dot, it is matched as the last past of the filename, for example ".c". 166 | * Otherwise the pattern is just searched inside the filenme, like "Makefile"). 167 | * 168 | * The list of keywords to highlight is just a list of words, however if they 169 | * a trailing '|' character is added at the end, they are highlighted in 170 | * a different color, so that you can have two different sets of keywords. 171 | * 172 | * Finally add a stanza in the HLDB global variable with two two arrays 173 | * of strings, and a set of flags in order to enable highlighting of 174 | * comments and numbers. 175 | * 176 | * The characters for single and multi line comments must be exactly two 177 | * and must be provided as well (see the C language example). 178 | * 179 | * There is no support to highlight patterns currently. */ 180 | 181 | /* C / C++ */ 182 | char *C_HL_extensions[] = {".c", ".h", ".cpp", ".hpp", ".cc", NULL}; 183 | char *C_HL_keywords[] = { 184 | /* C Keywords */ 185 | "auto", "break", "case", "continue", "default", "do", "else", "enum", 186 | "extern", "for", "goto", "if", "register", "return", "sizeof", "static", 187 | "struct", "switch", "typedef", "union", "volatile", "while", "NULL", 188 | 189 | /* C++ Keywords */ 190 | "alignas", "alignof", "and", "and_eq", "asm", "bitand", "bitor", "class", 191 | "compl", "constexpr", "const_cast", "deltype", "delete", "dynamic_cast", 192 | "explicit", "export", "false", "friend", "inline", "mutable", "namespace", 193 | "new", "noexcept", "not", "not_eq", "nullptr", "operator", "or", "or_eq", 194 | "private", "protected", "public", "reinterpret_cast", "static_assert", 195 | "static_cast", "template", "this", "thread_local", "throw", "true", "try", 196 | "typeid", "typename", "virtual", "xor", "xor_eq", 197 | 198 | /* C types */ 199 | "int|", "long|", "double|", "float|", "char|", "unsigned|", "signed|", 200 | "void|", "short|", "auto|", "const|", "bool|", NULL}; 201 | 202 | /* Here we define an array of syntax highlights by extensions, keywords, 203 | * comments delimiters and flags. */ 204 | struct editorSyntax HLDB[] = { 205 | {/* C / C++ */ 206 | C_HL_extensions, 207 | C_HL_keywords, 208 | "//", "/*", "*/", 209 | HL_HIGHLIGHT_STRINGS | HL_HIGHLIGHT_NUMBERS}}; 210 | 211 | #define HLDB_ENTRIES (sizeof(HLDB) / sizeof(HLDB[0])) 212 | 213 | /* ======================= Low level terminal handling ====================== */ 214 | 215 | static struct termios orig_termios; /* In order to restore at exit.*/ 216 | 217 | void disableRawMode(int fd) 218 | { 219 | /* Don't even check the return value as it's too late. */ 220 | if (E.rawmode) 221 | { 222 | tcsetattr(fd, TCSAFLUSH, &orig_termios); 223 | E.rawmode = 0; 224 | } 225 | } 226 | 227 | /* Called at exit to avoid remaining in raw mode. */ 228 | void editorAtExit(void) 229 | { 230 | disableRawMode(STDIN_FILENO); 231 | } 232 | 233 | /* Raw mode: 1960 magic shit. */ 234 | int enableRawMode(int fd) 235 | { 236 | struct termios raw; 237 | 238 | if (E.rawmode) 239 | return 0; /* Already enabled. */ 240 | if (!isatty(STDIN_FILENO)) 241 | goto fatal; 242 | atexit(editorAtExit); 243 | if (tcgetattr(fd, &orig_termios) == -1) 244 | goto fatal; 245 | 246 | raw = orig_termios; /* modify the original mode */ 247 | /* input modes: no break, no CR to NL, no parity check, no strip char, 248 | * no start/stop output control. */ 249 | raw.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON); 250 | /* output modes - disable post processing */ 251 | raw.c_oflag &= ~(OPOST); 252 | /* control modes - set 8 bit chars */ 253 | raw.c_cflag |= (CS8); 254 | /* local modes - choing off, canonical off, no extended functions, 255 | * no signal chars (^Z,^C) */ 256 | raw.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG); 257 | /* control chars - set return condition: min number of bytes and timer. */ 258 | raw.c_cc[VMIN] = 0; /* Return each byte, or zero for timeout. */ 259 | raw.c_cc[VTIME] = 1; /* 100 ms timeout (unit is tens of second). */ 260 | 261 | /* put terminal in raw mode after flushing */ 262 | if (tcsetattr(fd, TCSAFLUSH, &raw) < 0) 263 | goto fatal; 264 | E.rawmode = 1; 265 | return 0; 266 | 267 | fatal: 268 | errno = ENOTTY; 269 | return -1; 270 | } 271 | 272 | /* Read a key from the terminal put in raw mode, trying to handle 273 | * escape sequences. */ 274 | int editorReadKey(int fd) 275 | { 276 | int nread; 277 | char c, seq[3]; 278 | while ((nread = read(fd, &c, 1)) == 0) 279 | ; 280 | if (nread == -1) 281 | throw KiloException(1); 282 | 283 | while (1) 284 | { 285 | switch (c) 286 | { 287 | case ESC: /* escape sequence */ 288 | /* If this is just an ESC, we'll timeout here. */ 289 | if (read(fd, seq, 1) == 0) 290 | return ESC; 291 | if (read(fd, seq + 1, 1) == 0) 292 | return ESC; 293 | 294 | /* ESC [ sequences. */ 295 | if (seq[0] == '[') 296 | { 297 | if (seq[1] >= '0' && seq[1] <= '9') 298 | { 299 | /* Extended escape, read additional byte. */ 300 | if (read(fd, seq + 2, 1) == 0) 301 | return ESC; 302 | if (seq[2] == '~') 303 | { 304 | switch (seq[1]) 305 | { 306 | case '3': 307 | return DEL_KEY; 308 | case '5': 309 | return PAGE_UP; 310 | case '6': 311 | return PAGE_DOWN; 312 | } 313 | } 314 | } 315 | else 316 | { 317 | switch (seq[1]) 318 | { 319 | case 'A': 320 | return ARROW_UP; 321 | case 'B': 322 | return ARROW_DOWN; 323 | case 'C': 324 | return ARROW_RIGHT; 325 | case 'D': 326 | return ARROW_LEFT; 327 | case 'H': 328 | return HOME_KEY; 329 | case 'F': 330 | return END_KEY; 331 | } 332 | } 333 | } 334 | 335 | /* ESC O sequences. */ 336 | else if (seq[0] == 'O') 337 | { 338 | switch (seq[1]) 339 | { 340 | case 'H': 341 | return HOME_KEY; 342 | case 'F': 343 | return END_KEY; 344 | } 345 | } 346 | break; 347 | default: 348 | return c; 349 | } 350 | } 351 | } 352 | 353 | /* Use the ESC [6n escape sequence to query the horizontal cursor position 354 | * and return it. On error -1 is returned, on success the position of the 355 | * cursor is stored at *rows and *cols and 0 is returned. */ 356 | int getCursorPosition(int ifd, int ofd, int *rows, int *cols) 357 | { 358 | char buf[32]; 359 | unsigned int i = 0; 360 | 361 | /* Report cursor location */ 362 | if (write(ofd, "\x1b[6n", 4) != 4) 363 | return -1; 364 | 365 | /* Read the response: ESC [ rows ; cols R */ 366 | while (i < sizeof(buf) - 1) 367 | { 368 | if (read(ifd, buf + i, 1) != 1) 369 | break; 370 | if (buf[i] == 'R') 371 | break; 372 | i++; 373 | } 374 | buf[i] = '\0'; 375 | 376 | /* Parse it. */ 377 | if (buf[0] != ESC || buf[1] != '[') 378 | return -1; 379 | if (sscanf(buf + 2, "%d;%d", rows, cols) != 2) 380 | return -1; 381 | return 0; 382 | } 383 | 384 | /* Try to get the number of columns in the current terminal. If the ioctl() 385 | * call fails the function will try to query the terminal itself. 386 | * Returns 0 on success, -1 on error. */ 387 | int getWindowSize(int ifd, int ofd, int *rows, int *cols) 388 | { 389 | /* ioctl() failed. Try to query the terminal itself. */ 390 | int orig_row, orig_col, retval; 391 | 392 | /* Get the initial position so we can restore it later. */ 393 | retval = getCursorPosition(ifd, ofd, &orig_row, &orig_col); 394 | if (retval == -1) 395 | goto failed; 396 | 397 | /* Go to right/bottom margin and get position. */ 398 | if (write(ofd, "\x1b[999C\x1b[999B", 12) != 12) 399 | goto failed; 400 | retval = getCursorPosition(ifd, ofd, rows, cols); 401 | if (retval == -1) 402 | goto failed; 403 | 404 | /* Restore position. */ 405 | char seq[32]; 406 | snprintf(seq, 32, "\x1b[%d;%dH", orig_row, orig_col); 407 | if (write(ofd, seq, strlen(seq)) == -1) 408 | { 409 | /* Can't recover... */ 410 | } 411 | return 0; 412 | 413 | failed: 414 | return -1; 415 | } 416 | 417 | /* ====================== Syntax highlight color scheme ==================== */ 418 | 419 | int is_separator(int c) 420 | { 421 | return c == '\0' || isspace(c) || strchr(",.()+-/*=~%[];", c) != NULL; 422 | } 423 | 424 | /* Return true if the specified row last char is part of a multi line comment 425 | * that starts at this row or at one before, and does not end at the end 426 | * of the row but spawns to the next row. */ 427 | int editorRowHasOpenComment(erow *row) 428 | { 429 | if (row->hl && row->rsize && row->hl[row->rsize - 1] == HL_MLCOMMENT && 430 | (row->rsize < 2 || (row->render[row->rsize - 2] != '*' || 431 | row->render[row->rsize - 1] != '/'))) 432 | return 1; 433 | return 0; 434 | } 435 | 436 | /* Set every byte of row->hl (that corresponds to every character in the line) 437 | * to the right syntax highlight type (HL_* defines). */ 438 | void editorUpdateSyntax(erow *row) 439 | { 440 | row->hl = (unsigned char *)realloc(row->hl, row->rsize); 441 | memset(row->hl, HL_NORMAL, row->rsize); 442 | 443 | if (E.syntax == NULL) 444 | return; /* No syntax, everything is HL_NORMAL. */ 445 | 446 | int i, prev_sep, in_string, in_comment; 447 | char *p; 448 | char **keywords = E.syntax->keywords; 449 | char *scs = E.syntax->singleline_comment_start; 450 | char *mcs = E.syntax->multiline_comment_start; 451 | char *mce = E.syntax->multiline_comment_end; 452 | 453 | /* Point to the first non-space char. */ 454 | p = row->render; 455 | i = 0; /* Current char offset */ 456 | while (*p && isspace(*p)) 457 | { 458 | p++; 459 | i++; 460 | } 461 | prev_sep = 1; /* Tell the parser if 'i' points to start of word. */ 462 | in_string = 0; /* Are we inside "" or '' ? */ 463 | in_comment = 0; /* Are we inside multi-line comment? */ 464 | 465 | /* If the previous line has an open comment, this line starts 466 | * with an open comment state. */ 467 | if (row->idx > 0 && editorRowHasOpenComment(&E.row[row->idx - 1])) 468 | in_comment = 1; 469 | 470 | while (*p) 471 | { 472 | /* Handle // comments. */ 473 | if (prev_sep && *p == scs[0] && *(p + 1) == scs[1]) 474 | { 475 | /* From here to end is a comment */ 476 | memset(row->hl + i, HL_COMMENT, row->size - i); 477 | return; 478 | } 479 | 480 | /* Handle multi line comments. */ 481 | if (in_comment) 482 | { 483 | row->hl[i] = HL_MLCOMMENT; 484 | if (*p == mce[0] && *(p + 1) == mce[1]) 485 | { 486 | row->hl[i + 1] = HL_MLCOMMENT; 487 | p += 2; 488 | i += 2; 489 | in_comment = 0; 490 | prev_sep = 1; 491 | continue; 492 | } 493 | else 494 | { 495 | prev_sep = 0; 496 | p++; 497 | i++; 498 | continue; 499 | } 500 | } 501 | else if (*p == mcs[0] && *(p + 1) == mcs[1]) 502 | { 503 | row->hl[i] = HL_MLCOMMENT; 504 | row->hl[i + 1] = HL_MLCOMMENT; 505 | p += 2; 506 | i += 2; 507 | in_comment = 1; 508 | prev_sep = 0; 509 | continue; 510 | } 511 | 512 | /* Handle "" and '' */ 513 | if (in_string) 514 | { 515 | row->hl[i] = HL_STRING; 516 | if (*p == '\\') 517 | { 518 | row->hl[i + 1] = HL_STRING; 519 | p += 2; 520 | i += 2; 521 | prev_sep = 0; 522 | continue; 523 | } 524 | if (*p == in_string) 525 | in_string = 0; 526 | p++; 527 | i++; 528 | continue; 529 | } 530 | else 531 | { 532 | if (*p == '"' || *p == '\'') 533 | { 534 | in_string = *p; 535 | row->hl[i] = HL_STRING; 536 | p++; 537 | i++; 538 | prev_sep = 0; 539 | continue; 540 | } 541 | } 542 | 543 | /* Handle non printable chars. */ 544 | if (!isprint(*p)) 545 | { 546 | row->hl[i] = HL_NONPRINT; 547 | p++; 548 | i++; 549 | prev_sep = 0; 550 | continue; 551 | } 552 | 553 | /* Handle numbers */ 554 | if ((isdigit(*p) && (prev_sep || row->hl[i - 1] == HL_NUMBER)) || 555 | (*p == '.' && i > 0 && row->hl[i - 1] == HL_NUMBER)) 556 | { 557 | row->hl[i] = HL_NUMBER; 558 | p++; 559 | i++; 560 | prev_sep = 0; 561 | continue; 562 | } 563 | 564 | /* Handle keywords and lib calls */ 565 | if (prev_sep) 566 | { 567 | int j; 568 | for (j = 0; keywords[j]; j++) 569 | { 570 | int klen = strlen(keywords[j]); 571 | int kw2 = keywords[j][klen - 1] == '|'; 572 | if (kw2) 573 | klen--; 574 | 575 | if (!memcmp(p, keywords[j], klen) && 576 | is_separator(*(p + klen))) 577 | { 578 | /* Keyword */ 579 | memset(row->hl + i, kw2 ? HL_KEYWORD2 : HL_KEYWORD1, klen); 580 | p += klen; 581 | i += klen; 582 | break; 583 | } 584 | } 585 | if (keywords[j] != NULL) 586 | { 587 | prev_sep = 0; 588 | continue; /* We had a keyword match */ 589 | } 590 | } 591 | 592 | /* Not special chars */ 593 | prev_sep = is_separator(*p); 594 | p++; 595 | i++; 596 | } 597 | 598 | /* Propagate syntax change to the next row if the open commen 599 | * state changed. This may recursively affect all the following rows 600 | * in the file. */ 601 | int oc = editorRowHasOpenComment(row); 602 | if (row->hl_oc != oc && row->idx + 1 < E.numrows) 603 | editorUpdateSyntax(&E.row[row->idx + 1]); 604 | row->hl_oc = oc; 605 | } 606 | 607 | /* Maps syntax highlight token types to terminal colors. */ 608 | int editorSyntaxToColor(int hl) 609 | { 610 | switch (hl) 611 | { 612 | case HL_COMMENT: 613 | case HL_MLCOMMENT: 614 | return 36; /* cyan */ 615 | case HL_KEYWORD1: 616 | return 33; /* yellow */ 617 | case HL_KEYWORD2: 618 | return 32; /* green */ 619 | case HL_STRING: 620 | return 35; /* magenta */ 621 | case HL_NUMBER: 622 | return 31; /* red */ 623 | case HL_MATCH: 624 | return 34; /* blu */ 625 | default: 626 | return 37; /* white */ 627 | } 628 | } 629 | 630 | /* Select the syntax highlight scheme depending on the filename, 631 | * setting it in the global state E.syntax. */ 632 | void editorSelectSyntaxHighlight(char *filename) 633 | { 634 | for (unsigned int j = 0; j < HLDB_ENTRIES; j++) 635 | { 636 | struct editorSyntax *s = HLDB + j; 637 | unsigned int i = 0; 638 | while (s->filematch[i]) 639 | { 640 | char *p; 641 | int patlen = strlen(s->filematch[i]); 642 | if ((p = strstr(filename, s->filematch[i])) != NULL) 643 | { 644 | if (s->filematch[i][0] != '.' || p[patlen] == '\0') 645 | { 646 | E.syntax = s; 647 | return; 648 | } 649 | } 650 | i++; 651 | } 652 | } 653 | } 654 | 655 | /* ======================= Editor rows implementation ======================= */ 656 | 657 | /* Update the rendered version and the syntax highlight of a row. */ 658 | void editorUpdateRow(erow *row) 659 | { 660 | unsigned int tabs = 0, nonprint = 0; 661 | int j, idx; 662 | 663 | /* Create a version of the row we can directly print on the screen, 664 | * respecting tabs, substituting non printable characters with '?'. */ 665 | free(row->render); 666 | for (j = 0; j < row->size; j++) 667 | if (row->chars[j] == TAB) 668 | tabs++; 669 | 670 | unsigned long long allocsize = 671 | (unsigned long long)row->size + tabs * 8 + nonprint * 9 + 1; 672 | if (allocsize > UINT32_MAX) 673 | { 674 | printf("Some line of the edited file is too long for kilo\n"); 675 | throw KiloException(1); 676 | } 677 | 678 | row->render = (char *)malloc(row->size + tabs * 8 + nonprint * 9 + 1); 679 | idx = 0; 680 | for (j = 0; j < row->size; j++) 681 | { 682 | if (row->chars[j] == TAB) 683 | { 684 | row->render[idx++] = ' '; 685 | while ((idx + 1) % 8 != 0) 686 | row->render[idx++] = ' '; 687 | } 688 | else 689 | { 690 | row->render[idx++] = row->chars[j]; 691 | } 692 | } 693 | row->rsize = idx; 694 | row->render[idx] = '\0'; 695 | 696 | /* Update the syntax highlighting attributes of the row. */ 697 | editorUpdateSyntax(row); 698 | } 699 | 700 | /* Insert a row at the specified position, shifting the other rows on the bottom 701 | * if required. */ 702 | void editorInsertRow(int at, char *s, size_t len) 703 | { 704 | if (at > E.numrows) 705 | return; 706 | E.row = (erow *)realloc(E.row, sizeof(erow) * (E.numrows + 1)); 707 | if (at != E.numrows) 708 | { 709 | memmove(E.row + at + 1, E.row + at, sizeof(E.row[0]) * (E.numrows - at)); 710 | for (int j = at + 1; j <= E.numrows; j++) 711 | E.row[j].idx++; 712 | } 713 | E.row[at].size = len; 714 | E.row[at].chars = (char *)malloc(len + 1); 715 | memcpy(E.row[at].chars, s, len + 1); 716 | E.row[at].hl = NULL; 717 | E.row[at].hl_oc = 0; 718 | E.row[at].render = NULL; 719 | E.row[at].rsize = 0; 720 | E.row[at].idx = at; 721 | editorUpdateRow(E.row + at); 722 | E.numrows++; 723 | E.dirty++; 724 | } 725 | 726 | /* Free row's heap allocated stuff. */ 727 | void editorFreeRow(erow *row) 728 | { 729 | free(row->render); 730 | free(row->chars); 731 | free(row->hl); 732 | } 733 | 734 | /* Remove the row at the specified position, shifting the remainign on the 735 | * top. */ 736 | void editorDelRow(int at) 737 | { 738 | erow *row; 739 | 740 | if (at >= E.numrows) 741 | return; 742 | row = E.row + at; 743 | editorFreeRow(row); 744 | memmove(E.row + at, E.row + at + 1, sizeof(E.row[0]) * (E.numrows - at - 1)); 745 | for (int j = at; j < E.numrows - 1; j++) 746 | E.row[j].idx++; 747 | E.numrows--; 748 | E.dirty++; 749 | } 750 | 751 | /* Turn the editor rows into a single heap-allocated string. 752 | * Returns the pointer to the heap-allocated string and populate the 753 | * integer pointed by 'buflen' with the size of the string, escluding 754 | * the final nulterm. */ 755 | char *editorRowsToString(int *buflen) 756 | { 757 | char *buf = NULL, *p; 758 | int totlen = 0; 759 | int j; 760 | 761 | /* Compute count of bytes */ 762 | for (j = 0; j < E.numrows; j++) 763 | totlen += E.row[j].size + 1; /* +1 is for "\n" at end of every row */ 764 | *buflen = totlen; 765 | totlen++; /* Also make space for nulterm */ 766 | 767 | p = buf = (char *)malloc(totlen); 768 | for (j = 0; j < E.numrows; j++) 769 | { 770 | memcpy(p, E.row[j].chars, E.row[j].size); 771 | p += E.row[j].size; 772 | *p = '\n'; 773 | p++; 774 | } 775 | *p = '\0'; 776 | return buf; 777 | } 778 | 779 | /* Insert a character at the specified position in a row, moving the remaining 780 | * chars on the right if needed. */ 781 | void editorRowInsertChar(erow *row, int at, int c) 782 | { 783 | if (at > row->size) 784 | { 785 | /* Pad the string with spaces if the insert location is outside the 786 | * current length by more than a single character. */ 787 | int padlen = at - row->size; 788 | /* In the next line +2 means: new char and null term. */ 789 | row->chars = (char *)realloc(row->chars, row->size + padlen + 2); 790 | memset(row->chars + row->size, ' ', padlen); 791 | row->chars[row->size + padlen + 1] = '\0'; 792 | row->size += padlen + 1; 793 | } 794 | else 795 | { 796 | /* If we are in the middle of the string just make space for 1 new 797 | * char plus the (already existing) null term. */ 798 | row->chars = (char *)realloc(row->chars, row->size + 2); 799 | memmove(row->chars + at + 1, row->chars + at, row->size - at + 1); 800 | row->size++; 801 | } 802 | row->chars[at] = c; 803 | editorUpdateRow(row); 804 | E.dirty++; 805 | } 806 | 807 | /* Append the string 's' at the end of a row */ 808 | void editorRowAppendString(erow *row, char *s, size_t len) 809 | { 810 | row->chars = (char *)realloc(row->chars, row->size + len + 1); 811 | memcpy(row->chars + row->size, s, len); 812 | row->size += len; 813 | row->chars[row->size] = '\0'; 814 | editorUpdateRow(row); 815 | E.dirty++; 816 | } 817 | 818 | /* Delete the character at offset 'at' from the specified row. */ 819 | void editorRowDelChar(erow *row, int at) 820 | { 821 | if (row->size <= at) 822 | return; 823 | memmove(row->chars + at, row->chars + at + 1, row->size - at); 824 | editorUpdateRow(row); 825 | row->size--; 826 | E.dirty++; 827 | } 828 | 829 | /* Insert the specified char at the current prompt position. */ 830 | void editorInsertChar(int c) 831 | { 832 | int filerow = E.rowoff + E.cy; 833 | int filecol = E.coloff + E.cx; 834 | erow *row = (filerow >= E.numrows) ? NULL : &E.row[filerow]; 835 | 836 | /* If the row where the cursor is currently located does not exist in our 837 | * logical representaion of the file, add enough empty rows as needed. */ 838 | if (!row) 839 | { 840 | while (E.numrows <= filerow) 841 | editorInsertRow(E.numrows, "", 0); 842 | } 843 | row = &E.row[filerow]; 844 | editorRowInsertChar(row, filecol, c); 845 | if (E.cx == E.screencols - 1) 846 | E.coloff++; 847 | else 848 | E.cx++; 849 | E.dirty++; 850 | } 851 | 852 | /* Inserting a newline is slightly complex as we have to handle inserting a 853 | * newline in the middle of a line, splitting the line as needed. */ 854 | void editorInsertNewline(void) 855 | { 856 | int filerow = E.rowoff + E.cy; 857 | int filecol = E.coloff + E.cx; 858 | erow *row = (filerow >= E.numrows) ? NULL : &E.row[filerow]; 859 | 860 | if (!row) 861 | { 862 | if (filerow == E.numrows) 863 | { 864 | editorInsertRow(filerow, "", 0); 865 | goto fixcursor; 866 | } 867 | return; 868 | } 869 | /* If the cursor is over the current line size, we want to conceptually 870 | * think it's just over the last character. */ 871 | if (filecol >= row->size) 872 | filecol = row->size; 873 | if (filecol == 0) 874 | { 875 | editorInsertRow(filerow, "", 0); 876 | } 877 | else 878 | { 879 | /* We are in the middle of a line. Split it between two rows. */ 880 | editorInsertRow(filerow + 1, row->chars + filecol, row->size - filecol); 881 | row = &E.row[filerow]; 882 | row->chars[filecol] = '\0'; 883 | row->size = filecol; 884 | editorUpdateRow(row); 885 | } 886 | fixcursor: 887 | if (E.cy == E.screenrows - 1) 888 | { 889 | E.rowoff++; 890 | } 891 | else 892 | { 893 | E.cy++; 894 | } 895 | E.cx = 0; 896 | E.coloff = 0; 897 | } 898 | 899 | /* Delete the char at the current prompt position. */ 900 | void editorDelChar() 901 | { 902 | int filerow = E.rowoff + E.cy; 903 | int filecol = E.coloff + E.cx; 904 | erow *row = (filerow >= E.numrows) ? NULL : &E.row[filerow]; 905 | 906 | if (!row || (filecol == 0 && filerow == 0)) 907 | return; 908 | if (filecol == 0) 909 | { 910 | /* Handle the case of column 0, we need to move the current line 911 | * on the right of the previous one. */ 912 | filecol = E.row[filerow - 1].size; 913 | editorRowAppendString(&E.row[filerow - 1], row->chars, row->size); 914 | editorDelRow(filerow); 915 | row = NULL; 916 | if (E.cy == 0) 917 | E.rowoff--; 918 | else 919 | E.cy--; 920 | E.cx = filecol; 921 | if (E.cx >= E.screencols) 922 | { 923 | int shift = (E.screencols - E.cx) + 1; 924 | E.cx -= shift; 925 | E.coloff += shift; 926 | } 927 | } 928 | else 929 | { 930 | editorRowDelChar(row, filecol - 1); 931 | if (E.cx == 0 && E.coloff) 932 | E.coloff--; 933 | else 934 | E.cx--; 935 | } 936 | if (row) 937 | editorUpdateRow(row); 938 | E.dirty++; 939 | } 940 | 941 | /* Load the specified program in the editor memory and returns 0 on success 942 | * or 1 on error. */ 943 | int editorOpen(char *filename) 944 | { 945 | FILE *fp; 946 | 947 | E.dirty = 0; 948 | free(E.filename); 949 | size_t fnlen = strlen(filename) + 1; 950 | E.filename = (char *)malloc(fnlen); 951 | memcpy(E.filename, filename, fnlen); 952 | 953 | fp = fopen(filename, "r"); 954 | if (!fp) 955 | { 956 | if (errno != ENOENT) 957 | { 958 | perror("Opening file"); 959 | throw KiloException(1); 960 | } 961 | return 1; 962 | } 963 | 964 | char *line = NULL; 965 | size_t linecap = 0; 966 | ssize_t linelen; 967 | while ((linelen = __getline(&line, &linecap, fp)) != -1) 968 | { 969 | if (linelen && (line[linelen - 1] == '\n' || line[linelen - 1] == '\r')) 970 | line[--linelen] = '\0'; 971 | editorInsertRow(E.numrows, line, linelen); 972 | } 973 | free(line); 974 | fclose(fp); 975 | E.dirty = 0; 976 | return 0; 977 | } 978 | 979 | /* Save the current file on disk. Return 0 on success, 1 on error. */ 980 | int editorSave(void) 981 | { 982 | int len; 983 | char *buf = editorRowsToString(&len); 984 | 985 | FILE* file = fopen(E.filename, "wb"); 986 | if(!file) { 987 | goto writeerr; 988 | } 989 | 990 | for(int n=0; nb, ab->len + len); 1051 | 1052 | if (new_ == NULL) 1053 | return; 1054 | memcpy(new_ + ab->len, s, len); 1055 | ab->b = new_; 1056 | ab->len += len; 1057 | } 1058 | 1059 | void abFree(struct abuf *ab) 1060 | { 1061 | free(ab->b); 1062 | } 1063 | 1064 | /* This function writes the whole screen using VT100 escape characters 1065 | * starting from the logical state of the editor in the global state 'E'. */ 1066 | void editorRefreshScreen(void) 1067 | { 1068 | int y; 1069 | erow *r; 1070 | char buf[32]; 1071 | struct abuf ab = ABUF_INIT; 1072 | 1073 | abAppend(&ab, "\x1b[?25l", 6); /* Hide cursor. */ 1074 | abAppend(&ab, "\x1b[H", 3); /* Go home. */ 1075 | for (y = 0; y < E.screenrows; y++) 1076 | { 1077 | int filerow = E.rowoff + y; 1078 | 1079 | if (filerow >= E.numrows) 1080 | { 1081 | if (E.numrows == 0 && y == E.screenrows / 3) 1082 | { 1083 | char welcome[80]; 1084 | int welcomelen = snprintf(welcome, sizeof(welcome), 1085 | "Kilo editor -- verison %s\x1b[0K\r\n", KILO_VERSION); 1086 | int padding = (E.screencols - welcomelen) / 2; 1087 | if (padding) 1088 | { 1089 | abAppend(&ab, "~", 1); 1090 | padding--; 1091 | } 1092 | while (padding--) 1093 | abAppend(&ab, " ", 1); 1094 | abAppend(&ab, welcome, welcomelen); 1095 | } 1096 | else 1097 | { 1098 | abAppend(&ab, "~\x1b[0K\r\n", 7); 1099 | } 1100 | continue; 1101 | } 1102 | 1103 | r = &E.row[filerow]; 1104 | 1105 | int len = r->rsize - E.coloff; 1106 | int current_color = -1; 1107 | if (len > 0) 1108 | { 1109 | if (len > E.screencols) 1110 | len = E.screencols; 1111 | char *c = r->render + E.coloff; 1112 | unsigned char *hl = r->hl + E.coloff; 1113 | int j; 1114 | for (j = 0; j < len; j++) 1115 | { 1116 | if (hl[j] == HL_NONPRINT) 1117 | { 1118 | char sym; 1119 | abAppend(&ab, "\x1b[7m", 4); 1120 | if (c[j] <= 26) 1121 | sym = '@' + c[j]; 1122 | else 1123 | sym = '?'; 1124 | abAppend(&ab, &sym, 1); 1125 | abAppend(&ab, "\x1b[0m", 4); 1126 | } 1127 | else if (hl[j] == HL_NORMAL) 1128 | { 1129 | if (current_color != -1) 1130 | { 1131 | abAppend(&ab, "\x1b[39m", 5); 1132 | current_color = -1; 1133 | } 1134 | abAppend(&ab, c + j, 1); 1135 | } 1136 | else 1137 | { 1138 | int color = editorSyntaxToColor(hl[j]); 1139 | if (color != current_color) 1140 | { 1141 | char buf[16]; 1142 | int clen = snprintf(buf, sizeof(buf), "\x1b[%dm", color); 1143 | current_color = color; 1144 | abAppend(&ab, buf, clen); 1145 | } 1146 | abAppend(&ab, c + j, 1); 1147 | } 1148 | } 1149 | } 1150 | abAppend(&ab, "\x1b[39m", 5); 1151 | abAppend(&ab, "\x1b[0K", 4); 1152 | abAppend(&ab, "\r\n", 2); 1153 | } 1154 | 1155 | /* Create a two rows status. First row: */ 1156 | abAppend(&ab, "\x1b[0K", 4); 1157 | abAppend(&ab, "\x1b[7m", 4); 1158 | char status[80], rstatus[80]; 1159 | int len = snprintf(status, sizeof(status), "%.20s - %d lines %s", 1160 | E.filename, E.numrows, E.dirty ? "(modified)" : ""); 1161 | int rlen = snprintf(rstatus, sizeof(rstatus), 1162 | "%d/%d", E.rowoff + E.cy + 1, E.numrows); 1163 | if (len > E.screencols) 1164 | len = E.screencols; 1165 | abAppend(&ab, status, len); 1166 | while (len < E.screencols) 1167 | { 1168 | if (E.screencols - len == rlen) 1169 | { 1170 | abAppend(&ab, rstatus, rlen); 1171 | break; 1172 | } 1173 | else 1174 | { 1175 | abAppend(&ab, " ", 1); 1176 | len++; 1177 | } 1178 | } 1179 | abAppend(&ab, "\x1b[0m\r\n", 6); 1180 | 1181 | /* Second row depends on E.statusmsg and the status message update time. */ 1182 | abAppend(&ab, "\x1b[0K", 4); 1183 | int msglen = strlen(E.statusmsg); 1184 | if (msglen && time(NULL) - E.statusmsg_time < 5) 1185 | abAppend(&ab, E.statusmsg, msglen <= E.screencols ? msglen : E.screencols); 1186 | 1187 | /* Put cursor at its current position. Note that the horizontal position 1188 | * at which the cursor is displayed may be different compared to 'E.cx' 1189 | * because of TABs. */ 1190 | int j; 1191 | int cx = 1; 1192 | int filerow = E.rowoff + E.cy; 1193 | erow *row = (filerow >= E.numrows) ? NULL : &E.row[filerow]; 1194 | if (row) 1195 | { 1196 | for (j = E.coloff; j < (E.cx + E.coloff); j++) 1197 | { 1198 | if (j < row->size && row->chars[j] == TAB) 1199 | cx += 7 - ((cx) % 8); 1200 | cx++; 1201 | } 1202 | } 1203 | snprintf(buf, sizeof(buf), "\x1b[%d;%dH", E.cy + 1, cx); 1204 | abAppend(&ab, buf, strlen(buf)); 1205 | abAppend(&ab, "\x1b[?25h", 6); /* Show cursor. */ 1206 | write(STDOUT_FILENO, ab.b, ab.len); 1207 | abFree(&ab); 1208 | } 1209 | 1210 | /* Set an editor status message for the second line of the status, at the 1211 | * end of the screen. */ 1212 | void editorSetStatusMessage(const char *fmt, ...) 1213 | { 1214 | va_list ap; 1215 | va_start(ap, fmt); 1216 | vsnprintf(E.statusmsg, sizeof(E.statusmsg), fmt, ap); 1217 | va_end(ap); 1218 | E.statusmsg_time = time(NULL); 1219 | } 1220 | 1221 | /* =============================== Find mode ================================ */ 1222 | 1223 | #define KILO_QUERY_LEN 256 1224 | 1225 | void editorFind(int fd) 1226 | { 1227 | char query[KILO_QUERY_LEN + 1] = {0}; 1228 | int qlen = 0; 1229 | int last_match = -1; /* Last line where a match was found. -1 for none. */ 1230 | int find_next = 0; /* if 1 search next, if -1 search prev. */ 1231 | int saved_hl_line = -1; /* No saved HL */ 1232 | char *saved_hl = NULL; 1233 | 1234 | #define FIND_RESTORE_HL \ 1235 | do \ 1236 | { \ 1237 | if (saved_hl) \ 1238 | { \ 1239 | memcpy(E.row[saved_hl_line].hl, saved_hl, E.row[saved_hl_line].rsize); \ 1240 | free(saved_hl); \ 1241 | saved_hl = NULL; \ 1242 | } \ 1243 | } while (0) 1244 | 1245 | /* Save the cursor position in order to restore it later. */ 1246 | int saved_cx = E.cx, saved_cy = E.cy; 1247 | int saved_coloff = E.coloff, saved_rowoff = E.rowoff; 1248 | 1249 | while (1) 1250 | { 1251 | editorSetStatusMessage( 1252 | "Search: %s (Use ESC/Arrows/Enter)", query); 1253 | editorRefreshScreen(); 1254 | 1255 | int c = editorReadKey(fd); 1256 | if (c == DEL_KEY || c == CTRL_H || c == BACKSPACE) 1257 | { 1258 | if (qlen != 0) 1259 | query[--qlen] = '\0'; 1260 | last_match = -1; 1261 | } 1262 | else if (c == ESC || c == ENTER) 1263 | { 1264 | if (c == ESC) 1265 | { 1266 | E.cx = saved_cx; 1267 | E.cy = saved_cy; 1268 | E.coloff = saved_coloff; 1269 | E.rowoff = saved_rowoff; 1270 | } 1271 | FIND_RESTORE_HL; 1272 | editorSetStatusMessage(""); 1273 | return; 1274 | } 1275 | else if (c == ARROW_RIGHT || c == ARROW_DOWN) 1276 | { 1277 | find_next = 1; 1278 | } 1279 | else if (c == ARROW_LEFT || c == ARROW_UP) 1280 | { 1281 | find_next = -1; 1282 | } 1283 | else if (isprint(c)) 1284 | { 1285 | if (qlen < KILO_QUERY_LEN) 1286 | { 1287 | query[qlen++] = c; 1288 | query[qlen] = '\0'; 1289 | last_match = -1; 1290 | } 1291 | } 1292 | 1293 | /* Search occurrence. */ 1294 | if (last_match == -1) 1295 | find_next = 1; 1296 | if (find_next) 1297 | { 1298 | char *match = NULL; 1299 | int match_offset = 0; 1300 | int i, current = last_match; 1301 | 1302 | for (i = 0; i < E.numrows; i++) 1303 | { 1304 | current += find_next; 1305 | if (current == -1) 1306 | current = E.numrows - 1; 1307 | else if (current == E.numrows) 1308 | current = 0; 1309 | match = strstr(E.row[current].render, query); 1310 | if (match) 1311 | { 1312 | match_offset = match - E.row[current].render; 1313 | break; 1314 | } 1315 | } 1316 | find_next = 0; 1317 | 1318 | /* Highlight */ 1319 | FIND_RESTORE_HL; 1320 | 1321 | if (match) 1322 | { 1323 | erow *row = &E.row[current]; 1324 | last_match = current; 1325 | if (row->hl) 1326 | { 1327 | saved_hl_line = current; 1328 | saved_hl = (char *)malloc(row->rsize); 1329 | memcpy(saved_hl, row->hl, row->rsize); 1330 | memset(row->hl + match_offset, HL_MATCH, qlen); 1331 | } 1332 | E.cy = 0; 1333 | E.cx = match_offset; 1334 | E.rowoff = current; 1335 | E.coloff = 0; 1336 | /* Scroll horizontally as needed. */ 1337 | if (E.cx > E.screencols) 1338 | { 1339 | int diff = E.cx - E.screencols; 1340 | E.cx -= diff; 1341 | E.coloff += diff; 1342 | } 1343 | } 1344 | } 1345 | } 1346 | } 1347 | 1348 | /* ========================= Editor events handling ======================== */ 1349 | 1350 | /* Handle cursor position change because arrow keys were pressed. */ 1351 | void editorMoveCursor(int key) 1352 | { 1353 | int filerow = E.rowoff + E.cy; 1354 | int filecol = E.coloff + E.cx; 1355 | int rowlen; 1356 | erow *row = (filerow >= E.numrows) ? NULL : &E.row[filerow]; 1357 | 1358 | switch (key) 1359 | { 1360 | case ARROW_LEFT: 1361 | if (E.cx == 0) 1362 | { 1363 | if (E.coloff) 1364 | { 1365 | E.coloff--; 1366 | } 1367 | else 1368 | { 1369 | if (filerow > 0) 1370 | { 1371 | E.cy--; 1372 | E.cx = E.row[filerow - 1].size; 1373 | if (E.cx > E.screencols - 1) 1374 | { 1375 | E.coloff = E.cx - E.screencols + 1; 1376 | E.cx = E.screencols - 1; 1377 | } 1378 | } 1379 | } 1380 | } 1381 | else 1382 | { 1383 | E.cx -= 1; 1384 | } 1385 | break; 1386 | case ARROW_RIGHT: 1387 | if (row && filecol < row->size) 1388 | { 1389 | if (E.cx == E.screencols - 1) 1390 | { 1391 | E.coloff++; 1392 | } 1393 | else 1394 | { 1395 | E.cx += 1; 1396 | } 1397 | } 1398 | else if (row && filecol == row->size) 1399 | { 1400 | E.cx = 0; 1401 | E.coloff = 0; 1402 | if (E.cy == E.screenrows - 1) 1403 | { 1404 | E.rowoff++; 1405 | } 1406 | else 1407 | { 1408 | E.cy += 1; 1409 | } 1410 | } 1411 | break; 1412 | case ARROW_UP: 1413 | if (E.cy == 0) 1414 | { 1415 | if (E.rowoff) 1416 | E.rowoff--; 1417 | } 1418 | else 1419 | { 1420 | E.cy -= 1; 1421 | } 1422 | break; 1423 | case ARROW_DOWN: 1424 | if (filerow < E.numrows) 1425 | { 1426 | if (E.cy == E.screenrows - 1) 1427 | { 1428 | E.rowoff++; 1429 | } 1430 | else 1431 | { 1432 | E.cy += 1; 1433 | } 1434 | } 1435 | break; 1436 | } 1437 | /* Fix cx if the current line has not enough chars. */ 1438 | filerow = E.rowoff + E.cy; 1439 | filecol = E.coloff + E.cx; 1440 | row = (filerow >= E.numrows) ? NULL : &E.row[filerow]; 1441 | rowlen = row ? row->size : 0; 1442 | if (filecol > rowlen) 1443 | { 1444 | E.cx -= filecol - rowlen; 1445 | if (E.cx < 0) 1446 | { 1447 | E.coloff += E.cx; 1448 | E.cx = 0; 1449 | } 1450 | } 1451 | } 1452 | 1453 | /* Process events arriving from the standard input, which is, the user 1454 | * is typing stuff on the terminal. */ 1455 | #define KILO_QUIT_TIMES 3 1456 | void editorProcessKeypress(int fd) 1457 | { 1458 | /* When the file is modified, requires Ctrl-q to be pressed N times 1459 | * before actually quitting. */ 1460 | static int quit_times = KILO_QUIT_TIMES; 1461 | 1462 | int c = editorReadKey(fd); 1463 | switch (c) 1464 | { 1465 | case ENTER: /* Enter */ 1466 | editorInsertNewline(); 1467 | break; 1468 | case CTRL_C: /* Ctrl-c */ 1469 | /* We ignore ctrl-c, it can't be so simple to lose the changes 1470 | * to the edited file. */ 1471 | break; 1472 | case CTRL_Q: /* Ctrl-q */ 1473 | /* Quit if the file was already saved. */ 1474 | if (E.dirty && quit_times) 1475 | { 1476 | editorSetStatusMessage("WARNING!!! File has unsaved changes. " 1477 | "Press Ctrl-Q %d more times to quit.", 1478 | quit_times); 1479 | quit_times--; 1480 | return; 1481 | } 1482 | throw KiloException(0); 1483 | break; 1484 | case CTRL_S: /* Ctrl-s */ 1485 | editorSave(); 1486 | break; 1487 | case CTRL_F: 1488 | editorFind(fd); 1489 | break; 1490 | case BACKSPACE: /* Backspace */ 1491 | case CTRL_H: /* Ctrl-h */ 1492 | case DEL_KEY: 1493 | editorDelChar(); 1494 | break; 1495 | case PAGE_UP: 1496 | case PAGE_DOWN: 1497 | if (c == PAGE_UP && E.cy != 0) 1498 | E.cy = 0; 1499 | else if (c == PAGE_DOWN && E.cy != E.screenrows - 1) 1500 | E.cy = E.screenrows - 1; 1501 | { 1502 | int times = E.screenrows; 1503 | while (times--) 1504 | editorMoveCursor(c == PAGE_UP ? ARROW_UP : ARROW_DOWN); 1505 | } 1506 | break; 1507 | 1508 | case ARROW_UP: 1509 | case ARROW_DOWN: 1510 | case ARROW_LEFT: 1511 | case ARROW_RIGHT: 1512 | editorMoveCursor(c); 1513 | break; 1514 | case CTRL_L: /* ctrl+l, clear screen */ 1515 | /* Just refresht the line as side effect. */ 1516 | break; 1517 | case ESC: 1518 | /* Nothing to do for ESC in this mode. */ 1519 | break; 1520 | default: 1521 | editorInsertChar(c); 1522 | break; 1523 | } 1524 | 1525 | quit_times = KILO_QUIT_TIMES; /* Reset it to the original value. */ 1526 | } 1527 | 1528 | int editorFileWasModified(void) 1529 | { 1530 | return E.dirty; 1531 | } 1532 | 1533 | void updateWindowSize(void) 1534 | { 1535 | if (getWindowSize(STDIN_FILENO, STDOUT_FILENO, 1536 | &E.screenrows, &E.screencols) == -1) 1537 | { 1538 | perror("Unable to query the screen for size (columns / rows)"); 1539 | throw KiloException(1); 1540 | } 1541 | E.screenrows -= 2; /* Get room for status bar. */ 1542 | } 1543 | 1544 | void handleSigWinCh(int unused __attribute__((unused))) 1545 | { 1546 | updateWindowSize(); 1547 | if (E.cy > E.screenrows) 1548 | E.cy = E.screenrows - 1; 1549 | if (E.cx > E.screencols) 1550 | E.cx = E.screencols - 1; 1551 | editorRefreshScreen(); 1552 | } 1553 | 1554 | void initEditor(void) 1555 | { 1556 | E.cx = 0; 1557 | E.cy = 0; 1558 | E.rowoff = 0; 1559 | E.coloff = 0; 1560 | E.numrows = 0; 1561 | E.row = NULL; 1562 | E.dirty = 0; 1563 | E.filename = NULL; 1564 | E.syntax = NULL; 1565 | updateWindowSize(); 1566 | // signal(SIGWINCH, handleSigWinCh); 1567 | } 1568 | 1569 | int kilo(int argc, char **argv) 1570 | { 1571 | if (argc != 2) 1572 | { 1573 | fprintf(stderr, "Usage: edit \n"); 1574 | return 1; 1575 | } 1576 | 1577 | char filename[512]; 1578 | ESP32Console::console_realpath(argv[1], filename); 1579 | 1580 | try 1581 | { 1582 | initEditor(); 1583 | editorSelectSyntaxHighlight(filename); 1584 | editorOpen(filename); 1585 | enableRawMode(fileno(stdin)); 1586 | editorSetStatusMessage( 1587 | "HELP: Ctrl-S = save | Ctrl-Q = quit | Ctrl-F = find"); 1588 | while (1) 1589 | { 1590 | editorRefreshScreen(); 1591 | editorProcessKeypress(fileno(stdin)); 1592 | } 1593 | } 1594 | catch (KiloException ex) 1595 | { 1596 | // Ignore exception 1597 | } 1598 | 1599 | editorAtExit(); 1600 | 1601 | //Free some heap 1602 | for (int n = 0; n 29 | #include 30 | #include 31 | //#include 32 | #include 33 | #include 34 | #include 35 | #include 36 | #include 37 | #include 38 | #include 39 | #include 40 | #include 41 | #include 42 | #include 43 | 44 | #if defined(__GNUC__) && !defined(__clang__) 45 | # if (__GNUC__ * 10 + __GNUC_MINOR__) < 49 46 | # define CXXOPTS_NO_REGEX true 47 | # endif 48 | #endif 49 | 50 | #ifndef CXXOPTS_NO_REGEX 51 | # include 52 | #endif // CXXOPTS_NO_REGEX 53 | 54 | // Nonstandard before C++17, which is coincidentally what we also need for 55 | #ifdef __has_include 56 | # if __has_include() 57 | # include 58 | # ifdef __cpp_lib_optional 59 | # define CXXOPTS_HAS_OPTIONAL 60 | # endif 61 | # endif 62 | #endif 63 | 64 | #if __cplusplus >= 201603L 65 | #define CXXOPTS_NODISCARD [[nodiscard]] 66 | #else 67 | #define CXXOPTS_NODISCARD 68 | #endif 69 | 70 | #ifndef CXXOPTS_VECTOR_DELIMITER 71 | #define CXXOPTS_VECTOR_DELIMITER ',' 72 | #endif 73 | 74 | #define CXXOPTS__VERSION_MAJOR 3 75 | #define CXXOPTS__VERSION_MINOR 0 76 | #define CXXOPTS__VERSION_PATCH 0 77 | 78 | #if (__GNUC__ < 10 || (__GNUC__ == 10 && __GNUC_MINOR__ < 1)) && __GNUC__ >= 6 79 | #define CXXOPTS_NULL_DEREF_IGNORE 80 | #endif 81 | 82 | namespace cxxopts 83 | { 84 | static constexpr struct { 85 | uint8_t major, minor, patch; 86 | } version = { 87 | CXXOPTS__VERSION_MAJOR, 88 | CXXOPTS__VERSION_MINOR, 89 | CXXOPTS__VERSION_PATCH 90 | }; 91 | } // namespace cxxopts 92 | 93 | //when we ask cxxopts to use Unicode, help strings are processed using ICU, 94 | //which results in the correct lengths being computed for strings when they 95 | //are formatted for the help output 96 | //it is necessary to make sure that can be found by the 97 | //compiler, and that icu-uc is linked in to the binary. 98 | 99 | #ifdef CXXOPTS_USE_UNICODE 100 | #include 101 | 102 | namespace cxxopts 103 | { 104 | using String = icu::UnicodeString; 105 | 106 | inline 107 | String 108 | toLocalString(std::string s) 109 | { 110 | return icu::UnicodeString::fromUTF8(std::move(s)); 111 | } 112 | 113 | #if defined(__GNUC__) 114 | // GNU GCC with -Weffc++ will issue a warning regarding the upcoming class, we want to silence it: 115 | // warning: base class 'class std::enable_shared_from_this' has accessible non-virtual destructor 116 | #pragma GCC diagnostic push 117 | #pragma GCC diagnostic ignored "-Wnon-virtual-dtor" 118 | #pragma GCC diagnostic ignored "-Weffc++" 119 | // This will be ignored under other compilers like LLVM clang. 120 | #endif 121 | class UnicodeStringIterator : public 122 | std::iterator 123 | { 124 | public: 125 | 126 | UnicodeStringIterator(const icu::UnicodeString* string, int32_t pos) 127 | : s(string) 128 | , i(pos) 129 | { 130 | } 131 | 132 | value_type 133 | operator*() const 134 | { 135 | return s->char32At(i); 136 | } 137 | 138 | bool 139 | operator==(const UnicodeStringIterator& rhs) const 140 | { 141 | return s == rhs.s && i == rhs.i; 142 | } 143 | 144 | bool 145 | operator!=(const UnicodeStringIterator& rhs) const 146 | { 147 | return !(*this == rhs); 148 | } 149 | 150 | UnicodeStringIterator& 151 | operator++() 152 | { 153 | ++i; 154 | return *this; 155 | } 156 | 157 | UnicodeStringIterator 158 | operator+(int32_t v) 159 | { 160 | return UnicodeStringIterator(s, i + v); 161 | } 162 | 163 | private: 164 | const icu::UnicodeString* s; 165 | int32_t i; 166 | }; 167 | #if defined(__GNUC__) 168 | #pragma GCC diagnostic pop 169 | #endif 170 | 171 | inline 172 | String& 173 | stringAppend(String&s, String a) 174 | { 175 | return s.append(std::move(a)); 176 | } 177 | 178 | inline 179 | String& 180 | stringAppend(String& s, size_t n, UChar32 c) 181 | { 182 | for (size_t i = 0; i != n; ++i) 183 | { 184 | s.append(c); 185 | } 186 | 187 | return s; 188 | } 189 | 190 | template 191 | String& 192 | stringAppend(String& s, Iterator begin, Iterator end) 193 | { 194 | while (begin != end) 195 | { 196 | s.append(*begin); 197 | ++begin; 198 | } 199 | 200 | return s; 201 | } 202 | 203 | inline 204 | size_t 205 | stringLength(const String& s) 206 | { 207 | return s.length(); 208 | } 209 | 210 | inline 211 | std::string 212 | toUTF8String(const String& s) 213 | { 214 | std::string result; 215 | s.toUTF8String(result); 216 | 217 | return result; 218 | } 219 | 220 | inline 221 | bool 222 | empty(const String& s) 223 | { 224 | return s.isEmpty(); 225 | } 226 | } 227 | 228 | namespace std 229 | { 230 | inline 231 | cxxopts::UnicodeStringIterator 232 | begin(const icu::UnicodeString& s) 233 | { 234 | return cxxopts::UnicodeStringIterator(&s, 0); 235 | } 236 | 237 | inline 238 | cxxopts::UnicodeStringIterator 239 | end(const icu::UnicodeString& s) 240 | { 241 | return cxxopts::UnicodeStringIterator(&s, s.length()); 242 | } 243 | } 244 | 245 | //ifdef CXXOPTS_USE_UNICODE 246 | #else 247 | 248 | namespace cxxopts 249 | { 250 | using String = std::string; 251 | 252 | template 253 | T 254 | toLocalString(T&& t) 255 | { 256 | return std::forward(t); 257 | } 258 | 259 | inline 260 | size_t 261 | stringLength(const String& s) 262 | { 263 | return s.length(); 264 | } 265 | 266 | inline 267 | String& 268 | stringAppend(String&s, const String& a) 269 | { 270 | return s.append(a); 271 | } 272 | 273 | inline 274 | String& 275 | stringAppend(String& s, size_t n, char c) 276 | { 277 | return s.append(n, c); 278 | } 279 | 280 | template 281 | String& 282 | stringAppend(String& s, Iterator begin, Iterator end) 283 | { 284 | return s.append(begin, end); 285 | } 286 | 287 | template 288 | std::string 289 | toUTF8String(T&& t) 290 | { 291 | return std::forward(t); 292 | } 293 | 294 | inline 295 | bool 296 | empty(const std::string& s) 297 | { 298 | return s.empty(); 299 | } 300 | } // namespace cxxopts 301 | 302 | //ifdef CXXOPTS_USE_UNICODE 303 | #endif 304 | 305 | namespace cxxopts 306 | { 307 | namespace 308 | { 309 | #ifdef _WIN32 310 | const std::string LQUOTE("\'"); 311 | const std::string RQUOTE("\'"); 312 | #else 313 | const std::string LQUOTE("‘"); 314 | const std::string RQUOTE("’"); 315 | #endif 316 | } // namespace 317 | 318 | #if defined(__GNUC__) 319 | // GNU GCC with -Weffc++ will issue a warning regarding the upcoming class, we want to silence it: 320 | // warning: base class 'class std::enable_shared_from_this' has accessible non-virtual destructor 321 | #pragma GCC diagnostic push 322 | #pragma GCC diagnostic ignored "-Wnon-virtual-dtor" 323 | #pragma GCC diagnostic ignored "-Weffc++" 324 | // This will be ignored under other compilers like LLVM clang. 325 | #endif 326 | class Value : public std::enable_shared_from_this 327 | { 328 | public: 329 | 330 | virtual ~Value() = default; 331 | 332 | virtual 333 | std::shared_ptr 334 | clone() const = 0; 335 | 336 | virtual void 337 | parse(const std::string& text) const = 0; 338 | 339 | virtual void 340 | parse() const = 0; 341 | 342 | virtual bool 343 | has_default() const = 0; 344 | 345 | virtual bool 346 | is_container() const = 0; 347 | 348 | virtual bool 349 | has_implicit() const = 0; 350 | 351 | virtual std::string 352 | get_default_value() const = 0; 353 | 354 | virtual std::string 355 | get_implicit_value() const = 0; 356 | 357 | virtual std::shared_ptr 358 | default_value(const std::string& value) = 0; 359 | 360 | virtual std::shared_ptr 361 | implicit_value(const std::string& value) = 0; 362 | 363 | virtual std::shared_ptr 364 | no_implicit_value() = 0; 365 | 366 | virtual bool 367 | is_boolean() const = 0; 368 | }; 369 | #if defined(__GNUC__) 370 | #pragma GCC diagnostic pop 371 | #endif 372 | class OptionException : public std::exception 373 | { 374 | public: 375 | explicit OptionException(std::string message) 376 | : m_message(std::move(message)) 377 | { 378 | } 379 | 380 | CXXOPTS_NODISCARD 381 | const char* 382 | what() const noexcept override 383 | { 384 | return m_message.c_str(); 385 | } 386 | 387 | private: 388 | std::string m_message; 389 | }; 390 | 391 | class OptionSpecException : public OptionException 392 | { 393 | public: 394 | 395 | explicit OptionSpecException(const std::string& message) 396 | : OptionException(message) 397 | { 398 | } 399 | }; 400 | 401 | class OptionParseException : public OptionException 402 | { 403 | public: 404 | explicit OptionParseException(const std::string& message) 405 | : OptionException(message) 406 | { 407 | } 408 | }; 409 | 410 | class option_exists_error : public OptionSpecException 411 | { 412 | public: 413 | explicit option_exists_error(const std::string& option) 414 | : OptionSpecException("Option " + LQUOTE + option + RQUOTE + " already exists") 415 | { 416 | } 417 | }; 418 | 419 | class invalid_option_format_error : public OptionSpecException 420 | { 421 | public: 422 | explicit invalid_option_format_error(const std::string& format) 423 | : OptionSpecException("Invalid option format " + LQUOTE + format + RQUOTE) 424 | { 425 | } 426 | }; 427 | 428 | class option_syntax_exception : public OptionParseException { 429 | public: 430 | explicit option_syntax_exception(const std::string& text) 431 | : OptionParseException("Argument " + LQUOTE + text + RQUOTE + 432 | " starts with a - but has incorrect syntax") 433 | { 434 | } 435 | }; 436 | 437 | class option_not_exists_exception : public OptionParseException 438 | { 439 | public: 440 | explicit option_not_exists_exception(const std::string& option) 441 | : OptionParseException("Option " + LQUOTE + option + RQUOTE + " does not exist") 442 | { 443 | } 444 | }; 445 | 446 | class missing_argument_exception : public OptionParseException 447 | { 448 | public: 449 | explicit missing_argument_exception(const std::string& option) 450 | : OptionParseException( 451 | "Option " + LQUOTE + option + RQUOTE + " is missing an argument" 452 | ) 453 | { 454 | } 455 | }; 456 | 457 | class option_requires_argument_exception : public OptionParseException 458 | { 459 | public: 460 | explicit option_requires_argument_exception(const std::string& option) 461 | : OptionParseException( 462 | "Option " + LQUOTE + option + RQUOTE + " requires an argument" 463 | ) 464 | { 465 | } 466 | }; 467 | 468 | class option_not_has_argument_exception : public OptionParseException 469 | { 470 | public: 471 | option_not_has_argument_exception 472 | ( 473 | const std::string& option, 474 | const std::string& arg 475 | ) 476 | : OptionParseException( 477 | "Option " + LQUOTE + option + RQUOTE + 478 | " does not take an argument, but argument " + 479 | LQUOTE + arg + RQUOTE + " given" 480 | ) 481 | { 482 | } 483 | }; 484 | 485 | class option_not_present_exception : public OptionParseException 486 | { 487 | public: 488 | explicit option_not_present_exception(const std::string& option) 489 | : OptionParseException("Option " + LQUOTE + option + RQUOTE + " not present") 490 | { 491 | } 492 | }; 493 | 494 | class option_has_no_value_exception : public OptionException 495 | { 496 | public: 497 | explicit option_has_no_value_exception(const std::string& option) 498 | : OptionException( 499 | !option.empty() ? 500 | ("Option " + LQUOTE + option + RQUOTE + " has no value") : 501 | "Option has no value") 502 | { 503 | } 504 | }; 505 | 506 | class argument_incorrect_type : public OptionParseException 507 | { 508 | public: 509 | explicit argument_incorrect_type 510 | ( 511 | const std::string& arg 512 | ) 513 | : OptionParseException( 514 | "Argument " + LQUOTE + arg + RQUOTE + " failed to parse" 515 | ) 516 | { 517 | } 518 | }; 519 | 520 | class option_required_exception : public OptionParseException 521 | { 522 | public: 523 | explicit option_required_exception(const std::string& option) 524 | : OptionParseException( 525 | "Option " + LQUOTE + option + RQUOTE + " is required but not present" 526 | ) 527 | { 528 | } 529 | }; 530 | 531 | template 532 | void throw_or_mimic(const std::string& text) 533 | { 534 | static_assert(std::is_base_of::value, 535 | "throw_or_mimic only works on std::exception and " 536 | "deriving classes"); 537 | 538 | #ifndef CXXOPTS_NO_EXCEPTIONS 539 | // If CXXOPTS_NO_EXCEPTIONS is not defined, just throw 540 | throw T{text}; 541 | #else 542 | // Otherwise manually instantiate the exception, print what() to stderr, 543 | // and exit 544 | T exception{text}; 545 | std::cerr << exception.what() << std::endl; 546 | std::exit(EXIT_FAILURE); 547 | #endif 548 | } 549 | 550 | namespace values 551 | { 552 | namespace parser_tool 553 | { 554 | struct IntegerDesc 555 | { 556 | std::string negative = ""; 557 | std::string base = ""; 558 | std::string value = ""; 559 | }; 560 | struct ArguDesc { 561 | std::string arg_name = ""; 562 | bool grouping = false; 563 | bool set_value = false; 564 | std::string value = ""; 565 | }; 566 | #ifdef CXXOPTS_NO_REGEX 567 | inline IntegerDesc SplitInteger(const std::string &text) 568 | { 569 | if (text.empty()) 570 | { 571 | throw_or_mimic(text); 572 | } 573 | IntegerDesc desc; 574 | const char *pdata = text.c_str(); 575 | if (*pdata == '-') 576 | { 577 | pdata += 1; 578 | desc.negative = "-"; 579 | } 580 | if (strncmp(pdata, "0x", 2) == 0) 581 | { 582 | pdata += 2; 583 | desc.base = "0x"; 584 | } 585 | if (*pdata != '\0') 586 | { 587 | desc.value = std::string(pdata); 588 | } 589 | else 590 | { 591 | throw_or_mimic(text); 592 | } 593 | return desc; 594 | } 595 | 596 | inline bool IsTrueText(const std::string &text) 597 | { 598 | const char *pdata = text.c_str(); 599 | if (*pdata == 't' || *pdata == 'T') 600 | { 601 | pdata += 1; 602 | if (strncmp(pdata, "rue\0", 4) == 0) 603 | { 604 | return true; 605 | } 606 | } 607 | else if (strncmp(pdata, "1\0", 2) == 0) 608 | { 609 | return true; 610 | } 611 | return false; 612 | } 613 | 614 | inline bool IsFalseText(const std::string &text) 615 | { 616 | const char *pdata = text.c_str(); 617 | if (*pdata == 'f' || *pdata == 'F') 618 | { 619 | pdata += 1; 620 | if (strncmp(pdata, "alse\0", 5) == 0) 621 | { 622 | return true; 623 | } 624 | } 625 | else if (strncmp(pdata, "0\0", 2) == 0) 626 | { 627 | return true; 628 | } 629 | return false; 630 | } 631 | 632 | inline std::pair SplitSwitchDef(const std::string &text) 633 | { 634 | std::string short_sw, long_sw; 635 | const char *pdata = text.c_str(); 636 | if (isalnum(*pdata) && *(pdata + 1) == ',') { 637 | short_sw = std::string(1, *pdata); 638 | pdata += 2; 639 | } 640 | while (*pdata == ' ') { pdata += 1; } 641 | if (isalnum(*pdata)) { 642 | const char *store = pdata; 643 | pdata += 1; 644 | while (isalnum(*pdata) || *pdata == '-' || *pdata == '_') { 645 | pdata += 1; 646 | } 647 | if (*pdata == '\0') { 648 | long_sw = std::string(store, pdata - store); 649 | } else { 650 | throw_or_mimic(text); 651 | } 652 | } 653 | return std::pair(short_sw, long_sw); 654 | } 655 | 656 | inline ArguDesc ParseArgument(const char *arg, bool &matched) 657 | { 658 | ArguDesc argu_desc; 659 | const char *pdata = arg; 660 | matched = false; 661 | if (strncmp(pdata, "--", 2) == 0) 662 | { 663 | pdata += 2; 664 | if (isalnum(*pdata)) 665 | { 666 | argu_desc.arg_name.push_back(*pdata); 667 | pdata += 1; 668 | while (isalnum(*pdata) || *pdata == '-' || *pdata == '_') 669 | { 670 | argu_desc.arg_name.push_back(*pdata); 671 | pdata += 1; 672 | } 673 | if (argu_desc.arg_name.length() > 1) 674 | { 675 | if (*pdata == '=') 676 | { 677 | argu_desc.set_value = true; 678 | pdata += 1; 679 | if (*pdata != '\0') 680 | { 681 | argu_desc.value = std::string(pdata); 682 | } 683 | matched = true; 684 | } 685 | else if (*pdata == '\0') 686 | { 687 | matched = true; 688 | } 689 | } 690 | } 691 | } 692 | else if (strncmp(pdata, "-", 1) == 0) 693 | { 694 | pdata += 1; 695 | argu_desc.grouping = true; 696 | while (isalnum(*pdata)) 697 | { 698 | argu_desc.arg_name.push_back(*pdata); 699 | pdata += 1; 700 | } 701 | matched = !argu_desc.arg_name.empty() && *pdata == '\0'; 702 | } 703 | return argu_desc; 704 | } 705 | 706 | #else // CXXOPTS_NO_REGEX 707 | 708 | namespace 709 | { 710 | 711 | std::basic_regex integer_pattern 712 | ("(-)?(0x)?([0-9a-zA-Z]+)|((0x)?0)"); 713 | std::basic_regex truthy_pattern 714 | ("(t|T)(rue)?|1"); 715 | std::basic_regex falsy_pattern 716 | ("(f|F)(alse)?|0"); 717 | 718 | std::basic_regex option_matcher 719 | ("--([[:alnum:]][-_[:alnum:]]+)(=(.*))?|-([[:alnum:]]+)"); 720 | std::basic_regex option_specifier 721 | ("(([[:alnum:]]),)?[ ]*([[:alnum:]][-_[:alnum:]]*)?"); 722 | 723 | } // namespace 724 | 725 | inline IntegerDesc SplitInteger(const std::string &text) 726 | { 727 | std::smatch match; 728 | std::regex_match(text, match, integer_pattern); 729 | 730 | if (match.length() == 0) 731 | { 732 | throw_or_mimic(text); 733 | } 734 | 735 | IntegerDesc desc; 736 | desc.negative = match[1]; 737 | desc.base = match[2]; 738 | desc.value = match[3]; 739 | 740 | if (match.length(4) > 0) 741 | { 742 | desc.base = match[5]; 743 | desc.value = "0"; 744 | return desc; 745 | } 746 | 747 | return desc; 748 | } 749 | 750 | inline bool IsTrueText(const std::string &text) 751 | { 752 | std::smatch result; 753 | std::regex_match(text, result, truthy_pattern); 754 | return !result.empty(); 755 | } 756 | 757 | inline bool IsFalseText(const std::string &text) 758 | { 759 | std::smatch result; 760 | std::regex_match(text, result, falsy_pattern); 761 | return !result.empty(); 762 | } 763 | 764 | inline std::pair SplitSwitchDef(const std::string &text) 765 | { 766 | std::match_results result; 767 | std::regex_match(text.c_str(), result, option_specifier); 768 | if (result.empty()) 769 | { 770 | throw_or_mimic(text); 771 | } 772 | 773 | const std::string& short_sw = result[2]; 774 | const std::string& long_sw = result[3]; 775 | 776 | return std::pair(short_sw, long_sw); 777 | } 778 | 779 | inline ArguDesc ParseArgument(const char *arg, bool &matched) 780 | { 781 | std::match_results result; 782 | std::regex_match(arg, result, option_matcher); 783 | matched = !result.empty(); 784 | 785 | ArguDesc argu_desc; 786 | if (matched) { 787 | argu_desc.arg_name = result[1].str(); 788 | argu_desc.set_value = result[2].length() > 0; 789 | argu_desc.value = result[3].str(); 790 | if (result[4].length() > 0) 791 | { 792 | argu_desc.grouping = true; 793 | argu_desc.arg_name = result[4].str(); 794 | } 795 | } 796 | 797 | return argu_desc; 798 | } 799 | 800 | #endif // CXXOPTS_NO_REGEX 801 | #undef CXXOPTS_NO_REGEX 802 | } 803 | 804 | namespace detail 805 | { 806 | template 807 | struct SignedCheck; 808 | 809 | template 810 | struct SignedCheck 811 | { 812 | template 813 | void 814 | operator()(bool negative, U u, const std::string& text) 815 | { 816 | if (negative) 817 | { 818 | if (u > static_cast((std::numeric_limits::min)())) 819 | { 820 | throw_or_mimic(text); 821 | } 822 | } 823 | else 824 | { 825 | if (u > static_cast((std::numeric_limits::max)())) 826 | { 827 | throw_or_mimic(text); 828 | } 829 | } 830 | } 831 | }; 832 | 833 | template 834 | struct SignedCheck 835 | { 836 | template 837 | void 838 | operator()(bool, U, const std::string&) const {} 839 | }; 840 | 841 | template 842 | void 843 | check_signed_range(bool negative, U value, const std::string& text) 844 | { 845 | SignedCheck::is_signed>()(negative, value, text); 846 | } 847 | } // namespace detail 848 | 849 | template 850 | void 851 | checked_negate(R& r, T&& t, const std::string&, std::true_type) 852 | { 853 | // if we got to here, then `t` is a positive number that fits into 854 | // `R`. So to avoid MSVC C4146, we first cast it to `R`. 855 | // See https://github.com/jarro2783/cxxopts/issues/62 for more details. 856 | r = static_cast(-static_cast(t-1)-1); 857 | } 858 | 859 | template 860 | void 861 | checked_negate(R&, T&&, const std::string& text, std::false_type) 862 | { 863 | throw_or_mimic(text); 864 | } 865 | 866 | template 867 | void 868 | integer_parser(const std::string& text, T& value) 869 | { 870 | parser_tool::IntegerDesc int_desc = parser_tool::SplitInteger(text); 871 | 872 | using US = typename std::make_unsigned::type; 873 | constexpr bool is_signed = std::numeric_limits::is_signed; 874 | 875 | const bool negative = int_desc.negative.length() > 0; 876 | const uint8_t base = int_desc.base.length() > 0 ? 16 : 10; 877 | const std::string & value_match = int_desc.value; 878 | 879 | US result = 0; 880 | 881 | for (char ch : value_match) 882 | { 883 | US digit = 0; 884 | 885 | if (ch >= '0' && ch <= '9') 886 | { 887 | digit = static_cast(ch - '0'); 888 | } 889 | else if (base == 16 && ch >= 'a' && ch <= 'f') 890 | { 891 | digit = static_cast(ch - 'a' + 10); 892 | } 893 | else if (base == 16 && ch >= 'A' && ch <= 'F') 894 | { 895 | digit = static_cast(ch - 'A' + 10); 896 | } 897 | else 898 | { 899 | throw_or_mimic(text); 900 | } 901 | 902 | const US next = static_cast(result * base + digit); 903 | if (result > next) 904 | { 905 | throw_or_mimic(text); 906 | } 907 | 908 | result = next; 909 | } 910 | 911 | detail::check_signed_range(negative, result, text); 912 | 913 | if (negative) 914 | { 915 | checked_negate(value, result, text, std::integral_constant()); 916 | } 917 | else 918 | { 919 | value = static_cast(result); 920 | } 921 | } 922 | 923 | template 924 | void stringstream_parser(const std::string& text, T& value) 925 | { 926 | std::stringstream in(text); 927 | in >> value; 928 | if (!in) { 929 | throw_or_mimic(text); 930 | } 931 | } 932 | 933 | template ::value>::type* = nullptr 935 | > 936 | void parse_value(const std::string& text, T& value) 937 | { 938 | integer_parser(text, value); 939 | } 940 | 941 | inline 942 | void 943 | parse_value(const std::string& text, bool& value) 944 | { 945 | if (parser_tool::IsTrueText(text)) 946 | { 947 | value = true; 948 | return; 949 | } 950 | 951 | if (parser_tool::IsFalseText(text)) 952 | { 953 | value = false; 954 | return; 955 | } 956 | 957 | throw_or_mimic(text); 958 | } 959 | 960 | inline 961 | void 962 | parse_value(const std::string& text, std::string& value) 963 | { 964 | value = text; 965 | } 966 | 967 | // The fallback parser. It uses the stringstream parser to parse all types 968 | // that have not been overloaded explicitly. It has to be placed in the 969 | // source code before all other more specialized templates. 970 | template ::value>::type* = nullptr 972 | > 973 | void 974 | parse_value(const std::string& text, T& value) { 975 | stringstream_parser(text, value); 976 | } 977 | 978 | template 979 | void 980 | parse_value(const std::string& text, std::vector& value) 981 | { 982 | if (text.empty()) { 983 | T v; 984 | parse_value(text, v); 985 | value.emplace_back(std::move(v)); 986 | return; 987 | } 988 | std::stringstream in(text); 989 | std::string token; 990 | while(!in.eof() && std::getline(in, token, CXXOPTS_VECTOR_DELIMITER)) { 991 | T v; 992 | parse_value(token, v); 993 | value.emplace_back(std::move(v)); 994 | } 995 | } 996 | 997 | #ifdef CXXOPTS_HAS_OPTIONAL 998 | template 999 | void 1000 | parse_value(const std::string& text, std::optional& value) 1001 | { 1002 | T result; 1003 | parse_value(text, result); 1004 | value = std::move(result); 1005 | } 1006 | #endif 1007 | 1008 | inline 1009 | void parse_value(const std::string& text, char& c) 1010 | { 1011 | if (text.length() != 1) 1012 | { 1013 | throw_or_mimic(text); 1014 | } 1015 | 1016 | c = text[0]; 1017 | } 1018 | 1019 | template 1020 | struct type_is_container 1021 | { 1022 | static constexpr bool value = false; 1023 | }; 1024 | 1025 | template 1026 | struct type_is_container> 1027 | { 1028 | static constexpr bool value = true; 1029 | }; 1030 | 1031 | template 1032 | class abstract_value : public Value 1033 | { 1034 | using Self = abstract_value; 1035 | 1036 | public: 1037 | abstract_value() 1038 | : m_result(std::make_shared()) 1039 | , m_store(m_result.get()) 1040 | { 1041 | } 1042 | 1043 | explicit abstract_value(T* t) 1044 | : m_store(t) 1045 | { 1046 | } 1047 | 1048 | ~abstract_value() override = default; 1049 | 1050 | abstract_value& operator=(const abstract_value&) = default; 1051 | 1052 | abstract_value(const abstract_value& rhs) 1053 | { 1054 | if (rhs.m_result) 1055 | { 1056 | m_result = std::make_shared(); 1057 | m_store = m_result.get(); 1058 | } 1059 | else 1060 | { 1061 | m_store = rhs.m_store; 1062 | } 1063 | 1064 | m_default = rhs.m_default; 1065 | m_implicit = rhs.m_implicit; 1066 | m_default_value = rhs.m_default_value; 1067 | m_implicit_value = rhs.m_implicit_value; 1068 | } 1069 | 1070 | void 1071 | parse(const std::string& text) const override 1072 | { 1073 | parse_value(text, *m_store); 1074 | } 1075 | 1076 | bool 1077 | is_container() const override 1078 | { 1079 | return type_is_container::value; 1080 | } 1081 | 1082 | void 1083 | parse() const override 1084 | { 1085 | parse_value(m_default_value, *m_store); 1086 | } 1087 | 1088 | bool 1089 | has_default() const override 1090 | { 1091 | return m_default; 1092 | } 1093 | 1094 | bool 1095 | has_implicit() const override 1096 | { 1097 | return m_implicit; 1098 | } 1099 | 1100 | std::shared_ptr 1101 | default_value(const std::string& value) override 1102 | { 1103 | m_default = true; 1104 | m_default_value = value; 1105 | return shared_from_this(); 1106 | } 1107 | 1108 | std::shared_ptr 1109 | implicit_value(const std::string& value) override 1110 | { 1111 | m_implicit = true; 1112 | m_implicit_value = value; 1113 | return shared_from_this(); 1114 | } 1115 | 1116 | std::shared_ptr 1117 | no_implicit_value() override 1118 | { 1119 | m_implicit = false; 1120 | return shared_from_this(); 1121 | } 1122 | 1123 | std::string 1124 | get_default_value() const override 1125 | { 1126 | return m_default_value; 1127 | } 1128 | 1129 | std::string 1130 | get_implicit_value() const override 1131 | { 1132 | return m_implicit_value; 1133 | } 1134 | 1135 | bool 1136 | is_boolean() const override 1137 | { 1138 | return std::is_same::value; 1139 | } 1140 | 1141 | const T& 1142 | get() const 1143 | { 1144 | if (m_store == nullptr) 1145 | { 1146 | return *m_result; 1147 | } 1148 | return *m_store; 1149 | } 1150 | 1151 | protected: 1152 | std::shared_ptr m_result{}; 1153 | T* m_store{}; 1154 | 1155 | bool m_default = false; 1156 | bool m_implicit = false; 1157 | 1158 | std::string m_default_value{}; 1159 | std::string m_implicit_value{}; 1160 | }; 1161 | 1162 | template 1163 | class standard_value : public abstract_value 1164 | { 1165 | public: 1166 | using abstract_value::abstract_value; 1167 | 1168 | CXXOPTS_NODISCARD 1169 | std::shared_ptr 1170 | clone() const override 1171 | { 1172 | return std::make_shared>(*this); 1173 | } 1174 | }; 1175 | 1176 | template <> 1177 | class standard_value : public abstract_value 1178 | { 1179 | public: 1180 | ~standard_value() override = default; 1181 | 1182 | standard_value() 1183 | { 1184 | set_default_and_implicit(); 1185 | } 1186 | 1187 | explicit standard_value(bool* b) 1188 | : abstract_value(b) 1189 | { 1190 | set_default_and_implicit(); 1191 | } 1192 | 1193 | std::shared_ptr 1194 | clone() const override 1195 | { 1196 | return std::make_shared>(*this); 1197 | } 1198 | 1199 | private: 1200 | 1201 | void 1202 | set_default_and_implicit() 1203 | { 1204 | m_default = true; 1205 | m_default_value = "false"; 1206 | m_implicit = true; 1207 | m_implicit_value = "true"; 1208 | } 1209 | }; 1210 | } // namespace values 1211 | 1212 | template 1213 | std::shared_ptr 1214 | value() 1215 | { 1216 | return std::make_shared>(); 1217 | } 1218 | 1219 | template 1220 | std::shared_ptr 1221 | value(T& t) 1222 | { 1223 | return std::make_shared>(&t); 1224 | } 1225 | 1226 | class OptionAdder; 1227 | 1228 | class OptionDetails 1229 | { 1230 | public: 1231 | OptionDetails 1232 | ( 1233 | std::string short_, 1234 | std::string long_, 1235 | String desc, 1236 | std::shared_ptr val 1237 | ) 1238 | : m_short(std::move(short_)) 1239 | , m_long(std::move(long_)) 1240 | , m_desc(std::move(desc)) 1241 | , m_value(std::move(val)) 1242 | , m_count(0) 1243 | { 1244 | m_hash = std::hash{}(m_long + m_short); 1245 | } 1246 | 1247 | OptionDetails(const OptionDetails& rhs) 1248 | : m_desc(rhs.m_desc) 1249 | , m_value(rhs.m_value->clone()) 1250 | , m_count(rhs.m_count) 1251 | { 1252 | } 1253 | 1254 | OptionDetails(OptionDetails&& rhs) = default; 1255 | 1256 | CXXOPTS_NODISCARD 1257 | const String& 1258 | description() const 1259 | { 1260 | return m_desc; 1261 | } 1262 | 1263 | CXXOPTS_NODISCARD 1264 | const Value& 1265 | value() const { 1266 | return *m_value; 1267 | } 1268 | 1269 | CXXOPTS_NODISCARD 1270 | std::shared_ptr 1271 | make_storage() const 1272 | { 1273 | return m_value->clone(); 1274 | } 1275 | 1276 | CXXOPTS_NODISCARD 1277 | const std::string& 1278 | short_name() const 1279 | { 1280 | return m_short; 1281 | } 1282 | 1283 | CXXOPTS_NODISCARD 1284 | const std::string& 1285 | long_name() const 1286 | { 1287 | return m_long; 1288 | } 1289 | 1290 | CXXOPTS_NODISCARD 1291 | const std::string& 1292 | essential_name() const 1293 | { 1294 | return m_long.empty() ? m_short : m_long; 1295 | } 1296 | 1297 | size_t 1298 | hash() const 1299 | { 1300 | return m_hash; 1301 | } 1302 | 1303 | private: 1304 | std::string m_short{}; 1305 | std::string m_long{}; 1306 | String m_desc{}; 1307 | std::shared_ptr m_value{}; 1308 | int m_count; 1309 | 1310 | size_t m_hash{}; 1311 | }; 1312 | 1313 | struct HelpOptionDetails 1314 | { 1315 | std::string s; 1316 | std::string l; 1317 | String desc; 1318 | bool has_default; 1319 | std::string default_value; 1320 | bool has_implicit; 1321 | std::string implicit_value; 1322 | std::string arg_help; 1323 | bool is_container; 1324 | bool is_boolean; 1325 | }; 1326 | 1327 | struct HelpGroupDetails 1328 | { 1329 | std::string name{}; 1330 | std::string description{}; 1331 | std::vector options{}; 1332 | }; 1333 | 1334 | class OptionValue 1335 | { 1336 | public: 1337 | void 1338 | parse 1339 | ( 1340 | const std::shared_ptr& details, 1341 | const std::string& text 1342 | ) 1343 | { 1344 | ensure_value(details); 1345 | ++m_count; 1346 | m_value->parse(text); 1347 | m_long_name = &details->long_name(); 1348 | } 1349 | 1350 | void 1351 | parse_default(const std::shared_ptr& details) 1352 | { 1353 | ensure_value(details); 1354 | m_default = true; 1355 | m_long_name = &details->long_name(); 1356 | m_value->parse(); 1357 | } 1358 | 1359 | void 1360 | parse_no_value(const std::shared_ptr& details) 1361 | { 1362 | m_long_name = &details->long_name(); 1363 | } 1364 | 1365 | #if defined(CXXOPTS_NULL_DEREF_IGNORE) 1366 | #pragma GCC diagnostic push 1367 | #pragma GCC diagnostic ignored "-Wnull-dereference" 1368 | #endif 1369 | 1370 | CXXOPTS_NODISCARD 1371 | size_t 1372 | count() const noexcept 1373 | { 1374 | return m_count; 1375 | } 1376 | 1377 | #if defined(CXXOPTS_NULL_DEREF_IGNORE) 1378 | #pragma GCC diagnostic pop 1379 | #endif 1380 | 1381 | // TODO: maybe default options should count towards the number of arguments 1382 | CXXOPTS_NODISCARD 1383 | bool 1384 | has_default() const noexcept 1385 | { 1386 | return m_default; 1387 | } 1388 | 1389 | template 1390 | const T& 1391 | as() const 1392 | { 1393 | if (m_value == nullptr) { 1394 | throw_or_mimic( 1395 | m_long_name == nullptr ? "" : *m_long_name); 1396 | } 1397 | 1398 | #ifdef CXXOPTS_NO_RTTI 1399 | return static_cast&>(*m_value).get(); 1400 | #else 1401 | return dynamic_cast&>(*m_value).get(); 1402 | #endif 1403 | } 1404 | 1405 | private: 1406 | void 1407 | ensure_value(const std::shared_ptr& details) 1408 | { 1409 | if (m_value == nullptr) 1410 | { 1411 | m_value = details->make_storage(); 1412 | } 1413 | } 1414 | 1415 | 1416 | const std::string* m_long_name = nullptr; 1417 | // Holding this pointer is safe, since OptionValue's only exist in key-value pairs, 1418 | // where the key has the string we point to. 1419 | std::shared_ptr m_value{}; 1420 | size_t m_count = 0; 1421 | bool m_default = false; 1422 | }; 1423 | 1424 | class KeyValue 1425 | { 1426 | public: 1427 | KeyValue(std::string key_, std::string value_) 1428 | : m_key(std::move(key_)) 1429 | , m_value(std::move(value_)) 1430 | { 1431 | } 1432 | 1433 | CXXOPTS_NODISCARD 1434 | const std::string& 1435 | key() const 1436 | { 1437 | return m_key; 1438 | } 1439 | 1440 | CXXOPTS_NODISCARD 1441 | const std::string& 1442 | value() const 1443 | { 1444 | return m_value; 1445 | } 1446 | 1447 | template 1448 | T 1449 | as() const 1450 | { 1451 | T result; 1452 | values::parse_value(m_value, result); 1453 | return result; 1454 | } 1455 | 1456 | private: 1457 | std::string m_key; 1458 | std::string m_value; 1459 | }; 1460 | 1461 | using ParsedHashMap = std::unordered_map; 1462 | using NameHashMap = std::unordered_map; 1463 | 1464 | class ParseResult 1465 | { 1466 | public: 1467 | class Iterator 1468 | { 1469 | public: 1470 | using iterator_category = std::forward_iterator_tag; 1471 | using value_type = KeyValue; 1472 | using difference_type = void; 1473 | using pointer = const KeyValue*; 1474 | using reference = const KeyValue&; 1475 | 1476 | Iterator() = default; 1477 | Iterator(const Iterator&) = default; 1478 | 1479 | Iterator(const ParseResult *pr, bool end=false) 1480 | : m_pr(pr) 1481 | , m_iter(end? pr->m_defaults.end(): pr->m_sequential.begin()) 1482 | { 1483 | } 1484 | 1485 | Iterator& operator++() 1486 | { 1487 | ++m_iter; 1488 | if(m_iter == m_pr->m_sequential.end()) 1489 | { 1490 | m_iter = m_pr->m_defaults.begin(); 1491 | return *this; 1492 | } 1493 | return *this; 1494 | } 1495 | 1496 | Iterator operator++(int) 1497 | { 1498 | Iterator retval = *this; 1499 | ++(*this); 1500 | return retval; 1501 | } 1502 | 1503 | bool operator==(const Iterator& other) const 1504 | { 1505 | return m_iter == other.m_iter; 1506 | } 1507 | 1508 | bool operator!=(const Iterator& other) const 1509 | { 1510 | return !(*this == other); 1511 | } 1512 | 1513 | const KeyValue& operator*() 1514 | { 1515 | return *m_iter; 1516 | } 1517 | 1518 | const KeyValue* operator->() 1519 | { 1520 | return m_iter.operator->(); 1521 | } 1522 | 1523 | private: 1524 | const ParseResult* m_pr; 1525 | std::vector::const_iterator m_iter; 1526 | }; 1527 | 1528 | ParseResult() = default; 1529 | ParseResult(const ParseResult&) = default; 1530 | 1531 | ParseResult(NameHashMap&& keys, ParsedHashMap&& values, std::vector sequential, 1532 | std::vector default_opts, std::vector&& unmatched_args) 1533 | : m_keys(std::move(keys)) 1534 | , m_values(std::move(values)) 1535 | , m_sequential(std::move(sequential)) 1536 | , m_defaults(std::move(default_opts)) 1537 | , m_unmatched(std::move(unmatched_args)) 1538 | { 1539 | } 1540 | 1541 | ParseResult& operator=(ParseResult&&) = default; 1542 | ParseResult& operator=(const ParseResult&) = default; 1543 | 1544 | Iterator 1545 | begin() const 1546 | { 1547 | return Iterator(this); 1548 | } 1549 | 1550 | Iterator 1551 | end() const 1552 | { 1553 | return Iterator(this, true); 1554 | } 1555 | 1556 | size_t 1557 | count(const std::string& o) const 1558 | { 1559 | auto iter = m_keys.find(o); 1560 | if (iter == m_keys.end()) 1561 | { 1562 | return 0; 1563 | } 1564 | 1565 | auto viter = m_values.find(iter->second); 1566 | 1567 | if (viter == m_values.end()) 1568 | { 1569 | return 0; 1570 | } 1571 | 1572 | return viter->second.count(); 1573 | } 1574 | 1575 | const OptionValue& 1576 | operator[](const std::string& option) const 1577 | { 1578 | auto iter = m_keys.find(option); 1579 | 1580 | if (iter == m_keys.end()) 1581 | { 1582 | throw_or_mimic(option); 1583 | } 1584 | 1585 | auto viter = m_values.find(iter->second); 1586 | 1587 | if (viter == m_values.end()) 1588 | { 1589 | throw_or_mimic(option); 1590 | } 1591 | 1592 | return viter->second; 1593 | } 1594 | 1595 | const std::vector& 1596 | arguments() const 1597 | { 1598 | return m_sequential; 1599 | } 1600 | 1601 | const std::vector& 1602 | unmatched() const 1603 | { 1604 | return m_unmatched; 1605 | } 1606 | 1607 | const std::vector& 1608 | defaults() const 1609 | { 1610 | return m_defaults; 1611 | } 1612 | 1613 | const std::string 1614 | arguments_string() const 1615 | { 1616 | std::string result; 1617 | for(const auto& kv: m_sequential) 1618 | { 1619 | result += kv.key() + " = " + kv.value() + "\n"; 1620 | } 1621 | for(const auto& kv: m_defaults) 1622 | { 1623 | result += kv.key() + " = " + kv.value() + " " + "(default)" + "\n"; 1624 | } 1625 | return result; 1626 | } 1627 | 1628 | private: 1629 | NameHashMap m_keys{}; 1630 | ParsedHashMap m_values{}; 1631 | std::vector m_sequential{}; 1632 | std::vector m_defaults{}; 1633 | std::vector m_unmatched{}; 1634 | }; 1635 | 1636 | struct Option 1637 | { 1638 | Option 1639 | ( 1640 | std::string opts, 1641 | std::string desc, 1642 | std::shared_ptr value = ::cxxopts::value(), 1643 | std::string arg_help = "" 1644 | ) 1645 | : opts_(std::move(opts)) 1646 | , desc_(std::move(desc)) 1647 | , value_(std::move(value)) 1648 | , arg_help_(std::move(arg_help)) 1649 | { 1650 | } 1651 | 1652 | std::string opts_; 1653 | std::string desc_; 1654 | std::shared_ptr value_; 1655 | std::string arg_help_; 1656 | }; 1657 | 1658 | using OptionMap = std::unordered_map>; 1659 | using PositionalList = std::vector; 1660 | using PositionalListIterator = PositionalList::const_iterator; 1661 | 1662 | class OptionParser 1663 | { 1664 | public: 1665 | OptionParser(const OptionMap& options, const PositionalList& positional, bool allow_unrecognised) 1666 | : m_options(options) 1667 | , m_positional(positional) 1668 | , m_allow_unrecognised(allow_unrecognised) 1669 | { 1670 | } 1671 | 1672 | ParseResult 1673 | parse(int argc, const char* const* argv); 1674 | 1675 | bool 1676 | consume_positional(const std::string& a, PositionalListIterator& next); 1677 | 1678 | void 1679 | checked_parse_arg 1680 | ( 1681 | int argc, 1682 | const char* const* argv, 1683 | int& current, 1684 | const std::shared_ptr& value, 1685 | const std::string& name 1686 | ); 1687 | 1688 | void 1689 | add_to_option(OptionMap::const_iterator iter, const std::string& option, const std::string& arg); 1690 | 1691 | void 1692 | parse_option 1693 | ( 1694 | const std::shared_ptr& value, 1695 | const std::string& name, 1696 | const std::string& arg = "" 1697 | ); 1698 | 1699 | void 1700 | parse_default(const std::shared_ptr& details); 1701 | 1702 | void 1703 | parse_no_value(const std::shared_ptr& details); 1704 | 1705 | private: 1706 | 1707 | void finalise_aliases(); 1708 | 1709 | const OptionMap& m_options; 1710 | const PositionalList& m_positional; 1711 | 1712 | std::vector m_sequential{}; 1713 | std::vector m_defaults{}; 1714 | bool m_allow_unrecognised; 1715 | 1716 | ParsedHashMap m_parsed{}; 1717 | NameHashMap m_keys{}; 1718 | }; 1719 | 1720 | class Options 1721 | { 1722 | public: 1723 | 1724 | explicit Options(std::string program, std::string help_string = "") 1725 | : m_program(std::move(program)) 1726 | , m_help_string(toLocalString(std::move(help_string))) 1727 | , m_custom_help("[OPTION...]") 1728 | , m_positional_help("positional parameters") 1729 | , m_show_positional(false) 1730 | , m_allow_unrecognised(false) 1731 | , m_width(76) 1732 | , m_tab_expansion(false) 1733 | , m_options(std::make_shared()) 1734 | { 1735 | } 1736 | 1737 | Options& 1738 | positional_help(std::string help_text) 1739 | { 1740 | m_positional_help = std::move(help_text); 1741 | return *this; 1742 | } 1743 | 1744 | Options& 1745 | custom_help(std::string help_text) 1746 | { 1747 | m_custom_help = std::move(help_text); 1748 | return *this; 1749 | } 1750 | 1751 | Options& 1752 | show_positional_help() 1753 | { 1754 | m_show_positional = true; 1755 | return *this; 1756 | } 1757 | 1758 | Options& 1759 | allow_unrecognised_options() 1760 | { 1761 | m_allow_unrecognised = true; 1762 | return *this; 1763 | } 1764 | 1765 | Options& 1766 | set_width(size_t width) 1767 | { 1768 | m_width = width; 1769 | return *this; 1770 | } 1771 | 1772 | Options& 1773 | set_tab_expansion(bool expansion=true) 1774 | { 1775 | m_tab_expansion = expansion; 1776 | return *this; 1777 | } 1778 | 1779 | ParseResult 1780 | parse(int argc, const char* const* argv); 1781 | 1782 | OptionAdder 1783 | add_options(std::string group = ""); 1784 | 1785 | void 1786 | add_options 1787 | ( 1788 | const std::string& group, 1789 | std::initializer_list