diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..47eb61e --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +build/ +cmake-build-debug/ +.idea/workspace.xml \ No newline at end of file diff --git a/.idea/CppLinuxSerial.iml b/.idea/CppLinuxSerial.iml new file mode 100644 index 0000000..f08604b --- /dev/null +++ b/.idea/CppLinuxSerial.iml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..79b3c94 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..efb4d8f --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..3b2334a --- /dev/null +++ b/.travis.yml @@ -0,0 +1,22 @@ +language: cpp +compiler: g++ + +addons: + apt: + sources: + - llvm-toolchain-precise + - ubuntu-toolchain-r-test + packages: + - clang-3.7 + - g++-5 + - gcc-5 + +before_install: + - pip install --user cpp-coveralls + +install: + - if [ "$CXX" = "g++" ]; then export CXX="g++-5" CC="gcc-5"; fi + - if [ "$CXX" = "clang++" ]; then export CXX="clang++-3.7" CC="clang-3.7"; fi + +script: + - ./tools/build.sh -i \ No newline at end of file diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json new file mode 100644 index 0000000..f8ca022 --- /dev/null +++ b/.vscode/c_cpp_properties.json @@ -0,0 +1,81 @@ +{ + "configurations": [ + { + "name": "Mac", + "includePath": [ + "/usr/include", + "/usr/local/include", + "${workspaceRoot}" + ], + "defines": [], + "intelliSenseMode": "clang-x64", + "browse": { + "path": [ + "/usr/include", + "/usr/local/include", + "${workspaceRoot}" + ], + "limitSymbolsToIncludedHeaders": true, + "databaseFilename": "" + }, + "macFrameworkPath": [ + "/System/Library/Frameworks", + "/Library/Frameworks" + ] + }, + { + "name": "Linux", + "includePath": [ + "/usr/include/c++/6", + "/usr/include/x86_64-linux-gnu/c++/6", + "/usr/include/c++/6/backward", + "/usr/lib/gcc/x86_64-linux-gnu/6/include", + "/usr/local/include", + "/usr/lib/gcc/x86_64-linux-gnu/6/include-fixed", + "/usr/include/x86_64-linux-gnu", + "/usr/include", + "${workspaceRoot}", + "${workspaceRoot}/include" + ], + "defines": [], + "intelliSenseMode": "clang-x64", + "browse": { + "path": [ + "/usr/include/c++/6", + "/usr/include/x86_64-linux-gnu/c++/6", + "/usr/include/c++/6/backward", + "/usr/lib/gcc/x86_64-linux-gnu/6/include", + "/usr/local/include", + "/usr/lib/gcc/x86_64-linux-gnu/6/include-fixed", + "/usr/include/x86_64-linux-gnu", + "/usr/include", + "${workspaceRoot}", + "${workspaceRoot}/include" + ], + "limitSymbolsToIncludedHeaders": true, + "databaseFilename": "" + } + }, + { + "name": "Win32", + "includePath": [ + "C:/Program Files (x86)/Microsoft Visual Studio 14.0/VC/include", + "${workspaceRoot}" + ], + "defines": [ + "_DEBUG", + "UNICODE" + ], + "intelliSenseMode": "msvc-x64", + "browse": { + "path": [ + "C:/Program Files (x86)/Microsoft Visual Studio 14.0/VC/include/*", + "${workspaceRoot}" + ], + "limitSymbolsToIncludedHeaders": true, + "databaseFilename": "" + } + } + ], + "version": 3 +} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..926e6a1 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,35 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) +and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [v2.0.0] + +### Added +- Added CMake build support. +- Added basic, config and read/write unit tests using gtest. +- Improved read() performance due to removal of buffer creation on every call. +- TravisCI configuration file. +- Build script under `tools/`. + +### Changed +- Updated serial port to use C++14. +- Changed library name from serial-port to CppLinuxSerial. +- Updated Doxygen comments. + +## [v1.0.1] - 2014-05-21 + +### Changed +- Added ability to enable/disable echo with 'SerialPort::EnableEcho()'. + +## [v1.0.0] - 2014-05-15 + +### Added +- Initial commit. serial-port-cpp library has basic functions up and running. + +[Unreleased]: https://github.com/mbedded-ninja/CppLinuxSerial/compare/v2.0.0...HEAD +[v2.0.0]: https://github.com/mbedded-ninja/CppLinuxSerial/compare/v2.0.0...v1.0.1 +[v1.0.1]: https://github.com/mbedded-ninja/CppLinuxSerial/compare/v1.0.1...v1.0.0 \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..d3aabcd --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,60 @@ +cmake_minimum_required(VERSION 3.1.0) +project(CppLinuxSerial) + +add_definitions(-std=c++14) + +option(BUILD_TESTS "If set to true, unit tests will be build as part of make all." TRUE) +if (BUILD_TESTS) + message("BUILD_TESTS=TRUE, unit tests will be built.") +else () + message("BUILD_TESTS=FALSE, unit tests will NOT be built.") +endif () + +#=================================================================================================# +#========================================= gtest INSTALL =========================================# +#=================================================================================================# + +# Download and unpack googletest at configure time +configure_file(CMakeLists.txt.in googletest-download/CMakeLists.txt) +execute_process(COMMAND ${CMAKE_COMMAND} -G "${CMAKE_GENERATOR}" . + RESULT_VARIABLE result + WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/googletest-download ) +if(result) + message(FATAL_ERROR "CMake step for googletest failed: ${result}") +endif() +execute_process(COMMAND ${CMAKE_COMMAND} --build . + RESULT_VARIABLE result + WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/googletest-download ) +if(result) + message(FATAL_ERROR "Build step for googletest failed: ${result}") +endif() + +# Prevent overriding the parent project's compiler/linker +# settings on Windows +set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) + +# Add googletest directly to our build. This defines +# the gtest and gtest_main targets. +add_subdirectory(${CMAKE_BINARY_DIR}/googletest-src + ${CMAKE_BINARY_DIR}/googletest-build + EXCLUDE_FROM_ALL) + +# The gtest/gtest_main targets carry header search path +# dependencies automatically when using CMake 2.8.11 or +# later. Otherwise we have to add them here ourselves. +if (CMAKE_VERSION VERSION_LESS 2.8.11) + include_directories("${gtest_SOURCE_DIR}/include") +endif() + +#=================================================================================================# +#========================================= This Project ==========================================# +#=================================================================================================# + +# Now simply link your own targets against gtest, gmock, +# etc. as appropriate +include_directories(include) + +add_subdirectory(src) +if(BUILD_TESTS) + add_subdirectory(test/unit) +endif() diff --git a/CMakeLists.txt.in b/CMakeLists.txt.in new file mode 100644 index 0000000..d60a33e --- /dev/null +++ b/CMakeLists.txt.in @@ -0,0 +1,15 @@ +cmake_minimum_required(VERSION 2.8.2) + +project(googletest-download NONE) + +include(ExternalProject) +ExternalProject_Add(googletest + GIT_REPOSITORY https://github.com/google/googletest.git + GIT_TAG master + SOURCE_DIR "${CMAKE_BINARY_DIR}/googletest-src" + BINARY_DIR "${CMAKE_BINARY_DIR}/googletest-build" + CONFIGURE_COMMAND "" + BUILD_COMMAND "" + INSTALL_COMMAND "" + TEST_COMMAND "" +) \ No newline at end of file diff --git a/README.rst b/README.rst index bd285f4..a996c2a 100644 --- a/README.rst +++ b/README.rst @@ -1,27 +1,13 @@ -============================================================== -serial-port-cpp -============================================================== +============== +CppLinuxSerial +============== ---------------------------------- Serial port library written in C++ ---------------------------------- -.. image:: https://api.travis-ci.org/gbmhunter/serial-port-cpp.png?branch=master - :target: https://travis-ci.org/gbmhunter/serial-port-cpp - -- Author: gbmhunter (http://www.cladlab.com) -- Created: 2014/01/07 -- Last Modified: 2014/05/21 -- Version: v1.0.1.0 -- Company: CladLabs -- Project: Free Code Libraries -- Language: C++ -- Compiler: GCC -- uC Model: n/a -- Computer Architecture: n/a -- Operating System: n/a -- Documentation Format: Doxygen -- License: GPLv3 +.. image:: https://api.travis-ci.org/gbmhunter/CppLinuxSerial.png?branch=master + :target: https://travis-ci.org/gbmhunter/CppLinuxSerial .. role:: bash(code) :language: bash @@ -48,11 +34,13 @@ Dependencies The following table lists all of the libraries dependencies. -====================== ==================== ====================================================================== -Dependency Delivery Usage -====================== ==================== ====================================================================== - Standard C library snprintf() -====================== ==================== ====================================================================== +====================== ====================================================================== +Dependency Comments +====================== ====================================================================== +C++14 C++14 used for strongly typed enums, std::chrono and literals. + snprintf() +stty Used in unit tests to verify the serial port is configured correctly. +====================== ====================================================================== Issues ====== @@ -62,20 +50,7 @@ See GitHub Issues. Usage ===== -In main.c add... - -:: - - - - - int main() - { - - - } - - +Nothing here yet... FAQ === @@ -86,9 +61,4 @@ FAQ Changelog ========= -========= ========== =================================================================================================== -Version Date Comment -========= ========== =================================================================================================== -v1.0.1.0 2014/05/21 Added ability to enable/disable echo with 'SerialPort::EnableEcho()'. -v1.0.0.0 2014/05/15 Initial commit. serial-port-cpp library has basic functions up and running. -========= ========== =================================================================================================== \ No newline at end of file +See CHANGELOG.md. \ No newline at end of file diff --git a/api/SerialPortApi.hpp b/api/SerialPortApi.hpp deleted file mode 100644 index 72acf9b..0000000 --- a/api/SerialPortApi.hpp +++ /dev/null @@ -1,17 +0,0 @@ -//! -//! @file SerialPortApi.hpp -//! @author Geoffrey Hunter (www.cladlab.com) -//! @created 2014/05/15 -//! @last-modified 2014/05/15 -//! @brief File which contains all the API definitions needed to use the serial-port-cpp library. -//! @details -//! See README.rst in repo root dir for more info. - -// Header guard -#ifndef SERIAL_PORT_SERIAL_PORT_API_H -#define SERIAL_PORT_SERIAL_PORT_API_H - -// User headers -#include "../include/SerialPort.hpp" - -#endif // #ifndef SERIAL_PORT_SERIAL_PORT_API_H diff --git a/include/Config.hpp b/include/Config.hpp deleted file mode 100644 index c1a043f..0000000 --- a/include/Config.hpp +++ /dev/null @@ -1,15 +0,0 @@ -//! -//! @file Config.hpp -//! @author Geoffrey Hunter () -//! @created 2014/01/07 -//! @last-modified 2014/01/07 -//! @brief Config file for the ComPort library. -//! @details -//! See README.rst in repo root dir for more info. - - -namespace SerialPort -{ - - -} diff --git a/include/CppLinuxSerial/Exception.hpp b/include/CppLinuxSerial/Exception.hpp new file mode 100644 index 0000000..9bb57ae --- /dev/null +++ b/include/CppLinuxSerial/Exception.hpp @@ -0,0 +1,47 @@ +/// +/// \file Exception.hpp +/// \author Geoffrey Hunter (www.mbedded.ninja) +/// \edited n/a +/// \created 2017-11-09 +/// \last-modified 2017-11-27 +/// \brief Contains the Exception class. File originally from https://github.com/mbedded-ninja/CppUtils. +/// \details +/// See README.md in root dir for more info. + +#ifndef MN_CPP_LINUX_SERIAL_EXCEPTION_H_ +#define MN_CPP_LINUX_SERIAL_EXCEPTION_H_ + +// System includes +#include +#include +#include +#include + +namespace mn { + namespace CppLinuxSerial { + + class Exception : public std::runtime_error { + + public: + Exception(const char *file, int line, const std::string &arg) : + std::runtime_error(arg) { + msg_ = std::string(file) + ":" + std::to_string(line) + ": " + arg; + } + + ~Exception() throw() {} + + const char *what() const throw() override { + return msg_.c_str(); + } + + private: + std::string msg_; + }; + + } // namespace CppLinuxSerial +} // namespace mn + +#define THROW_EXCEPT(arg) throw Exception(__FILE__, __LINE__, arg); + + +#endif // MN_CPP_LINUX_SERIAL_EXCEPTION_H_ \ No newline at end of file diff --git a/include/CppLinuxSerial/SerialPort.hpp b/include/CppLinuxSerial/SerialPort.hpp new file mode 100644 index 0000000..b12565b --- /dev/null +++ b/include/CppLinuxSerial/SerialPort.hpp @@ -0,0 +1,131 @@ +/// +/// \file SerialPort.hpp +/// \author Geoffrey Hunter () +/// \created 2014-01-07 +/// \last-modified 2017-11-23 +/// \brief The main serial port class. +/// \details +/// See README.rst in repo root dir for more info. + +// Header guard +#ifndef SERIAL_PORT_SERIAL_PORT_H +#define SERIAL_PORT_SERIAL_PORT_H + +// System headers +#include +#include // For file I/O (reading/writing to COM port) +#include +#include // POSIX terminal control definitions (struct termios) +#include + +// User headers + +namespace mn { + namespace CppLinuxSerial { + + /// \brief Strongly-typed enumeration of baud rates for use with the SerialPort class + enum class BaudRate { + B_9600, + B_38400, + B_57600, + B_115200, + CUSTOM + }; + + enum class State { + CLOSED, + OPEN + }; + +/// \brief SerialPort object is used to perform rx/tx serial communication. + class SerialPort { + + public: + /// \brief Default constructor. You must specify at least the device before calling Open(). + SerialPort(); + + /// \brief Constructor that sets up serial port with the basic (required) parameters. + SerialPort(const std::string &device, BaudRate baudRate); + + //! @brief Destructor. Closes serial port if still open. + virtual ~SerialPort(); + + /// \brief Sets the device to use for serial port communications. + /// \details Method can be called when serial port is in any state. + void SetDevice(const std::string &device); + + void SetBaudRate(BaudRate baudRate); + + /// \brief Sets the read timeout (in milliseconds)/blocking mode. + /// \details Only call when state != OPEN. This method manupulates VMIN and VTIME. + /// \param timeout_ms Set to -1 to infinite timeout, 0 to return immediately with any data (non + /// blocking, or >0 to wait for data for a specified number of milliseconds). Timeout will + /// be rounded to the nearest 100ms (a Linux API restriction). Maximum value limited to + /// 25500ms (another Linux API restriction). + void SetTimeout(int32_t timeout_ms); + + /// \brief Enables/disables echo. + /// \param value Pass in true to enable echo, false to disable echo. + void SetEcho(bool value); + + /// \brief Opens the COM port for use. + /// \throws CppLinuxSerial::Exception if device cannot be opened. + /// \note Must call this before you can configure the COM port. + void Open(); + + /// \brief Closes the COM port. + void Close(); + + /// \brief Sends a message over the com port. + /// \param data The data that will be written to the COM port. + /// \throws CppLinuxSerial::Exception if state != OPEN. + void Write(const std::string& data); + + /// \brief Use to read from the COM port. + /// \param data The object the read characters from the COM port will be saved to. + /// \param wait_ms The amount of time to wait for data. Set to 0 for non-blocking mode. Set to -1 + /// to wait indefinitely for new data. + /// \throws CppLinuxSerial::Exception if state != OPEN. + void Read(std::string& data); + + private: + + /// \brief Returns a populated termios structure for the passed in file descriptor. + termios GetTermios(); + + /// \brief Configures the tty device as a serial port. + /// \warning Device must be open (valid file descriptor) when this is called. + void ConfigureTermios(); + + void SetTermios(termios myTermios); + + /// \brief Keeps track of the serial port's state. + State state_; + + /// \brief The file path to the serial port device (e.g. "/dev/ttyUSB0"). + std::string device_; + + /// \brief The current baud rate. + BaudRate baudRate_; + + /// \brief The file descriptor for the open file. This gets written to when Open() is called. + int fileDesc_; + + bool echo_; + + int32_t timeout_ms_; + + std::vector readBuffer_; + unsigned char readBufferSize_B_; + + static constexpr BaudRate defaultBaudRate_ = BaudRate::B_57600; + static constexpr int32_t defaultTimeout_ms_ = -1; + static constexpr unsigned char defaultReadBufferSize_B_ = 255; + + + }; + + } // namespace CppLinuxSerial +} // namespace mn + +#endif // #ifndef SERIAL_PORT_SERIAL_PORT_H diff --git a/include/SerialPort.hpp b/include/SerialPort.hpp deleted file mode 100644 index 7a035a7..0000000 --- a/include/SerialPort.hpp +++ /dev/null @@ -1,103 +0,0 @@ -//! -//! @file SerialPort.hpp -//! @author Geoffrey Hunter () -//! @created 2014/01/07 -//! @last-modified 2014/05/21 -//! @brief The main serial port class. -//! @details -//! See README.rst in repo root dir for more info. - -// Header guard -#ifndef SERIAL_PORT_SERIAL_PORT_H -#define SERIAL_PORT_SERIAL_PORT_H - -// System headers -#include // For file I/O (reading/writing to COM port) -#include -#include // POSIX terminal control definitions (struct termios) - -// User headers -#include "lib/SmartPrint/include/Sp.hpp" - -namespace SerialPort -{ - - //! @brief Strongly-typed enumeration of baud rates for use with the SerialPort class - enum class BaudRates - { - none, - b9600, - b57600 - }; - - //! @brief SerialPort object is used to perform rx/tx serial communication. - class SerialPort - { - - public: - - //! @brief Constructor - SerialPort(); - - //! @brief Destructor - virtual ~SerialPort(); - - //! @brief Sets the file path to use for communications. The file path must be set before Open() is called, otherwise Open() will return an error. - void SetFilePath(std::string filePath); - - void SetBaudRate(BaudRates baudRate); - - //! @brief Controls what happens when Read() is called. - //! @param numOfCharToWait Minimum number of characters to wait for before returning. Set to 0 for non-blocking mode. - void SetNumCharsToWait(uint32_t numCharsToWait); - - //! @brief Enables/disables echo. - //! param echoOn Pass in true to enable echo, false to disable echo. - void EnableEcho(bool echoOn); - - //! @brief Opens the COM port for use. - //! @throws {std::runtime_error} if filename has not been set. - //! {std::system_error} if system open() operation fails. - //! @note Must call this before you can configure the COM port. - void Open(); - - //! @brief Sets all settings for the com port to common defaults. - void SetEverythingToCommonDefaults(); - - //! @brief Closes the COM port. - void Close(); - - //! @brief Sends a message over the com port. - //! @param str Reference to an string containing the characters to write to the COM port. - //! @throws {std::runtime_error} if filename has not been set. - //! {std::system_error} if system write() operation fails. - void Write(std::string* str); - - //! @brief Use to read from the COM port. - //! @param str Reference to a string that the read characters from the COM port will be saved to. - //! @throws {std::runtime_error} if filename has not been set. - //! {std::system_error} if system read() operation fails. - void Read(std::string* str); - - private: - - std::string filePath; - - BaudRates baudRate; - - //! @brief The file descriptor for the open file. This gets written to when Open() is called. - int fileDesc; - - //! @brief Object for printing debug and error messages with - SmartPrint::Sp* sp; - - //! @brief Returns a populated termios structure for the passed in file descriptor. - termios GetTermios(); - - void SetTermios(termios myTermios); - - }; - -} // namespace SerialPort - -#endif // #ifndef SERIAL_PORT_SERIAL_PORT_H diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 0000000..12773f3 --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,19 @@ + + +file(GLOB_RECURSE CppLinuxSerial_SRC + "*.cpp") + +file(GLOB_RECURSE CppLinuxSerial_HEADERS + "${CMAKE_SOURCE_DIR}/include/*.hpp") + +add_library(CppLinuxSerial ${CppLinuxSerial_SRC} ${CppLinuxSerial_HEADERS}) + +target_include_directories(CppLinuxSerial PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) + +# On Linux, "sudo make install" will typically copy the library +# into the folder /usr/local/bin +install(TARGETS CppLinuxSerial DESTINATION lib) + +# On Linux, "sudo make install" will typically copy the +# folder into /usr/local/include +install(DIRECTORY ${CMAKE_SOURCE_DIR}/include/CppLinuxSerial DESTINATION include) \ No newline at end of file diff --git a/src/SerialPort.cpp b/src/SerialPort.cpp index ea6636b..c80a831 100644 --- a/src/SerialPort.cpp +++ b/src/SerialPort.cpp @@ -1,12 +1,13 @@ //! //! @file SerialPort.cpp -//! @author Geoffrey Hunter () -//! @created 2014/01/07 -//! @last-modified 2014/05/21 +//! @author Geoffrey Hunter (www.mbedded.ninja) +//! @created 2014-01-07 +//! @last-modified 2017-11-27 //! @brief The main serial port class. //! @details //! See README.rst in repo root dir for more info. +// System includes #include #include #include // Standard input/output definitions @@ -17,82 +18,57 @@ #include // POSIX terminal control definitions (struct termios) #include // For throwing std::system_error -#include "../include/Config.hpp" -#include "../include/SerialPort.hpp" -#include "lib/SmartPrint/api/SmartPrint.hpp" - -namespace SerialPort -{ - - SerialPort::SerialPort() : - filePath(std::string()), - baudRate(BaudRates::none), - fileDesc(0), - sp( - new SmartPrint::Sp( - "Port", - &std::cout, - &SmartPrint::Colours::yellow, - &std::cout, - &SmartPrint::Colours::yellow, - &std::cerr, - &SmartPrint::Colours::red)) - { - // Everything setup in initialiser list - } +// User includes +#include "CppLinuxSerial/Exception.hpp" +#include "CppLinuxSerial/SerialPort.hpp" - SerialPort::~SerialPort() - { +namespace mn { +namespace CppLinuxSerial { + SerialPort::SerialPort() { + echo_ = false; + timeout_ms_ = defaultTimeout_ms_; + baudRate_ = defaultBaudRate_; + readBufferSize_B_ = defaultReadBufferSize_B_; + readBuffer_.reserve(readBufferSize_B_); } - void SerialPort::SetFilePath(std::string filePath) - { - // Save a pointer to the file path - this->filePath = filePath; + SerialPort::SerialPort(const std::string& device, BaudRate baudRate) : + SerialPort() { + device_ = device; + baudRate_ = baudRate; } - void SerialPort::SetBaudRate(BaudRates baudRate) - { + SerialPort::~SerialPort() { + try { + Close(); + } catch(...) { + // We can't do anything about this! + // But we don't want to throw within destructor, so swallow + } + } - // Get current termios struct - termios myTermios = this->GetTermios(); + void SerialPort::SetDevice(const std::string& device) { + device_ = device; + if(state_ == State::OPEN) - switch(baudRate) - { - case BaudRates::none: - // Error, baud rate has not been set yet - throw std::runtime_error("Baud rate for '" + this->filePath + "' cannot be set to none."); - break; - case BaudRates::b9600: - cfsetispeed(&myTermios, B9600); - cfsetospeed(&myTermios, B9600); - break; - case BaudRates::b57600: - cfsetispeed(&myTermios, B57600); - cfsetospeed(&myTermios, B57600); - break; - } - // Save back to file - this->SetTermios(myTermios); + ConfigureTermios(); + } - // Setting the baudrate must of been successful, so we can now store this - // new value internally. This must be done last! - this->baudRate = baudRate; + void SerialPort::SetBaudRate(BaudRate baudRate) { + baudRate_ = baudRate; + if(state_ == State::OPEN) + ConfigureTermios(); } void SerialPort::Open() { - this->sp->PrintDebug(SmartPrint::Ss() << "Attempting to open COM port \"" << this->filePath << "\"."); - - if(this->filePath.size() == 0) - { - //this->sp->PrintError(SmartPrint::Ss() << "Attempted to open file when file path has not been assigned to."); - //return false; + std::cout << "Attempting to open COM port \"" << device_ << "\"." << std::endl; - throw std::runtime_error("Attempted to open file when file path has not been assigned to."); + if(device_.empty()) { + THROW_EXCEPT("Attempted to open file when file path has not been assigned to."); } // Attempt to open file @@ -100,73 +76,31 @@ namespace SerialPort // O_RDONLY for read-only, O_WRONLY for write only, O_RDWR for both read/write access // 3rd, optional parameter is mode_t mode - this->fileDesc = open(this->filePath.c_str(), O_RDWR); + fileDesc_ = open(device_.c_str(), O_RDWR); // Check status - if (this->fileDesc == -1) - { - // Could not open COM port - //this->sp->PrintError(SmartPrint::Ss() << "Unable to open " << this->filePath << " - " << strerror(errno)); - //return false; - - throw std::system_error(EFAULT, std::system_category()); + if(fileDesc_ == -1) { + THROW_EXCEPT("Could not open device " + device_ + ". Is the device name correct and do you have read/write permission?"); } - this->sp->PrintDebug(SmartPrint::Ss() << "COM port opened successfully."); - - // If code reaches here, open and config must of been successful + ConfigureTermios(); + std::cout << "COM port opened successfully." << std::endl; + state_ = State::OPEN; } - void SerialPort::EnableEcho(bool echoOn) - { - termios settings = this->GetTermios(); - settings.c_lflag = echoOn - ? (settings.c_lflag | ECHO ) - : (settings.c_lflag & ~(ECHO)); - //tcsetattr( STDIN_FILENO, TCSANOW, &settings ); - this->SetTermios(settings); + void SerialPort::SetEcho(bool value) { + echo_ = value; + ConfigureTermios(); } - void SerialPort::SetEverythingToCommonDefaults() + void SerialPort::ConfigureTermios() { - this->sp->PrintDebug(SmartPrint::Ss() << "Configuring COM port \"" << this->filePath << "\"."); + std::cout << "Configuring COM port \"" << device_ << "\"." << std::endl; //================== CONFIGURE ==================// - termios tty = this->GetTermios(); - /*struct termios tty; - memset(&tty, 0, sizeof(tty)); - - // Get current settings (will be stored in termios structure) - if(tcgetattr(this->fileDesc, &tty) != 0) - { - // Error occurred - this->sp->PrintError(SmartPrint::Ss() << "Could not get terminal attributes for \"" << this->filePath << "\" - " << strerror(errno)); - //return false; - return; - }*/ - - //========================= SET UP BAUD RATES =========================// - - this->SetBaudRate(BaudRates::b57600); - - /* - switch(this->baudRate) - { - case BaudRates::none: - // Error, baud rate has not been set yet - this->sp->PrintError(SmartPrint::Ss() << "Baud rate for \"" << this->filePath << "\" has not been set."); - return; - case BaudRates::b9600: - cfsetispeed(&tty, B9600); - cfsetospeed(&tty, B9600); - break; - case BaudRates::b57600: - cfsetispeed(&tty, B57600); - cfsetospeed(&tty, B57600); - break; - }*/ + termios tty = GetTermios(); //================= (.c_cflag) ===============// @@ -178,6 +112,32 @@ namespace SerialPort tty.c_cflag |= CREAD | CLOCAL; // Turn on READ & ignore ctrl lines (CLOCAL = 1) + //===================== BAUD RATE =================// + + switch(baudRate_) { + case BaudRate::B_9600: + cfsetispeed(&tty, B9600); + cfsetospeed(&tty, B9600); + break; + case BaudRate::B_38400: + cfsetispeed(&tty, B38400); + cfsetospeed(&tty, B38400); + break; + case BaudRate::B_57600: + cfsetispeed(&tty, B57600); + cfsetospeed(&tty, B57600); + break; + case BaudRate::B_115200: + cfsetispeed(&tty, B115200); + cfsetospeed(&tty, B115200); + break; + case BaudRate::CUSTOM: + // See https://gist.github.com/kennethryerson/f7d1abcf2633b7c03cf0 + throw std::runtime_error("Custom baud rate not yet supported."); + default: + throw std::runtime_error(std::string() + "baudRate passed to " + __PRETTY_FUNCTION__ + " unrecognized."); + } + //===================== (.c_oflag) =================// tty.c_oflag = 0; // No remapping, no delays @@ -185,14 +145,29 @@ namespace SerialPort //================= CONTROL CHARACTERS (.c_cc[]) ==================// - // c_cc[WMIN] sets the number of characters to block (wait) for when read() is called. - // Set to 0 if you don't want read to block. Only meaningful when port set to non-canonical mode - //tty.c_cc[VMIN] = 1; - this->SetNumCharsToWait(1); - // c_cc[VTIME] sets the inter-character timer, in units of 0.1s. // Only meaningful when port is set to non-canonical mode - tty.c_cc[VTIME] = 5; // 0.5 seconds read timeout + // VMIN = 0, VTIME = 0: No blocking, return immediately with what is available + // VMIN > 0, VTIME = 0: read() waits for VMIN bytes, could block indefinitely + // VMIN = 0, VTIME > 0: Block until any amount of data is available, OR timeout occurs + // VMIN > 0, VTIME > 0: Block until either VMIN characters have been received, or VTIME + // after first character has elapsed + // c_cc[WMIN] sets the number of characters to block (wait) for when read() is called. + // Set to 0 if you don't want read to block. Only meaningful when port set to non-canonical mode + + if(timeout_ms_ == -1) { + // Always wait for at least one byte, this could + // block indefinitely + tty.c_cc[VTIME] = 0; + tty.c_cc[VMIN] = 1; + } else if(timeout_ms_ == 0) { + // Setting both to 0 will give a non-blocking read + tty.c_cc[VTIME] = 0; + tty.c_cc[VMIN] = 0; + } else if(timeout_ms_ > 0) { + tty.c_cc[VTIME] = (cc_t)(timeout_ms_/100); // 0.5 seconds read timeout + tty.c_cc[VMIN] = 0; + } //======================== (.c_iflag) ====================// @@ -200,16 +175,20 @@ namespace SerialPort tty.c_iflag &= ~(IXON | IXOFF | IXANY); // Turn off s/w flow ctrl tty.c_iflag &= ~(IGNBRK|BRKINT|PARMRK|ISTRIP|INLCR|IGNCR|ICRNL); + + //=========================== LOCAL MODES (c_lflag) =======================// // Canonical input is when read waits for EOL or EOF characters before returning. In non-canonical mode, the rate at which // read() returns is instead controlled by c_cc[VMIN] and c_cc[VTIME] tty.c_lflag &= ~ICANON; // Turn off canonical input, which is suitable for pass-through - tty.c_lflag &= ~ECHO; // Turn off echo + echo_ ? (tty.c_lflag | ECHO ) : (tty.c_lflag & ~(ECHO)); // Configure echo depending on echo_ boolean tty.c_lflag &= ~ECHOE; // Turn off echo erase (echo erase only relevant if canonical input is active) tty.c_lflag &= ~ECHONL; // tty.c_lflag &= ~ISIG; // Disables recognition of INTR (interrupt), QUIT and SUSP (suspend) characters + + // Try and use raw function call //cfmakeraw(&tty); @@ -227,93 +206,73 @@ namespace SerialPort }*/ } - void SerialPort::SetNumCharsToWait(uint32_t numCharsToWait) - { - // Get current termios struct - termios myTermios = this->GetTermios(); + void SerialPort::Write(const std::string& data) { - // Save the number of characters to wait for - // to the control register - myTermios.c_cc[VMIN] = numCharsToWait; + if(state_ != State::OPEN) + THROW_EXCEPT(std::string() + __PRETTY_FUNCTION__ + " called but state != OPEN. Please call Open() first."); - // Save termios back - this->SetTermios(myTermios); - } - - void SerialPort::Write(std::string* str) - { - if(this->fileDesc == 0) - { - //this->sp->PrintError(SmartPrint::Ss() << ); - //return false; - - throw std::runtime_error("SendMsg called but file descriptor (fileDesc) was 0, indicating file has not been opened."); + if(fileDesc_ < 0) { + THROW_EXCEPT(std::string() + __PRETTY_FUNCTION__ + " called but file descriptor < 0, indicating file has not been opened."); } - int writeResult = write(this->fileDesc, str->c_str(), str->size()); + int writeResult = write(fileDesc_, data.c_str(), data.size()); // Check status - if (writeResult == -1) - { - // Could not open COM port - //this->sp->PrintError(SmartPrint::Ss() << "Unable to write to \"" << this->filePath << "\" - " << strerror(errno)); - //return false; - + if (writeResult == -1) { throw std::system_error(EFAULT, std::system_category()); } - - // If code reaches here than write must of been successful } - void SerialPort::Read(std::string* str) + void SerialPort::Read(std::string& data) { - if(this->fileDesc == 0) - { + data.clear(); + + if(fileDesc_ == 0) { //this->sp->PrintError(SmartPrint::Ss() << "Read() was called but file descriptor (fileDesc) was 0, indicating file has not been opened."); //return false; - throw std::runtime_error("Read() was called but file descriptor (fileDesc) was 0, indicating file has not been opened."); + THROW_EXCEPT("Read() was called but file descriptor (fileDesc) was 0, indicating file has not been opened."); } // Allocate memory for read buffer - char buf [256]; - memset (&buf, '\0', sizeof buf); +// char buf [256]; +// memset (&buf, '\0', sizeof buf); // Read from file - int n = read(this->fileDesc, &buf, sizeof(buf)); + // We provide the underlying raw array from the readBuffer_ vector to this C api. + // This will work because we do not delete/resize the vector while this method + // is called + ssize_t n = read(fileDesc_, &readBuffer_[0], readBufferSize_B_); // Error Handling - if(n < 0) - { - // Could not open COM port - //this->sp->PrintError(SmartPrint::Ss() << "Unable to read from \"" << this->filePath << "\" - " << strerror(errno)); - //return false; - + if(n < 0) { + // Read was unsuccessful throw std::system_error(EFAULT, std::system_category()); } - if(n > 0) - { - //this->sp->PrintDebug(SmartPrint::Ss() << "\"" << n << "\" characters have been read from \"" << this->filePath << "\""); - // Characters have been read - buf[n] = '\0'; + if(n > 0) { + +// buf[n] = '\0'; //printf("%s\r\n", buf); - str->append(buf); +// data.append(buf); + data = std::string(&readBuffer_[0], n); //std::cout << *str << " and size of string =" << str->size() << "\r\n"; } // If code reaches here, read must of been successful } - termios SerialPort::GetTermios() - { + termios SerialPort::GetTermios() { + if(fileDesc_ == -1) + throw std::runtime_error("GetTermios() called but file descriptor was not valid."); + struct termios tty; memset(&tty, 0, sizeof(tty)); // Get current settings (will be stored in termios structure) - if(tcgetattr(this->fileDesc, &tty) != 0) + if(tcgetattr(fileDesc_, &tty) != 0) { // Error occurred - this->sp->PrintError(SmartPrint::Ss() << "Could not get terminal attributes for \"" << this->filePath << "\" - " << strerror(errno)); + std::cout << "Could not get terminal attributes for \"" << device_ << "\" - " << strerror(errno) << std::endl; throw std::system_error(EFAULT, std::system_category()); //return false; } @@ -324,12 +283,12 @@ namespace SerialPort void SerialPort::SetTermios(termios myTermios) { // Flush port, then apply attributes - tcflush(this->fileDesc, TCIFLUSH); + tcflush(fileDesc_, TCIFLUSH); - if(tcsetattr(this->fileDesc, TCSANOW, &myTermios) != 0) + if(tcsetattr(fileDesc_, TCSANOW, &myTermios) != 0) { // Error occurred - this->sp->PrintError(SmartPrint::Ss() << "Could not apply terminal attributes for \"" << this->filePath << "\" - " << strerror(errno)); + std::cout << "Could not apply terminal attributes for \"" << device_ << "\" - " << strerror(errno) << std::endl; throw std::system_error(EFAULT, std::system_category()); } @@ -337,4 +296,27 @@ namespace SerialPort // Successful! } -} // namespace ComPort + void SerialPort::Close() { + if(fileDesc_ != -1) { + auto retVal = close(fileDesc_); + if(retVal != 0) + THROW_EXCEPT("Tried to close serial port " + device_ + ", but close() failed."); + + fileDesc_ = -1; + } + + state_ = State::CLOSED; + } + + void SerialPort::SetTimeout(int32_t timeout_ms) { + if(timeout_ms < -1) + THROW_EXCEPT(std::string() + "timeout_ms provided to " + __PRETTY_FUNCTION__ + " was < -1, which is invalid."); + if(timeout_ms > 25500) + THROW_EXCEPT(std::string() + "timeout_ms provided to " + __PRETTY_FUNCTION__ + " was > 25500, which is invalid."); + if(state_ == State::OPEN) + THROW_EXCEPT(std::string() + __PRETTY_FUNCTION__ + " called while state == OPEN."); + timeout_ms_ = timeout_ms; + } + +} // namespace CppLinuxSerial +} // namespace mn diff --git a/test/unit/BasicTests.cpp b/test/unit/BasicTests.cpp new file mode 100644 index 0000000..b918c8e --- /dev/null +++ b/test/unit/BasicTests.cpp @@ -0,0 +1,77 @@ +/// +/// \file BasicTests.cpp +/// \author Geoffrey Hunter (www.mbedded.ninja) +/// \created 2017-11-24 +/// \last-modified 2017-11-24 +/// \brief Basic tests for the SerialPort class. +/// \details +/// See README.rst in repo root dir for more info. + +// System includes +#include "gtest/gtest.h" + +// 3rd party includes +#include "CppLinuxSerial/SerialPort.hpp" + +// User includes +#include "TestUtil.hpp" + +using namespace mn::CppLinuxSerial; + +namespace { + + class BasicTests : public ::testing::Test { + protected: + + BasicTests() { + } + + virtual ~BasicTests() { + } + + std::string device0Name_ = TestUtil::GetInstance().GetDevice0Name(); + std::string device1Name_ = TestUtil::GetInstance().GetDevice1Name(); + }; + + TEST_F(BasicTests, CanBeConstructed) { + SerialPort serialPort; + EXPECT_EQ(true, true); + } + + TEST_F(BasicTests, CanOpen) { + SerialPort serialPort0(device0Name_, BaudRate::B_57600); + serialPort0.Open(); + } + + TEST_F(BasicTests, ReadWrite) { + SerialPort serialPort0(device0Name_, BaudRate::B_57600); + serialPort0.Open(); + + SerialPort serialPort1(device1Name_, BaudRate::B_57600); + serialPort1.Open(); + + serialPort0.Write("Hello"); + + std::string readData; + serialPort1.Read(readData); + + ASSERT_EQ("Hello", readData); + } + + + TEST_F(BasicTests, ReadWriteDiffBaudRates) { + SerialPort serialPort0(device0Name_, BaudRate::B_9600); + serialPort0.Open(); + + SerialPort serialPort1(device1Name_, BaudRate::B_57600); + serialPort1.Open(); + + serialPort0.Write("Hello"); + + std::string readData; + serialPort1.Read(readData); + + ASSERT_EQ("Hello", readData); + } + +} // namespace \ No newline at end of file diff --git a/test/unit/CMakeLists.txt b/test/unit/CMakeLists.txt new file mode 100644 index 0000000..33c2543 --- /dev/null +++ b/test/unit/CMakeLists.txt @@ -0,0 +1,41 @@ +# +# \file CMakeLists.txt +# \author Geoffrey Hunter (www.mbedded.ninja) +# \edited n/a +# \created 2017-11-24 +# \last-modified 2017-11-24 +# \brief Contains instructions for building the unit tests. +# \details +# See README.md in root dir for more info. + +enable_testing() +find_package (Threads) +#find_package(GTest REQUIRED) +#message("gtest libraries found at ${GTEST_BOTH_LIBRARIES}") + +file(GLOB_RECURSE CppLinuxSerialUnitTests_SRC + "*.cpp" + "*.hpp") + + + +add_executable(CppLinuxSerialUnitTests ${CppLinuxSerialUnitTests_SRC}) + +if(COVERAGE) + message("Coverage enabled.") + target_compile_options(CppLinuxSerialUnitTests PRIVATE --coverage) + target_link_libraries(CppLinuxSerialUnitTests PRIVATE --coverage) +endif() + +target_link_libraries(CppLinuxSerialUnitTests LINK_PUBLIC CppLinuxSerial gtest_main ${CMAKE_THREAD_LIBS_INIT}) + +# The custom target and custom command below allow the unit tests +# to be run. +# If you want them to run automatically by CMake, uncomment #ALL +add_custom_target( + run_unit_tests #ALL + DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/CppLinuxSerialUnitTests.touch CppLinuxSerialUnitTests) + +add_custom_command( + OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/CppLinuxSerialUnitTests.touch + COMMAND ${CMAKE_CURRENT_BINARY_DIR}/CppLinuxSerialUnitTests) \ No newline at end of file diff --git a/test/unit/ConfigTests.cpp b/test/unit/ConfigTests.cpp new file mode 100644 index 0000000..38315f7 --- /dev/null +++ b/test/unit/ConfigTests.cpp @@ -0,0 +1,64 @@ +/// +/// \file ConfigTests.cpp +/// \author Geoffrey Hunter (www.mbedded.ninja) +/// \created 2017-11-24 +/// \last-modified 2017-11-24 +/// \brief Configuration tests for the SerialPort class. +/// \details +/// See README.rst in repo root dir for more info. + +// System includes +#include "gtest/gtest.h" + +// 3rd party includes +#include "CppLinuxSerial/SerialPort.hpp" + +// User includes +#include "TestUtil.hpp" + +using namespace mn::CppLinuxSerial; + +namespace { + + class ConfigTests : public ::testing::Test { + protected: + + ConfigTests() { + serialPort_ = SerialPort(TestUtil::GetInstance().GetDevice0Name(), BaudRate::B_57600); + serialPort_.Open(); + sttyOutput_ = TestUtil::GetInstance().Exec("stty -a -F " + TestUtil::GetInstance().GetDevice0Name()); + } + + virtual ~ConfigTests() { + } + + SerialPort serialPort_; + std::string sttyOutput_; + }; + + TEST_F(ConfigTests, BaudRateSetCorrectly) { + EXPECT_NE(std::string::npos, sttyOutput_.find("speed 57600 baud")); + serialPort_.SetBaudRate(BaudRate::B_115200); + sttyOutput_ = TestUtil::GetInstance().Exec("stty -a -F " + TestUtil::GetInstance().GetDevice0Name()); + EXPECT_NE(std::string::npos, sttyOutput_.find("speed 115200 baud")); + } + + //================================================================================================// + //======================================= LOCAL MODES (c_lflag) ==================================// + //================================================================================================// + + TEST_F(ConfigTests, CanonicalModeOff) { + EXPECT_NE(std::string::npos, sttyOutput_.find("-icanon")); + } + + TEST_F(ConfigTests, EchoModeOff) { + EXPECT_NE(std::string::npos, sttyOutput_.find("-echo")); + EXPECT_NE(std::string::npos, sttyOutput_.find("-echoe")); + EXPECT_NE(std::string::npos, sttyOutput_.find("-echonl")); + } + + TEST_F(ConfigTests, InterruptQuitSuspCharsOff) { + EXPECT_NE(std::string::npos, sttyOutput_.find("-isig")); + } + +} // namespace \ No newline at end of file diff --git a/test/unit/TestUtil.hpp b/test/unit/TestUtil.hpp new file mode 100644 index 0000000..1cf1a34 --- /dev/null +++ b/test/unit/TestUtil.hpp @@ -0,0 +1,94 @@ +/// +/// \file TestUtil.hpp +/// \author Geoffrey Hunter (www.mbedded.ninja) +/// \created 2017-11-24 +/// \last-modified 2017-11-24 +/// \brief Contains utility methods to help with testing. +/// \details +/// See README.rst in repo root dir for more info. + +#ifndef MN_CPP_LINUX_SERIAL_TEST_UTIL_H_ +#define MN_CPP_LINUX_SERIAL_TEST_UTIL_H_ + +// System includes +#include +#include +#include +#include +#include +#include + +// 3rd party includes + + +using namespace std::literals; + + +namespace mn { + namespace CppLinuxSerial { + + class TestUtil { + + public: + + static TestUtil& GetInstance() { + static TestUtil testUtil; + return testUtil; + } + + /// \brief Executes a command on the Linux command-line. + /// \details Blocks until command is complete. + /// \throws std::runtime_error is popen() fails. + std::string Exec(const std::string &cmd) { + std::array buffer; + std::string result; + std::shared_ptr pipe(popen(cmd.c_str(), "r"), pclose); + if (!pipe) throw std::runtime_error("popen() failed!"); + + while (!feof(pipe.get())) { + if (fgets(buffer.data(), 128, pipe.get()) != nullptr) + result += buffer.data(); + } + + return result; + } + + void CreateVirtualSerialPortPair() { + std::cout << "Creating virtual serial port pair..." << std::endl; + std::system("nohup sudo socat -d -d pty,raw,echo=0,link=/dev/ttyS10 pty,raw,echo=0,link=/dev/ttyS11 &"); + + // Hacky! Since socat is detached, we have no idea at what point it has created + // ttyS10 and ttyS11. Assume 1 second is long enough... + std::this_thread::sleep_for(1s); + std::system("sudo chmod a+rw /dev/ttyS10"); + std::system("sudo chmod a+rw /dev/ttyS11"); + } + + void CloseSerialPorts() { + // Dangerous! Kills all socat processes running + // on computer + std::system("sudo pkill socat"); + } + + std::string GetDevice0Name() { + return device0Name_; + } + + std::string GetDevice1Name() { + return device1Name_; + } + + std::string device0Name_ = "/dev/ttyS10"; + std::string device1Name_ = "/dev/ttyS11"; + + protected: + + TestUtil() { + + } + + }; + } // namespace CppLinuxSerial +} // namespace mn + +#endif // #ifndef MN_CPP_LINUX_SERIAL_TEST_UTIL_H_ diff --git a/test/unit/main.cpp b/test/unit/main.cpp new file mode 100644 index 0000000..10d32db --- /dev/null +++ b/test/unit/main.cpp @@ -0,0 +1,42 @@ +/// +/// \file main.cpp +/// \author Geoffrey Hunter (www.mbedded.ninja) +/// \edited n/a +/// \created 2017-11-24 +/// \last-modified 2017-11-24 +/// \brief Contains the main entry point for the unit tests. +/// \details +/// See README.md in root dir for more info. + +// System includes +#include "gtest/gtest.h" + +// User includes +#include "TestUtil.hpp" + +using namespace mn::CppLinuxSerial; + +class Environment : public testing::Environment { +public: + virtual ~Environment() {} + // Override this to define how to set up the environment. + virtual void SetUp() { + std::cout << __PRETTY_FUNCTION__ << " called." << std::endl; + TestUtil::GetInstance().CreateVirtualSerialPortPair(); + } + // Override this to define how to tear down the environment. + virtual void TearDown() { + TestUtil::GetInstance().CloseSerialPorts(); + } +}; + +int main(int argc, char **argv) +{ + ::testing::InitGoogleTest(&argc, argv); + + // Create and register global test setup + // (gtest takes ownership of pointer, do not delete manaully!) + ::testing::AddGlobalTestEnvironment(new Environment); + + return RUN_ALL_TESTS(); +} \ No newline at end of file diff --git a/tools/build.sh b/tools/build.sh new file mode 100755 index 0000000..33a94e2 --- /dev/null +++ b/tools/build.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash + +# +# \file build.sh +# \author Geoffrey Hunter (www.mbedded.ninja) +# \edited n/a +# \created 2017-09-27 +# \last-modified 2017-11-27 +# \brief Bash script for building/installing the source code. +# \details +# See README.md in root dir for more info. + +# Get script path +script_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +# 3rd party imports +. ${script_dir}/lib/shflags + +# User imports +. ${script_dir}/lib/utilities.sh + +printInfo "==========================================================================================" +printInfo "================================= CppLinuxSerial build.sh ================================" +printInfo "==========================================================================================" + +set +e + +# Define the command-line arguments +DEFINE_boolean 'install' 'false' 'Do you want to [i]nstall the CppLinuxSerial header files onto your local system after build?' 'i' +DEFINE_boolean 'coverage' 'false' 'Do you want to record test [c]overage metrics?' 'c' + +# parse the command-line +FLAGS "$@" || exit 1 +eval set -- "${FLAGS_ARGV}" + +# Any subsequent commands which fail will cause the shell script to exit immediately +# WARNING: Make sure to only activate this AFTER shflags has parsed command-line arguments +set -e + +printInfo "install = ${FLAGS_install}" +printInfo "coverage = ${FLAGS_coverage}" + +BUILD_DIRECTORY_NAME="build" + +# This will only make the build directory if it doesn't already +# exist. If it does exist, there is likely to be build artifacts +# in there already. +printInfo "Making and/or changing into build directory (${script_dir}/../${BUILD_DIRECTORY_NAME}/)..." +mkdir -p ${script_dir}/../${BUILD_DIRECTORY_NAME}/ +cd ${script_dir}/../${BUILD_DIRECTORY_NAME}/ + +if [[ "$FLAGS_coverage" == $FLAGS_TRUE ]]; then + printInfo 'Invoking cmake with -DCOVERAGE=1...' + cmake -DCOVERAGE=1 .. +else + printInfo 'Invoking cmake without -DCOVERAGE=1...' + cmake .. +fi + +printInfo 'Invoking make...' +make -j8 + +printInfo 'Running unit tests...' +make -j8 run_unit_tests + +if [[ "$FLAGS_install" == $FLAGS_TRUE ]]; then + printInfo "Installing CppLinuxSerial headers onto local system..." + sudo make install +fi diff --git a/tools/lib/shflags b/tools/lib/shflags new file mode 100755 index 0000000..71a2f22 --- /dev/null +++ b/tools/lib/shflags @@ -0,0 +1,1155 @@ +# vim:et:ft=sh:sts=2:sw=2 +# +# Copyright 2008-2016 Kate Ward. All Rights Reserved. +# Released under the Apache License 2.0. +# +# shFlags -- Advanced command-line flag library for Unix shell scripts. +# http://code.google.com/p/shflags/ +# +# Author: kate.ward@forestent.com (Kate Ward) +# +# This module implements something like the google-gflags library available +# from http://code.google.com/p/google-gflags/. +# +# FLAG TYPES: This is a list of the DEFINE_*'s that you can do. All flags take +# a name, default value, help-string, and optional 'short' name (one-letter +# name). Some flags have other arguments, which are described with the flag. +# +# DEFINE_string: takes any input, and intreprets it as a string. +# +# DEFINE_boolean: does not take any arguments. Say --myflag to set +# FLAGS_myflag to true, or --nomyflag to set FLAGS_myflag to false. For short +# flags, passing the flag on the command-line negates the default value, i.e. +# if the default is true, passing the flag sets the value to false. +# +# DEFINE_float: takes an input and intreprets it as a floating point number. As +# shell does not support floats per-se, the input is merely validated as +# being a valid floating point value. +# +# DEFINE_integer: takes an input and intreprets it as an integer. +# +# SPECIAL FLAGS: There are a few flags that have special meaning: +# --help (or -?) prints a list of all the flags in a human-readable fashion +# --flagfile=foo read flags from foo. (not implemented yet) +# -- as in getopt(), terminates flag-processing +# +# EXAMPLE USAGE: +# +# -- begin hello.sh -- +# #! /bin/sh +# . ./shflags +# DEFINE_string name 'world' "somebody's name" n +# FLAGS "$@" || exit $? +# eval set -- "${FLAGS_ARGV}" +# echo "Hello, ${FLAGS_name}." +# -- end hello.sh -- +# +# $ ./hello.sh -n Kate +# Hello, Kate. +# +# CUSTOMIZABLE BEHAVIOR: +# +# A script can override the default 'getopt' command by providing the path to +# an alternate implementation by defining the FLAGS_GETOPT_CMD variable. +# +# NOTES: +# +# * Not all systems include a getopt version that supports long flags. On these +# systems, only short flags are recognized. + +#============================================================================== +# shFlags +# +# Shared attributes: +# flags_error: last error message +# flags_output: last function output (rarely valid) +# flags_return: last return value +# +# __flags_longNames: list of long names for all flags +# __flags_shortNames: list of short names for all flags +# __flags_boolNames: list of boolean flag names +# +# __flags_opts: options parsed by getopt +# +# Per-flag attributes: +# FLAGS_: contains value of flag named 'flag_name' +# __flags__default: the default flag value +# __flags__help: the flag help string +# __flags__short: the flag short name +# __flags__type: the flag type +# +# Notes: +# - lists of strings are space separated, and a null value is the '~' char. + +# return if FLAGS already loaded +[ -n "${FLAGS_VERSION:-}" ] && return 0 +FLAGS_VERSION='1.2.0' + +# return values that scripts can use +FLAGS_TRUE=0 +FLAGS_FALSE=1 +FLAGS_ERROR=2 + +# determine some reasonable command defaults +__FLAGS_UNAME_S=`uname -s` +case "${__FLAGS_UNAME_S}" in + BSD) __FLAGS_EXPR_CMD='gexpr' ;; + *) __FLAGS_EXPR_CMD='expr' ;; +esac + +# commands a user can override if needed +FLAGS_EXPR_CMD=${FLAGS_EXPR_CMD:-${__FLAGS_EXPR_CMD}} +FLAGS_GETOPT_CMD=${FLAGS_GETOPT_CMD:-getopt} + +# specific shell checks +if [ -n "${ZSH_VERSION:-}" ]; then + setopt |grep "^shwordsplit$" >/dev/null + if [ $? -ne ${FLAGS_TRUE} ]; then + _flags_fatal 'zsh shwordsplit option is required for proper zsh operation' + fi + if [ -z "${FLAGS_PARENT:-}" ]; then + _flags_fatal "zsh does not pass \$0 through properly. please declare' \ +\"FLAGS_PARENT=\$0\" before calling shFlags" + fi +fi + +# can we use built-ins? +( echo "${FLAGS_TRUE#0}"; ) >/dev/null 2>&1 +if [ $? -eq ${FLAGS_TRUE} ]; then + __FLAGS_USE_BUILTIN=${FLAGS_TRUE} +else + __FLAGS_USE_BUILTIN=${FLAGS_FALSE} +fi + +# +# constants +# + +# reserved flag names +__FLAGS_RESERVED_LIST=' ARGC ARGV ERROR FALSE GETOPT_CMD HELP PARENT TRUE ' +__FLAGS_RESERVED_LIST="${__FLAGS_RESERVED_LIST} VERSION " + +# getopt version +__FLAGS_GETOPT_VERS_STD=0 +__FLAGS_GETOPT_VERS_ENH=1 +__FLAGS_GETOPT_VERS_BSD=2 + +${FLAGS_GETOPT_CMD} >/dev/null 2>&1 +case $? in + 0) __FLAGS_GETOPT_VERS=${__FLAGS_GETOPT_VERS_STD} ;; # bsd getopt + 2) + # TODO(kward): look into '-T' option to test the internal getopt() version + if [ "`${FLAGS_GETOPT_CMD} --version`" = '-- ' ]; then + __FLAGS_GETOPT_VERS=${__FLAGS_GETOPT_VERS_STD} + else + __FLAGS_GETOPT_VERS=${__FLAGS_GETOPT_VERS_ENH} + fi + ;; + *) _flags_fatal 'unable to determine getopt version' ;; +esac + +# getopt optstring lengths +__FLAGS_OPTSTR_SHORT=0 +__FLAGS_OPTSTR_LONG=1 + +__FLAGS_NULL='~' + +# flag info strings +__FLAGS_INFO_DEFAULT='default' +__FLAGS_INFO_HELP='help' +__FLAGS_INFO_SHORT='short' +__FLAGS_INFO_TYPE='type' + +# flag lengths +__FLAGS_LEN_SHORT=0 +__FLAGS_LEN_LONG=1 + +# flag types +__FLAGS_TYPE_NONE=0 +__FLAGS_TYPE_BOOLEAN=1 +__FLAGS_TYPE_FLOAT=2 +__FLAGS_TYPE_INTEGER=3 +__FLAGS_TYPE_STRING=4 + +# set the constants readonly +__flags_constants=`set |awk -F= '/^FLAGS_/ || /^__FLAGS_/ {print $1}'` +for __flags_const in ${__flags_constants}; do + # skip certain flags + case ${__flags_const} in + FLAGS_HELP) continue ;; + FLAGS_PARENT) continue ;; + esac + # set flag readonly + if [ -z "${ZSH_VERSION:-}" ]; then + readonly ${__flags_const} + else # handle zsh + case ${ZSH_VERSION} in + [123].*) readonly ${__flags_const} ;; + *) readonly -g ${__flags_const} ;; # declare readonly constants globally + esac + fi +done +unset __flags_const __flags_constants + +# +# internal variables +# + +# space separated lists +__flags_boolNames=' ' # boolean flag names +__flags_longNames=' ' # long flag names +__flags_shortNames=' ' # short flag names +__flags_definedNames=' ' # defined flag names (used for validation) + +__flags_columns='' # screen width in columns +__flags_opts='' # temporary storage for parsed getopt flags + +#------------------------------------------------------------------------------ +# private functions +# + +# logging functions +_flags_debug() { echo "flags:DEBUG $@" >&2; } +_flags_warn() { echo "flags:WARN $@" >&2; } +_flags_error() { echo "flags:ERROR $@" >&2; } +_flags_fatal() { echo "flags:FATAL $@" >&2; exit ${FLAGS_ERROR}; } + +# Define a flag. +# +# Calling this function will define the following info variables for the +# specified flag: +# FLAGS_flagname - the name for this flag (based upon the long flag name) +# __flags__default - the default value +# __flags_flagname_help - the help string +# __flags_flagname_short - the single letter alias +# __flags_flagname_type - the type of flag (one of __FLAGS_TYPE_*) +# +# Args: +# _flags__type: integer: internal type of flag (__FLAGS_TYPE_*) +# _flags__name: string: long flag name +# _flags__default: default flag value +# _flags__help: string: help string +# _flags__short: string: (optional) short flag name +# Returns: +# integer: success of operation, or error +_flags_define() +{ + if [ $# -lt 4 ]; then + flags_error='DEFINE error: too few arguments' + flags_return=${FLAGS_ERROR} + _flags_error "${flags_error}" + return ${flags_return} + fi + + _flags_type_=$1 + _flags_name_=$2 + _flags_default_=$3 + _flags_help_=$4 + _flags_short_=${5:-${__FLAGS_NULL}} + + _flags_return_=${FLAGS_TRUE} + _flags_usName_=`_flags_underscoreName ${_flags_name_}` + + # check whether the flag name is reserved + _flags_itemInList ${_flags_usName_} "${__FLAGS_RESERVED_LIST}" + if [ $? -eq ${FLAGS_TRUE} ]; then + flags_error="flag name (${_flags_name_}) is reserved" + _flags_return_=${FLAGS_ERROR} + fi + + # require short option for getopt that don't support long options + if [ ${_flags_return_} -eq ${FLAGS_TRUE} \ + -a ${__FLAGS_GETOPT_VERS} -ne ${__FLAGS_GETOPT_VERS_ENH} \ + -a "${_flags_short_}" = "${__FLAGS_NULL}" ] + then + flags_error="short flag required for (${_flags_name_}) on this platform" + _flags_return_=${FLAGS_ERROR} + fi + + # check for existing long name definition + if [ ${_flags_return_} -eq ${FLAGS_TRUE} ]; then + if _flags_itemInList ${_flags_usName_} ${__flags_definedNames}; then + flags_error="definition for ([no]${_flags_name_}) already exists" + _flags_warn "${flags_error}" + _flags_return_=${FLAGS_FALSE} + fi + fi + + # check for existing short name definition + if [ ${_flags_return_} -eq ${FLAGS_TRUE} \ + -a "${_flags_short_}" != "${__FLAGS_NULL}" ] + then + if _flags_itemInList "${_flags_short_}" ${__flags_shortNames}; then + flags_error="flag short name (${_flags_short_}) already defined" + _flags_warn "${flags_error}" + _flags_return_=${FLAGS_FALSE} + fi + fi + + # handle default value. note, on several occasions the 'if' portion of an + # if/then/else contains just a ':' which does nothing. a binary reversal via + # '!' is not done because it does not work on all shells. + if [ ${_flags_return_} -eq ${FLAGS_TRUE} ]; then + case ${_flags_type_} in + ${__FLAGS_TYPE_BOOLEAN}) + if _flags_validBool "${_flags_default_}"; then + case ${_flags_default_} in + true|t|0) _flags_default_=${FLAGS_TRUE} ;; + false|f|1) _flags_default_=${FLAGS_FALSE} ;; + esac + else + flags_error="invalid default flag value '${_flags_default_}'" + _flags_return_=${FLAGS_ERROR} + fi + ;; + + ${__FLAGS_TYPE_FLOAT}) + if _flags_validFloat "${_flags_default_}"; then + : + else + flags_error="invalid default flag value '${_flags_default_}'" + _flags_return_=${FLAGS_ERROR} + fi + ;; + + ${__FLAGS_TYPE_INTEGER}) + if _flags_validInt "${_flags_default_}"; then + : + else + flags_error="invalid default flag value '${_flags_default_}'" + _flags_return_=${FLAGS_ERROR} + fi + ;; + + ${__FLAGS_TYPE_STRING}) ;; # everything in shell is a valid string + + *) + flags_error="unrecognized flag type '${_flags_type_}'" + _flags_return_=${FLAGS_ERROR} + ;; + esac + fi + + if [ ${_flags_return_} -eq ${FLAGS_TRUE} ]; then + # store flag information + eval "FLAGS_${_flags_usName_}='${_flags_default_}'" + eval "__flags_${_flags_usName_}_${__FLAGS_INFO_TYPE}=${_flags_type_}" + eval "__flags_${_flags_usName_}_${__FLAGS_INFO_DEFAULT}=\ +\"${_flags_default_}\"" + eval "__flags_${_flags_usName_}_${__FLAGS_INFO_HELP}=\"${_flags_help_}\"" + eval "__flags_${_flags_usName_}_${__FLAGS_INFO_SHORT}='${_flags_short_}'" + + # append flag names to name lists + __flags_shortNames="${__flags_shortNames}${_flags_short_} " + __flags_longNames="${__flags_longNames}${_flags_name_} " + [ ${_flags_type_} -eq ${__FLAGS_TYPE_BOOLEAN} ] && \ + __flags_boolNames="${__flags_boolNames}no${_flags_name_} " + + # append flag names to defined names for later validation checks + __flags_definedNames="${__flags_definedNames}${_flags_usName_} " + [ ${_flags_type_} -eq ${__FLAGS_TYPE_BOOLEAN} ] && \ + __flags_definedNames="${__flags_definedNames}no${_flags_usName_} " + fi + + flags_return=${_flags_return_} + unset _flags_default_ _flags_help_ _flags_name_ _flags_return_ \ + _flags_short_ _flags_type_ _flags_usName_ + [ ${flags_return} -eq ${FLAGS_ERROR} ] && _flags_error "${flags_error}" + return ${flags_return} +} + +# Underscore a flag name by replacing dashes with underscores. +# +# Args: +# unnamed: string: log flag name +# Output: +# string: underscored name +_flags_underscoreName() +{ + echo $1 |tr '-' '_' +} + +# Return valid getopt options using currently defined list of long options. +# +# This function builds a proper getopt option string for short (and long) +# options, using the current list of long options for reference. +# +# Args: +# _flags_optStr: integer: option string type (__FLAGS_OPTSTR_*) +# Output: +# string: generated option string for getopt +# Returns: +# boolean: success of operation (always returns True) +_flags_genOptStr() +{ + _flags_optStrType_=$1 + + _flags_opts_='' + + for _flags_name_ in ${__flags_longNames}; do + _flags_usName_=`_flags_underscoreName ${_flags_name_}` + _flags_type_=`_flags_getFlagInfo ${_flags_usName_} ${__FLAGS_INFO_TYPE}` + [ $? -eq ${FLAGS_TRUE} ] || _flags_fatal 'call to _flags_type_ failed' + case ${_flags_optStrType_} in + ${__FLAGS_OPTSTR_SHORT}) + _flags_shortName_=`_flags_getFlagInfo \ + ${_flags_usName_} ${__FLAGS_INFO_SHORT}` + if [ "${_flags_shortName_}" != "${__FLAGS_NULL}" ]; then + _flags_opts_="${_flags_opts_}${_flags_shortName_}" + # getopt needs a trailing ':' to indicate a required argument + [ ${_flags_type_} -ne ${__FLAGS_TYPE_BOOLEAN} ] && \ + _flags_opts_="${_flags_opts_}:" + fi + ;; + + ${__FLAGS_OPTSTR_LONG}) + _flags_opts_="${_flags_opts_:+${_flags_opts_},}${_flags_name_}" + # getopt needs a trailing ':' to indicate a required argument + [ ${_flags_type_} -ne ${__FLAGS_TYPE_BOOLEAN} ] && \ + _flags_opts_="${_flags_opts_}:" + ;; + esac + done + + echo "${_flags_opts_}" + unset _flags_name_ _flags_opts_ _flags_optStrType_ _flags_shortName_ \ + _flags_type_ _flags_usName_ + return ${FLAGS_TRUE} +} + +# Returns flag details based on a flag name and flag info. +# +# Args: +# string: underscored flag name +# string: flag info (see the _flags_define function for valid info types) +# Output: +# string: value of dereferenced flag variable +# Returns: +# integer: one of FLAGS_{TRUE|FALSE|ERROR} +_flags_getFlagInfo() +{ + # note: adding gFI to variable names to prevent naming conflicts with calling + # functions + _flags_gFI_usName_=$1 + _flags_gFI_info_=$2 + + _flags_infoVar_="__flags_${_flags_gFI_usName_}_${_flags_gFI_info_}" + _flags_strToEval_="_flags_infoValue_=\"\${${_flags_infoVar_}:-}\"" + eval "${_flags_strToEval_}" + if [ -n "${_flags_infoValue_}" ]; then + flags_return=${FLAGS_TRUE} + else + # see if the _flags_gFI_usName_ variable is a string as strings can be + # empty... + # note: the DRY principle would say to have this function call itself for + # the next three lines, but doing so results in an infinite loop as an + # invalid _flags_name_ will also not have the associated _type variable. + # Because it doesn't (it will evaluate to an empty string) the logic will + # try to find the _type variable of the _type variable, and so on. Not so + # good ;-) + _flags_typeVar_="__flags_${_flags_gFI_usName_}_${__FLAGS_INFO_TYPE}" + _flags_strToEval_="_flags_typeValue_=\"\${${_flags_typeVar_}:-}\"" + eval "${_flags_strToEval_}" + if [ "${_flags_typeValue_}" = "${__FLAGS_TYPE_STRING}" ]; then + flags_return=${FLAGS_TRUE} + else + flags_return=${FLAGS_ERROR} + flags_error="missing flag info variable (${_flags_infoVar_})" + fi + fi + + echo "${_flags_infoValue_}" + unset _flags_gFI_usName_ _flags_gfI_info_ _flags_infoValue_ _flags_infoVar_ \ + _flags_strToEval_ _flags_typeValue_ _flags_typeVar_ + [ ${flags_return} -eq ${FLAGS_ERROR} ] && _flags_error "${flags_error}" + return ${flags_return} +} + +# Check for presense of item in a list. +# +# Passed a string (e.g. 'abc'), this function will determine if the string is +# present in the list of strings (e.g. ' foo bar abc '). +# +# Args: +# _flags_str_: string: string to search for in a list of strings +# unnamed: list: list of strings +# Returns: +# boolean: true if item is in the list +_flags_itemInList() { + _flags_str_=$1 + shift + + echo " ${*:-} " |grep " ${_flags_str_} " >/dev/null + if [ $? -eq 0 ]; then + flags_return=${FLAGS_TRUE} + else + flags_return=${FLAGS_FALSE} + fi + + unset _flags_str_ + return ${flags_return} +} + +# Returns the width of the current screen. +# +# Output: +# integer: width in columns of the current screen. +_flags_columns() +{ + if [ -z "${__flags_columns}" ]; then + # determine the value and store it + if eval stty size >/dev/null 2>&1; then + # stty size worked :-) + set -- `stty size` + __flags_columns=$2 + elif eval tput cols >/dev/null 2>&1; then + set -- `tput cols` + __flags_columns=$1 + else + __flags_columns=80 # default terminal width + fi + fi + echo ${__flags_columns} +} + +# Validate a boolean. +# +# Args: +# _flags__bool: boolean: value to validate +# Returns: +# bool: true if the value is a valid boolean +_flags_validBool() +{ + _flags_bool_=$1 + + flags_return=${FLAGS_TRUE} + case "${_flags_bool_}" in + true|t|0) ;; + false|f|1) ;; + *) flags_return=${FLAGS_FALSE} ;; + esac + + unset _flags_bool_ + return ${flags_return} +} + +# Validate a float. +# +# Args: +# _flags_float_: float: value to validate +# Returns: +# bool: true if the value is a valid integer +_flags_validFloat() +{ + flags_return=${FLAGS_FALSE} + [ -n "$1" ] || return ${flags_return} + _flags_float_=$1 + + if _flags_validInt ${_flags_float_}; then + flags_return=${FLAGS_TRUE} + elif _flags_useBuiltin; then + _flags_float_whole_=${_flags_float_%.*} + _flags_float_fraction_=${_flags_float_#*.} + if _flags_validInt ${_flags_float_whole_:-0} -a \ + _flags_validInt ${_flags_float_fraction_}; then + flags_return=${FLAGS_TRUE} + fi + unset _flags_float_whole_ _flags_float_fraction_ + else + flags_return=${FLAGS_TRUE} + case ${_flags_float_} in + -*) # negative floats + _flags_test_=`${FLAGS_EXPR_CMD} -- "${_flags_float_}" :\ + '\(-[0-9]*\.[0-9]*\)'` + ;; + *) # positive floats + _flags_test_=`${FLAGS_EXPR_CMD} -- "${_flags_float_}" :\ + '\([0-9]*\.[0-9]*\)'` + ;; + esac + [ "${_flags_test_}" != "${_flags_float_}" ] && flags_return=${FLAGS_FALSE} + unset _flags_test_ + fi + + unset _flags_float_ _flags_float_whole_ _flags_float_fraction_ + return ${flags_return} +} + +# Validate an integer. +# +# Args: +# _flags_int_: integer: value to validate +# Returns: +# bool: true if the value is a valid integer +_flags_validInt() +{ + flags_return=${FLAGS_FALSE} + [ -n "$1" ] || return ${flags_return} + _flags_int_=$1 + + case ${_flags_int_} in + -*.*) ;; # ignore negative floats (we'll invalidate them later) + -*) # strip possible leading negative sign + if _flags_useBuiltin; then + _flags_int_=${_flags_int_#-} + else + _flags_int_=`${FLAGS_EXPR_CMD} -- "${_flags_int_}" : '-\([0-9][0-9]*\)'` + fi + ;; + esac + + case ${_flags_int_} in + *[!0-9]*) flags_return=${FLAGS_FALSE} ;; + *) flags_return=${FLAGS_TRUE} ;; + esac + + unset _flags_int_ + return ${flags_return} +} + +# Parse command-line options using the standard getopt. +# +# Note: the flag options are passed around in the global __flags_opts so that +# the formatting is not lost due to shell parsing and such. +# +# Args: +# @: varies: command-line options to parse +# Returns: +# integer: a FLAGS success condition +_flags_getoptStandard() +{ + flags_return=${FLAGS_TRUE} + _flags_shortOpts_=`_flags_genOptStr ${__FLAGS_OPTSTR_SHORT}` + + # check for spaces in passed options + for _flags_opt_ in "$@"; do + # note: the silliness with the x's is purely for ksh93 on Ubuntu 6.06 + _flags_match_=`echo "x${_flags_opt_}x" |sed 's/ //g'` + if [ "${_flags_match_}" != "x${_flags_opt_}x" ]; then + flags_error='the available getopt does not support spaces in options' + flags_return=${FLAGS_ERROR} + break + fi + done + + if [ ${flags_return} -eq ${FLAGS_TRUE} ]; then + __flags_opts=`getopt ${_flags_shortOpts_} $@ 2>&1` + _flags_rtrn_=$? + if [ ${_flags_rtrn_} -ne ${FLAGS_TRUE} ]; then + _flags_warn "${__flags_opts}" + flags_error='unable to parse provided options with getopt.' + flags_return=${FLAGS_ERROR} + fi + fi + + unset _flags_match_ _flags_opt_ _flags_rtrn_ _flags_shortOpts_ + return ${flags_return} +} + +# Parse command-line options using the enhanced getopt. +# +# Note: the flag options are passed around in the global __flags_opts so that +# the formatting is not lost due to shell parsing and such. +# +# Args: +# @: varies: command-line options to parse +# Returns: +# integer: a FLAGS success condition +_flags_getoptEnhanced() +{ + flags_return=${FLAGS_TRUE} + _flags_shortOpts_=`_flags_genOptStr ${__FLAGS_OPTSTR_SHORT}` + _flags_boolOpts_=`echo "${__flags_boolNames}" \ + |sed 's/^ *//;s/ *$//;s/ /,/g'` + _flags_longOpts_=`_flags_genOptStr ${__FLAGS_OPTSTR_LONG}` + + __flags_opts=`${FLAGS_GETOPT_CMD} \ + -o ${_flags_shortOpts_} \ + -l "${_flags_longOpts_},${_flags_boolOpts_}" \ + -- "$@" 2>&1` + _flags_rtrn_=$? + if [ ${_flags_rtrn_} -ne ${FLAGS_TRUE} ]; then + _flags_warn "${__flags_opts}" + flags_error='unable to parse provided options with getopt.' + flags_return=${FLAGS_ERROR} + fi + + unset _flags_boolOpts_ _flags_longOpts_ _flags_rtrn_ _flags_shortOpts_ + return ${flags_return} +} + +# Dynamically parse a getopt result and set appropriate variables. +# +# This function does the actual conversion of getopt output and runs it through +# the standard case structure for parsing. The case structure is actually quite +# dynamic to support any number of flags. +# +# Args: +# argc: int: original command-line argument count +# @: varies: output from getopt parsing +# Returns: +# integer: a FLAGS success condition +_flags_parseGetopt() +{ + _flags_argc_=$1 + shift + + flags_return=${FLAGS_TRUE} + + if [ ${__FLAGS_GETOPT_VERS} -ne ${__FLAGS_GETOPT_VERS_ENH} ]; then + set -- $@ + else + # note the quotes around the `$@' -- they are essential! + eval set -- "$@" + fi + + # Provide user with the number of arguments to shift by later. + # NOTE: the FLAGS_ARGC variable is obsolete as of 1.0.3 because it does not + # properly give user access to non-flag arguments mixed in between flag + # arguments. Its usage was replaced by FLAGS_ARGV, and it is being kept only + # for backwards compatibility reasons. + FLAGS_ARGC=`_flags_math "$# - 1 - ${_flags_argc_}"` + + # handle options. note options with values must do an additional shift + while true; do + _flags_opt_=$1 + _flags_arg_=${2:-} + _flags_type_=${__FLAGS_TYPE_NONE} + _flags_name_='' + + # determine long flag name + case "${_flags_opt_}" in + --) shift; break ;; # discontinue option parsing + + --*) # long option + if _flags_useBuiltin; then + _flags_opt_=${_flags_opt_#*--} + else + _flags_opt_=`${FLAGS_EXPR_CMD} -- "${_flags_opt_}" : '--\(.*\)'` + fi + _flags_len_=${__FLAGS_LEN_LONG} + if _flags_itemInList "${_flags_opt_}" ${__flags_longNames}; then + _flags_name_=${_flags_opt_} + else + # check for negated long boolean version + if _flags_itemInList "${_flags_opt_}" ${__flags_boolNames}; then + if _flags_useBuiltin; then + _flags_name_=${_flags_opt_#*no} + else + _flags_name_=`${FLAGS_EXPR_CMD} -- "${_flags_opt_}" : 'no\(.*\)'` + fi + _flags_type_=${__FLAGS_TYPE_BOOLEAN} + _flags_arg_=${__FLAGS_NULL} + fi + fi + ;; + + -*) # short option + if _flags_useBuiltin; then + _flags_opt_=${_flags_opt_#*-} + else + _flags_opt_=`${FLAGS_EXPR_CMD} -- "${_flags_opt_}" : '-\(.*\)'` + fi + _flags_len_=${__FLAGS_LEN_SHORT} + if _flags_itemInList "${_flags_opt_}" ${__flags_shortNames}; then + # yes. match short name to long name. note purposeful off-by-one + # (too high) with awk calculations. + _flags_pos_=`echo "${__flags_shortNames}" \ + |awk 'BEGIN{RS=" ";rn=0}$0==e{rn=NR}END{print rn}' \ + e=${_flags_opt_}` + _flags_name_=`echo "${__flags_longNames}" \ + |awk 'BEGIN{RS=" "}rn==NR{print $0}' rn="${_flags_pos_}"` + fi + ;; + esac + + # die if the flag was unrecognized + if [ -z "${_flags_name_}" ]; then + flags_error="unrecognized option (${_flags_opt_})" + flags_return=${FLAGS_ERROR} + break + fi + + # set new flag value + _flags_usName_=`_flags_underscoreName ${_flags_name_}` + [ ${_flags_type_} -eq ${__FLAGS_TYPE_NONE} ] && \ + _flags_type_=`_flags_getFlagInfo \ + "${_flags_usName_}" ${__FLAGS_INFO_TYPE}` + case ${_flags_type_} in + ${__FLAGS_TYPE_BOOLEAN}) + if [ ${_flags_len_} -eq ${__FLAGS_LEN_LONG} ]; then + if [ "${_flags_arg_}" != "${__FLAGS_NULL}" ]; then + eval "FLAGS_${_flags_usName_}=${FLAGS_TRUE}" + else + eval "FLAGS_${_flags_usName_}=${FLAGS_FALSE}" + fi + else + _flags_strToEval_="_flags_val_=\ +\${__flags_${_flags_usName_}_${__FLAGS_INFO_DEFAULT}}" + eval "${_flags_strToEval_}" + if [ ${_flags_val_} -eq ${FLAGS_FALSE} ]; then + eval "FLAGS_${_flags_usName_}=${FLAGS_TRUE}" + else + eval "FLAGS_${_flags_usName_}=${FLAGS_FALSE}" + fi + fi + ;; + + ${__FLAGS_TYPE_FLOAT}) + if _flags_validFloat "${_flags_arg_}"; then + eval "FLAGS_${_flags_usName_}='${_flags_arg_}'" + else + flags_error="invalid float value (${_flags_arg_})" + flags_return=${FLAGS_ERROR} + break + fi + ;; + + ${__FLAGS_TYPE_INTEGER}) + if _flags_validInt "${_flags_arg_}"; then + eval "FLAGS_${_flags_usName_}='${_flags_arg_}'" + else + flags_error="invalid integer value (${_flags_arg_})" + flags_return=${FLAGS_ERROR} + break + fi + ;; + + ${__FLAGS_TYPE_STRING}) + eval "FLAGS_${_flags_usName_}='${_flags_arg_}'" + ;; + esac + + # handle special case help flag + if [ "${_flags_usName_}" = 'help' ]; then + if [ ${FLAGS_help} -eq ${FLAGS_TRUE} ]; then + flags_help + flags_error='help requested' + flags_return=${FLAGS_FALSE} + break + fi + fi + + # shift the option and non-boolean arguements out. + shift + [ ${_flags_type_} != ${__FLAGS_TYPE_BOOLEAN} ] && shift + done + + # give user back non-flag arguments + FLAGS_ARGV='' + while [ $# -gt 0 ]; do + FLAGS_ARGV="${FLAGS_ARGV:+${FLAGS_ARGV} }'$1'" + shift + done + + unset _flags_arg_ _flags_len_ _flags_name_ _flags_opt_ _flags_pos_ \ + _flags_strToEval_ _flags_type_ _flags_usName_ _flags_val_ + return ${flags_return} +} + +# Perform some path using built-ins. +# +# Args: +# $@: string: math expression to evaluate +# Output: +# integer: the result +# Returns: +# bool: success of math evaluation +_flags_math() +{ + if [ $# -eq 0 ]; then + flags_return=${FLAGS_FALSE} + elif _flags_useBuiltin; then + # Variable assignment is needed as workaround for Solaris Bourne shell, + # which cannot parse a bare $((expression)). + _flags_expr_='$(($@))' + eval echo ${_flags_expr_} + flags_return=$? + unset _flags_expr_ + else + eval expr $@ + flags_return=$? + fi + + return ${flags_return} +} + +# Cross-platform strlen() implementation. +# +# Args: +# _flags_str: string: to determine length of +# Output: +# integer: length of string +# Returns: +# bool: success of strlen evaluation +_flags_strlen() +{ + _flags_str_=${1:-} + + if [ -z "${_flags_str_}" ]; then + flags_output=0 + elif _flags_useBuiltin; then + flags_output=${#_flags_str_} + else + flags_output=`${FLAGS_EXPR_CMD} -- "${_flags_str_}" : '.*'` + fi + flags_return=$? + + unset _flags_str_ + echo ${flags_output} + return ${flags_return} +} + +# Use built-in helper function to enable unit testing. +# +# Args: +# None +# Returns: +# bool: true if built-ins should be used +_flags_useBuiltin() +{ + return ${__FLAGS_USE_BUILTIN} +} + +#------------------------------------------------------------------------------ +# public functions +# +# A basic boolean flag. Boolean flags do not take any arguments, and their +# value is either 1 (false) or 0 (true). For long flags, the false value is +# specified on the command line by prepending the word 'no'. With short flags, +# the presense of the flag toggles the current value between true and false. +# Specifying a short boolean flag twice on the command results in returning the +# value back to the default value. +# +# A default value is required for boolean flags. +# +# For example, lets say a Boolean flag was created whose long name was 'update' +# and whose short name was 'x', and the default value was 'false'. This flag +# could be explicitly set to 'true' with '--update' or by '-x', and it could be +# explicitly set to 'false' with '--noupdate'. +DEFINE_boolean() { _flags_define ${__FLAGS_TYPE_BOOLEAN} "$@"; } + +# Other basic flags. +DEFINE_float() { _flags_define ${__FLAGS_TYPE_FLOAT} "$@"; } +DEFINE_integer() { _flags_define ${__FLAGS_TYPE_INTEGER} "$@"; } +DEFINE_string() { _flags_define ${__FLAGS_TYPE_STRING} "$@"; } + +# Parse the flags. +# +# Args: +# unnamed: list: command-line flags to parse +# Returns: +# integer: success of operation, or error +FLAGS() +{ + # define a standard 'help' flag if one isn't already defined + [ -z "${__flags_help_type:-}" ] && \ + DEFINE_boolean 'help' false 'show this help' 'h' + + # parse options + if [ $# -gt 0 ]; then + if [ ${__FLAGS_GETOPT_VERS} -ne ${__FLAGS_GETOPT_VERS_ENH} ]; then + _flags_getoptStandard "$@" + else + _flags_getoptEnhanced "$@" + fi + flags_return=$? + else + # nothing passed; won't bother running getopt + __flags_opts='--' + flags_return=${FLAGS_TRUE} + fi + + if [ ${flags_return} -eq ${FLAGS_TRUE} ]; then + _flags_parseGetopt $# "${__flags_opts}" + flags_return=$? + fi + + [ ${flags_return} -eq ${FLAGS_ERROR} ] && _flags_fatal "${flags_error}" + return ${flags_return} +} + +# This is a helper function for determining the 'getopt' version for platforms +# where the detection isn't working. It simply outputs debug information that +# can be included in a bug report. +# +# Args: +# none +# Output: +# debug info that can be included in a bug report +# Returns: +# nothing +flags_getoptInfo() +{ + # platform info + _flags_debug "uname -a: `uname -a`" + _flags_debug "PATH: ${PATH}" + + # shell info + if [ -n "${BASH_VERSION:-}" ]; then + _flags_debug 'shell: bash' + _flags_debug "BASH_VERSION: ${BASH_VERSION}" + elif [ -n "${ZSH_VERSION:-}" ]; then + _flags_debug 'shell: zsh' + _flags_debug "ZSH_VERSION: ${ZSH_VERSION}" + fi + + # getopt info + ${FLAGS_GETOPT_CMD} >/dev/null + _flags_getoptReturn=$? + _flags_debug "getopt return: ${_flags_getoptReturn}" + _flags_debug "getopt --version: `${FLAGS_GETOPT_CMD} --version 2>&1`" + + unset _flags_getoptReturn +} + +# Returns whether the detected getopt version is the enhanced version. +# +# Args: +# none +# Output: +# none +# Returns: +# bool: true if getopt is the enhanced version +flags_getoptIsEnh() +{ + test ${__FLAGS_GETOPT_VERS} -eq ${__FLAGS_GETOPT_VERS_ENH} +} + +# Returns whether the detected getopt version is the standard version. +# +# Args: +# none +# Returns: +# bool: true if getopt is the standard version +flags_getoptIsStd() +{ + test ${__FLAGS_GETOPT_VERS} -eq ${__FLAGS_GETOPT_VERS_STD} +} + +# This is effectively a 'usage()' function. It prints usage information and +# exits the program with ${FLAGS_FALSE} if it is ever found in the command line +# arguments. Note this function can be overridden so other apps can define +# their own --help flag, replacing this one, if they want. +# +# Args: +# none +# Returns: +# integer: success of operation (always returns true) +flags_help() +{ + if [ -n "${FLAGS_HELP:-}" ]; then + echo "${FLAGS_HELP}" >&2 + else + echo "USAGE: ${FLAGS_PARENT:-$0} [flags] args" >&2 + fi + if [ -n "${__flags_longNames}" ]; then + echo 'flags:' >&2 + for flags_name_ in ${__flags_longNames}; do + flags_flagStr_='' + flags_boolStr_='' + flags_usName_=`_flags_underscoreName ${flags_name_}` + + flags_default_=`_flags_getFlagInfo \ + "${flags_usName_}" ${__FLAGS_INFO_DEFAULT}` + flags_help_=`_flags_getFlagInfo \ + "${flags_usName_}" ${__FLAGS_INFO_HELP}` + flags_short_=`_flags_getFlagInfo \ + "${flags_usName_}" ${__FLAGS_INFO_SHORT}` + flags_type_=`_flags_getFlagInfo \ + "${flags_usName_}" ${__FLAGS_INFO_TYPE}` + + [ "${flags_short_}" != "${__FLAGS_NULL}" ] && \ + flags_flagStr_="-${flags_short_}" + + if [ ${__FLAGS_GETOPT_VERS} -eq ${__FLAGS_GETOPT_VERS_ENH} ]; then + [ "${flags_short_}" != "${__FLAGS_NULL}" ] && \ + flags_flagStr_="${flags_flagStr_}," + # add [no] to long boolean flag names, except the 'help' flag + [ ${flags_type_} -eq ${__FLAGS_TYPE_BOOLEAN} \ + -a "${flags_usName_}" != 'help' ] && \ + flags_boolStr_='[no]' + flags_flagStr_="${flags_flagStr_}--${flags_boolStr_}${flags_name_}:" + fi + + case ${flags_type_} in + ${__FLAGS_TYPE_BOOLEAN}) + if [ ${flags_default_} -eq ${FLAGS_TRUE} ]; then + flags_defaultStr_='true' + else + flags_defaultStr_='false' + fi + ;; + ${__FLAGS_TYPE_FLOAT}|${__FLAGS_TYPE_INTEGER}) + flags_defaultStr_=${flags_default_} ;; + ${__FLAGS_TYPE_STRING}) flags_defaultStr_="'${flags_default_}'" ;; + esac + flags_defaultStr_="(default: ${flags_defaultStr_})" + + flags_helpStr_=" ${flags_flagStr_} ${flags_help_} ${flags_defaultStr_}" + _flags_strlen "${flags_helpStr_}" >/dev/null + flags_helpStrLen_=${flags_output} + flags_columns_=`_flags_columns` + + if [ ${flags_helpStrLen_} -lt ${flags_columns_} ]; then + echo "${flags_helpStr_}" >&2 + else + echo " ${flags_flagStr_} ${flags_help_}" >&2 + # note: the silliness with the x's is purely for ksh93 on Ubuntu 6.06 + # because it doesn't like empty strings when used in this manner. + flags_emptyStr_="`echo \"x${flags_flagStr_}x\" \ + |awk '{printf "%"length($0)-2"s", ""}'`" + flags_helpStr_=" ${flags_emptyStr_} ${flags_defaultStr_}" + _flags_strlen "${flags_helpStr_}" >/dev/null + flags_helpStrLen_=${flags_output} + + if [ ${__FLAGS_GETOPT_VERS} -eq ${__FLAGS_GETOPT_VERS_STD} \ + -o ${flags_helpStrLen_} -lt ${flags_columns_} ]; then + # indented to match help string + echo "${flags_helpStr_}" >&2 + else + # indented four from left to allow for longer defaults as long flag + # names might be used too, making things too long + echo " ${flags_defaultStr_}" >&2 + fi + fi + done + fi + + unset flags_boolStr_ flags_default_ flags_defaultStr_ flags_emptyStr_ \ + flags_flagStr_ flags_help_ flags_helpStr flags_helpStrLen flags_name_ \ + flags_columns_ flags_short_ flags_type_ flags_usName_ + return ${FLAGS_TRUE} +} + +# Reset shflags back to an uninitialized state. +# +# Args: +# none +# Returns: +# nothing +flags_reset() +{ + for flags_name_ in ${__flags_longNames}; do + flags_usName_=`_flags_underscoreName ${flags_name_}` + flags_strToEval_="unset FLAGS_${flags_usName_}" + for flags_type_ in \ + ${__FLAGS_INFO_DEFAULT} \ + ${__FLAGS_INFO_HELP} \ + ${__FLAGS_INFO_SHORT} \ + ${__FLAGS_INFO_TYPE} + do + flags_strToEval_=\ +"${flags_strToEval_} __flags_${flags_usName_}_${flags_type_}" + done + eval ${flags_strToEval_} + done + + # reset internal variables + __flags_boolNames=' ' + __flags_longNames=' ' + __flags_shortNames=' ' + __flags_definedNames=' ' + + unset flags_name_ flags_type_ flags_strToEval_ flags_usName_ +} diff --git a/tools/lib/utilities.sh b/tools/lib/utilities.sh new file mode 100755 index 0000000..5eba631 --- /dev/null +++ b/tools/lib/utilities.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash + +# Any subsequent commands which fail will cause the shell script to exit immediately +# set -e + +# ANSI escape codes for message colouring +RED='\033[0;31m' +GREEN='\033[0;32m' +LIGHT_GREEN='\033[1;32m' +NC='\033[0m' # No Color + +printInfo () { + echo -e "${LIGHT_GREEN}${1}${NC}" +} +export -f printInfo + +printError () { + echo -e "${RED}${1}${NC}" +} +export -f printError \ No newline at end of file