]> git.armaanb.net Git - gen-shell.git/commitdiff
first push
authorArmaan Bhojwani <3fb650a9-b47e-4604-a282-1dd91953b2ee@anonaddy.me>
Mon, 26 Oct 2020 17:07:58 +0000 (13:07 -0400)
committerArmaan Bhojwani <3fb650a9-b47e-4604-a282-1dd91953b2ee@anonaddy.me>
Mon, 26 Oct 2020 17:07:58 +0000 (13:07 -0400)
85 files changed:
.gitmodules [new file with mode: 0644]
AUTHORS [new file with mode: 0644]
CMakeLists.txt [new file with mode: 0644]
COPYING [new file with mode: 0644]
ChangeLog [new file with mode: 0644]
INSTALL [new file with mode: 0644]
LICENSE [new file with mode: 0644]
NEWS [new file with mode: 0644]
README.md [new file with mode: 0644]
cmake.h [new file with mode: 0644]
cmake.h.in [new file with mode: 0644]
cmake/CXXSniffer.cmake [new file with mode: 0644]
cmake/Modules/FindReadline.cmake [new file with mode: 0644]
commit.h [new file with mode: 0644]
commit.h.in [new file with mode: 0644]
doc/CMakeLists.txt [new file with mode: 0644]
doc/man/tasksh.1 [new file with mode: 0644]
doc/man/tasksh.1.in [new file with mode: 0644]
src/CMakeLists.txt [new file with mode: 0644]
src/diag.cpp [new file with mode: 0644]
src/help.cpp [new file with mode: 0644]
src/liblibshared.a [new file with mode: 0644]
src/libshared/AUTHORS [new file with mode: 0644]
src/libshared/CMakeLists.txt [new file with mode: 0644]
src/libshared/ChangeLog [new file with mode: 0644]
src/libshared/LICENSE [new file with mode: 0644]
src/libshared/cmake.h.in [new file with mode: 0644]
src/libshared/cmake/CXXSniffer.cmake [new file with mode: 0644]
src/libshared/src/Args.cpp [new file with mode: 0644]
src/libshared/src/Args.h [new file with mode: 0644]
src/libshared/src/CMakeLists.txt [new file with mode: 0644]
src/libshared/src/Color.cpp [new file with mode: 0644]
src/libshared/src/Color.h [new file with mode: 0644]
src/libshared/src/Composite.cpp [new file with mode: 0644]
src/libshared/src/Composite.h [new file with mode: 0644]
src/libshared/src/Configuration.cpp [new file with mode: 0644]
src/libshared/src/Configuration.h [new file with mode: 0644]
src/libshared/src/Datetime.cpp [new file with mode: 0644]
src/libshared/src/Datetime.h [new file with mode: 0644]
src/libshared/src/Duration.cpp [new file with mode: 0644]
src/libshared/src/Duration.h [new file with mode: 0644]
src/libshared/src/FS.cpp [new file with mode: 0644]
src/libshared/src/FS.h [new file with mode: 0644]
src/libshared/src/JSON.cpp [new file with mode: 0644]
src/libshared/src/JSON.h [new file with mode: 0644]
src/libshared/src/Lexer.cpp [new file with mode: 0644]
src/libshared/src/Lexer.h [new file with mode: 0644]
src/libshared/src/Log.cpp [new file with mode: 0644]
src/libshared/src/Log.h [new file with mode: 0644]
src/libshared/src/Msg.cpp [new file with mode: 0644]
src/libshared/src/Msg.h [new file with mode: 0644]
src/libshared/src/PEG.cpp [new file with mode: 0644]
src/libshared/src/PEG.h [new file with mode: 0644]
src/libshared/src/Packrat.cpp [new file with mode: 0644]
src/libshared/src/Packrat.h [new file with mode: 0644]
src/libshared/src/Palette.cpp [new file with mode: 0644]
src/libshared/src/Palette.h [new file with mode: 0644]
src/libshared/src/Pig.cpp [new file with mode: 0644]
src/libshared/src/Pig.h [new file with mode: 0644]
src/libshared/src/README [new file with mode: 0644]
src/libshared/src/RX.cpp [new file with mode: 0644]
src/libshared/src/RX.h [new file with mode: 0644]
src/libshared/src/SAX.cpp [new file with mode: 0644]
src/libshared/src/Table.cpp [new file with mode: 0644]
src/libshared/src/Table.h [new file with mode: 0644]
src/libshared/src/Timer.cpp [new file with mode: 0644]
src/libshared/src/Timer.h [new file with mode: 0644]
src/libshared/src/Tree.cpp [new file with mode: 0644]
src/libshared/src/Tree.h [new file with mode: 0644]
src/libshared/src/format.cpp [new file with mode: 0644]
src/libshared/src/format.h [new file with mode: 0644]
src/libshared/src/ip.cpp [new file with mode: 0644]
src/libshared/src/lex.cpp [new file with mode: 0644]
src/libshared/src/shared.cpp [new file with mode: 0644]
src/libshared/src/shared.h [new file with mode: 0644]
src/libshared/src/unicode.cpp [new file with mode: 0644]
src/libshared/src/unicode.h [new file with mode: 0644]
src/libshared/src/utf8.cpp [new file with mode: 0644]
src/libshared/src/utf8.h [new file with mode: 0644]
src/libshared/src/wcwidth6.cpp [new file with mode: 0644]
src/libtasksh.a [new file with mode: 0644]
src/main.cpp [new file with mode: 0644]
src/prompt.cpp [new file with mode: 0644]
src/review.cpp [new file with mode: 0644]
src/shell.cpp [new file with mode: 0644]

diff --git a/.gitmodules b/.gitmodules
new file mode 100644 (file)
index 0000000..675b3d4
--- /dev/null
@@ -0,0 +1,3 @@
+[submodule "src/libshared"]
+       path = src/libshared
+       url = https://git.tasktools.org/TM/libshared.git
diff --git a/AUTHORS b/AUTHORS
new file mode 100644 (file)
index 0000000..9617cca
--- /dev/null
+++ b/AUTHORS
@@ -0,0 +1,28 @@
+The development of tasksh was made possible by the significant contributions of
+the following people:
+
+  Paul Beckingham    (Principal Author)
+  Federico Hernandez (Principal Author)
+  Dirk Deimeke       (Technical Advisor & Evangelist)
+
+The following submitted code, packages or analysis, and deserve special thanks:
+
+  Jörg Krause
+  Ben Boeckel
+  ilove zfs
+  Paul Fenwick
+
+Thanks to the following, who submitted detailed bug reports and excellent
+suggestions:
+
+  Kevin Gunn
+  Fidel Mato
+  David Stahl
+  David Patrick
+  jonbobbly
+  hosaka
+  Lars Kumbier
+  Iain R. Learmonth
+  Eric Hymowitz
+  bjonnh
+
diff --git a/CMakeLists.txt b/CMakeLists.txt
new file mode 100644 (file)
index 0000000..205cf7b
--- /dev/null
@@ -0,0 +1,79 @@
+cmake_minimum_required (VERSION 2.8)
+set (CMAKE_LEGACY_CYGWIN_WIN32 0) # Remove when CMake >= 2.8.4 is required
+set (CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake")
+set (HAVE_CMAKE true)
+
+project (tasksh)
+include (CXXSniffer)
+
+set (PROJECT_VERSION "1.2.0")
+
+include (CheckFunctionExists)
+include (CheckStructHasMember)
+include (CheckCXXCompilerFlag)
+
+message ("-- Looking for SHA1 references")
+if (EXISTS ${CMAKE_SOURCE_DIR}/.git/index)
+  set (HAVE_COMMIT true)
+  execute_process (COMMAND git log -1 --pretty=format:%h
+                   WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
+                   OUTPUT_VARIABLE COMMIT)
+  configure_file ( ${CMAKE_SOURCE_DIR}/commit.h.in
+                   ${CMAKE_SOURCE_DIR}/commit.h)
+  message ("-- Found SHA1 reference: ${COMMIT}")
+endif (EXISTS ${CMAKE_SOURCE_DIR}/.git/index)
+
+set (PACKAGE "${PROJECT_NAME}")
+set (VERSION "${PROJECT_VERSION}")
+set (PACKAGE_BUGREPORT "support@taskwarrior.org")
+set (PACKAGE_NAME "${PACKAGE}")
+set (PACKAGE_TARNAME "${PACKAGE}")
+set (PACKAGE_VERSION "${VERSION}")
+set (PACKAGE_STRING "${PACKAGE} ${VERSION}")
+
+if (FREEBSD)
+SET (TASKSH_MAN1DIR man/man1            CACHE STRING "Installation directory for man pages, section 1")
+else (FREEBSD)
+SET (TASKSH_MAN1DIR share/man/man1      CACHE STRING "Installation directory for man pages, section 1")
+endif (FREEBSD)
+SET (TASKSH_DOCDIR  share/doc/tasksh    CACHE STRING "Installation directory for doc files")
+SET (TASKSH_RCDIR "${TASKSH_DOCDIR}/rc" CACHE STRING "Installation directory for configuration files")
+SET (TASKSH_BINDIR  bin                 CACHE STRING "Installation directory for the binary")
+
+# include the readline library finder module
+set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake/Modules")
+
+# find readline
+message ("-- Looking for GNU Readline")
+find_package (Readline REQUIRED)
+if (READLINE_FOUND)
+  set (HAVE_READLINE true)
+  set (TASKSH_INCLUDE_DIRS ${TASKSH_INCLUDE_DIRS} ${READLINE_INCLUDE_DIR})
+  set (TASKSH_LIBRARIES    ${TASKSH_LIBRARIES}    ${READLINE_LIBRARIES})
+endif (READLINE_FOUND)
+
+message ("-- Configuring cmake.h")
+configure_file (
+  ${CMAKE_SOURCE_DIR}/cmake.h.in
+  ${CMAKE_SOURCE_DIR}/cmake.h)
+
+add_subdirectory (src)
+add_subdirectory (doc)
+if (EXISTS ${CMAKE_SOURCE_DIR}/test)
+  add_subdirectory (test EXCLUDE_FROM_ALL)
+endif (EXISTS ${CMAKE_SOURCE_DIR}/test)
+
+set (doc_FILES NEWS ChangeLog INSTALL AUTHORS COPYING)
+foreach (doc_FILE ${doc_FILES})
+  install (FILES ${doc_FILE}  DESTINATION ${TASKSH_DOCDIR})
+endforeach (doc_FILE)
+
+# ---
+
+set (CPACK_SOURCE_GENERATOR "TGZ")
+set (CPACK_SOURCE_PACKAGE_FILE_NAME ${PACKAGE_NAME}-${PACKAGE_VERSION})
+set (CPACK_SOURCE_IGNORE_FILES  "CMakeCache" "CMakeFiles" "CPackConfig" "CPackSourceConfig"
+                                "_CPack_Packages" "cmake_install" "install_manifest" "Makefile$"
+                                "test" "package-config" "misc/*" "src/tasksh$" "README.md"
+                                "/\\\\.gitignore" "/\\\\.git/" "swp$")
+include (CPack)
diff --git a/COPYING b/COPYING
new file mode 100644 (file)
index 0000000..9295662
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,23 @@
+tasksh - a shell/frontend for the command line task list manager taskwarrior.
+
+Copyright 2006 - 2017, Paul Beckingham, Federico Hernandez.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+http://www.opensource.org/licenses/mit-license.php
diff --git a/ChangeLog b/ChangeLog
new file mode 100644 (file)
index 0000000..733d5fa
--- /dev/null
+++ b/ChangeLog
@@ -0,0 +1,50 @@
+1.2.0 (2017-05-10) -
+
+- TS-29   tasksh hangs trying to read task from stdin
+          (thanks to ilove zfs).
+- TS-32   control-d to exit
+          (thanks to Eric Hymowitz, Paul Fenwick).
+- TS-34   Tasksh throw a warning at the end of a review command
+          (thanks to bjonnh).
+- Review report now defaults to 6 days instead of 1 weeķ, which is more
+  convenient for those who review weekly
+          (thanks to Dirk Deimeke).
+
+------ current release ---------------------------
+
+1.1.0 (2016-09-06) 464f5ae19f853911e739c2489897aef64345c388
+
+- TD-120  Missing cmakedefine for HAVE_GET_CURRENT_DIR_NAME
+          (Thanks to Jörg Krause, Ben Boeckel).
+- TW-1845 Cygwin build fails, missing get_current_dir_name
+          (thanks to hosaka).
+- TS-11   Autoclear in the Task Shell
+          (thanks to Lars Kumbier).
+- TS-24   add review option (m)odify
+          (thanks to David Patrick).
+- TS-28   Please add a (m)odify feature for review
+          (thanks to Iain R. Learmonth).
+- Implemented 'review' command.
+- Implemented 'diag' command.
+- Added 'review N' option, to specify the number of tasks you would like to
+  review.
+- Integrated libshared.git.
+
+------ old releases ------------------------------
+
+1.0.0 (2014-12-21) 5934dfcefac6d037a359bc733a8382e42e32552e
+
+- TS-1    Apostrophe inside tasksh 'log' causes segmentation fault
+          (thanks to David Stahl).
+- TS-2    tasksh segfaults if quotes not closed
+          (thanks to Fidel Mato).
+- TS-5    tasksh segfaults
+          (thanks to David Patrick).
+- TS-13   Quotes included when using task shell
+          (thanks to Kevin Gunn).
+- libreadline support added for line editing and command history.
+
+------ start -----------------------------------
+
+Project started 2014-06-08
+
diff --git a/INSTALL b/INSTALL
new file mode 100644 (file)
index 0000000..95f9120
--- /dev/null
+++ b/INSTALL
@@ -0,0 +1,131 @@
+Installation Instructions
+-------------------------
+
+Please follow the instructions below to build and install tasksh from source.
+
+
+Dependencies
+------------
+
+You will need the CMake build system installed in order to build tasksh from
+source.  Information on cmake can be obtained at http://cmake.org
+
+Additionally, you will need:
+
+  libreadline
+
+
+Basic Installation
+------------------
+
+Briefly, these shell commands will unpack, build and install Tasksh:
+
+  $ tar xzf tasksh-X.Y.Z.tar.gz                   [1]
+  $ cd tasksh-X.Y.Z                               [2]
+  $ cmake -DCMAKE_BUILD_TYPE=release .            [3]
+  $ make                                          [4]
+  $ sudo make install                             [5]
+  $ cd .. ; rm -r tasksh-X.Y.Z                    [6]
+
+These commands are explained below:
+
+  1. Unpacks the source tarball.  This creates the directory tasksh-X.Y.Z,
+     containing all the code.
+
+  2. Change directory to the root of the distribution.
+
+  3. Invokes CMake to scan for dependencies and machine-specific details, then
+     generate the makefiles.  Requests an optimized build, which will run faster
+     and be more compact.  This may take a minute.
+
+  4. Builds tasksh.  This may take a minute.
+
+  5. Installs the program, documentation and other data files.
+
+  6. Removes the temporary directory.
+
+
+Build and configurations options
+--------------------------------
+
+You can customize the configuration run with cmake variables. This will modify
+the installation process:
+
+To change the installation directory you use the following configuration
+variable:
+
+  $ cmake -DCMAKE_INSTALL_PREFIX=<path-to-installation-dir> .
+
+cmake configuration variables are applied with the -D option and consist of a
+<name> and a <value>:
+
+  $ cmake -D<name>=<value> .
+
+Four more variables can customize the installation process. The following table
+lists them and their defaults plus the CMAKE_INSTALL_PREFIX:
+
+  CMAKE_INSTALL_PREFIX  /usr/local
+  TASKSH_BINDIR         bin
+  TASKSH_DOCDIR         share/doc/tasksh
+  TASKSH_MAN1DIR        share/man/man1
+
+The corresponding TASKSH_* variables will be combined with CMAKE_INSTALL_PREFIX to
+get absolute installation directories:
+
+  CMAKE_INSTALL_PREFIX/TASKSH_BINDIR   /usr/local/bin
+  CMAKE_INSTALL_PREFIX/TASKSH_DOCDIR   /usr/local/share/doc/tasksh
+  CMAKE_INSTALL_PREFIX/TASKSH_MAN1DIR  /usr/local/share/man/man1
+
+
+Uninstallation
+--------------
+
+There is no uninstall option in CMake makefiles. This is a manual process.
+
+To uninstall Tasksh, remove the files listed in the install_manifest.txt file
+that was generated when you built Tasksh.
+
+
+Tasksh Build Notes
+-----------------------
+
+Tasksh has dependencies that are detected by CMake in almost all cases, but
+there are situations and operating systems that mean you will need to offer a
+little help.
+
+If Tasksh will not build on your system, first take a look at the Operating
+System notes below.  If this doesn't help, then go to the Troubleshooting
+section, which includes instructions on how to contact us for help.
+
+
+Operating System Notes
+----------------------
+
+Cygwin
+  If 'make install' fails when writing to the /usr/local/share/ directory,
+  this may be because your current login doesn't have permission to write
+  to the Windows directory containing your Cygwin installation. Either
+  login to Windows as an Administrator and try the 'make install' process
+  again, or reinstall Cygwin under your normal Windows login.
+
+
+Troubleshooting
+---------------
+
+If you've recently made changes to dependencies (by reinstalling them, for
+example) be sure to rerun 'cmake .' before trying to execute 'make' again.
+
+CMake will run and locate all the necessary pieces for the build, and create
+a Makefile.  There may be errors and warnings when running CMake, or there
+may be compiler errors and warnings when running 'make'.  Sometimes you will run
+CMake with no reported problems, and the build will fail later.  This is
+almost always because CMake is mistaken about some assumption.
+
+If a build does not succeed, please send the contents of the 'CMakeCache.txt'
+and 'CMakeFiles/CMakeOutput.log' files to support@taskwarrior.org.
+
+If CMake runs but tasksh does not build, please send the contents of the above
+files as well as a transcript from the build, which is not written to a file
+and must be captured from the terminal.
+
+---
diff --git a/LICENSE b/LICENSE
new file mode 100644 (file)
index 0000000..f9ab300
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,23 @@
+tasksh - a shell/frontend for ithe command line task list manager taskwarrior.
+
+Copyright 2006 - 2017, Paul Beckingham, Federico Hernandez.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+http://www.opensource.org/licenses/mit-license.php
diff --git a/NEWS b/NEWS
new file mode 100644 (file)
index 0000000..4f9546e
--- /dev/null
+++ b/NEWS
@@ -0,0 +1,43 @@
+
+New Features in tasksh 1.2.0
+
+  - Responds to Ctrl-D by exiting.
+
+New commands in tasksh 1.2.0
+
+  - 
+
+New configuration options in tasksh 1.2.0
+
+  - 
+
+Known Issues
+
+  - 
+
+Tasksh has been built and tested on the following configurations:
+
+  * macOS
+  * Fedora
+  * Ubuntu
+  * Debian
+  * Arch
+  * FreeBSD
+  * Cygwin
+
+---
+
+While Tasksh has undergone testing, bugs are sure to remain.  If you
+encounter a bug, please enter a new issue at:
+
+  http://bug.tasktools.org
+
+Or you can also report the issue in the forums at:
+
+  http://answers.tasktools.org
+
+Or just send a message to:
+
+  support@taskwarrior.org
+
+Thank you.
diff --git a/README.md b/README.md
new file mode 100644 (file)
index 0000000..953d7c7
--- /dev/null
+++ b/README.md
@@ -0,0 +1,2 @@
+# gen-shell
+A work in progress generic shell forked form [taskshell](https://github.com/GothenburgBitFactory/taskshell)
diff --git a/cmake.h b/cmake.h
new file mode 100644 (file)
index 0000000..6a8947b
--- /dev/null
+++ b/cmake.h
@@ -0,0 +1,60 @@
+/* cmake.h.in. Creates cmake.h during a cmake run */
+
+/* Product identification */
+#define PRODUCT_TASKSH 1
+
+/* Package information */
+#define PACKAGE           "tasksh"
+#define VERSION           "1.2.0"
+#define PACKAGE_BUGREPORT "support@taskwarrior.org"
+#define PACKAGE_NAME      "tasksh"
+#define PACKAGE_TARNAME   "tasksh"
+#define PACKAGE_VERSION   "1.2.0"
+#define PACKAGE_STRING    "tasksh 1.2.0"
+
+#define CMAKE_BUILD_TYPE  ""
+
+/* Localization */
+#define PACKAGE_LANGUAGE 
+#define LANGUAGE_ENG_USA 
+
+/* git information */
+#define HAVE_COMMIT
+
+/* cmake information */
+#define HAVE_CMAKE
+#define CMAKE_VERSION "3.8.1"
+
+/* Compiling platform */
+/* #undef LINUX */
+#define DARWIN
+/* #undef CYGWIN */
+/* #undef FREEBSD */
+/* #undef OPENBSD */
+/* #undef NETBSD */
+/* #undef HAIKU */
+/* #undef SOLARIS */
+/* #undef KFREEBSD */
+/* #undef GNUHURD */
+/* #undef UNKNOWN */
+
+/* Found the Readline library */
+#define HAVE_READLINE
+
+/* Found the pthread library */
+/* #undef HAVE_LIBPTHREAD */
+
+/* Found wordexp.h */
+/* #undef HAVE_WORDEXP */
+
+/* Found tm.tm_gmtoff struct member */
+/* #undef HAVE_TM_GMTOFF */
+
+/* Found st.st_birthtime struct member */
+/* #undef HAVE_ST_BIRTHTIME */
+
+/* Functions */
+/* #undef HAVE_GET_CURRENT_DIR_NAME */
+/* #undef HAVE_TIMEGM */
+/* #undef HAVE_UUID_UNPARSE_LOWER */
+
diff --git a/cmake.h.in b/cmake.h.in
new file mode 100644 (file)
index 0000000..e905598
--- /dev/null
@@ -0,0 +1,60 @@
+/* cmake.h.in. Creates cmake.h during a cmake run */
+
+/* Product identification */
+#define PRODUCT_TASKSH 1
+
+/* Package information */
+#define PACKAGE           "${PACKAGE}"
+#define VERSION           "${VERSION}"
+#define PACKAGE_BUGREPORT "${PACKAGE_BUGREPORT}"
+#define PACKAGE_NAME      "${PACKAGE_NAME}"
+#define PACKAGE_TARNAME   "${PACKAGE_TARNAME}"
+#define PACKAGE_VERSION   "${PACKAGE_VERSION}"
+#define PACKAGE_STRING    "${PACKAGE_STRING}"
+
+#define CMAKE_BUILD_TYPE  "${CMAKE_BUILD_TYPE}"
+
+/* Localization */
+#define PACKAGE_LANGUAGE ${PACKAGE_LANGUAGE}
+#define LANGUAGE_ENG_USA ${LANGUAGE_ENG_USA}
+
+/* git information */
+#cmakedefine HAVE_COMMIT
+
+/* cmake information */
+#cmakedefine HAVE_CMAKE
+#define CMAKE_VERSION "${CMAKE_VERSION}"
+
+/* Compiling platform */
+#cmakedefine LINUX
+#cmakedefine DARWIN
+#cmakedefine CYGWIN
+#cmakedefine FREEBSD
+#cmakedefine OPENBSD
+#cmakedefine NETBSD
+#cmakedefine HAIKU
+#cmakedefine SOLARIS
+#cmakedefine KFREEBSD
+#cmakedefine GNUHURD
+#cmakedefine UNKNOWN
+
+/* Found the Readline library */
+#cmakedefine HAVE_READLINE
+
+/* Found the pthread library */
+#cmakedefine HAVE_LIBPTHREAD
+
+/* Found wordexp.h */
+#cmakedefine HAVE_WORDEXP
+
+/* Found tm.tm_gmtoff struct member */
+#cmakedefine HAVE_TM_GMTOFF
+
+/* Found st.st_birthtime struct member */
+#cmakedefine HAVE_ST_BIRTHTIME
+
+/* Functions */
+#cmakedefine HAVE_GET_CURRENT_DIR_NAME
+#cmakedefine HAVE_TIMEGM
+#cmakedefine HAVE_UUID_UNPARSE_LOWER
+
diff --git a/cmake/CXXSniffer.cmake b/cmake/CXXSniffer.cmake
new file mode 100644 (file)
index 0000000..9606226
--- /dev/null
@@ -0,0 +1,51 @@
+message ("-- Configuring C++11")
+message ("-- System: ${CMAKE_SYSTEM_NAME}")
+
+include (CheckCXXCompilerFlag)
+
+# NOTE: Phase out -std=gnu++0x and --std=c++0x as soon as realistically possible.
+CHECK_CXX_COMPILER_FLAG("-std=c++11"   _HAS_CXX11)
+CHECK_CXX_COMPILER_FLAG("-std=c++0x"   _HAS_CXX0X)
+CHECK_CXX_COMPILER_FLAG("-std=gnu++0x" _HAS_GNU0X)
+
+if (_HAS_CXX11)
+  set (_CXX11_FLAGS "-std=c++11")
+elseif (_HAS_CXX0X)
+  message (WARNING "Enabling -std=c++0x draft compile flag. Your compiler does not support the standard '-std=c++11' option.  Consider upgrading.")
+  set (_CXX11_FLAGS "-std=c++0x")
+elseif (_HAS_GNU0X)
+  message (WARNING "Enabling -std=gnu++0x draft compile flag. Your compiler does not support the standard '-std=c++11' option. Consider upgrading.")
+  set (_CXX11_FLAGS "-std=gnu++0x")
+else (_HAS_CXX11)
+ message (FATAL_ERROR "C++11 support missing. Try upgrading your C++ compiler. If you have a good reason for using an outdated compiler, please let us know at support@taskwarrior.org.")
+endif (_HAS_CXX11)
+
+if (${CMAKE_SYSTEM_NAME} MATCHES "Linux")
+  set (LINUX true)
+elseif (${CMAKE_SYSTEM_NAME} MATCHES "Darwin")
+  set (DARWIN true)
+  set (_CXX11_FLAGS "${_CXX11_FLAGS} -stdlib=libc++")
+elseif (${CMAKE_SYSTEM_NAME} MATCHES "kFreeBSD")
+  set (KFREEBSD true)
+elseif (${CMAKE_SYSTEM_NAME} MATCHES "FreeBSD")
+  set (FREEBSD true)
+elseif (${CMAKE_SYSTEM_NAME} MATCHES "OpenBSD")
+  set (OPENBSD true)
+elseif (${CMAKE_SYSTEM_NAME} MATCHES "NetBSD")
+  set (NETBSD true)
+elseif (${CMAKE_SYSTEM_NAME} MATCHES "SunOS")
+  set (SOLARIS true)
+elseif (${CMAKE_SYSTEM_NAME} STREQUAL "GNU")
+  set (GNUHURD true)
+elseif (${CMAKE_SYSTEM_NAME} STREQUAL "CYGWIN")
+  set (CYGWIN true)
+  # NOTE: Not setting -std=gnu++0x leads to compile errors even with
+  #       GCC 4.8.3, and debugging those leads to insanity. Adding this
+  #       workaround instead of fixing Cygwin.
+  set (_CXX11_FLAGS "-std=gnu++0x")
+else (${CMAKE_SYSTEM_NAME} MATCHES "Linux")
+  set (UNKNOWN true)
+endif (${CMAKE_SYSTEM_NAME} MATCHES "Linux")
+
+set (CMAKE_CXX_FLAGS "${_CXX11_FLAGS} ${CMAKE_CXX_FLAGS}")
+set (CMAKE_CXX_FLAGS "-Wall -Wextra -Wsign-compare -Wreturn-type ${CMAKE_CXX_FLAGS}")
diff --git a/cmake/Modules/FindReadline.cmake b/cmake/Modules/FindReadline.cmake
new file mode 100644 (file)
index 0000000..8fdaec1
--- /dev/null
@@ -0,0 +1,81 @@
+# - Find the readline library
+# This module defines
+#  READLINE_INCLUDE_DIR, path to readline/readline.h, etc.
+#  READLINE_LIBRARIES, the libraries required to use READLINE.
+#  READLINE_FOUND, If false, do not try to use READLINE.
+# also defined, but not for general use are
+# READLINE_readline_LIBRARY, where to find the READLINE library.
+# READLINE_ncurses_LIBRARY, where to find the ncurses library [might not be defined]
+
+# Apple readline does not support readline hooks
+# So we look for another one by default
+IF (APPLE OR FREEBSD)
+  FIND_PATH (READLINE_INCLUDE_DIR NAMES readline/readline.h PATHS
+    /usr/include/
+    /sw/include
+    /opt/local/include
+    /opt/include
+    /usr/local/include
+    NO_DEFAULT_PATH
+    )
+ENDIF (APPLE OR FREEBSD)
+FIND_PATH (READLINE_INCLUDE_DIR NAMES readline/readline.h)
+
+
+# Apple readline does not support readline hooks
+# So we look for another one by default
+IF (APPLE OR FREEBSD)
+  FIND_LIBRARY (READLINE_readline_LIBRARY NAMES readline PATHS
+    /usr/lib
+    /sw/lib
+    /opt/local/lib
+    /opt/lib
+    /usr/local/lib
+    NO_DEFAULT_PATH
+    )
+ENDIF (APPLE OR FREEBSD)
+FIND_LIBRARY (READLINE_readline_LIBRARY NAMES readline)
+
+# Sometimes readline really needs ncurses
+IF (APPLE OR FREEBSD)
+  FIND_LIBRARY (READLINE_ncurses_LIBRARY NAMES ncurses PATHS
+    /usr/lib
+    /sw/lib
+    /opt/local/lib
+    /opt/lib
+    /usr/local/lib
+    /usr/lib
+    NO_DEFAULT_PATH
+    )
+ENDIF (APPLE OR FREEBSD)
+FIND_LIBRARY (READLINE_ncurses_LIBRARY NAMES ncurses)
+
+MARK_AS_ADVANCED (
+  READLINE_INCLUDE_DIR
+  READLINE_readline_LIBRARY
+  READLINE_ncurses_LIBRARY
+  )
+
+SET (READLINE_FOUND "NO" )
+IF (READLINE_INCLUDE_DIR)
+  IF (READLINE_readline_LIBRARY)
+    SET (READLINE_FOUND "YES" )
+    SET (READLINE_LIBRARIES
+      ${READLINE_readline_LIBRARY} 
+      )
+
+    # some readline libraries depend on ncurses
+    IF (READLINE_ncurses_LIBRARY)
+      SET (READLINE_LIBRARIES ${READLINE_LIBRARIES} ${READLINE_ncurses_LIBRARY})
+    ENDIF (READLINE_ncurses_LIBRARY)
+
+  ENDIF (READLINE_readline_LIBRARY)
+ENDIF (READLINE_INCLUDE_DIR)
+
+IF (READLINE_FOUND)
+  MESSAGE (STATUS "Found readline library")
+ELSE (READLINE_FOUND)
+  IF (READLINE_FIND_REQUIRED)
+    MESSAGE (FATAL_ERROR "Could not find readline -- please give some paths to CMake")
+  ENDIF (READLINE_FIND_REQUIRED)
+ENDIF (READLINE_FOUND)
diff --git a/commit.h b/commit.h
new file mode 100644 (file)
index 0000000..a968aef
--- /dev/null
+++ b/commit.h
@@ -0,0 +1,4 @@
+/* commit.h.in. Creates commit.h during a cmake run */
+
+/* git information */
+#define COMMIT "e6d0532"
diff --git a/commit.h.in b/commit.h.in
new file mode 100644 (file)
index 0000000..3a6fe82
--- /dev/null
@@ -0,0 +1,4 @@
+/* commit.h.in. Creates commit.h during a cmake run */
+
+/* git information */
+#define COMMIT "${COMMIT}"
diff --git a/doc/CMakeLists.txt b/doc/CMakeLists.txt
new file mode 100644 (file)
index 0000000..180543d
--- /dev/null
@@ -0,0 +1,12 @@
+cmake_minimum_required (VERSION 2.8)
+message ("-- Configuring man pages")
+set (man_FILES tasksh.1)
+foreach (man_FILE ${man_FILES})
+  configure_file (
+    man/${man_FILE}.in
+    man/${man_FILE})
+endforeach (man_FILE)
+
+install (DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/man/   DESTINATION ${TASKSH_MAN1DIR}
+                                                      FILES_MATCHING PATTERN "*.1")
+
diff --git a/doc/man/tasksh.1 b/doc/man/tasksh.1
new file mode 100644 (file)
index 0000000..ec53447
--- /dev/null
@@ -0,0 +1,175 @@
+.TH tasksh 1 2017-05-10 "tasksh 1.2.0" "User Manuals"
+
+.SH NAME
+tasksh \- Interactive taskwarrior shell
+
+.SH SYNOPSIS
+.B tasksh
+.br
+.B tasksh --version
+
+.SH DESCRIPTION
+Tasksh can be used to create a more immersive taskwarrior environment.
+Any task command you run outside the shell can also be run inside
+the shell, without the need to prefix every command with "task".
+
+When built with libreadline, tasksh provides command editing and history.
+
+Tasksh has an integrated 'review' command that leads you through an interactive
+review session.
+
+Tasksh supports all recent versions of Taskwarrior.
+
+.SH COMMANDS
+Tasksh supports the following commands.  All other commands are passed intact to
+Taskwarrior.
+
+.TP
+.B diagnostics
+Displays settings pertinent to tasksh, for diagnosing problems.
+
+.TP
+.B exec <commands>
+This command allows you to run shell commands from within Tasksh. This is ideal
+for accessing man pages such as this. The '!' command can be used in place of
+the 'exec' keyword. Once the command is run, control returns to Tasksh.
+
+.TP
+.B exit/quit
+These commands cause tasksh to terminate, returning you to your system shell.
+
+.TP
+.B help
+Shows a summary of commands, and how to obtain help.
+
+.TP
+.B review [N]
+Begins an interactive review session, where you can mark tasks as reviewed,
+edit them using your text editor, provide modification commands, or skip them.
+You can terminate a review session at any time, and the next review session
+will resume at the right place.
+
+To find tasks needing review, the '_reviewed' custom report is created and run,
+which filters tasks that have a missing 'reviewed' UDA date, or have not been
+reviewed for a week.
+
+This means that if you run a review session to completion, there will be no
+need to review again for a week, and the review command will simply do nothing
+until then.
+
+The one week review cycle is defined by the '_reviewed' custom report, which
+can be modified if you prefer a monthly review cycle.
+
+If 'N' is provided, the session is limited to reviewing only N tasks.
+
+Note: requires Taskwarrior 2.5.0 or later.
+For full details, see: 
+<https://taskwarrior.org/docs/review.html>
+
+.SH USAGE
+Here is an example tasksh session.
+
+$ tasksh
+.br
+task> projects
+.br
+
+.br
+Project Tasks Pri:None Pri:L Pri:M Pri:H
+.br
+------- ----- -------- ----- ----- -----
+.br
+            7        7     0     0     0
+.br
+home        2        2     0     0     0
+.br
+party       6        3     0     0     3
+.br
+
+.br
+3 projects (15 tasks)
+.br
+task> tags
+.br
+
+.br
+Tag  Count
+.br
+mall     2
+.br
+
+.br
+1 tag (15 tasks)
+.br
+task> list
+.br
+
+.br
+ID Project Pri Due        Active Age   Description
+.br
+---------------------------------------------------------------------
+.br
+ 2 party   H   10/17/2015        2 hrs Select and book a venue
+.br
+ 5 party   H   10/22/2015        2 hrs Design invitations
+.br
+ 9 home        10/31/2015         1 hr Pay rent
+.br
+ 3 party                         2 hrs Mail invitations
+.br
+ 4 party                         2 hrs Select a caterer
+.br
+ 6 party                         2 hrs Print invitations
+.br
+
+.br
+ 8 tasks
+.br
+ task> quit
+.br
+ $
+.br
+
+.SH CONFIGURATION
+Tasksh piggybacks on Taskwarrior's .taskrc configuration file, and refers
+to settings there. If you use a non-standard location for your .task database
+, and .taskrc file, Tasksh will not find them unless you set the TASKDATA and
+TASKRC environment variables. See 'man taskrc' for more details.
+
+The review command storeѕ a UDA ('reviewed') and report definition ('_reviewed').
+
+.TP
+.B tasksh.autoclear=1
+If set to "1", causes each tasksh command to be preceded by a 'clear screen' and
+cursor reset. Default is "0".
+
+.SH "CREDITS & COPYRIGHTS"
+Copyright (C) 2006 \- 2017 P. Beckingham, F. Hernandez.
+
+This man page was originally written by Federico Hernandez.
+
+Tasksh is distributed under the MIT license. See
+http://www.opensource.org/licenses/mit-license.php for more information.
+
+.SH SEE ALSO
+.BR task(1),
+
+For more information regarding tasksh, see the following:
+
+.TP
+The official site at
+<http://taskwarrior.org/tools>
+
+.TP
+The official code repository at
+<https://git.tasktools.org/scm/ex/tasksh.git>
+
+.TP
+You can contact the project by emailing
+<support@tasktools.org>
+
+.SH REPORTING BUGS
+.TP
+Bugs in tasksh may be reported to the issue-tracker at
+<http://bug.tasktools.org>
+
diff --git a/doc/man/tasksh.1.in b/doc/man/tasksh.1.in
new file mode 100644 (file)
index 0000000..0b0354c
--- /dev/null
@@ -0,0 +1,175 @@
+.TH tasksh 1 2017-05-10 "${PACKAGE_STRING}" "User Manuals"
+
+.SH NAME
+tasksh \- Interactive taskwarrior shell
+
+.SH SYNOPSIS
+.B tasksh
+.br
+.B tasksh --version
+
+.SH DESCRIPTION
+Tasksh can be used to create a more immersive taskwarrior environment.
+Any task command you run outside the shell can also be run inside
+the shell, without the need to prefix every command with "task".
+
+When built with libreadline, tasksh provides command editing and history.
+
+Tasksh has an integrated 'review' command that leads you through an interactive
+review session.
+
+Tasksh supports all recent versions of Taskwarrior.
+
+.SH COMMANDS
+Tasksh supports the following commands.  All other commands are passed intact to
+Taskwarrior.
+
+.TP
+.B diagnostics
+Displays settings pertinent to tasksh, for diagnosing problems.
+
+.TP
+.B exec <commands>
+This command allows you to run shell commands from within Tasksh. This is ideal
+for accessing man pages such as this. The '!' command can be used in place of
+the 'exec' keyword. Once the command is run, control returns to Tasksh.
+
+.TP
+.B exit/quit
+These commands cause tasksh to terminate, returning you to your system shell.
+
+.TP
+.B help
+Shows a summary of commands, and how to obtain help.
+
+.TP
+.B review [N]
+Begins an interactive review session, where you can mark tasks as reviewed,
+edit them using your text editor, provide modification commands, or skip them.
+You can terminate a review session at any time, and the next review session
+will resume at the right place.
+
+To find tasks needing review, the '_reviewed' custom report is created and run,
+which filters tasks that have a missing 'reviewed' UDA date, or have not been
+reviewed for a week.
+
+This means that if you run a review session to completion, there will be no
+need to review again for a week, and the review command will simply do nothing
+until then.
+
+The one week review cycle is defined by the '_reviewed' custom report, which
+can be modified if you prefer a monthly review cycle.
+
+If 'N' is provided, the session is limited to reviewing only N tasks.
+
+Note: requires Taskwarrior 2.5.0 or later.
+For full details, see: 
+<https://taskwarrior.org/docs/review.html>
+
+.SH USAGE
+Here is an example tasksh session.
+
+$ tasksh
+.br
+task> projects
+.br
+
+.br
+Project Tasks Pri:None Pri:L Pri:M Pri:H
+.br
+------- ----- -------- ----- ----- -----
+.br
+            7        7     0     0     0
+.br
+home        2        2     0     0     0
+.br
+party       6        3     0     0     3
+.br
+
+.br
+3 projects (15 tasks)
+.br
+task> tags
+.br
+
+.br
+Tag  Count
+.br
+mall     2
+.br
+
+.br
+1 tag (15 tasks)
+.br
+task> list
+.br
+
+.br
+ID Project Pri Due        Active Age   Description
+.br
+---------------------------------------------------------------------
+.br
+ 2 party   H   10/17/2015        2 hrs Select and book a venue
+.br
+ 5 party   H   10/22/2015        2 hrs Design invitations
+.br
+ 9 home        10/31/2015         1 hr Pay rent
+.br
+ 3 party                         2 hrs Mail invitations
+.br
+ 4 party                         2 hrs Select a caterer
+.br
+ 6 party                         2 hrs Print invitations
+.br
+
+.br
+ 8 tasks
+.br
+ task> quit
+.br
+ $
+.br
+
+.SH CONFIGURATION
+Tasksh piggybacks on Taskwarrior's .taskrc configuration file, and refers
+to settings there. If you use a non-standard location for your .task database
+, and .taskrc file, Tasksh will not find them unless you set the TASKDATA and
+TASKRC environment variables. See 'man taskrc' for more details.
+
+The review command storeѕ a UDA ('reviewed') and report definition ('_reviewed').
+
+.TP
+.B tasksh.autoclear=1
+If set to "1", causes each tasksh command to be preceded by a 'clear screen' and
+cursor reset. Default is "0".
+
+.SH "CREDITS & COPYRIGHTS"
+Copyright (C) 2006 \- 2017 P. Beckingham, F. Hernandez.
+
+This man page was originally written by Federico Hernandez.
+
+Tasksh is distributed under the MIT license. See
+http://www.opensource.org/licenses/mit-license.php for more information.
+
+.SH SEE ALSO
+.BR task(1),
+
+For more information regarding tasksh, see the following:
+
+.TP
+The official site at
+<http://taskwarrior.org/tools>
+
+.TP
+The official code repository at
+<https://git.tasktools.org/scm/ex/tasksh.git>
+
+.TP
+You can contact the project by emailing
+<support@tasktools.org>
+
+.SH REPORTING BUGS
+.TP
+Bugs in tasksh may be reported to the issue-tracker at
+<http://bug.tasktools.org>
+
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
new file mode 100644 (file)
index 0000000..787640d
--- /dev/null
@@ -0,0 +1,34 @@
+cmake_minimum_required (VERSION 2.8)
+include_directories (${CMAKE_SOURCE_DIR}
+                     ${CMAKE_SOURCE_DIR}/src
+                     ${CMAKE_SOURCE_DIR}/src/libshared/src
+                     ${TASKSH_INCLUDE_DIRS})
+
+set (tasksh_SRCS diag.cpp
+                 help.cpp
+                 prompt.cpp
+                 review.cpp
+                 shell.cpp)
+
+set (libshared_SRCS libshared/src/Color.cpp         libshared/src/Color.h
+                    libshared/src/Datetime.cpp      libshared/src/Datetime.h
+                    libshared/src/Duration.cpp      libshared/src/Duration.h
+                    libshared/src/FS.cpp            libshared/src/FS.h
+                    libshared/src/Lexer.cpp         libshared/src/Lexer.h
+                    libshared/src/Pig.cpp           libshared/src/Pig.h
+                    libshared/src/shared.cpp        libshared/src/shared.h
+                    libshared/src/format.cpp        libshared/src/format.h
+                    libshared/src/unicode.cpp       libshared/src/unicode.h
+                    libshared/src/utf8.cpp          libshared/src/utf8.h
+                    libshared/src/wcwidth6.cpp)
+
+add_library (tasksh    STATIC ${tasksh_SRCS})
+add_library (libshared STATIC ${libshared_SRCS})
+add_executable (tasksh_executable main.cpp)
+
+target_link_libraries (tasksh_executable tasksh libshared ${TASKSH_LIBRARIES})
+
+set_property (TARGET tasksh_executable PROPERTY OUTPUT_NAME "tasksh")
+
+install (TARGETS tasksh_executable DESTINATION ${TASKSH_BINDIR})
+
diff --git a/src/diag.cpp b/src/diag.cpp
new file mode 100644 (file)
index 0000000..8478fba
--- /dev/null
@@ -0,0 +1,163 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Copyright 2006 - 2017, Paul Beckingham, Federico Hernandez.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+// http://www.opensource.org/licenses/mit-license.php
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include <cmake.h>
+#include <commit.h>
+#include <iostream>
+#include <string>
+#include <vector>
+#include <cstring>
+#include <stdlib.h>
+#include <FS.h>
+#include <Color.h>
+#include <shared.h>
+#include <format.h>
+
+#ifdef HAVE_READLINE
+#include <readline/readline.h>
+#include <readline/history.h>
+#endif
+
+////////////////////////////////////////////////////////////////////////////////
+int cmdDiagnostics ()
+{
+  Color bold ("bold");
+
+  std::cout << "\n"
+            << bold.colorize (PACKAGE_STRING)
+            << "\n"
+            << "   " << "Platform: " << osName ()
+            << "\n\n";
+
+  // Compiler.
+  std::cout << bold.colorize ("Compiler")
+            << "\n"
+#ifdef __VERSION__
+            << "    " << "Version: "
+            << __VERSION__ << "\n"
+#endif
+            << "       " << "Caps:"
+#ifdef __STDC__
+            << " +stdc"
+#endif
+#ifdef __STDC_HOSTED__
+            << " +stdc_hosted"
+#endif
+#ifdef __STDC_VERSION__
+            << " +" << __STDC_VERSION__
+#endif
+#ifdef _POSIX_VERSION
+            << " +" << _POSIX_VERSION
+#endif
+#ifdef _POSIX2_C_VERSION
+            << " +" << _POSIX2_C_VERSION
+#endif
+#ifdef _ILP32
+            << " +ILP32"
+#endif
+#ifdef _LP64
+            << " +LP64"
+#endif
+            << " +c"      << 8 * sizeof (char)
+            << " +i"      << 8 * sizeof (int)
+            << " +l"      << 8 * sizeof (long)
+            << " +vp"     << 8 * sizeof (void*)
+            << " +time_t" << 8 * sizeof (time_t)
+            << "\n";
+
+  // Compiler compliance level.
+  std::cout << " Compliance: "
+            << cppCompliance ()
+            << "\n\n";
+
+  std::cout << bold.colorize ("Build Features")
+            << "\n"
+
+  // Build date.
+            << "      " << "Built: " << __DATE__ << " " << __TIME__ << "\n"
+#ifdef HAVE_COMMIT
+            << "     " << "Commit: " << COMMIT << "\n"
+#endif
+            << "      CMake: " << CMAKE_VERSION << "\n";
+
+  std::cout << "libreadline: "
+#ifdef HAVE_READLINE
+#ifdef RL_VERSION_MAJOR
+            << RL_VERSION_MAJOR << "." << RL_VERSION_MINOR
+#elif defined RL_READLINE_VERSION
+            << "0x" << std::hex << RL_READLINE_VERSION
+#endif
+#else
+            << "n/a"
+#endif
+            << "\n";
+
+  std::cout << " Build type: "
+#ifdef CMAKE_BUILD_TYPE
+            << CMAKE_BUILD_TYPE
+#else
+            << "-"
+#endif
+            << "\n\n";
+
+  std::cout << bold.colorize ("Configuration")
+            << "\n";
+
+  auto env = getenv ("TASKRC");
+  std::cout << "     TASKRC: "
+            << (env ? env : "")
+            << "\n";
+
+  env = getenv ("TASKDATA");
+  std::cout << "   TASKDATA: "
+            << (env ? env : "")
+            << "\n";
+
+  // Taskwarrior version + location
+  std::string path (getenv ("PATH"));
+  std::cout << "       PATH: " << path << "\n";
+
+  for (const auto& i : split (path, ':'))
+  {
+    File task (i + "/task");
+    if (task.exists ())
+    {
+      std::string input;
+      std::string output;
+      execute ("task", {"--version"}, input, output);
+
+      std::cout << "Taskwarrior: "
+                << i
+                << "/task "
+                << output; // Still has \n
+    }
+  }
+
+  std::cout << "\n";
+  return 0;
+}
+
+////////////////////////////////////////////////////////////////////////////////
diff --git a/src/help.cpp b/src/help.cpp
new file mode 100644 (file)
index 0000000..2af1a01
--- /dev/null
@@ -0,0 +1,48 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Copyright 2006 - 2017, Paul Beckingham, Federico Hernandez.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+// http://www.opensource.org/licenses/mit-license.php
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include <cmake.h>
+#include <iostream>
+
+////////////////////////////////////////////////////////////////////////////////
+int cmdHelp ()
+{
+  std::cout << '\n'
+            << "  Commands:\n"
+            << "    tasksh> list             Or any other Taskwarrior command\n"
+            << "    tasksh> review [N]       Task review session, with optional cutoff after N tasks\n"
+            << "    tasksh> exec ls -al      Any shell command.  May also use '!ls -al'\n"
+            << "    tasksh> help             Tasksh help\n"
+            << "    tasksh> diagnostics      Tasksh diagnostics\n"
+            << "    tasksh> quit             End of session. May also use 'exit'\n"
+            << '\n'
+            << "Run 'man tasksh' from your shell prompt.\n"
+            << "Run '! man tasksh' from inside tasksh.\n"
+            << '\n';
+  return 0;
+}
+
+////////////////////////////////////////////////////////////////////////////////
diff --git a/src/liblibshared.a b/src/liblibshared.a
new file mode 100644 (file)
index 0000000..fe9bdc8
Binary files /dev/null and b/src/liblibshared.a differ
diff --git a/src/libshared/AUTHORS b/src/libshared/AUTHORS
new file mode 100644 (file)
index 0000000..8aab4e2
--- /dev/null
@@ -0,0 +1,21 @@
+The development of libshared was made possible by the significant contributions
+of the following people:
+
+  Paul Beckingham    (Principal Author)
+  Federico Hernandez (Principal Author)
+
+The following submitted code, packages or analysis, and deserve special thanks:
+
+  Lynoure Braakman
+  Jörg Krause
+  Ben Boeckel
+  Iain R. Learmonth
+  Toyam Cox
+
+Thanks to the following, who submitted detailed bug reports and excellent
+suggestions:
+
+  Sunil Joshi
+  Ellington Santos
+  Yury Vidineev
+  hosaka
diff --git a/src/libshared/CMakeLists.txt b/src/libshared/CMakeLists.txt
new file mode 100644 (file)
index 0000000..c1fccb2
--- /dev/null
@@ -0,0 +1,28 @@
+cmake_minimum_required (VERSION 2.8)
+set (CMAKE_LEGACY_CYGWIN_WIN32 0) # Remove when CMake >= 2.8.4 is required
+set (CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake")
+set (HAVE_CMAKE true)
+
+project (shared)
+include (CXXSniffer)
+
+set (PROJECT_VERSION "1.0.0")
+
+set (PACKAGE "${PROJECT_NAME}")
+set (VERSION "${PROJECT_VERSION}")
+set (PACKAGE_BUGREPORT "support@taskwarrior.org")
+set (PACKAGE_NAME "${PACKAGE}")
+set (PACKAGE_TARNAME "${PACKAGE}")
+set (PACKAGE_VERSION "${VERSION}")
+set (PACKAGE_STRING "${PACKAGE} ${VERSION}")
+
+message ("-- Configuring cmake.h")
+configure_file (
+  ${CMAKE_SOURCE_DIR}/cmake.h.in
+  ${CMAKE_SOURCE_DIR}/cmake.h)
+
+add_subdirectory (src)
+if (EXISTS ${CMAKE_SOURCE_DIR}/test)
+  add_subdirectory (test EXCLUDE_FROM_ALL)
+endif (EXISTS ${CMAKE_SOURCE_DIR}/test)
+
diff --git a/src/libshared/ChangeLog b/src/libshared/ChangeLog
new file mode 100644 (file)
index 0000000..12ab900
--- /dev/null
@@ -0,0 +1,89 @@
+master/HEAD
+
+- TI-53 Fix musl-libc compatibility
+          (thanks to Toyam Cox).
+- Define PATH_MAX if it's not defined
+          (thanks to Iain R. Learmonth).
+- Removed 'std::' from stdtoimax call.
+          (thanks to fornwall).
+- When Lexer::noOperator () is called, prevent ::isWord boundaries from being
+  comprised of operators.
+- Added Pig::getCharacter, which was oddly missing.
+- Added 'Tree' class.
+- Added FS error handling for POSIX call failure.
+- Updated Timer class to use std::chrono.
+- Args now tolerates undeclared option queries.
+- Added case (in)sentitive find functions.
+- Pig no longer makes a copy of the input string.
+- Fixed bug where Pig::getUntil included the terminator if it was the last
+  character.
+- Table now uses the correct include latch.
+- Migrated obfuscateText from Taskwarrior.
+- Added unicodeHorizontalWhitespace and unicodeVerticalWhitespace.
+- Added unicodePunctuation.
+- Added unicodeAlpha.
+- Added osName.
+- Duration::formatVague can now pad all values to the same length.
+- Combined JSON.h, JSON2.h, eliminated duplicate encode/decode implementations.
+- Table::addRow{,Odd,Even} allows a user-specified notion of 'odd' row.
+- Added isIPv4Address and isIPv6Address.
+- Added PEG parser.
+- Added Packrat parser.
+- Datetime/Duration can now parse dates from an embedded string, with negative
+  lookahead.
+- Table supports unwrapped columns.
+- Table supports colored columns.
+
+tasksh-1.1.0 (2016-09-05)
+anomaly-1.1.0 (2016-09-04)
+
+- TD-120  Missing cmakedefine for HAVE_GET_CURRENT_DIR_NAME
+          (Thanks to Jörg Krause, Ben Boeckel).
+- TW-1845 Cygwin build fails, missing get_current_dir_name
+          (thanks to hosaka).
+- Lexer can now disable individual token types.
+- Pig is more careful about string bounds in ::peek.
+- Pig can extract substrings.
+- FS now has strict error handling, requiring that file existence is checked before
+  readability.
+
+timew-1.0.0 (2016-08-17)
+
+- TI-30   10:00am isn't recognized as date
+          (thanks to Yurї Videneev).
+- Datetime::weekStart set to 1 (Monday), per ISO-8601.
+- Datetime no longer users 23:59:59 as EOD, but 24:00:00. All date ranges should
+  therefore be [...) instead of [...].
+- Datetime now uses whole days, not 86400 seconds for calculating date offsets.
+- Datetime now properly calculates day names when looking backwards.
+- Datetime considerѕ forwards/backwards when calculating informal time.
+
+clog-1.3.0 (2016-06-27)
+
+- TW-1741 Warning "ignoring return value of ‘int ftruncate" while doing make on
+          xubuntu15.10
+          (thanks to Sunil Joshi).
+- TW-1807 dateformat lacks a flag to display day of week
+          (thanks to Ellington Santos).
+- Bug   '12pm' was getting 12 hours added because of the 'pm', which is wrong.
+- Added 'juhannus' as a synonym for 'midsommarafton'
+          (thanks to Lynoure Braakman).
+- Added 'join' function.
+- Added 'str_replace' function.
+- Added Datetime support for informal time, '8am', '2:30p'.
+- Added 'JSON2' SAX parser.
+- Introduced the new shared submodule.
+- Added Datetime support for 'socq', 'eocq', 'socy', 'eocy'.
+- Added Composite object.
+- Added Palette object.
+- Added Lexer object.
+- Added Msg::set overload.
+
+Design completed 2015-12-XX
+Project started  2015-11-29
+
+------ current release ---------------------------
+
+Note: There are no releases. There are tags applied when a project is released.
+
+------ start -----------------------------------
diff --git a/src/libshared/LICENSE b/src/libshared/LICENSE
new file mode 100644 (file)
index 0000000..4cd3104
--- /dev/null
@@ -0,0 +1,23 @@
+libshared
+
+Copyright 2015 - 2017, Paul Beckingham, Federico Hernandez.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+http://www.opensource.org/licenses/mit-license.php
diff --git a/src/libshared/cmake.h.in b/src/libshared/cmake.h.in
new file mode 100644 (file)
index 0000000..92d4b9b
--- /dev/null
@@ -0,0 +1,36 @@
+/* cmake.h.in. Creates cmake.h during a cmake run */
+
+/* Package information */
+#define PACKAGE           "${PACKAGE}"
+#define VERSION           "${VERSION}"
+#define PACKAGE_BUGREPORT "${PACKAGE_BUGREPORT}"
+#define PACKAGE_NAME      "${PACKAGE_NAME}"
+#define PACKAGE_TARNAME   "${PACKAGE_TARNAME}"
+#define PACKAGE_VERSION   "${PACKAGE_VERSION}"
+#define PACKAGE_STRING    "${PACKAGE_STRING}"
+
+#define CMAKE_BUILD_TYPE  "${CMAKE_BUILD_TYPE}"
+
+/* Compiling platform */
+#cmakedefine LINUX
+#cmakedefine DARWIN
+#cmakedefine CYGWIN
+#cmakedefine FREEBSD
+#cmakedefine OPENBSD
+#cmakedefine NETBSD
+#cmakedefine SOLARIS
+#cmakedefine KFREEBSD
+#cmakedefine GNUHURD
+#cmakedefine UNKNOWN
+
+/* Found tm.tm_gmtoff struct member */
+#cmakedefine HAVE_TM_GMTOFF
+
+/* Found st.st_birthtime struct member */
+#cmakedefine HAVE_ST_BIRTHTIME
+
+/* Functions */
+#cmakedefine HAVE_GET_CURRENT_DIR_NAME
+#cmakedefine HAVE_TIMEGM
+#cmakedefine HAVE_UUID_UNPARSE_LOWER
+
diff --git a/src/libshared/cmake/CXXSniffer.cmake b/src/libshared/cmake/CXXSniffer.cmake
new file mode 100644 (file)
index 0000000..9606226
--- /dev/null
@@ -0,0 +1,51 @@
+message ("-- Configuring C++11")
+message ("-- System: ${CMAKE_SYSTEM_NAME}")
+
+include (CheckCXXCompilerFlag)
+
+# NOTE: Phase out -std=gnu++0x and --std=c++0x as soon as realistically possible.
+CHECK_CXX_COMPILER_FLAG("-std=c++11"   _HAS_CXX11)
+CHECK_CXX_COMPILER_FLAG("-std=c++0x"   _HAS_CXX0X)
+CHECK_CXX_COMPILER_FLAG("-std=gnu++0x" _HAS_GNU0X)
+
+if (_HAS_CXX11)
+  set (_CXX11_FLAGS "-std=c++11")
+elseif (_HAS_CXX0X)
+  message (WARNING "Enabling -std=c++0x draft compile flag. Your compiler does not support the standard '-std=c++11' option.  Consider upgrading.")
+  set (_CXX11_FLAGS "-std=c++0x")
+elseif (_HAS_GNU0X)
+  message (WARNING "Enabling -std=gnu++0x draft compile flag. Your compiler does not support the standard '-std=c++11' option. Consider upgrading.")
+  set (_CXX11_FLAGS "-std=gnu++0x")
+else (_HAS_CXX11)
+ message (FATAL_ERROR "C++11 support missing. Try upgrading your C++ compiler. If you have a good reason for using an outdated compiler, please let us know at support@taskwarrior.org.")
+endif (_HAS_CXX11)
+
+if (${CMAKE_SYSTEM_NAME} MATCHES "Linux")
+  set (LINUX true)
+elseif (${CMAKE_SYSTEM_NAME} MATCHES "Darwin")
+  set (DARWIN true)
+  set (_CXX11_FLAGS "${_CXX11_FLAGS} -stdlib=libc++")
+elseif (${CMAKE_SYSTEM_NAME} MATCHES "kFreeBSD")
+  set (KFREEBSD true)
+elseif (${CMAKE_SYSTEM_NAME} MATCHES "FreeBSD")
+  set (FREEBSD true)
+elseif (${CMAKE_SYSTEM_NAME} MATCHES "OpenBSD")
+  set (OPENBSD true)
+elseif (${CMAKE_SYSTEM_NAME} MATCHES "NetBSD")
+  set (NETBSD true)
+elseif (${CMAKE_SYSTEM_NAME} MATCHES "SunOS")
+  set (SOLARIS true)
+elseif (${CMAKE_SYSTEM_NAME} STREQUAL "GNU")
+  set (GNUHURD true)
+elseif (${CMAKE_SYSTEM_NAME} STREQUAL "CYGWIN")
+  set (CYGWIN true)
+  # NOTE: Not setting -std=gnu++0x leads to compile errors even with
+  #       GCC 4.8.3, and debugging those leads to insanity. Adding this
+  #       workaround instead of fixing Cygwin.
+  set (_CXX11_FLAGS "-std=gnu++0x")
+else (${CMAKE_SYSTEM_NAME} MATCHES "Linux")
+  set (UNKNOWN true)
+endif (${CMAKE_SYSTEM_NAME} MATCHES "Linux")
+
+set (CMAKE_CXX_FLAGS "${_CXX11_FLAGS} ${CMAKE_CXX_FLAGS}")
+set (CMAKE_CXX_FLAGS "-Wall -Wextra -Wsign-compare -Wreturn-type ${CMAKE_CXX_FLAGS}")
diff --git a/src/libshared/src/Args.cpp b/src/libshared/src/Args.cpp
new file mode 100644 (file)
index 0000000..0c396f0
--- /dev/null
@@ -0,0 +1,246 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Copyright 2012 - 2017, Paul Beckingham, Federico Hernandez.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+// http://www.opensource.org/licenses/mit-license.php
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include <cmake.h>
+#include <Args.h>
+#include <shared.h>
+#include <sstream>
+#include <string.h>
+
+////////////////////////////////////////////////////////////////////////////////
+void Args::addOption (const std::string& name, bool defaultValue)
+{
+  _options[name] = defaultValue;
+  _optionCount[name] = 0;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+void Args::addNamed (const std::string& name, const std::string& defaultValue)
+{
+  _named[name] = defaultValue;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+void Args::limitPositionals (int limit)
+{
+  _limit = limit;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+void Args::enableNegatives ()
+{
+  _negatives = true;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+void Args::scan (int argc, const char** argv)
+{
+  for (int i = 1; i < argc; ++i)
+  {
+    // Is an option or named arg.
+    if (argv[i][0] == '-' && strlen (argv[i]) > 1)
+    {
+      auto name = ltrim (argv[i], "-");
+
+      std::string canonical;
+      if (canonicalizeOption (name, canonical))
+      {
+        bool negated = _negatives && name.find ("no") == 0;
+        _options[canonical] = ! negated;
+        _optionCount[canonical]++;
+      }
+
+      else if (canonicalizeNamed (name, canonical))
+      {
+        if (i >= argc)
+          throw std::string ("Argument '" + canonical + "' has no value.");
+
+        ++i;
+        _named[canonical] = argv[i];
+      }
+
+      else
+        throw std::string ("Unrecognized argument '" + name + "'.");
+    }
+
+    // Or a positional.
+    else
+    {
+      _positionals.push_back (argv[i]);
+      if (_limit != -1 &&
+          static_cast <int> (_positionals.size ()) > _limit)
+        throw std::string ("Too many positional arguments.");
+    }
+  }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool Args::getOption (const std::string& name) const
+{
+  if (_options.find (name) == _options.end ())
+    return false;
+
+  return _options.at (name);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+int Args::getOptionCount (const std::string& name) const
+{
+  if (_optionCount.find (name) == _optionCount.end ())
+    return false;
+
+  return _optionCount.at (name);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+std::string Args::getNamed (const std::string& name) const
+{
+  if (_named.find (name) == _named.end ())
+    return "";
+
+  return _named.at (name);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+int Args::getPositionalCount () const
+{
+  return static_cast <int> (_positionals.size ());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+std::string Args::getPositional (int n) const
+{
+  return _positionals.at (n);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Assuming "abc" is a declared option, support the following canonicalization:
+//
+//   abc --> abc (exact match always canonicalizes)
+//    ab --> abc (if unique)
+//     a --> abc (if unique)
+// noabc --> abc (exact negation match always canonicalizes)
+//  noab --> abc (if unique)
+//   noa --> abc (if unique)
+//
+bool Args::canonicalizeOption (const std::string& partial, std::string& canonical) const
+{
+  bool negated = _negatives && partial.find ("no") == 0;
+
+  // Look for exact positive or negative matches first, which should succeed
+  // regardless of any longer partial matches.
+  if (_options.find (partial) != _options.end ())
+  {
+    canonical = partial;
+    return true;
+  }
+
+  if (negated &&
+      _options.find (partial.substr (2)) != _options.end ())
+  {
+    canonical = partial.substr (2);
+    return true;
+  }
+
+  // Iterate over all options, and look for partial matches.  If there is only
+  // one, we have canonicalization.
+  std::vector <std::string> candidates;
+  for (const auto& option : _options)
+  {
+    if (option.first.find (partial) == 0 ||
+        (negated && option.first.find (partial, 2) == 2))
+    {
+      candidates.push_back (option.first);
+    }
+  }
+
+  if (candidates.size () == 1)
+  {
+    canonical = candidates[0];
+    return true;
+  }
+
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Assuming "abc" is a declared name, support the following canonicalization:
+//
+//   abc --> abc (exact match always canonicalizes)
+//    ab --> abc (if unique)
+//     a --> abc (if unique)
+//
+bool Args::canonicalizeNamed (const std::string& partial, std::string& canonical) const
+{
+  // Look for exact positive or negative matches first, which should succeed
+  // regardless of longer partial matches.
+  if (_named.find (partial) != _named.end ())
+  {
+    canonical = partial;
+    return true;
+  }
+
+  // Iterate over all options, and look for partial matches.  If there is only
+  // one, we have canonicalization.
+  std::vector <std::string> candidates;
+  for (const auto& name : _named)
+    if (name.first.find (partial) == 0)
+      candidates.push_back (name.first);
+
+  if (candidates.size () == 1)
+  {
+    canonical = candidates[0];
+    return true;
+  }
+
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+std::string Args::dump () const
+{
+  std::stringstream out;
+  out << "Args\n"
+      << "  Options\n";
+  for (const auto& arg : _options)
+    out << "    " << arg.first << " = " << arg.second << " (" << _optionCount.at (arg.first) << ")\n";
+
+  out << "  Named\n";
+  for (const auto& arg : _named)
+    out << "    " << arg.first << " = " << arg.second << '\n';
+
+  out << "  Positionals\n"
+      << "    limit = " << _limit << '\n';
+  for (const auto& arg : _positionals)
+    out << "    " << arg << '\n';
+
+  out << "  Negatives\n"
+      << "    enabled = " << _negatives << '\n';
+
+  return out.str ();
+}
+
+////////////////////////////////////////////////////////////////////////////////
diff --git a/src/libshared/src/Args.h b/src/libshared/src/Args.h
new file mode 100644 (file)
index 0000000..84a0070
--- /dev/null
@@ -0,0 +1,68 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Copyright 2012 - 2017, Paul Beckingham, Federico Hernandez.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+// http://www.opensource.org/licenses/mit-license.php
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#ifndef INCLUDED_ARGS
+#define INCLUDED_ARGS
+
+#include <string>
+#include <vector>
+#include <map>
+
+class Args
+{
+public:
+  Args () = default;
+
+  void addOption (const std::string&, bool defaultValue = true);
+  void addNamed  (const std::string&, const std::string& defaultValue = "");
+  void limitPositionals (int);
+  void enableNegatives ();
+
+  void scan (int, const char**);
+
+  bool getOption (const std::string&) const;
+  int getOptionCount (const std::string&) const;
+  std::string getNamed (const std::string&) const;
+  int getPositionalCount () const;
+  std::string getPositional (int) const;
+
+  std::string dump () const;
+
+private:
+  bool canonicalizeOption (const std::string&, std::string&) const;
+  bool canonicalizeNamed (const std::string&, std::string&) const;
+
+private:
+  std::map <std::string, bool>        _options     {};
+  std::map <std::string, int>         _optionCount {};
+  std::map <std::string, std::string> _named       {};
+  std::vector <std::string>           _positionals {};
+  int                                 _limit       {-1};
+  bool                                _negatives   {false};
+};
+
+#endif
+
diff --git a/src/libshared/src/CMakeLists.txt b/src/libshared/src/CMakeLists.txt
new file mode 100644 (file)
index 0000000..8b3ec70
--- /dev/null
@@ -0,0 +1,65 @@
+cmake_minimum_required (VERSION 2.8)
+include_directories (${CMAKE_SOURCE_DIR}
+                     ${CMAKE_SOURCE_DIR}/src)
+
+set (shared_HEADERS Args.h
+                    Color.h
+                    Composite.h
+                    Configuration.h
+                    Datetime.h
+                    Duration.h
+                    FS.h
+                    JSON.h
+                    Lexer.h
+                    Log.h
+                    Msg.h
+                    Packrat.h
+                    Palette.h
+                    PEG.h
+                    Pig.h
+                    RX.h
+                    Table.h
+                    Timer.h
+                    Tree.h
+                    shared.h
+                    format.h
+                    unicode.h
+                    utf8.h)
+
+set (shared_SRCS Args.cpp
+                 Color.cpp
+                 Composite.cpp
+                 Configuration.cpp
+                 Datetime.cpp
+                 Duration.cpp
+                 FS.cpp
+                 JSON.cpp
+                 Lexer.cpp
+                 Log.cpp
+                 Msg.cpp
+                 Packrat.cpp
+                 Palette.cpp
+                 PEG.cpp
+                 Pig.cpp
+                 RX.cpp
+                 SAX.cpp
+                 Table.cpp
+                 Timer.cpp
+                 Tree.cpp
+                 format.cpp
+                 ip.cpp
+                 shared.cpp
+                 unicode.cpp
+                 utf8.cpp
+                 wcwidth6.cpp
+                 ${shared_HEADERS})
+
+add_library (shared STATIC ${shared_SRCS})
+
+set (CMAKE_INSTALL_LIBDIR lib CACHE PATH "Output directory for libraries")
+install (TARGETS shared DESTINATION lib)
+install (FILES ${shared_HEADERS} DESTINATION include)
+
+add_executable (lex_executable lex.cpp)
+target_link_libraries (lex_executable shared)
+set_property (TARGET lex_executable PROPERTY OUTPUT_NAME "lex")
diff --git a/src/libshared/src/Color.cpp b/src/libshared/src/Color.cpp
new file mode 100644 (file)
index 0000000..0673d81
--- /dev/null
@@ -0,0 +1,682 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Copyright 2006 - 2017, Paul Beckingham, Federico Hernandez.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+// http://www.opensource.org/licenses/mit-license.php
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include <cmake.h>
+#include <Color.h>
+#include <sstream>
+#include <vector>
+#include <cstdlib>
+#include <shared.h>
+#include <format.h>
+
+// uint to string lookup table for Color::_colorize()
+// _colorize() gets called _a lot_, having this lookup table is a cheap
+// performance optimization.
+const char *colorstring[] = {
+    "0",   "1",   "2",   "3",   "4",   "5",   "6",   "7",   "8",   "9",
+   "10",  "11",  "12",  "13",  "14",  "15",  "16",  "17",  "18",  "19",
+   "20",  "21",  "22",  "23",  "24",  "25",  "26",  "27",  "28",  "29",
+   "30",  "31",  "32",  "33",  "34",  "35",  "36",  "37",  "38",  "39",
+   "40",  "41",  "42",  "43",  "44",  "45",  "46",  "47",  "48",  "49",
+   "50",  "51",  "52",  "53",  "54",  "55",  "56",  "57",  "58",  "59",
+   "60",  "61",  "62",  "63",  "64",  "65",  "66",  "67",  "68",  "69",
+   "70",  "71",  "72",  "73",  "74",  "75",  "76",  "77",  "78",  "79",
+   "80",  "81",  "82",  "83",  "84",  "85",  "86",  "87",  "88",  "89",
+   "90",  "91",  "92",  "93",  "94",  "95",  "96",  "97",  "98",  "99",
+  "100", "101", "102", "103", "104", "105", "106", "107", "108", "109",
+  "110", "111", "112", "113", "114", "115", "116", "117", "118", "119",
+  "120", "121", "122", "123", "124", "125", "126", "127", "128", "129",
+  "130", "131", "132", "133", "134", "135", "136", "137", "138", "139",
+  "140", "141", "142", "143", "144", "145", "146", "147", "148", "149",
+  "150", "151", "152", "153", "154", "155", "156", "157", "158", "159",
+  "160", "161", "162", "163", "164", "165", "166", "167", "168", "169",
+  "170", "171", "172", "173", "174", "175", "176", "177", "178", "179",
+  "180", "181", "182", "183", "184", "185", "186", "187", "188", "189",
+  "190", "191", "192", "193", "194", "195", "196", "197", "198", "199",
+  "200", "201", "202", "203", "204", "205", "206", "207", "208", "209",
+  "210", "211", "212", "213", "214", "215", "216", "217", "218", "219",
+  "220", "221", "222", "223", "224", "225", "226", "227", "228", "229",
+  "230", "231", "232", "233", "234", "235", "236", "237", "238", "239",
+  "240", "241", "242", "243", "244", "245", "246", "247", "248", "249",
+  "250", "251", "252", "253", "254", "255"
+};
+
+////////////////////////////////////////////////////////////////////////////////
+static struct
+{
+  Color::color_id id;
+  std::string english_name;
+  int index;                    // offset red=3 (therefore fg=33, bg=43)
+} allColors[] =
+{
+  // Color.h enum    English     Index
+  { Color::nocolor,  "none",     0},
+  { Color::black,    "black",    1}, // fg 29+0  bg 39+0
+  { Color::red,      "red",      2},
+  { Color::green,    "green",    3},
+  { Color::yellow,   "yellow",   4},
+  { Color::blue,     "blue",     5},
+  { Color::magenta,  "magenta",  6},
+  { Color::cyan,     "cyan",     7},
+  { Color::white,    "white",    8},
+
+};
+
+#define NUM_COLORS (sizeof (allColors) / sizeof (allColors[0]))
+
+////////////////////////////////////////////////////////////////////////////////
+Color::Color ()
+: _value (0)
+{
+}
+
+////////////////////////////////////////////////////////////////////////////////
+Color::Color (const Color& other)
+{
+  _value = other._value;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+Color::Color (unsigned int c)
+: _value (0)
+{
+  if (!(c & _COLOR_HASFG)) _value &= ~_COLOR_FG;
+  if (!(c & _COLOR_HASBG)) _value &= ~_COLOR_BG;
+
+  _value = c & (_COLOR_256 | _COLOR_HASBG | _COLOR_HASFG |_COLOR_UNDERLINE |
+                _COLOR_INVERSE | _COLOR_BOLD | _COLOR_BRIGHT | _COLOR_BG |
+                _COLOR_FG);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Supports the following constructs:
+//   [bright] [color] [on color] [bright] [underline]
+//
+// Where [color] is one of:
+//   black
+//   red
+//   ...
+//   grayN  0 <= N <= 23       fg 38;5;232 + N              bg 48;5;232 + N
+//   greyN  0 <= N <= 23       fg 38;5;232 + N              bg 48;5;232 + N
+//   colorN 0 <= N <= 255      fg 38;5;N                    bg 48;5;N
+//   rgbRGB 0 <= R,G,B <= 5    fg 38;5;16 + R*36 + G*6 + B  bg 48;5;16 + R*36 + G*6 + B
+Color::Color (const std::string& spec)
+: _value (0)
+{
+  // Split spec into words.
+  auto words = split (spec, ' ');
+
+  // Construct the color as two separate colors, then blend them later.  This
+  // make it possible to declare a color such as "color1 on black", and have
+  // the upgrade work properly.
+  unsigned int fg_value = 0;
+  unsigned int bg_value = 0;
+
+  bool bg = false;
+  int index;
+  for (auto& word : words)
+  {
+    word = lowerCase (trim (word));
+
+         if (word == "bold")      fg_value |= _COLOR_BOLD;
+    else if (word == "bright")    bg_value |= _COLOR_BRIGHT;
+    else if (word == "underline") fg_value |= _COLOR_UNDERLINE;
+    else if (word == "inverse")   fg_value |= _COLOR_INVERSE;
+    else if (word == "on")        bg = true;
+
+    // X where X is one of black, red, blue ...
+    else if ((index = find (word)) != -1)
+    {
+      if (index)
+      {
+        if (bg)
+        {
+          bg_value |= _COLOR_HASBG;
+          bg_value |= index << 8;
+        }
+        else
+        {
+          fg_value |= _COLOR_HASFG;
+          fg_value |= index;
+        }
+      }
+    }
+
+    // greyN/grayN, where 0 <= N <= 23.
+    else if (! word.compare (0, 4, "grey", 4) ||
+             ! word.compare (0, 4, "gray", 4))
+    {
+      index = strtol (word.substr (4).c_str (), nullptr, 10);
+      if (index < 0 || index > 23)
+        throw format ("The color '{1}' is not recognized.", word);
+
+      if (bg)
+      {
+        bg_value |= _COLOR_HASBG;
+        bg_value |= (index + 232) << 8;
+        bg_value |= _COLOR_256;
+      }
+      else
+      {
+        fg_value |= _COLOR_HASFG;
+        fg_value |= index + 232;
+        fg_value |= _COLOR_256;
+      }
+    }
+
+    // rgbRGB, where 0 <= R,G,B <= 5.
+    else if (! word.compare (0, 3, "rgb", 3))
+    {
+      index = strtol (word.substr (3).c_str (), nullptr, 10);
+      if (word.length () != 6 ||
+          index < 0 || index > 555)
+        throw format ("The color '{1}' is not recognized.", word);
+
+      int r = strtol (word.substr (3, 1).c_str (), nullptr, 10);
+      int g = strtol (word.substr (4, 1).c_str (), nullptr, 10);
+      int b = strtol (word.substr (5, 1).c_str (), nullptr, 10);
+      if (r < 0 || r > 5 ||
+          g < 0 || g > 5 ||
+          b < 0 || b > 5)
+        throw format ("The color '{1}' is not recognized.", word);
+
+      index = 16 + r*36 + g*6 + b;
+
+      if (bg)
+      {
+        bg_value |= _COLOR_HASBG;
+        bg_value |= index << 8;
+        bg_value |= _COLOR_256;
+      }
+      else
+      {
+        fg_value |= _COLOR_HASFG;
+        fg_value |= index;
+        fg_value |= _COLOR_256;
+      }
+    }
+
+    // colorN, where 0 <= N <= 255.
+    else if (! word.compare (0, 5, "color", 5))
+    {
+      index = strtol (word.substr (5).c_str (), nullptr, 10);
+      if (index < 0 || index > 255)
+        throw format ("The color '{1}' is not recognized.", word);
+
+      upgrade ();
+
+      if (bg)
+      {
+        bg_value |= _COLOR_HASBG;
+        bg_value |= index << 8;
+        bg_value |= _COLOR_256;
+      }
+      else
+      {
+        fg_value |= _COLOR_HASFG;
+        fg_value |= index;
+        fg_value |= _COLOR_256;
+      }
+    }
+    else if (word != "")
+      throw format ("The color '{1}' is not recognized.", word);
+  }
+
+  // Now combine the fg and bg into a single color.
+  _value = fg_value;
+  blend (Color (bg_value));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+Color::Color (color_id fg)
+: _value (0)
+{
+  if (fg != Color::nocolor)
+  {
+    _value |= _COLOR_HASFG;
+    _value |= fg;
+  }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+Color::Color (color_id fg, color_id bg, bool underline, bool bold, bool bright)
+: _value (0)
+{
+  _value |= ((underline ? 1 : 0) << 18)
+         |  ((bold      ? 1 : 0) << 17)
+         |  ((bright    ? 1 : 0) << 16);
+
+  if (bg != Color::nocolor)
+  {
+    _value |= _COLOR_HASBG;
+    _value |= (bg << 8);
+  }
+
+  if (fg != Color::nocolor)
+  {
+    _value |= _COLOR_HASFG;
+    _value |= fg;
+  }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+Color::operator std::string () const
+{
+  std::string description;
+  if (_value & _COLOR_BOLD) description += "bold";
+
+  if (_value & _COLOR_UNDERLINE)
+    description += std::string (description.length () ? " " : "") + "underline";
+
+  if (_value & _COLOR_INVERSE)
+    description += std::string (description.length () ? " " : "") + "inverse";
+
+  if (_value & _COLOR_HASFG)
+    description += std::string (description.length () ? " " : "") + fg ();
+
+  if (_value & _COLOR_HASBG)
+  {
+    description += std::string (description.length () ? " " : "") + "on";
+
+    if (_value & _COLOR_BRIGHT)
+      description += std::string (description.length () ? " " : "") + "bright";
+
+    description += " " + bg ();
+  }
+
+  return description;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+Color::operator int () const
+{
+  return (int) _value;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// If 'other' has styles that are compatible, merge them into this.  Colors in
+// other take precedence.
+void Color::blend (const Color& other)
+{
+  if (!other.nontrivial ())
+    return;
+
+  Color c (other);
+  _value |= (c._value & _COLOR_UNDERLINE);    // Always inherit underline.
+  _value |= (c._value & _COLOR_INVERSE);      // Always inherit inverse.
+
+  // 16 <-- 16.
+  if (!(_value   & _COLOR_256) &&
+      !(c._value & _COLOR_256))
+  {
+    _value |= (c._value & _COLOR_BOLD);       // Inherit bold.
+    _value |= (c._value & _COLOR_BRIGHT);     // Inherit bright.
+
+    if (c._value & _COLOR_HASFG)
+    {
+      _value |= _COLOR_HASFG;                 // There is now a color.
+      _value &= ~_COLOR_FG;                   // Remove previous color.
+      _value |= (c._value & _COLOR_FG);       // Apply other color.
+    }
+
+    if (c._value & _COLOR_HASBG)
+    {
+      _value |= _COLOR_HASBG;                 // There is now a color.
+      _value &= ~_COLOR_BG;                   // Remove previous color.
+      _value |= (c._value & _COLOR_BG);       // Apply other color.
+    }
+
+    return;
+  }
+  else
+  {
+    // Upgrade either color, if necessary.
+    if (!(_value   & _COLOR_256)) upgrade ();
+    if (!(c._value & _COLOR_256)) c.upgrade ();
+
+    // 256 <-- 256.
+    if (c._value & _COLOR_HASFG)
+    {
+      _value |= _COLOR_HASFG;                  // There is now a color.
+      _value &= ~_COLOR_FG;                    // Remove previous color.
+      _value |= (c._value & _COLOR_FG);        // Apply other color.
+    }
+
+    if (c._value & _COLOR_HASBG)
+    {
+      _value |= _COLOR_HASBG;                  // There is now a color.
+      _value &= ~_COLOR_BG;                    // Remove previous color.
+      _value |= (c._value & _COLOR_BG);        // Apply other color.
+    }
+  }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+void Color::upgrade ()
+{
+  if (!(_value & _COLOR_256))
+  {
+    if (_value & _COLOR_HASFG)
+    {
+      bool bold = _value & _COLOR_BOLD;
+      unsigned int fg = _value & _COLOR_FG;
+      _value &= ~_COLOR_FG;
+      _value &= ~_COLOR_BOLD;
+      _value |= (bold ? fg + 7 : fg - 1);
+    }
+
+    if (_value & _COLOR_HASBG)
+    {
+      bool bright = _value & _COLOR_BRIGHT;
+      unsigned int bg = (_value & _COLOR_BG) >> 8;
+      _value &= ~_COLOR_BG;
+      _value &= ~_COLOR_BRIGHT;
+      _value |= (bright ? bg + 7 : bg - 1) << 8;
+    }
+
+    _value |= _COLOR_256;
+  }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+std::string Color::colorize (const std::string& input) const
+{
+  std::string result;
+  _colorize (result, input);
+  return result;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Sample color codes:
+//   red                  \033[31m
+//   bold red             \033[91m
+//   underline red        \033[4;31m
+//   bold underline red   \033[1;4;31m
+//
+//   on red               \033[41m
+//   on bright red        \033[101m
+//
+//   256 fg               \033[38;5;Nm
+//   256 bg               \033[48;5;Nm
+void Color::_colorize (std::string &result, const std::string& input) const
+{
+  if (!nontrivial ())
+  {
+    result += input;
+    return;
+  }
+
+  int count = 0;
+
+  // 256 color
+  if (_value & _COLOR_256)
+  {
+    if (_value & _COLOR_UNDERLINE)
+      result += "\033[4m";
+
+    if (_value & _COLOR_INVERSE)
+      result += "\033[7m";
+
+    if (_value & _COLOR_HASFG)
+    {
+      result += "\033[38;5;";
+      result += colorstring[(_value & _COLOR_FG)];
+      result += 'm';
+    }
+
+    if (_value & _COLOR_HASBG)
+    {
+      result += "\033[48;5;";
+      result += colorstring[((_value & _COLOR_BG) >> 8)];
+      result += 'm';
+    }
+
+    result += input;
+    result += "\033[0m";
+  }
+
+  // 16 color
+  else
+  {
+    result += "\033[";
+
+    if (_value & _COLOR_BOLD)
+    {
+      if (count++) result += ';';
+      result += '1';
+    }
+
+    if (_value & _COLOR_UNDERLINE)
+    {
+      if (count++) result += ';';
+      result += '4';
+    }
+
+    if (_value & _COLOR_INVERSE)
+    {
+      if (count++) result += ';';
+      result += '7';
+    }
+
+    if (_value & _COLOR_HASFG)
+    {
+      if (count++) result += ';';
+      result += colorstring[(29 + (_value & _COLOR_FG))];
+    }
+
+    if (_value & _COLOR_HASBG)
+    {
+      if (count++) result += ';';
+      result += colorstring[((_value & _COLOR_BRIGHT ? 99 : 39) + ((_value & _COLOR_BG) >> 8))];
+    }
+
+    result += 'm';
+    result += input;
+    result += "\033[0m";
+  }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Remove color codes from a string.
+std::string Color::strip (const std::string& input)
+{
+  int length = input.length ();
+  bool inside = false;
+  std::string output;
+  for (int i = 0; i < length; ++i)
+  {
+    if (inside)
+    {
+      if (input[i] == 'm')
+        inside = false;
+    }
+    else
+    {
+      if (input[i] == 033)
+        inside = true;
+      else
+        output += input[i];
+    }
+  }
+
+  return output;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+std::string Color::colorize (const std::string& input, const std::string& spec)
+{
+  Color c (spec);
+  return c.colorize (input);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+std::string Color::code () const
+{
+  if (! nontrivial ())
+    return "";
+
+  std::string result;
+
+  // 256 color
+  if (_value & _COLOR_256)
+  {
+    if (_value & _COLOR_UNDERLINE)
+      result += "\033[4m";
+
+    if (_value & _COLOR_INVERSE)
+      result += "\033[7m";
+
+    if (_value & _COLOR_HASFG)
+    {
+      result += "\033[38;5;";
+      result += colorstring[(_value & _COLOR_FG)];
+      result += 'm';
+    }
+
+    if (_value & _COLOR_HASBG)
+    {
+      result += "\033[48;5;";
+      result += colorstring[((_value & _COLOR_BG) >> 8)];
+      result += 'm';
+    }
+  }
+
+  // 16 color
+  else
+  {
+    int count = 0;
+    result += "\033[";
+
+    if (_value & _COLOR_BOLD)
+    {
+      if (count++) result += ';';
+      result += '1';
+    }
+
+    if (_value & _COLOR_UNDERLINE)
+    {
+      if (count++) result += ';';
+      result += '4';
+    }
+
+    if (_value & _COLOR_INVERSE)
+    {
+      if (count++) result += ';';
+      result += '7';
+    }
+
+    if (_value & _COLOR_HASFG)
+    {
+      if (count++) result += ';';
+      result += colorstring[(29 + (_value & _COLOR_FG))];
+    }
+
+    if (_value & _COLOR_HASBG)
+    {
+      if (count++) result += ';';
+      result += colorstring[((_value & _COLOR_BRIGHT ? 99 : 39) + ((_value & _COLOR_BG) >> 8))];
+    }
+
+    result += 'm';
+  }
+
+  return result;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+std::string Color::end () const
+{
+  if (nontrivial ())
+    return "\033[0m";
+
+  return "";
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool Color::nontrivial () const
+{
+  return _value != 0 ? true : false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+int Color::find (const std::string& input)
+{
+  for (unsigned int i = 0; i < NUM_COLORS; ++i)
+    if (allColors[i].english_name == input)
+      return (int) i;
+
+  return -1;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+std::string Color::fg () const
+{
+  int index = _value & _COLOR_FG;
+
+  if (_value & _COLOR_256)
+  {
+    if (_value & _COLOR_HASFG)
+    {
+      std::stringstream s;
+      s << "color" << (_value & _COLOR_FG);
+      return s.str ();
+    }
+  }
+  else
+  {
+    for (unsigned int i = 0; i < NUM_COLORS; ++i)
+      if (allColors[i].index == index)
+        return allColors[i].english_name;
+  }
+
+  return "";
+}
+
+////////////////////////////////////////////////////////////////////////////////
+std::string Color::bg () const
+{
+  int index = (_value & _COLOR_BG) >> 8;
+
+  if (_value & _COLOR_256)
+  {
+    if (_value & _COLOR_HASBG)
+    {
+      std::stringstream s;
+      s << "color" << ((_value & _COLOR_BG) >> 8);
+      return s.str ();
+    }
+  }
+  else
+  {
+    for (unsigned int i = 0; i < NUM_COLORS; ++i)
+      if (allColors[i].index == index)
+        return allColors[i].english_name;
+  }
+
+  return "";
+}
+
+////////////////////////////////////////////////////////////////////////////////
diff --git a/src/libshared/src/Color.h b/src/libshared/src/Color.h
new file mode 100644 (file)
index 0000000..d8393c2
--- /dev/null
@@ -0,0 +1,78 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Copyright 2006 - 2017, Paul Beckingham, Federico Hernandez.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+// http://www.opensource.org/licenses/mit-license.php
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#ifndef INCLUDED_COLOR
+#define INCLUDED_COLOR
+
+#include <string>
+
+#define _COLOR_INVERSE   0x00400000  // Inverse attribute.
+#define _COLOR_256       0x00200000  // 256-color mode.
+#define _COLOR_HASBG     0x00100000  // Has background color (all values taken).
+#define _COLOR_HASFG     0x00080000  // Has foreground color (all values taken).
+#define _COLOR_UNDERLINE 0x00040000  // General underline attribute.
+#define _COLOR_BOLD      0x00020000  // 16-color bold attribute.
+#define _COLOR_BRIGHT    0x00010000  // 16-color bright background attribute.
+#define _COLOR_BG        0x0000FF00  // 8-bit background color index.
+#define _COLOR_FG        0x000000FF  // 8-bit foreground color index.
+
+class Color
+{
+public:
+  enum color_id {nocolor = 0, black, red, green, yellow, blue, magenta, cyan, white};
+
+  Color ();
+  Color (const Color&);
+  Color (unsigned int);                         // 256 | INVERSE | UNDERLINE | BOLD | BRIGHT | (BG << 8) | FG
+  Color (const std::string&);                   // "red on bright black"
+  Color (color_id);                             // fg.
+  Color (color_id, color_id, bool, bool, bool); // fg, bg, underline, bold, bright
+  operator std::string () const;
+  operator int () const;
+
+  void upgrade ();
+  void blend (const Color&);
+
+  std::string colorize (const std::string&) const;
+  static std::string colorize (const std::string&, const std::string&);
+  void _colorize (std::string&, const std::string&) const;
+  static std::string strip (const std::string&);
+
+  std::string code () const;
+  std::string end () const;
+
+  bool nontrivial () const;
+
+private:
+  int find (const std::string&);
+  std::string fg () const;
+  std::string bg () const;
+
+private:
+  unsigned int _value;
+};
+
+#endif
diff --git a/src/libshared/src/Composite.cpp b/src/libshared/src/Composite.cpp
new file mode 100644 (file)
index 0000000..98bd49c
--- /dev/null
@@ -0,0 +1,148 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Copyright 2015 - 2017, Paul Beckingham, Federico Hernandez.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+// http://www.opensource.org/licenses/mit-license.php
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include <cmake.h>
+#include <Composite.h>
+#include <utf8.h>
+#include <sstream>
+#include <stack>
+
+////////////////////////////////////////////////////////////////////////////////
+// Initially assume no text, but infinite virtual space.
+//
+// Ållow overlay placement of arbitrary text at any offset, real or virtual, and
+// using a specific color.
+//
+// For example:
+//   Composite c;
+//   c.add ("aaaaaaaaaa",  2, Color ("..."));    // Layer 1
+//   c.add ("bbbbb",       5, Color ("..."));    // Layer 2
+//   c.add ("c",          15, Color ("..."));    // Layer 3
+//
+//   _layers = { std::make_tuple ("aaaaaaaaaa",  2, Color ("...")),
+//               std::make_tuple ("bbbbb",       5, Color ("...")),
+//               std::make_tuple ("c",          15, Color ("..."))};
+//
+void Composite::add (
+  const std::string& text,
+  std::string::size_type offset,
+  const Color& color)
+{
+  _layers.push_back (std::make_tuple (text, offset, color));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Merge the layers of text and color into one string.
+//
+// For example:
+//   Composite c;
+//   c.add ("aaaaaaaaaa",  2, Color ("..."));    // Layer 1
+//   c.add ("bbbbb",       5, Color ("..."));    // Layer 2
+//   c.add ("c",          15, Color ("..."));    // Layer 3
+//
+//   _layers = { std::make_tuple ("aaaaaaaaaa",  2, Color ("...")),
+//               std::make_tuple ("bbbbb",       5, Color ("...")),
+//               std::make_tuple ("c",          15, Color ("..."))};
+//
+// Arrange strings conceptually:
+//              111111
+//    0123456789012345     // Position
+//
+//      aaaaaaaaaa         // Layer 1
+//         bbbbb           // Layer 2
+//                   c     // Layer 3
+//
+// Walk all strings left to right, selecting the character and color from the
+// highest numbered layer. Emit color codes only on edge detection.
+//
+std::string Composite::str () const
+{
+  // The strings are broken into a vector of int, for UTF8 support.
+  std::vector <int> characters;
+  std::vector <int> colors;
+  for (unsigned int layer = 0; layer < _layers.size (); ++layer)
+  {
+    auto text   = std::get <0> (_layers[layer]);
+    auto offset = std::get <1> (_layers[layer]);
+    auto len    = utf8_text_length (text);
+
+    // Make sure the vectors are large enough to support a write operator[].
+    if (characters.size () < offset + len)
+    {
+      characters.resize (offset + len, 32);
+      colors.resize     (offset + len, 0);
+    }
+
+    // Copy in the layer characters and color indexes.
+    std::string::size_type cursor = 0;
+    int character;
+    int count = 0;
+    while ((character = utf8_next_char (text, cursor)))
+    {
+      characters[offset + count] = character;
+      colors    [offset + count] = layer + 1;
+      ++count;
+    }
+  }
+
+  // Now walk the character and color vector, emitting every character and
+  // every detected color change.
+  std::stringstream out;
+  int prev_color = 0;
+  for (unsigned int i = 0; i < characters.size (); ++i)
+  {
+    // A change in color triggers a code emit.
+    if (prev_color != colors[i])
+    {
+      if (prev_color)
+        out << std::get <2> (_layers[prev_color - 1]).end ();
+
+      if (colors[i])
+        out << std::get <2> (_layers[colors[i] - 1]).code ();
+      else
+        out << std::get <2> (_layers[prev_color - 1]).end ();
+
+      prev_color = colors[i];
+    }
+
+    out << utf8_character (characters[i]);
+  }
+
+  // Terminate the color codes, if necessary.
+  if (prev_color)
+    out << std::get <2> (_layers[prev_color - 1]).end ();
+
+  return out.str ();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// So the same instance can be reused.
+void Composite::clear ()
+{
+  _layers.clear ();
+}
+
+////////////////////////////////////////////////////////////////////////////////
diff --git a/src/libshared/src/Composite.h b/src/libshared/src/Composite.h
new file mode 100644 (file)
index 0000000..7309c51
--- /dev/null
@@ -0,0 +1,47 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Copyright 2015 - 2017, Paul Beckingham, Federico Hernandez.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+// http://www.opensource.org/licenses/mit-license.php
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#ifndef INCLUDED_COMPOSITE
+#define INCLUDED_COMPOSITE
+
+#include <Color.h>
+#include <vector>
+#include <string>
+#include <tuple>
+
+class Composite
+{
+public:
+  Composite () = default;
+  void add (const std::string&, std::string::size_type, const Color&);
+  std::string str () const;
+  void clear ();
+
+private:
+  std::vector <std::tuple <std::string, std::string::size_type, Color>> _layers;
+};
+
+#endif
diff --git a/src/libshared/src/Configuration.cpp b/src/libshared/src/Configuration.cpp
new file mode 100644 (file)
index 0000000..fc9331c
--- /dev/null
@@ -0,0 +1,323 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Copyright 2006 - 2017, Paul Beckingham, Federico Hernandez.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+// http://www.opensource.org/licenses/mit-license.php
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include <cmake.h>
+#include <Configuration.h>
+#include <inttypes.h>
+#include <stdlib.h>
+#include <FS.h>
+#include <JSON.h>
+#include <shared.h>
+#include <format.h>
+
+////////////////////////////////////////////////////////////////////////////////
+bool setVariableInFile (
+  const std::string& file,
+  const std::string& name,
+  const std::string& value)
+{
+  // Read the file contents.
+  std::vector <std::string> contents;
+  File::read (file, contents);
+
+  bool found = false;
+  bool change = false;
+
+  for (auto& line : contents)
+  {
+    // If there is a comment on the line, it must follow the pattern.
+    auto comment = line.find ('#');
+    auto pos     = line.find (name + '=');
+
+    if (pos != std::string::npos &&
+        (comment == std::string::npos ||
+         comment > pos))
+    {
+      found = true;
+      if (comment != std::string::npos)
+        line = name + '=' + value + ' ' + line.substr (comment);
+      else
+        line = name + '=' + value;
+
+      change = true;
+    }
+  }
+
+  // Not found, so append instead.
+  if (! found)
+  {
+    contents.push_back (name + '=' + value);
+    change = true;
+  }
+
+  if (change)
+    File::write (file, contents);
+
+  return change;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool unsetVariableInFile (
+  const std::string& file,
+  const std::string& name)
+{
+  // Read configuration file.
+  std::vector <std::string> contents;
+  File::read (file, contents);
+
+  bool change = false;
+
+  for (auto line = contents.begin (); line != contents.end (); )
+  {
+    bool lineDeleted = false;
+
+    // If there is a comment on the line, it must follow the pattern.
+    auto comment = line->find ('#');
+    auto pos     = line->find (name + '=');
+
+    if (pos != std::string::npos &&
+        (comment == std::string::npos ||
+         comment > pos))
+    {
+      // vector::erase method returns a valid iterator to the next object
+      line = contents.erase (line);
+      lineDeleted = true;
+      change = true;
+    }
+
+    if (! lineDeleted)
+      line++;
+  }
+
+  if (change)
+    File::write (file, contents);
+
+  return change;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Read the Configuration file and populate the *this map.  The file format is
+// simply lines with name=value pairs.  Whitespace between name, = and value is
+// not tolerated, but blank lines and comments starting with # are allowed.
+//
+// Nested files are now supported, with the following construct:
+//   include /absolute/path/to/file
+//
+void Configuration::load (const std::string& file, int nest /* = 1 */)
+{
+  if (nest > 10)
+    throw std::string ("Configuration files may only be nested to 10 levels.");
+
+  // Read the file, then parse the contents.
+  File config (file);
+
+  if (nest == 1)
+    _original_file = config;
+
+  if (config.exists () &&
+      config.readable ())
+  {
+    std::string contents;
+    if (File::read (file, contents) && contents.length ())
+      parse (contents, nest);
+  }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Write the Configuration file.
+void Configuration::save ()
+{
+  std::string contents;
+  for (const auto& i : *this)
+    contents += i.first + "=" + i.second + '\n';
+
+  File::write (_original_file, contents);
+  _dirty = false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+void Configuration::parse (const std::string& input, int nest /* = 1 */)
+{
+  // Shortcut case for default constructor.
+  if (input.length () == 0)
+    return;
+
+  // Parse each line.
+  for (auto& line : split (input, '\n'))
+  {
+    // Remove comments.
+    auto pound = line.find ('#');
+    if (pound != std::string::npos)
+      line = line.substr (0, pound);
+
+    // Skip empty lines.
+    line = trim (line);
+    if (line.length () > 0)
+    {
+      auto equal = line.find ('=');
+      if (equal != std::string::npos)
+      {
+        std::string key   = trim (line.substr (0, equal));
+        std::string value = trim (line.substr (equal+1, line.length () - equal));
+
+        (*this)[key] = json::decode (value);
+      }
+      else
+      {
+        auto include = line.find ("include");
+        if (include != std::string::npos)
+        {
+          Path included (trim (line.substr (include + 7)));
+          if (included.is_absolute ())
+          {
+            if (included.readable ())
+              load (included, nest + 1);
+            else
+              throw format ("Could not read include file '{1}'.", included._data);
+          }
+          else
+            throw format ("Can only include files with absolute paths, not '{1}'", included._data);
+        }
+        else
+          throw format ("Malformed entry '{1}' in config file.", line);
+      }
+    }
+  }
+
+  _dirty = true;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool Configuration::has (const std::string& key) const
+{
+  return (*this).find (key) != (*this).end ();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Return the configuration value given the specified key.
+std::string Configuration::get (const std::string& key) const
+{
+  auto found = find (key);
+  if (found != end ())
+    return found->second;
+
+  return "";
+}
+
+////////////////////////////////////////////////////////////////////////////////
+int Configuration::getInteger (const std::string& key) const
+{
+  auto found = find (key);
+  if (found != end ())
+    return strtoimax (found->second.c_str (), nullptr, 10);
+
+  return 0;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+double Configuration::getReal (const std::string& key) const
+{
+  auto found = find (key);
+  if (found != end ())
+    return strtod (found->second.c_str (), nullptr);
+
+  return 0.0;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool Configuration::getBoolean (const std::string& key) const
+{
+  auto found = find (key);
+  if (found != end ())
+  {
+    auto value = lowerCase (found->second);
+    if (value == "true"   ||
+        value == "1"      ||
+        value == "y"      ||
+        value == "yes"    ||
+        value == "on")
+      return true;
+  }
+
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+void Configuration::set (const std::string& key, const int value)
+{
+  (*this)[key] = format (value);
+  _dirty = true;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+void Configuration::set (const std::string& key, const double value)
+{
+  (*this)[key] = format (value, 1, 8);
+  _dirty = true;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+void Configuration::set (const std::string& key, const std::string& value)
+{
+  (*this)[key] = value;
+  _dirty = true;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Autovivification is ok here.
+void Configuration::setIfBlank (const std::string& key, const std::string& value)
+{
+  if ((*this)[key] == "")
+  {
+    (*this)[key] = value;
+    _dirty = true;
+  }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Provide a vector of all configuration keys.
+std::vector <std::string> Configuration::all () const
+{
+  std::vector <std::string> items;
+  for (const auto& it : *this)
+    items.push_back (it.first);
+
+  return items;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+std::string Configuration::file () const
+{
+  return _original_file._data;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool Configuration::dirty ()
+{
+  return _dirty;
+}
+
+////////////////////////////////////////////////////////////////////////////////
diff --git a/src/libshared/src/Configuration.h b/src/libshared/src/Configuration.h
new file mode 100644 (file)
index 0000000..067affe
--- /dev/null
@@ -0,0 +1,66 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Copyright 2006 - 2017, Paul Beckingham, Federico Hernandez.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+// http://www.opensource.org/licenses/mit-license.php
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#ifndef INCLUDED_CONFIGURATION
+#define INCLUDED_CONFIGURATION
+
+#include <map>
+#include <vector>
+#include <string>
+#include <FS.h>
+
+bool setVariableInFile   (const std::string&, const std::string&, const std::string&);
+bool unsetVariableInFile (const std::string&, const std::string&);
+
+class Configuration : public std::map <std::string, std::string>
+{
+public:
+  void load  (const std::string&, int nest = 1);
+  void save ();
+  void parse (const std::string&, int nest = 1);
+
+  bool        has        (const std::string&) const;
+  std::string get        (const std::string&) const;
+  int         getInteger (const std::string&) const;
+  double      getReal    (const std::string&) const;
+  bool        getBoolean (const std::string&) const;
+
+  void set (const std::string&, const int);
+  void set (const std::string&, const double);
+  void set (const std::string&, const std::string&);
+  void setIfBlank (const std::string&, const std::string&);
+  std::vector <std::string> all () const;
+
+  std::string file () const;
+
+  bool dirty ();
+
+private:
+  File _original_file {};
+  bool _dirty         {false};
+};
+
+#endif
diff --git a/src/libshared/src/Datetime.cpp b/src/libshared/src/Datetime.cpp
new file mode 100644 (file)
index 0000000..726d9dc
--- /dev/null
@@ -0,0 +1,3787 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Copyright 2006 - 2017, Paul Beckingham, Federico Hernandez.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+// http://www.opensource.org/licenses/mit-license.php
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include <cmake.h>
+#include <Datetime.h>
+#include <algorithm>
+#include <iostream>
+#include <sstream>
+#include <iomanip>
+#include <cassert>
+#include <stdlib.h>
+#include <shared.h>
+#include <format.h>
+#include <unicode.h>
+#include <utf8.h>
+
+static std::vector <std::string> dayNames {
+  "sunday",
+  "monday",
+  "tuesday",
+  "wednesday",
+  "thursday",
+  "friday",
+  "saturday"};
+
+static std::vector <std::string> monthNames {
+  "january",
+  "february",
+  "march",
+  "april",
+  "may",
+  "june",
+  "july",
+  "august",
+  "september",
+  "october",
+  "november",
+  "december"};
+
+int Datetime::weekstart = 1; // Monday, per ISO-8601.
+int Datetime::minimumMatchLength = 3;
+bool Datetime::isoEnabled            = true;
+bool Datetime::standaloneDateEnabled = true;
+bool Datetime::standaloneTimeEnabled = true;
+
+////////////////////////////////////////////////////////////////////////////////
+Datetime::Datetime ()
+{
+  clear ();
+  _date = time (nullptr);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+Datetime::Datetime (const std::string& input, const std::string& format)
+{
+  clear ();
+  std::string::size_type start = 0;
+  if (! parse (input, start, format))
+    throw ::format ("'{1}' is not a valid date in the '{2}' format.", input, format);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+Datetime::Datetime (const time_t t)
+{
+  clear ();
+  _date = t;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+Datetime::Datetime (const int y, const int m, const int d)
+{
+  // Protect against arguments being passed in the wrong order.
+  assert (y >= 1969 && y < 2100);
+  assert (m >= 1 && m <= 12);
+  assert (d >= 1 && d <= 31);
+
+  clear ();
+
+  // Error if not valid.
+  struct tm t {};
+  t.tm_isdst = -1;   // Requests that mktime determine summer time effect.
+  t.tm_mday  = d;
+  t.tm_mon   = m - 1;
+  t.tm_year  = y - 1900;
+
+  _date = mktime (&t);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+Datetime::Datetime (const int y,  const int m,  const int d,
+                    const int hr, const int mi, const int se)
+{
+  // Protect against arguments being passed in the wrong order.
+  assert (y >= 1969 && y < 2100);
+  assert (m >= 1 && m <= 12);
+  assert (d >= 1 && d <= 31);
+  assert (hr >= 0 && hr <= 24);
+  assert (mi >= 0 && mi < 60);
+  assert (se >= 0 && se < 60);
+
+  clear ();
+
+  // Error if not valid.
+  struct tm t {};
+  t.tm_isdst = -1;   // Requests that mktime determine summer time effect.
+  t.tm_mday  = d;
+  t.tm_mon   = m - 1;
+  t.tm_year  = y - 1900;
+  t.tm_hour  = hr;
+  t.tm_min   = mi;
+  t.tm_sec   = se;
+
+  _date = mktime (&t);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool Datetime::parse (
+  const std::string& input,
+  std::string::size_type& start,
+  const std::string& format)
+{
+  auto i = start;
+  Pig pig (input);
+  if (i)
+    pig.skipN (static_cast <int> (i));
+
+  auto checkpoint = pig.cursor ();
+
+  // Parse epoch first, as it's the most common scenario.
+  if (parse_epoch (pig))
+  {
+    // ::validate and ::resolve are not needed in this case.
+    start = pig.cursor ();
+    return true;
+  }
+
+  if (parse_formatted (pig, format))
+  {
+    // Check the values and determine time_t.
+    if (validate ())
+    {
+      start = pig.cursor ();
+      resolve ();
+      return true;
+    }
+  }
+
+  // Allow parse_date_time and parse_date_time_ext regardless of
+  // Datetime::isoEnabled setting, because these formats are relied upon by
+  // the 'import' command, JSON parser and hook system.
+  if (parse_date_time_ext   (pig) || // Strictest first.
+      parse_date_time       (pig) ||
+      (Datetime::isoEnabled &&
+       (                                    parse_date_ext      (pig)  ||
+        (Datetime::standaloneDateEnabled && parse_date          (pig)) ||
+                                            parse_time_utc_ext  (pig)  ||
+                                            parse_time_utc      (pig)  ||
+                                            parse_time_off_ext  (pig)  ||
+                                            parse_time_off      (pig)  ||
+                                            parse_time_ext      (pig)  ||
+        (Datetime::standaloneTimeEnabled && parse_time          (pig)) // Time last, as it is the most permissive.
+       )
+      )
+     )
+  {
+    // Check the values and determine time_t.
+    if (validate ())
+    {
+      start = pig.cursor ();
+      resolve ();
+      return true;
+    }
+  }
+
+  pig.restoreTo (checkpoint);
+
+  if (parse_named (pig))
+  {
+    // ::validate and ::resolve are not needed in this case.
+    start = pig.cursor ();
+    return true;
+  }
+
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+void Datetime::clear ()
+{
+  _year    = 0;
+  _month   = 0;
+  _week    = 0;
+  _weekday = 0;
+  _julian  = 0;
+  _day     = 0;
+  _seconds = 0;
+  _offset  = 0;
+  _utc     = false;
+  _date    = 0;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool Datetime::parse_formatted (Pig& pig, const std::string& format)
+{
+  // Short-circuit on missing format.
+  if (format == "")
+    return false;
+
+  auto checkpoint = pig.cursor ();
+
+  int month  {-1};   // So we can check later.
+  int day    {-1};
+  int year   {-1};
+  int hour   {-1};
+  int minute {-1};
+  int second {-1};
+
+  // For parsing, unused.
+  int wday   {-1};
+  int week   {-1};
+
+  for (unsigned int f = 0; f < format.length (); ++f)
+  {
+    switch (format[f])
+    {
+    case 'm':
+      if (pig.getDigit (month))
+      {
+        if (month == 0)
+          pig.getDigit (month);
+
+        if (month == 1)
+          if (pig.getDigit (month))
+            month += 10;
+      }
+      else
+      {
+        pig.restoreTo (checkpoint);
+        return false;
+      }
+      break;
+
+    case 'M':
+      if (! pig.getDigit2 (month))
+      {
+        pig.restoreTo (checkpoint);
+        return false;
+      }
+      break;
+
+    case 'd':
+      if (pig.getDigit (day))
+      {
+        if (day == 0)
+          pig.getDigit (day);
+
+        if (day == 1 || day == 2 || day == 3)
+        {
+          int tens = day;
+          if (pig.getDigit (day))
+            day += 10 * tens;
+        }
+      }
+      else
+      {
+        pig.restoreTo (checkpoint);
+        return false;
+      }
+      break;
+
+    case 'D':
+      if (! pig.getDigit2 (day))
+      {
+        pig.restoreTo (checkpoint);
+        return false;
+      }
+      break;
+
+    case 'y':
+      if (! pig.getDigit2 (year))
+      {
+        pig.restoreTo (checkpoint);
+        return false;
+      }
+      year += 2000;
+      break;
+
+    case 'Y':
+      if (! pig.getDigit4 (year))
+      {
+        pig.restoreTo (checkpoint);
+        return false;
+      }
+      break;
+
+    case 'h':
+      if (pig.getDigit (hour))
+      {
+        if (hour == 0)
+          pig.getDigit (hour);
+
+        if (hour == 1 || hour == 2)
+        {
+          int tens = hour;
+          if (pig.getDigit (hour))
+            hour += 10 * tens;
+        }
+      }
+      else
+      {
+        pig.restoreTo (checkpoint);
+        return false;
+      }
+      break;
+
+    case 'H':
+      if (! pig.getDigit2 (hour))
+      {
+        pig.restoreTo (checkpoint);
+        return false;
+      }
+      break;
+
+    case 'n':
+      if (pig.getDigit (minute))
+      {
+        if (minute == 0)
+          pig.getDigit (minute);
+
+        if (minute < 6)
+        {
+          int tens = minute;
+          if (pig.getDigit (minute))
+            minute += 10 * tens;
+        }
+      }
+      else
+      {
+        pig.restoreTo (checkpoint);
+        return false;
+      }
+      break;
+
+    case 'N':
+      if (! pig.getDigit2 (minute))
+      {
+        pig.restoreTo (checkpoint);
+        return false;
+      }
+      break;
+
+    case 's':
+      if (pig.getDigit (second))
+      {
+        if (second == 0)
+          pig.getDigit (second);
+
+        if (second < 6)
+        {
+          int tens = second;
+          if (pig.getDigit (second))
+            second += 10 * tens;
+        }
+      }
+      else
+      {
+        pig.restoreTo (checkpoint);
+        return false;
+      }
+      break;
+
+    case 'S':
+      if (! pig.getDigit2 (second))
+      {
+        pig.restoreTo (checkpoint);
+        return false;
+      }
+      break;
+
+    case 'v':
+      if (pig.getDigit (week))
+      {
+        if (week == 0)
+          pig.getDigit (week);
+
+        if (week < 6)
+        {
+          int tens = week;
+          if (pig.getDigit (week))
+            week += 10 * tens;
+        }
+      }
+      else
+      {
+        pig.restoreTo (checkpoint);
+        return false;
+      }
+      break;
+
+    case 'V':
+      if (! pig.getDigit2 (week))
+      {
+        pig.restoreTo (checkpoint);
+        return false;
+      }
+      break;
+
+    case 'a':
+      wday = Datetime::dayOfWeek (pig.str ().substr (0, 3));
+      if (wday == -1)
+      {
+        pig.restoreTo (checkpoint);
+        return false;
+      }
+
+      pig.skipN (3);
+      break;
+
+    case 'A':
+      {
+        std::string dayName;
+        if (pig.getUntil (format[f + 1], dayName))
+        {
+          wday = Datetime::dayOfWeek (dayName);
+          if (wday == -1)
+          {
+            pig.restoreTo (checkpoint);
+            return false;
+          }
+        }
+      }
+      break;
+
+    case 'b':
+      month = Datetime::monthOfYear (pig.str ().substr (0, 3));
+      if (month == -1)
+      {
+        pig.restoreTo (checkpoint);
+        return false;
+      }
+
+      pig.skipN (3);
+      break;
+
+    case 'B':
+      {
+        std::string monthName;
+        if (pig.getUntil (format[f + 1], monthName))
+        {
+          month = Datetime::monthOfYear (monthName);
+          if (month == -1)
+          {
+            pig.restoreTo (checkpoint);
+            return false;
+          }
+        }
+      }
+      break;
+
+    default:
+      if (! pig.skip (format[f]))
+      {
+        pig.restoreTo (checkpoint);
+        return false;
+      }
+      break;
+    }
+  }
+
+  // It is possible that the format='Y-M-D', and the input is Y-M-DTH:N:SZ, and
+  // this should not be considered a match.
+  if (! pig.eos () && ! unicodeWhitespace (pig.peek ()))
+  {
+    pig.restoreTo (checkpoint);
+    return false;
+  }
+
+  // Missing values are filled in from the current date.
+  if (year == -1)
+  {
+    Datetime now;
+    year = now.year ();
+    if (month == -1)
+    {
+      month = now.month ();
+      if (day == -1)
+      {
+        day = now.day ();
+        if (hour == -1)
+        {
+          hour = now.hour ();
+          if (minute == -1)
+          {
+            minute = now.minute ();
+            if (second == -1)
+              second = now.second ();
+          }
+        }
+      }
+    }
+  }
+
+  // Any remaining undefined values are assigned defaults.
+  if (month  == -1) month  = 1;
+  if (day    == -1) day    = 1;
+  if (hour   == -1) hour   = 0;
+  if (minute == -1) minute = 0;
+  if (second == -1) second = 0;
+
+  _year    = year;
+  _month   = month;
+  _day     = day;
+  _seconds = (hour * 3600) + (minute * 60) + second;
+
+  return true;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Note how these are all single words.
+//
+// Examples and descriptions, assuming now == 2017-03-05T12:34:56.
+//
+//                  Example              Notes
+//                  -------------------  ------------------
+//   now            2017-03-05T12:34:56  Unaffected
+//   yesterday      2017-03-04T00:00:00  Unaffected
+//   today          2017-03-05T00:00:00  Unaffected
+//   tomorrow       2017-03-06T00:00:00  Unaffected
+//   <ordinal> 12th 2017-03-12T00:00:00
+//   <day> monday   2017-03-06T00:00:00
+//   <month> april  2017-04-01T00:00:00
+//   later          2038-01-18T00:00:00  Unaffected
+//   someday        2038-01-18T00:00:00  Unaffected
+//   sopd           2017-03-04T00:00:00  Unaffected
+//   sod            2017-03-05T00:00:00  Unaffected
+//   sond           2017-03-06T00:00:00  Unaffected
+//   eopd           2017-03-05T00:00:00  Unaffected
+//   eod            2017-03-06T00:00:00  Unaffected
+//   eond           2017-03-07T00:00:00  Unaffected
+//   sopw           2017-02-26T00:00:00  Unaffected
+//   sow            2017-03-05T00:00:00  Unaffected
+//   sonw           2017-03-12T00:00:00  Unaffected
+//   eopw           2017-03-05T00:00:00  Unaffected
+//   eow            2017-03-12T00:00:00  Unaffected
+//   eonw           2017-03-19T00:00:00  Unaffected
+//   sopww          2017-02-27T00:00:00  Unaffected
+//   soww           2017-03-06T00:00:00
+//   sonww          2017-03-06T00:00:00  Unaffected
+//   eopww          2017-03-03T00:00:00  Unaffected
+//   eoww           2017-03-10T00:00:00
+//   eonww          2017-03-17T00:00:00  Unaffected
+//   sopm           2017-02-01T00:00:00  Unaffected
+//   som            2017-03-01T00:00:00  Unaffected
+//   sonm           2017-04-01T00:00:00  Unaffected
+//   eopm           2017-03-01T00:00:00  Unaffected
+//   eom            2017-04-01T00:00:00  Unaffected
+//   eonm           2017-05-01T00:00:00  Unaffected
+//   sopq           2017-10-01T00:00:00  Unaffected
+//   soq            2017-01-01T00:00:00  Unaffected
+//   sonq           2017-04-01T00:00:00  Unaffected
+//   eopq           2017-01-01T00:00:00  Unaffected
+//   eoq            2017-04-01T00:00:00  Unaffected
+//   eonq           2017-07-01T00:00:00  Unaffected
+//   sopy           2016-01-01T00:00:00  Unaffected
+//   soy            2017-01-01T00:00:00  Unaffected
+//   sony           2018-01-01T00:00:00  Unaffected
+//   eopy           2017-01-01T00:00:00  Unaffected
+//   eoy            2018-01-01T00:00:00  Unaffected
+//   eony           2019-01-01T00:00:00  Unaffected
+//   easter         2017-04-16T00:00:00
+//   eastermonday   2017-04-16T00:00:00
+//   ascension      2017-05-25T00:00:00
+//   pentecost      2017-06-04T00:00:00
+//   goodfriday     2017-04-14T00:00:00
+//   midsommar      2017-06-24T00:00:00  midnight, 1st Saturday after 20th June
+//   midsommarafton 2017-06-23T00:00:00  midnight, 1st Friday after 19th June
+//   juhannus       2017-06-23T00:00:00  midnight, 1st Friday after 19th June
+//
+bool Datetime::parse_named (Pig& pig)
+{
+  auto checkpoint = pig.cursor ();
+
+  // Experimental handling of date phrases, such as "first monday in march".
+  // Note that this requires that phrases are deliminted by EOS or WS.
+  std::string token;
+  std::vector <std::string> tokens;
+  while (pig.getUntilWS (token))
+  {
+    tokens.push_back (token);
+    if (! pig.skipWS ())
+      break;
+  }
+
+/*
+  // This grpoup contains "1st monday ..." which must be processed before
+  // initializeOrdinal below.
+  if (initializeNthDayInMonth (tokens))
+  {
+    return true;
+  }
+*/
+
+  // Restoration necessary because of the tokenization.
+  pig.restoreTo (checkpoint);
+
+  if (initializeNow            (pig) ||
+      initializeYesterday      (pig) ||
+      initializeToday          (pig) ||
+      initializeTomorrow       (pig) ||
+      initializeOrdinal        (pig) ||
+      initializeDayName        (pig) ||
+      initializeMonthName      (pig) ||
+      initializeLater          (pig) ||
+      initializeSopd           (pig) ||
+      initializeSod            (pig) ||
+      initializeSond           (pig) ||
+      initializeEopd           (pig) ||
+      initializeEod            (pig) ||
+      initializeEond           (pig) ||
+      initializeSopw           (pig) ||
+      initializeSow            (pig) ||
+      initializeSonw           (pig) ||
+      initializeEopw           (pig) ||
+      initializeEow            (pig) ||
+      initializeEonw           (pig) ||
+      initializeSopww          (pig) ||  // Must appear after sopw
+      initializeSonww          (pig) ||  // Must appear after sonw
+      initializeSoww           (pig) ||  // Must appear after sow
+      initializeEopww          (pig) ||  // Must appear after eopw
+      initializeEonww          (pig) ||  // Must appear after eonw
+      initializeEoww           (pig) ||  // Must appear after eow
+      initializeSopm           (pig) ||
+      initializeSom            (pig) ||
+      initializeSonm           (pig) ||
+      initializeEopm           (pig) ||
+      initializeEom            (pig) ||
+      initializeEonm           (pig) ||
+      initializeSopq           (pig) ||
+      initializeSoq            (pig) ||
+      initializeSonq           (pig) ||
+      initializeEopq           (pig) ||
+      initializeEoq            (pig) ||
+      initializeEonq           (pig) ||
+      initializeSopy           (pig) ||
+      initializeSoy            (pig) ||
+      initializeSony           (pig) ||
+      initializeEopy           (pig) ||
+      initializeEoy            (pig) ||
+      initializeEony           (pig) ||
+      initializeEaster         (pig) ||
+      initializeMidsommar      (pig) ||
+      initializeMidsommarafton (pig) ||
+      initializeInformalTime   (pig))
+  {
+    return true;
+  }
+
+  pig.restoreTo (checkpoint);
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Valid epoch values are unsigned integers after 1980-01-01T00:00:00Z. This
+// restriction means that '12' will not be identified as an epoch date.
+bool Datetime::parse_epoch (Pig& pig)
+{
+  auto checkpoint = pig.cursor ();
+
+  int epoch {};
+  if (pig.getDigits (epoch)             &&
+      ! unicodeLatinAlpha (pig.peek ()) &&
+      epoch >= 315532800)
+  {
+    _date = static_cast <time_t> (epoch);
+    //std::cout << "# parse_epoch \e[33msucceed\e[0m " << pig.dump () << "\n";
+    return true;
+  }
+
+  //std::cout << "# parse_epoch fail\n";
+  pig.restoreTo (checkpoint);
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// date_ext 'T' time_utc_ext 'Z'
+// date_ext 'T' time_off_ext
+// date_ext 'T' time_ext
+bool Datetime::parse_date_time_ext (Pig& pig)
+{
+  auto checkpoint = pig.cursor ();
+
+  if (parse_date_ext (pig) &&
+      pig.skip ('T')       &&
+      (parse_time_utc_ext (pig) ||
+       parse_time_off_ext (pig) ||
+       parse_time_ext     (pig)))
+  {
+    return true;
+  }
+
+  pig.restoreTo (checkpoint);
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// YYYY-MM-DD
+// YYYY-MM
+// YYYY-DDD
+// YYYY-Www-D
+// YYYY-Www
+bool Datetime::parse_date_ext (Pig& pig)
+{
+  auto checkpoint = pig.cursor ();
+
+  int year {};
+  if (parse_year (pig, year) &&
+      pig.skip ('-'))
+  {
+    auto checkpointYear = pig.cursor ();
+
+    int month {};
+    int day {};
+    int julian {};
+
+    if (pig.skip ('W') &&
+        parse_week (pig, _week))
+    {
+      if (pig.skip ('-') &&
+          pig.getDigit (_weekday))
+      {
+        // What is happening here - must be something to do?
+      }
+
+      if (! unicodeLatinDigit (pig.peek ()))
+      {
+        _year = year;
+        return true;
+      }
+    }
+
+    pig.restoreTo (checkpointYear);
+
+    if (parse_month (pig, month) &&
+        pig.skip ('-')           &&
+        parse_day (pig, day)     &&
+        ! unicodeLatinDigit (pig.peek ()))
+    {
+      _year = year;
+      _month = month;
+      _day = day;
+      return true;
+    }
+
+    pig.restoreTo (checkpointYear);
+
+    if (parse_julian (pig, julian) &&
+        ! unicodeLatinDigit (pig.peek ()))
+    {
+      _year = year;
+      _julian = julian;
+      return true;
+    }
+
+    pig.restoreTo (checkpointYear);
+
+    if (parse_month (pig, month) &&
+        pig.peek () != '-'       &&
+        ! unicodeLatinDigit (pig.peek ()))
+    {
+      _year = year;
+      _month = month;
+      _day = 1;
+      return true;
+    }
+  }
+
+  pig.restoreTo (checkpoint);
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// ±hh[:mm]
+bool Datetime::parse_off_ext (Pig& pig)
+{
+  auto checkpoint = pig.cursor ();
+
+  int sign = pig.peek ();
+  if (sign == '+' || sign == '-')
+  {
+    pig.skipN (1);
+
+    int hour {0};
+    int minute {0};
+
+    if (parse_off_hour (pig, hour))
+    {
+      if (pig.skip (':'))
+      {
+        if (! parse_off_minute (pig, minute))
+        {
+          pig.restoreTo (checkpoint);
+          return false;
+        }
+      }
+
+      _offset = (hour * 3600) + (minute * 60);
+      if (sign == '-')
+        _offset = - _offset;
+
+      if (! unicodeLatinDigit (pig.peek ()))
+        return true;
+    }
+  }
+
+  pig.restoreTo (checkpoint);
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// hh:mm[:ss]
+bool Datetime::parse_time_ext (Pig& pig, bool terminated)
+{
+  auto checkpoint = pig.cursor ();
+
+  int hour {};
+  int minute {};
+  if (parse_hour (pig, hour) &&
+      pig.skip (':')         &&
+      parse_minute (pig, minute))
+  {
+    if (pig.skip (':'))
+    {
+      int second {};
+      if (parse_second (pig, second) &&
+          ! unicodeLatinDigit (pig.peek ()) &&
+          (! terminated || (pig.peek () != '-' && pig.peek () != '+')))
+      {
+        _seconds = (hour * 3600) + (minute * 60) + second;
+        return true;
+      }
+
+      pig.restoreTo (checkpoint);
+      return false;
+    }
+
+    auto following = pig.peek ();
+    if (! unicodeLatinDigit (following)    &&
+        (! terminated || (following != '+' && following != '-')) &&
+        following != 'A'                   &&
+        following != 'a'                   &&
+        following != 'P'                   &&
+        following != 'p')
+    {
+      _seconds = (hour * 3600) + (minute * 60);
+      return true;
+    }
+  }
+
+  pig.restoreTo (checkpoint);
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// time-ext 'Z'
+bool Datetime::parse_time_utc_ext (Pig& pig)
+{
+  auto checkpoint = pig.cursor ();
+
+  if (parse_time_ext (pig, false) &&
+      pig.skip ('Z'))
+  {
+    if (! unicodeLatinDigit (pig.peek ()))
+    {
+      _utc = true;
+      return true;
+    }
+  }
+
+  pig.restoreTo (checkpoint);
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// time-ext off-ext
+bool Datetime::parse_time_off_ext (Pig& pig)
+{
+  auto checkpoint = pig.cursor ();
+
+  if (parse_time_ext (pig, false) &&
+      parse_off_ext (pig))
+  {
+    return true;
+  }
+
+  pig.restoreTo (checkpoint);
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// YYYYMMDDTHHMMSSZ
+// YYYYMMDDTHHMMSS
+bool Datetime::parse_date_time (Pig& pig)
+{
+  auto checkpoint = pig.cursor ();
+
+  if (parse_date (pig) &&
+      pig.skip ('T')   &&
+      (parse_time_utc (pig) ||
+       parse_time_off (pig) ||
+       parse_time     (pig)))
+  {
+    return true;
+  }
+
+  pig.restoreTo (checkpoint);
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// YYYYWww
+// YYYYDDD
+// YYYYMMDD
+// YYYYMM
+bool Datetime::parse_date (Pig& pig)
+{
+  auto checkpoint = pig.cursor ();
+
+  int year {};
+  int month {};
+  int julian {};
+  int week {};
+  int weekday {};
+  int day {};
+  if (parse_year (pig, year))
+  {
+    auto checkpointYear = pig.cursor ();
+
+    if (pig.skip ('W') &&
+        parse_week (pig, week))
+    {
+      if (pig.getDigit (weekday))
+        _weekday = weekday;
+
+      if (! unicodeLatinDigit (pig.peek ()))
+      {
+        _year = year;
+        _week = week;
+        return true;
+      }
+    }
+
+    pig.restoreTo (checkpointYear);
+
+    if (parse_julian (pig, julian) &&
+        ! unicodeLatinDigit (pig.peek ()))
+    {
+      _year = year;
+      _julian = julian;
+      return true;
+    }
+
+    pig.restoreTo (checkpointYear);
+
+    if (parse_month (pig, month))
+    {
+      if (parse_day (pig, day))
+      {
+        if (! unicodeLatinDigit (pig.peek ()))
+        {
+          _year = year;
+          _month = month;
+          _day = day;
+          return true;
+        }
+      }
+      else
+      {
+        if (! unicodeLatinDigit (pig.peek ()))
+        {
+          _year = year;
+          _month = month;
+          _day = 1;
+          return true;
+        }
+      }
+    }
+  }
+
+  pig.restoreTo (checkpoint);
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// <time> Z
+bool Datetime::parse_time_utc (Pig& pig)
+{
+  auto checkpoint = pig.cursor ();
+
+  if (parse_time (pig, false) &&
+      pig.skip ('Z'))
+  {
+    _utc = true;
+    if (! unicodeLatinDigit (pig.peek ()))
+      return true;
+  }
+
+  pig.restoreTo (checkpoint);
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// <time> <off>
+bool Datetime::parse_time_off (Pig& pig)
+{
+  auto checkpoint = pig.cursor ();
+
+  if (parse_time (pig, false) &&
+      parse_off (pig))
+  {
+    auto terminator = pig.peek ();
+    if (terminator != '-' && ! unicodeLatinDigit (terminator))
+    {
+      return true;
+    }
+  }
+
+  pig.restoreTo (checkpoint);
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// hhmmss
+// hhmm
+bool Datetime::parse_time (Pig& pig, bool terminated)
+{
+  auto checkpoint = pig.cursor ();
+
+  int hour {};
+  int minute {};
+  if (parse_hour (pig, hour) &&
+      parse_minute (pig, minute))
+  {
+    int second {};
+    parse_second (pig, second);
+
+    auto terminator = pig.peek ();
+    if (! terminated ||
+        (! unicodeLatinDigit (terminator) && terminator != '-' && terminator != '+'))
+    {
+      _seconds = (hour * 3600) + (minute * 60) + second;
+      return true;
+    }
+  }
+
+  pig.restoreTo (checkpoint);
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// ±hhmm
+// ±hh
+bool Datetime::parse_off (Pig& pig)
+{
+  auto checkpoint = pig.cursor ();
+
+  int sign = pig.peek ();
+  if (sign == '+' || sign == '-')
+  {
+    pig.skipN (1);
+
+    int hour {};
+    if (parse_off_hour (pig, hour))
+    {
+      int minute {};
+      parse_off_minute (pig, minute);
+
+      if (! unicodeLatinDigit (pig.peek ()))
+      {
+        _offset = (hour * 3600) + (minute * 60);
+        if (sign == '-')
+          _offset = - _offset;
+
+        return true;
+      }
+    }
+  }
+
+  pig.restoreTo (checkpoint);
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool Datetime::parse_year (Pig& pig, int& value)
+{
+  auto checkpoint = pig.cursor ();
+
+  int year;
+  if (pig.getDigit4 (year) &&
+      year > 1969)
+  {
+    value = year;
+    return true;
+  }
+
+  pig.restoreTo (checkpoint);
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool Datetime::parse_month (Pig& pig, int& value)
+{
+  auto checkpoint = pig.cursor ();
+
+  int month;
+  if (pig.getDigit2 (month) &&
+      month > 0             &&
+      month <= 12)
+  {
+    value = month;
+    return true;
+  }
+
+  pig.restoreTo (checkpoint);
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool Datetime::parse_week (Pig& pig, int& value)
+{
+  auto checkpoint = pig.cursor ();
+
+  int week;
+  if (pig.getDigit2 (week) &&
+      week > 0             &&
+      week <= 53)
+  {
+    value = week;
+    return true;
+  }
+
+  pig.restoreTo (checkpoint);
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool Datetime::parse_julian (Pig& pig, int& value)
+{
+  auto checkpoint = pig.cursor ();
+
+  int julian;
+  if (pig.getDigit3 (julian) &&
+      julian > 0             &&
+      julian <= 366)
+  {
+    value = julian;
+    return true;
+  }
+
+  pig.restoreTo (checkpoint);
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool Datetime::parse_day (Pig& pig, int& value)
+{
+  auto checkpoint = pig.cursor ();
+
+  int day;
+  if (pig.getDigit2 (day) &&
+      day > 0             &&
+      day <= 31)
+  {
+    value = day;
+    return true;
+  }
+
+  pig.restoreTo (checkpoint);
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool Datetime::parse_weekday (Pig& pig, int& value)
+{
+  auto checkpoint = pig.cursor ();
+
+  int weekday;
+  if (pig.getDigit (weekday) &&
+      weekday >= 1           &&
+      weekday <= 7)
+  {
+    value = weekday;
+    return true;
+  }
+
+  pig.restoreTo (checkpoint);
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool Datetime::parse_hour (Pig& pig, int& value)
+{
+  auto checkpoint = pig.cursor ();
+
+  int hour;
+  if (pig.getDigit2 (hour) &&
+      hour >= 0            &&
+      hour < 24)
+  {
+    value = hour;
+    return true;
+  }
+
+  pig.restoreTo (checkpoint);
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool Datetime::parse_minute (Pig& pig, int& value)
+{
+  auto checkpoint = pig.cursor ();
+
+  int minute;
+  if (pig.getDigit2 (minute) &&
+      minute >= 0            &&
+      minute < 60)
+  {
+    value = minute;
+    return true;
+  }
+
+  pig.restoreTo (checkpoint);
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool Datetime::parse_second (Pig& pig, int& value)
+{
+  auto checkpoint = pig.cursor ();
+
+  int second;
+  if (pig.getDigit2 (second) &&
+      second >= 0            &&
+      second < 60)
+  {
+    value = second;
+    return true;
+  }
+
+  pig.restoreTo (checkpoint);
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool Datetime::parse_off_hour (Pig& pig, int& value)
+{
+  auto checkpoint = pig.cursor ();
+
+  int hour;
+  if (pig.getDigit2 (hour) &&
+      hour >= 0            &&
+      hour <= 12)
+  {
+    value = hour;
+    return true;
+  }
+
+  pig.restoreTo (checkpoint);
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool Datetime::parse_off_minute (Pig& pig, int& value)
+{
+  auto checkpoint = pig.cursor ();
+
+  int minute;
+  if (pig.getDigit2 (minute) &&
+      minute >= 0            &&
+      minute < 60)
+  {
+    value = minute;
+    return true;
+  }
+
+  pig.restoreTo (checkpoint);
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// now [ !<alpha> && !<digit> ]
+bool Datetime::initializeNow (Pig& pig)
+{
+  auto checkpoint = pig.cursor ();
+
+  if (pig.skipLiteral ("now"))
+  {
+    auto following = pig.peek ();
+    if (! unicodeLatinAlpha (following) &&
+        ! unicodeLatinDigit (following))
+    {
+      _date = time (nullptr);
+      return true;
+    }
+  }
+
+  pig.restoreTo (checkpoint);
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// yesterday/abbrev  [ !<alpha> && !<digit> ]
+bool Datetime::initializeYesterday (Pig& pig)
+{
+  auto checkpoint = pig.cursor ();
+
+  std::string token;
+  if (pig.skipPartial ("yesterday", token) &&
+      token.length () >= static_cast <std::string::size_type> (Datetime::minimumMatchLength))
+  {
+    auto following = pig.peek ();
+    if (! unicodeLatinAlpha (following) &&
+        ! unicodeLatinDigit (following))
+    {
+      time_t now = time (nullptr);
+      struct tm* t = localtime (&now);
+
+      t->tm_hour = t->tm_min = t->tm_sec = 0;
+      t->tm_isdst = -1;
+      t->tm_mday -= 1;
+      _date = mktime (t);
+      return true;
+    }
+  }
+
+  pig.restoreTo (checkpoint);
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// today/abbrev  [ !<alpha> && !<digit> ]
+bool Datetime::initializeToday (Pig& pig)
+{
+  auto checkpoint = pig.cursor ();
+
+  std::string token;
+  if (pig.skipPartial ("today", token) &&
+      token.length () >= static_cast <std::string::size_type> (Datetime::minimumMatchLength))
+  {
+    auto following = pig.peek ();
+    if (! unicodeLatinAlpha (following) &&
+        ! unicodeLatinDigit (following))
+    {
+      time_t now = time (nullptr);
+      struct tm* t = localtime (&now);
+
+      t->tm_hour = t->tm_min = t->tm_sec = 0;
+      t->tm_isdst = -1;
+      _date = mktime (t);
+
+      return true;
+    }
+  }
+
+  pig.restoreTo (checkpoint);
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// tomcorrow/abbrev  [ !<alpha> && !<digit> ]
+bool Datetime::initializeTomorrow (Pig& pig)
+{
+  auto checkpoint = pig.cursor ();
+
+  std::string token;
+  if (pig.skipPartial ("tomorrow", token) &&
+      token.length () >= static_cast <std::string::size_type> (Datetime::minimumMatchLength))
+  {
+    auto following = pig.peek ();
+    if (! unicodeLatinAlpha (following) &&
+        ! unicodeLatinDigit (following))
+    {
+      time_t now = time (nullptr);
+      struct tm* t = localtime (&now);
+
+      t->tm_mday++;
+      t->tm_hour = t->tm_min = t->tm_sec = 0;
+      t->tm_isdst = -1;
+      _date = mktime (t);
+      return true;
+    }
+  }
+
+  pig.restoreTo (checkpoint);
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// <digit>+ [ "st" | "nd" | "rd" | "th" ] [ !<alpha> && !<digit> ]
+bool Datetime::initializeOrdinal (Pig& pig)
+{
+  auto checkpoint = pig.cursor ();
+
+  int number = 0;
+  if (pig.getDigits (number) &&
+      number > 0             &&
+      number <= 31)
+  {
+    int character1;
+    int character2;
+    if (pig.getCharacter (character1)     &&
+        pig.getCharacter (character2)     &&
+        ! unicodeLatinAlpha (pig.peek ()) &&
+        ! unicodeLatinDigit (pig.peek ()))
+    {
+      int remainder1 = number % 10;
+      int remainder2 = number % 100;
+      if ((remainder2 != 11 && remainder1 == 1 && character1 == 's' && character2 == 't') ||
+          (remainder2 != 12 && remainder1 == 2 && character1 == 'n' && character2 == 'd') ||
+          (remainder2 != 13 && remainder1 == 3 && character1 == 'r' && character2 == 'd') ||
+          ((remainder2 == 11 ||
+            remainder2 == 12 ||
+            remainder2 == 13 ||
+            remainder1 == 0 ||
+            remainder1 > 3) && character1 == 't' && character2 == 'h'))
+      {
+        time_t now = time (nullptr);
+        struct tm* t = localtime (&now);
+
+        int y = t->tm_year + 1900;
+        int m = t->tm_mon + 1;
+        int d = t->tm_mday;
+
+        // If it is this month.
+        if (d < number &&
+            number <= daysInMonth (y, m))
+        {
+          t->tm_hour = t->tm_min = t->tm_sec = 0;
+          t->tm_mon  = m - 1;
+          t->tm_mday = number;
+          t->tm_year = y - 1900;
+          t->tm_isdst = -1;
+          _date = mktime (t);
+        }
+        else
+        {
+          if (++m > 12)
+          {
+            m = 1;
+            y++;
+          }
+
+          t->tm_hour = t->tm_min = t->tm_sec = 0;
+          t->tm_mon  = m - 1;
+          t->tm_mday = number;
+          t->tm_year = y - 1900;
+          t->tm_isdst = -1;
+          _date = mktime (t);
+        }
+
+        return true;
+      }
+    }
+  }
+
+  pig.restoreTo (checkpoint);
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// sunday/abbrev [ !<alpha> && !<digit> && !: && != ]
+bool Datetime::initializeDayName (Pig& pig)
+{
+  auto checkpoint = pig.cursor ();
+
+  std::string token;
+  for (int day = 0; day <= 7; ++day)   // Deliberate <= so that 'sunday' is either 0 or 7.
+  {
+    if (pig.skipPartial (dayNames[day % 7], token) &&
+        token.length () >= static_cast <std::string::size_type> (Datetime::minimumMatchLength))
+    {
+      auto following = pig.peek ();
+      if (! unicodeLatinAlpha (following) &&
+          ! unicodeLatinDigit (following) &&
+          following != ':' &&
+          following != '=')
+      {
+        time_t now = time (nullptr);
+        struct tm* t = localtime (&now);
+
+        if (t->tm_wday >= day)
+          t->tm_mday += day - t->tm_wday + 7;
+        else
+          t->tm_mday += day - t->tm_wday;
+
+        t->tm_hour = t->tm_min = t->tm_sec = 0;
+        t->tm_isdst = -1;
+        _date = mktime (t);
+        return true;
+      }
+    }
+
+    pig.restoreTo (checkpoint);
+  }
+
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// january/abbrev [ !<alpha> && !<digit> && !: && != ]
+bool Datetime::initializeMonthName (Pig& pig)
+{
+  auto checkpoint = pig.cursor ();
+
+  std::string token;
+  for (int month = 0; month < 12; ++month)
+  {
+    if (pig.skipPartial (monthNames[month], token) &&
+        token.length () >= static_cast <std::string::size_type> (Datetime::minimumMatchLength))
+    {
+      auto following = pig.peek ();
+      if (! unicodeLatinAlpha (following) &&
+          ! unicodeLatinDigit (following) &&
+          following != ':' &&
+          following != '=')
+      {
+        time_t now = time (nullptr);
+        struct tm* t = localtime (&now);
+
+        if (t->tm_mon >= month)
+          t->tm_year++;
+
+        t->tm_mon = month;
+        t->tm_mday = 1;
+        t->tm_hour = t->tm_min = t->tm_sec = 0;
+        t->tm_isdst = -1;
+        _date = mktime (t);
+        return true;
+      }
+    }
+
+    pig.restoreTo (checkpoint);
+  }
+
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// later/abbrev  [ !<alpha> && !<digit> ]
+// someday/abbrev  [ !<alpha> && !<digit> ]
+bool Datetime::initializeLater (Pig& pig)
+{
+  auto checkpoint = pig.cursor ();
+
+  std::string token;
+  if ((pig.skipPartial ("later", token) &&
+      token.length () >= static_cast <std::string::size_type> (Datetime::minimumMatchLength))
+
+      ||
+
+     (pig.skipPartial ("someday", token) &&
+      token.length () >= static_cast <std::string::size_type> (std::max (Datetime::minimumMatchLength, 4))))
+  {
+    auto following = pig.peek ();
+    if (! unicodeLatinAlpha (following) &&
+        ! unicodeLatinDigit (following))
+    {
+      time_t now = time (nullptr);
+      struct tm* t = localtime (&now);
+
+      t->tm_hour = t->tm_min = t->tm_sec = 0;
+      t->tm_year = 138;
+      t->tm_mon = 0;
+      t->tm_mday = 18;
+      t->tm_isdst = -1;
+      _date = mktime (t);
+      return true;
+    }
+  }
+
+  pig.restoreTo (checkpoint);
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// sopd [ !<alpha> && !<digit> ]
+bool Datetime::initializeSopd (Pig& pig)
+{
+  auto checkpoint = pig.cursor ();
+
+  if (pig.skipLiteral ("sopd"))
+  {
+    auto following = pig.peek ();
+    if (! unicodeLatinAlpha (following) &&
+        ! unicodeLatinDigit (following))
+    {
+      time_t now = time (nullptr);
+      struct tm* t = localtime (&now);
+
+      t->tm_hour = t->tm_min = t->tm_sec = 0;
+      t->tm_isdst = -1;
+      t->tm_mday -= 1;
+      _date = mktime (t);
+      return true;
+    }
+  }
+
+  pig.restoreTo (checkpoint);
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// sod [ !<alpha> && !<digit> ]
+bool Datetime::initializeSod (Pig& pig)
+{
+  auto checkpoint = pig.cursor ();
+
+  if (pig.skipLiteral ("sod"))
+  {
+    auto following = pig.peek ();
+    if (! unicodeLatinAlpha (following) &&
+        ! unicodeLatinDigit (following))
+    {
+      time_t now = time (nullptr);
+      struct tm* t = localtime (&now);
+
+      t->tm_hour = t->tm_min = t->tm_sec = 0;
+      t->tm_isdst = -1;
+      _date = mktime (t);
+      return true;
+    }
+  }
+
+  pig.restoreTo (checkpoint);
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// sond [ !<alpha> && !<digit> ]
+bool Datetime::initializeSond (Pig& pig)
+{
+  auto checkpoint = pig.cursor ();
+
+  if (pig.skipLiteral ("sond"))
+  {
+    auto following = pig.peek ();
+    if (! unicodeLatinAlpha (following) &&
+        ! unicodeLatinDigit (following))
+    {
+      time_t now = time (nullptr);
+      struct tm* t = localtime (&now);
+
+      t->tm_mday++;
+      t->tm_hour = t->tm_min = t->tm_sec = 0;
+      t->tm_isdst = -1;
+      _date = mktime (t);
+      return true;
+    }
+  }
+
+  pig.restoreTo (checkpoint);
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// eopd [ !<alpha> && !<digit> ]
+bool Datetime::initializeEopd (Pig& pig)
+{
+  auto checkpoint = pig.cursor ();
+
+  if (pig.skipLiteral ("eopd"))
+  {
+    auto following = pig.peek ();
+    if (! unicodeLatinAlpha (following) &&
+        ! unicodeLatinDigit (following))
+    {
+      time_t now = time (nullptr);
+      struct tm* t = localtime (&now);
+
+      t->tm_hour = t->tm_min = t->tm_sec = 0;
+      t->tm_isdst = -1;
+      _date = mktime (t);
+      return true;
+    }
+  }
+
+  pig.restoreTo (checkpoint);
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// eod [ !<alpha> && !<digit> ]
+bool Datetime::initializeEod (Pig& pig)
+{
+  auto checkpoint = pig.cursor ();
+
+  if (pig.skipLiteral ("eod"))
+  {
+    auto following = pig.peek ();
+    if (! unicodeLatinAlpha (following) &&
+        ! unicodeLatinDigit (following))
+    {
+      time_t now = time (nullptr);
+      struct tm* t = localtime (&now);
+
+      t->tm_mday++;
+      t->tm_hour = t->tm_min = t->tm_sec = 0;
+      t->tm_isdst = -1;
+      _date = mktime (t);
+      return true;
+    }
+  }
+
+  pig.restoreTo (checkpoint);
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// eond [ !<alpha> && !<digit> ]
+bool Datetime::initializeEond (Pig& pig)
+{
+  auto checkpoint = pig.cursor ();
+
+  if (pig.skipLiteral ("eond"))
+  {
+    auto following = pig.peek ();
+    if (! unicodeLatinAlpha (following) &&
+        ! unicodeLatinDigit (following))
+    {
+      time_t now = time (nullptr);
+      struct tm* t = localtime (&now);
+
+      t->tm_mday += 2;
+      t->tm_hour = t->tm_min = t->tm_sec = 0;
+      t->tm_isdst = -1;
+      _date = mktime (t);
+      return true;
+    }
+  }
+
+  pig.restoreTo (checkpoint);
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// sopw [ !<alpha> && !<digit> ]
+bool Datetime::initializeSopw (Pig& pig)
+{
+  auto checkpoint = pig.cursor ();
+
+  if (pig.skipLiteral ("sopw"))
+  {
+    auto following = pig.peek ();
+    if (! unicodeLatinAlpha (following) &&
+        ! unicodeLatinDigit (following))
+    {
+      time_t now = time (nullptr);
+      struct tm* t = localtime (&now);
+      t->tm_hour = t->tm_min = t->tm_sec = 0;
+
+      int extra = (t->tm_wday + 6) % 7;
+      t->tm_mday -= extra;
+      t->tm_mday -= 7;
+
+      t->tm_isdst = -1;
+      _date = mktime (t);
+      return true;
+    }
+  }
+
+  pig.restoreTo (checkpoint);
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// sow [ !<alpha> && !<digit> ]
+bool Datetime::initializeSow (Pig& pig)
+{
+  auto checkpoint = pig.cursor ();
+
+  if (pig.skipLiteral ("sow"))
+  {
+    auto following = pig.peek ();
+    if (! unicodeLatinAlpha (following) &&
+        ! unicodeLatinDigit (following))
+    {
+      time_t now = time (nullptr);
+      struct tm* t = localtime (&now);
+      t->tm_hour = t->tm_min = t->tm_sec = 0;
+
+      int extra = (t->tm_wday + 6) % 7;
+      t->tm_mday -= extra;
+
+      t->tm_isdst = -1;
+      _date = mktime (t);
+      return true;
+    }
+  }
+
+  pig.restoreTo (checkpoint);
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// sonw [ !<alpha> && !<digit> ]
+bool Datetime::initializeSonw (Pig& pig)
+{
+  auto checkpoint = pig.cursor ();
+
+  if (pig.skipLiteral ("sonw"))
+  {
+    auto following = pig.peek ();
+    if (! unicodeLatinAlpha (following) &&
+        ! unicodeLatinDigit (following))
+    {
+      time_t now = time (nullptr);
+      struct tm* t = localtime (&now);
+      t->tm_hour = t->tm_min = t->tm_sec = 0;
+
+      int extra = (t->tm_wday + 6) % 7;
+      t->tm_mday -= extra;
+      t->tm_mday += 7;
+
+      t->tm_isdst = -1;
+      _date = mktime (t);
+      return true;
+    }
+  }
+
+  pig.restoreTo (checkpoint);
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// eopw [ !<alpha> && !<digit> ]
+bool Datetime::initializeEopw (Pig& pig)
+{
+  auto checkpoint = pig.cursor ();
+
+  if (pig.skipLiteral ("eopw"))
+  {
+    auto following = pig.peek ();
+    if (! unicodeLatinAlpha (following) &&
+        ! unicodeLatinDigit (following))
+    {
+      time_t now = time (nullptr);
+      struct tm* t = localtime (&now);
+      t->tm_hour = t->tm_min = t->tm_sec = 0;
+
+      int extra = (t->tm_wday + 6) % 7;
+      t->tm_mday -= extra;
+
+      t->tm_isdst = -1;
+      _date = mktime (t);
+      return true;
+    }
+  }
+
+  pig.restoreTo (checkpoint);
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// eow [ !<alpha> && !<digit> ]
+bool Datetime::initializeEow (Pig& pig)
+{
+  auto checkpoint = pig.cursor ();
+
+  if (pig.skipLiteral ("eow"))
+  {
+    auto following = pig.peek ();
+    if (! unicodeLatinAlpha (following) &&
+        ! unicodeLatinDigit (following))
+    {
+      time_t now = time (nullptr);
+      struct tm* t = localtime (&now);
+      t->tm_hour = t->tm_min = t->tm_sec = 0;
+
+      int extra = (t->tm_wday + 6) % 7;
+      t->tm_mday -= extra;
+      t->tm_mday += 7;
+
+      t->tm_isdst = -1;
+      _date = mktime (t);
+      return true;
+    }
+  }
+
+  pig.restoreTo (checkpoint);
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// eonw [ !<alpha> && !<digit> ]
+bool Datetime::initializeEonw (Pig& pig)
+{
+  auto checkpoint = pig.cursor ();
+
+  if (pig.skipLiteral ("eonw"))
+  {
+    auto following = pig.peek ();
+    if (! unicodeLatinAlpha (following) &&
+        ! unicodeLatinDigit (following))
+    {
+      time_t now = time (nullptr);
+      struct tm* t = localtime (&now);
+
+      t->tm_mday += 15 - t->tm_wday;
+      t->tm_hour = t->tm_min = t->tm_sec = 0;
+      t->tm_isdst = -1;
+      _date = mktime (t);
+      return true;
+    }
+  }
+
+  pig.restoreTo (checkpoint);
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// sopww [ !<alpha> && !<digit> ]
+bool Datetime::initializeSopww (Pig& pig)
+{
+  auto checkpoint = pig.cursor ();
+
+  if (pig.skipLiteral ("sopww"))
+  {
+    auto following = pig.peek ();
+    if (! unicodeLatinAlpha (following) &&
+        ! unicodeLatinDigit (following))
+    {
+      time_t now = time (nullptr);
+      struct tm* t = localtime (&now);
+
+      t->tm_mday += -6 - t->tm_wday;
+      t->tm_hour = t->tm_min = t->tm_sec = 0;
+      t->tm_isdst = -1;
+      _date = mktime (t);
+      return true;
+    }
+  }
+
+  pig.restoreTo (checkpoint);
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// soww [ !<alpha> && !<digit> ]
+bool Datetime::initializeSoww (Pig& pig)
+{
+  auto checkpoint = pig.cursor ();
+
+  if (pig.skipLiteral ("soww"))
+  {
+    auto following = pig.peek ();
+    if (! unicodeLatinAlpha (following) &&
+        ! unicodeLatinDigit (following))
+    {
+      time_t now = time (nullptr);
+      struct tm* t = localtime (&now);
+
+      t->tm_mday += 8 - t->tm_wday;
+      t->tm_hour = t->tm_min = t->tm_sec = 0;
+      t->tm_isdst = -1;
+      _date = mktime (t);
+      return true;
+    }
+  }
+
+  pig.restoreTo (checkpoint);
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// sonww [ !<alpha> && !<digit> ]
+bool Datetime::initializeSonww (Pig& pig)
+{
+  auto checkpoint = pig.cursor ();
+
+  if (pig.skipLiteral ("sonww"))
+  {
+    auto following = pig.peek ();
+    if (! unicodeLatinAlpha (following) &&
+        ! unicodeLatinDigit (following))
+    {
+      time_t now = time (nullptr);
+      struct tm* t = localtime (&now);
+
+      t->tm_mday += 8 - t->tm_wday;
+      t->tm_hour = t->tm_min = t->tm_sec = 0;
+      t->tm_isdst = -1;
+      _date = mktime (t);
+      return true;
+    }
+  }
+
+  pig.restoreTo (checkpoint);
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// eopww [ !<alpha> && !<digit> ]
+bool Datetime::initializeEopww (Pig& pig)
+{
+  auto checkpoint = pig.cursor ();
+
+  if (pig.skipLiteral ("eopww"))
+  {
+    auto following = pig.peek ();
+    if (! unicodeLatinAlpha (following) &&
+        ! unicodeLatinDigit (following))
+    {
+      time_t now = time (nullptr);
+      struct tm* t = localtime (&now);
+
+      t->tm_mday -= (t->tm_wday + 1) % 7;
+      t->tm_hour = t->tm_min = t->tm_sec = 0;
+      t->tm_isdst = -1;
+      _date = mktime (t);
+      return true;
+    }
+  }
+
+  pig.restoreTo (checkpoint);
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// eoww [ !<alpha> && !<digit> ]
+bool Datetime::initializeEoww (Pig& pig)
+{
+  auto checkpoint = pig.cursor ();
+
+  if (pig.skipLiteral ("eoww"))
+  {
+    auto following = pig.peek ();
+    if (! unicodeLatinAlpha (following) &&
+        ! unicodeLatinDigit (following))
+    {
+      time_t now = time (nullptr);
+      struct tm* t = localtime (&now);
+
+      t->tm_mday += 6 - t->tm_wday;
+      t->tm_hour = t->tm_min = t->tm_sec = 0;
+      t->tm_isdst = -1;
+      _date = mktime (t);
+      return true;
+    }
+  }
+
+  pig.restoreTo (checkpoint);
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// eonww [ !<alpha> && !<digit> ]
+bool Datetime::initializeEonww (Pig& pig)
+{
+  auto checkpoint = pig.cursor ();
+
+  if (pig.skipLiteral ("eonww"))
+  {
+    auto following = pig.peek ();
+    if (! unicodeLatinAlpha (following) &&
+        ! unicodeLatinDigit (following))
+    {
+      time_t now = time (nullptr);
+      struct tm* t = localtime (&now);
+
+      t->tm_mday += 13 - t->tm_wday;
+      t->tm_hour = t->tm_min = t->tm_sec = 0;
+      t->tm_isdst = -1;
+      _date = mktime (t);
+      return true;
+    }
+  }
+
+  pig.restoreTo (checkpoint);
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// sopm [ !<alpha> && !<digit> ]
+bool Datetime::initializeSopm (Pig& pig)
+{
+  auto checkpoint = pig.cursor ();
+
+  if (pig.skipLiteral ("sopm"))
+  {
+    auto following = pig.peek ();
+    if (! unicodeLatinAlpha (following) &&
+        ! unicodeLatinDigit (following))
+    {
+      time_t now = time (nullptr);
+      struct tm* t = localtime (&now);
+
+      t->tm_hour = t->tm_min = t->tm_sec = 0;
+
+      if (t->tm_mon == 0)
+      {
+        t->tm_year--;
+        t->tm_mon = 11;
+      }
+      else
+        t->tm_mon--;
+
+      t->tm_mday = 1;
+      t->tm_isdst = -1;
+      _date = mktime (t);
+      return true;
+    }
+  }
+
+  pig.restoreTo (checkpoint);
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// som [ !<alpha> && !<digit> ]
+bool Datetime::initializeSom (Pig& pig)
+{
+  auto checkpoint = pig.cursor ();
+
+  if (pig.skipLiteral ("som"))
+  {
+    auto following = pig.peek ();
+    if (! unicodeLatinAlpha (following) &&
+        ! unicodeLatinDigit (following))
+    {
+      time_t now = time (nullptr);
+      struct tm* t = localtime (&now);
+
+      t->tm_hour = t->tm_min = t->tm_sec = 0;
+      t->tm_mday = 1;
+      t->tm_isdst = -1;
+      _date = mktime (t);
+      return true;
+    }
+  }
+
+  pig.restoreTo (checkpoint);
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// sonm [ !<alpha> && !<digit> ]
+bool Datetime::initializeSonm (Pig& pig)
+{
+  auto checkpoint = pig.cursor ();
+
+  if (pig.skipLiteral ("sonm"))
+  {
+    auto following = pig.peek ();
+    if (! unicodeLatinAlpha (following) &&
+        ! unicodeLatinDigit (following))
+    {
+      time_t now = time (nullptr);
+      struct tm* t = localtime (&now);
+
+      t->tm_hour = t->tm_min = t->tm_sec = 0;
+
+      t->tm_mon++;
+      if (t->tm_mon > 11)
+      {
+        t->tm_year++;
+        t->tm_mon = 0;
+      }
+
+      t->tm_mday = 1;
+      t->tm_isdst = -1;
+      _date = mktime (t);
+      return true;
+    }
+  }
+
+  pig.restoreTo (checkpoint);
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// eopm [ !<alpha> && !<digit> ]
+bool Datetime::initializeEopm (Pig& pig)
+{
+  auto checkpoint = pig.cursor ();
+
+  if (pig.skipLiteral ("eopm"))
+  {
+    auto following = pig.peek ();
+    if (! unicodeLatinAlpha (following) &&
+        ! unicodeLatinDigit (following))
+    {
+      time_t now = time (nullptr);
+      struct tm* t = localtime (&now);
+
+      t->tm_hour = t->tm_min = t->tm_sec = 0;
+      t->tm_mday = 1;
+      t->tm_isdst = -1;
+      _date = mktime (t);
+      return true;
+    }
+  }
+
+  pig.restoreTo (checkpoint);
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// eom [ !<alpha> && !<digit> ]
+bool Datetime::initializeEom (Pig& pig)
+{
+  auto checkpoint = pig.cursor ();
+
+  if (pig.skipLiteral ("eom"))
+  {
+    auto following = pig.peek ();
+    if (! unicodeLatinAlpha (following) &&
+        ! unicodeLatinDigit (following))
+    {
+      time_t now = time (nullptr);
+      struct tm* t = localtime (&now);
+
+      t->tm_hour = t->tm_min = t->tm_sec = 0;
+
+      t->tm_mon++;
+      if (t->tm_mon > 11)
+      {
+        t->tm_year++;
+        t->tm_mon = 0;
+      }
+
+      t->tm_mday = 1;
+      t->tm_isdst = -1;
+      _date = mktime (t);
+      return true;
+    }
+  }
+
+  pig.restoreTo (checkpoint);
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// eonm [ !<alpha> && !<digit> ]
+bool Datetime::initializeEonm (Pig& pig)
+{
+  auto checkpoint = pig.cursor ();
+
+  if (pig.skipLiteral ("eonm"))
+  {
+    auto following = pig.peek ();
+    if (! unicodeLatinAlpha (following) &&
+        ! unicodeLatinDigit (following))
+    {
+      time_t now = time (nullptr);
+      struct tm* t = localtime (&now);
+
+      t->tm_hour = t->tm_min = t->tm_sec = 0;
+      t->tm_mday = 1;
+      t->tm_mon += 2;
+      if (t->tm_mon > 11)
+      {
+        t->tm_year++;
+        t->tm_mon -= 12;
+      }
+
+      t->tm_isdst = -1;
+      _date = mktime (t);
+      return true;
+    }
+  }
+
+  pig.restoreTo (checkpoint);
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// sopq [ !<alpha> && !<digit> ]
+bool Datetime::initializeSopq (Pig& pig)
+{
+  auto checkpoint = pig.cursor ();
+
+  if (pig.skipLiteral ("sopq"))
+  {
+    auto following = pig.peek ();
+    if (! unicodeLatinAlpha (following) &&
+        ! unicodeLatinDigit (following))
+    {
+      time_t now = time (nullptr);
+      struct tm* t = localtime (&now);
+
+      t->tm_mon -= t->tm_mon % 3;
+      t->tm_mon -= 3;
+      if (t->tm_mon < 0)
+      {
+        t->tm_mon += 12;
+        t->tm_year--;
+      }
+
+      t->tm_hour = t->tm_min = t->tm_sec = 0;
+      t->tm_mday = 1;
+      t->tm_isdst = -1;
+      _date = mktime (t);
+      return true;
+    }
+  }
+
+  pig.restoreTo (checkpoint);
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// soq [ !<alpha> && !<digit> ]
+bool Datetime::initializeSoq (Pig& pig)
+{
+  auto checkpoint = pig.cursor ();
+
+  if (pig.skipLiteral ("soq"))
+  {
+    auto following = pig.peek ();
+    if (! unicodeLatinAlpha (following) &&
+        ! unicodeLatinDigit (following))
+    {
+      time_t now = time (nullptr);
+      struct tm* t = localtime (&now);
+
+      t->tm_hour = t->tm_min = t->tm_sec = 0;
+      t->tm_mon -= t->tm_mon % 3;
+      t->tm_mday = 1;
+      t->tm_isdst = -1;
+      _date = mktime (t);
+      return true;
+    }
+  }
+
+  pig.restoreTo (checkpoint);
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// sonq [ !<alpha> && !<digit> ]
+bool Datetime::initializeSonq (Pig& pig)
+{
+  auto checkpoint = pig.cursor ();
+
+  if (pig.skipLiteral ("sonq"))
+  {
+    auto following = pig.peek ();
+    if (! unicodeLatinAlpha (following) &&
+        ! unicodeLatinDigit (following))
+    {
+      time_t now = time (nullptr);
+      struct tm* t = localtime (&now);
+
+      t->tm_mon += 3 - (t->tm_mon % 3);
+      if (t->tm_mon > 11)
+      {
+        t->tm_mon -= 12;
+        ++t->tm_year;
+      }
+
+      t->tm_hour = t->tm_min = t->tm_sec = 0;
+      t->tm_mday = 1;
+      t->tm_isdst = -1;
+      _date = mktime (t);
+      return true;
+    }
+  }
+
+  pig.restoreTo (checkpoint);
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// eopq [ !<alpha> && !<digit> ]
+bool Datetime::initializeEopq (Pig& pig)
+{
+  auto checkpoint = pig.cursor ();
+
+  if (pig.skipLiteral ("eopq"))
+  {
+    auto following = pig.peek ();
+    if (! unicodeLatinAlpha (following) &&
+        ! unicodeLatinDigit (following))
+    {
+      time_t now = time (nullptr);
+      struct tm* t = localtime (&now);
+
+      t->tm_hour = t->tm_min = t->tm_sec = 0;
+      t->tm_mon -= t->tm_mon % 3;
+      t->tm_mday = 1;
+      t->tm_isdst = -1;
+      _date = mktime (t);
+      return true;
+    }
+  }
+
+  pig.restoreTo (checkpoint);
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// eoq [ !<alpha> && !<digit> ]
+bool Datetime::initializeEoq (Pig& pig)
+{
+  auto checkpoint = pig.cursor ();
+
+  if (pig.skipLiteral ("eoq"))
+  {
+    auto following = pig.peek ();
+    if (! unicodeLatinAlpha (following) &&
+        ! unicodeLatinDigit (following))
+    {
+      time_t now = time (nullptr);
+      struct tm* t = localtime (&now);
+
+      t->tm_mon += 3 - (t->tm_mon % 3);
+      if (t->tm_mon > 11)
+      {
+        t->tm_mon -= 12;
+        ++t->tm_year;
+      }
+
+      t->tm_hour = t->tm_min = t->tm_sec = 0;
+      t->tm_mday = 1;
+      t->tm_isdst = -1;
+      _date = mktime (t);
+      return true;
+    }
+  }
+
+  pig.restoreTo (checkpoint);
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// eonq [ !<alpha> && !<digit> ]
+bool Datetime::initializeEonq (Pig& pig)
+{
+  auto checkpoint = pig.cursor ();
+
+  if (pig.skipLiteral ("eonq"))
+  {
+    auto following = pig.peek ();
+    if (! unicodeLatinAlpha (following) &&
+        ! unicodeLatinDigit (following))
+    {
+      time_t now = time (nullptr);
+      struct tm* t = localtime (&now);
+
+      t->tm_hour = t->tm_min = t->tm_sec = 0;
+      t->tm_mon += 6 - (t->tm_mon % 3);
+      if (t->tm_mon > 11)
+      {
+        t->tm_mon -= 12;
+        ++t->tm_year;
+      }
+
+      t->tm_mday = 1;
+      t->tm_isdst = -1;
+      _date = mktime (t);
+      return true;
+    }
+  }
+
+  pig.restoreTo (checkpoint);
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// sopy [ !<alpha> && !<digit> ]
+bool Datetime::initializeSopy (Pig& pig)
+{
+  auto checkpoint = pig.cursor ();
+
+  if (pig.skipLiteral ("sopy"))
+  {
+    auto following = pig.peek ();
+    if (! unicodeLatinAlpha (following) &&
+        ! unicodeLatinDigit (following))
+    {
+      time_t now = time (nullptr);
+      struct tm* t = localtime (&now);
+
+      t->tm_hour = t->tm_min = t->tm_sec = 0;
+      t->tm_mon = 0;
+      t->tm_mday = 1;
+      t->tm_year--;
+      t->tm_isdst = -1;
+      _date = mktime (t);
+      return true;
+    }
+  }
+
+  pig.restoreTo (checkpoint);
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// soy [ !<alpha> && !<digit> ]
+bool Datetime::initializeSoy (Pig& pig)
+{
+  auto checkpoint = pig.cursor ();
+
+  if (pig.skipLiteral ("soy"))
+  {
+    auto following = pig.peek ();
+    if (! unicodeLatinAlpha (following) &&
+        ! unicodeLatinDigit (following))
+    {
+      time_t now = time (nullptr);
+      struct tm* t = localtime (&now);
+
+      t->tm_hour = t->tm_min = t->tm_sec = 0;
+      t->tm_mon = 0;
+      t->tm_mday = 1;
+      t->tm_isdst = -1;
+      _date = mktime (t);
+      return true;
+    }
+  }
+
+  pig.restoreTo (checkpoint);
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// sony [ !<alpha> && !<digit> ]
+bool Datetime::initializeSony (Pig& pig)
+{
+  auto checkpoint = pig.cursor ();
+
+  if (pig.skipLiteral ("sony"))
+  {
+    auto following = pig.peek ();
+    if (! unicodeLatinAlpha (following) &&
+        ! unicodeLatinDigit (following))
+    {
+      time_t now = time (nullptr);
+      struct tm* t = localtime (&now);
+
+      t->tm_hour = t->tm_min = t->tm_sec = 0;
+      t->tm_mon = 0;
+      t->tm_mday = 1;
+      t->tm_year++;
+      t->tm_isdst = -1;
+      _date = mktime (t);
+      return true;
+    }
+  }
+
+  pig.restoreTo (checkpoint);
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// eopy [ !<alpha> && !<digit> ]
+bool Datetime::initializeEopy (Pig& pig)
+{
+  auto checkpoint = pig.cursor ();
+
+  if (pig.skipLiteral ("eopy"))
+  {
+    auto following = pig.peek ();
+    if (! unicodeLatinAlpha (following) &&
+        ! unicodeLatinDigit (following))
+    {
+      time_t now = time (nullptr);
+      struct tm* t = localtime (&now);
+
+      t->tm_hour = t->tm_min = t->tm_sec = 0;
+      t->tm_mon = 0;
+      t->tm_mday = 1;
+      t->tm_isdst = -1;
+      _date = mktime (t);
+      return true;
+    }
+  }
+
+  pig.restoreTo (checkpoint);
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// eoy [ !<alpha> && !<digit> ]
+bool Datetime::initializeEoy (Pig& pig)
+{
+  auto checkpoint = pig.cursor ();
+
+  if (pig.skipLiteral ("eoy"))
+  {
+    auto following = pig.peek ();
+    if (! unicodeLatinAlpha (following) &&
+        ! unicodeLatinDigit (following))
+    {
+      time_t now = time (nullptr);
+      struct tm* t = localtime (&now);
+
+      t->tm_hour = t->tm_min = t->tm_sec = 0;
+      t->tm_mon = 0;
+      t->tm_mday = 1;
+      t->tm_year++;
+      t->tm_isdst = -1;
+      _date = mktime (t);
+      return true;
+    }
+  }
+
+  pig.restoreTo (checkpoint);
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// eony [ !<alpha> && !<digit> ]
+bool Datetime::initializeEony (Pig& pig)
+{
+  auto checkpoint = pig.cursor ();
+
+  if (pig.skipLiteral ("eony"))
+  {
+    auto following = pig.peek ();
+    if (! unicodeLatinAlpha (following) &&
+        ! unicodeLatinDigit (following))
+    {
+      time_t now = time (nullptr);
+      struct tm* t = localtime (&now);
+
+      t->tm_hour = t->tm_min = t->tm_sec = 0;
+      t->tm_mon = 0;
+      t->tm_mday = 1;
+      t->tm_year += 2;
+      t->tm_isdst = -1;
+      _date = mktime (t);
+      return true;
+    }
+  }
+
+  pig.restoreTo (checkpoint);
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// easter       [ !<alpha> && !<digit> ]
+// eastermonday [ !<alpha> && !<digit> ]
+// ascension    [ !<alpha> && !<digit> ]
+// pentecost    [ !<alpha> && !<digit> ]
+// goodfriday   [ !<alpha> && !<digit> ]
+bool Datetime::initializeEaster (Pig& pig)
+{
+  auto checkpoint = pig.cursor ();
+
+  std::vector <std::string> holidays = {"eastermonday", "easter", "ascension", "pentecost", "goodfriday"};
+  std::vector <int>         offsets  = {             1,        0,          39,          49,           -2};
+
+  std::string token;
+  for (int holiday = 0; holiday < 5; ++holiday)
+  {
+   if (pig.skipLiteral (holidays[holiday]) &&
+       ! unicodeLatinAlpha (pig.peek ()) &&
+       ! unicodeLatinDigit (pig.peek ()))
+    {
+      time_t now = time (nullptr);
+      struct tm* t = localtime (&now);
+
+      easter (t);
+      _date = mktime (t);
+
+      // If the result is earlier this year, then recalc for next year.
+      if (_date < now)
+      {
+        t = localtime (&now);
+        t->tm_year++;
+        easter (t);
+      }
+
+      // Adjust according to holiday-specific offsets.
+      t->tm_mday += offsets[holiday];
+
+      _date = mktime (t);
+      return true;
+    }
+  }
+
+  pig.restoreTo (checkpoint);
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// midsommar [ !<alpha> && !<digit> ]
+bool Datetime::initializeMidsommar (Pig& pig)
+{
+  auto checkpoint = pig.cursor ();
+
+  if (pig.skipLiteral ("midsommar"))
+  {
+    auto following = pig.peek ();
+    if (! unicodeLatinAlpha (following) &&
+        ! unicodeLatinDigit (following))
+    {
+      time_t now = time (nullptr);
+      struct tm* t = localtime (&now);
+      midsommar (t);
+      _date = mktime (t);
+
+      // If the result is earlier this year, then recalc for next year.
+      if (_date < now)
+      {
+        t = localtime (&now);
+        t->tm_year++;
+        midsommar (t);
+      }
+
+      _date = mktime (t);
+      return true;
+    }
+  }
+
+  pig.restoreTo (checkpoint);
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// midsommarafton [ !<alpha> && !<digit> ]
+// juhannus [ !<alpha> && !<digit> ]
+bool Datetime::initializeMidsommarafton (Pig& pig)
+{
+  auto checkpoint = pig.cursor ();
+
+  if (pig.skipLiteral ("midsommarafton") ||
+      pig.skipLiteral ("juhannus"))
+  {
+    auto following = pig.peek ();
+    if (! unicodeLatinAlpha (following) &&
+        ! unicodeLatinDigit (following))
+    {
+      time_t now = time (nullptr);
+      struct tm* t = localtime (&now);
+      midsommarafton (t);
+      _date = mktime (t);
+
+      // If the result is earlier this year, then recalc for next year.
+      if (_date < now)
+      {
+        t = localtime (&now);
+        t->tm_year++;
+        midsommarafton (t);
+      }
+
+      _date = mktime (t);
+      return true;
+    }
+  }
+
+  pig.restoreTo (checkpoint);
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// 8am
+// 8a
+// 8:30am
+// 8:30a
+// 8:30
+//
+// \d+ [ : \d{2} ] [ am | a | pm | p ] [ !<alpha> && !<digit> && !: && !+ && !- ]
+//
+bool Datetime::initializeInformalTime (Pig& pig)
+{
+  auto checkpoint = pig.cursor ();
+
+  int digit = 0;
+  bool needDesignator = true;   // Require am/pm.
+  bool haveDesignator = false;  // Provided am/pm.
+  if (pig.getDigit (digit))
+  {
+    int hours = digit;
+    if (pig.getDigit (digit))
+      hours = 10 * hours + digit;
+
+    int minutes = 0;
+    int seconds = 0;
+    if (pig.skip (':'))
+    {
+      if (! pig.getDigit2 (minutes))
+      {
+        pig.restoreTo (checkpoint);
+        return false;
+      }
+
+      if (pig.skip (':'))
+      {
+        if (! pig.getDigits (seconds))
+        {
+          pig.restoreTo (checkpoint);
+          return false;
+        }
+      }
+
+      needDesignator = false;
+    }
+
+    if (pig.skipLiteral ("am") ||
+        pig.skipLiteral ("a"))
+    {
+      haveDesignator = true;
+      if (hours == 12)
+        hours = 0;
+    }
+
+    else if (pig.skipLiteral ("pm") ||
+             pig.skipLiteral ("p"))
+    {
+      // Note: '12pm is an exception:
+      //  12am = 0h
+      //  11am = 11h + 12h
+      //  12pm = 12h
+      //  1pm  = 1h + 12h
+      if (hours != 12)
+        hours += 12;
+
+      haveDesignator = true;
+    }
+
+    // Informal time needs to be terminated.
+    auto following = pig.peek ();
+    if (unicodeLatinAlpha (following) ||
+        unicodeLatinDigit (following) ||
+        following == ':'              ||
+        following == '-'              ||
+        following == '+')
+    {
+      pig.restoreTo (checkpoint);
+      return false;
+    }
+
+    if (haveDesignator || ! needDesignator)
+    {
+      // Midnight today + hours:minutes:seconds.
+      time_t now = time (nullptr);
+      struct tm* t = localtime (&now);
+
+      int now_seconds  = (t->tm_hour * 3600) + (t->tm_min * 60) + t->tm_sec;
+      int calc_seconds = (hours      * 3600) + (minutes   * 60) + seconds;
+
+      if (calc_seconds < now_seconds)
+        ++t->tm_mday;
+
+      // Basic validation.
+      if (hours   >= 0 && hours   < 24 &&
+          minutes >= 0 && minutes < 60 &&
+          seconds >= 0 && seconds < 60)
+      {
+        t->tm_hour = hours;
+        t->tm_min = minutes;
+        t->tm_sec = seconds;
+        t->tm_isdst = -1;
+        _date = mktime (t);
+
+        return true;
+      }
+    }
+  }
+
+  pig.restoreTo (checkpoint);
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+void Datetime::easter (struct tm* t) const
+{
+  int Y = t->tm_year + 1900;
+  int a = Y % 19;
+  int b = Y / 100;
+  int c = Y % 100;
+  int d = b / 4;
+  int e = b % 4;
+  int f = (b + 8) / 25;
+  int g = (b - f + 1) / 3;
+  int h = (19 * a + b - d - g + 15) % 30;
+  int i = c / 4;
+  int k = c % 4;
+  int L = (32 + 2 * e + 2 * i - h - k) % 7;
+  int m = (a + 11 * h + 22 * L) / 451;
+  int month = (h + L - 7 * m + 114) / 31;
+  int day = ((h + L - 7 * m + 114) % 31) + 1;
+
+  t->tm_isdst = -1;   // Requests that mktime determine summer time effect.
+  t->tm_mday  = day;
+  t->tm_mon   = month - 1;
+  t->tm_year  = Y - 1900;
+  t->tm_isdst = -1;
+  t->tm_hour = t->tm_min = t->tm_sec = 0;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+void Datetime::midsommar (struct tm* t) const
+{
+  t->tm_mon = 5;                          // June.
+  t->tm_mday = 20;                        // Saturday after 20th.
+  t->tm_hour = t->tm_min = t->tm_sec = 0; // Midnight.
+  t->tm_isdst = -1;                       // Probably DST, but check.
+
+  time_t then = mktime (t);               // Obtain the weekday of June 20th.
+  struct tm* mid = localtime (&then);
+  t->tm_mday += 6 - mid->tm_wday;         // How many days after 20th.
+}
+
+////////////////////////////////////////////////////////////////////////////////
+void Datetime::midsommarafton (struct tm* t) const
+{
+  t->tm_mon = 5;                          // June.
+  t->tm_mday = 19;                        // Saturday after 20th.
+  t->tm_hour = t->tm_min = t->tm_sec = 0; // Midnight.
+  t->tm_isdst = -1;                       // Probably DST, but check.
+
+  time_t then = mktime (t);               // Obtain the weekday of June 19th.
+  struct tm* mid = localtime (&then);
+  t->tm_mday += 5 - mid->tm_wday;         // How many days after 19th.
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Suggested date expressions:
+//   {ordinal} {day} in|of {month}
+//   last|past|next|this {day}
+//   last|past|next|this {month}
+//   last|past|next|this week
+//   last|past|next|this month
+//   last|past|next|this weekend
+//   last|past|next|this year
+//   {day} last|past|next|this week
+//   {day} [at] {time}
+//   {time} {day}
+//
+// Candidates:
+//   <dayname> <time>
+//   <time>
+//   tue 9am
+//   Friday before easter
+//   3 days before eom
+//   in the morning
+//   am|pm
+//   4pm
+//   noon
+//   midnight
+//   tomorrow in one year
+//   in two weeks
+//   2 weeks from now
+//   2 weeks ago tuesday
+//   thursday in 2 weeks
+//   last day next month
+//   10 days from today
+//   thursday before last weekend in may
+//   friday last full week in may
+//   3rd wednesday this month
+//   3 weeks after 2nd tuesday next month
+//   100 days from the beginning of the month
+//   10 days after last monday
+//   sunday in the evening
+//   in 6 hours
+//   6 in the morning
+//   kl 18
+//   feb 11
+//   11 feb
+//   2011-02-08
+//   11/19/2011
+//   next business day
+//   new moon
+//   full moon
+//   in 28 days
+//   3rd quarter
+//   week 23
+//   {number} {unit}
+//   - {number} {unit}
+//   {ordinal} {unit} in {larger-unit}
+//   end of day tomorrow
+//   end of {day}
+//   by {day}
+//   first thing {day}
+//
+
+////////////////////////////////////////////////////////////////////////////////
+// <ordinal> <weekday> in|of <month>
+bool Datetime::initializeNthDayInMonth (const std::vector <std::string>& tokens)
+{
+  if (tokens.size () == 4)
+  {
+    int ordinal {0};
+    if (isOrdinal (tokens[0], ordinal))
+    {
+      auto day = Datetime::dayOfWeek (tokens[1]);
+      if (day != -1)
+      {
+        if (tokens[2] == "in" ||
+            tokens[2] == "of")
+        {
+          auto month = Datetime::monthOfYear (tokens[3]);
+          if (month != -1)
+          {
+            std::cout << "# ordinal=" << ordinal << " day=" << day << " in month=" << month << '\n';
+
+            // TODO Assume 1st of the month
+            // TODO Assume current year
+            // TODO Determine the day
+            // TODO Project forwards/backwards, to the desired day
+            // TODO Add ((ordinal - 1) * 7) days
+
+            return true;
+          }
+        }
+      }
+    }
+  }
+
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool Datetime::isOrdinal (const std::string& token, int& ordinal)
+{
+  Pig p (token);
+  int number;
+  std::string suffix;
+  if (p.getDigits (number) &&
+      p.getRemainder (suffix))
+  {
+    if (((number >= 11 || number <= 13) && suffix == "th") ||
+        (number % 10 == 1               && suffix == "st") ||
+        (number % 10 == 2               && suffix == "nd") ||
+        (number % 10 == 3               && suffix == "rd") ||
+        (                                  suffix == "th"))
+    {
+      ordinal = number;
+      return true;
+    }
+  }
+
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Validation via simple range checking.
+bool Datetime::validate ()
+{
+  // _year;
+  if ((_year    && (_year    <   1900 || _year    >                                  2200)) ||
+      (_month   && (_month   <      1 || _month   >                                    12)) ||
+      (_week    && (_week    <      1 || _week    >                                    53)) ||
+      (_weekday && (_weekday <      0 || _weekday >                                     6)) ||
+      (_julian  && (_julian  <      1 || _julian  >          Datetime::daysInYear (_year))) ||
+      (_day     && (_day     <      1 || _day     > Datetime::daysInMonth (_year, _month))) ||
+      (_seconds && (_seconds <      1 || _seconds >                                 86400)) ||
+      (_offset  && (_offset  < -86400 || _offset  >                                 86400)))
+    return false;
+
+  return true;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// int tm_sec;       seconds (0 - 60)
+// int tm_min;       minutes (0 - 59)
+// int tm_hour;      hours (0 - 23)
+// int tm_mday;      day of month (1 - 31)
+// int tm_mon;       month of year (0 - 11)
+// int tm_year;      year - 1900
+// int tm_wday;      day of week (Sunday = 0)
+// int tm_yday;      day of year (0 - 365)
+// int tm_isdst;     is summer time in effect?
+// char *tm_zone;    abbreviation of timezone name
+// long tm_gmtoff;   offset from UTC in seconds
+void Datetime::resolve ()
+{
+  // Don't touch the original values.
+  int year    = _year;
+  int month   = _month;
+  int week    = _week;
+  int weekday = _weekday;
+  int julian  = _julian;
+  int day     = _day;
+  int seconds = _seconds;
+  int offset  = _offset;
+  bool utc    = _utc;
+
+  // Get current time.
+  time_t now = time (nullptr);
+
+  // A UTC offset needs to be accommodated.  Once the offset is subtracted,
+  // only local and UTC times remain.
+  if (offset)
+  {
+    seconds -= offset;
+    now -= offset;
+    utc = true;
+  }
+
+  // Get 'now' in the relevant location.
+  struct tm* t_now = utc ? gmtime (&now) : localtime (&now);
+
+  int seconds_now = (t_now->tm_hour * 3600) +
+                    (t_now->tm_min  *   60) +
+                     t_now->tm_sec;
+
+  // Project forward one day if the specified seconds are earlier in the day
+  // than the current seconds.
+  // TODO This does not cover the inverse case of subtracting 86400.
+  if (year    == 0           &&
+      month   == 0           &&
+      day     == 0           &&
+      week    == 0           &&
+      weekday == 0           &&
+      seconds < seconds_now)
+  {
+    seconds += 86400;
+  }
+
+  // Convert week + weekday --> julian.
+  if (week)
+  {
+    julian = (week * 7) + weekday - dayOfWeek (year, 1, 4) - 3;
+  }
+
+  // Provide default values for year, month, day.
+  else
+  {
+    // Default values for year, month, day:
+    //
+    // y   m   d  -->  y   m   d
+    // y   m   -  -->  y   m   1
+    // y   -   -  -->  y   1   1
+    // -   -   -  -->  now now now
+    //
+    if (year == 0)
+    {
+      year  = t_now->tm_year + 1900;
+      month = t_now->tm_mon + 1;
+      day   = t_now->tm_mday;
+    }
+    else
+    {
+      if (month == 0)
+      {
+        month = 1;
+        day   = 1;
+      }
+      else if (day == 0)
+        day = 1;
+    }
+  }
+
+  if (julian)
+  {
+    month = 1;
+    day = julian;
+  }
+
+  struct tm t {};
+  t.tm_isdst = -1;  // Requests that mktime/gmtime determine summer time effect.
+  t.tm_year = year - 1900;
+  t.tm_mon = month - 1;
+  t.tm_mday = day;
+
+  if (seconds > 86400)
+  {
+    int days = seconds / 86400;
+    t.tm_mday += days;
+    seconds %= 86400;
+  }
+
+  t.tm_hour = seconds / 3600;
+  t.tm_min = (seconds % 3600) / 60;
+  t.tm_sec = seconds % 60;
+
+  _date = utc ? timegm (&t) : mktime (&t);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+time_t Datetime::toEpoch () const
+{
+  return _date;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+std::string Datetime::toEpochString () const
+{
+  return format ("{1}", _date);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// 19980119T070000Z =  YYYYMMDDThhmmssZ
+std::string Datetime::toISO () const
+{
+  struct tm* t = gmtime (&_date);
+
+  std::stringstream iso;
+  iso << std::setw (4) << std::setfill ('0') << t->tm_year + 1900
+      << std::setw (2) << std::setfill ('0') << t->tm_mon + 1
+      << std::setw (2) << std::setfill ('0') << t->tm_mday
+      << 'T'
+      << std::setw (2) << std::setfill ('0') << t->tm_hour
+      << std::setw (2) << std::setfill ('0') << t->tm_min
+      << std::setw (2) << std::setfill ('0') << t->tm_sec
+      << 'Z';
+
+  return iso.str ();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// 1998-01-19T07:00:00 =  YYYY-MM-DDThh:mm:ss
+std::string Datetime::toISOLocalExtended () const
+{
+  struct tm* t = localtime (&_date);
+
+  std::stringstream iso;
+  iso << std::setw (4) << std::setfill ('0') << t->tm_year + 1900
+      << '-'
+      << std::setw (2) << std::setfill ('0') << t->tm_mon + 1
+      << '-'
+      << std::setw (2) << std::setfill ('0') << t->tm_mday
+      << 'T'
+      << std::setw (2) << std::setfill ('0') << t->tm_hour
+      << ':'
+      << std::setw (2) << std::setfill ('0') << t->tm_min
+      << ':'
+      << std::setw (2) << std::setfill ('0') << t->tm_sec;
+
+  return iso.str ();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+double Datetime::toJulian () const
+{
+  return (_date / 86400.0) + 2440587.5;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+void Datetime::toYMD (int& y, int& m, int& d) const
+{
+  struct tm* t = localtime (&_date);
+
+  m = t->tm_mon + 1;
+  d = t->tm_mday;
+  y = t->tm_year + 1900;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+const std::string Datetime::toString (const std::string& format) const
+{
+  std::stringstream formatted;
+  for (unsigned int i = 0; i < format.length (); ++i)
+  {
+    int c = format[i];
+    switch (c)
+    {
+    case 'm': formatted                                        << month ();               break;
+    case 'M': formatted << std::setw (2) << std::setfill ('0') << month ();               break;
+    case 'd': formatted                                        << day ();                 break;
+    case 'D': formatted << std::setw (2) << std::setfill ('0') << day ();                 break;
+    case 'y': formatted << std::setw (2) << std::setfill ('0') << (year () % 100);        break;
+    case 'Y': formatted                                        << year ();                break;
+    case 'a': formatted                                        << Datetime::dayNameShort (dayOfWeek ()); break;
+    case 'A': formatted                                        << Datetime::dayName (dayOfWeek ());      break;
+    case 'b': formatted                                        << Datetime::monthNameShort (month ());   break;
+    case 'B': formatted                                        << Datetime::monthName (month ());        break;
+    case 'v': formatted                                        << week ();                break;
+    case 'V': formatted << std::setw (2) << std::setfill ('0') << week ();                break;
+    case 'h': formatted                                        << hour ();                break;
+    case 'H': formatted << std::setw (2) << std::setfill ('0') << hour ();                break;
+    case 'n': formatted                                        << minute ();              break;
+    case 'N': formatted << std::setw (2) << std::setfill ('0') << minute ();              break;
+    case 's': formatted                                        << second ();              break;
+    case 'S': formatted << std::setw (2) << std::setfill ('0') << second ();              break;
+    case 'j': formatted                                        << dayOfYear ();           break;
+    case 'J': formatted << std::setw (3) << std::setfill ('0') << dayOfYear ();           break;
+    case 'w': formatted                                        << dayOfWeek ();           break;
+    default:  formatted                                        << static_cast <char> (c); break;
+    }
+  }
+
+  return formatted.str ();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+Datetime Datetime::startOfDay () const
+{
+  return Datetime (year (), month (), day ());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+Datetime Datetime::startOfWeek () const
+{
+  Datetime sow (_date);
+  sow -= (dayOfWeek () * 86400);
+  return Datetime (sow.year (), sow.month (), sow.day ());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+Datetime Datetime::startOfMonth () const
+{
+  return Datetime (year (), month (), 1);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+Datetime Datetime::startOfYear () const
+{
+  return Datetime (year (), 1, 1);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool Datetime::valid (const std::string& input, const std::string& format)
+{
+  try
+  {
+    Datetime test (input, format);
+  }
+
+  catch (...)
+  {
+    return false;
+  }
+
+  return true;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool Datetime::valid (
+  const int y, const int m, const int d,
+  const int hr, const int mi, const int se)
+{
+  if (hr < 0 || hr > 24)
+    return false;
+
+  if (mi < 0 || mi > 59)
+    return false;
+
+  if (se < 0 || se > 59)
+    return false;
+
+  if (hr == 24 &&
+      (mi != 0 ||
+       se != 0))
+    return false;
+
+  return Datetime::valid (y, m, d);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool Datetime::valid (const int y, const int m, const int d)
+{
+  // Check that the year is valid.
+  if (y < 0)
+    return false;
+
+  // Check that the month is valid.
+  if (m < 1 || m > 12)
+    return false;
+
+  // Finally check that the days fall within the acceptable range for this
+  // month, and whether or not this is a leap year.
+  if (d < 1 || d > Datetime::daysInMonth (y, m))
+    return false;
+
+  return true;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Julian
+bool Datetime::valid (const int y, const int d)
+{
+  // Check that the year is valid.
+  if (y < 0)
+    return false;
+
+  if (d < 1 || d > Datetime::daysInYear (y))
+    return false;
+
+  return true;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Static
+bool Datetime::leapYear (int year)
+{
+  return ((! (year % 4)) && (year % 100)) ||
+         ! (year % 400);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Static
+int Datetime::daysInMonth (int year, int month)
+{
+  // Protect against arguments being passed in the wrong order.
+  assert (year >= 1969 && year < 2100);
+  assert (month >= 1 && month <= 31);
+
+  static int days[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
+
+  if (month == 2 && Datetime::leapYear (year))
+    return 29;
+
+  return days[month - 1];
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Static
+int Datetime::daysInYear (int year)
+{
+  return Datetime::leapYear (year) ? 366 : 365;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Static
+std::string Datetime::monthName (int month)
+{
+  assert (month > 0);
+  assert (month <= 12);
+  return upperCaseFirst (monthNames[month - 1]);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Static
+std::string Datetime::monthNameShort (int month)
+{
+  assert (month > 0);
+  assert (month <= 12);
+  return upperCaseFirst (monthNames[month - 1]).substr (0, 3);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Static
+std::string Datetime::dayName (int dow)
+{
+  assert (dow >= 0);
+  assert (dow <= 6);
+  return upperCaseFirst (dayNames[dow]);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Static
+std::string Datetime::dayNameShort (int dow)
+{
+  assert (dow >= 0);
+  assert (dow <= 6);
+  return upperCaseFirst (dayNames[dow]).substr (0, 3);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Static
+int Datetime::dayOfWeek (const std::string& input)
+{
+  if (Datetime::minimumMatchLength== 0)
+    Datetime::minimumMatchLength = 3;
+
+  for (unsigned int i = 0; i < dayNames.size (); ++i)
+    if (closeEnough (dayNames[i], input, Datetime::minimumMatchLength))
+       return i;
+
+  return -1;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Using Zeller's Congruence.
+// Static
+int Datetime::dayOfWeek (int year, int month, int day)
+{
+  int adj = (14 - month) / 12;
+  int m = month + 12 * adj - 2;
+  int y = year - adj;
+  return (day + (13 * m - 1) / 5 + y + y / 4 - y / 100 + y / 400) % 7;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Static
+int Datetime::monthOfYear (const std::string& input)
+{
+  if (Datetime::minimumMatchLength== 0)
+    Datetime::minimumMatchLength = 3;
+
+  for (unsigned int i = 0; i < monthNames.size (); ++i)
+    if (closeEnough (monthNames[i], input, Datetime::minimumMatchLength))
+       return i + 1;
+
+  return -1;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Static
+int Datetime::length (const std::string& format)
+{
+  int len = 0;
+  for (auto& i : format)
+  {
+    switch (i)
+    {
+    case 'm':
+    case 'M':
+    case 'd':
+    case 'D':
+    case 'y':
+    case 'v':
+    case 'V':
+    case 'h':
+    case 'H':
+    case 'n':
+    case 'N':
+    case 's':
+    case 'S': len += 2;  break;
+    case 'b':
+    case 'j':
+    case 'J':
+    case 'a': len += 3;  break;
+    case 'Y': len += 4;  break;
+    case 'A':
+    case 'B': len += 10; break;
+
+    // Calculate the width, don't assume a single character width.
+    default:  len += mk_wcwidth (i); break;
+    }
+  }
+
+  return len;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+int Datetime::month () const
+{
+  struct tm* t = localtime (&_date);
+  return t->tm_mon + 1;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+int Datetime::week () const
+{
+  struct tm* t = localtime (&_date);
+
+  char weekStr[3];
+  if (Datetime::weekstart == 0)
+    strftime (weekStr, sizeof (weekStr), "%U", t);
+  else if (Datetime::weekstart == 1)
+    strftime (weekStr, sizeof (weekStr), "%V", t);
+  else
+    throw std::string ("The week may only start on a Sunday or Monday.");
+
+  int weekNumber = strtol (weekStr, nullptr, 10);
+  if (weekstart == 0)
+    weekNumber += 1;
+
+  return weekNumber;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+int Datetime::day () const
+{
+  struct tm* t = localtime (&_date);
+  return t->tm_mday;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+int Datetime::year () const
+{
+  struct tm* t = localtime (&_date);
+  return t->tm_year + 1900;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+int Datetime::dayOfWeek () const
+{
+  struct tm* t = localtime (&_date);
+  return t->tm_wday;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+int Datetime::dayOfYear () const
+{
+  struct tm* t = localtime (&_date);
+  return t->tm_yday + 1;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+int Datetime::hour () const
+{
+  struct tm* t = localtime (&_date);
+  return t->tm_hour;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+int Datetime::minute () const
+{
+  struct tm* t = localtime (&_date);
+  return t->tm_min;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+int Datetime::second () const
+{
+  struct tm* t = localtime (&_date);
+  return t->tm_sec;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool Datetime::operator== (const Datetime& rhs) const
+{
+  return rhs._date == _date;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool Datetime::operator!= (const Datetime& rhs) const
+{
+  return rhs._date != _date;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool Datetime::operator< (const Datetime& rhs) const
+{
+  return _date < rhs._date;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool Datetime::operator> (const Datetime& rhs) const
+{
+  return _date > rhs._date;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool Datetime::operator<= (const Datetime& rhs) const
+{
+  return _date <= rhs._date;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool Datetime::operator>= (const Datetime& rhs) const
+{
+  return _date >= rhs._date;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool Datetime::sameHour (const Datetime& rhs) const
+{
+  return year ()  == rhs.year ()  &&
+         month () == rhs.month () &&
+         day ()   == rhs.day ()   &&
+         hour ()  == rhs.hour ();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool Datetime::sameDay (const Datetime& rhs) const
+{
+  return year ()  == rhs.year ()  &&
+         month () == rhs.month () &&
+         day ()   == rhs.day ();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool Datetime::sameWeek (const Datetime& rhs) const
+{
+  return year () == rhs.year () &&
+         week () == rhs.week ();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool Datetime::sameMonth (const Datetime& rhs) const
+{
+  return year ()  == rhs.year () &&
+         month () == rhs.month ();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool Datetime::sameQuarter (const Datetime& rhs) const
+{
+  return year () == rhs.year () &&
+         ((month () - 1) / 3) == ((rhs.month () - 1) / 3);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool Datetime::sameYear (const Datetime& rhs) const
+{
+  return year () == rhs.year ();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+Datetime Datetime::operator+ (const int delta)
+{
+  return Datetime (_date + delta);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+Datetime Datetime::operator- (const int delta)
+{
+  return Datetime (_date - delta);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+Datetime& Datetime::operator+= (const int delta)
+{
+  _date += (time_t) delta;
+  return *this;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+Datetime& Datetime::operator-= (const int delta)
+{
+  _date -= (time_t) delta;
+  return *this;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+time_t Datetime::operator- (const Datetime& rhs)
+{
+  return _date - rhs._date;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Prefix decrement by one day.
+void Datetime::operator-- ()
+{
+  Datetime yesterday = startOfDay () - 1;
+  yesterday = Datetime (yesterday.year (),
+                        yesterday.month (),
+                        yesterday.day (),
+                        hour (),
+                        minute (),
+                        second ());
+  _date = yesterday._date;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Postfix decrement by one day.
+void Datetime::operator-- (int)
+{
+  Datetime yesterday = startOfDay () - 1;
+  yesterday = Datetime (yesterday.year (),
+                        yesterday.month (),
+                        yesterday.day (),
+                        hour (),
+                        minute (),
+                        second ());
+  _date = yesterday._date;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Prefix increment by one day.
+void Datetime::operator++ ()
+{
+  Datetime tomorrow = (startOfDay () + 90001).startOfDay ();
+  tomorrow = Datetime (tomorrow.year (),
+                       tomorrow.month (),
+                       tomorrow.day (),
+                       hour (),
+                       minute (),
+                       second ());
+  _date = tomorrow._date;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Postfix increment by one day.
+void Datetime::operator++ (int)
+{
+  Datetime tomorrow = (startOfDay () + 90001).startOfDay ();
+  tomorrow = Datetime (tomorrow.year (),
+                       tomorrow.month (),
+                       tomorrow.day (),
+                       hour (),
+                       minute (),
+                       second ());
+  _date = tomorrow._date;
+}
+
+////////////////////////////////////////////////////////////////////////////////
diff --git a/src/libshared/src/Datetime.h b/src/libshared/src/Datetime.h
new file mode 100644 (file)
index 0000000..1b9b82a
--- /dev/null
@@ -0,0 +1,215 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Copyright 2006 - 2017, Paul Beckingham, Federico Hernandez.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+// http://www.opensource.org/licenses/mit-license.php
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#ifndef INCLUDED_DATETIME
+#define INCLUDED_DATETIME
+
+#include <string>
+#include <ctime>
+#include <Pig.h>
+
+class Datetime
+{
+public:
+  static int weekstart;
+  static int minimumMatchLength;
+  static bool isoEnabled;
+  static bool standaloneDateEnabled;
+  static bool standaloneTimeEnabled;
+
+  Datetime ();
+  Datetime (const std::string&, const std::string& format = "");
+  Datetime (time_t);
+  Datetime (const int, const int, const int);
+  Datetime (const int, const int, const int, const int, const int, const int);
+  bool parse (const std::string&, std::string::size_type&, const std::string& format = "");
+  time_t toEpoch () const;
+  std::string toEpochString () const;
+  std::string toISO () const;
+  std::string toISOLocalExtended () const;
+  double toJulian () const;
+  void toYMD (int&, int&, int&) const;
+  const std::string toString (const std::string& format = "Y-M-D") const;
+
+  Datetime startOfDay () const;
+  Datetime startOfWeek () const;
+  Datetime startOfMonth () const;
+  Datetime startOfYear () const;
+
+  static bool valid (const std::string&, const std::string& format = "");
+  static bool valid (const int, const int, const int, const int, const int, const int);
+  static bool valid (const int, const int, const int);
+  static bool valid (const int, const int);
+  static bool leapYear (int);
+  static int daysInMonth (int, int);
+  static int daysInYear (int);
+  static std::string monthName (int);
+  static std::string monthNameShort (int);
+  static std::string dayName (int);
+  static std::string dayNameShort (int);
+  static int dayOfWeek (const std::string&);
+  static int dayOfWeek (int, int, int);
+  static int monthOfYear (const std::string&);
+  static int length (const std::string&);
+
+  int month () const;
+  int week () const;
+  int day () const;
+  int year () const;
+  int dayOfWeek () const;
+  int dayOfYear () const;
+  int hour () const;
+  int minute () const;
+  int second () const;
+
+  bool operator==  (const Datetime&) const;
+  bool operator!=  (const Datetime&) const;
+  bool operator<   (const Datetime&) const;
+  bool operator>   (const Datetime&) const;
+  bool operator<=  (const Datetime&) const;
+  bool operator>=  (const Datetime&) const;
+  bool sameHour    (const Datetime&) const;
+  bool sameDay     (const Datetime&) const;
+  bool sameWeek    (const Datetime&) const;
+  bool sameMonth   (const Datetime&) const;
+  bool sameQuarter (const Datetime&) const;
+  bool sameYear    (const Datetime&) const;
+  Datetime operator+  (const int);
+  Datetime operator-  (const int);
+  Datetime& operator+= (const int);
+  Datetime& operator-= (const int);
+  time_t operator- (const Datetime&);
+  void operator--  ();    // Prefix
+  void operator--  (int); // Postfix
+  void operator++  ();    // Prefix
+  void operator++  (int); // Postfix
+
+private:
+  void clear ();
+  bool parse_formatted     (Pig&, const std::string&);
+  bool parse_named         (Pig&);
+  bool parse_epoch         (Pig&);
+  bool parse_date_time_ext (Pig&);
+  bool parse_date_ext      (Pig&);
+  bool parse_off_ext       (Pig&);
+  bool parse_time_ext      (Pig&, bool terminated = true);
+  bool parse_time_utc_ext  (Pig&);
+  bool parse_time_off_ext  (Pig&);
+  bool parse_date_time     (Pig&);
+  bool parse_date          (Pig&);
+  bool parse_time_utc      (Pig&);
+  bool parse_time_off      (Pig&);
+  bool parse_time          (Pig&, bool terminated = true);
+  bool parse_off           (Pig&);
+
+  bool parse_year          (Pig&, int&);
+  bool parse_month         (Pig&, int&);
+  bool parse_week          (Pig&, int&);
+  bool parse_julian        (Pig&, int&);
+  bool parse_day           (Pig&, int&);
+  bool parse_weekday       (Pig&, int&);
+  bool parse_hour          (Pig&, int&);
+  bool parse_minute        (Pig&, int&);
+  bool parse_second        (Pig&, int&);
+  bool parse_off_hour      (Pig&, int&);
+  bool parse_off_minute    (Pig&, int&);
+
+  bool initializeNow            (Pig&);
+  bool initializeYesterday      (Pig&);
+  bool initializeToday          (Pig&);
+  bool initializeTomorrow       (Pig&);
+  bool initializeOrdinal        (Pig&);
+  bool initializeDayName        (Pig&);
+  bool initializeMonthName      (Pig&);
+  bool initializeLater          (Pig&);
+  bool initializeSopd           (Pig&);
+  bool initializeSod            (Pig&);
+  bool initializeSond           (Pig&);
+  bool initializeEopd           (Pig&);
+  bool initializeEod            (Pig&);
+  bool initializeEond           (Pig&);
+  bool initializeSopw           (Pig&);
+  bool initializeSow            (Pig&);
+  bool initializeSonw           (Pig&);
+  bool initializeEopw           (Pig&);
+  bool initializeEow            (Pig&);
+  bool initializeEonw           (Pig&);
+  bool initializeSopww          (Pig&);
+  bool initializeSonww          (Pig&);
+  bool initializeSoww           (Pig&);
+  bool initializeEopww          (Pig&);
+  bool initializeEonww          (Pig&);
+  bool initializeEoww           (Pig&);
+  bool initializeSopm           (Pig&);
+  bool initializeSom            (Pig&);
+  bool initializeSonm           (Pig&);
+  bool initializeEopm           (Pig&);
+  bool initializeEom            (Pig&);
+  bool initializeEonm           (Pig&);
+  bool initializeSopq           (Pig&);
+  bool initializeSoq            (Pig&);
+  bool initializeSonq           (Pig&);
+  bool initializeEopq           (Pig&);
+  bool initializeEoq            (Pig&);
+  bool initializeEonq           (Pig&);
+  bool initializeSopy           (Pig&);
+  bool initializeSoy            (Pig&);
+  bool initializeSony           (Pig&);
+  bool initializeEopy           (Pig&);
+  bool initializeEoy            (Pig&);
+  bool initializeEony           (Pig&);
+  bool initializeEaster         (Pig&);
+  bool initializeMidsommar      (Pig&);
+  bool initializeMidsommarafton (Pig&);
+  bool initializeInformalTime   (Pig&);
+  void easter (struct tm*) const;
+  void midsommar (struct tm*) const;
+  void midsommarafton (struct tm*) const;
+
+  bool initializeNthDayInMonth  (const std::vector <std::string>&);
+
+  bool isOrdinal (const std::string&, int&);
+
+  bool validate ();
+  void resolve ();
+  std::string dump () const;
+
+public:
+  int _year    {0};
+  int _month   {0};
+  int _week    {0};
+  int _weekday {0};
+  int _julian  {0};
+  int _day     {0};
+  int _seconds {0};
+  int _offset  {0};
+  bool _utc    {false};
+  time_t _date {0};
+};
+
+#endif
+
+////////////////////////////////////////////////////////////////////////////////
diff --git a/src/libshared/src/Duration.cpp b/src/libshared/src/Duration.cpp
new file mode 100644 (file)
index 0000000..fee8afe
--- /dev/null
@@ -0,0 +1,552 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Copyright 2006 - 2017, Paul Beckingham, Federico Hernandez.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+// http://www.opensource.org/licenses/mit-license.php
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include <cmake.h>
+#include <Duration.h>
+#include <unicode.h>
+#include <sstream>
+#include <iomanip>
+#include <vector>
+
+bool Duration::standaloneSecondsEnabled = true;
+
+#define DAY    86400
+#define HOUR    3600
+#define MINUTE    60
+#define SECOND     1
+
+static struct
+{
+  std::string unit;
+  int seconds;
+  bool standalone;
+} durations[] =
+{
+  // These are sorted by first character, then length, so that Pig::getOneOf
+  // returns a maximal match.
+  {"annual",     365 * DAY,    true },
+  {"biannual",   730 * DAY,    true },
+  {"bimonthly",   61 * DAY,    true },
+  {"biweekly",    14 * DAY,    true },
+  {"biyearly",   730 * DAY,    true },
+  {"daily",        1 * DAY,    true },
+  {"days",         1 * DAY,    false},
+  {"day",          1 * DAY,    true },
+  {"d",            1 * DAY,    false},
+  {"fortnight",   14 * DAY,    true },
+  {"hours",        1 * HOUR,   false},
+  {"hour",         1 * HOUR,   true },
+  {"hrs",          1 * HOUR,   false},
+  {"hr",           1 * HOUR,   true },
+  {"h",            1 * HOUR,   false},
+  {"minutes",      1 * MINUTE, false},
+  {"minute",       1 * MINUTE, true },
+  {"mins",         1 * MINUTE, false},
+  {"min",          1 * MINUTE, true },
+  {"monthly",     30 * DAY,    true },
+  {"months",      30 * DAY,    false},
+  {"month",       30 * DAY,    true },
+  {"mnths",       30 * DAY,    false},
+  {"mths",        30 * DAY,    false},
+  {"mth",         30 * DAY,    true },
+  {"mos",         30 * DAY,    false},
+  {"mo",          30 * DAY,    true },
+  {"m",           30 * DAY,    false},
+  {"quarterly",   91 * DAY,    true },
+  {"quarters",    91 * DAY,    false},
+  {"quarter",     91 * DAY,    true },
+  {"qrtrs",       91 * DAY,    false},
+  {"qrtr",        91 * DAY,    true },
+  {"qtrs",        91 * DAY,    false},
+  {"qtr",         91 * DAY,    true },
+  {"q",           91 * DAY,    false},
+  {"semiannual", 183 * DAY,    true },
+  {"sennight",    14 * DAY,    false},
+  {"seconds",      1 * SECOND, false},
+  {"second",       1 * SECOND, true },
+  {"secs",         1 * SECOND, false},
+  {"sec",          1 * SECOND, true },
+  {"s",            1 * SECOND, false},
+  {"weekdays",     1 * DAY,    true },
+  {"weekly",       7 * DAY,    true },
+  {"weeks",        7 * DAY,    false},
+  {"week",         7 * DAY,    true },
+  {"wks",          7 * DAY,    false},
+  {"wk",           7 * DAY,    true },
+  {"w",            7 * DAY,    false},
+  {"yearly",     365 * DAY,    true },
+  {"years",      365 * DAY,    false},
+  {"year",       365 * DAY,    true },
+  {"yrs",        365 * DAY,    false},
+  {"yr",         365 * DAY,    true },
+  {"y",          365 * DAY,    false},
+};
+
+#define NUM_DURATIONS (sizeof (durations) / sizeof (durations[0]))
+
+////////////////////////////////////////////////////////////////////////////////
+Duration::Duration ()
+{
+  clear ();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+Duration::Duration (const std::string& input)
+{
+  clear ();
+  std::string::size_type idx = 0;
+  parse (input, idx);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+Duration::Duration (time_t input)
+{
+  clear ();
+  _period = input;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool Duration::operator< (const Duration& other)
+{
+  return _period < other._period;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool Duration::operator> (const Duration& other)
+{
+  return _period > other._period;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool Duration::operator<= (const Duration& other)
+{
+  return _period <= other._period;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool Duration::operator>= (const Duration& other)
+{
+  return _period >= other._period;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+std::string Duration::toString () const
+{
+  std::stringstream s;
+  s << _period;
+  return s.str ();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+time_t Duration::toTime_t () const
+{
+  return _period;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool Duration::parse (const std::string& input, std::string::size_type& start)
+{
+  auto i = start;
+  Pig pig (input);
+  if (i)
+    pig.skipN (static_cast <int> (i));
+
+  if (Duration::standaloneSecondsEnabled && parse_seconds (pig))
+  {
+    // ::resolve is not needed in this case.
+    start = pig.cursor ();
+    return true;
+  }
+
+  else if (parse_designated (pig) ||
+           parse_weeks (pig)      ||
+           parse_units (pig))
+  {
+    start = pig.cursor ();
+    resolve ();
+    return true;
+  }
+
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool Duration::parse_seconds (Pig& pig)
+{
+  auto checkpoint = pig.cursor ();
+
+  int epoch {};
+  if (pig.getDigits (epoch)             &&
+      ! unicodeLatinAlpha (pig.peek ()) &&
+      (epoch == 0 ||
+       epoch > 60))
+  {
+    _period = static_cast <time_t> (epoch);
+    return true;
+  }
+
+  pig.restoreTo (checkpoint);
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// 'P' [nn 'Y'] [nn 'M'] [nn 'D'] ['T' [nn 'H'] [nn 'M'] [nn 'S']]
+bool Duration::parse_designated (Pig& pig)
+{
+  auto checkpoint = pig.cursor ();
+
+  if (pig.skip ('P') &&
+      ! pig.eos ())
+  {
+    int value;
+    pig.save ();
+    if (pig.getDigits (value) && pig.skip ('Y'))
+      _year = value;
+    else
+      pig.restore ();
+
+    pig.save ();
+    if (pig.getDigits (value) && pig.skip ('M'))
+      _month = value;
+    else
+      pig.restore ();
+
+    pig.save ();
+    if (pig.getDigits (value) && pig.skip ('D'))
+      _day = value;
+    else
+      pig.restore ();
+
+    if (pig.skip ('T') &&
+        ! pig.eos ())
+    {
+      pig.save ();
+      if (pig.getDigits (value) && pig.skip ('H'))
+        _hours = value;
+      else
+        pig.restore ();
+
+      pig.save ();
+      if (pig.getDigits (value) && pig.skip ('M'))
+        _minutes = value;
+      else
+        pig.restore ();
+
+      pig.save ();
+      if (pig.getDigits (value) && pig.skip ('S'))
+        _seconds = value;
+      else
+        pig.restore ();
+    }
+
+    auto following = pig.peek ();
+    if (pig.cursor () - checkpoint >= 3   &&
+        ! unicodeLatinAlpha (following) &&
+        ! unicodeLatinDigit (following))
+      return true;
+  }
+
+  pig.restoreTo (checkpoint);
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// 'P' [nn 'W']
+bool Duration::parse_weeks (Pig& pig)
+{
+  auto checkpoint = pig.cursor ();
+
+  if (pig.skip ('P') &&
+      ! pig.eos ())
+  {
+    int value;
+    pig.save ();
+    if (pig.getDigits (value) && pig.skip ('W'))
+      _weeks = value;
+    else
+      pig.restore ();
+
+    auto following = pig.peek ();
+    if (pig.cursor () - checkpoint >= 3   &&
+        ! unicodeLatinAlpha (following) &&
+        ! unicodeLatinDigit (following))
+      return true;
+  }
+
+  pig.restoreTo (checkpoint);
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool Duration::parse_units (Pig& pig)
+{
+  auto checkpoint = pig.cursor ();
+
+  // Static and so preserved between calls.
+  static std::vector <std::string> units;
+  if (units.size () == 0)
+    for (unsigned int i = 0; i < NUM_DURATIONS; i++)
+      units.push_back (durations[i].unit);
+
+  double number;
+  std::string unit;
+  if (pig.getOneOf (units, unit))
+  {
+    auto following = pig.peek ();
+    if (! unicodeLatinAlpha (following) &&
+        ! unicodeLatinDigit (following))
+    {
+      for (unsigned int i = 0; i < NUM_DURATIONS; i++)
+      {
+        if (durations[i].unit == unit &&
+            durations[i].standalone)
+        {
+          _period = static_cast <int> (durations[i].seconds);
+          return true;
+        }
+      }
+    }
+    else
+      pig.restoreTo (checkpoint);
+  }
+
+  else if (pig.getDecimal (number))
+  {
+    pig.skipWS ();
+    if (pig.getOneOf (units, unit))
+    {
+      // The "d" unit is a special case, because it is the only one that can
+      // legitimately occur at the beginning of a UUID, and be followed by an
+      // operator:
+      //
+      //   1111111d-0000-0000-0000-000000000000
+      //
+      // Because Lexer::isDuration is higher precedence than Lexer::isUUID,
+      // the above UUID looks like:
+      //
+      //   <1111111d> <-> ...
+      //   duration   op  ...
+      //
+      // So as a special case, durations, with units of "d" are rejected if the
+      // quantity exceeds 10000.
+      //
+      if (unit == "d" && number > 10000.0)
+      {
+        pig.restoreTo (checkpoint);
+        return false;
+      }
+
+      auto following = pig.peek ();
+      if (! unicodeLatinAlpha (following) &&
+          ! unicodeLatinDigit (following))
+      {
+        // Linear lookup - should instead be logarithmic.
+        double seconds = 1;
+        for (unsigned int i = 0; i < NUM_DURATIONS; i++)
+        {
+          if (durations[i].unit == unit)
+          {
+            seconds = durations[i].seconds;
+            _period = static_cast <int> (number * static_cast <double> (seconds));
+            return true;
+          }
+        }
+      }
+    }
+  }
+
+  pig.restoreTo (checkpoint);
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+void Duration::clear ()
+{
+  _year    = 0;
+  _month   = 0;
+  _weeks   = 0;
+  _day     = 0;
+  _hours   = 0;
+  _minutes = 0;
+  _seconds = 0;
+  _period  = 0;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+const std::string Duration::format () const
+{
+  if (_period)
+  {
+    time_t t = _period;
+    int seconds = t % 60; t /= 60;
+    int minutes = t % 60; t /= 60;
+    int hours   = t % 24; t /= 24;
+    int days    = t;
+
+    std::stringstream s;
+    if (days)
+      s << days << "d ";
+
+    s << hours
+      << ':'
+      << std::setw (2) << std::setfill ('0') << minutes
+      << ':'
+      << std::setw (2) << std::setfill ('0') << seconds;
+
+    return s.str ();
+  }
+  else
+  {
+    return "0:00:00";
+  }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+const std::string Duration::formatHours () const
+{
+  if (_period)
+  {
+    time_t t = _period;
+    int seconds = t % 60; t /= 60;
+    int minutes = t % 60; t /= 60;
+    int hours   = t;
+
+    std::stringstream s;
+    s << hours
+      << ':'
+      << std::setw (2) << std::setfill ('0') << minutes
+      << ':'
+      << std::setw (2) << std::setfill ('0') << seconds;
+
+    return s.str ();
+  }
+  else
+  {
+    return "0:00:00";
+  }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+const std::string Duration::formatISO () const
+{
+  if (_period)
+  {
+    time_t t = _period;
+    int seconds = t % 60; t /= 60;
+    int minutes = t % 60; t /= 60;
+    int hours   = t % 24; t /= 24;
+    int days    = t;
+
+    std::stringstream s;
+    s << 'P';
+    if (days)   s << days   << 'D';
+
+    if (hours || minutes || seconds)
+    {
+      s << 'T';
+      if (hours)   s << hours   << 'H';
+      if (minutes) s << minutes << 'M';
+      if (seconds) s << seconds << 'S';
+    }
+
+    return s.str ();
+  }
+  else
+  {
+    return "PT0S";
+  }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Range      Representation
+// ---------  ---------------------
+// >= 365d    {n.n}y
+// >= 90d     {n}mo
+// >= 14d     {n}w
+// >= 1d      {n}d
+// >= 1h      {n}h
+// >= 1min    {n}min
+//            {n}s
+//
+const std::string Duration::formatVague (bool padding) const
+{
+  float days = (float) _period / 86400.0;
+
+  std::stringstream formatted;
+       if (_period >= 86400 * 365) formatted << std::fixed << std::setprecision (1) << (days / 365) << (padding ? "y  " : "y");
+  else if (_period >= 86400 * 90)  formatted << static_cast <int> (days / 30)       << (padding ? "mo " : "mo");
+  else if (_period >= 86400 * 14)  formatted << static_cast <int> (days / 7)        << (padding ? "w  " : "w");
+  else if (_period >= 86400)       formatted << static_cast <int> (days)            << (padding ? "d  " : "d");
+  else if (_period >= 3600)        formatted << static_cast <int> (_period / 3600)  << (padding ? "h  " : "h");
+  else if (_period >= 60)          formatted << static_cast <int> (_period / 60)    << "min";  // Longest suffix - no padding
+  else if (_period >= 1)           formatted << static_cast <int> (_period)         << (padding ? "s  " : "s");
+
+  return formatted.str ();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+int Duration::days () const
+{
+  return _period / 86400;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+int Duration::hours () const
+{
+  return _period / 3600;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+int Duration::minutes () const
+{
+  return _period / 60;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+int Duration::seconds () const
+{
+  return _period;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Allow un-normalized values.
+void Duration::resolve ()
+{
+  if (! _period)
+  {
+    if (_weeks)
+      _period = (_weeks * 7 * 86400);
+    else
+      _period = (_year  * 365 * 86400) +
+                (_month  * 30 * 86400) +
+                (_day         * 86400) +
+                (_hours       *  3600) +
+                (_minutes     *    60) +
+                _seconds;
+  }
+}
+
+////////////////////////////////////////////////////////////////////////////////
diff --git a/src/libshared/src/Duration.h b/src/libshared/src/Duration.h
new file mode 100644 (file)
index 0000000..5fb607a
--- /dev/null
@@ -0,0 +1,81 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Copyright 2006 - 2017, Paul Beckingham, Federico Hernandez.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+// http://www.opensource.org/licenses/mit-license.php
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#ifndef INCLUDED_DURATION
+#define INCLUDED_DURATION
+
+#include <Pig.h>
+#include <string>
+#include <time.h>
+
+class Duration
+{
+public:
+  static bool standaloneSecondsEnabled;
+
+  Duration ();
+  Duration (const std::string&);
+  Duration (time_t);
+  bool operator< (const Duration&);
+  bool operator> (const Duration&);
+  bool operator<= (const Duration&);
+  bool operator>= (const Duration&);
+  std::string toString () const;
+  time_t toTime_t () const;
+  bool parse (const std::string&, std::string::size_type&);
+  bool parse_seconds (Pig&);
+  bool parse_designated (Pig&);
+  bool parse_weeks (Pig&);
+  bool parse_units (Pig&);
+  const std::string format () const;
+  const std::string formatHours () const;
+  const std::string formatISO () const;
+  const std::string formatVague (bool padding = false) const;
+
+  int days () const;
+  int hours () const;
+  int minutes () const;
+  int seconds () const;
+
+private:
+  void clear ();
+  void resolve ();
+  std::string dump () const;
+
+public:
+  int _year      {0};
+  int _month     {0};
+  int _weeks     {0};
+  int _day       {0};
+  int _hours     {0};
+  int _minutes   {0};
+  int _seconds   {0};
+  time_t _period {0};
+};
+
+#endif
+
+////////////////////////////////////////////////////////////////////////////////
diff --git a/src/libshared/src/FS.cpp b/src/libshared/src/FS.cpp
new file mode 100644 (file)
index 0000000..1036062
--- /dev/null
@@ -0,0 +1,1025 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Copyright 2006 - 2017, Paul Beckingham, Federico Hernandez.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+// http://www.opensource.org/licenses/mit-license.php
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include <cmake.h>
+#include <FS.h>
+#include <fstream>
+#include <glob.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <stdlib.h>
+#include <pwd.h>
+#include <cstdio>
+#include <unistd.h>
+#include <fcntl.h>
+#include <dirent.h>
+#include <errno.h>
+#include <string.h>
+#include <shared.h>
+#include <format.h>
+
+#if defined SOLARIS || defined NETBSD || defined FREEBSD || !defined(__GLIBC__)
+#include <limits.h>
+#endif
+
+#if defined __APPLE__
+#include <sys/syslimits.h>
+#endif
+
+// Fixes build with musl libc.
+#ifndef GLOB_TILDE
+#define GLOB_TILDE 0
+#endif
+
+#ifndef GLOB_BRACE
+#define GLOB_BRACE 0
+#endif
+
+////////////////////////////////////////////////////////////////////////////////
+Path::Path ()
+{
+}
+
+////////////////////////////////////////////////////////////////////////////////
+Path::Path (const Path& other)
+{
+  if (this != &other)
+  {
+    _original = other._original;
+    _data     = other._data;
+  }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+Path::Path (const std::string& in)
+{
+  _original = in;
+  _data     = expand (in);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+Path& Path::operator= (const Path& other)
+{
+  if (this != &other)
+  {
+    this->_original = other._original;
+    this->_data     = other._data;
+  }
+
+  return *this;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool Path::operator== (const Path& other)
+{
+  return _data == other._data;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+Path& Path::operator+= (const std::string& dir)
+{
+  _data += '/' + dir;
+  return *this;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+Path::operator std::string () const
+{
+  return _data;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+std::string Path::name () const
+{
+  if (_data.length ())
+  {
+    auto slash = _data.rfind ('/');
+    if (slash != std::string::npos)
+      return _data.substr (slash + 1, std::string::npos);
+  }
+
+ return _data;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+std::string Path::parent () const
+{
+  if (_data.length ())
+  {
+    auto slash = _data.rfind ('/');
+    if (slash != std::string::npos)
+      return _data.substr (0, slash);
+  }
+
+  return "";
+}
+
+////////////////////////////////////////////////////////////////////////////////
+std::string Path::extension () const
+{
+  if (_data.length ())
+  {
+    auto dot = _data.rfind ('.');
+    if (dot != std::string::npos)
+      return _data.substr (dot + 1, std::string::npos);
+  }
+
+  return "";
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool Path::exists () const
+{
+  return access (_data.c_str (), F_OK) ? false : true;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool Path::is_directory () const
+{
+  if (exists ())
+  {
+    struct stat s {};
+    if (stat (_data.c_str (), &s))
+      throw format ("stat error {1}: {2}", errno, strerror (errno));
+
+    return S_ISDIR (s.st_mode);
+  }
+
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool Path::is_absolute () const
+{
+  if (_data.length () && _data[0] == '/')
+    return true;
+
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool Path::is_link () const
+{
+  struct stat s {};
+  if (lstat (_data.c_str (), &s))
+    throw format ("lstat error {1}: {2}", errno, strerror (errno));
+
+  return S_ISLNK (s.st_mode);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// EACCES is a permissions problem which is exactly what this method is trying
+// to determine.
+bool Path::readable () const
+{
+  auto status = access (_data.c_str (), R_OK);
+  if (status == -1 && errno != EACCES)
+    throw format ("access error {1}: {2}", errno, strerror (errno));
+
+  return status ? false : true;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// EACCES is a permissions problem which is exactly what this method is trying
+// to determine.
+bool Path::writable () const
+{
+  auto status = access (_data.c_str (), W_OK);
+  if (status == -1 && errno != EACCES)
+    throw format ("access error {1}: {2}", errno, strerror (errno));
+
+  return status ? false : true;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// EACCES is a permissions problem which is exactly what this method is trying
+// to determine.
+bool Path::executable () const
+{
+  auto status = access (_data.c_str (), X_OK);
+  if (status == -1 && errno != EACCES)
+    throw format ("access error {1}: {2}", errno, strerror (errno));
+
+  return status ? false : true;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool Path::rename (const std::string& new_name)
+{
+  auto expanded = expand (new_name);
+  if (_data != expanded)
+  {
+    if (std::rename (_data.c_str (), expanded.c_str ()) == 0)
+    {
+      _data = expanded;
+      return true;
+    }
+  }
+
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// ~      --> /home/user
+// ~foo/x --> /home/foo/s
+// ~/x    --> /home/foo/x
+// ./x    --> $PWD/x
+// x      --> $PWD/x
+std::string Path::expand (const std::string& in)
+{
+  std::string copy = in;
+
+  auto tilde = copy.find ('~');
+  std::string::size_type slash;
+
+  if (tilde != std::string::npos)
+  {
+    const char *home = getenv("HOME");
+    if (home == nullptr)
+    {
+      struct passwd* pw = getpwuid (getuid ());
+      home = pw->pw_dir;
+    }
+
+    // Convert: ~ --> /home/user
+    if (copy.length () == 1)
+      copy = home;
+
+    // Convert: ~/x --> /home/user/x
+    else if (copy.length () > tilde + 1 &&
+             copy[tilde + 1] == '/')
+    {
+      copy.replace (tilde, 1, home);
+    }
+
+    // Convert: ~foo/x --> /home/foo/x
+    else if ((slash = copy.find ('/', tilde)) != std::string::npos)
+    {
+      std::string name = copy.substr (tilde + 1, slash - tilde - 1);
+      struct passwd* pw = getpwnam (name.c_str ());
+      if (pw)
+        copy.replace (tilde, slash - tilde, pw->pw_dir);
+    }
+  }
+
+  // Relative paths
+  else if (in.length () > 2 &&
+           in.substr (0, 2) == "./")
+  {
+    copy = Directory::cwd () + in.substr (1);
+  }
+  else if (in.length () > 1 &&
+           in[0] != '.' &&
+           in[0] != '/')
+  {
+    copy = Directory::cwd () + '/' + in;
+  }
+
+  return copy;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+std::vector <std::string> Path::glob (const std::string& pattern)
+{
+  std::vector <std::string> results;
+
+  glob_t g;
+#ifdef SOLARIS
+  if (!::glob (pattern.c_str (), GLOB_ERR, nullptr, &g))
+#else
+  if (!::glob (pattern.c_str (), GLOB_ERR | GLOB_BRACE | GLOB_TILDE, nullptr, &g))
+#endif
+    for (int i = 0; i < (int) g.gl_pathc; ++i)
+      results.push_back (g.gl_pathv[i]);
+
+  globfree (&g);
+  return results;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+File::File ()
+: Path::Path ()
+, _fh (nullptr)
+, _h (-1)
+, _locked (false)
+{
+}
+
+////////////////////////////////////////////////////////////////////////////////
+File::File (const Path& other)
+: Path::Path (other)
+, _fh (nullptr)
+, _h (-1)
+, _locked (false)
+{
+}
+
+////////////////////////////////////////////////////////////////////////////////
+File::File (const File& other)
+: Path::Path (other)
+, _fh (nullptr)
+, _h (-1)
+, _locked (false)
+{
+}
+
+////////////////////////////////////////////////////////////////////////////////
+File::File (const std::string& in)
+: Path::Path (in)
+, _fh (nullptr)
+, _h (-1)
+, _locked (false)
+{
+}
+
+////////////////////////////////////////////////////////////////////////////////
+File::~File ()
+{
+  if (_fh)
+    close ();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+File& File::operator= (const File& other)
+{
+  if (this != &other)
+    Path::operator= (other);
+
+  _locked = false;
+  return *this;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool File::create (int mode /* = 0640 */)
+{
+  if (open ())
+  {
+    fchmod (_h, mode);
+    close ();
+    return true;
+  }
+
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool File::remove () const
+{
+  return unlink (_data.c_str ()) == 0 ? true : false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+std::string File::removeBOM (const std::string& input)
+{
+  if (input[0] && input[0] == '\xEF' &&
+      input[1] && input[1] == '\xBB' &&
+      input[2] && input[2] == '\xBF')
+    return input.substr (3);
+
+  return input;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool File::open ()
+{
+  if (_data != "")
+  {
+    if (! _fh)
+    {
+      bool already_exists = exists ();
+      if (already_exists)
+        if (!readable () || !writable ())
+          throw std::string (format ("Insufficient permissions for '{1}'.", _data));
+
+      _fh = fopen (_data.c_str (), (already_exists ? "r+" : "w+"));
+      if (_fh)
+      {
+        _h = fileno (_fh);
+        _locked = false;
+        return true;
+      }
+      else
+        throw format ("fopen error {1}: {2}", errno, strerror (errno));
+    }
+    else
+      return true;
+  }
+
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+void File::close ()
+{
+  if (_fh)
+  {
+    if (_locked)
+      unlock ();
+
+    if (fclose (_fh))
+      throw format ("fclose error {1}: {2}", errno, strerror (errno));
+
+    _fh = nullptr;
+    _h = -1;
+    _locked = false;
+  }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool File::lock ()
+{
+  _locked = false;
+  if (_fh && _h != -1)
+  {
+#ifdef FREEBSD
+                    // l_type   l_whence  l_start  l_len  l_pid  l_sysid
+    struct flock fl = {F_WRLCK, SEEK_SET, 0,       0,     0,     0 };
+#else
+                    // l_type   l_whence  l_start  l_len  l_pid
+    struct flock fl = {F_WRLCK, SEEK_SET, 0,       0,     0 };
+#endif
+    fl.l_pid = getpid ();
+    if (fcntl (_h, F_SETLKW, &fl) == 0)
+      _locked = true;
+  }
+
+  return _locked;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+void File::unlock ()
+{
+  if (_locked)
+  {
+#ifdef FREEBSD
+                    // l_type   l_whence  l_start  l_len  l_pid  l_sysid
+    struct flock fl = {F_WRLCK, SEEK_SET, 0,       0,     0,     0 };
+#else
+                    // l_type   l_whence  l_start  l_len  l_pid
+    struct flock fl = {F_WRLCK, SEEK_SET, 0,       0,     0 };
+#endif
+    fl.l_pid = getpid ();
+
+    fcntl (_h, F_SETLK, &fl);
+    _locked = false;
+  }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Opens if necessary.
+void File::read (std::string& contents)
+{
+  contents = "";
+  contents.reserve (size ());
+
+  std::ifstream in (_data.c_str ());
+  if (in.good ())
+  {
+    bool first = true;
+    std::string line;
+    line.reserve (512 * 1024);
+    while (getline (in, line))
+    {
+      // Detect forbidden BOM on first line.
+      if (first)
+      {
+        line = File::removeBOM (line);
+        first = false;
+      }
+
+      contents += line + '\n';
+    }
+
+    in.close ();
+  }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Opens if necessary.
+void File::read (std::vector <std::string>& contents)
+{
+  contents.clear ();
+
+  std::ifstream in (_data.c_str ());
+  if (in.good ())
+  {
+    bool first = true;
+    std::string line;
+    line.reserve (512 * 1024);
+    while (getline (in, line))
+    {
+      // Detect forbidden BOM on first line.
+      if (first)
+      {
+        line = File::removeBOM (line);
+        first = false;
+      }
+
+      contents.push_back (line);
+    }
+
+    in.close ();
+  }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Opens if necessary.
+void File::append (const std::string& line)
+{
+  if (!_fh)
+    open ();
+
+  if (_fh)
+  {
+    fseek (_fh, 0, SEEK_END);
+
+    if (fputs (line.c_str (), _fh) == EOF)
+      throw format ("fputs error {1}: {2}", errno, strerror (errno));
+  }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Opens if necessary.
+void File::append (const std::vector <std::string>& lines)
+{
+  if (!_fh)
+    open ();
+
+  if (_fh)
+  {
+    fseek (_fh, 0, SEEK_END);
+
+    for (auto& line : lines)
+      if (fputs (line.c_str (), _fh) == EOF)
+        throw format ("fputs error {1}: {2}", errno, strerror (errno));
+  }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+void File::write_raw (const std::string& line)
+{
+  if (!_fh)
+    open ();
+
+  if (_fh)
+    if (fputs (line.c_str (), _fh) == EOF)
+      throw format ("fputs error {1}: {2}", errno, strerror (errno));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+void File::truncate ()
+{
+  if (!_fh)
+    open ();
+
+  if (_fh)
+    if (ftruncate (_h, 0))
+      throw format ("ftruncate error {1}: {2}", errno, strerror (errno));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+//  S_IFMT          0170000  type of file
+//         S_IFIFO  0010000  named pipe (fifo)
+//         S_IFCHR  0020000  character special
+//         S_IFDIR  0040000  directory
+//         S_IFBLK  0060000  block special
+//         S_IFREG  0100000  regular
+//         S_IFLNK  0120000  symbolic link
+//         S_IFSOCK 0140000  socket
+//         S_IFWHT  0160000  whiteout
+//  S_ISUID         0004000  set user id on execution
+//  S_ISGID         0002000  set group id on execution
+//  S_ISVTX         0001000  save swapped text even after use
+//  S_IRUSR         0000400  read permission, owner
+//  S_IWUSR         0000200  write permission, owner
+//  S_IXUSR         0000100  execute/search permission, owner
+mode_t File::mode ()
+{
+  struct stat s;
+  if (stat (_data.c_str (), &s))
+    throw format ("stat error {1}: {2}", errno, strerror (errno));
+
+  return s.st_mode;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+size_t File::size () const
+{
+  struct stat s;
+  if (stat (_data.c_str (), &s))
+    throw format ("stat error {1}: {2}", errno, strerror (errno));
+
+  return s.st_size;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+time_t File::mtime () const
+{
+  struct stat s;
+  if (stat (_data.c_str (), &s))
+    throw format ("stat error {1}: {2}", errno, strerror (errno));
+
+  return s.st_mtime;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+time_t File::ctime () const
+{
+  struct stat s;
+  if (stat (_data.c_str (), &s))
+    throw format ("stat error {1}: {2}", errno, strerror (errno));
+
+  return s.st_ctime;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+time_t File::btime () const
+{
+  struct stat s;
+  if (stat (_data.c_str (), &s))
+    throw format ("stat error {1}: {2}", errno, strerror (errno));
+
+#ifdef HAVE_ST_BIRTHTIME
+  return s.st_birthtime;
+#else
+  return s.st_ctime;
+#endif
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool File::create (const std::string& name, int mode /* = 0640 */)
+{
+  std::string full_name = expand (name);
+  std::ofstream out (full_name.c_str ());
+  if (out.good ())
+  {
+    out.close ();
+    if (chmod (full_name.c_str (), mode))
+      throw format ("chmod error {1}: {2}", errno, strerror (errno));
+
+    return true;
+  }
+
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool File::read (const std::string& name, std::string& contents)
+{
+  contents = "";
+
+  std::ifstream in (name.c_str ());
+  if (in.good ())
+  {
+    bool first = true;
+    std::string line;
+    line.reserve (1024);
+    while (getline (in, line))
+    {
+      // Detect forbidden BOM on first line.
+      if (first)
+      {
+        line = File::removeBOM (line);
+        first = false;
+      }
+
+      contents += line + '\n';
+    }
+
+    in.close ();
+    return true;
+  }
+
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool File::read (const std::string& name, std::vector <std::string>& contents)
+{
+  contents.clear ();
+
+  std::ifstream in (name.c_str ());
+  if (in.good ())
+  {
+    bool first = true;
+    std::string line;
+    line.reserve (1024);
+    while (getline (in, line))
+    {
+      // Detect forbidden BOM on first line.
+      if (first)
+      {
+        line = File::removeBOM (line);
+        first = false;
+      }
+
+      contents.push_back (line);
+    }
+
+    in.close ();
+    return true;
+  }
+
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool File::write (const std::string& name, const std::string& contents)
+{
+  std::ofstream out (expand (name).c_str (),
+                     std::ios_base::out | std::ios_base::trunc);
+  if (out.good ())
+  {
+    out << contents;
+    out.close ();
+    return true;
+  }
+
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool File::write (
+  const std::string& name,
+  const std::vector <std::string>& lines,
+  bool addNewlines /* = true */)
+{
+  std::ofstream out (expand (name).c_str (),
+                     std::ios_base::out | std::ios_base::trunc);
+  if (out.good ())
+  {
+    for (auto& line : lines)
+    {
+      out << line;
+
+      if (addNewlines)
+        out << '\n';
+    }
+
+    out.close ();
+    return true;
+  }
+
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool File::remove (const std::string& name)
+{
+  return unlink (expand (name).c_str ()) == 0 ? true : false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool File::copy (const std::string& from, const std::string& to)
+{
+  // 'from' must exist.
+  if (! access (from.c_str (), F_OK))
+  {
+    std::ifstream src (from, std::ios::binary);
+    std::ofstream dst (to,   std::ios::binary);
+
+    dst << src.rdbuf ();
+    return true;
+  }
+
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool File::move (const std::string& from, const std::string& to)
+{
+  auto expanded = expand (to);
+  if (from != expanded)
+    if (std::rename (from.c_str (), to.c_str ()) == 0)
+      return true;
+
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+Directory::Directory ()
+{
+}
+
+////////////////////////////////////////////////////////////////////////////////
+Directory::Directory (const Directory& other)
+: File::File (other)
+{
+}
+
+////////////////////////////////////////////////////////////////////////////////
+Directory::Directory (const File& other)
+: File::File (other)
+{
+}
+
+////////////////////////////////////////////////////////////////////////////////
+Directory::Directory (const Path& other)
+: File::File (other)
+{
+}
+
+////////////////////////////////////////////////////////////////////////////////
+Directory::Directory (const std::string& in)
+: File::File (in)
+{
+}
+
+////////////////////////////////////////////////////////////////////////////////
+Directory& Directory::operator= (const Directory& other)
+{
+  if (this != &other)
+    File::operator= (other);
+
+  return *this;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool Directory::create (int mode /* = 0755 */)
+{
+  // No error handling because we want failure to be silent, somewhat emulating
+  // "mkdir -p".
+  return mkdir (_data.c_str (), mode) == 0 ? true : false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool Directory::remove () const
+{
+  return remove_directory (_data);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool Directory::remove_directory (const std::string& dir) const
+{
+  DIR* dp = opendir (dir.c_str ());
+  if (dp != nullptr)
+  {
+    struct dirent* de;
+    while ((de = readdir (dp)) != nullptr)
+    {
+      if (! strcmp (de->d_name, ".") ||
+          ! strcmp (de->d_name, ".."))
+        continue;
+
+#if defined (SOLARIS) || defined (HAIKU)
+      struct stat s;
+      if (lstat ((dir + '/' + de->d_name).c_str (), &s))
+        throw format ("lstat error {1}: {2}", errno, strerror (errno));
+
+      if (S_ISDIR (s.st_mode))
+        remove_directory (dir + '/' + de->d_name);
+      else
+        unlink ((dir + '/' + de->d_name).c_str ());
+#else
+      if (de->d_type == DT_UNKNOWN)
+      {
+        struct stat s;
+        if (lstat ((dir + '/' + de->d_name).c_str (), &s))
+          throw format ("lstat error {1}: {2}", errno, strerror (errno));
+
+        if (S_ISDIR (s.st_mode))
+          de->d_type = DT_DIR;
+      }
+      if (de->d_type == DT_DIR)
+        remove_directory (dir + '/' + de->d_name);
+      else
+        unlink ((dir + '/' + de->d_name).c_str ());
+#endif
+    }
+
+    closedir (dp);
+  }
+
+  return rmdir (dir.c_str ()) ? false : true;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+std::vector <std::string> Directory::list ()
+{
+  std::vector <std::string> files;
+  list (_data, files, false);
+  return files;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+std::vector <std::string> Directory::listRecursive ()
+{
+  std::vector <std::string> files;
+  list (_data, files, true);
+  return files;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+std::string Directory::cwd ()
+{
+#ifdef HAVE_GET_CURRENT_DIR_NAME
+  char *buf = get_current_dir_name ();
+  if (buf == nullptr)
+    throw std::bad_alloc ();
+  std::string result (buf);
+  free (buf);
+  return result;
+#else
+  char buf[PATH_MAX];
+  getcwd (buf, PATH_MAX - 1);
+  return std::string (buf);
+#endif
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool Directory::up ()
+{
+  if (_data == "/")
+    return false;
+
+  auto slash = _data.rfind ('/');
+  if (slash == 0)
+  {
+    _data = "/";  // Root dir should retain the slash.
+    return true;
+  }
+  else if (slash != std::string::npos)
+  {
+    _data = _data.substr (0, slash);
+    return true;
+  }
+
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool Directory::cd () const
+{
+  return chdir (_data.c_str ()) == 0 ? true : false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+void Directory::list (
+  const std::string& base,
+  std::vector <std::string>& results,
+  bool recursive)
+{
+  DIR* dp = opendir (base.c_str ());
+  if (dp != nullptr)
+  {
+    struct dirent* de;
+    while ((de = readdir (dp)) != nullptr)
+    {
+      if (!strcmp (de->d_name, ".") ||
+          !strcmp (de->d_name, ".."))
+        continue;
+
+#if defined (SOLARIS) || defined (HAIKU)
+      struct stat s;
+      if (stat ((base + '/' + de->d_name).c_str (), &s))
+        throw format ("stat error {1}: {2}", errno, strerror (errno));
+
+      if (recursive && S_ISDIR (s.st_mode))
+        list (base + '/' + de->d_name, results, recursive);
+      else
+        results.push_back (base + '/' + de->d_name);
+#else
+      if (recursive && de->d_type == DT_UNKNOWN)
+      {
+        struct stat s;
+        if (lstat ((base + '/' + de->d_name).c_str (), &s))
+          throw format ("lstat error {1}: {2}", errno, strerror (errno));
+
+        if (S_ISDIR (s.st_mode))
+          de->d_type = DT_DIR;
+      }
+      if (recursive && de->d_type == DT_DIR)
+        list (base + '/' + de->d_name, results, recursive);
+      else
+        results.push_back (base + '/' + de->d_name);
+#endif
+    }
+
+    closedir (dp);
+  }
+}
+
+////////////////////////////////////////////////////////////////////////////////
diff --git a/src/libshared/src/FS.h b/src/libshared/src/FS.h
new file mode 100644 (file)
index 0000000..e2c4560
--- /dev/null
@@ -0,0 +1,149 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Copyright 2006 - 2017, Paul Beckingham, Federico Hernandez.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+// http://www.opensource.org/licenses/mit-license.php
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#ifndef INCLUDED_FS
+#define INCLUDED_FS
+
+#include <stdio.h>
+#include <string>
+#include <vector>
+#include <sys/stat.h>
+
+#ifndef PATH_MAX
+#define PATH_MAX 4096
+#endif
+
+class Path
+{
+public:
+  Path ();
+  explicit Path (const Path&);
+  Path (const std::string&);
+
+  Path& operator= (const Path&);
+  bool operator== (const Path&);
+  Path& operator+= (const std::string&);
+  operator std::string () const;
+
+  std::string name () const;
+  std::string parent () const;
+  std::string extension () const;
+  bool exists () const;
+  bool is_directory () const;
+  bool is_absolute () const;
+  bool is_link () const;
+  bool readable () const;
+  bool writable () const;
+  bool executable () const;
+  bool rename (const std::string&);
+
+  // Statics
+  static std::string expand (const std::string&);
+  static std::vector<std::string> glob (const std::string&);
+
+public:
+  std::string _original;
+  std::string _data;
+};
+
+class File : public Path
+{
+public:
+  File ();
+  explicit File (const Path&);
+  explicit File (const File&);
+  File (const std::string&);
+  virtual ~File ();
+
+  File& operator= (const File&);
+
+  virtual bool create (int mode = 0640);
+  virtual bool remove () const;
+
+  bool open ();
+  void close ();
+
+  bool lock ();
+  void unlock ();
+
+  void read (std::string&);
+  void read (std::vector <std::string>&);
+
+  void append (const std::string&);
+  void append (const std::vector <std::string>&);
+  void write_raw (const std::string&);
+
+  void truncate ();
+
+  virtual mode_t mode ();
+  virtual size_t size () const;
+  virtual time_t mtime () const;
+  virtual time_t ctime () const;
+  virtual time_t btime () const;
+
+  static bool create (const std::string&, int mode = 0640);
+  static bool read (const std::string&, std::string&);
+  static bool read (const std::string&, std::vector <std::string>&);
+  static bool write (const std::string&, const std::string&);
+  static bool write (const std::string&, const std::vector <std::string>&, bool addNewlines = true);
+  static bool remove (const std::string&);
+  static bool copy (const std::string&, const std::string&);
+  static bool move (const std::string&, const std::string&);
+  static std::string removeBOM (const std::string&);
+
+private:
+  FILE* _fh;
+  int   _h;
+  bool  _locked;
+};
+
+class Directory : public File
+{
+public:
+  Directory ();
+  explicit Directory (const Directory&);
+  explicit Directory (const File&);
+  explicit Directory (const Path&);
+  Directory (const std::string&);
+
+  Directory& operator= (const Directory&);
+
+  virtual bool create (int mode = 0755);
+  virtual bool remove () const;
+
+  std::vector <std::string> list ();
+  std::vector <std::string> listRecursive ();
+
+  static std::string cwd ();
+  bool up ();
+  bool cd () const;
+
+private:
+  void list (const std::string&, std::vector <std::string>&, bool);
+  bool remove_directory (const std::string&) const;
+};
+
+#endif
diff --git a/src/libshared/src/JSON.cpp b/src/libshared/src/JSON.cpp
new file mode 100644 (file)
index 0000000..d9db45f
--- /dev/null
@@ -0,0 +1,490 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Copyright 2006 - 2017, Paul Beckingham, Federico Hernandez.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+// http://www.opensource.org/licenses/mit-license.php
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include <cmake.h>
+#include <JSON.h>
+#include <shared.h>
+#include <format.h>
+#include <utf8.h>
+
+const char *json_encode[] = {
+  "\x00", "\x01", "\x02", "\x03", "\x04", "\x05", "\x06", "\x07",
+   "\\b",  "\\t",  "\\n", "\x0b",  "\\f",  "\\r", "\x0e", "\x0f",
+  "\x10", "\x11", "\x12", "\x13", "\x14", "\x15", "\x16", "\x17",
+  "\x18", "\x19", "\x1a", "\x1b", "\x1c", "\x1d", "\x1e", "\x1f",
+  "\x20", "\x21", "\\\"", "\x23", "\x24", "\x25", "\x26", "\x27",
+  "\x28", "\x29", "\x2a", "\x2b", "\x2c", "\x2d", "\x2e",  "\\/",
+  "\x30", "\x31", "\x32", "\x33", "\x34", "\x35", "\x36", "\x37",
+  "\x38", "\x39", "\x3a", "\x3b", "\x3c", "\x3d", "\x3e", "\x3f",
+  "\x40", "\x41", "\x42", "\x43", "\x44", "\x45", "\x46", "\x47",
+  "\x48", "\x49", "\x4a", "\x4b", "\x4c", "\x4d", "\x4e", "\x4f",
+  "\x50", "\x51", "\x52", "\x53", "\x54", "\x55", "\x56", "\x57",
+  "\x58", "\x59", "\x5a", "\x5b", "\\\\", "\x5d", "\x5e", "\x5f",
+  "\x60", "\x61", "\x62", "\x63", "\x64", "\x65", "\x66", "\x67",
+  "\x68", "\x69", "\x6a", "\x6b", "\x6c", "\x6d", "\x6e", "\x6f",
+  "\x70", "\x71", "\x72", "\x73", "\x74", "\x75", "\x76", "\x77",
+  "\x78", "\x79", "\x7a", "\x7b", "\x7c", "\x7d", "\x7e", "\x7f",
+  "\x80", "\x81", "\x82", "\x83", "\x84", "\x85", "\x86", "\x87",
+  "\x88", "\x89", "\x8a", "\x8b", "\x8c", "\x8d", "\x8e", "\x8f",
+  "\x90", "\x91", "\x92", "\x93", "\x94", "\x95", "\x96", "\x97",
+  "\x98", "\x99", "\x9a", "\x9b", "\x9c", "\x9d", "\x9e", "\x9f",
+  "\xa0", "\xa1", "\xa2", "\xa3", "\xa4", "\xa5", "\xa6", "\xa7",
+  "\xa8", "\xa9", "\xaa", "\xab", "\xac", "\xad", "\xae", "\xaf",
+  "\xb0", "\xb1", "\xb2", "\xb3", "\xb4", "\xb5", "\xb6", "\xb7",
+  "\xb8", "\xb9", "\xba", "\xbb", "\xbc", "\xbd", "\xbe", "\xbf",
+  "\xc0", "\xc1", "\xc2", "\xc3", "\xc4", "\xc5", "\xc6", "\xc7",
+  "\xc8", "\xc9", "\xca", "\xcb", "\xcc", "\xcd", "\xce", "\xcf",
+  "\xd0", "\xd1", "\xd2", "\xd3", "\xd4", "\xd5", "\xd6", "\xd7",
+  "\xd8", "\xd9", "\xda", "\xdb", "\xdc", "\xdd", "\xde", "\xdf",
+  "\xe0", "\xe1", "\xe2", "\xe3", "\xe4", "\xe5", "\xe6", "\xe7",
+  "\xe8", "\xe9", "\xea", "\xeb", "\xec", "\xed", "\xee", "\xef",
+  "\xf0", "\xf1", "\xf2", "\xf3", "\xf4", "\xf5", "\xf6", "\xf7",
+  "\xf8", "\xf9", "\xfa", "\xfb", "\xfc", "\xfd", "\xfe", "\xff"
+};
+
+////////////////////////////////////////////////////////////////////////////////
+json::value* json::value::parse (Pig& pig)
+{
+  json::value* v;
+  if ((v = json::object::parse  (pig)) ||
+      (v = json::array::parse   (pig)) ||
+      (v = json::string::parse  (pig)) ||
+      (v = json::number::parse  (pig)) ||
+      (v = json::literal::parse (pig)))
+    return v;
+
+  return NULL;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+json::jtype json::value::type ()
+{
+  return json::j_value;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+std::string json::value::dump () const
+{
+  return "<value>";
+}
+
+////////////////////////////////////////////////////////////////////////////////
+json::string::string (const std::string& other)
+{
+  _data = other;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+json::string* json::string::parse (Pig& pig)
+{
+  std::string value;
+  if (pig.getQuoted ('"', value))
+  {
+    json::string* s = new json::string ();
+    s->_data = value;
+    return s;
+  }
+
+  return NULL;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+json::jtype json::string::type ()
+{
+  return json::j_string;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+std::string json::string::dump () const
+{
+  return std::string ("\"") + _data + '"';
+}
+
+////////////////////////////////////////////////////////////////////////////////
+json::number* json::number::parse (Pig& pig)
+{
+  double d;
+  if (pig.getNumber (d))
+  {
+    json::number* s = new json::number ();
+    s->_dvalue = d;
+    return s;
+  }
+
+  return NULL;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+json::jtype json::number::type ()
+{
+  return json::j_number;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+std::string json::number::dump () const
+{
+  return format (_dvalue);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+json::number::operator double () const
+{
+  return _dvalue;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+json::literal* json::literal::parse (Pig& pig)
+{
+  if (pig.skipLiteral ("null"))
+  {
+    json::literal* s = new json::literal ();
+    s->_lvalue = nullvalue;
+    return s;
+  }
+  else if (pig.skipLiteral ("false"))
+  {
+    json::literal* s = new json::literal ();
+    s->_lvalue = falsevalue;
+    return s;
+  }
+  else if (pig.skipLiteral ("true"))
+  {
+    json::literal* s = new json::literal ();
+    s->_lvalue = truevalue;
+    return s;
+  }
+
+  return NULL;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+json::jtype json::literal::type ()
+{
+  return json::j_literal;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+std::string json::literal::dump () const
+{
+       if (_lvalue == nullvalue)  return "null";
+  else if (_lvalue == falsevalue) return "false";
+  else                            return "true";
+}
+
+////////////////////////////////////////////////////////////////////////////////
+json::array::~array ()
+{
+  for (auto& i : _data)
+    delete i;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+json::array* json::array::parse (Pig& pig)
+{
+  auto checkpoint = pig.cursor ();
+
+  pig.skipWS ();
+  if (pig.skip ('['))
+  {
+    pig.skipWS ();
+
+    json::array* arr = new json::array ();
+
+    json::value* value;
+    if ((value = json::value::parse (pig)))
+    {
+      arr->_data.push_back (value);
+      value = NULL; // Not a leak.  Looks like a leak.
+      pig.skipWS ();
+      while (pig.skip (','))
+      {
+        pig.skipWS ();
+
+        if ((value = json::value::parse (pig)))
+        {
+          arr->_data.push_back (value);
+          pig.skipWS ();
+        }
+        else
+        {
+          delete arr;
+          throw format ("Error: missing value after ',' at position {1}", (int) pig.cursor ());
+        }
+      }
+    }
+
+    if (pig.skip (']'))
+      return arr;
+    else
+      throw format ("Error: missing ']' at position {1}", (int) pig.cursor ());
+
+    delete arr;
+  }
+
+  pig.restoreTo (checkpoint);
+  return NULL;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+json::jtype json::array::type ()
+{
+  return json::j_array;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+std::string json::array::dump () const
+{
+  std::string output;
+  output += '[';
+
+  for (auto i = _data.begin (); i != _data.end (); ++i)
+  {
+    if (i != _data.begin ())
+      output += ',';
+
+    output += (*i)->dump ();
+  }
+
+  output += ']';
+  return output;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+json::object::~object ()
+{
+  for (auto& i : _data)
+    delete i.second;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+json::object* json::object::parse (Pig& pig)
+{
+  auto checkpoint = pig.cursor ();
+
+  pig.skipWS ();
+  if (pig.skip ('{'))
+  {
+    pig.skipWS ();
+
+    json::object* obj = new json::object ();
+
+    std::string name;
+    json::value* value;
+    if (json::object::parse_pair (pig, name, value))
+    {
+      obj->_data.insert (std::pair <std::string, json::value*> (name, value));
+      value = NULL; // Not a leak.  Looks like a leak.
+
+      pig.skipWS ();
+      while (pig.skip (','))
+      {
+        pig.skipWS ();
+
+        if (json::object::parse_pair (pig, name, value))
+        {
+          obj->_data.insert (std::pair <std::string, json::value*> (name, value));
+          pig.skipWS ();
+        }
+        else
+        {
+          delete obj;
+          throw format ("Error: missing value after ',' at position {1}", (int) pig.cursor ());
+        }
+      }
+    }
+
+    if (pig.skip ('}'))
+      return obj;
+    else
+      throw format ("Error: missing '}' at position {1}", (int) pig.cursor ());
+
+    delete obj;
+  }
+
+  pig.restoreTo (checkpoint);
+  return NULL;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool json::object::parse_pair (
+  Pig& pig,
+  std::string& name,
+  json::value*& val)
+{
+  auto checkpoint = pig.cursor ();
+
+  if (pig.getQuoted ('"', name))
+  {
+    pig.skipWS ();
+    if (pig.skip (':'))
+    {
+      pig.skipWS ();
+      if ((val = json::value::parse (pig)))
+        return true;
+      else
+        throw format ("Error: missing value at position {1}", (int) pig.cursor ());
+    }
+    else
+      throw format ("Error: missing ':' at position {1}", (int) pig.cursor ());
+  }
+
+  pig.restoreTo (checkpoint);
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+json::jtype json::object::type ()
+{
+  return json::j_object;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+std::string json::object::dump () const
+{
+  std::string output;
+  output += '{';
+
+  for (auto i = _data.begin (); i != _data.end (); ++i)
+  {
+    if (i != _data.begin ())
+      output += ',';
+
+    output += '"' + i->first + "\":";
+    output += i->second->dump ();
+  }
+
+  output += '}';
+  return output;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+json::value* json::parse (const std::string& input)
+{
+  json::value* root = NULL;
+
+  Pig n (input);
+  n.skipWS ();
+
+       if (n.peek () == '{') root = json::object::parse (n);
+  else if (n.peek () == '[') root = json::array::parse (n);
+  else
+    throw format ("Error: expected '{' or '[' at position {1}", (int) n.cursor ());
+
+  // Check for end condition.
+  n.skipWS ();
+  if (!n.eos ())
+  {
+    delete root;
+    throw format ("Error: extra characters found at position {1}", (int) n.cursor ());
+  }
+
+  return root;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+std::string json::encode (const std::string& input)
+{
+  std::string output;
+  output.reserve ((input.size () * 6) / 5);  // 20% increase.
+
+  auto last = input.begin ();
+  for (auto i = input.begin (); i != input.end (); ++i)
+  {
+    switch (*i)
+    {
+    // Simple translations.
+    case  '"':
+    case '\\':
+    case  '/':
+    case '\b':
+    case '\f':
+    case '\n':
+    case '\r':
+    case '\t':
+      output.append (last, i);
+      output += json_encode[(unsigned char)(*i)];
+      last = i + 1;
+
+    // Default NOP.
+    }
+  }
+
+  output.append (last, input.end ());
+
+  return output;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+std::string json::decode (const std::string& input)
+{
+  std::string output;
+  output.reserve (input.size ());  // Same size.
+
+  size_t pos = 0;
+
+  while (pos < input.length ())
+  {
+    if (input[pos] == '\\')
+    {
+      ++pos;
+      switch (input[pos])
+      {
+        // Simple translations.
+        case '"':  output += '"';  break;
+        case '\\': output += '\\'; break;
+        case '/':  output += '/';  break;
+        case 'b':  output += '\b'; break;
+        case 'f':  output += '\f'; break;
+        case 'n':  output += '\n'; break;
+        case 'r':  output += '\r'; break;
+        case 't':  output += '\t'; break;
+
+        // Compose a UTF8 unicode character.
+        case 'u':
+          output += utf8_character (utf8_codepoint (input.substr (++pos)));
+          pos += 3;
+          break;
+
+        // If it is an unrecognized sequence, do nothing.
+        default:
+          output += '\\';
+          output += input[pos];
+          break;
+      }
+      ++pos;
+    }
+    else
+    {
+      size_t next_backslash = input.find ('\\', pos);
+      output.append (input, pos, next_backslash - pos);
+      pos = next_backslash;
+    }
+  }
+
+  return output;
+}
+
+////////////////////////////////////////////////////////////////////////////////
diff --git a/src/libshared/src/JSON.h b/src/libshared/src/JSON.h
new file mode 100644 (file)
index 0000000..7a7465e
--- /dev/null
@@ -0,0 +1,183 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Copyright 2006 - 2017, Paul Beckingham, Federico Hernandez.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+// http://www.opensource.org/licenses/mit-license.php
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#ifndef INCLUDED_JSON
+#define INCLUDED_JSON
+
+#include <map>
+#include <vector>
+#include <string>
+#include <Pig.h>
+
+namespace json
+{
+  enum jtype
+  {
+    j_value,    // 0
+    j_object,   // 1
+    j_array,    // 2
+    j_string,   // 3
+    j_number,   // 4
+    j_literal   // 5
+  };
+
+  class value
+  {
+  public:
+    value () {}
+    virtual ~value () {}
+    static value* parse (Pig&);
+    virtual jtype type ();
+    virtual std::string dump () const;
+  };
+
+  class string : public value
+  {
+  public:
+    string () {}
+    string (const std::string&);
+    ~string () {}
+    static string* parse (Pig&);
+    jtype type ();
+    std::string dump () const;
+
+  public:
+    std::string _data;
+  };
+
+  class number : public value
+  {
+  public:
+    number () : _dvalue (0.0) {}
+    ~number () {}
+    static number* parse (Pig&);
+    jtype type ();
+    std::string dump () const;
+    operator double () const;
+
+  public:
+    double _dvalue;
+  };
+
+  class literal : public value
+  {
+  public:
+    literal () : _lvalue (none) {}
+    ~literal () {}
+    static literal* parse (Pig&);
+    jtype type ();
+    std::string dump () const;
+
+  public:
+    enum literal_value {none, nullvalue, falsevalue, truevalue};
+    literal_value _lvalue;
+  };
+
+  class array : public value
+  {
+  public:
+    array () {}
+    ~array ();
+    static array* parse (Pig&);
+    jtype type ();
+    std::string dump () const;
+
+  public:
+    std::vector <value*> _data;
+  };
+
+  class object : public value
+  {
+  public:
+    object () {}
+    ~object ();
+    static object* parse (Pig&);
+    static bool parse_pair (Pig&, std::string&, value*&);
+    jtype type ();
+    std::string dump () const;
+
+  public:
+    std::map <std::string, value*> _data;
+  };
+
+  // Parser entry point.
+  value* parse (const std::string&);
+
+  // Encode/decode for JSON entities.
+  std::string encode (const std::string&);
+  std::string decode (const std::string&);
+
+  class SAX
+  {
+  public:
+    class Sink
+    {
+    public:
+      virtual void eventDocStart () {}
+      virtual void eventDocEnd () {}
+      virtual void eventObjectStart () {}
+      virtual void eventObjectEnd (int) {}
+      virtual void eventArrayStart () {}
+      virtual void eventArrayEnd (int) {}
+      virtual void eventName (const std::string&) {}
+      virtual void eventValueNull () {}
+      virtual void eventValueBool (bool) {}
+      virtual void eventValueInt (int64_t) {}
+      virtual void eventValueUint (uint64_t) {}
+      virtual void eventValueDouble (double) {}
+      virtual void eventValueString (const std::string&) {}
+    };
+
+    bool parse (const std::string&, json::SAX::Sink&);
+
+  private:
+    void ignoreWhitespace (const std::string&, std::string::size_type&);
+    bool isObject         (const std::string&, std::string::size_type&, SAX::Sink&);
+    bool isArray          (const std::string&, std::string::size_type&, SAX::Sink&);
+    bool isPair           (const std::string&, std::string::size_type&, SAX::Sink&);
+    bool isValue          (const std::string&, std::string::size_type&, SAX::Sink&);
+    bool isKey            (const std::string&, std::string::size_type&, SAX::Sink&);
+    bool isString         (const std::string&, std::string::size_type&, SAX::Sink&);
+    bool isStringValue    (const std::string&, std::string::size_type&, std::string&);
+    bool isNumber         (const std::string&, std::string::size_type&, SAX::Sink&);
+    bool isInt            (const std::string&, std::string::size_type&, std::string&);
+    bool isFrac           (const std::string&, std::string::size_type&, std::string&);
+    bool isDigits         (const std::string&, std::string::size_type&);
+    bool isDecDigit       (int);
+    bool isHexDigit       (int);
+    int  hexToInt         (int);
+    int  hexToInt         (int, int, int, int);
+    bool isExp            (const std::string&, std::string::size_type&, std::string&);
+    bool isE              (const std::string&, std::string::size_type&);
+    bool isBool           (const std::string&, std::string::size_type&, SAX::Sink&);
+    bool isNull           (const std::string&, std::string::size_type&, SAX::Sink&);
+    bool isLiteral        (const std::string&, char, std::string::size_type&);
+    void error            (const std::string&, std::string::size_type);
+  };
+}
+
+#endif
+////////////////////////////////////////////////////////////////////////////////
diff --git a/src/libshared/src/Lexer.cpp b/src/libshared/src/Lexer.cpp
new file mode 100644 (file)
index 0000000..95512a8
--- /dev/null
@@ -0,0 +1,943 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Copyright 2013 - 2017, Paul Beckingham, Federico Hernandez.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+// http://www.opensource.org/licenses/mit-license.php
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include <cmake.h>
+#include <Lexer.h>
+#include <Datetime.h>
+#include <Duration.h>
+#include <algorithm>
+#include <tuple>
+#include <ctype.h>
+#include <unicode.h>
+#include <utf8.h>
+
+static const std::string uuid_pattern = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx";
+static const unsigned int uuid_min_length = 8;
+
+std::string Lexer::dateFormat = "";
+
+////////////////////////////////////////////////////////////////////////////////
+Lexer::Lexer (const std::string& text)
+: _text (text)
+, _eos (text.size ())
+{
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// When a Lexer object is constructed with a string, this method walks through
+// the stream of low-level tokens.
+bool Lexer::token (std::string& token, Lexer::Type& type)
+{
+  // Eat white space.
+  while (unicodeWhitespace (_text[_cursor]))
+    utf8_next_char (_text, _cursor);
+
+  // Terminate at EOS.
+  if (isEOS ())
+    return false;
+
+  if (isString    (token, type, "'\"") ||
+      isUUID      (token, type, true)  ||
+      isDate      (token, type)        ||
+      isDuration  (token, type)        ||
+      isURL       (token, type)        ||
+      isHexNumber (token, type)        ||
+      isNumber    (token, type)        ||
+      isPath      (token, type)        ||
+      isPattern   (token, type)        ||
+      isOperator  (token, type)        ||
+      isWord      (token, type))
+    return true;
+
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+std::vector <std::tuple <std::string, Lexer::Type>> Lexer::tokenize (const std::string& input)
+{
+  std::vector <std::tuple <std::string, Lexer::Type>> tokens;
+
+  std::string token;
+  Lexer::Type type;
+  Lexer lexer (input);
+  while (lexer.token (token, type))
+    tokens.push_back (std::make_tuple (token, type));
+
+  return tokens;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// No L10N - these are for internal purposes.
+const std::string Lexer::typeName (const Lexer::Type& type)
+{
+  switch (type)
+  {
+  case Lexer::Type::uuid:         return "uuid";
+  case Lexer::Type::number:       return "number";
+  case Lexer::Type::hex:          return "hex";
+  case Lexer::Type::string:       return "string";
+  case Lexer::Type::url:          return "url";
+  case Lexer::Type::path:         return "path";
+  case Lexer::Type::pattern:      return "pattern";
+  case Lexer::Type::op:           return "op";
+  case Lexer::Type::word:         return "word";
+  case Lexer::Type::date:         return "date";
+  case Lexer::Type::duration:     return "duration";
+  }
+
+  return "unknown";
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Lexer::Type::number
+//   \d+
+//   [ . \d+ ]
+//   [ e|E [ +|- ] \d+ [ . \d+ ] ]
+//   not followed by non-operator.
+bool Lexer::isNumber (std::string& token, Lexer::Type& type)
+{
+  std::size_t marker = _cursor;
+
+  if (unicodeLatinDigit (_text[marker]))
+  {
+    ++marker;
+    while (unicodeLatinDigit (_text[marker]))
+      utf8_next_char (_text, marker);
+
+    if (_text[marker] == '.')
+    {
+      ++marker;
+      if (unicodeLatinDigit (_text[marker]))
+      {
+        ++marker;
+        while (unicodeLatinDigit (_text[marker]))
+          utf8_next_char (_text, marker);
+      }
+    }
+
+    if (_text[marker] == 'e' ||
+        _text[marker] == 'E')
+    {
+      ++marker;
+
+      if (_text[marker] == '+' ||
+          _text[marker] == '-')
+        ++marker;
+
+      if (unicodeLatinDigit (_text[marker]))
+      {
+        ++marker;
+        while (unicodeLatinDigit (_text[marker]))
+          utf8_next_char (_text, marker);
+
+        if (_text[marker] == '.')
+        {
+          ++marker;
+          if (unicodeLatinDigit (_text[marker]))
+          {
+            ++marker;
+            while (unicodeLatinDigit (_text[marker]))
+              utf8_next_char (_text, marker);
+          }
+        }
+      }
+    }
+
+    // Lookahread: !<unicodeWhitespace> | !<isSingleCharOperator>
+    // If there is an immediately consecutive character, that is not an operator, fail.
+    if (_eos > marker &&
+        ! unicodeWhitespace (_text[marker]) &&
+        ! isSingleCharOperator (_text[marker]))
+      return false;
+
+    token = _text.substr (_cursor, marker - _cursor);
+    type = Lexer::Type::number;
+    _cursor = marker;
+    return true;
+  }
+
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Lexer::Type::number
+//   \d+
+bool Lexer::isInteger (std::string& token, Lexer::Type& type)
+{
+  std::size_t marker = _cursor;
+
+  if (unicodeLatinDigit (_text[marker]))
+  {
+    ++marker;
+    while (unicodeLatinDigit (_text[marker]))
+      utf8_next_char (_text, marker);
+
+    token = _text.substr (_cursor, marker - _cursor);
+    type = Lexer::Type::number;
+    _cursor = marker;
+    return true;
+  }
+
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool Lexer::isSingleCharOperator (int c)
+{
+  return c == '+' ||  // Addition
+         c == '-' ||  // Subtraction or unary minus = ambiguous
+         c == '*' ||  // Multiplication
+         c == '/' ||  // Diviѕion
+         c == '(' ||  // Precedence open parenthesis
+         c == ')' ||  // Precedence close parenthesis
+         c == '<' ||  // Less than
+         c == '>' ||  // Greater than
+         c == '^' ||  // Exponent
+         c == '!' ||  // Unary not
+         c == '%' ||  // Modulus
+         c == '=' ||  // Partial match
+         c == '~';    // Pattern match
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool Lexer::isDoubleCharOperator (int c0, int c1, int c2)
+{
+  return (c0 == '=' && c1 == '=')                        ||
+         (c0 == '!' && c1 == '=')                        ||
+         (c0 == '<' && c1 == '=')                        ||
+         (c0 == '>' && c1 == '=')                        ||
+         (c0 == 'o' && c1 == 'r' && isBoundary (c1, c2)) ||
+         (c0 == '|' && c1 == '|')                        ||
+         (c0 == '&' && c1 == '&')                        ||
+         (c0 == '!' && c1 == '~');
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool Lexer::isTripleCharOperator (int c0, int c1, int c2, int c3)
+{
+  return (c0 == 'a' && c1 == 'n' && c2 == 'd' && isBoundary (c2, c3)) ||
+         (c0 == 'x' && c1 == 'o' && c2 == 'r' && isBoundary (c2, c3)) ||
+         (c0 == '!' && c1 == '=' && c2 == '=');
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool Lexer::isBoundary (int left, int right)
+{
+  // EOS
+  if (right == '\0')                                         return true;
+
+  // XOR
+  if (unicodeLatinAlpha (left) != unicodeLatinAlpha (right)) return true;
+  if (unicodeLatinDigit (left) != unicodeLatinDigit (right)) return true;
+  if (unicodeWhitespace (left) != unicodeWhitespace (right)) return true;
+
+  // OR
+  if (isPunctuation (left) || isPunctuation (right))         return true;
+
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool Lexer::isHardBoundary (int left, int right)
+{
+  // EOS
+  if (right == '\0')
+    return true;
+
+  // FILTER operators that don't need to be surrounded by whitespace.
+  if (left == '(' ||
+      left == ')' ||
+      right == '(' ||
+      right == ')')
+    return true;
+
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool Lexer::isPunctuation (int c)
+{
+  return isprint (c)   &&
+         c != ' '      &&
+         c != '@'      &&
+         c != '#'      &&
+         c != '$'      &&
+         c != '_'      &&
+         ! unicodeLatinDigit (c) &&
+         ! unicodeLatinAlpha (c);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Assumes that quotes is a string containing a non-trivial set of quote
+// characters.
+std::string Lexer::dequote (const std::string& input, const std::string& quotes)
+{
+  if (input.length () > 1)
+  {
+    int quote = input[0];
+    if (quotes.find (quote) != std::string::npos)
+    {
+      size_t len = input.length ();
+      if (quote == input[len - 1])
+        return input.substr (1, len - 2);
+    }
+  }
+
+  return input;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Detects characters in an input string that indicate quotes were required, or
+// escapes, to get them past the shell.
+bool Lexer::wasQuoted (const std::string& input)
+{
+  if (input.find_first_of (" \t()<>&~") != std::string::npos)
+    return true;
+
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool Lexer::isEOS () const
+{
+  return _cursor >= _eos;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Converts '0'     -> 0
+//          '9'     -> 9
+//          'a'/'A' -> 10
+//          'f'/'F' -> 15
+int Lexer::hexToInt (int c)
+{
+       if (c >= '0' && c <= '9') return (c - '0');
+  else if (c >= 'a' && c <= 'f') return (c - 'a' + 10);
+  else                           return (c - 'A' + 10);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+int Lexer::hexToInt (int c0, int c1)
+{
+  return (hexToInt (c0) << 4) + hexToInt (c1);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+int Lexer::hexToInt (int c0, int c1, int c2, int c3)
+{
+  return (hexToInt (c0) << 12) +
+         (hexToInt (c1) << 8)  +
+         (hexToInt (c2) << 4)  +
+          hexToInt (c3);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+std::string Lexer::trimLeft (const std::string& in, const std::string& t /*= " "*/)
+{
+  std::string::size_type ws = in.find_first_not_of (t);
+  if (ws > 0)
+  {
+    std::string out {in};
+    return out.erase (0, ws);
+  }
+
+  return in;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+std::string Lexer::trimRight (const std::string& in, const std::string& t /*= " "*/)
+{
+  std::string out {in};
+  return out.erase (in.find_last_not_of (t) + 1);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+std::string Lexer::trim (const std::string& in, const std::string& t /*= " "*/)
+{
+  return trimLeft (trimRight (in, t), t);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Lexer::Type::string
+//   '|"
+//   [ U+XXXX | \uXXXX | \" | \' | \\ | \/ | \b | \f | \n | \r | \t | . ]
+//   '|"
+bool Lexer::isString (std::string& token, Lexer::Type& type, const std::string& quotes)
+{
+  if (_enableString)
+  {
+    std::size_t marker = _cursor;
+    if (readWord (_text, quotes, marker, token))
+    {
+      type = Lexer::Type::string;
+      _cursor = marker;
+      return true;
+    }
+  }
+
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Lexer::Type::date
+//   <Datetime> (followed by eos, WS, operator)
+bool Lexer::isDate (std::string& token, Lexer::Type& type)
+{
+  if (_enableDate)
+  {
+    // Try an ISO date parse.
+    std::size_t i = _cursor;
+    Datetime d;
+    if (d.parse (_text, i, Lexer::dateFormat) &&
+        (i >= _eos ||
+         unicodeWhitespace (_text[i]) ||
+         isSingleCharOperator (_text[i])))
+    {
+      type = Lexer::Type::date;
+      token = _text.substr (_cursor, i - _cursor);
+      _cursor = i;
+      return true;
+    }
+  }
+
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Lexer::Type::duration
+//   <Duration> (followed by eos, WS, operator)
+bool Lexer::isDuration (std::string& token, Lexer::Type& type)
+{
+  if (_enableDuration)
+  {
+    std::size_t marker = _cursor;
+
+    std::string extractedToken;
+    Lexer::Type extractedType;
+    if (isOperator(extractedToken, extractedType))
+    {
+      _cursor = marker;
+      return false;
+    }
+
+    marker = _cursor;
+    Duration dur;
+    if (dur.parse (_text, marker) &&
+        (marker >= _eos ||
+         unicodeWhitespace (_text[marker]) ||
+         isSingleCharOperator (_text[marker])))
+    {
+      type = Lexer::Type::duration;
+      token = _text.substr (_cursor, marker - _cursor);
+      _cursor = marker;
+      return true;
+    }
+  }
+
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Lexer::Type::uuid
+//   XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
+//   XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXX
+//   XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXX
+//   XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXX
+//   ...
+//   XXXXXXXX-XX
+//   XXXXXXXX-X
+//   XXXXXXXX-
+//   XXXXXXXX
+//   Followed only by EOS, whitespace, or single character operator.
+bool Lexer::isUUID (std::string& token, Lexer::Type& type, bool endBoundary)
+{
+  if (_enableUUID)
+  {
+    std::size_t marker = _cursor;
+
+    // Greedy.
+    std::size_t i = 0;
+    for (; i < 36 && marker + i < _eos; i++)
+    {
+      if (uuid_pattern[i] == 'x')
+      {
+        if (! unicodeHexDigit (_text[marker + i]))
+          break;
+      }
+      else if (uuid_pattern[i] != _text[marker + i])
+        break;
+    }
+
+    if (i >= uuid_min_length                   &&
+        (! endBoundary                         ||
+         ! _text[marker + i]                   ||
+         unicodeWhitespace (_text[marker + i]) ||
+         isSingleCharOperator (_text[marker + i])))
+    {
+      token = _text.substr (_cursor, i);
+      type = Lexer::Type::uuid;
+      _cursor += i;
+      return true;
+    }
+
+  }
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Lexer::Type::hex
+//   0xX+
+bool Lexer::isHexNumber (std::string& token, Lexer::Type& type)
+{
+  if (_enableHexNumber)
+  {
+    std::size_t marker = _cursor;
+
+    if (_eos - marker >= 3 &&
+        _text[marker + 0] == '0' &&
+        _text[marker + 1] == 'x')
+    {
+      marker += 2;
+
+      while (unicodeHexDigit (_text[marker]))
+        ++marker;
+
+      if (marker - _cursor > 2)
+      {
+        token = _text.substr (_cursor, marker - _cursor);
+        type = Lexer::Type::hex;
+        _cursor = marker;
+        return true;
+      }
+    }
+  }
+
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Lexer::Type::word
+//   [^\s]+
+bool Lexer::isWord (std::string& token, Lexer::Type& type)
+{
+  if (_enableWord)
+  {
+    std::size_t marker = _cursor;
+
+    while (_text[marker] &&
+           ! unicodeWhitespace (_text[marker]) &&
+           (! _enableOperator || ! isSingleCharOperator (_text[marker])))
+      utf8_next_char (_text, marker);
+
+    if (marker > _cursor)
+    {
+      token = _text.substr (_cursor, marker - _cursor);
+      type = Lexer::Type::word;
+      _cursor = marker;
+      return true;
+    }
+  }
+
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Lexer::Type::url
+//   http [s] :// ...
+bool Lexer::isURL (std::string& token, Lexer::Type& type)
+{
+  if (_enableURL)
+  {
+    std::size_t marker = _cursor;
+
+    if (_eos - _cursor > 9 &&    // length 'https://*'
+        (_text[marker + 0] == 'h' || _text[marker + 0] == 'H') &&
+        (_text[marker + 1] == 't' || _text[marker + 1] == 'T') &&
+        (_text[marker + 2] == 't' || _text[marker + 2] == 'T') &&
+        (_text[marker + 3] == 'p' || _text[marker + 3] == 'P'))
+    {
+      marker += 4;
+      if (_text[marker + 0] == 's' || _text[marker + 0] == 'S')
+        ++marker;
+
+      if (_text[marker + 0] == ':' &&
+          _text[marker + 1] == '/' &&
+          _text[marker + 2] == '/')
+      {
+        marker += 3;
+
+        while (marker < _eos &&
+               ! unicodeWhitespace (_text[marker]))
+          utf8_next_char (_text, marker);
+
+        token = _text.substr (_cursor, marker - _cursor);
+        type = Lexer::Type::url;
+        _cursor = marker;
+        return true;
+      }
+    }
+  }
+
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Lexer::Type::path
+//   ( / <non-slash, non-whitespace> )+
+bool Lexer::isPath (std::string& token, Lexer::Type& type)
+{
+  if (_enablePath)
+  {
+    std::size_t marker = _cursor;
+    int slashCount = 0;
+
+    while (1)
+    {
+      if (_text[marker] == '/')
+      {
+        ++marker;
+        ++slashCount;
+      }
+      else
+        break;
+
+      if (_text[marker] &&
+          ! unicodeWhitespace (_text[marker]) &&
+          _text[marker] != '/')
+      {
+        utf8_next_char (_text, marker);
+        while (_text[marker] &&
+               ! unicodeWhitespace (_text[marker]) &&
+               _text[marker] != '/')
+          utf8_next_char (_text, marker);
+      }
+      else
+        break;
+    }
+
+    if (marker > _cursor &&
+        slashCount > 3)
+    {
+      type = Lexer::Type::path;
+      token = _text.substr (_cursor, marker - _cursor);
+      _cursor = marker;
+      return true;
+    }
+  }
+
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Lexer::Type::pattern
+//   / <unquoted-string> /  <EOS> | <unicodeWhitespace>
+bool Lexer::isPattern (std::string& token, Lexer::Type& type)
+{
+  if (_enablePattern)
+  {
+    std::size_t marker = _cursor;
+
+    std::string word;
+    if (readWord (_text, "/", _cursor, word) &&
+        (isEOS () ||
+         unicodeWhitespace (_text[_cursor])))
+    {
+      token = _text.substr (marker, _cursor - marker);
+      type = Lexer::Type::pattern;
+      return true;
+    }
+
+    _cursor = marker;
+  }
+
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Lexer::Type::op
+//   _hastag_ | _notag | _neg_ | _pos_ |
+//   <isTripleCharOperator> |
+//   <isDoubleCharOperator> |
+//   <isSingleCharOperator> |
+bool Lexer::isOperator (std::string& token, Lexer::Type& type)
+{
+  if (_enableOperator)
+  {
+    std::size_t marker = _cursor;
+
+    if (_eos - marker >= 8 && _text.substr (marker, 8) == "_hastag_")
+    {
+      marker += 8;
+      type = Lexer::Type::op;
+      token = _text.substr (_cursor, marker - _cursor);
+      _cursor = marker;
+      return true;
+    }
+
+    else if (_eos - marker >= 7 && _text.substr (marker, 7) == "_notag_")
+    {
+      marker += 7;
+      type = Lexer::Type::op;
+      token = _text.substr (_cursor, marker - _cursor);
+      _cursor = marker;
+      return true;
+    }
+
+    else if (_eos - marker >= 5 && _text.substr (marker, 5) == "_neg_")
+    {
+      marker += 5;
+      type = Lexer::Type::op;
+      token = _text.substr (_cursor, marker - _cursor);
+      _cursor = marker;
+      return true;
+    }
+
+    else if (_eos - marker >= 5 && _text.substr (marker, 5) == "_pos_")
+    {
+      marker += 5;
+      type = Lexer::Type::op;
+      token = _text.substr (_cursor, marker - _cursor);
+      _cursor = marker;
+      return true;
+    }
+
+    else if (_eos - marker >= 3 &&
+        isTripleCharOperator (_text[marker], _text[marker + 1], _text[marker + 2], _text[marker + 3]))
+    {
+      marker += 3;
+      type = Lexer::Type::op;
+      token = _text.substr (_cursor, marker - _cursor);
+      _cursor = marker;
+      return true;
+    }
+
+    else if (_eos - marker >= 2 &&
+        isDoubleCharOperator (_text[marker], _text[marker + 1], _text[marker + 2]))
+    {
+      marker += 2;
+      type = Lexer::Type::op;
+      token = _text.substr (_cursor, marker - _cursor);
+      _cursor = marker;
+      return true;
+    }
+
+    else if (isSingleCharOperator (_text[marker]))
+    {
+      token = _text[marker];
+      type = Lexer::Type::op;
+      _cursor = ++marker;
+      return true;
+    }
+  }
+
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Static
+std::string Lexer::typeToString (Lexer::Type type)
+{
+       if (type == Lexer::Type::string)       return std::string ("\033[38;5;7m\033[48;5;3m")    + "string"       + "\033[0m";
+  else if (type == Lexer::Type::uuid)         return std::string ("\033[38;5;7m\033[48;5;10m")   + "uuid"         + "\033[0m";
+  else if (type == Lexer::Type::hex)          return std::string ("\033[38;5;7m\033[48;5;14m")   + "hex"          + "\033[0m";
+  else if (type == Lexer::Type::number)       return std::string ("\033[38;5;7m\033[48;5;6m")    + "number"       + "\033[0m";
+  else if (type == Lexer::Type::url)          return std::string ("\033[38;5;7m\033[48;5;4m")    + "url"          + "\033[0m";
+  else if (type == Lexer::Type::path)         return std::string ("\033[37;102m")                + "path"         + "\033[0m";
+  else if (type == Lexer::Type::pattern)      return std::string ("\033[37;42m")                 + "pattern"      + "\033[0m";
+  else if (type == Lexer::Type::op)           return std::string ("\033[38;5;7m\033[48;5;203m")  + "op"           + "\033[0m";
+  else if (type == Lexer::Type::word)         return std::string ("\033[38;5;15m\033[48;5;236m") + "word"         + "\033[0m";
+  else if (type == Lexer::Type::date)         return std::string ("\033[38;5;15m\033[48;5;34m")  + "date"         + "\033[0m";
+  else if (type == Lexer::Type::duration)     return std::string ("\033[38;5;15m\033[48;5;34m")  + "duration"     + "\033[0m";
+  else                                        return std::string ("\033[37;41m")                 + "unknown"      + "\033[0m";
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Full implementation of a quoted word.  Includes:
+//   '\''
+//   '"'
+//   "'"
+//   "\""
+//   'one two'
+// Result includes the quotes.
+bool Lexer::readWord (
+  const std::string& text,
+  const std::string& quotes,
+  std::string::size_type& cursor,
+  std::string& word)
+{
+  if (quotes.find (text[cursor]) == std::string::npos)
+    return false;
+
+  std::string::size_type eos = text.length ();
+  int quote = text[cursor++];
+  word = quote;
+
+  int c;
+  while ((c = text[cursor]))
+  {
+    // Quoted word ends on a quote.
+    if (quote && quote == c)
+    {
+      word += utf8_character (utf8_next_char (text, cursor));
+      break;
+    }
+
+    // Unicode U+XXXX or \uXXXX codepoint.
+    else if (eos - cursor >= 6 &&
+             ((text[cursor + 0] == 'U'  && text[cursor + 1] == '+') ||
+              (text[cursor + 0] == '\\' && text[cursor + 1] == 'u')) &&
+             unicodeHexDigit (text[cursor + 2]) &&
+             unicodeHexDigit (text[cursor + 3]) &&
+             unicodeHexDigit (text[cursor + 4]) &&
+             unicodeHexDigit (text[cursor + 5]))
+    {
+      word += utf8_character (
+                hexToInt (
+                  text[cursor + 2],
+                  text[cursor + 3],
+                  text[cursor + 4],
+                  text[cursor + 5]));
+      cursor += 6;
+    }
+
+    // An escaped thing.
+    else if (c == '\\')
+    {
+      c = text[++cursor];
+
+      switch (c)
+      {
+      case '"':  word += (char) 0x22; ++cursor; break;
+      case '\'': word += (char) 0x27; ++cursor; break;
+      case '\\': word += (char) 0x5C; ++cursor; break;
+      case 'b':  word += (char) 0x08; ++cursor; break;
+      case 'f':  word += (char) 0x0C; ++cursor; break;
+      case 'n':  word += (char) 0x0A; ++cursor; break;
+      case 'r':  word += (char) 0x0D; ++cursor; break;
+      case 't':  word += (char) 0x09; ++cursor; break;
+      case 'v':  word += (char) 0x0B; ++cursor; break;
+
+      // This pass-through default case means that anything can be escaped
+      // harmlessly. In particular 'quote' is included, if it not one of the
+      // above characters.
+      default:   word += (char) c;    ++cursor; break;
+      }
+    }
+
+    // Ordinary character.
+    else
+      word += utf8_character (utf8_next_char (text, cursor));
+  }
+
+  // Verify termination.
+  return word[0]                  == quote &&
+         word[word.length () - 1] == quote &&
+         word.length () >= 2;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Full implementation of an unquoted word.  Includes:
+//   one\ two
+//   abcU+0020def
+//   abc\u0020def
+//   a\tb
+//
+// Ends at:
+//   Lexer::isEOS
+//   unicodeWhitespace
+//   Lexer::isHardBoundary
+bool Lexer::readWord (
+  const std::string& text,
+  std::string::size_type& cursor,
+  std::string& word)
+{
+  std::string::size_type eos = text.length ();
+
+  word = "";
+  int c;
+  int prev = 0;
+  while ((c = text[cursor]))  // Handles EOS.
+  {
+    // Unquoted word ends on white space.
+    if (unicodeWhitespace (c))
+      break;
+
+    // Parentheses mostly.
+    if (prev && Lexer::isHardBoundary (prev, c))
+      break;
+
+    // Unicode U+XXXX or \uXXXX codepoint.
+    else if (eos - cursor >= 6 &&
+             ((text[cursor + 0] == 'U'  && text[cursor + 1] == '+') ||
+              (text[cursor + 0] == '\\' && text[cursor + 1] == 'u')) &&
+             unicodeHexDigit (text[cursor + 2]) &&
+             unicodeHexDigit (text[cursor + 3]) &&
+             unicodeHexDigit (text[cursor + 4]) &&
+             unicodeHexDigit (text[cursor + 5]))
+    {
+      word += utf8_character (
+                hexToInt (
+                  text[cursor + 2],
+                  text[cursor + 3],
+                  text[cursor + 4],
+                  text[cursor + 5]));
+      cursor += 6;
+    }
+
+    // An escaped thing.
+    else if (c == '\\')
+    {
+      c = text[++cursor];
+
+      switch (c)
+      {
+      case '"':  word += (char) 0x22; ++cursor; break;
+      case '\'': word += (char) 0x27; ++cursor; break;
+      case '\\': word += (char) 0x5C; ++cursor; break;
+      case 'b':  word += (char) 0x08; ++cursor; break;
+      case 'f':  word += (char) 0x0C; ++cursor; break;
+      case 'n':  word += (char) 0x0A; ++cursor; break;
+      case 'r':  word += (char) 0x0D; ++cursor; break;
+      case 't':  word += (char) 0x09; ++cursor; break;
+      case 'v':  word += (char) 0x0B; ++cursor; break;
+
+      // This pass-through default case means that anything can be escaped
+      // harmlessly. In particular 'quote' is included, if it not one of the
+      // above characters.
+      default:   word += (char) c;    ++cursor; break;
+      }
+    }
+
+    // Ordinary character.
+    else
+      word += utf8_character (utf8_next_char (text, cursor));
+
+    prev = c;
+  }
+
+  return word.length () > 0 ? true : false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
diff --git a/src/libshared/src/Lexer.h b/src/libshared/src/Lexer.h
new file mode 100644 (file)
index 0000000..03b5166
--- /dev/null
@@ -0,0 +1,119 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Copyright 2013 - 2017, Paul Beckingham, Federico Hernandez.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+// http://www.opensource.org/licenses/mit-license.php
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#ifndef INCLUDED_LEXER
+#define INCLUDED_LEXER
+
+#include <string>
+#include <map>
+#include <vector>
+#include <tuple>
+#include <cstddef>
+
+class Lexer
+{
+public:
+  // These are overridable.
+  static std::string dateFormat;
+
+  enum class Type { uuid, number, hex,
+                    string,
+                    url,
+                    path,
+                    pattern,
+                    op,
+                    word,
+                    date, duration };
+
+  explicit Lexer (const std::string&);
+  bool token (std::string&, Lexer::Type&);
+  static std::string typeToString (Lexer::Type);
+
+  // Static helpers.
+  static std::vector <std::tuple <std::string, Lexer::Type>> tokenize (const std::string&);
+  static const std::string typeName          (const Lexer::Type&);
+  static bool isSingleCharOperator           (int);
+  static bool isDoubleCharOperator           (int, int, int);
+  static bool isTripleCharOperator           (int, int, int, int);
+  static bool isBoundary                     (int, int);
+  static bool isHardBoundary                 (int, int);
+  static bool isPunctuation                  (int);
+  static bool wasQuoted                      (const std::string&);
+  static bool readWord                       (const std::string&, const std::string&, std::string::size_type&, std::string&);
+  static bool readWord                       (const std::string&, std::string::size_type&, std::string&);
+  static int hexToInt                        (int);
+  static int hexToInt                        (int, int);
+  static int hexToInt                        (int, int, int, int);
+  static std::string trimLeft                (const std::string& in, const std::string& t = " ");
+  static std::string trimRight               (const std::string& in, const std::string& t = " ");
+  static std::string trim                    (const std::string& in, const std::string& t = " ");
+  static std::string dequote                 (const std::string&, const std::string& quotes = "'\"");
+
+  // Stream Classifiers.
+  bool isEOS          () const;
+  bool isString       (std::string&, Lexer::Type&, const std::string&);
+  bool isDate         (std::string&, Lexer::Type&);
+  bool isDuration     (std::string&, Lexer::Type&);
+  bool isUUID         (std::string&, Lexer::Type&, bool);
+  bool isNumber       (std::string&, Lexer::Type&);
+  bool isInteger      (std::string&, Lexer::Type&);
+  bool isHexNumber    (std::string&, Lexer::Type&);
+  bool isURL          (std::string&, Lexer::Type&);
+  bool isPath         (std::string&, Lexer::Type&);
+  bool isPattern      (std::string&, Lexer::Type&);
+  bool isOperator     (std::string&, Lexer::Type&);
+  bool isWord         (std::string&, Lexer::Type&);
+
+  // Disabling features.
+  void noString ()    { _enableString    = false; }
+  void noDate ()      { _enableDate      = false; }
+  void noDuration ()  { _enableDuration  = false; }
+  void noUUID ()      { _enableUUID      = false; }
+  void noHexNumber () { _enableHexNumber = false; }
+  void noWord ()      { _enableWord      = false; }
+  void noURL ()       { _enableURL       = false; }
+  void noPath ()      { _enablePath      = false; }
+  void noPattern ()   { _enablePattern   = false; }
+  void noOperator ()  { _enableOperator  = false; }
+
+private:
+  std::string _text          {};
+  std::size_t _cursor        {0};
+  std::size_t _eos           {0};
+
+  bool        _enableString    {true};
+  bool        _enableDate      {true};
+  bool        _enableDuration  {true};
+  bool        _enableUUID      {true};
+  bool        _enableHexNumber {true};
+  bool        _enableWord      {true};
+  bool        _enableURL       {true};
+  bool        _enablePath      {true};
+  bool        _enablePattern   {true};
+  bool        _enableOperator  {true};
+};
+
+#endif
diff --git a/src/libshared/src/Log.cpp b/src/libshared/src/Log.cpp
new file mode 100644 (file)
index 0000000..a77d600
--- /dev/null
@@ -0,0 +1,123 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Copyright 2015 - 2017, Paul Beckingham, Federico Hernandez.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+// http://www.opensource.org/licenses/mit-license.php
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include <cmake.h>
+#include <Log.h>
+#include <shared.h>
+#include <format.h>
+
+////////////////////////////////////////////////////////////////////////////////
+std::string Log::file () const
+{
+  return _name;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+void Log::file (const std::string& name)
+{
+  _name = name;
+  _file = File (_name);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+void Log::ignore (const std::string& category)
+{
+  _ignore.insert (category);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+void Log::write (const std::string& line)
+{
+  write ("info", line);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+void Log::write (const std::string& category, const std::string& line)
+{
+  // Determine if this category is in the ignore list.
+  if (_ignore.find (category) != _ignore.end ())
+    return;
+
+  if (line != "")
+  {
+    // If line contains newlines, split it into separate lines and log each one.
+    if (line.find ('\n') != std::string::npos)
+    {
+      for (const auto& single : split (line, '\n'))
+        write (category, single);
+
+      return;
+    }
+
+    // Single line.
+    if (line == _prior)
+    {
+      ++_repetition;
+    }
+    else
+    {
+      _prior = line;
+
+      // Catch up by writing out the backlog.
+      if (_name != "" && _backlog.size ())
+      {
+        for (const auto& line : _backlog)
+          _file.append (line); 
+
+        _backlog.clear ();
+      }
+
+      if (_repetition)
+      {
+        std::string message = Datetime ().toISO ()
+                            + ' ' + PACKAGE_VERSION
+                            + ' ' + category
+                            + ' ' + format ("(Repeated {1} times)", _repetition)
+                            + '\n';
+
+        if (_name != "")
+          _file.append (message);
+        else
+          _backlog.push_back (message);
+
+        _repetition = 0;
+      }
+
+      std::string message = Datetime ().toISO ()
+                          + ' ' + PACKAGE_VERSION
+                          + ' ' + category
+                          + ' ' + line
+                          + '\n';
+
+      if (_name != "")
+        _file.append (message);
+      else
+        _backlog.push_back (message);
+    }
+  }
+}
+
+////////////////////////////////////////////////////////////////////////////////
diff --git a/src/libshared/src/Log.h b/src/libshared/src/Log.h
new file mode 100644 (file)
index 0000000..06981ed
--- /dev/null
@@ -0,0 +1,56 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Copyright 2015 - 2017, Paul Beckingham, Federico Hernandez.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+// http://www.opensource.org/licenses/mit-license.php
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#ifndef INCLUDED_LOG
+#define INCLUDED_LOG
+
+#include <FS.h>
+#include <Datetime.h>
+#include <vector>
+#include <set>
+#include <string>
+#include <stdio.h>
+
+class Log
+{
+public:
+  std::string file () const;
+  void file (const std::string&);
+  void ignore (const std::string&);
+  void write (const std::string&);
+  void write (const std::string&, const std::string&);
+
+private:
+  std::string               _name       {};
+  std::vector <std::string> _backlog    {};
+  File                      _file       {};
+  std::string               _prior      {"none"};
+  int                       _repetition {0};
+  std::set <std::string>    _ignore     {};
+};
+
+#endif
+
diff --git a/src/libshared/src/Msg.cpp b/src/libshared/src/Msg.cpp
new file mode 100644 (file)
index 0000000..99664b9
--- /dev/null
@@ -0,0 +1,115 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Copyright 2006 - 2017, Paul Beckingham, Federico Hernandez.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+// http://www.opensource.org/licenses/mit-license.php
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include <cmake.h>
+#include <Msg.h>
+#include <shared.h>
+#include <format.h>
+
+////////////////////////////////////////////////////////////////////////////////
+void Msg::set (const std::string& name, int value)
+{
+  _header[name] = format (value);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+void Msg::set (const std::string& name, const std::string& value)
+{
+  _header[name] = value;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+std::string Msg::get (const std::string& name) const
+{
+  auto i = _header.find (name);
+  if (i != _header.end ())
+    return i->second;
+
+  return "";
+}
+
+////////////////////////////////////////////////////////////////////////////////
+void Msg::setPayload (const std::string& payload)
+{
+  _payload = payload;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+std::string Msg::getPayload () const
+{
+  return _payload;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+std::vector <std::string> Msg::all () const
+{
+  std::vector <std::string> names;
+  for (auto& i : _header)
+    names.push_back (i.first);
+
+  return names;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+std::string Msg::serialize () const
+{
+  std::string output;
+  for (auto& i : _header)
+    output += i.first + ": " + i.second + '\n';
+
+  output += '\n' + _payload + '\n';
+
+  return output;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool Msg::parse (const std::string& input)
+{
+  _header.clear ();
+  _payload = "";
+
+  auto separator = input.find ("\n\n");
+  if (separator == std::string::npos)
+    throw std::string ("Malformed message");
+
+  // Parse header.
+  auto lines = split (input.substr (0, separator), '\n');
+  for (auto& i : lines)
+  {
+    auto delimiter = i.find (':');
+    if (delimiter == std::string::npos)
+        throw std::string ("Malformed message header '") + i + '\'';
+
+    _header[trim (i.substr (0, delimiter))] = trim (i.substr (delimiter + 1));
+  }
+
+  // Parse payload.
+  _payload = input.substr (separator + 2);
+
+  return true;
+}
+
+////////////////////////////////////////////////////////////////////////////////
diff --git a/src/libshared/src/Msg.h b/src/libshared/src/Msg.h
new file mode 100644 (file)
index 0000000..69ed2da
--- /dev/null
@@ -0,0 +1,54 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Copyright 2006 - 2017, Paul Beckingham, Federico Hernandez.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+// http://www.opensource.org/licenses/mit-license.php
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#ifndef INCLUDED_MSG
+#define INCLUDED_MSG
+
+#include <map>
+#include <vector>
+#include <string>
+
+class Msg
+{
+public:
+  void set (const std::string&, int);
+  void set (const std::string&, const std::string&);
+  std::string get (const std::string&) const;
+
+  void setPayload (const std::string&);
+  std::string getPayload () const;
+
+  std::vector <std::string> all () const;
+  std::string serialize () const;
+  bool parse (const std::string&);
+
+private:
+  std::map <std::string, std::string> _header {};
+  std::string _payload                        {""};
+};
+
+#endif
+
diff --git a/src/libshared/src/PEG.cpp b/src/libshared/src/PEG.cpp
new file mode 100644 (file)
index 0000000..efa5f61
--- /dev/null
@@ -0,0 +1,436 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Copyright 2015 - 2017, Paul Beckingham, Federico Hernandez.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+// http://www.opensource.org/licenses/mit-license.php
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include <cmake.h>
+#include <PEG.h>
+#include <Lexer.h>
+#include <shared.h>
+#include <format.h>
+#include <iostream>
+
+////////////////////////////////////////////////////////////////////////////////
+std::string PEG::Token::dump () const
+{
+  std::stringstream out;
+  out << (_lookahead == Token::Lookahead::positive ? "\e[34m&\e[0m" :
+          _lookahead == Token::Lookahead::negative ? "\e[34m!\e[0m" : "")
+      << _token
+      << (_quantifier == Token::Quantifier::zero_or_one  ? "\e[34m?\e[0m" :
+          _quantifier == Token::Quantifier::one_or_more  ? "\e[34m+\e[0m" :
+          _quantifier == Token::Quantifier::zero_or_more ? "\e[34m*\e[0m" : "");
+
+  for (const auto& tag : _tags)
+    out << " \e[34m" << tag << "\e[0m";
+
+  return out.str ();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+void PEG::loadFromFile (File& file)
+{
+  if (! file.exists ())
+    throw format ("PEG file '{1}' not found.", file._data);
+
+  std::string contents;
+  file.read (contents);
+
+  // TODO Instead of simply reading a file, read a file and allow lines that
+  //      match /^include <path>$/ to represent nested files.
+
+  loadFromString (contents);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Load and parse PEG.
+//
+// Syntax:
+//   rule-name:  alternate1-token1 alternate1-token2
+//               alternate2-token1
+//
+// - Rules are aligned at left margin only, followed by colon.
+// - Productions are indented and never at left margin.
+// - Blank lines delineate rules.
+//
+// Details:
+// - String literals are always double-quoted.
+// - Character literals are alをays single-quoted.
+// - "*", "+" and "?" suffixes have POSIX wildcard semantics.
+//
+void PEG::loadFromString (const std::string& input)
+{
+  // This is a state machine.  Read each line.
+  std::string rule_name = "";
+  for (auto& line : loadImports (split (input, '\n')))
+  {
+    line = trim (line);
+
+    // Eliminate inline comments.
+    auto hash = line.find ('#');
+    if (hash != std::string::npos)
+    {
+      line.resize (hash);
+      line = trim (line);
+
+      if (line == "")
+        continue;
+    }
+    else
+    {
+      line = trim (line);
+    }
+
+    // Skip blank lines with no semantics.
+    if (line == "" and rule_name == "")
+      continue;
+
+    if (line != "")
+    {
+      int token_count = 0;
+
+      // Instantiate and configure the Lexer.
+      Lexer l (line);
+      l.noDate ();
+      l.noDuration ();
+      l.noUUID ();
+      l.noHexNumber ();
+      l.noURL ();
+      l.noPath ();
+      l.noPattern ();
+      l.noOperator ();
+
+      Lexer::Type type;
+      std::string token;
+      while (l.token (token, type))
+      {
+        ++token_count;
+
+        // Rule definitions end in a colon.
+        if (token.back () == ':')
+        {
+          // Capture the Rule_name.
+          rule_name = token.substr (0, token.size () - 1);
+
+          // If this is the first Rule, capture it as a starting point.
+          if (_start == "")
+            _start = rule_name;
+
+          _rules[rule_name] = PEG::Rule ();
+          token_count = 0;
+        }
+        // Production definition.
+        else
+        {
+          // If no Production was added yet, add one.
+          if (token_count <= 1)
+            _rules[rule_name].push_back (PEG::Production ());
+
+          // Decorate the token, if necessary.
+          std::string::size_type start = 0;
+          std::string::size_type end = token.length ();
+
+          auto q = Token::Quantifier::one;
+          auto l = Token::Lookahead::none;
+
+          if (token.back () == '?')
+          {
+            q = Token::Quantifier::zero_or_one;
+            --end;
+          }
+          else if (token.back () == '+')
+          {
+            q = Token::Quantifier::one_or_more;
+            --end;
+          }
+          else if (token.back () == '*')
+          {
+            q = Token::Quantifier::zero_or_more;
+            --end;
+          }
+
+          if (token.front () == '&')
+          {
+            l = Token::Lookahead::positive;
+            ++start;
+          }
+          else if (token.front () == '!')
+          {
+            l = Token::Lookahead::negative;
+            ++start;
+          }
+
+          PEG::Token t (token.substr (start, end - start));
+          t._quantifier = q;
+          t._lookahead = l;
+
+          if (type == Lexer::Type::string)
+          {
+            t.tag ("literal");
+            t.tag (token[0] == '\'' ? "character" : "string");
+          }
+          else if (t._token.front () == '<' and
+                   t._token.back ()  == '>')
+          {
+            t.tag ("intrinsic");
+
+            if (t._token.substr (0, 8) == "<entity:")
+              t.tag ("entity");
+
+            if (t._token.substr (0, 10) == "<external:")
+              t.tag ("external");
+          }
+
+          // Add the new Token to the most recent Production, of the current
+          // Rule.
+          _rules[rule_name].back ().push_back (t);
+        }
+      }
+    }
+
+    // A blank line in the input ends the current rule definition.
+    else
+      rule_name = "";
+  }
+
+  if (_debug)
+    std::cout << dump ();
+
+  // Validate the parsed grammar.
+  validate ();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+std::map <std::string, PEG::Rule> PEG::syntax () const
+{
+  return _rules;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+std::string PEG::firstRule () const
+{
+  return _start;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+void PEG::debug ()
+{
+  ++_debug;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+void PEG::strict (bool value)
+{
+  _strict = value;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+std::string PEG::dump () const
+{
+  std::stringstream out;
+  out << "PEG\n";
+
+  // Show the import files, if any.
+  if (_imports.size ())
+  {
+    for (const auto& import : _imports)
+      out << "  import " << import << '\n';
+    out << '\n';
+  }
+
+  // Determine longest rule name, for display alignment.
+  size_t longest = 0;
+  for (const auto& rule : _rules)
+    if (rule.first.length () > longest)
+      longest = rule.first.length ();
+
+  for (const auto& rule : _rules)
+  {
+    // Indicate the start Rule.
+    out << "  "
+        << (rule.first == _start ? "▶" : " ")
+        << ' '
+        << rule.first
+        << ':'
+        << std::string (1 + longest - rule.first.length (), ' ');
+
+    int count = 0;
+    for (const auto& production : rule.second)
+    {
+      if (count)
+        out << std::string (6 + longest, ' ');
+
+      for (const auto& token : production)
+        out << token.dump () << ' ';
+
+      out << "\n";
+      ++count;
+    }
+
+    out << "\n";
+  }
+
+  return out.str ();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+std::vector <std::string> PEG::loadImports (const std::vector <std::string>& lines)
+{
+  std::vector <std::string> resolved;
+
+  for (auto& line : lines)
+  {
+    auto copy = trim (line);
+
+    auto hash = copy.find ('#');
+    if (hash != std::string::npos)
+    {
+      copy.resize (hash);
+      copy = trim (copy);
+    }
+
+    if (copy.find ("import ") == 0)
+    {
+      File file (trim (copy.substr (7)));
+      if (file.exists () &&
+          file.readable ())
+      {
+        // Only import files that are not already imported.
+        if (std::find (_imports.begin (), _imports.end (), file._data) == _imports.end ())
+        {
+          _imports.push_back (file._data);
+
+          std::vector <std::string> imported;
+          file.read (imported);
+          imported = loadImports (imported);
+
+          resolved.insert(std::end(resolved), std::begin(imported), std::end(imported));
+        }
+      }
+      else
+        throw format ("Cannot import '{1}'", file._data);
+    }
+    else
+    {
+      // Store original line.
+      resolved.push_back (line);
+    }
+  }
+
+  return resolved;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+void PEG::validate () const
+{
+  if (_start == "")
+    throw std::string ("There are no rules defined.");
+
+  std::vector <std::string> allRules;
+  std::vector <std::string> allTokens;
+  std::vector <std::string> allLeftRecursive;
+  std::vector <std::string> intrinsics;
+  std::vector <std::string> externals;
+
+  for (const auto& rule : _rules)
+  {
+    allRules.push_back (rule.first);
+
+    for (const auto& production : rule.second)
+    {
+      for (const auto& token : production)
+      {
+        if (token.hasTag ("intrinsic"))
+          intrinsics.push_back (token._token);
+
+        else if (token.hasTag ("external"))
+          externals.push_back (token._token);
+
+        else if (! token.hasTag ("literal"))
+          allTokens.push_back (token._token);
+
+        if (token._token == production[0]._token and
+            rule.first == production[0]._token   and
+            production.size () == 1)
+          allLeftRecursive.push_back (token._token);
+      }
+    }
+  }
+
+  std::vector <std::string> notUsed;
+  std::vector <std::string> notDefined;
+  listDiff (allRules, allTokens, notUsed, notDefined);
+
+  // Undefined value - these are definitions that appear in token, but are
+  // not in _rules.
+  for (const auto& nd : notDefined)
+    if (std::find (externals.begin (), externals.end (), nd) == externals.end ())
+      throw format ("Definition '{1}' referenced, but not defined.", nd);
+
+  // Circular definitions - these are names in _rules that also appear as
+  // the only token in any of the alternates for that definition.
+  for (const auto& lr : allLeftRecursive)
+    throw format ("Definition '{1}' is left recursive.", lr);
+
+  for (const auto& r : allRules)
+    if (r[0] == '<')
+      throw format ("Definition '{1}' may not redefine an intrinsic.");
+
+  for (const auto& r : allRules)
+    if (r[0] == '"' or
+        r[0] == '\'')
+      throw format ("Definition '{1}' may not be a literal.");
+
+  // Unused definitions - these are names in _rules that are never
+  // referenced as token.
+  for (const auto& nu : notUsed)
+  {
+    if (std::find (externals.begin (), externals.end (), nu) == externals.end () &&
+        nu != _start)
+    {
+      if (_strict)
+        throw format ("Definition '{1}' is defined, but not referenced.", nu);
+      else
+        std::cout << "Warning: Definition '" << nu << "' is defined, but not referenced.\n";
+    }
+  }
+
+  // Intrinsics must be recognized.
+  for (auto& intrinsic : intrinsics)
+    if (intrinsic != "<digit>"                 &&
+        intrinsic != "<hex>"                   &&
+        intrinsic != "<punct>"                 &&
+        intrinsic != "<eol>"                   &&
+        intrinsic != "<sep>"                   &&
+        intrinsic != "<ws>"                    &&
+        intrinsic != "<alpha>"                 &&
+        intrinsic != "<character>"             &&
+        intrinsic != "<word>"                  &&
+        intrinsic != "<token>"                 &&
+        intrinsic.substr (0, 8)  != "<entity:" &&
+        intrinsic.substr (0, 10) != "<external:")
+      throw format ("Specified intrinsic '{1}' is not supported.", intrinsic);
+}
+
+////////////////////////////////////////////////////////////////////////////////
diff --git a/src/libshared/src/PEG.h b/src/libshared/src/PEG.h
new file mode 100644 (file)
index 0000000..bf99b34
--- /dev/null
@@ -0,0 +1,89 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Copyright 2015 - 2017, Paul Beckingham, Federico Hernandez.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+// http://www.opensource.org/licenses/mit-license.php
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#ifndef INCLUDED_PEG
+#define INCLUDED_PEG
+
+#include <FS.h>
+#include <string>
+#include <vector>
+#include <map>
+#include <set>
+
+class PEG
+{
+public:
+  class Token
+  {
+  public:
+    Token (const std::string& value)           { _token = value; }
+    void tag (const std::string& tag)          { _tags.insert (tag); }
+    bool hasTag (const std::string& tag) const { return _tags.find (tag) != _tags.end (); };
+    std::string dump () const;
+
+    enum class Quantifier                      { one, zero_or_one, one_or_more, zero_or_more };
+    enum class Lookahead                       { none, positive, negative };
+
+    std::string _token                         {};
+    std::set <std::string> _tags               {};
+    Quantifier  _quantifier                    {Quantifier::one};
+    Lookahead   _lookahead                     {Lookahead::none};
+    // TODO Added Lexer::Type support, which allows the PEG to specify
+    //      "<Lexer::Type>" as a built-in type.
+  };
+
+  class Production : public std::vector <Token>
+  {
+  };
+
+  class Rule : public std::vector <Production>
+  {
+  };
+
+public:
+  void loadFromFile (File&);
+  void loadFromString (const std::string&);
+  std::map <std::string, PEG::Rule> syntax () const;
+  std::string firstRule () const;
+  void debug ();
+  void strict (bool);
+  std::string dump () const;
+
+private:
+  std::vector <std::string> loadImports (const std::vector <std::string>&);
+  void validate () const;
+
+private:
+  //        rule name    rule
+  //        |            |
+  std::map <std::string, PEG::Rule> _rules     {};
+  std::string                       _start     {};
+  int                               _debug     {0};
+  bool                              _strict    {false};
+  std::vector <std::string>         _imports   {};
+};
+
+#endif
diff --git a/src/libshared/src/Packrat.cpp b/src/libshared/src/Packrat.cpp
new file mode 100644 (file)
index 0000000..2d1d358
--- /dev/null
@@ -0,0 +1,770 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Copyright 2015 - 2017, Paul Beckingham, Federico Hernandez.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+// http://www.opensource.org/licenses/mit-license.php
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include <cmake.h>
+#include <Packrat.h>
+#include <shared.h>
+#include <format.h>
+#include <unicode.h>
+#include <utf8.h>
+#include <iostream>
+
+int Packrat::minimumMatchLength = 3;
+
+////////////////////////////////////////////////////////////////////////////////
+void Packrat::debug ()
+{
+  ++_debug;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Walk the grammar tree to parse the input text, resulting in a parse tree.
+void Packrat::parse (const PEG& peg, const std::string& input)
+{
+  // Used to walk the grammar tree.
+  // Note there is only one rule at the top of the syntax tree, which was the
+  // first one defined.
+  _syntax = peg.syntax ();
+  _tree->_name = peg.firstRule ();
+
+  // The pig that will be sent down the pipe.
+  Pig pig (input);
+  if (_debug)
+    std::cout << "trace " << pig.dump () << "\n";
+
+  // Match the first rule.  Recursion does the rest.
+  if (! matchRule (_tree->_name, pig, _tree, 0))
+    throw std::string ("Parse failed.");
+
+  if (! pig.eos ())
+    throw format ("Parse failed - extra character at position {1}.", pig.cursor ());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+void Packrat::entity (const std::string& category, const std::string& name)
+{
+  // Walk the list of entities for category.
+  auto c = _entities.equal_range (category);
+  for (auto e = c.first; e != c.second; ++e)
+    if (e->second == name)
+      return;
+
+  // The category/name pair was not found, therefore add it.
+  _entities.insert (std::pair <std::string, std::string> (category, name));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+void Packrat::external (
+  const std::string& rule,
+  bool (*fn)(Pig&, std::shared_ptr <Tree>))
+{
+  if (_externals.find (rule) != _externals.end ())
+    throw format ("There is already an external parser defined for rule '{1}'.", rule);
+
+  _externals[rule] = fn;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// If there is a match, pig advances further down the pipe.
+bool Packrat::matchRule (
+  const std::string& rule,
+  Pig& pig,
+  std::shared_ptr <Tree> parseTree,
+  int indent)
+{
+  if (_debug > 1)
+    std::cout << "trace " << std::string (indent, ' ') << "matchRule " << rule << "\n";
+  auto checkpoint = pig.cursor ();
+
+  for (const auto& production : _syntax.find (rule)->second)
+    if (matchProduction (production, pig, parseTree, indent + 1))
+      return true;
+
+  pig.restoreTo (checkpoint);
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool Packrat::matchProduction (
+  const PEG::Production& production,
+  Pig& pig,
+  std::shared_ptr <Tree> parseTree,
+  int indent)
+{
+  if (_debug > 1)
+    std::cout << "trace " << std::string (indent, ' ') << "matchProduction\n";
+  auto checkpoint = pig.cursor ();
+
+  auto collector = std::make_shared <Tree> ();
+  for (const auto& token : production)
+  {
+    auto b = std::make_shared <Tree> ();
+    if (! matchTokenQuant (token, pig, b, indent + 1))
+    {
+      pig.restoreTo (checkpoint);
+      return false;
+    }
+
+    // Accumulate branches.
+    collector->addBranch (b);
+  }
+
+  // On success transfer all sub-branches.
+  for (auto& b : collector->_branches)
+    for (auto sub : b->_branches)
+      parseTree->addBranch (sub);
+
+  return true;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Wraps calls to matchTokenLookahead, while properly handling the quantifier.
+bool Packrat::matchTokenQuant (
+  const PEG::Token& token,
+  Pig& pig,
+  std::shared_ptr <Tree> parseTree,
+  int indent)
+{
+  if (_debug > 1)
+    std::cout << "trace " << std::string (indent, ' ') << "matchTokenQuant " << token.dump () << "\n";
+
+  // Must match exactly once, so run once and return the result.
+  if (token._quantifier == PEG::Token::Quantifier::one)
+  {
+    return matchTokenLookahead (token, pig, parseTree, indent + 1);
+  }
+
+  // May match zero or one time.  If it matches, the cursor will be advanced.
+  // If it fails, the cursor will not be advanced, but this is still considered
+  // successful.  Return true either way, but backtrack the cursor on failure.
+  else if (token._quantifier == PEG::Token::Quantifier::zero_or_one)
+  {
+    // Check for a single match, succeed anyway.
+    matchTokenLookahead (token, pig, parseTree, indent + 1);
+    if (_debug > 1)
+      std::cout << "trace " << std::string (indent, ' ') << "\e[32mmatch ?\e[0m " << token.dump () << "\n";
+    if (_debug)
+      std::cout << "trace " << pig.dump () << ' ' << token.dump () << "\n";
+    return true;
+  }
+
+  // May match 1 or more times.  If it matches on the first attempt, continue
+  // to greedily match until it fails.  If it fails on the first attempt, then
+  // the rule fails.
+  else if (token._quantifier == PEG::Token::Quantifier::one_or_more)
+  {
+    if (! matchTokenLookahead (token, pig, parseTree, indent + 1))
+      return false;
+
+    while (matchTokenLookahead (token, pig, parseTree, indent + 1))
+    {
+      // "Forget it, he's rolling."
+    }
+
+    if (_debug > 1)
+      std::cout << "trace " << std::string (indent, ' ') << "\e[32mmatch +\e[0m " << token.dump () << "\n";
+    if (_debug)
+      std::cout << "trace " << pig.dump () << ' ' << token.dump () << "\n";
+    return true;
+  }
+
+  // May match zero or more times.  Keep calling while there are matches, and
+  // return true always.  Backtrack the cursor on failure.
+  else if (token._quantifier == PEG::Token::Quantifier::zero_or_more)
+  {
+    while (matchTokenLookahead (token, pig, parseTree, indent + 1))
+    {
+      // Let it go.
+    }
+
+    if (_debug > 1)
+      std::cout << "trace " << std::string (indent, ' ') << "\e[32mmatch *\e[0m " << token.dump () << "\n";
+    if (_debug)
+      std::cout << "trace " << pig.dump () << ' ' << token.dump () << "\n";
+    return true;
+  }
+
+  throw std::string ("This should never happen.");
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Wraps calls to matchToken, while properly handling lookahead.
+bool Packrat::matchTokenLookahead (
+  const PEG::Token& token,
+  Pig& pig,
+  std::shared_ptr <Tree> parseTree,
+  int indent)
+{
+  if (_debug > 1)
+    std::cout << "trace " << std::string (indent, ' ') << "matchTokenLookahead " << token.dump () << "\n";
+
+  if (token._lookahead == PEG::Token::Lookahead::none)
+  {
+    return matchToken (token, pig, parseTree, indent + 1);
+  }
+  else if (token._lookahead == PEG::Token::Lookahead::positive)
+  {
+    auto checkpoint = pig.cursor ();
+    auto b = std::make_shared <Tree> ();
+    if (matchToken (token, pig, b, indent + 1))
+    {
+      pig.restoreTo (checkpoint);
+      return true;
+    }
+  }
+  else if (token._lookahead == PEG::Token::Lookahead::negative)
+  {
+    auto checkpoint = pig.cursor ();
+    auto b = std::make_shared <Tree> ();
+    if (! matchToken (token, pig, b, indent + 1))
+    {
+      return true;
+    }
+
+    pig.restoreTo (checkpoint);
+  }
+
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool Packrat::matchToken (
+  const PEG::Token& token,
+  Pig& pig,
+  std::shared_ptr <Tree> parseTree,
+  int indent)
+{
+  if (_debug > 1)
+    std::cout << "trace " << std::string (indent, ' ') << "matchToken " << token.dump () << "\n";
+
+  auto checkpoint = pig.cursor ();
+  auto b = std::make_shared <Tree> ();
+
+  if (token.hasTag ("intrinsic") &&
+      matchIntrinsic (token, pig, parseTree, indent + 1))
+  {
+    return true;
+  }
+
+  else if (_syntax.find (token._token) != _syntax.end () &&
+           matchRule (token._token, pig, b, indent + 1))
+  {
+    // This is the only case that adds a sub-branch.
+    b->_name = token._token;
+    parseTree->addBranch (b);
+    return true;
+  }
+
+  else if (token.hasTag ("literal") &&
+           token.hasTag ("character") &&
+           matchCharLiteral (token, pig, parseTree, indent + 1))
+  {
+   return true;
+  }
+
+  else if (token.hasTag ("literal") &&
+           token.hasTag ("string") &&
+           matchStringLiteral (token, pig, parseTree, indent + 1))
+  {
+    return true;
+  }
+
+  pig.restoreTo (checkpoint);
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Supports the following:
+//   <digit>      --> unicodeLatinDigit
+//   <hex>        --> unicodeHexDigit
+//   <character>  --> anything
+//   <alpha>      --> unicodeAlpha
+//   <punct>      --> unicodePunctuation
+//   <ws>         --> unicodeWhitespace
+//   <sep>        --> unicodeHorizontalWhitespace
+//   <eol>        --> unicodeVerticalWhitespace
+//   <word>       --> <alpha>
+//   <token>      --> consecutive non <ws>
+//   <entity:e>   --> Any category 'e' token
+//   <external:x> --> Delegate to external function
+bool Packrat::matchIntrinsic (
+  const PEG::Token& token,
+  Pig& pig,
+  std::shared_ptr <Tree> parseTree,
+  int indent)
+{
+  if (_debug > 1)
+    std::cout << "trace " << std::string (indent, ' ') << "matchIntrinsic " << token.dump () << "\n";
+  auto checkpoint = pig.cursor ();
+
+  // There are only 10 digits.
+  if (token._token == "<digit>")
+  {
+    int digit;
+    if (pig.getDigit (digit))
+    {
+      // Create a populated branch.
+      auto b = std::make_shared <Tree> ();
+      b->_name = "intrinsic";
+      b->attribute ("expected", token._token);
+      b->attribute ("value", format ("{1}", digit));
+      parseTree->addBranch (b);
+
+      if (_debug > 1)
+        std::cout << "trace " << std::string (indent, ' ') << "\e[32mmatch\e[0m " << digit << "\n";
+      if (_debug)
+        std::cout << "trace " << pig.dump () << ' ' << token.dump () << "\n";
+      return true;
+    }
+  }
+
+  // Upper or lower case hex digit.
+  if (token._token == "<hex>")
+  {
+    int digit;
+    if (pig.getHexDigit (digit))
+    {
+      // Create a populated branch.
+      auto b = std::make_shared <Tree> ();
+      b->_name = "intrinsic";
+      b->attribute ("expected", token._token);
+      b->attribute ("value", format ("{1}", digit));
+      parseTree->addBranch (b);
+
+      if (_debug > 1)
+        std::cout << "trace " << std::string (indent, ' ') << "\e[32mmatch\e[0m " << digit << "\n";
+      if (_debug)
+        std::cout << "trace " << pig.dump () << ' ' << token.dump () << "\n";
+      return true;
+    }
+  }
+
+  // Character means anything.
+  else if (token._token == "<character>")
+  {
+    int character;
+    if (pig.getCharacter (character))
+    {
+      // Create a populated branch.
+      auto b = std::make_shared <Tree> ();
+      b->_name = "intrinsic";
+      b->attribute ("expected", token._token);
+      b->attribute ("value", format ("{1}", character));
+      parseTree->addBranch (b);
+
+      if (_debug > 1)
+        std::cout << "trace " << std::string (indent, ' ') << "\e[32mmatch\e[0m " << character << "\n";
+      if (_debug)
+        std::cout << "trace " << pig.dump () << ' ' << token.dump () << "\n";
+      return true;
+    }
+  }
+
+  // <punct> ::ispunct
+  else if (token._token == "<punct>")
+  {
+    int character = pig.peek ();
+    if (unicodePunctuation (character))
+    {
+      pig.skip (character);
+
+      // Create a populated branch.
+      auto b = std::make_shared <Tree> ();
+      b->_name = "intrinsic";
+      b->attribute ("expected", token._token);
+      b->attribute ("value", format ("{1}", character));
+      parseTree->addBranch (b);
+
+      if (_debug > 1)
+        std::cout << "trace " << std::string (indent, ' ') << "\e[32mmatch\e[0m " << character << "\n";
+      if (_debug)
+        std::cout << "trace " << pig.dump () << ' ' << token.dump () << "\n";
+      return true;
+    }
+  }
+
+  // <alpha>
+  else if (token._token == "<alpha>")
+  {
+    int character = pig.peek ();
+    if (unicodeAlpha (character))
+    {
+      pig.skip (character);
+
+      // Create a populated branch.
+      auto b = std::make_shared <Tree> ();
+      b->_name = "intrinsic";
+      b->attribute ("expected", token._token);
+      b->attribute ("value", format ("{1}", character));
+      parseTree->addBranch (b);
+
+      if (_debug > 1)
+        std::cout << "trace " << std::string (indent, ' ') << "\e[32mmatch\e[0m " << character << "\n";
+      if (_debug)
+        std::cout << "trace " << pig.dump () << ' ' << token.dump () << "\n";
+      return true;
+    }
+  }
+
+  // <ws>
+  else if (token._token == "<ws>")
+  {
+    int character = pig.peek ();
+    if (unicodeWhitespace (character))
+    {
+      pig.skip (character);
+
+      // Create a populated branch.
+      auto b = std::make_shared <Tree> ();
+      b->_name = "intrinsic";
+      b->attribute ("expected", token._token);
+      b->attribute ("value", format ("{1}", character));
+      parseTree->addBranch (b);
+
+      if (_debug > 10)
+        std::cout << "trace " << std::string (indent, ' ') << "\e[32mmatch\e[0m " << character << "\n";
+      if (_debug)
+        std::cout << "trace " << pig.dump () << ' ' << token.dump () << "\n";
+      return true;
+    }
+  }
+
+  // <sep>
+  else if (token._token == "<sep>")
+  {
+    int character = pig.peek ();
+    if (unicodeHorizontalWhitespace (character))
+    {
+      pig.skip (character);
+
+      // Create a populated branch.
+      auto b = std::make_shared <Tree> ();
+      b->_name = "intrinsic";
+      b->attribute ("expected", token._token);
+      b->attribute ("value", format ("{1}", character));
+      parseTree->addBranch (b);
+
+      if (_debug > 1)
+        std::cout << "trace " << std::string (indent, ' ') << "\e[32mmatch\e[0m " << character << "\n";
+      if (_debug)
+        std::cout << "trace " << pig.dump () << ' ' << token.dump () << "\n";
+      return true;
+    }
+  }
+
+  // <eol>
+  else if (token._token == "<eol>")
+  {
+    int character = pig.peek ();
+    if (unicodeVerticalWhitespace (character))
+    {
+      pig.skip (character);
+
+      // Create a populated branch.
+      auto b = std::make_shared <Tree> ();
+      b->_name = "intrinsic";
+      b->attribute ("expected", token._token);
+      b->attribute ("value", format ("{1}", character));
+      parseTree->addBranch (b);
+
+      if (_debug > 1)
+        std::cout << "trace " << std::string (indent, ' ') << "\e[32mmatch\e[0m " << character << "\n";
+      if (_debug)
+        std::cout << "trace " << pig.dump () << ' ' << token.dump () << "\n";
+      return true;
+    }
+  }
+
+  // <word> consecutive non-<ws>, non-<punct>.
+  else if (token._token == "<word>")
+  {
+    while (auto character = pig.peek ())
+    {
+      if (! character ||
+          unicodeWhitespace (character) ||
+          unicodePunctuation (character))
+        break;
+
+      pig.skip (character);
+    }
+
+    if (pig.cursor () > checkpoint)
+    {
+      auto word = pig.substr (checkpoint, pig.cursor () - checkpoint + 1);
+
+      // Create a populated branch.
+      auto b = std::make_shared <Tree> ();
+      b->_name = "intrinsic";
+      b->attribute ("expected", token._token);
+      b->attribute ("value", word);
+      parseTree->addBranch (b);
+
+      if (_debug > 1)
+        std::cout << "trace " << std::string (indent, ' ') << "\e[32mmatch\e[0m " << word << "\n";
+      if (_debug)
+        std::cout << "trace " << pig.dump () << ' ' << token.dump () << "\n";
+      return true;
+    }
+  }
+
+  // <token> consecutive non-<ws>.
+  else if (token._token == "<token>")
+  {
+    while (auto character = pig.peek ())
+    {
+      if (! character ||
+          unicodeWhitespace (character))
+        break;
+
+      pig.skip (character);
+    }
+
+    if (pig.cursor () > checkpoint)
+    {
+      auto word = pig.substr (checkpoint, pig.cursor () - checkpoint);
+
+      // Create a populated branch.
+      auto b = std::make_shared <Tree> ();
+      b->_name = "intrinsic";
+      b->attribute ("expected", token._token);
+      b->attribute ("value", word);
+      parseTree->addBranch (b);
+
+      if (_debug > 1)
+        std::cout << "trace " << std::string (indent, ' ') << "\e[32mmatch\e[0m " << word << "\n";
+      if (_debug)
+        std::cout << "trace " << pig.dump () << ' ' << token.dump () << "\n";
+      return true;
+    }
+  }
+
+  // <entity:category>.
+  else if (token._token.find ("<entity:") == 0)
+  {
+    // Extract entity category
+    auto category = token._token.substr (8, token._token.length () - 9);
+
+    // Match against any one of the entity values in this category.
+    auto values = _entities.equal_range (category);
+    for (auto value = values.first; value != values.second; ++value)
+    {
+      if (pig.skipLiteral (value->second))
+      {
+        // Create a populated branch.
+        auto b = std::make_shared <Tree> ();
+        b->_name = "intrinsic";
+        b->tag ("entity");
+        b->attribute ("expected", token._token);
+        b->attribute ("value", value->second);
+        parseTree->addBranch (b);
+
+        if (_debug > 1)
+          std::cout << "trace " << std::string (indent, ' ') << "\e[32mmatch\e[0m " << value->second << "\n";
+        if (_debug)
+          std::cout << "trace " << pig.dump () << ' ' << token.dump () << "\n";
+
+        return true;
+      }
+    }
+  }
+
+  // <external:rule>
+  else if (token._token.find ("<external:") == 0)
+  {
+    // Extract entity category
+    auto rule = token._token.substr (10, token._token.length () - 11);
+
+    // Any rule can be overridden by an external parser.
+    if (_externals.find (rule) != _externals.end ())
+    {
+      // Create a pre-populated branch, which is attached on success only.
+      auto newBranch = std::make_shared <Tree> ();
+      newBranch->_name = "intrinsic";
+      newBranch->tag ("external");
+      newBranch->attribute ("expected", token._token);
+
+      if (_externals[rule] (pig, newBranch))
+      {
+        // Determine what was parsed.
+        auto word = pig.substr (checkpoint, pig.cursor () - checkpoint);
+
+        // Attach the new branch.
+        newBranch->attribute ("value", word);
+        parseTree->addBranch (newBranch);
+
+        if (_debug > 1)
+          std::cout << "trace " << std::string (indent, ' ') << "\e[32mmatch\e[0m " << word << "\n";
+        if (_debug)
+          std::cout << "trace " << pig.dump () << ' ' << token.dump () << "\n";
+
+        return true;
+      }
+
+      // Note: Branch 'newBranch' goes out of scope here if parsing fails.
+    }
+  }
+
+  if (_debug > 1)
+     std::cout << "trace " << std::string (indent, ' ') << "\e[31mfail\e[0m " << token._token << "\n";
+  pig.restoreTo (checkpoint);
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool Packrat::matchCharLiteral (
+  const PEG::Token& token,
+  Pig& pig,
+  std::shared_ptr <Tree> parseTree,
+  int indent)
+{
+  if (_debug > 1)
+    std::cout << "trace " << std::string (indent, ' ') << "matchCharLiteral " << token.dump () << "\n";
+  auto checkpoint = pig.cursor ();
+
+  if (token._token.length () >= 3 &&
+      token._token[0] == '\'' &&
+      token._token[2] == '\'')
+  {
+    int literal = token._token[1];
+    if (pig.skip (literal))
+    {
+      // Create a populated branch.
+      auto b = std::make_shared <Tree> ();
+      b->_name = "charLiteral";
+      b->attribute ("expected", token._token);
+      b->attribute ("value", utf8_character (literal));
+      parseTree->addBranch (b);
+
+      if (_debug > 1)
+        std::cout << "trace " << std::string (indent, ' ') << "\e[32mmatch\e[0m " << token._token << "\n";
+      if (_debug)
+        std::cout << "trace " << pig.dump () << ' ' << token.dump () << "\n";
+      return true;
+    }
+  }
+
+  if (_debug > 1)
+    std::cout << "trace " << std::string (indent, ' ') << "\e[31mfail\e[0m " << token._token << "\n";
+  pig.restoreTo (checkpoint);
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool Packrat::matchStringLiteral (
+  const PEG::Token& token,
+  Pig& pig,
+  std::shared_ptr <Tree> parseTree,
+  int indent)
+{
+  if (_debug > 1)
+    std::cout << "trace " << std::string (indent, ' ') << "matchStringLiteral " << token.dump () << "\n";
+  auto checkpoint = pig.cursor ();
+
+  std::string literal = token._token.substr (1, token._token.length () - 2);
+  if (pig.skipLiteral (literal))
+  {
+    // Create a populated branch.
+    auto b = std::make_shared <Tree> ();
+    b->_name = "stringLiteral";
+    b->attribute ("expected", token._token);
+    b->attribute ("value", literal);
+    parseTree->addBranch (b);
+
+    if (_debug > 1)
+      std::cout << "trace " << std::string (indent, ' ') << "\e[32mmatch\e[0m " << literal << "\n";
+    if (_debug)
+      std::cout << "trace " << pig.dump () << ' ' << token.dump () << "\n";
+    return true;
+  }
+
+  if (_debug > 1)
+    std::cout << "trace " << std::string (indent, ' ') << "\e[31mfail\e[0m " << token._token << "\n";
+  pig.restoreTo (checkpoint);
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Search for 'value' in _entities category, return canonicalized value.
+bool Packrat::canonicalize (
+  std::string& canonicalized,
+  const std::string& category,
+  const std::string& value) const
+{
+  // Extract a list of entities for category.
+  std::vector <std::string> options;
+  auto c = _entities.equal_range (category);
+  for (auto e = c.first; e != c.second; ++e)
+  {
+    // Shortcut: if an exact match is found, success.
+    if (value == e->second)
+    {
+      canonicalized = value;
+      return true;
+    }
+
+    options.push_back (e->second);
+  }
+
+  // Match against the options, throw away results.
+  std::vector <std::string> matches;
+  if (autoComplete (value, options, matches, minimumMatchLength) == 1)
+  {
+    canonicalized = matches[0];
+    return true;
+  }
+
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+std::string Packrat::dump () const
+{
+  std::stringstream out;
+  if (_debug)
+    out << '\n';
+
+  out << "Packrat Parse "
+      << _tree->dump ();
+
+  if (_entities.size ())
+  {
+    out << "  Entities\n";
+    for (const auto& entity : _entities)
+      out << "    " << entity.first << ':' << entity.second << '\n';
+  }
+
+  if (_externals.size ())
+  {
+    out << "  Externals\n";
+    for (const auto& external : _externals)
+      out << "    " << external.first << "\n";
+  }
+
+  return out.str ();
+}
+
+////////////////////////////////////////////////////////////////////////////////
diff --git a/src/libshared/src/Packrat.h b/src/libshared/src/Packrat.h
new file mode 100644 (file)
index 0000000..0585077
--- /dev/null
@@ -0,0 +1,70 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Copyright 2015 - 2017, Paul Beckingham, Federico Hernandez.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+// http://www.opensource.org/licenses/mit-license.php
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#ifndef INCLUDED_PACKRAT
+#define INCLUDED_PACKRAT
+
+#include <PEG.h>
+#include <Tree.h>
+#include <Pig.h>
+#include <string>
+
+class Packrat
+{
+public:
+  void parse (const PEG&, const std::string&);
+  void entity (const std::string&, const std::string&);
+  void external (const std::string&, bool (*)(Pig&, std::shared_ptr <Tree>));
+
+  void debug ();
+  std::string dump () const;
+
+private:
+  bool matchRule           (const std::string&,     Pig&, std::shared_ptr <Tree>, int);
+  bool matchProduction     (const PEG::Production&, Pig&, std::shared_ptr <Tree>, int);
+  bool matchTokenQuant     (const PEG::Token&,      Pig&, std::shared_ptr <Tree>, int);
+  bool matchTokenLookahead (const PEG::Token&,      Pig&, std::shared_ptr <Tree>, int);
+  bool matchToken          (const PEG::Token&,      Pig&, std::shared_ptr <Tree>, int);
+  bool matchIntrinsic      (const PEG::Token&,      Pig&, std::shared_ptr <Tree>, int);
+  bool matchCharLiteral    (const PEG::Token&,      Pig&, std::shared_ptr <Tree>, int);
+  bool matchStringLiteral  (const PEG::Token&,      Pig&, std::shared_ptr <Tree>, int);
+
+  bool canonicalize (std::string&, const std::string&, const std::string&) const;
+
+public:
+  static int minimumMatchLength;
+
+private:
+  int                                                            _debug    {0};
+  std::map <std::string, PEG::Rule>                              _syntax   {};
+  std::shared_ptr <Tree>                                         _tree     {std::make_shared <Tree> ()};
+  std::multimap <std::string, std::string>                       _entities {};
+  std::map <std::string, bool (*)(Pig&, std::shared_ptr <Tree>)> _externals {};
+};
+
+#endif
+
+////////////////////////////////////////////////////////////////////////////////
diff --git a/src/libshared/src/Palette.cpp b/src/libshared/src/Palette.cpp
new file mode 100644 (file)
index 0000000..62fb08f
--- /dev/null
@@ -0,0 +1,82 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Copyright 2015 - 2017, Paul Beckingham, Federico Hernandez.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+// http://www.opensource.org/licenses/mit-license.php
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include <cmake.h>
+#include <Palette.h>
+
+////////////////////////////////////////////////////////////////////////////////
+// Use a default palette, which is overwritten in ::initialize.
+Palette::Palette ()
+{
+  _colors = {
+    Color ("white on red"),
+    Color ("white on blue"),
+    Color ("black on green"),
+    Color ("black on magenta"),
+    Color ("black on cyan"),
+    Color ("black on yellow"),
+    Color ("black on white"),
+    Color ("white on bright red"),
+    Color ("white on bright blue"),
+    Color ("black on bright green"),
+    Color ("black on bright magenta"),
+    Color ("black on bright cyan"),
+    Color ("black on bright yellow"),
+  };
+
+  _current = 0;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+void Palette::add (Color c)
+{
+  _colors.push_back (c);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Return the next color in the list.  Cycle to the beginning if necessary.
+Color Palette::next ()
+{
+  if (enabled)
+    return _colors[_current++ % _colors.size ()];
+
+  return Color ();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+int Palette::size () const
+{
+  return static_cast <int> (_colors.size ());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+void Palette::clear ()
+{
+  _colors.clear ();
+  _current = 0;
+}
+
+////////////////////////////////////////////////////////////////////////////////
diff --git a/src/libshared/src/Palette.h b/src/libshared/src/Palette.h
new file mode 100644 (file)
index 0000000..771b285
--- /dev/null
@@ -0,0 +1,51 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Copyright 2015 - 2017, Paul Beckingham, Federico Hernandez.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+// http://www.opensource.org/licenses/mit-license.php
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#ifndef INCLUDED_PALETTE
+#define INCLUDED_PALETTE
+
+#include <Color.h>
+#include <vector>
+#include <string>
+
+class Palette
+{
+public:
+  Palette ();
+  void add (Color);
+  Color next ();
+  int size () const;
+  void clear ();
+
+public:
+  bool enabled {true};
+
+private:
+  std::vector <Color> _colors  {};
+  int                 _current {0};
+};
+
+#endif
diff --git a/src/libshared/src/Pig.cpp b/src/libshared/src/Pig.cpp
new file mode 100644 (file)
index 0000000..fd8d23c
--- /dev/null
@@ -0,0 +1,668 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Copyright 2015 - 2017, Paul Beckingham, Federico Hernandez.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+// http://www.opensource.org/licenses/mit-license.php
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include <cmake.h>
+#include <Pig.h>
+#include <shared.h>
+#include <unicode.h>
+#include <utf8.h>
+#include <algorithm>
+#include <sstream>
+#include <cinttypes>
+#include <cstdlib>
+
+////////////////////////////////////////////////////////////////////////////////
+Pig::Pig (const std::string& text)
+: _text {std::make_shared <std::string> (text)}
+{
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool Pig::skip (int c)
+{
+  if ((*_text)[_cursor] == c)
+  {
+    ++_cursor;
+    return true;
+  }
+
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool Pig::skipN (const int quantity)
+{
+  auto save = _cursor;
+
+  auto count = 0;
+  while (count++ < quantity)
+  {
+    if (! utf8_next_char (*_text, _cursor))
+    {
+      _cursor = save;
+      return false;
+    }
+  }
+
+  return true;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool Pig::skipWS ()
+{
+  auto save = _cursor;
+
+  int c;
+  auto prev = _cursor;
+  while ((c = utf8_next_char (*_text, _cursor)))
+  {
+    if (! unicodeWhitespace (c))
+    {
+      _cursor = prev;
+      break;
+    }
+    prev = _cursor;
+  }
+
+  return _cursor > save;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool Pig::skipLiteral (const std::string& literal)
+{
+  if (_text->find (literal, _cursor) == _cursor)
+  {
+    _cursor += literal.length ();
+    return true;
+  }
+
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool Pig::skipPartial (const std::string& reference, std::string& result)
+{
+  // Walk the common substring.
+  auto pos = 0;
+  while (reference[pos] &&
+         (*_text)[_cursor + pos] &&
+         reference[pos] == (*_text)[_cursor + pos])
+    ++pos;
+
+  if (pos > 0)
+  {
+    result = _text->substr (_cursor, pos);
+    _cursor += pos;
+    return true;
+  }
+
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool Pig::getUntil (int end, std::string& result)
+{
+  auto save = _cursor;
+
+  int c;
+  auto prev = _cursor;
+  while ((c = utf8_next_char (*_text, _cursor)))
+  {
+    if (c == end)
+    {
+      _cursor = prev;
+      result = _text->substr (save, _cursor - save);
+      return true;
+    }
+
+    else if (eos ())
+    {
+      result = _text->substr (save, _cursor - save);
+      return true;
+    }
+
+    prev = _cursor;
+  }
+
+  return _cursor > save;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool Pig::getUntilWS (std::string& result)
+{
+  auto save = _cursor;
+
+  int c;
+  auto prev = _cursor;
+  while ((c = utf8_next_char (*_text, _cursor)))
+  {
+    if (unicodeWhitespace (c))
+    {
+      _cursor = prev;
+      result = _text->substr (save, _cursor - save);
+      return true;
+    }
+
+    // Note: This test must follow the above unicodeWhitespace(c) test because
+    //       it is testing the value of 'c', and eos() is testing _cursor,
+    //       which has already been advanced.
+    else if (eos ())
+    {
+      result = _text->substr (save, _cursor - save);
+      return true;
+    }
+
+    prev = _cursor;
+  }
+
+  return _cursor > save;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool Pig::getCharacter (int& result)
+{
+  int c = (*_text)[_cursor];
+  if (c)
+  {
+    result = c;
+    ++_cursor;
+    return true;
+  }
+
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool Pig::getDigit (int& result)
+{
+  int c = (*_text)[_cursor];
+  if (c &&
+      unicodeLatinDigit (c))
+  {
+    result = c - '0';
+    ++_cursor;
+    return true;
+  }
+
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool Pig::getDigit2 (int& result)
+{
+  if (unicodeLatinDigit ((*_text)[_cursor + 0]))
+  {
+    if (unicodeLatinDigit ((*_text)[_cursor + 1]))
+    {
+      result = strtoimax (_text->substr (_cursor, 2).c_str (), NULL, 10);
+      _cursor += 2;
+      return true;
+    }
+  }
+
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool Pig::getDigit3 (int& result)
+{
+  if (unicodeLatinDigit ((*_text)[_cursor + 0]))
+  {
+    if (unicodeLatinDigit ((*_text)[_cursor + 1]))
+    {
+      if (unicodeLatinDigit ((*_text)[_cursor + 2]))
+      {
+        result = strtoimax (_text->substr (_cursor, 3).c_str (), NULL, 10);
+        _cursor += 3;
+        return true;
+      }
+    }
+  }
+
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool Pig::getDigit4 (int& result)
+{
+  if (unicodeLatinDigit ((*_text)[_cursor + 0]))
+  {
+    if (unicodeLatinDigit ((*_text)[_cursor + 1]))
+    {
+      if (unicodeLatinDigit ((*_text)[_cursor + 2]))
+      {
+        if (unicodeLatinDigit ((*_text)[_cursor + 3]))
+        {
+          result = strtoimax (_text->substr (_cursor, 4).c_str (), NULL, 10);
+          _cursor += 4;
+          return true;
+        }
+      }
+    }
+  }
+
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool Pig::getDigits (int& result)
+{
+  auto save = _cursor;
+
+  int c;
+  auto prev = _cursor;
+  while ((c = utf8_next_char (*_text, _cursor)))
+  {
+    if (! unicodeLatinDigit (c))
+    {
+      _cursor = prev;
+      break;
+    }
+
+    prev = _cursor;
+  }
+
+  if (_cursor > save)
+  {
+    result = strtoimax (_text->substr (save, _cursor - save).c_str (), NULL, 10);
+    return true;
+  }
+
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool Pig::getHexDigit (int& result)
+{
+  int c = (*_text)[_cursor];
+  if (c &&
+      unicodeHexDigit (c))
+  {
+    if (c >= '0' && c <= '9')
+    {
+      result = c - '0';
+      ++_cursor;
+      return true;
+    }
+    else if (c >= 'A' && c <= 'F')
+    {
+      result = c - 'A' + 10;
+      ++_cursor;
+      return true;
+    }
+    else if (c >= 'a' && c <= 'f')
+    {
+      result = c - 'a' + 10;
+      ++_cursor;
+      return true;
+    }
+  }
+
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// number:
+//   int frac? exp?
+//
+// int:
+//   (-|+)? digit+
+//
+// frac:
+//   . digit+
+//
+// exp:
+//   e digit+
+//
+// e:
+//   e|E (+|-)?
+//
+bool Pig::getNumber (std::string& result)
+{
+  auto i = _cursor;
+
+  // [+-]?
+  if ((*_text)[i] &&
+      ((*_text)[i] == '-' ||
+       (*_text)[i] == '+'))
+    ++i;
+
+  // digit+
+  if ((*_text)[i] &&
+      unicodeLatinDigit ((*_text)[i]))
+  {
+    ++i;
+
+    while ((*_text)[i] && unicodeLatinDigit ((*_text)[i]))
+      ++i;
+
+    // ( . digit+ )?
+    if ((*_text)[i] && (*_text)[i] == '.')
+    {
+      ++i;
+
+      while ((*_text)[i] && unicodeLatinDigit ((*_text)[i]))
+        ++i;
+    }
+
+    // ( [eE] [+-]? digit+ )?
+    if ((*_text)[i] &&
+        ((*_text)[i] == 'e' ||
+         (*_text)[i] == 'E'))
+    {
+      ++i;
+
+      if ((*_text)[i] &&
+          ((*_text)[i] == '+' ||
+           (*_text)[i] == '-'))
+        ++i;
+
+      if ((*_text)[i] && unicodeLatinDigit ((*_text)[i]))
+      {
+        ++i;
+
+        while ((*_text)[i] && unicodeLatinDigit ((*_text)[i]))
+          ++i;
+
+        result = _text->substr (_cursor, i - _cursor);
+        _cursor = i;
+        return true;
+      }
+
+      return false;
+    }
+
+    result = _text->substr (_cursor, i - _cursor);
+    _cursor = i;
+    return true;
+  }
+
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool Pig::getNumber (double& result)
+{
+  std::string s;
+  if (getNumber (s))
+  {
+    result = std::strtod (s.c_str (), NULL);
+    return true;
+  }
+
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// [ + | - ] \d+ [ . [ \d+ ]]
+bool Pig::getDecimal (std::string& result)
+{
+  auto i = _cursor;
+
+  // [+-]?
+  if ((*_text)[i] &&
+      ((*_text)[i] == '-' ||
+       (*_text)[i] == '+'))
+    ++i;
+
+  // digit+
+  if ((*_text)[i] && unicodeLatinDigit ((*_text)[i]))
+  {
+    ++i;
+
+    while ((*_text)[i] && unicodeLatinDigit ((*_text)[i]))
+      ++i;
+
+    // ( . digit+ )?
+    if ((*_text)[i] && (*_text)[i] == '.')
+    {
+      ++i;
+
+      while ((*_text)[i] && unicodeLatinDigit ((*_text)[i]))
+        ++i;
+    }
+
+    result = _text->substr (_cursor, i - _cursor);
+    _cursor = i;
+    return true;
+  }
+
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool Pig::getDecimal (double& result)
+{
+  std::string s;
+  if (getDecimal (s))
+  {
+    result = std::strtod (s.c_str (), NULL);
+    return true;
+  }
+
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Gets quote content:      "foobar" -> foobar      (for c = '"')
+// Handles escaped quotes:  "foo\"bar" -> foo\"bar  (for c = '"')
+// Returns false if first character is not c, or if there is no closing c.
+// Does not modify content between quotes.
+bool Pig::getQuoted (int quote, std::string& result)
+{
+  if (! (*_text)[_cursor] ||
+      (*_text)[_cursor] != quote)
+    return false;
+
+  auto start = _cursor + utf8_sequence (quote);
+  auto i = start;
+
+  while ((*_text)[i])
+  {
+    i = _text->find (quote, i);
+    if (i == std::string::npos)
+      return false;  // Unclosed quote.  Short cut, not definitive.
+
+    if (i == start)
+    {
+      // Empty quote
+      _cursor += 2 * utf8_sequence (quote);  // Skip both quote chars
+      result = "";
+      return true;
+    }
+
+    if ((*_text)[i - 1] == '\\')
+    {
+      // Check for escaped backslashes.  Backtracking like this is not very
+      // efficient, but is only done in extreme corner cases.
+
+      auto j = i - 2;  // Start one character further left
+      bool is_escaped_quote = true;
+      while (j >= start && (*_text)[j] == '\\')
+      {
+        // Toggle flag for each further backslash encountered.
+        is_escaped_quote = is_escaped_quote ? false : true;
+        --j;
+      }
+
+      if (is_escaped_quote)
+      {
+        // Keep searching
+        ++i;
+        continue;
+      }
+    }
+
+    // None of the above applied, we must have found the closing quote char.
+    result.assign (*_text, start, i - start);
+    _cursor = i + utf8_sequence (quote);  // Skip closing quote char
+    return true;
+  }
+
+  // This should never be reached.  We could throw here instead.
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Assumes that the options are sorted by decreasing length, so that if the
+// options contain 'fourteen' and 'four', the stream is first matched against
+// the longer entry.
+bool Pig::getOneOf (
+  const std::vector <std::string>& options,
+  std::string& found)
+{
+  for (const auto& option : options)
+  {
+    if (skipLiteral (option))
+    {
+      found = option;
+      return true;
+    }
+  }
+
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool Pig::getHMS (int& hours, int& minutes, int& seconds)
+{
+  auto save = _cursor;
+
+  if ((getDigit2 (hours) || getDigit (hours)) &&
+      skip (':')                              &&
+      getDigit2 (minutes))
+  {
+    seconds = 0;
+    if (skip (':') &&
+        ! getDigit2 (seconds))
+      return false;
+
+    return true;
+  }
+
+  _cursor = save;
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool Pig::getRemainder (std::string& result)
+{
+  if ((*_text)[_cursor])
+  {
+    result = _text->substr (_cursor);
+    _cursor += result.length ();
+    return true;
+  }
+
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool Pig::eos () const
+{
+  return (*_text)[_cursor] == '\0';
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Peeks ahead - does not move cursor.
+int Pig::peek () const
+{
+  return (*_text)[_cursor];
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Peeks ahead - does not move cursor.
+std::string Pig::peek (const int quantity) const
+{
+  std::string::size_type adjusted = std::min (static_cast <std::string::size_type> (quantity), _text->length () - _cursor);
+  if ((*_text)[_cursor])
+    return _text->substr (_cursor, adjusted);
+
+  return "";
+}
+
+////////////////////////////////////////////////////////////////////////////////
+std::string::size_type Pig::cursor () const
+{
+  return _cursor;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Note: never called internally, otherwise the client cannot rely on iṫ.
+std::string::size_type Pig::save ()
+{
+  return _saved = _cursor;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Note: never called internally, otherwise the client cannot rely on iṫ.
+std::string::size_type Pig::restore ()
+{
+  return _cursor = _saved;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+std::string::size_type Pig::restoreTo (std::string::size_type previous)
+{
+  return _cursor = previous;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+std::string Pig::substr (
+  std::string::size_type start,
+  std::string::size_type end) const
+{
+  return _text->substr (start, end - start);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+std::string Pig::str () const
+{
+  return _text->substr (_cursor);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Show the text, with the matched part in white on green, and the unmatched
+// part white on red, followed by the index equivalent.
+std::string Pig::dump () const
+{
+  std::stringstream out;
+  if (_cursor)
+    out << "\e[37;42m"
+        << _text->substr (0, _cursor)
+        << "\e[0m";
+
+  out << "\e[37;41m"
+      << _text->substr (_cursor)
+      << "\e[0m "
+      << _cursor
+      << '/'
+      << _text->length ();
+
+  return str_replace (out.str (), "\n", "\\n");
+}
+
+////////////////////////////////////////////////////////////////////////////////
diff --git a/src/libshared/src/Pig.h b/src/libshared/src/Pig.h
new file mode 100644 (file)
index 0000000..3485e43
--- /dev/null
@@ -0,0 +1,81 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Copyright 2015 - 2017, Paul Beckingham, Federico Hernandez.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+// http://www.opensource.org/licenses/mit-license.php
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#ifndef INCLUDED_PIG
+#define INCLUDED_PIG
+
+#include <string>
+#include <vector>
+#include <memory>
+
+class Pig
+{
+public:
+  explicit Pig (const std::string&);
+
+  bool skip (int);
+  bool skipN (const int quantity = 1);
+  bool skipWS ();
+  bool skipLiteral (const std::string&);
+  bool skipPartial (const std::string&, std::string&);
+
+  bool getUntil (int, std::string&);
+  bool getUntilWS (std::string&);
+  bool getCharacter (int&);
+  bool getDigit (int&);
+  bool getDigit2 (int&);
+  bool getDigit3 (int&);
+  bool getDigit4 (int&);
+  bool getDigits (int&);
+  bool getHexDigit (int&);
+  bool getNumber (std::string&);
+  bool getNumber (double&);
+  bool getDecimal (std::string&);
+  bool getDecimal (double&);
+  bool getQuoted (int, std::string&);
+  bool getOneOf (const std::vector <std::string>&, std::string&);
+  bool getHMS (int&, int&, int&);
+  bool getRemainder (std::string&);
+
+  bool eos () const;
+  int peek () const;
+  std::string peek (const int) const;
+  std::string::size_type cursor () const;
+  std::string::size_type save ();
+  std::string::size_type restore ();
+  std::string::size_type restoreTo (std::string::size_type);
+
+  std::string substr (std::string::size_type, std::string::size_type) const;
+  std::string str () const;
+  std::string dump () const;
+
+private:
+  std::shared_ptr<std::string> _text;
+  std::string::size_type       _cursor {0};
+  std::string::size_type       _saved  {0};
+};
+
+#endif
diff --git a/src/libshared/src/README b/src/libshared/src/README
new file mode 100644 (file)
index 0000000..dc654da
--- /dev/null
@@ -0,0 +1,16 @@
+Shared
+======
+
+The 'shared' library is a set of reusable objects to be shared as-is between
+various programs, including Taskwarrior, Taskserver, Tasksh and Timewarrior.
+
+In order to be reusable, and therefore a member of libshared, conditions must
+be met:
+
+  - Shared objects may only make use of other shared objects, or external
+    dependencies (libuuid, gettext, readline etc), but no objects from the
+    project directories.
+
+  - Shared objects may assume only one external 'cmake.h' that defines
+    platform/portability constants.
+
diff --git a/src/libshared/src/RX.cpp b/src/libshared/src/RX.cpp
new file mode 100644 (file)
index 0000000..59a683f
--- /dev/null
@@ -0,0 +1,159 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Copyright 2006 - 2017, Paul Beckingham, Federico Hernandez.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+// http://www.opensource.org/licenses/mit-license.php
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include <cmake.h>
+#include <RX.h>
+#include <cstdlib>
+#include <cstring>
+
+////////////////////////////////////////////////////////////////////////////////
+RX::RX ()
+{
+}
+
+////////////////////////////////////////////////////////////////////////////////
+RX::RX (
+  const std::string& pattern,
+  bool case_sensitive /* = true */)
+: _compiled (false)
+, _pattern (pattern)
+, _case_sensitive (case_sensitive)
+{
+  compile ();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+RX::RX (const RX& other)
+{
+  _compiled       = false;
+  _pattern        = other._pattern;
+  _case_sensitive = other._case_sensitive;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+RX::~RX ()
+{
+  if (_compiled)
+    regfree (&_regex);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+RX& RX::operator= (const RX& other)
+{
+  _compiled       = false;
+  _pattern        = other._pattern;
+  _case_sensitive = other._case_sensitive;
+
+  return *this;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+void RX::compile ()
+{
+  if (! _compiled)
+  {
+    memset (&_regex, 0, sizeof (regex_t));
+
+    int result;
+    if ((result = regcomp (&_regex, _pattern.c_str (),
+#if defined REG_ENHANCED
+                           REG_ENHANCED | REG_EXTENDED | REG_NEWLINE |
+#else
+                           REG_EXTENDED | REG_NEWLINE |
+#endif
+                           (_case_sensitive ? 0 : REG_ICASE))) != 0)
+    {
+      char message[256];
+      regerror (result, &_regex, message, 256);
+      throw std::string (message);
+    }
+
+    _compiled = true;
+  }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool RX::match (const std::string& in)
+{
+  if (! _compiled)
+    compile ();
+
+  return regexec (&_regex, in.c_str (), 0, nullptr, 0) == 0 ? true : false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool RX::match (
+  std::vector<std::string>& matches,
+  const std::string& in)
+{
+  if (! _compiled)
+    compile ();
+
+  regmatch_t rm[2];
+  int offset = 0;
+  int length = in.length ();
+  while (regexec (&_regex, in.c_str () + offset, 2, &rm[0], 0) == 0 &&
+         offset < length)
+  {
+    matches.push_back (in.substr (rm[0].rm_so + offset, rm[0].rm_eo - rm[0].rm_so));
+    offset += rm[0].rm_eo;
+
+    // Protection against zero-width patterns causing infinite loops.
+    if (rm[0].rm_so == rm[0].rm_eo)
+      ++offset;
+  }
+
+  return matches.size () ? true : false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool RX::match (
+  std::vector <int>& start,
+  std::vector <int>& end,
+  const std::string& in)
+{
+  if (! _compiled)
+    compile ();
+
+  regmatch_t rm[2];
+  int offset = 0;
+  int length = in.length ();
+  while (regexec (&_regex, in.c_str () + offset, 2, &rm[0], 0) == 0 &&
+         offset < length)
+  {
+    start.push_back (rm[0].rm_so + offset);
+    end.push_back   (rm[0].rm_eo + offset);
+    offset += rm[0].rm_eo;
+
+    // Protection against zero-width patterns causing infinite loops.
+    if (rm[0].rm_so == rm[0].rm_eo)
+      ++offset;
+  }
+
+  return start.size () ? true : false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
diff --git a/src/libshared/src/RX.h b/src/libshared/src/RX.h
new file mode 100644 (file)
index 0000000..1626582
--- /dev/null
@@ -0,0 +1,58 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Copyright 2006 - 2017, Paul Beckingham, Federico Hernandez.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+// http://www.opensource.org/licenses/mit-license.php
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#ifndef INCLUDED_RX
+#define INCLUDED_RX
+
+#include <string>
+#include <vector>
+#include <regex.h>
+
+class RX
+{
+public:
+  RX ();
+  RX (const std::string&, bool caseSensitive = true);
+  RX (const RX&);
+  ~RX ();
+  RX& operator= (const RX&);
+
+  bool match (const std::string&);
+  bool match (std::vector<std::string>&, const std::string&);
+  bool match (std::vector <int>&, std::vector <int>&, const std::string&);
+
+private:
+  void compile ();
+
+private:
+  bool _compiled       {false};
+  std::string _pattern {};
+  bool _case_sensitive {false};
+  regex_t _regex;
+};
+
+#endif
+
diff --git a/src/libshared/src/SAX.cpp b/src/libshared/src/SAX.cpp
new file mode 100644 (file)
index 0000000..5503593
--- /dev/null
@@ -0,0 +1,579 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Copyright 2006 - 2017, Paul Beckingham, Federico Hernandez.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+// http://www.opensource.org/licenses/mit-license.php
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include <cmake.h>
+#include <JSON.h>
+#include <utf8.h>
+#include <sstream>
+#include <stdlib.h>
+#include <inttypes.h>
+#include <errno.h>
+
+////////////////////////////////////////////////////////////////////////////////
+bool json::SAX::parse (const std::string& input, SAX::Sink& sink)
+{
+  sink.eventDocStart ();
+  std::string::size_type cursor = 0;
+  ignoreWhitespace (input, cursor);
+  if (isObject (input, cursor, sink) ||
+      isArray  (input, cursor, sink))
+  {
+    ignoreWhitespace (input, cursor);
+    if (cursor < input.length ())
+      error ("Error: extra characters found at position ", cursor);
+
+    sink.eventDocEnd ();
+    return true;
+  }
+
+  error ("Error: Missing '{' or '[' at position ", cursor);
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Complete Unicode whitespace list.
+//
+// http://en.wikipedia.org/wiki/Whitespace_character
+// Updated 2015-09-13
+void json::SAX::ignoreWhitespace (const std::string& input, std::string::size_type& cursor)
+{
+  int c = input[cursor];
+  while (c == 0x0020 ||   // space Common  Separator, space
+         c == 0x0009 ||   // Common  Other, control  HT, Horizontal Tab
+         c == 0x000A ||   // Common  Other, control  LF, Line feed
+         c == 0x000B ||   // Common  Other, control  VT, Vertical Tab
+         c == 0x000C ||   // Common  Other, control  FF, Form feed
+         c == 0x000D ||   // Common  Other, control  CR, Carriage return
+         c == 0x0085 ||   // Common  Other, control  NEL, Next line
+         c == 0x00A0 ||   // no-break space  Common  Separator, space
+         c == 0x1680 ||   // ogham space mark  Ogham Separator, space
+         c == 0x180E ||   // mongolian vowel separator Mongolian Separator, space
+         c == 0x2000 ||   // en quad Common  Separator, space
+         c == 0x2001 ||   // em quad Common  Separator, space
+         c == 0x2002 ||   // en space  Common  Separator, space
+         c == 0x2003 ||   // em space  Common  Separator, space
+         c == 0x2004 ||   // three-per-em space  Common  Separator, space
+         c == 0x2005 ||   // four-per-em space Common  Separator, space
+         c == 0x2006 ||   // six-per-em space  Common  Separator, space
+         c == 0x2007 ||   // figure space  Common  Separator, space
+         c == 0x2008 ||   // punctuation space Common  Separator, space
+         c == 0x2009 ||   // thin space  Common  Separator, space
+         c == 0x200A ||   // hair space  Common  Separator, space
+         c == 0x200B ||   // zero width space
+         c == 0x200C ||   // zero width non-joiner
+         c == 0x200D ||   // zero width joiner
+         c == 0x2028 ||   // line separator  Common  Separator, line
+         c == 0x2029 ||   // paragraph separator Common  Separator, paragraph
+         c == 0x202F ||   // narrow no-break space Common  Separator, space
+         c == 0x205F ||   // medium mathematical space Common  Separator, space
+         c == 0x2060 ||   // word joiner
+         c == 0x3000)     // ideographic space Common  Separator, space
+  {
+    c = input[++cursor];
+  }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// object := '{' [<pair> [, <pair> ...]] '}'
+bool json::SAX::isObject (const std::string& input, std::string::size_type& cursor, SAX::Sink& sink)
+{
+  ignoreWhitespace (input, cursor);
+  auto backup = cursor;
+
+  if (isLiteral (input, '{', cursor))
+  {
+    sink.eventObjectStart ();
+    int counter = 0;
+
+    if (isPair (input, cursor, sink))
+    {
+      ++counter;
+      while (isLiteral (input, ',', cursor) &&
+             isPair    (input, cursor, sink))
+      {
+        ++counter;
+      }
+    }
+
+    ignoreWhitespace (input, cursor);
+    if (isLiteral (input, '}', cursor))
+    {
+      sink.eventObjectEnd (counter);
+      return true;
+    }
+    else
+      error ("Error: Missing '}' at position ", cursor);
+  }
+
+  cursor = backup;
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// array := '[' [<value> [, <value> ...]] ']'
+bool json::SAX::isArray (const std::string& input, std::string::size_type& cursor, SAX::Sink& sink)
+{
+  ignoreWhitespace (input, cursor);
+  auto backup = cursor;
+
+  if (isLiteral (input, '[', cursor))
+  {
+    sink.eventArrayStart ();
+    int counter = 0;
+
+    if (isValue (input, cursor, sink))
+    {
+      ++counter;
+      while (isLiteral (input, ',', cursor) &&
+             isValue   (input,      cursor, sink))
+      {
+        ++counter;
+      }
+    }
+
+    ignoreWhitespace (input, cursor);
+    if (isLiteral (input, ']', cursor))
+    {
+      sink.eventArrayEnd (counter);
+      return true;
+    }
+    else
+      error ("Error: Missing ']' at position ", cursor);
+  }
+
+  cursor = backup;
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// pair := <string> ':' <value>
+bool json::SAX::isPair (const std::string& input, std::string::size_type& cursor, SAX::Sink& sink)
+{
+  ignoreWhitespace (input, cursor);
+  auto backup = cursor;
+
+  if (isKey (input, cursor, sink))
+  {
+    if (isLiteral (input, ':', cursor))
+    {
+      if (isValue (input, cursor, sink))
+        return true;
+
+      error ("Error: Missing value at position ", cursor);
+    }
+    else
+      error ("Error: Missing ':' at position ", cursor);
+  }
+
+  cursor = backup;
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// value  := <string>
+//         | <number>
+//         | <object>
+//         | <array>
+//         | 'true'
+//         | 'false'
+//         | 'null'
+bool json::SAX::isValue (const std::string& input, std::string::size_type& cursor, SAX::Sink& sink)
+{
+  ignoreWhitespace (input, cursor);
+
+  return isString (input, cursor, sink) ||
+         isNumber (input, cursor, sink) ||
+         isObject (input, cursor, sink) ||
+         isArray  (input, cursor, sink) ||
+         isBool   (input, cursor, sink) ||
+         isNull   (input, cursor, sink);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool json::SAX::isKey (const std::string& input, std::string::size_type& cursor, SAX::Sink& sink)
+{
+  ignoreWhitespace (input, cursor);
+
+  std::string value;
+  if (isStringValue (input, cursor, value))
+  {
+    sink.eventName (value);
+    return true;
+  }
+
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool json::SAX::isString (const std::string& input, std::string::size_type& cursor, SAX::Sink& sink)
+{
+  ignoreWhitespace (input, cursor);
+
+  std::string value;
+  if (isStringValue (input, cursor, value))
+  {
+    sink.eventValueString (value);
+    return true;
+  }
+
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// string := '"' [<chars> ...] '"'
+// chars  := <unicode>
+//         | '\"'
+//         | '\\'
+//         | '\/'
+//         | '\b'
+//         | '\f'
+//         | '\n'
+//         | '\r'
+//         | '\t'
+//         | \uXXXX
+bool json::SAX::isStringValue (const std::string& input, std::string::size_type& cursor, std::string& value)
+{
+  auto backup = cursor;
+
+  if (isLiteral (input, '"', cursor))
+  {
+    std::string word;
+    int c;
+    while ((c = input[cursor]))
+    {
+      // EOS.
+      if (c == '"')
+      {
+        ++cursor;
+        value = word;
+        return true;
+      }
+
+      // Unicode \uXXXX codepoint.
+      else if (input[cursor + 0] == '\\'  &&
+               input[cursor + 1] == 'u'   &&
+               isHexDigit (input[cursor + 2]) &&
+               isHexDigit (input[cursor + 3]) &&
+               isHexDigit (input[cursor + 4]) &&
+               isHexDigit (input[cursor + 5]))
+      {
+        word += utf8_character (
+                  hexToInt (
+                    input[cursor + 2],
+                    input[cursor + 3],
+                    input[cursor + 4],
+                    input[cursor + 5]));
+        cursor += 6;
+      }
+
+      // An escaped thing.
+      else if (c == '\\')
+      {
+        c = input[++cursor];
+        switch (c)
+        {
+        case '"':  word += (char) 0x22; ++cursor; break;
+        case '\'': word += (char) 0x27; ++cursor; break;
+        case '\\': word += (char) 0x5C; ++cursor; break;
+        case 'b':  word += (char) 0x08; ++cursor; break;
+        case 'f':  word += (char) 0x0C; ++cursor; break;
+        case 'n':  word += (char) 0x0A; ++cursor; break;
+        case 'r':  word += (char) 0x0D; ++cursor; break;
+        case 't':  word += (char) 0x09; ++cursor; break;
+        case 'v':  word += (char) 0x0B; ++cursor; break;
+
+        // This pass-through default case means that anything can be escaped
+        // harmlessly. In particular 'quote' is included, if it not one of the
+        // above characters.
+        default:   word += (char) c;    ++cursor; break;
+        }
+      }
+
+      // Ordinary character.
+      else
+      {
+        word += (char) c;
+        ++cursor;
+      }
+    }
+
+    error ("Error: Missing '\"' at position ", cursor);
+  }
+
+  cursor = backup;
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// number := <int> [<frac>] [<exp>]
+bool json::SAX::isNumber (const std::string& input, std::string::size_type& cursor, SAX::Sink& sink)
+{
+  ignoreWhitespace (input, cursor);
+  auto backup = cursor;
+
+  std::string integerPart;
+  if (isInt (input, cursor, integerPart))
+  {
+    std::string fractionalPart;
+    isFrac (input, cursor, fractionalPart);
+
+    std::string exponentPart;
+    isExp (input, cursor, exponentPart);
+
+    // Does it fit in a long?
+    std::string combined = integerPart + fractionalPart + exponentPart;
+    char* end;
+    long longValue = strtol (combined.c_str (), &end, 10);
+    if (! *end && errno != ERANGE)
+    {
+      sink.eventValueInt (longValue);
+      return true;
+    }
+
+    // Does it fit in an unsigned long?
+    unsigned long ulongValue = strtoul (combined.c_str (), &end, 10);
+    if (! *end && errno != ERANGE)
+    {
+      sink.eventValueUint (ulongValue);
+      return true;
+    }
+
+    // If the above fail, allow this one to be capped at imax.
+    double doubleValue = strtod (combined.c_str (), &end);
+    if (! *end)
+    {
+      sink.eventValueDouble (doubleValue);
+      return true;
+    }
+  }
+
+  cursor = backup;
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// int := ['-'] <digits>
+bool json::SAX::isInt (const std::string& input, std::string::size_type& cursor, std::string& value)
+{
+  auto backup = cursor;
+
+  isLiteral (input, '-', cursor);
+  if (isDigits (input, cursor))
+  {
+    value = input.substr (backup, cursor - backup);
+    return true;
+  }
+
+  // No restore necessary.
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// frac := '.' <digits>
+bool json::SAX::isFrac (const std::string& input, std::string::size_type& cursor, std::string& value)
+{
+  auto backup = cursor;
+
+  if (isLiteral (input, '.', cursor) &&
+      isDigits  (input,      cursor))
+  {
+    value = input.substr (backup, cursor - backup);
+    return true;
+  }
+
+  cursor = backup;
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// digits := <digit> [<digit> ...]
+bool json::SAX::isDigits (const std::string& input, std::string::size_type& cursor)
+{
+  int c = input[cursor];
+  if (isDecDigit (c))
+  {
+    c = input[++cursor];
+
+    while (isDecDigit (c))
+      c = input[++cursor];
+
+    return true;
+  }
+
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// digit := 0x30 ('0') .. 0x39 ('9')
+bool json::SAX::isDecDigit (int c)
+{
+  return c >= 0x30 && c <= 0x39;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// hex := 0x30 ('0') .. 0x39 ('9')
+bool json::SAX::isHexDigit (int c)
+{
+  return (c >= 0x30 && c <= 0x39) ||
+         (c >= 0x61 && c <= 0x66) ||
+         (c >= 0x41 && c <= 0x46);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// exp := <e> <digits>
+bool json::SAX::isExp (const std::string& input, std::string::size_type& cursor, std::string& value)
+{
+  auto backup = cursor;
+
+  if (isE      (input, cursor) &&
+      isDigits (input, cursor))
+  {
+    value = input.substr (backup, cursor - backup);
+    return true;
+  }
+
+  cursor = backup;
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// e := e
+//    | e+
+//    | e-
+//    | E
+//    | E+
+//    | E-
+bool json::SAX::isE (const std::string& input, std::string::size_type& cursor)
+{
+  int c = input[cursor];
+  if (c == 'e' ||
+      c == 'E')
+  {
+    c = input[++cursor];
+
+    if (c == '+' ||
+        c == '-')
+    {
+      ++cursor;
+    }
+
+    return true;
+  }
+
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool json::SAX::isBool (const std::string& input, std::string::size_type& cursor, SAX::Sink& sink)
+{
+  ignoreWhitespace (input, cursor);
+
+  if (input[cursor + 0] == 't' &&
+      input[cursor + 1] == 'r' &&
+      input[cursor + 2] == 'u' &&
+      input[cursor + 3] == 'e')
+  {
+    cursor += 4;
+    sink.eventValueBool (true);
+    return true;
+  }
+  else if (input[cursor + 0] == 'f' &&
+           input[cursor + 1] == 'a' &&
+           input[cursor + 2] == 'l' &&
+           input[cursor + 3] == 's' &&
+           input[cursor + 4] == 'e')
+  {
+    cursor += 5;
+    sink.eventValueBool (false);
+    return true;
+  }
+
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool json::SAX::isNull (const std::string& input, std::string::size_type& cursor, SAX::Sink& sink)
+{
+  ignoreWhitespace (input, cursor);
+
+  if (input[cursor + 0] == 'n' &&
+      input[cursor + 1] == 'u' &&
+      input[cursor + 2] == 'l' &&
+      input[cursor + 3] == 'l')
+  {
+    cursor += 4;
+    sink.eventValueNull ();
+    return true;
+  }
+
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool json::SAX::isLiteral (const std::string& input, char literal, std::string::size_type& cursor)
+{
+  ignoreWhitespace (input, cursor);
+
+  if (input[cursor] == literal)
+  {
+    ++cursor;
+    return true;
+  }
+
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Converts '0'     -> 0
+//          '9'     -> 9
+//          'a'/'A' -> 10
+//          'f'/'F' -> 15
+int json::SAX::hexToInt (int c)
+{
+       if (c >= 0x30 && c <= 0x39) return (c - 0x30);
+  else if (c >= 0x41 && c <= 0x46) return (c - 0x41 + 10);
+  else                             return (c - 0x61 + 10);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+int json::SAX::hexToInt (int c0, int c1, int c2, int c3)
+{
+  return (hexToInt (c0) << 12) +
+         (hexToInt (c1) << 8)  +
+         (hexToInt (c2) << 4)  +
+          hexToInt (c3);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+void json::SAX::error (const std::string& message, std::string::size_type cursor)
+{
+  std::stringstream error;
+  error << message << cursor;
+  throw error.str ();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
diff --git a/src/libshared/src/Table.cpp b/src/libshared/src/Table.cpp
new file mode 100644 (file)
index 0000000..d1e63bf
--- /dev/null
@@ -0,0 +1,374 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Copyright 2006 - 2017, Paul Beckingham, Federico Hernandez.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+// http://www.opensource.org/licenses/mit-license.php
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include <cmake.h>
+#include <Table.h>
+#include <shared.h>
+#include <format.h>
+#include <utf8.h>
+#include <unistd.h>
+
+////////////////////////////////////////////////////////////////////////////////
+void Table::add (const std::string& col, bool alignLeft, bool wrap)
+{
+  _columns.push_back (col);
+  _align.push_back (alignLeft);
+  _wrap.push_back (wrap);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+int Table::addRow ()
+{
+  _data.push_back (std::vector <std::string> (_columns.size (), ""));
+  _color.push_back (std::vector <Color> (_columns.size (), Color::nocolor));
+  _oddness.push_back (_data.size () % 2 ? true : false);
+  return _data.size () - 1;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+int Table::addRowOdd ()
+{
+  _data.push_back (std::vector <std::string> (_columns.size (), ""));
+  _color.push_back (std::vector <Color> (_columns.size (), Color::nocolor));
+  _oddness.push_back (true);
+  return _data.size () - 1;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+int Table::addRowEven ()
+{
+  _data.push_back (std::vector <std::string> (_columns.size (), ""));
+  _color.push_back (std::vector <Color> (_columns.size (), Color::nocolor));
+  _oddness.push_back (false);
+  return _data.size () - 1;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+void Table::set (int row, int col, const std::string& value, const Color color)
+{
+  _data[row][col] = value;
+
+  if (color.nontrivial ())
+    _color[row][col] = color;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+void Table::set (int row, int col, int value, const Color color)
+{
+  std::string string_value = format (value);
+  _data[row][col] = string_value;
+
+  if (color.nontrivial ())
+    _color[row][col] = color;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+void Table::set (int row, int col, const Color color)
+{
+  if (color.nontrivial ())
+    _color[row][col] = color;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+std::string Table::render ()
+{
+  // Piped output disables color, unless overridden.
+  if (! _forceColor &&
+      ! isatty (STDOUT_FILENO))
+  {
+    _header     = Color ("");
+    _odd        = Color ("");
+    _even       = Color ("");
+    _intra_odd  = Color ("");
+    _intra_even = Color ("");
+    _extra_odd  = Color ("");
+    _extra_even = Color ("");
+
+    for (auto& row : _color)
+      for (auto& col : row)
+        col = Color ("");
+
+    _underline_headers = true;
+  }
+
+  // Determine minimal, ideal column widths.
+  std::vector <int> minimal;
+  std::vector <int> ideal;
+  for (unsigned int col = 0; col < _columns.size (); ++col)
+  {
+    // Headers factor in to width calculations.
+    unsigned int global_min = utf8_text_width (_columns[col]);
+    unsigned int global_ideal = global_min;
+
+    for (unsigned int row = 0; row < _data.size (); ++row)
+    {
+      // Determine minimum and ideal width for this column.
+      unsigned int min = 0;
+      unsigned int ideal = 0;
+      measureCell (_data[row][col], min, ideal);
+
+      if (min   > global_min)   global_min = min;
+      if (ideal > global_ideal) global_ideal = ideal;
+    }
+
+    minimal.push_back (global_min);
+    ideal.push_back (global_ideal);
+  }
+
+  // Sum the minimal widths.
+  int sum_minimal = 0;
+  for (const auto& c : minimal)
+    sum_minimal += c;
+
+  // Sum the ideal widths.
+  int sum_ideal = 0;
+  for (const auto& c : ideal)
+    sum_ideal += c;
+
+  // Calculate final column widths.
+  int overage = _width
+              - _left_margin
+              - (2 * _extra_padding)
+              - ((_columns.size () - 1) * _intra_padding);
+
+  std::vector <int> widths;
+  if (sum_ideal <= overage)
+    widths = ideal;
+  else if (sum_minimal > overage || overage < 0)
+    widths = minimal;
+  else if (overage > 0)
+  {
+    widths = minimal;
+    overage -= sum_minimal;
+
+    // Spread 'overage' among columns where width[i] < ideal[i]
+    while (overage)
+    {
+      for (unsigned int i = 0; i < _columns.size () && overage; ++i)
+      {
+        if (widths[i] < ideal[i])
+        {
+          ++widths[i];
+          --overage;
+        }
+      }
+    }
+  }
+
+  // Compose column headers.
+  unsigned int max_lines = 0;
+  std::vector <std::vector <std::string>> headers;
+  for (unsigned int c = 0; c < _columns.size (); ++c)
+  {
+    headers.push_back ({});
+    renderCell (headers[c], _columns[c], widths[c], _align[c], _wrap[c], _header);
+
+    if (headers[c].size () > max_lines)
+      max_lines = headers[c].size ();
+  }
+
+  // Output string.
+  std::string out;
+  _lines = 0;
+
+  // Render column headers.
+  std::string left_margin = std::string (_left_margin, ' ');
+  std::string extra       = std::string (_extra_padding, ' ');
+  std::string intra       = std::string (_intra_padding, ' ');
+
+  std::string extra_odd   = _extra_odd.colorize  (extra);
+  std::string extra_even  = _extra_even.colorize (extra);
+  std::string intra_odd   = _intra_odd.colorize  (intra);
+  std::string intra_even  = _intra_even.colorize (intra);
+
+  for (unsigned int i = 0; i < max_lines; ++i)
+  {
+    out += left_margin + extra;
+
+    for (unsigned int c = 0; c < _columns.size (); ++c)
+    {
+      if (c)
+        out += intra;
+
+      if (headers[c].size () < max_lines - i)
+        out += _header.colorize (std::string (widths[c], ' '));
+      else
+        out += headers[c][i];
+    }
+
+    out += extra;
+
+    // Trim right.
+    out.erase (out.find_last_not_of (" ") + 1);
+    out += '\n';
+
+    // Stop if the line limit is exceeded.
+    if (++_lines >= _truncate_lines && _truncate_lines != 0)
+      return out;
+  }
+
+  // Underline headers with ------ if necessary.
+  if (_underline_headers)
+  {
+    out += left_margin + extra;
+    for (unsigned int c = 0; c < _columns.size (); ++c)
+    {
+      if (c)
+        out += intra;
+
+      out += _header.colorize (std::string (widths[c], '-'));
+    }
+
+    out += '\n';
+  }
+
+  // Compose, render columns, in sequence.
+  _rows = 0;
+  std::vector <std::vector <std::string>> cells;
+  for (unsigned int row = 0; row < _data.size (); ++row)
+  {
+    max_lines = 0;
+
+    // Alternate rows based on |s % 2|
+    auto oddness = _oddness[row];
+    Color row_color = oddness ? _odd : _even;
+
+    // TODO row_color.blend (provided color);
+    // TODO Problem: colors for columns are specified, not rows,
+    //      therefore there are only cell colors, not intra colors.
+
+    Color cell_color;
+    for (unsigned int col = 0; col < _columns.size (); ++col)
+    {
+      cell_color = row_color;
+      cell_color.blend (_color[row][col]);
+
+      cells.push_back (std::vector <std::string> ());
+      renderCell (cells[col], _data[row][col], widths[col], _align[col], _wrap[col], cell_color);
+
+      if (cells[col].size () > max_lines)
+        max_lines = cells[col].size ();
+
+      if (_obfuscate)
+        for (auto& value : cells[col])
+          value = obfuscateText (value);
+    }
+
+    for (unsigned int i = 0; i < max_lines; ++i)
+    {
+      out += left_margin + (oddness ? extra_odd : extra_even);
+
+      for (unsigned int col = 0; col < _columns.size (); ++col)
+      {
+        if (col)
+        {
+          if (row_color.nontrivial ())
+            out += row_color.colorize (intra);
+          else
+            out += (oddness ? intra_odd : intra_even);
+        }
+
+        if (i < cells[col].size ())
+          out += cells[col][i];
+        else
+        {
+          cell_color = row_color;
+          cell_color.blend (_color[row][col]);
+          out += cell_color.colorize (std::string (widths[col], ' '));
+        }
+      }
+
+      out += (oddness ? extra_odd : extra_even);
+
+      // Trim right.
+      out.erase (out.find_last_not_of (" ") + 1);
+      out += '\n';
+
+      // Stop if the line limit is exceeded.
+      if (++_lines >= _truncate_lines && _truncate_lines != 0)
+        return out;
+    }
+
+    cells.clear ();
+
+    // Stop if the row limit is exceeded.
+    if (++_rows >= _truncate_rows && _truncate_rows != 0)
+      return out;
+  }
+
+  return out;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+void Table::measureCell (
+  const std::string& data,
+  unsigned int& minimum,
+  unsigned int& maximum) const
+{
+  std::string stripped = Color::strip (data);
+  maximum = longestLine (stripped);
+  minimum = longestWord (stripped);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+void Table::renderCell (
+  std::vector <std::string>& lines,
+  const std::string& value,
+  int width,
+  bool alignLeft,
+  bool wrap,
+  const Color& color) const
+{
+  if (wrap)
+  {
+    std::vector <std::string> raw;
+    wrapText (raw, value, width, false);
+
+    for (const auto& line : raw)
+      if (alignLeft)
+        lines.push_back (
+          color.colorize (
+            leftJustify (line, width)));
+      else
+        lines.push_back (
+          color.colorize (
+            rightJustify (line, width)));
+  }
+  else
+  {
+    if (alignLeft)
+      lines.push_back (
+        color.colorize (
+          leftJustify (value, width)));
+    else
+      lines.push_back (
+        color.colorize (
+          rightJustify (value, width)));
+  }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
diff --git a/src/libshared/src/Table.h b/src/libshared/src/Table.h
new file mode 100644 (file)
index 0000000..977fa99
--- /dev/null
@@ -0,0 +1,100 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Copyright 2006 - 2017, Paul Beckingham, Federico Hernandez.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+// http://www.opensource.org/licenses/mit-license.php
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#ifndef INCLUDED_TABLE
+#define INCLUDED_TABLE
+
+#include <string>
+#include <vector>
+#include <Color.h>
+
+class Table
+{
+public:
+  // View specifications.
+  void add (const std::string& col, bool alignLeft = true, bool wrap = true);
+  void width (int width)               { _width = width;             }
+  void leftMargin (int margin)         { _left_margin = margin;      }
+  void colorHeader (const Color& c)    { _header = c;                }
+  void colorOdd (const Color& c)       { _odd = c;                   }
+  void colorEven (const Color& c)      { _even = c;                  }
+  void intraPadding (int padding)      { _intra_padding = padding;   }
+  void intraColorOdd (const Color& c)  { _intra_odd = c;             }
+  void intraColorEven (const Color& c) { _intra_even = c;            }
+  void extraPadding (int padding)      { _extra_padding = padding;   }
+  void extraColorOdd (const Color& c)  { _extra_odd = c;             }
+  void extraColorEven (const Color& c) { _extra_even = c;            }
+  void truncateLines (int n)           { _truncate_lines = n;        }
+  void truncateRows (int n)            { _truncate_rows = n;         }
+  void forceColor ()                   { _forceColor = true;         }
+  void obfuscate ()                    { _obfuscate = true;          }
+  void underlineHeaders ()             { _underline_headers = true;  }
+  int lines ()                         { return _lines;              }
+  int rows ()                          { return (int) _data.size (); }
+
+  // Data provision.
+  int addRow ();
+  int addRowOdd ();
+  int addRowEven ();
+  void set (int, int, const std::string&, const Color color = Color::nocolor);
+  void set (int, int, int, const Color color = Color::nocolor);
+  void set (int, int, const Color);
+
+  // View rendering.
+  std::string render ();
+
+private:
+  void measureCell (const std::string&, unsigned int&, unsigned int&) const;
+  void renderCell (std::vector <std::string>&, const std::string&, int, bool, bool, const Color&) const;
+
+private:
+  std::vector <std::vector <std::string>> _data;
+  std::vector <std::vector <Color>>       _color;
+  std::vector <std::string>               _columns;
+  std::vector <bool>                      _align;
+  std::vector <bool>                      _wrap;
+  std::vector <bool>                      _oddness;
+  int                                     _width             {0};
+  int                                     _left_margin       {0};
+  Color                                   _header            {0};
+  Color                                   _odd               {0};
+  Color                                   _even              {0};
+  int                                     _intra_padding     {1};
+  Color                                   _intra_odd         {0};
+  Color                                   _intra_even        {0};
+  int                                     _extra_padding     {0};
+  Color                                   _extra_odd         {0};
+  Color                                   _extra_even        {0};
+  int                                     _truncate_lines    {0};
+  int                                     _truncate_rows     {0};
+  bool                                    _forceColor        {false};
+  bool                                    _obfuscate         {false};
+  bool                                    _underline_headers {false};
+  int                                     _lines             {0};
+  int                                     _rows              {0};
+};
+
+#endif
diff --git a/src/libshared/src/Timer.cpp b/src/libshared/src/Timer.cpp
new file mode 100644 (file)
index 0000000..a60e7f2
--- /dev/null
@@ -0,0 +1,94 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Copyright 2006 - 2017, Paul Beckingham, Federico Hernandez.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+// http://www.opensource.org/licenses/mit-license.php
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include <cmake.h>
+#include <Timer.h>
+
+////////////////////////////////////////////////////////////////////////////////
+Timer::Timer ()
+{
+  start ();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+void Timer::start ()
+{
+  _start = std::chrono::high_resolution_clock::now ();
+  _end = {};
+  _running = true;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+void Timer::stop ()
+{
+  if (_running)
+  {
+    _end = std::chrono::high_resolution_clock::now ();
+    _running = false;
+  }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+double Timer::total_s () const
+{
+  auto endpoint = _end;
+  if (_running)
+    endpoint = std::chrono::high_resolution_clock::now ();
+
+  return std::chrono::duration_cast<std::chrono::seconds>(endpoint - _start).count();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+double Timer::total_ms () const
+{
+  auto endpoint = _end;
+  if (_running)
+    endpoint = std::chrono::high_resolution_clock::now ();
+
+  return std::chrono::duration_cast<std::chrono::milliseconds>(endpoint - _start).count();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+double Timer::total_us () const
+{
+  auto endpoint = _end;
+  if (_running)
+    endpoint = std::chrono::high_resolution_clock::now ();
+
+  return std::chrono::duration_cast<std::chrono::microseconds>(endpoint - _start).count();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+double Timer::total_ns () const
+{
+  auto endpoint = _end;
+  if (_running)
+    endpoint = std::chrono::high_resolution_clock::now ();
+
+  return std::chrono::duration_cast<std::chrono::nanoseconds>(endpoint - _start).count();
+}
+
+////////////////////////////////////////////////////////////////////////////////
diff --git a/src/libshared/src/Timer.h b/src/libshared/src/Timer.h
new file mode 100644 (file)
index 0000000..0c3b7dc
--- /dev/null
@@ -0,0 +1,53 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Copyright 2006 - 2017, Paul Beckingham, Federico Hernandez.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+// http://www.opensource.org/licenses/mit-license.php
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#ifndef INCLUDED_TIMER
+#define INCLUDED_TIMER
+
+#include <string>
+#include <chrono>
+
+class Timer
+{
+public:
+  Timer ();
+  ~Timer () = default;
+
+  void start ();
+  void stop ();
+
+  double total_s () const;
+  double total_ms () const;
+  double total_us () const;
+  double total_ns () const;
+
+private:
+  std::chrono::time_point<std::chrono::high_resolution_clock> _start   {};
+  std::chrono::time_point<std::chrono::high_resolution_clock> _end     {};
+  bool                                                        _running {false};
+};
+
+#endif
diff --git a/src/libshared/src/Tree.cpp b/src/libshared/src/Tree.cpp
new file mode 100644 (file)
index 0000000..91fd82b
--- /dev/null
@@ -0,0 +1,275 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Copyright 2010 - 2017, Paul Beckingham, Federico Hernandez.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+// http://www.opensource.org/licenses/mit-license.php
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include <cmake.h>
+#include <Tree.h>
+#include <algorithm>
+#include <iostream>
+#include <sstream>
+#include <shared.h>
+#include <format.h>
+
+////////////////////////////////////////////////////////////////////////////////
+//  - Tree, Branch and Node are synonymous.
+//  - A Tree may contain any number of branches.
+//  - A Branch may contain any number of name/value pairs, unique by name.
+//  - The destructor will delete all branches recursively.
+//  - Tree::enumerate is a snapshot, and is invalidated by modification.
+//  - Branch sequence is preserved.
+void Tree::addBranch (std::shared_ptr <Tree> branch)
+{
+  if (! branch)
+    throw "Failed to allocate memory for parse tree.";
+
+  _branches.push_back (branch);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+void Tree::removeBranch (std::shared_ptr <Tree> branch)
+{
+  for (auto i = _branches.begin (); i != _branches.end (); ++i)
+  {
+    if (branch == *i)
+    {
+      _branches.erase (i);
+      return;
+    }
+  }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+void Tree::removeAllBranches ()
+{
+  _branches.erase (_branches.begin (), _branches.end ());
+}
+
+////////////////////////////////////////////////////////////////////////////////
+void Tree::replaceBranch (std::shared_ptr <Tree> from, std::shared_ptr <Tree> to)
+{
+  for (unsigned int i = 0; i < _branches.size (); ++i)
+  {
+    if (_branches[i] == from)
+    {
+      _branches[i] = to;
+      return;
+    }
+  }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Accessor for attributes.
+void Tree::attribute (const std::string& name, const std::string& value)
+{
+  _attributes[name] = value;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Accessor for attributes.
+void Tree::attribute (const std::string& name, const int value)
+{
+  _attributes[name] = format (value);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Accessor for attributes.
+void Tree::attribute (const std::string& name, const double value)
+{
+  _attributes[name] = format (value, 1, 8);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Accessor for attributes.
+std::string Tree::attribute (const std::string& name)
+{
+  // Prevent autovivification.
+  auto i = _attributes.find (name);
+  if (i != _attributes.end ())
+    return i->second;
+
+  return "";
+}
+
+////////////////////////////////////////////////////////////////////////////////
+void Tree::removeAttribute (const std::string& name)
+{
+  _attributes.erase (name);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Recursively builds a list of std::shared_ptr <Tree> objects, left to right,
+// depth first. The reason for the depth-first enumeration is that a client may
+// wish to traverse the tree and delete nodes.  With a depth-first iteration,
+// this is a safe mechanism, and a node pointer will never be dereferenced after
+// it has been deleted.
+void Tree::enumerate (std::vector <std::shared_ptr <Tree>>& all) const
+{
+  for (auto& i : _branches)
+  {
+    i->enumerate (all);
+    all.push_back (i);
+  }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool Tree::hasTag (const std::string& tag) const
+{
+  return std::find (_tags.begin (), _tags.end (), tag) != _tags.end ();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+void Tree::tag (const std::string& tag)
+{
+  if (! hasTag (tag))
+    _tags.push_back (tag);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+void Tree::unTag (const std::string& tag)
+{
+  auto i = std::find (_tags.begin (), _tags.end (), tag);
+  if (i != _tags.end ())
+    _tags.erase (i);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+int Tree::countTags () const
+{
+  return _tags.size ();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+int Tree::count () const
+{
+  // This branch.
+  int total = 1;
+
+  // Recurse and count the branches.
+  for (auto& i : _branches)
+    total += i->count ();
+
+  return total;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+std::shared_ptr <Tree> Tree::find (const std::string& path)
+{
+  std::vector <std::string> elements = split (path, '/');
+
+  // Must start at the trunk.
+  auto cursor = std::make_shared <Tree> (*this);
+  auto it = elements.begin ();
+  if (cursor->_name != *it)
+    return nullptr;
+
+  // Perhaps the trunk is what is needed?
+  if (elements.size () == 1)
+    return cursor;
+
+  // Now look for the next branch.
+  for (++it; it != elements.end (); ++it)
+  {
+    bool found = false;
+
+    // If the cursor has a branch that matches *it, proceed.
+    for (auto i = cursor->_branches.begin (); i != cursor->_branches.end (); ++i)
+    {
+      if ((*i)->_name == *it)
+      {
+        cursor = *i;
+        found = true;
+        break;
+      }
+    }
+
+    if (! found)
+      return nullptr;
+  }
+
+  return cursor;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+std::string Tree::dumpNode (
+  const std::shared_ptr <Tree> t,
+  int depth) const
+{
+  std::stringstream out;
+
+  // Dump node
+  for (int i = 0; i < depth; ++i)
+    out << "  ";
+
+  out
+      // Useful for debugging tree node new/delete errors.
+      // << std::hex << t << " "
+      << "\033[1m" << t->_name << "\033[0m";
+
+  // Dump attributes.
+  std::string atts;
+  for (auto& a : t->_attributes)
+  {
+    if (atts != "")
+      atts += ' ';
+
+    atts += a.first + "='\033[33m" + a.second + "\033[0m'";
+  }
+
+  if (atts.length ())
+    out << ' ' << atts;
+
+  // Dump tags.
+  std::string tags;
+  for (auto& tag : t->_tags)
+  {
+    if (tags.length ())
+      tags += ' ';
+
+    tags += "\033[32m" + tag + "\033[0m";
+  }
+
+  if (tags.length ())
+    out << ' ' << tags;
+  out << '\n';
+
+  // Recurse for branches.
+  for (auto& b : t->_branches)
+    out << dumpNode (b, depth + 1);
+
+  return out.str ();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+std::string Tree::dump () const
+{
+  std::stringstream out;
+  out << "Tree (" << count () << " nodes)\n"
+      << dumpNode (std::make_shared <Tree> (*this), 1);
+
+  return out.str ();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
diff --git a/src/libshared/src/Tree.h b/src/libshared/src/Tree.h
new file mode 100644 (file)
index 0000000..4444d75
--- /dev/null
@@ -0,0 +1,76 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Copyright 2010 - 2017, Paul Beckingham, Federico Hernandez.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+// http://www.opensource.org/licenses/mit-license.php
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#ifndef INCLUDED_TREE
+#define INCLUDED_TREE
+
+#include <map>
+#include <vector>
+#include <string>
+#include <memory>
+
+class Tree;
+
+class Tree
+{
+public:
+  void addBranch (std::shared_ptr <Tree>);
+  void removeBranch (std::shared_ptr <Tree>);
+  void removeAllBranches ();
+  void replaceBranch (std::shared_ptr <Tree>, std::shared_ptr <Tree>);
+
+  void attribute (const std::string&, const std::string&);
+  void attribute (const std::string&, const int);
+  void attribute (const std::string&, const double);
+  std::string attribute (const std::string&);
+  void removeAttribute (const std::string&);
+
+  void enumerate (std::vector <std::shared_ptr <Tree>>& all) const;
+
+  bool hasTag (const std::string&) const;
+  void tag (const std::string&);
+  void unTag (const std::string&);
+  int countTags () const;
+
+  int count () const;
+
+  std::shared_ptr <Tree> find (const std::string&);
+
+  std::string dump () const;
+
+private:
+  std::string dumpNode (const std::shared_ptr <Tree>, int) const;
+
+public:
+  std::string                          _name       {"Unknown"};  // Name.
+  std::vector <std::shared_ptr <Tree>> _branches   {};           // Children.
+  std::map <std::string, std::string>  _attributes {};           // Attributes (name->value).
+  std::vector <std::string>            _tags       {};           // Tags (tag, tag ...).
+};
+
+#endif
+
+////////////////////////////////////////////////////////////////////////////////
diff --git a/src/libshared/src/format.cpp b/src/libshared/src/format.cpp
new file mode 100644 (file)
index 0000000..82e67a7
--- /dev/null
@@ -0,0 +1,353 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Copyright 2006 - 2017, Paul Beckingham, Federico Hernandez.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+// http://www.opensource.org/licenses/mit-license.php
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include <cmake.h>
+#include <format.h>
+#include <utf8.h>
+#include <algorithm>
+#include <sstream>
+#include <iostream>
+#include <iomanip>
+#include <cctype>
+#include <strings.h>
+#include <string.h>
+#include <unistd.h>
+#include <sys/select.h>
+#include <time.h>
+#include <csignal>
+#include <cmath>
+
+////////////////////////////////////////////////////////////////////////////////
+const std::string format (std::string& value)
+{
+  return value;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+const std::string format (const char* value)
+{
+  std::string s (value);
+  return s;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+const std::string formatHex (int value)
+{
+  std::stringstream s;
+  s.setf (std::ios::hex, std::ios::basefield);
+  s << value;
+  return s.str ();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+const std::string format (float value, int width, int precision)
+{
+  std::stringstream s;
+  s.width (width);
+  s.precision (precision);
+  if (0 < value && value < 1)
+  {
+    // For value close to zero, width - 2 (2 accounts for the first zero and
+    // the dot) is the number of digits after zero that are significant
+    double factor = 1;
+    for (int i = 2; i < width; i++)
+      factor *= 10;
+    value = roundf (value * factor) / factor;
+  }
+  s << value;
+  return s.str ();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+const std::string format (double value, int width, int precision)
+{
+  std::stringstream s;
+  s.width (width);
+  s.precision (precision);
+  if (0 < value && value < 1)
+  {
+    // For value close to zero, width - 2 (2 accounts for the first zero and
+    // the dot) is the number of digits after zero that are significant
+    double factor = 1;
+    for (int i = 2; i < width; i++)
+      factor *= 10;
+    value = round (value * factor) / factor;
+  }
+  s << value;
+  return s.str ();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+const std::string format (double value)
+{
+  std::stringstream s;
+  s << std::fixed << value;
+  return s.str ();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+void replace_positional (
+  std::string& fmt,
+  const std::string& from,
+  const std::string& to)
+{
+  std::string::size_type pos = 0;
+  while ((pos = fmt.find (from, pos)) != std::string::npos)
+  {
+    fmt.replace (pos, from.length (), to);
+    pos += to.length ();
+  }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+std::string leftJustify (const int input, const int width)
+{
+  std::stringstream s;
+  s << input;
+  std::string output = s.str ();
+  return output + std::string (width - output.length (), ' ');
+}
+
+////////////////////////////////////////////////////////////////////////////////
+std::string leftJustify (const std::string& input, const int width)
+{
+  return input + std::string (width - utf8_text_width (input), ' ');
+}
+
+////////////////////////////////////////////////////////////////////////////////
+std::string rightJustifyZero (const int input, const int width)
+{
+  std::stringstream s;
+  s << std::setw (width) << std::setfill ('0') << input;
+  return s.str ();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+std::string rightJustify (const int input, const int width)
+{
+  std::stringstream s;
+  s << std::setw (width) << std::setfill (' ') << input;
+  return s.str ();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+std::string rightJustify (const std::string& input, const int width)
+{
+  unsigned int len = utf8_text_width (input);
+  return (((unsigned int) width > len)
+           ? std::string (width - len, ' ')
+           : "")
+         + input;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+std::string commify (const std::string& data)
+{
+  // First scan for decimal point and end of digits.
+  int decimalPoint = -1;
+  int end          = -1;
+
+  int i;
+  for (int i = 0; i < (int) data.length (); ++i)
+  {
+    if (isdigit (data[i]))
+      end = i;
+
+    if (data[i] == '.')
+      decimalPoint = i;
+  }
+
+  std::string result;
+  if (decimalPoint != -1)
+  {
+    // In reverse order, transfer all digits up to, and including the decimal
+    // point.
+    for (i = (int) data.length () - 1; i >= decimalPoint; --i)
+      result += data[i];
+
+    int consecutiveDigits = 0;
+    for (; i >= 0; --i)
+    {
+      if (isdigit (data[i]))
+      {
+        result += data[i];
+
+        if (++consecutiveDigits == 3 && i && isdigit (data[i - 1]))
+        {
+          result += ',';
+          consecutiveDigits = 0;
+        }
+      }
+      else
+        result += data[i];
+    }
+  }
+  else
+  {
+    // In reverse order, transfer all digits up to, but not including the last
+    // digit.
+    for (i = (int) data.length () - 1; i > end; --i)
+      result += data[i];
+
+    int consecutiveDigits = 0;
+    for (; i >= 0; --i)
+    {
+      if (isdigit (data[i]))
+      {
+        result += data[i];
+
+        if (++consecutiveDigits == 3 && i && isdigit (data[i - 1]))
+        {
+          result += ',';
+          consecutiveDigits = 0;
+        }
+      }
+      else
+        result += data[i];
+    }
+  }
+
+  // reverse result into data.
+  std::string done;
+  for (int i = (int) result.length () - 1; i >= 0; --i)
+    done += result[i];
+
+  return done;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Convert a quantity in bytes to a more readable format.
+std::string formatBytes (size_t bytes)
+{
+  char formatted[24];
+
+       if (bytes >=  995000000) sprintf (formatted, "%.1f GiB", bytes / 1000000000.0);
+  else if (bytes >=     995000) sprintf (formatted, "%.1f MiB", bytes /    1000000.0);
+  else if (bytes >=        995) sprintf (formatted, "%.1f KiB", bytes /       1000.0);
+  else                          sprintf (formatted, "%d B",     (int)bytes);
+
+  return commify (formatted);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Convert a quantity in seconds to a more readable format.
+std::string formatTime (time_t seconds)
+{
+  char formatted[24];
+  float days = (float) seconds / 86400.0;
+
+       if (seconds >= 86400 * 365) sprintf (formatted, "%.1f y", (days / 365.0));
+  else if (seconds >= 86400 * 84)  sprintf (formatted, "%1d mo", (int) (days / 30));
+  else if (seconds >= 86400 * 13)  sprintf (formatted, "%d wk",  (int) (float) (days / 7.0));
+  else if (seconds >= 86400)       sprintf (formatted, "%d d",   (int) days);
+  else if (seconds >= 3600)        sprintf (formatted, "%d h",   (int) (seconds / 3600));
+  else if (seconds >= 60)          sprintf (formatted, "%d m",   (int) (seconds / 60));
+  else if (seconds >= 1)           sprintf (formatted, "%d s",   (int) seconds);
+  else                             strcpy (formatted, "-");
+
+  return std::string (formatted);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+std::string printable (const std::string& input)
+{
+  // Sanitize 'message'.
+  std::string sanitized = input;
+  std::string::size_type bad;
+  while ((bad = sanitized.find ("\r")) != std::string::npos)
+    sanitized.replace (bad, 1, "\\r");
+
+  while ((bad = sanitized.find ("\n")) != std::string::npos)
+    sanitized.replace (bad, 1, "\\n");
+
+  while ((bad = sanitized.find ("\f")) != std::string::npos)
+    sanitized.replace (bad, 1, "\\f");
+
+  while ((bad = sanitized.find ("\t")) != std::string::npos)
+    sanitized.replace (bad, 1, "\\t");
+
+  while ((bad = sanitized.find ("\v")) != std::string::npos)
+    sanitized.replace (bad, 1, "\\v");
+
+  return sanitized;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+std::string printable (char input)
+{
+  // Sanitize 'message'.
+  char stringized[2] = {0};
+  stringized[0] = input;
+
+  std::string sanitized = stringized;
+  switch (input)
+  {
+  case '\r': sanitized = "\\r"; break;
+  case '\n': sanitized = "\\n"; break;
+  case '\f': sanitized = "\\f"; break;
+  case '\t': sanitized = "\\t"; break;
+  case '\v': sanitized = "\\v"; break;
+  default:   sanitized = input; break;
+  }
+
+  return sanitized;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Iterate over the input, converting text to 'x'.
+// Does not modify color codes.
+std::string obfuscateText (const std::string& input)
+{
+  std::stringstream output;
+  std::string::size_type i = 0;
+  int character;
+  bool inside = false;
+
+  while ((character = utf8_next_char (input, i)))
+  {
+    if (inside)
+    {
+      output << (char) character;
+
+      if (character == 'm')
+        inside = false;
+    }
+    else
+    {
+      if (character == 033)
+        inside = true;
+
+      if (inside || character == ' ')
+        output << (char) character;
+      else
+        output << 'x';
+    }
+  }
+
+  return output.str ();
+}
+
+////////////////////////////////////////////////////////////////////////////////
diff --git a/src/libshared/src/format.h b/src/libshared/src/format.h
new file mode 100644 (file)
index 0000000..57f6236
--- /dev/null
@@ -0,0 +1,87 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Copyright 2006 - 2017, Paul Beckingham, Federico Hernandez.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+// http://www.opensource.org/licenses/mit-license.php
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#ifndef INCLUDED_FORMAT
+#define INCLUDED_FORMAT
+
+#include <sstream>
+#include <algorithm>
+#include <string>
+#include <vector>
+
+const std::string format (std::string&);
+const std::string format (const char*);
+const std::string formatHex (int);
+const std::string format (float, int, int);
+const std::string format (double, int, int);
+const std::string format (double);
+
+void replace_positional (std::string&, const std::string&, const std::string&);
+
+template<typename T>
+const std::string format (T value)
+{
+    std::stringstream s;
+    s << value;
+    return s.str ();
+}
+
+template<typename T>
+const std::string format (int fmt_num, const std::string& fmt, T arg)
+{
+    std::string output = fmt;
+    replace_positional (output, '{' + format (fmt_num) + '}', format (arg));
+    return output;
+}
+
+template<typename T, typename... Args>
+const std::string format (int fmt_num, const std::string& fmt, T arg, Args... args)
+{
+    const std::string fmt_replaced (format (fmt_num, fmt, arg));
+    return format (fmt_num+1, fmt_replaced, args...);
+}
+
+template<typename... Args>
+const std::string format (const std::string& fmt, Args... args)
+{
+    return format (1, fmt, args...);
+}
+
+std::string leftJustify (const int, const int);
+std::string leftJustify (const std::string&, const int);
+std::string rightJustifyZero (const int, const int);
+std::string rightJustify (const int, const int);
+std::string rightJustify (const std::string&, const int);
+
+std::string commify (const std::string&);
+std::string formatBytes (size_t);
+std::string formatTime (time_t);
+std::string printable (const std::string&);
+std::string printable (char);
+
+std::string obfuscateText (const std::string&);
+
+#endif
diff --git a/src/libshared/src/ip.cpp b/src/libshared/src/ip.cpp
new file mode 100644 (file)
index 0000000..511e6f4
--- /dev/null
@@ -0,0 +1,237 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Copyright 2006 - 2017, Paul Beckingham, Federico Hernandez.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+// http://www.opensource.org/licenses/mit-license.php
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include <cmake.h>
+#include <shared.h>
+#include <cctype>
+
+static bool isPort (const std::string&, unsigned int&);
+static bool isChar (const std::string&, char, unsigned int&);
+static bool isEOS (const std::string&, unsigned int&);
+static bool isIPv4Block (const std::string&, unsigned int&);
+static bool isIPv4BlockSet (const std::string&, unsigned int&);
+static bool isIPv6Block (const std::string&, unsigned int&);
+static bool isIPv6BlockSet (const std::string&, unsigned int&);
+
+////////////////////////////////////////////////////////////////////////////////
+static bool isPort (const std::string& input, unsigned int& c)
+{
+  auto start = c;
+  while (std::isdigit (input[c]))
+    ++c;
+
+  return c - start > 0 &&
+         c - start < 6;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+static bool isChar (const std::string& input, char character, unsigned int& c)
+{
+  if (input[c] == character)
+  {
+    ++c;
+    return true;
+  }
+
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+static bool isEOS (const std::string& input, unsigned int& c)
+{
+  return c >= input.length ();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+static bool isIPv4Block (const std::string& input, unsigned int& c)
+{
+  auto start = c;
+  while (std::isdigit (input[c]))
+    ++c;
+
+  if (c - start > 0 &&
+      c - start < 4)
+  {
+    auto byte = std::stoi (input.substr (start, c - start));
+    if (byte < 256)
+      return true;
+  }
+
+  c = start;
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+static bool isIPv4BlockSet (const std::string& input, unsigned int& c)
+{
+  auto start = c;
+
+  if (isIPv4Block (input,      c) &&
+      isChar      (input, '.', c) &&
+      isIPv4Block (input,      c) &&
+      isChar      (input, '.', c) &&
+      isIPv4Block (input,      c) &&
+      isChar      (input, '.', c) &&
+      isIPv4Block (input,      c))
+  {
+
+    return true;
+  }
+
+  c = start;
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+static bool isIPv6Block (const std::string& input, unsigned int& c)
+{
+  auto start = c;
+  while (std::isxdigit (input[c]))
+    ++c;
+
+  if (c - start > 0 &&
+      c - start < 5)
+  {
+    return true;
+  }
+
+  c = start;
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// at least one non-empty block, or '::'
+// at least two colons
+// 8 or less blocks
+static bool isIPv6BlockSet (const std::string& input, unsigned int& c)
+{
+  auto start = c;
+  int count_colons {0};
+  int count_blocks {0};
+
+  while (1)
+  {
+    if (isEOS (input, c))
+    {
+      if (c == start)
+        return false;
+
+      break;
+    }
+
+    else if (isChar (input, ':', c))
+      ++count_colons;
+
+    else if (isIPv4BlockSet (input, c))
+      ++count_blocks;
+
+    else if (isIPv6Block (input, c))
+      ++count_blocks;
+
+    else if (isChar (input, '.', c) ||
+             isChar (input, ']', c))
+    {
+      --c;
+      break;
+    }
+
+    else
+      break;
+  }
+
+  if (count_colons >= 2 &&
+      count_colons <= 7 &&
+      ((count_blocks == 0 && input.substr (start, c) == "::") || count_blocks >= 1) &&
+      count_blocks <= 8)
+  {
+    return true;
+  }
+
+  c = start;
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// <address>:<port>
+// <address>
+bool isIPv4Address (const std::string& input, std::string& address, int& port)
+{
+  unsigned int c = 0;
+  if (isIPv4BlockSet (input, c))
+  {
+    auto colon = c;
+    if (isChar (input, ':', c))
+      if (! isPort (input, c))
+        return false;
+
+    if (isEOS (input, c))
+    {
+      address = input.substr (0, std::min (c, colon));
+      if (! isEOS (input, colon))
+        port = std::stoi (input.substr (colon + 1));
+
+      return true;
+    }
+  }
+
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+//  [<address>]:<port>
+//  <address>
+bool isIPv6Address (const std::string& input, std::string& address, int& port)
+{
+  unsigned int c = 0;
+
+  if (isChar         (input, '[', c) &&
+      isIPv6BlockSet (input,      c) &&
+      isChar         (input, ']', c))
+  {
+    auto colon = c;
+    if (isChar         (input, ':', c) &&
+        isPort         (input,      c) &&
+        isEOS          (input,      c))
+    {
+      address = input.substr (1, colon - 2);
+      port = std::stoi (input.substr (colon + 1));
+      return true;
+    }
+  }
+
+  c = 0;
+  if (isIPv6BlockSet (input, c) &&
+      isEOS          (input, c))
+  {
+    address = input;
+    port = 0;
+    return true;
+  }
+
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
diff --git a/src/libshared/src/lex.cpp b/src/libshared/src/lex.cpp
new file mode 100644 (file)
index 0000000..2a96862
--- /dev/null
@@ -0,0 +1,20 @@
+////////////////////////////////////////////////////////////////////////////////
+
+#include <iostream>
+#include <Lexer.h>
+
+int main (int argc, char** argv)
+{
+  for (auto i = 1; i < argc; i++)
+  {
+    std::cout << "argument '" << argv[i] << "'\n";
+
+    Lexer l (argv[i]);
+    std::string token;
+    Lexer::Type type;
+    while (l.token (token, type))
+      std::cout << "  token '" << token << "' " << Lexer::typeToString (type) << "\n";
+  }
+}
+
+////////////////////////////////////////////////////////////////////////////////
diff --git a/src/libshared/src/shared.cpp b/src/libshared/src/shared.cpp
new file mode 100644 (file)
index 0000000..14f4c2c
--- /dev/null
@@ -0,0 +1,861 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Copyright 2006 - 2017, Paul Beckingham, Federico Hernandez.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+// http://www.opensource.org/licenses/mit-license.php
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include <cmake.h>
+#include <shared.h>
+#include <utf8.h>
+#include <algorithm>
+#include <sstream>
+#include <iostream>
+#include <iomanip>
+#include <cctype>
+#include <strings.h>
+#include <unistd.h>
+#include <sys/select.h>
+#include <cerrno>
+#include <csignal>
+#include <cmath>
+#include <cstring>
+#include <sys/wait.h>
+#include <format.h>
+
+///////////////////////////////////////////////////////////////////////////////
+void wrapText (
+  std::vector <std::string>& lines,
+  const std::string& text,
+  const int width,
+  bool hyphenate)
+{
+  std::string line;
+  unsigned int offset = 0;
+  while (extractLine (line, text, width, hyphenate, offset))
+    lines.push_back (line);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Split in a separator. Two adjacent separators means empty token.
+std::vector <std::string> split (const std::string& input, const char delimiter)
+{
+  std::vector <std::string> results;
+  std::string::size_type start = 0;
+  std::string::size_type i;
+  while ((i = input.find (delimiter, start)) != std::string::npos)
+  {
+    results.push_back (input.substr (start, i - start));
+    start = i + 1;
+  }
+
+  if (input.length ())
+    results.push_back (input.substr (start));
+
+  return results;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Split on words. Adjacent separators collapsed.
+std::vector <std::string> split (const std::string& input)
+{
+  static std::string delims = " \t\n\f\r";
+  std::vector <std::string> results;
+
+  std::string::size_type start = 0;
+  std::string::size_type end;
+  while ((start = input.find_first_not_of (delims, start)) != std::string::npos)
+  {
+    if ((end = input.find_first_of (delims, start)) != std::string::npos)
+    {
+      results.push_back (input.substr (start, end - start));
+      start = end;
+    }
+    else
+    {
+      results.push_back (input.substr (start));
+      start = std::string::npos;
+    }
+  }
+
+  return results;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+std::string join (
+  const std::string& separator,
+  const std::vector<int>& items)
+{
+  std::stringstream s;
+  auto size = items.size ();
+  for (unsigned int i = 0; i < size; ++i)
+  {
+    if (i)
+      s << separator;
+
+    s << items[i];
+  }
+
+  return s.str ();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+std::string join (
+  const std::string& separator,
+  const std::vector<std::string>& items)
+{
+  std::stringstream s;
+  auto size = items.size ();
+  for (unsigned int i = 0; i < size; ++i)
+  {
+    if (i)
+      s << separator;
+
+    s << items[i];
+  }
+
+  return s.str ();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+std::string str_replace (
+  const std::string &str,
+  const std::string& search,
+  const std::string& replacement)
+{
+  std::string modified {str};
+  std::string::size_type pos = 0;
+  while ((pos = modified.find (search, pos)) != std::string::npos)
+  {
+    modified.replace (pos, search.length (), replacement);
+    pos += replacement.length ();
+  }
+
+  return modified;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+std::string trim (const std::string& input, const std::string& edible)
+{
+  auto start = input.find_first_not_of (edible);
+  auto end   = input.find_last_not_of  (edible);
+
+  if (start == std::string::npos)
+    return "";
+
+  if (end == std::string::npos)
+    return input.substr (start);
+
+  return input.substr (start, end - start + 1);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+std::string ltrim (const std::string& input, const std::string& edible)
+{
+  auto start = input.find_first_not_of (edible);
+  if (start == std::string::npos)
+    return "";
+
+  return input.substr (start);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+std::string rtrim (const std::string& input, const std::string& edible)
+{
+  if (input.find_first_not_of (edible) == std::string::npos)
+    return "";
+
+  auto end = input.find_last_not_of (edible);
+  if (end == std::string::npos)
+    return input;
+
+  return input.substr (0, end + 1);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+int longestWord (const std::string& input)
+{
+  int longest = 0;
+  int length = 0;
+  std::string::size_type i = 0;
+  int character;
+
+  while ((character = utf8_next_char (input, i)))
+  {
+    if (character == ' ')
+    {
+      if (length > longest)
+        longest = length;
+
+      length = 0;
+    }
+    else
+      length += mk_wcwidth (character);
+  }
+
+  if (length > longest)
+    longest = length;
+
+  return longest;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+int longestLine (const std::string& input)
+{
+  int longest = 0;
+  int length = 0;
+  std::string::size_type i = 0;
+  int character;
+
+  while ((character = utf8_next_char (input, i)))
+  {
+    if (character == '\n')
+    {
+      if (length > longest)
+        longest = length;
+
+      length = 0;
+    }
+    else
+      length += mk_wcwidth (character);
+  }
+
+  if (length > longest)
+    longest = length;
+
+  return longest;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Walk the input text looking for a break point.  A break point is one of:
+//   - EOS
+//   - \n
+//   - last space before 'length' characters
+//   - last punctuation (, ; . :) before 'length' characters, even if not
+//     followed by a space
+//   - first 'length' characters
+//
+// text       "one two three\n  four"
+// bytes       0123456789012 3456789
+// characters  1234567890a23 4567890
+//
+// leading_ws
+// ws             ^   ^       ^^
+// punct
+// break                     ^
+bool extractLine (
+  std::string& line,
+  const std::string& text,
+  int width,
+  bool hyphenate,
+  unsigned int& offset)
+{
+  // Terminate processing.
+  // Note: bytes vs bytes.
+  if (offset >= text.length ())
+    return false;
+
+  std::string::size_type last_last_bytes = offset;
+  std::string::size_type last_bytes = offset;
+  std::string::size_type bytes = offset;
+  unsigned int last_ws = 0;
+  int character;
+  int char_width = 0;
+  int line_width = 0;
+  while (1)
+  {
+    last_last_bytes = last_bytes;
+    last_bytes = bytes;
+    character = utf8_next_char (text, bytes);
+
+    if (character == 0 ||
+        character == '\n')
+    {
+      line = text.substr (offset, last_bytes - offset);
+      offset = bytes;
+      break;
+    }
+    else if (character == ' ')
+      last_ws = last_bytes;
+
+    char_width = mk_wcwidth (character);
+    if (line_width + char_width > width)
+    {
+      int last_last_character = text[last_last_bytes];
+      int last_character = text[last_bytes];
+
+      // [case 1] one| two --> last_last != 32, last == 32, ws == 0
+      if (last_last_character != ' ' &&
+          last_character      == ' ')
+      {
+        line = text.substr (offset, last_bytes - offset);
+        offset = last_bytes + 1;
+        break;
+      }
+
+      // [case 2] one |two --> last_last == 32, last != 32, ws != 0
+      else if (last_last_character == ' ' &&
+               last_character      != ' ' &&
+               last_ws             != 0)
+      {
+        line = text.substr (offset, last_bytes - offset - 1);
+        offset = last_bytes;
+        break;
+      }
+
+      else if (last_last_character != ' ' &&
+               last_character      != ' ')
+      {
+        // [case 3] one t|wo --> last_last != 32, last != 32, ws != 0
+        if (last_ws != 0)
+        {
+          line = text.substr (offset, last_ws - offset);
+          offset = last_ws + 1;
+          break;
+        }
+        // [case 4] on|e two --> last_last != 32, last != 32, ws == 0
+        else
+        {
+          if (hyphenate)
+          {
+            line = text.substr (offset, last_bytes - offset - 1) + '-';
+            offset = last_last_bytes;
+          }
+          else
+          {
+            line = text.substr (offset, last_bytes - offset);
+            offset = last_bytes;
+          }
+        }
+
+        break;
+      }
+    }
+
+    line_width += char_width;
+  }
+
+  return true;
+}
+/*
+
+TODO Resolve above against below, which is from Taskwarrior 2.6.0, and known to
+     be wrong.
+////////////////////////////////////////////////////////////////////////////////
+// Break UTF8 text into chunks no more than width characters.
+bool extractLine (
+  std::string& line,
+  const std::string& text,
+  int width,
+  bool hyphenate,
+  unsigned int& offset)
+{
+  // Terminate processing.
+  if (offset >= text.length ())
+    return false;
+
+  int line_length                     {0};
+  int character                       {0};
+  std::string::size_type lastWordEnd  {std::string::npos};
+  bool something                      {false};
+  std::string::size_type cursor       {offset};
+  std::string::size_type prior_cursor {offset};
+  while ((character = utf8_next_char (text, cursor)))
+  {
+    // Premature EOL.
+    if (character == '\n')
+    {
+      line = text.substr (offset, line_length);
+      offset = cursor;
+      return true;
+    }
+
+    if (! Lexer::isWhitespace (character))
+    {
+      something = true;
+      if (! text[cursor] || Lexer::isWhitespace (text[cursor]))
+        lastWordEnd = prior_cursor;
+    }
+
+    line_length += mk_wcwidth (character);
+
+    if (line_length >= width)
+    {
+      // Backtrack to previous word end.
+      if (lastWordEnd != std::string::npos)
+      {
+        // Eat one WS after lastWordEnd.
+        std::string::size_type lastBreak = lastWordEnd;
+        utf8_next_char (text, lastBreak);
+
+        // Position offset at following char.
+        std::string::size_type nextStart = lastBreak;
+        utf8_next_char (text, nextStart);
+
+        line = text.substr (offset, lastBreak - offset);
+        offset = nextStart;
+        return true;
+      }
+
+      // No backtrack, possible hyphenation.
+      else if (hyphenate)
+      {
+        line = text.substr (offset, prior_cursor - offset) + '-';
+        offset = prior_cursor;
+        return true;
+      }
+
+      // No hyphenation, just truncation.
+      else
+      {
+        line = text.substr (offset, cursor - offset);
+        offset = cursor;
+        return true;
+      }
+    }
+
+    // Hindsight.
+    prior_cursor = cursor;
+  }
+
+  // Residual text.
+  if (something)
+  {
+    line = text.substr (offset, cursor - offset);
+     offset = cursor;
+    return true;
+  }
+
+  return false;
+}
+*/
+
+////////////////////////////////////////////////////////////////////////////////
+bool compare (
+  const std::string& left,
+  const std::string& right,
+  bool sensitive /*= true*/)
+{
+  // Use strcasecmp if required.
+  if (! sensitive)
+    return strcasecmp (left.c_str (), right.c_str ()) == 0 ? true : false;
+
+  // Otherwise, just use std::string::operator==.
+  return left == right;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool closeEnough (
+  const std::string& reference,
+  const std::string& attempt,
+  unsigned int minLength /* = 0 */)
+{
+  // An exact match is accepted first.
+  if (compare (reference, attempt, false))
+    return true;
+
+  // A partial match will suffice.
+  if (attempt.length () < reference.length () &&
+      attempt.length () >= minLength)
+    return compare (reference.substr (0, attempt.length ()), attempt, false);
+
+  return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+int matchLength (
+  const std::string& left,
+  const std::string& right)
+{
+  int pos = 0;
+  while (left[pos] &&
+         right[pos] &&
+         left[pos] == right[pos])
+    ++pos;
+
+  return pos;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+std::string::size_type find (
+  const std::string& text,
+  const std::string& pattern,
+  bool sensitive)
+{
+  return find (text, pattern, 0, sensitive);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+std::string::size_type find (
+  const std::string& text,
+  const std::string& pattern,
+  std::string::size_type begin,
+  bool sensitive)
+{
+  // Implement a sensitive find, which is really just a loop withing a loop,
+  // comparing lower-case versions of each character in turn.
+  if (!sensitive)
+  {
+    // Handle empty pattern.
+    const char* p = pattern.c_str ();
+    size_t len = pattern.length ();
+    if (len == 0)
+      return 0;
+
+    // Handle bad begin.
+    if (begin >= text.length ())
+      return std::string::npos;
+
+    // Evaluate these once, for performance reasons.
+    const char* start = text.c_str ();
+    const char* t = start + begin;
+    const char* end = start + text.size ();
+
+    for (; t <= end - len; ++t)
+    {
+      int diff = 0;
+      for (size_t i = 0; i < len; ++i)
+        if ((diff = tolower (t[i]) - tolower (p[i])))
+          break;
+
+      // diff == 0 means there was no break from the loop, which only occurs
+      // when a difference is detected.  Therefore, the loop terminated, and
+      // diff is zero.
+      if (diff == 0)
+        return t - start;
+    }
+
+    return std::string::npos;
+  }
+
+  // Otherwise, just use std::string::find.
+  return text.find (pattern, begin);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+std::string lowerCase (const std::string& input)
+{
+  std::string output {input};
+  std::transform (output.begin (), output.end (), output.begin (), tolower);
+  return output;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+std::string upperCase (const std::string& input)
+{
+  std::string output {input};
+  std::transform (output.begin (), output.end (), output.begin (), toupper);
+  return output;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+std::string upperCaseFirst (const std::string& input)
+{
+  std::string output {input};
+  output[0] = toupper (output[0]);
+  return output;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+int autoComplete (
+  const std::string& partial,
+  const std::vector<std::string>& list,
+  std::vector<std::string>& matches,
+  int minimum/* = 1*/)
+{
+  matches.clear ();
+
+  // Handle trivial case.
+  unsigned int length = partial.length ();
+  if (length)
+  {
+    for (auto& item : list)
+    {
+      // An exact match is a special case.  Assume there is only one exact match
+      // and return immediately.
+      if (partial == item)
+      {
+        matches.clear ();
+        matches.push_back (item);
+        return 1;
+      }
+
+      // Maintain a list of partial matches.
+      else if (length >= (unsigned) minimum &&
+               length <= item.length ()     &&
+               partial == item.substr (0, length))
+        matches.push_back (item);
+    }
+  }
+
+  return matches.size ();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Uses std::getline, because std::cin eats leading whitespace, and that means
+// that if a newline is entered, std::cin eats it and never returns from the
+// "std::cin >> answer;" line, but it does display the newline.  This way, with
+// std::getline, the newline can be detected, and the prompt re-written.
+static void signal_handler (int s)
+{
+  if (s == SIGINT)
+  {
+    std::cout << "\n\nInterrupted: No changes made.\n";
+    exit (1);
+  }
+}
+
+bool confirm (const std::string& question)
+{
+  std::vector <std::string> options {"yes", "no"};
+  std::vector <std::string> matches;
+
+  signal (SIGINT, signal_handler);
+
+  do
+  {
+    std::cout << question
+              << " (yes/no) ";
+
+    std::string answer {""};
+    std::getline (std::cin, answer);
+    answer = std::cin.eof () ? "no" : lowerCase (trim (answer));
+
+    autoComplete (answer, options, matches, 1); // Hard-coded 1.
+  }
+  while (! std::cin.eof () && matches.size () != 1);
+
+  signal (SIGINT, SIG_DFL);
+  return matches.size () == 1 && matches[0] == "yes" ? true : false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Run a binary with args, capturing output.
+int execute (
+  const std::string& executable,
+  const std::vector <std::string>& args,
+  const std::string& input,
+  std::string& output)
+{
+  pid_t pid;
+  int pin[2], pout[2];
+  fd_set rfds, wfds;
+  struct timeval tv;
+  int select_retval, read_retval, write_retval;
+  char buf[16384];
+  unsigned int written;
+  const char* input_cstr = input.c_str ();
+
+  if (signal (SIGPIPE, SIG_IGN) == SIG_ERR) // Handled locally with EPIPE.
+    throw std::string (std::strerror (errno));
+
+  if (pipe (pin) == -1)
+    throw std::string (std::strerror (errno));
+
+  if (pipe (pout) == -1)
+    throw std::string (std::strerror (errno));
+
+  if ((pid = fork ()) == -1)
+    throw std::string (std::strerror (errno));
+
+  if (pid == 0)
+  {
+    // This is only reached in the child
+    close (pin[1]);   // Close the write end of the input pipe.
+    close (pout[0]);  // Close the read end of the output pipe.
+
+    // Parent writes to pin[1]. Set read end pin[0] as STDIN for child.
+    if (dup2 (pin[0], STDIN_FILENO) == -1)
+      throw std::string (std::strerror (errno));
+    close (pin[0]);
+
+    // Parent reads from pout[0]. Set write end pout[1] as STDOUT for child.
+    if (dup2 (pout[1], STDOUT_FILENO) == -1)
+      throw std::string (std::strerror (errno));
+    close (pout[1]);
+
+    // Add executable as argv[0] and NULL-terminate the array for execvp().
+    char** argv = new char* [args.size () + 2];
+    argv[0] = (char*) executable.c_str ();
+    for (unsigned int i = 0; i < args.size (); ++i)
+      argv[i+1] = (char*) args[i].c_str ();
+
+    argv[args.size () + 1] = NULL;
+    _exit (execvp (executable.c_str (), argv));
+  }
+
+  // This is only reached in the parent
+  close (pin[0]);   // Close the read end of the input pipe.
+  close (pout[1]);  // Close the write end of the output pipe.
+
+  if (input.size () == 0)
+  {
+    // Nothing to send to the child, close the pipe early.
+    close (pin[1]);
+  }
+
+  output = "";
+  read_retval = -1;
+  written = 0;
+  while (read_retval != 0 || input.size () != written)
+  {
+    FD_ZERO (&rfds);
+    if (read_retval != 0)
+      FD_SET (pout[0], &rfds);
+
+    FD_ZERO (&wfds);
+    if (input.size () != written)
+      FD_SET (pin[1], &wfds);
+
+    // On Linux, tv may be overwritten by select().  Reset it each time.
+    // NOTE: Timeout chosen arbitrarily - we don't time out execute() calls.
+    // select() is run over and over again unless the child exits or closes
+    // its pipes.
+    tv.tv_sec = 5;
+    tv.tv_usec = 0;
+
+    select_retval = select (std::max (pout[0], pin[1]) + 1, &rfds, &wfds, NULL, &tv);
+
+    if (select_retval == -1)
+      throw std::string (std::strerror (errno));
+
+    // Write data to child's STDIN
+    if (FD_ISSET (pin[1], &wfds))
+    {
+      write_retval = write (pin[1], input_cstr + written, input.size () - written);
+      if (write_retval == -1)
+      {
+        if (errno == EPIPE)
+        {
+          // Child died (or closed the pipe) before reading all input.
+          // We don't really care; pretend we wrote it all.
+          write_retval = input.size () - written;
+        }
+        else
+        {
+          throw std::string (std::strerror (errno));
+        }
+      }
+      written += write_retval;
+
+      if (written == input.size ())
+      {
+        // Let the child know that no more input is coming by closing the pipe.
+        close (pin[1]);
+      }
+    }
+
+    // Read data from child's STDOUT
+    if (FD_ISSET (pout[0], &rfds))
+    {
+      read_retval = read (pout[0], &buf, sizeof (buf) - 1);
+      if (read_retval == -1)
+        throw std::string (std::strerror (errno));
+
+      buf[read_retval] = '\0';
+      output += buf;
+    }
+  }
+
+  close (pout[0]);  // Close the read end of the output pipe.
+
+  int status = -1;
+  if (wait (&status) == -1)
+    throw std::string (std::strerror (errno));
+
+  if (WIFEXITED (status))
+  {
+    status = WEXITSTATUS (status);
+  }
+  else
+  {
+    throw std::string ("Error: Could not get Hook exit status!");
+  }
+
+  if (signal (SIGPIPE, SIG_DFL) == SIG_ERR)  // We're done, return to default.
+    throw std::string (std::strerror (errno));
+
+  return status;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+std::string osName ()
+{
+#if defined (DARWIN)
+  return "Darwin";
+#elif defined (SOLARIS)
+  return "Solaris";
+#elif defined (CYGWIN)
+  return "Cygwin";
+#elif defined (HAIKU)
+  return "Haiku";
+#elif defined (OPENBSD)
+  return "OpenBSD";
+#elif defined (FREEBSD)
+  return "FreeBSD";
+#elif defined (NETBSD)
+  return "NetBSD";
+#elif defined (LINUX)
+  return "Linux";
+#elif defined (KFREEBSD)
+  return "GNU/kFreeBSD";
+#elif defined (GNUHURD)
+  return "GNU/Hurd";
+#else
+  return "<unknown>";
+#endif
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// 16.8 Predefined macro names [cpp.predefined]
+//
+// The following macro names shall be defined by the implementation:
+//
+// __cplusplus
+//   The name __cplusplus is defined to the value 201402L when compiling a C++
+//   translation unit.156
+//
+// ---
+//   156) It is intended that future versions of this standard will replace the
+//   value of this macro with a greater value. Non-conforming compilers should
+//   use a value with at most five decimal digits.
+std::string cppCompliance ()
+{
+#ifdef __cplusplus
+  auto level = __cplusplus;
+
+       if (level == 199711) return "C++98/03";
+  else if (level == 201103) return "C++11";
+  else if (level == 201402) return "C++14";
+
+  // This is a hack.  Replace with correct value on standard publication.
+  else if (level >  201700) return "C++17";
+
+  // Unknown, just show the value.
+  else if (level >   99999) return format (__cplusplus);
+#endif
+
+  // No C++.
+  return "non-compliant";
+}
+
+////////////////////////////////////////////////////////////////////////////////
diff --git a/src/libshared/src/shared.h b/src/libshared/src/shared.h
new file mode 100644 (file)
index 0000000..d8a56bb
--- /dev/null
@@ -0,0 +1,95 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Copyright 2006 - 2017, Paul Beckingham, Federico Hernandez.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+// http://www.opensource.org/licenses/mit-license.php
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#ifndef INCLUDED_SHARED
+#define INCLUDED_SHARED
+
+#include <sstream>
+#include <algorithm>
+#include <string>
+#include <vector>
+
+// shared.cpp, Non-UTF-8 aware.
+void wrapText (std::vector <std::string>&, const std::string&, const int, bool);
+int longestWord (const std::string&);
+int longestLine (const std::string&);
+bool extractLine (std::string&, const std::string&, int, bool, unsigned int&);
+std::vector <std::string> split (const std::string&, const char);
+std::vector <std::string> split (const std::string&);
+std::string join (const std::string&, const std::vector<int>&);
+std::string join (const std::string&, const std::vector<std::string>&);
+std::string str_replace (const std::string&, const std::string&, const std::string&);
+std::string trim (const std::string&, const std::string& edible = " \t\n\f\r");
+std::string ltrim (const std::string&, const std::string& edible = " \t\n\f\r");
+std::string rtrim (const std::string&, const std::string& edible = " \t\n\f\r");
+bool compare (const std::string&, const std::string&, bool sensitive = true);
+bool closeEnough (const std::string&, const std::string&, unsigned int minLength = 0);
+int matchLength (const std::string&, const std::string&);
+std::string::size_type find (const std::string&, const std::string&, bool sensitive = true);
+std::string::size_type find (const std::string&, const std::string&, std::string::size_type, bool sensitive = true);
+
+// List operations.
+template <class T> void listDiff (
+  const T& left, const T& right, T& leftOnly, T& rightOnly)
+{
+  leftOnly.clear ();
+  for (auto& l : left)
+    if (std::find (right.begin (), right.end (), l) == right.end ())
+      leftOnly.push_back (l);
+
+  rightOnly.clear ();
+  for (auto& r : right)
+    if (std::find (left.begin (), left.end (), r) == left.end ())
+      rightOnly.push_back (r);
+}
+
+template <class T> T listIntersect (
+  const T& left, const T& right)
+{
+  T intersection;
+  for (auto& l : left)
+    if (std::find (right.begin (), right.end (), l) != right.end ())
+      intersection.push_back (l);
+
+  return intersection;
+}
+
+std::string lowerCase (const std::string&);
+std::string upperCase (const std::string&);
+std::string upperCaseFirst (const std::string&);
+
+int autoComplete (const std::string&, const std::vector<std::string>&, std::vector<std::string>&, int minimum = 1);
+bool confirm (const std::string&);
+
+int execute (const std::string&, const std::vector <std::string>&, const std::string&, std::string&);
+std::string osName ();
+std::string cppCompliance ();
+
+// ip.cpp
+bool isIPv4Address (const std::string&, std::string&, int&);
+bool isIPv6Address (const std::string&, std::string&, int&);
+
+#endif
diff --git a/src/libshared/src/unicode.cpp b/src/libshared/src/unicode.cpp
new file mode 100644 (file)
index 0000000..fa5a164
--- /dev/null
@@ -0,0 +1,133 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Copyright 2015 - 2017, Paul Beckingham, Federico Hernandez.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+// http://www.opensource.org/licenses/mit-license.php
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include <cmake.h>
+#include <unicode.h>
+#include <cwctype>
+
+////////////////////////////////////////////////////////////////////////////////
+// Complete Unicode whitespace list.
+bool unicodeWhitespace (unsigned int c)
+{
+  return unicodeHorizontalWhitespace (c) ||
+         unicodeVerticalWhitespace   (c);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Complete Unicode whitespace list.
+//
+// http://en.wikipedia.org/wiki/Whitespace_character
+// Updated 2015-09-13
+// Static
+//
+// TODO This list should be derived from the Unicode database.
+bool unicodeHorizontalWhitespace (unsigned int c)
+{
+  return (c == 0x0020 ||   // space Common  Separator, space
+          c == 0x0009 ||   // Common  Other, control  HT, Horizontal Tab
+          c == 0x00A0 ||   // no-break space  Common  Separator, space
+          c == 0x1680 ||   // ogham space mark  Ogham Separator, space
+          c == 0x180E ||   // mongolian vowel separator Mongolian Separator, space
+          c == 0x2000 ||   // en quad Common  Separator, space
+          c == 0x2001 ||   // em quad Common  Separator, space
+          c == 0x2002 ||   // en space  Common  Separator, space
+          c == 0x2003 ||   // em space  Common  Separator, space
+          c == 0x2004 ||   // three-per-em space  Common  Separator, space
+          c == 0x2005 ||   // four-per-em space Common  Separator, space
+          c == 0x2006 ||   // six-per-em space  Common  Separator, space
+          c == 0x2007 ||   // figure space  Common  Separator, space
+          c == 0x2008 ||   // punctuation space Common  Separator, space
+          c == 0x2009 ||   // thin space  Common  Separator, space
+          c == 0x200A ||   // hair space  Common  Separator, space
+          c == 0x200B ||   // zero width space
+          c == 0x200C ||   // zero width non-joiner
+          c == 0x200D ||   // zero width joiner
+          c == 0x202F ||   // narrow no-break space Common  Separator, space
+          c == 0x205F ||   // medium mathematical space Common  Separator, space
+          c == 0x2060 ||   // word joiner
+          c == 0x3000);    // ideographic space Common  Separator, space
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Complete Unicode whitespace list.
+//
+// http://en.wikipedia.org/wiki/Whitespace_character
+// Updated 2015-09-13
+// Static
+//
+// TODO This list should be derived from the Unicode database.
+bool unicodeVerticalWhitespace (unsigned int c)
+{
+  return (c == 0x000A ||   // Common  Other, control  LF, Line feed
+          c == 0x000B ||   // Common  Other, control  VT, Vertical Tab
+          c == 0x000C ||   // Common  Other, control  FF, Form feed
+          c == 0x000D ||   // Common  Other, control  CR, Carriage return
+          c == 0x0085 ||   // Common  Other, control  NEL, Next line
+          c == 0x2028 ||   // line separator  Common  Separator, line
+          c == 0x2029);    // paragraph separator Common  Separator, paragraph
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool unicodePunctuation (unsigned int c)
+{
+  return iswpunct (c) ? true : false;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+bool unicodeAlpha (unsigned int c)
+{
+  return iswprint (c)             &&
+         ! iswpunct (c)           &&
+         ! unicodeWhitespace (c);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// TODO Needs better definition.
+bool unicodeLatinAlpha (unsigned int c)
+{
+  return (c >= 'A' && c <= 'Z') ||
+         (c >= 'a' && c <= 'z');
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Digits 0-9.
+//
+// TODO This list should be derived from the Unicode database.
+bool unicodeLatinDigit (unsigned int c)
+{
+  return c >= 0x30 && c <= 0x39;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Digits 0-9 a-f A-F.
+bool unicodeHexDigit (unsigned int c)
+{
+  return (c >= '0' && c <= '9') ||
+         (c >= 'a' && c <= 'f') ||
+         (c >= 'A' && c <= 'F');
+}
+
+////////////////////////////////////////////////////////////////////////////////
diff --git a/src/libshared/src/unicode.h b/src/libshared/src/unicode.h
new file mode 100644 (file)
index 0000000..b28017b
--- /dev/null
@@ -0,0 +1,39 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Copyright 2015 - 2017, Paul Beckingham, Federico Hernandez.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+// http://www.opensource.org/licenses/mit-license.php
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#ifndef INCLUDED_UNICODE
+#define INCLUDED_UNICODE
+
+bool unicodeWhitespace           (unsigned int);
+bool unicodeHorizontalWhitespace (unsigned int);
+bool unicodeVerticalWhitespace   (unsigned int);
+bool unicodePunctuation          (unsigned int);
+bool unicodeAlpha                (unsigned int);
+bool unicodeLatinAlpha           (unsigned int);
+bool unicodeLatinDigit           (unsigned int);
+bool unicodeHexDigit             (unsigned int);
+
+#endif
diff --git a/src/libshared/src/utf8.cpp b/src/libshared/src/utf8.cpp
new file mode 100644 (file)
index 0000000..580d9ec
--- /dev/null
@@ -0,0 +1,295 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Copyright 2013 - 2017, Paul Beckingham, Federico Hernandez.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+// http://www.opensource.org/licenses/mit-license.php
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include <cmake.h>
+#include <utf8.h>
+
+////////////////////////////////////////////////////////////////////////////////
+// Converts '0'     -> 0
+//          '9'     -> 9
+//          'a'/'A' -> 10
+//          'f'/'F' -> 15
+#define XDIGIT(x) ((x) >= '0' && (x) <= '9' ? ((x) - '0') : \
+                   (x) >= 'a' && (x) <= 'f' ? ((x) + 10 - 'a') : \
+                   (x) >= 'A' && (x) <= 'F' ? ((x) + 10 - 'A') : 0)
+
+////////////////////////////////////////////////////////////////////////////////
+// Note: Assumes 4-digit hex codepoints:
+//         xxxx
+//         \uxxxx
+//         U+xxxx
+unsigned int utf8_codepoint (const std::string& input)
+{
+  unsigned int codepoint = 0;
+  int length = input.length ();
+
+  // U+xxxx, \uxxxx
+  if (length >= 6 &&
+      ((input[0] == 'U'  && input[1] == '+') ||
+       (input[0] == '\\' && input[1] == 'u')))
+  {
+    codepoint = XDIGIT (input[2]) << 12 |
+                XDIGIT (input[3]) <<  8 |
+                XDIGIT (input[4]) <<  4 |
+                XDIGIT (input[5]);
+  }
+  else if (length >= 4)
+  {
+    codepoint = XDIGIT (input[0]) << 12 |
+                XDIGIT (input[1]) <<  8 |
+                XDIGIT (input[2]) <<  4 |
+                XDIGIT (input[3]);
+  }
+
+  return codepoint;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Iterates along a UTF8 string.
+//   - argument i counts bytes advanced through the string
+//   - returns the next character
+unsigned int utf8_next_char (const std::string& input, std::string::size_type& i)
+{
+  if (input[i] == '\0')
+    return 0;
+
+  // How many bytes in the sequence?
+  int length = utf8_sequence (input[i]);
+  i += length;
+
+  // 0xxxxxxx -> 0xxxxxxx
+  if (length == 1)
+    return input[i - 1];
+
+  // 110yyyyy 10xxxxxx -> 00000yyy yyxxxxxx
+  if (length == 2)
+    return ((input[i - 2] & 0x1F) << 6) +
+            (input[i - 1] & 0x3F);
+
+  // 1110zzzz 10yyyyyy 10xxxxxx -> zzzzyyyy yyxxxxxx
+  if (length == 3)
+    return ((input[i - 3] & 0xF)  << 12) +
+           ((input[i - 2] & 0x3F) <<  6) +
+            (input[i - 1] & 0x3F);
+
+  // 11110www 10zzzzzz 10yyyyyy 10xxxxxx -> 000wwwzz zzzzyyyy yyxxxxxx
+  if (length == 4)
+    return ((input[i - 4] & 0x7)  << 18) +
+           ((input[i - 3] & 0x3F) << 12) +
+           ((input[i - 2] & 0x3F) <<  6) +
+            (input[i - 1] & 0x3F);
+
+  // Default: pretend as though it's a single character.
+  // TODO Or should this throw?
+  return input[i - 1];
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// http://en.wikipedia.org/wiki/UTF-8
+std::string utf8_character (unsigned int codepoint)
+{
+  char sequence[5] {};
+
+  // 0xxxxxxx -> 0xxxxxxx
+  if (codepoint < 0x80)
+  {
+    sequence[0] = codepoint;
+  }
+
+  // 00000yyy yyxxxxxx -> 110yyyyy 10xxxxxx
+  else if (codepoint < 0x800)
+  {
+    sequence[0] = 0xC0 | (codepoint & 0x7C0) >> 6;
+    sequence[1] = 0x80 | (codepoint & 0x3F);
+  }
+
+  // zzzzyyyy yyxxxxxx -> 1110zzzz 10yyyyyy 10xxxxxx
+  else if (codepoint < 0x10000)
+  {
+    sequence[0] = 0xE0 | (codepoint & 0xF000) >> 12;
+    sequence[1] = 0x80 | (codepoint & 0xFC0)  >> 6;
+    sequence[2] = 0x80 | (codepoint & 0x3F);
+  }
+
+  // 000wwwzz zzzzyyyy yyxxxxxx -> 11110www 10zzzzzz 10yyyyyy 10xxxxxx
+  else if (codepoint < 0x110000)
+  {
+    sequence[0] = 0xF0 | (codepoint & 0x1C0000) >> 18;
+    sequence[1] = 0x80 | (codepoint & 0x03F000) >> 12;
+    sequence[2] = 0x80 | (codepoint & 0x0FC0)   >> 6;
+    sequence[3] = 0x80 | (codepoint & 0x3F);
+  }
+
+  return std::string (sequence);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+int utf8_sequence (unsigned int character)
+{
+  if ((character & 0xE0) == 0xC0)
+    return 2;
+
+  if ((character & 0xF0) == 0xE0)
+    return 3;
+
+  if ((character & 0xF8) == 0xF0)
+    return 4;
+
+  return 1;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Length of a string in characters.
+unsigned int utf8_length (const std::string& str)
+{
+  int byteLength = str.length ();
+  int charLength = byteLength;
+  const char* data = str.data ();
+
+  // Decrement the number of bytes for each byte that matches 0b10??????
+  // this way only the first byte of any utf8 sequence is counted.
+  for (int i = 0; i < byteLength; i++)
+  {
+    // Extract the first two bits and check whether they are 10
+    if ((data[i] & 0xC0) == 0x80)
+      charLength--;
+  }
+
+  return charLength;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Width of a string in character cells.
+unsigned int utf8_width (const std::string& str)
+{
+  unsigned int length = 0;
+  std::string::size_type i = 0;
+  unsigned int c;
+  while ((c = utf8_next_char (str, i)))
+  {
+    // Control characters, and more especially newline characters, make
+    // mk_wcwidth() return -1.  Ignore that, thereby "adding zero" to length.
+    // Since control characters are not displayed in reports, this is a valid
+    // choice.
+    int l = mk_wcwidth (c);
+    if (l != -1)
+      length += l;
+  }
+
+  return length;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+unsigned int utf8_text_length (const std::string& str)
+{
+  int byteLength = str.length ();
+  int charLength = byteLength;
+  const char* data = str.data ();
+  bool in_color = false;
+
+  // Decrement the number of bytes for each byte that matches 0b10??????
+  // this way only the first byte of any utf8 sequence is counted.
+  for (int i = 0; i < byteLength; i++)
+  {
+    if (in_color)
+    {
+      if (data[i] == 'm')
+        in_color = false;
+
+      --charLength;
+    }
+    else
+    {
+      if (data[i] == 033)
+      {
+        in_color = true;
+        --charLength;
+      }
+      else
+      {
+        // Extract the first two bits and check whether they are 10
+        if ((data[i] & 0xC0) == 0x80)
+          --charLength;
+      }
+    }
+  }
+
+  return charLength;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+unsigned int utf8_text_width (const std::string& str)
+{
+  bool in_color = false;
+
+  unsigned int length = 0;
+  std::string::size_type i = 0;
+  unsigned int c;
+  while ((c = utf8_next_char (str, i)))
+  {
+    if (in_color)
+    {
+      if (c == 'm')
+        in_color = false;
+    }
+    else if (c == 033)
+    {
+      in_color = true;
+    }
+    else
+      length += mk_wcwidth (c);
+  }
+
+  return length;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+const std::string utf8_substr (
+  const std::string& input,
+  unsigned int start,
+  unsigned int length /* = 0 */)
+{
+  // Find the starting index.
+  std::string::size_type index_start = 0;
+  for (unsigned int i = 0; i < start; i++)
+    utf8_next_char (input, index_start);
+
+  std::string result;
+  if (length)
+  {
+    std::string::size_type index_end = index_start;
+    for (unsigned int i = 0; i < length; i++)
+      utf8_next_char (input, index_end);
+
+    result = input.substr (index_start, index_end - index_start);
+  }
+  else
+    result = input.substr (index_start);
+
+  return result;
+}
+
+////////////////////////////////////////////////////////////////////////////////
diff --git a/src/libshared/src/utf8.h b/src/libshared/src/utf8.h
new file mode 100644 (file)
index 0000000..0edc3d8
--- /dev/null
@@ -0,0 +1,44 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Copyright 2013 - 2017, Paul Beckingham, Federico Hernandez.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+// http://www.opensource.org/licenses/mit-license.php
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#ifndef INCLUDED_UTF8
+#define INCLUDED_UTF8
+
+#include <string>
+
+unsigned int utf8_codepoint (const std::string&);
+unsigned int utf8_next_char (const std::string&, std::string::size_type&);
+std::string utf8_character (unsigned int);
+int utf8_sequence (unsigned int);
+unsigned int utf8_length (const std::string&);
+unsigned int utf8_text_length (const std::string&);
+unsigned int utf8_width (const std::string& str);
+unsigned int utf8_text_width (const std::string&);
+const std::string utf8_substr (const std::string&, unsigned int, unsigned int length = 0);
+
+int mk_wcwidth (wchar_t);
+
+#endif
diff --git a/src/libshared/src/wcwidth6.cpp b/src/libshared/src/wcwidth6.cpp
new file mode 100644 (file)
index 0000000..b26aa8f
--- /dev/null
@@ -0,0 +1,211 @@
+/*
+ * This is an implementation of wcwidth() and wcswidth() (defined in
+ * IEEE Std 1002.1-2001) for Unicode.
+ *
+ * http://www.opengroup.org/onlinepubs/007904975/functions/wcwidth.html
+ * http://www.opengroup.org/onlinepubs/007904975/functions/wcswidth.html
+ *
+ * In fixed-width output devices, Latin characters all occupy a single
+ * "cell" position of equal width, whereas ideographic CJK characters
+ * occupy two such cells. Interoperability between terminal-line
+ * applications and (teletype-style) character terminals using the
+ * UTF-8 encoding requires agreement on which character should advance
+ * the cursor by how many cell positions. No established formal
+ * standards exist at present on which Unicode character shall occupy
+ * how many cell positions on character terminals. These routines are
+ * a first attempt of defining such behavior based on simple rules
+ * applied to data provided by the Unicode Consortium.
+ *
+ * For some graphical characters, the Unicode standard explicitly
+ * defines a character-cell width via the definition of the East Asian
+ * FullWidth (F), Wide (W), Half-width (H), and Narrow (Na) classes.
+ * In all these cases, there is no ambiguity about which width a
+ * terminal shall use. For characters in the East Asian Ambiguous (A)
+ * class, the width choice depends purely on a preference of backward
+ * compatibility with either historic CJK or Western practice.
+ * Choosing single-width for these characters is easy to justify as
+ * the appropriate long-term solution, as the CJK practice of
+ * displaying these characters as double-width comes from historic
+ * implementation simplicity (8-bit encoded characters were displayed
+ * single-width and 16-bit ones double-width, even for Greek,
+ * Cyrillic, etc.) and not any typographic considerations.
+ *
+ * Much less clear is the choice of width for the Not East Asian
+ * (Neutral) class. Existing practice does not dictate a width for any
+ * of these characters. It would nevertheless make sense
+ * typographically to allocate two character cells to characters such
+ * as for instance EM SPACE or VOLUME INTEGRAL, which cannot be
+ * represented adequately with a single-width glyph. The following
+ * routines at present merely assign a single-cell width to all
+ * neutral characters, in the interest of simplicity. This is not
+ * entirely satisfactory and should be reconsidered before
+ * establishing a formal standard in this area. At the moment, the
+ * decision which Not East Asian (Neutral) characters should be
+ * represented by double-width glyphs cannot yet be answered by
+ * applying a simple rule from the Unicode database content. Setting
+ * up a proper standard for the behavior of UTF-8 character terminals
+ * will require a careful analysis not only of each Unicode character,
+ * but also of each presentation form, something the author of these
+ * routines has avoided to do so far.
+ *
+ * http://www.unicode.org/unicode/reports/tr11/
+ *
+ * Markus Kuhn -- 2007-05-26 (Unicode 5.0)
+ *
+ * Permission to use, copy, modify, and distribute this software
+ * for any purpose and without fee is hereby granted. The author
+ * disclaims all warranties with regard to this software.
+ *
+ * Latest version: http://www.cl.cam.ac.uk/~mgk25/ucs/wcwidth.c
+ */
+
+#include <cmake.h>
+#include <wchar.h>
+
+struct interval {
+    int first;
+    int last;
+};
+
+/* auxiliary function for binary search in interval table */
+static int bisearch(wchar_t ucs, const struct interval *table, int max) {
+    int min = 0;
+    int mid;
+
+    if (ucs < table[0].first || ucs > table[max].last)
+        return 0;
+    while (max >= min) {
+        mid = (min + max) / 2;
+        if (ucs > table[mid].last)
+            min = mid + 1;
+        else if (ucs < table[mid].first)
+            max = mid - 1;
+        else
+            return 1;
+    }
+
+    return 0;
+}
+
+
+/* The following two functions define the column width of an ISO 10646
+ * character as follows:
+ *
+ *    - The null character (U+0000) has a column width of 0.
+ *
+ *    - Other C0/C1 control characters and DEL will lead to a return
+ *      value of -1.
+ *
+ *    - Non-spacing and enclosing combining characters (general
+ *      category code Mn or Me in the Unicode database) have a
+ *      column width of 0.
+ *
+ *    - SOFT HYPHEN (U+00AD) has a column width of 1.
+ *
+ *    - Other format characters (general category code Cf in the Unicode
+ *      database) and ZERO WIDTH SPACE (U+200B) have a column width of 0.
+ *
+ *    - Hangul Jamo medial vowels and final consonants (U+1160-U+11FF)
+ *      have a column width of 0.
+ *
+ *    - Spacing characters in the East Asian Wide (W) or East Asian
+ *      Full-width (F) category as defined in Unicode Technical
+ *      Report #11 have a column width of 2.
+ *
+ *    - All remaining characters (including all printable
+ *      ISO 8859-1 and WGL4 characters, Unicode control characters,
+ *      etc.) have a column width of 1.
+ *
+ * This implementation assumes that wchar_t characters are encoded
+ * in ISO 10646.
+ */
+
+int mk_wcwidth(wchar_t ucs)
+{
+  /* sorted list of non-overlapping intervals of non-spacing characters */
+  /* generated by "uniset +cat=Me +cat=Mn +cat=Cf -00AD +1160-11FF +200B c" */
+  static const struct interval combining[] = {
+    { 0x0300, 0x036F }, { 0x0483, 0x0486 }, { 0x0488, 0x0489 },
+    { 0x0591, 0x05BD }, { 0x05BF, 0x05BF }, { 0x05C1, 0x05C2 },
+    { 0x05C4, 0x05C5 }, { 0x05C7, 0x05C7 }, { 0x0600, 0x0603 },
+    { 0x0610, 0x0615 }, { 0x064B, 0x065E }, { 0x0670, 0x0670 },
+    { 0x06D6, 0x06E4 }, { 0x06E7, 0x06E8 }, { 0x06EA, 0x06ED },
+    { 0x070F, 0x070F }, { 0x0711, 0x0711 }, { 0x0730, 0x074A },
+    { 0x07A6, 0x07B0 }, { 0x07EB, 0x07F3 }, { 0x0901, 0x0902 },
+    { 0x093C, 0x093C }, { 0x0941, 0x0948 }, { 0x094D, 0x094D },
+    { 0x0951, 0x0954 }, { 0x0962, 0x0963 }, { 0x0981, 0x0981 },
+    { 0x09BC, 0x09BC }, { 0x09C1, 0x09C4 }, { 0x09CD, 0x09CD },
+    { 0x09E2, 0x09E3 }, { 0x0A01, 0x0A02 }, { 0x0A3C, 0x0A3C },
+    { 0x0A41, 0x0A42 }, { 0x0A47, 0x0A48 }, { 0x0A4B, 0x0A4D },
+    { 0x0A70, 0x0A71 }, { 0x0A81, 0x0A82 }, { 0x0ABC, 0x0ABC },
+    { 0x0AC1, 0x0AC5 }, { 0x0AC7, 0x0AC8 }, { 0x0ACD, 0x0ACD },
+    { 0x0AE2, 0x0AE3 }, { 0x0B01, 0x0B01 }, { 0x0B3C, 0x0B3C },
+    { 0x0B3F, 0x0B3F }, { 0x0B41, 0x0B43 }, { 0x0B4D, 0x0B4D },
+    { 0x0B56, 0x0B56 }, { 0x0B82, 0x0B82 }, { 0x0BC0, 0x0BC0 },
+    { 0x0BCD, 0x0BCD }, { 0x0C3E, 0x0C40 }, { 0x0C46, 0x0C48 },
+    { 0x0C4A, 0x0C4D }, { 0x0C55, 0x0C56 }, { 0x0CBC, 0x0CBC },
+    { 0x0CBF, 0x0CBF }, { 0x0CC6, 0x0CC6 }, { 0x0CCC, 0x0CCD },
+    { 0x0CE2, 0x0CE3 }, { 0x0D41, 0x0D43 }, { 0x0D4D, 0x0D4D },
+    { 0x0DCA, 0x0DCA }, { 0x0DD2, 0x0DD4 }, { 0x0DD6, 0x0DD6 },
+    { 0x0E31, 0x0E31 }, { 0x0E34, 0x0E3A }, { 0x0E47, 0x0E4E },
+    { 0x0EB1, 0x0EB1 }, { 0x0EB4, 0x0EB9 }, { 0x0EBB, 0x0EBC },
+    { 0x0EC8, 0x0ECD }, { 0x0F18, 0x0F19 }, { 0x0F35, 0x0F35 },
+    { 0x0F37, 0x0F37 }, { 0x0F39, 0x0F39 }, { 0x0F71, 0x0F7E },
+    { 0x0F80, 0x0F84 }, { 0x0F86, 0x0F87 }, { 0x0F90, 0x0F97 },
+    { 0x0F99, 0x0FBC }, { 0x0FC6, 0x0FC6 }, { 0x102D, 0x1030 },
+    { 0x1032, 0x1032 }, { 0x1036, 0x1037 }, { 0x1039, 0x1039 },
+    { 0x1058, 0x1059 }, { 0x1160, 0x11FF }, { 0x135F, 0x135F },
+    { 0x1712, 0x1714 }, { 0x1732, 0x1734 }, { 0x1752, 0x1753 },
+    { 0x1772, 0x1773 }, { 0x17B4, 0x17B5 }, { 0x17B7, 0x17BD },
+    { 0x17C6, 0x17C6 }, { 0x17C9, 0x17D3 }, { 0x17DD, 0x17DD },
+    { 0x180B, 0x180D }, { 0x18A9, 0x18A9 }, { 0x1920, 0x1922 },
+    { 0x1927, 0x1928 }, { 0x1932, 0x1932 }, { 0x1939, 0x193B },
+    { 0x1A17, 0x1A18 }, { 0x1B00, 0x1B03 }, { 0x1B34, 0x1B34 },
+    { 0x1B36, 0x1B3A }, { 0x1B3C, 0x1B3C }, { 0x1B42, 0x1B42 },
+    { 0x1B6B, 0x1B73 }, { 0x1DC0, 0x1DCA }, { 0x1DFE, 0x1DFF },
+    { 0x200B, 0x200F }, { 0x202A, 0x202E }, { 0x2060, 0x2063 },
+    { 0x206A, 0x206F }, { 0x20D0, 0x20EF }, { 0x302A, 0x302F },
+    { 0x3099, 0x309A }, { 0xA806, 0xA806 }, { 0xA80B, 0xA80B },
+    { 0xA825, 0xA826 }, { 0xFB1E, 0xFB1E }, { 0xFE00, 0xFE0F },
+    { 0xFE20, 0xFE23 }, { 0xFEFF, 0xFEFF }, { 0xFFF9, 0xFFFB },
+    { 0x10A01, 0x10A03 }, { 0x10A05, 0x10A06 }, { 0x10A0C, 0x10A0F },
+    { 0x10A38, 0x10A3A }, { 0x10A3F, 0x10A3F }, { 0x1D167, 0x1D169 },
+    { 0x1D173, 0x1D182 }, { 0x1D185, 0x1D18B }, { 0x1D1AA, 0x1D1AD },
+    { 0x1D242, 0x1D244 }, { 0xE0001, 0xE0001 }, { 0xE0020, 0xE007F },
+    { 0xE0100, 0xE01EF }
+  };
+
+  /* test for 8-bit control characters */
+  if (ucs == 0)
+    return 0;
+  if (ucs < 32 || (ucs >= 0x7f && ucs < 0xa0))
+    return -1;
+
+  /* binary search in table of non-spacing characters */
+  if (bisearch(ucs, combining,
+         sizeof(combining) / sizeof(struct interval) - 1))
+    return 0;
+
+  /* if we arrive here, ucs is not a combining or C0/C1 control character */
+
+  return 1 +
+    (ucs >= 0x1100 &&
+     (ucs <= 0x115f ||                    /* Hangul Jamo init. consonants */
+      ucs == 0x2329 || ucs == 0x232a ||
+      (ucs >= 0x2e80 && ucs <= 0xa4cf &&
+       ucs != 0x303f) ||                  /* CJK ... Yi */
+      (ucs >= 0xac00 && ucs <= 0xd7a3) || /* Hangul Syllables */
+      (ucs >= 0xf900 && ucs <= 0xfaff) || /* CJK Compatibility Ideographs */
+      (ucs >= 0xfe10 && ucs <= 0xfe19) || /* Vertical forms */
+      (ucs >= 0xfe30 && ucs <= 0xfe6f) || /* CJK Compatibility Forms */
+      (ucs >= 0xff00 && ucs <= 0xff60) || /* Fullwidth Forms */
+      (ucs >= 0xffe0 && ucs <= 0xffe6)
+#ifndef CYGWIN
+                                       ||
+      (ucs >= 0x20000 && ucs <= 0x2fffd) ||
+      (ucs >= 0x30000 && ucs <= 0x3fffd)
+#endif
+     )
+    );
+}
+
diff --git a/src/libtasksh.a b/src/libtasksh.a
new file mode 100644 (file)
index 0000000..385a8c1
Binary files /dev/null and b/src/libtasksh.a differ
diff --git a/src/main.cpp b/src/main.cpp
new file mode 100644 (file)
index 0000000..afba727
--- /dev/null
@@ -0,0 +1,192 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Copyright 2006 - 2017, Paul Beckingham, Federico Hernandez.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+// http://www.opensource.org/licenses/mit-license.php
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include <cmake.h>
+#include <iostream>
+#include <vector>
+#include <string>
+#include <cstring>
+#include <cstdio>
+#include <stdlib.h>
+#include <unistd.h>
+#include <shared.h>
+
+#ifdef HAVE_READLINE
+#include <readline/readline.h>
+#include <readline/history.h>
+#endif
+
+// TODO These conflict with tw commands. This needs to be resolved.
+//      Perhaps an escape, such as '-- help' could invoke local help, or using
+//      a 'task' prefix could disambiguate.
+
+// tasksh commands.
+int cmdHelp ();
+int cmdDiagnostics ();
+int cmdReview (const std::vector <std::string>&, bool);
+int cmdShell (const std::vector <std::string>&);
+std::string promptCompose ();
+std::string findTaskwarrior ();
+
+////////////////////////////////////////////////////////////////////////////////
+static void welcome ()
+{
+  std::cout << PACKAGE_STRING << "\n";
+  cmdHelp ();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+const std::string getResponse (const std::string& prompt)
+{
+  std::string response {""};
+
+  // Display prompt, get input.
+#ifdef HAVE_READLINE
+  char *line_read = readline (prompt.c_str ());
+  if (! line_read)
+  {
+    std::cout << "\n";
+    response = "<EOF>";
+  }
+  else
+  {
+    // Save history.
+    if (*line_read)
+      add_history (line_read);
+
+    response = std::string (line_read);
+    free (line_read);
+  }
+#else
+  std::cout << prompt;
+  std::getline (std::cin, response);
+  if (std::cin.eof () == 1)
+  {
+    std::cout << "\n";
+    response = "<EOF>";
+  }
+#endif
+
+  return response;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+static int commandLoop (bool autoClear)
+{
+  // Compose the prompt.
+  auto prompt = promptCompose ();
+
+  // Display prompt, get input.
+  auto command = getResponse (prompt);
+
+  // Obey Taskwarrior's rc.tasksh.autoclear.
+  if (autoClear)
+    std::cout << "\033[2J\033[0;0H";
+
+  int status = 0;
+  if (! isatty (fileno (stdin)) && command == "")
+  {
+    status = -1;
+  }
+  else if (command != "")
+  {
+    auto args = split (command, ' ');
+
+    // Dispatch command.
+         if (args[0] == "<EOF>")                      status = -1;
+    else if (closeEnough ("exit",        args[0], 3)) status = -1;
+    else if (closeEnough ("quit",        args[0], 3)) status = -1;
+    else if (closeEnough ("help",        args[0], 3)) status = cmdHelp ();
+    else if (closeEnough ("diagnostics", args[0], 3)) status = cmdDiagnostics ();
+    else if (closeEnough ("review",      args[0], 3)) status = cmdReview (args, autoClear);
+    else if (closeEnough ("exec",        args[0], 3) ||
+             args[0][0] == '!')                       status = cmdShell (args);
+    else if (command != "")
+    {
+      command = "task " + command;
+      std::cout << "[" << command << "]\n";
+      system (command.c_str ());
+
+      // Deliberately ignoreѕ taskwarrior exit status, otherwise empty filters
+      // cause the shell to terminate.
+    }
+  }
+
+  return status;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+int main (int argc, const char** argv)
+{
+  int status = 0;
+
+  // Lightweight version checking that doesn't require initialization or any I/O.
+  if (argc == 2 && !strcmp (argv[1], "--version"))
+  {
+    std::cout << VERSION << "\n";
+  }
+  else
+  {
+    try
+    {
+      // Get the Taskwarrior rc.tasksh.autoclear Boolean setting.
+      bool autoClear = false;
+      std::string input;
+      std::string output;
+      execute ("task", {"_get", "rc.tasksh.autoclear"}, input, output);
+      output = lowerCase (output);
+      autoClear = (output == "true\n" ||
+                   output == "1\n"    ||
+                   output == "y\n"    ||
+                   output == "yes\n"  ||
+                   output == "on\n");
+
+      if (isatty (fileno (stdin)))
+        welcome ();
+
+      while ((status = commandLoop (autoClear)) == 0)
+        ;
+    }
+
+    catch (const std::string& error)
+    {
+      std::cerr << error << "\n";
+      status = -1;
+    }
+
+    catch (...)
+    {
+      std::cerr << "Unknown error." << "\n";
+      status = -2;
+    }
+  }
+
+  // Returning -1 drops out of the command loop, but gets translated to 0 here,
+  // so that there is a clean way to exit.
+  return status == -1 ? 0 : status;
+}
+
+////////////////////////////////////////////////////////////////////////////////
diff --git a/src/prompt.cpp b/src/prompt.cpp
new file mode 100644 (file)
index 0000000..b1d40f5
--- /dev/null
@@ -0,0 +1,103 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Copyright 2006 - 2017, Paul Beckingham, Federico Hernandez.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+// http://www.opensource.org/licenses/mit-license.php
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include <cmake.h>
+#include <vector>
+#include <string>
+#include <Color.h>
+
+static std::vector <std::string> contextColors = {
+  "bold white on red",
+  "bold white on blue",
+  "bold white on green",
+  "bold white on magenta",
+  "black on cyan",
+  "black on yellow",
+  "black on white",
+};
+
+static std::vector <std::string> contexts;
+
+std::string composeContexts (bool pretty = false);
+
+////////////////////////////////////////////////////////////////////////////////
+int promptClear ()
+{
+  contexts.clear ();
+  return 0;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+int promptRemove ()
+{
+  if (contexts.size ())
+    contexts.pop_back ();
+
+  return 0;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+int promptAdd (const std::string& context)
+{
+  contexts.push_back (context);
+  return 0;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+std::string composeContexts (bool pretty /* = false */)
+{
+  std::string combined;
+  for (unsigned int i = 0; i < contexts.size (); i++)
+    if (pretty)
+      combined += (combined != "" ? " " : "")
+                + std::string ("\001")
+                + Color::colorize ("\002 " + contexts[i] + " \001", contextColors[i % contextColors.size ()])
+                + "\002";
+    else
+      combined += (combined != "" ? " " : "") + contexts[i];
+
+  if (combined != "")
+    combined += ' ';
+
+  return combined;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+std::string promptCompose ()
+{
+  // TODO The prompt may be composed of different elements:
+  // TODO - The configurable text
+  // TODO - The accumulated context, as colored tokens.
+  // TODO - sync status
+  // TODO - time
+  auto decoration = composeContexts (true);
+  if (decoration.length ())
+    return "task " + decoration + "> ";
+
+  return "tasksh> ";
+}
+
+////////////////////////////////////////////////////////////////////////////////
diff --git a/src/review.cpp b/src/review.cpp
new file mode 100644 (file)
index 0000000..86ca12f
--- /dev/null
@@ -0,0 +1,320 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Copyright 2006 - 2017, Paul Beckingham, Federico Hernandez.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+// http://www.opensource.org/licenses/mit-license.php
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include <cmake.h>
+#include <iostream>
+#include <sstream>
+#include <vector>
+#include <string>
+#include <algorithm>
+#include <stdlib.h>
+
+#ifdef HAVE_READLINE
+#include <readline/readline.h>
+#include <readline/history.h>
+#endif
+
+#include <unistd.h>
+#include <sys/ioctl.h>
+
+#ifdef SOLARIS
+#include <sys/termios.h>
+#endif
+
+#include <Color.h>
+#include <Lexer.h>
+#include <shared.h>
+#include <format.h>
+
+std::string getResponse (const std::string&);
+
+////////////////////////////////////////////////////////////////////////////////
+static unsigned int getWidth ()
+{
+  // Determine window size.
+//  int width = config.getInteger ("defaultwidth");
+  static auto width = 0;
+
+  if (width == 0)
+  {
+    unsigned short buff[4];
+    if (ioctl (STDOUT_FILENO, TIOCGWINSZ, &buff) != -1)
+      width = buff[1];
+  }
+
+  return width;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+static void editTask (const std::string& uuid)
+{
+  std::string command = "task rc.confirmation:no rc.verbose:nothing " + uuid + " edit";
+  system (command.c_str ());
+
+  command = "task rc.confirmation:no rc.verbose:nothing " + uuid + " modify reviewed:now";
+  system (command.c_str ());
+  std::cout << "Modified.\n\n\n\n";
+}
+
+////////////////////////////////////////////////////////////////////////////////
+static void modifyTask (const std::string& uuid)
+{
+  Color text ("color15 on gray6");
+  std::string modifications;
+  do
+  {
+    modifications = getResponse (text.colorize (" Enter modification args [example: +tag -tag /teh/the/ project:X] ") + " ");
+  }
+  while (modifications == "");
+
+  std::string command = "task rc.confirmation:no rc.verbose:nothing " + uuid + " modify " + modifications;
+  system (command.c_str ());
+
+  std::cout << "Modified.\n\n\n\n";
+}
+
+////////////////////////////////////////////////////////////////////////////////
+static void reviewTask (const std::string& uuid)
+{
+  std::string command = "task rc.confirmation:no rc.verbose:nothing " + uuid + " modify reviewed:now";
+  system (command.c_str ());
+  std::cout << "Marked as reviewed.\n\n\n\n";
+}
+
+////////////////////////////////////////////////////////////////////////////////
+static void completeTask (const std::string& uuid)
+{
+  std::string command = "task rc.confirmation:no rc.verbose:nothing " + uuid + " done";
+  system (command.c_str ());
+  std::cout << "Completed.\n\n\n\n";
+}
+
+////////////////////////////////////////////////////////////////////////////////
+static void deleteTask (const std::string& uuid)
+{
+  std::string command = "task rc.confirmation:no rc.verbose:nothing " + uuid + " delete";
+  system (command.c_str ());
+  std::cout << "Deleted.\n\n\n\n";
+}
+
+////////////////////////////////////////////////////////////////////////////////
+static const std::string reviewNothing ()
+{
+  return "\nThere are no tasks needing review.\n\n";
+}
+
+////////////////////////////////////////////////////////////////////////////////
+static const std::string reviewStart (
+  unsigned int width)
+{
+  std::string welcome = "The review process is important for keeping your list "
+                        "accurate, so you are working on the right tasks.\n"
+                        "\n"
+                        "For each task you are shown, look at the metadata. "
+                        "Determine whether the task needs to be changed (enter "
+                        "'e' to edit), or whether it is accurate ('enter' or "
+                        "'r' to mark as reviewed). You may skip a task ('s') "
+                        "but a skipped task is not considered reviewed.\n"
+                        "\n"
+                        "You may stop at any time, and resume later right "
+                        "where you left off. See 'man tasksh' for more details.";
+
+  std::vector <std::string> lines;
+  wrapText (lines, welcome, width, false);
+  welcome = join ("\n", lines);
+
+  return "\n" + welcome + "\n\n";
+}
+
+////////////////////////////////////////////////////////////////////////////////
+static const std::string banner (
+  unsigned int current,
+  unsigned int total,
+  unsigned int width,
+  const std::string& message)
+{
+  std::stringstream progress;
+  progress << " ["
+           << current
+           << " of "
+           << total
+           << "] ";
+
+  Color progressColor ("color15 on color9");
+  Color descColor     ("color15 on gray6");
+
+  std::string composed;
+  if (progress.str ().length () + message.length () + 1 < width)
+    composed = progressColor.colorize (progress.str ()) +
+               descColor.colorize (" " + message +
+                 std::string (width - progress.str ().length () - message.length () - 1, ' '));
+  else
+    composed = progressColor.colorize (progress.str ()) +
+               descColor.colorize (" " + message.substr (0, message.length () - 3) + "...");
+
+  return composed + "\n";
+}
+
+////////////////////////////////////////////////////////////////////////////////
+static const std::string menu ()
+{
+  return Color ("color15 on gray6").colorize (" (Enter) Mark as reviewed, (s)kip, (e)dit, (m)odify, (c)omplete, (d)elete, (q)uit ") + " ";
+}
+
+////////////////////////////////////////////////////////////////////////////////
+static void reviewLoop (const std::vector <std::string>& uuids, unsigned int limit, bool autoClear)
+{
+  auto width = getWidth ();
+  unsigned int reviewed = 0;
+
+  // If a limit was specified ('review 10'), then it should override the data
+  // set size, if it is smaller.
+  unsigned int total = uuids.size ();
+  if (limit)
+    total = std::min (total, limit);
+
+  if (total == 0)
+  {
+    std::cout << reviewNothing ();
+    return;
+  }
+
+  std::cout << reviewStart (width);
+
+  unsigned int current = 0;
+  while (current < total &&
+         (limit == 0 || reviewed < limit))
+  {
+    // Run 'info' report for task.
+    auto uuid = uuids[current];
+
+    // Display banner for this task.
+    std::string dummy;
+    std::string description;
+    execute ("task",
+             {"_get", uuid + ".description"},
+             dummy,
+             description);
+
+    std::string response;
+    bool repeat;
+    do
+    {
+      repeat = false;
+      std::cout << banner (current + 1, total, width, Lexer::trimRight (description, "\n"));
+
+      // Use 'system' to run the command and show the output.
+      std::string command = "task " + uuid + " information";
+      system (command.c_str ());
+
+      // Display prompt, get input.
+      response = getResponse (menu ());
+
+           if (response == "e") { editTask (uuid);                                   }
+      else if (response == "m") { modifyTask (uuid);          repeat = true;         }
+      else if (response == "s") { std::cout << "Skipped\n\n"; ++current;             }
+      else if (response == "c") { completeTask (uuid);        ++current; ++reviewed; }
+      else if (response == "d") { deleteTask (uuid);          ++current; ++reviewed; }
+      else if (response == "")  { reviewTask (uuid);          ++current; ++reviewed; }
+      else if (response == "r") { reviewTask (uuid);          ++current; ++reviewed; }
+      else if (response == "q") { break;                                             }
+
+      else
+      {
+        std::cout << format ("Command '{1}' is not recognized.", response) << "\n";
+      }
+
+      // Note that just hitting <Enter> yields an empty command, which does
+      // nothing but advance to the next task.
+
+      if (autoClear)
+        std::cout << "\033[2J\033[0;0H";
+    }
+    while (repeat);
+
+    if (response == "q")
+      break;
+  }
+
+  std::cout << "\n"
+            << format ("End of review. {1} out of {2} tasks reviewed.", reviewed, total)
+            << "\n\n";
+}
+
+////////////////////////////////////////////////////////////////////////////////
+int cmdReview (const std::vector <std::string>& args, bool autoClear)
+{
+  // Is there a specified limit?
+  unsigned int limit = 0;
+  if (args.size () == 2)
+    limit = strtol (args[1].c_str (), NULL, 10);
+
+  // Configure 'reviewed' UDA, but only if necessary.
+  std::string input;
+  std::string output;
+  auto status = execute ("task", {"_get", "rc.uda.reviewed.type"}, input, output);
+  if (status || output != "date\n")
+  {
+    if (confirm ("Tasksh needs to define a 'reviewed' UDA of type 'date' for all tasks.  Ok to proceed?"))
+    {
+      execute ("task", {"rc.confirmation:no", "rc.verbose:nothing", "config", "uda.reviewed.type",  "date"},     input, output);
+      execute ("task", {"rc.confirmation:no", "rc.verbose:nothing", "config", "uda.reviewed.label", "Reviewed"}, input, output);
+    }
+  }
+
+  // Configure '_reviewed' report, but only if necessary.
+  status = execute ("task", {"_get", "rc.report._reviewed.columns"}, input, output);
+  if (status || output != "uuid\n")
+  {
+    if (confirm ("Tasksh needs to define a '_reviewed' report to identify tasks needing review.  Ok to proceed?"))
+    {
+      execute ("task", {"rc.confirmation:no", "rc.verbose:nothing", "config", "report._reviewed.description",
+                        "Tasksh review report.  Adjust the filter to your needs."                                              }, input, output);
+      execute ("task", {"rc.confirmation:no", "rc.verbose:nothing", "config", "report._reviewed.columns", "uuid"               }, input, output);
+      execute ("task", {"rc.confirmation:no", "rc.verbose:nothing", "config", "report._reviewed.sort",    "reviewed+,modified+"}, input, output);
+      execute ("task", {"rc.confirmation:no", "rc.verbose:nothing", "config", "report._reviewed.filter",
+                        "( reviewed.none: or reviewed.before:now-6days ) and ( +PENDING or +WAITING )"                         }, input, output);
+    }
+  }
+
+  // Obtain a list of UUIDs to review.
+  status = execute ("task",
+                    {
+                      "rc.color=off",
+                      "rc.detection=off",
+                      "rc._forcecolor=off",
+                      "rc.verbose=nothing",
+                      "_reviewed"
+                    },
+                    input, output);
+
+  // Review the set of UUIDs.
+  auto uuids = split (Lexer::trimRight (output, "\n"), '\n');
+  reviewLoop (uuids, limit, autoClear);
+  return 0;
+}
+
+////////////////////////////////////////////////////////////////////////////////
diff --git a/src/shell.cpp b/src/shell.cpp
new file mode 100644 (file)
index 0000000..8c14590
--- /dev/null
@@ -0,0 +1,46 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Copyright 2006 - 2017, Paul Beckingham, Federico Hernandez.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+// http://www.opensource.org/licenses/mit-license.php
+//
+////////////////////////////////////////////////////////////////////////////////
+
+#include <cmake.h>
+#include <vector>
+#include <string>
+#include <stdlib.h>
+#include <shared.h>
+
+////////////////////////////////////////////////////////////////////////////////
+int cmdShell (const std::vector <std::string>& args)
+{
+  auto combined = join (" ", args);
+
+  // Support '!ls' as well as '! ls'.
+  if (combined[0] == '!')
+    combined = combined.substr (1);
+
+  system (combined.c_str ());
+  return 0; // Ignore system return code.
+}
+
+////////////////////////////////////////////////////////////////////////////////