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