diff --git a/external/header_only/SimpleGlob.h b/external/header_only/SimpleGlob.h new file mode 100644 index 0000000000..dddc8d1bd1 --- /dev/null +++ b/external/header_only/SimpleGlob.h @@ -0,0 +1,959 @@ +/*! @file SimpleGlob.h + + @version 3.6 + + @brief A cross-platform file globbing library providing the ability to + expand wildcards in command-line arguments to a list of all matching + files. It is designed explicitly to be portable to any platform and has + been tested on Windows and Linux. See CSimpleGlobTempl for the class + definition. + + @section features FEATURES + - MIT Licence allows free use in all software (including GPL and + commercial) + - multi-platform (Windows 95/98/ME/NT/2K/XP, Linux, Unix) + - supports most of the standard linux glob() options + - recognition of a forward paths as equivalent to a backward slash + on Windows. e.g. "c:/path/foo*" is equivalent to "c:\path\foo*". + - implemented with only a single C++ header file + - char, wchar_t and Windows TCHAR in the same program + - complete working examples included + - compiles cleanly at warning level 4 (Windows/VC.NET 2003), + warning level 3 (Windows/VC6) and -Wall (Linux/gcc) + + @section usage USAGE + The SimpleGlob class is used by following these steps: +
    +
  1. Include the SimpleGlob.h header file + +
    +        \#include "SimpleGlob.h"
    +        
    + +
  2. Instantiate a CSimpleGlob object supplying the appropriate flags. + +
    +        @link CSimpleGlobTempl CSimpleGlob @endlink glob(FLAGS);
    +        
    + +
  3. Add all file specifications to the glob class. + +
    +        glob.Add("file*");
    +        glob.Add(argc, argv);
    +        
    + +
  4. Process all files with File(), Files() and FileCount() + +
    +        for (int n = 0; n < glob.FileCount(); ++n) {
    +            ProcessFile(glob.File(n));
    +        }
    +        
    + +
+ + @section licence MIT LICENCE +
+    The licence text below is the boilerplate "MIT Licence" used from:
+    http://www.opensource.org/licenses/mit-license.php
+
+    Copyright (c) 2006-2013, Brodie Thiesfield
+
+    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.
+
+*/ + +#ifndef INCLUDED_SimpleGlob +#define INCLUDED_SimpleGlob + +/*! @brief The operation of SimpleGlob is fine-tuned via the use of a + combination of the following flags. + + The flags may be passed at initialization of the class and used for every + filespec added, or alternatively they may optionally be specified in the + call to Add() and be different for each filespec. + + @param SG_GLOB_ERR + Return upon read error (e.g. directory does not have read permission) + + @param SG_GLOB_MARK + Append a slash (backslash in Windows) to every path which corresponds + to a directory + + @param SG_GLOB_NOSORT + By default, files are returned in sorted into string order. With this + flag, no sorting is done. This is not compatible with + SG_GLOB_FULLSORT. + + @param SG_GLOB_FULLSORT + By default, files are sorted in groups belonging to each filespec that + was added. For example if the filespec "b*" was added before the + filespec "a*" then the argv array will contain all b* files sorted in + order, followed by all a* files sorted in order. If this flag is + specified, the entire array will be sorted ignoring the filespec + groups. + + @param SG_GLOB_NOCHECK + If the pattern doesn't match anything, return the original pattern. + + @param SG_GLOB_TILDE + Tilde expansion is carried out (on Unix platforms) + + @param SG_GLOB_ONLYDIR + Return only directories which match (not compatible with + SG_GLOB_ONLYFILE) + + @param SG_GLOB_ONLYFILE + Return only files which match (not compatible with SG_GLOB_ONLYDIR) + + @param SG_GLOB_NODOT + Do not return the "." or ".." special directories. + */ +enum SG_Flags { + SG_GLOB_ERR = 1 << 0, + SG_GLOB_MARK = 1 << 1, + SG_GLOB_NOSORT = 1 << 2, + SG_GLOB_NOCHECK = 1 << 3, + SG_GLOB_TILDE = 1 << 4, + SG_GLOB_ONLYDIR = 1 << 5, + SG_GLOB_ONLYFILE = 1 << 6, + SG_GLOB_NODOT = 1 << 7, + SG_GLOB_FULLSORT = 1 << 8 +}; + +/*! @brief Error return codes */ +enum SG_Error { + SG_SUCCESS = 0, + SG_ERR_NOMATCH = 1, + SG_ERR_MEMORY = -1, + SG_ERR_FAILURE = -2 +}; + +// --------------------------------------------------------------------------- +// Platform dependent implementations + +// if we aren't on Windows and we have ICU available, then enable ICU +// by default. Define this to 0 to intentially disable it. +#ifndef SG_HAVE_ICU +# if !defined(_WIN32) && defined(USTRING_H) +# define SG_HAVE_ICU 1 +# else +# define SG_HAVE_ICU 0 +# endif +#endif + +// don't include this in documentation as it isn't relevant +#ifndef DOXYGEN + +// on Windows we want to use MBCS aware string functions and mimic the +// Unix glob functionality. On Unix we just use glob. +#ifdef _WIN32 +# include +# define sg_strchr ::_mbschr +# define sg_strrchr ::_mbsrchr +# define sg_strlen ::_mbslen +# if __STDC_WANT_SECURE_LIB__ +# define sg_strcpy_s(a,n,b) ::_mbscpy_s(a,n,b) +# else +# define sg_strcpy_s(a,n,b) ::_mbscpy(a,b) +# endif +# define sg_strcmp ::_mbscmp +# define sg_strcasecmp ::_mbsicmp +# define SOCHAR_T unsigned char +#else +# include +# include +# include +# include +# define MAX_PATH PATH_MAX +# define sg_strchr ::strchr +# define sg_strrchr ::strrchr +# define sg_strlen ::strlen +# define sg_strcpy_s(a,n,b) ::strcpy(a,b) +# define sg_strcmp ::strcmp +# define sg_strcasecmp ::strcasecmp +# define SOCHAR_T char +#endif + +#include +#include +#include + +// use assertions to test the input data +#ifdef _DEBUG +# ifdef _MSC_VER +# include +# define SG_ASSERT(b) _ASSERTE(b) +# else +# include +# define SG_ASSERT(b) assert(b) +# endif +#else +# define SG_ASSERT(b) +#endif + +/*! @brief String manipulation functions. */ +class SimpleGlobUtil +{ +public: + static const char * strchr(const char *s, char c) { + return (char *) sg_strchr((const SOCHAR_T *)s, c); + } + static const wchar_t * strchr(const wchar_t *s, wchar_t c) { + return ::wcschr(s, c); + } +#if SG_HAVE_ICU + static const UChar * strchr(const UChar *s, UChar c) { + return ::u_strchr(s, c); + } +#endif + + static const char * strrchr(const char *s, char c) { + return (char *) sg_strrchr((const SOCHAR_T *)s, c); + } + static const wchar_t * strrchr(const wchar_t *s, wchar_t c) { + return ::wcsrchr(s, c); + } +#if SG_HAVE_ICU + static const UChar * strrchr(const UChar *s, UChar c) { + return ::u_strrchr(s, c); + } +#endif + + // Note: char strlen returns number of bytes, not characters + static size_t strlen(const char *s) { return ::strlen(s); } + static size_t strlen(const wchar_t *s) { return ::wcslen(s); } +#if SG_HAVE_ICU + static size_t strlen(const UChar *s) { return ::u_strlen(s); } +#endif + + static void strcpy_s(char *dst, size_t n, const char *src) { + (void) n; + sg_strcpy_s((SOCHAR_T *)dst, n, (const SOCHAR_T *)src); + } + static void strcpy_s(wchar_t *dst, size_t n, const wchar_t *src) { +# if __STDC_WANT_SECURE_LIB__ + ::wcscpy_s(dst, n, src); +#else + (void) n; + ::wcscpy(dst, src); +#endif + } +#if SG_HAVE_ICU + static void strcpy_s(UChar *dst, size_t n, const UChar *src) { + ::u_strncpy(dst, src, n); + } +#endif + + static int strcmp(const char *s1, const char *s2) { + return sg_strcmp((const SOCHAR_T *)s1, (const SOCHAR_T *)s2); + } + static int strcmp(const wchar_t *s1, const wchar_t *s2) { + return ::wcscmp(s1, s2); + } +#if SG_HAVE_ICU + static int strcmp(const UChar *s1, const UChar *s2) { + return ::u_strcmp(s1, s2); + } +#endif + + static int strcasecmp(const char *s1, const char *s2) { + return sg_strcasecmp((const SOCHAR_T *)s1, (const SOCHAR_T *)s2); + } +#if _WIN32 + static int strcasecmp(const wchar_t *s1, const wchar_t *s2) { + return ::_wcsicmp(s1, s2); + } +#endif // _WIN32 +#if SG_HAVE_ICU + static int strcasecmp(const UChar *s1, const UChar *s2) { + return u_strcasecmp(s1, s2, 0); + } +#endif +}; + +enum SG_FileType { + SG_FILETYPE_INVALID, + SG_FILETYPE_FILE, + SG_FILETYPE_DIR +}; + +#ifdef _WIN32 + +#ifndef INVALID_FILE_ATTRIBUTES +# define INVALID_FILE_ATTRIBUTES ((DWORD)-1) +#endif + +#define SG_PATH_CHAR '\\' + +/*! @brief Windows glob implementation. */ +template +struct SimpleGlobBase +{ + SimpleGlobBase() : m_hFind(INVALID_HANDLE_VALUE) { } + + int FindFirstFileS(const char * a_pszFileSpec, unsigned int) { + m_hFind = FindFirstFileA(a_pszFileSpec, &m_oFindDataA); + if (m_hFind != INVALID_HANDLE_VALUE) { + return SG_SUCCESS; + } + DWORD dwErr = GetLastError(); + if (dwErr == ERROR_FILE_NOT_FOUND) { + return SG_ERR_NOMATCH; + } + return SG_ERR_FAILURE; + } + int FindFirstFileS(const wchar_t * a_pszFileSpec, unsigned int) { + m_hFind = FindFirstFileW(a_pszFileSpec, &m_oFindDataW); + if (m_hFind != INVALID_HANDLE_VALUE) { + return SG_SUCCESS; + } + DWORD dwErr = GetLastError(); + if (dwErr == ERROR_FILE_NOT_FOUND) { + return SG_ERR_NOMATCH; + } + return SG_ERR_FAILURE; + } + + bool FindNextFileS(char) { + return FindNextFileA(m_hFind, &m_oFindDataA) != FALSE; + } + bool FindNextFileS(wchar_t) { + return FindNextFileW(m_hFind, &m_oFindDataW) != FALSE; + } + + void FindDone() { + FindClose(m_hFind); + } + + const char * GetFileNameS(char) const { + return m_oFindDataA.cFileName; + } + const wchar_t * GetFileNameS(wchar_t) const { + return m_oFindDataW.cFileName; + } + + bool IsDirS(char) const { + return this->GetFileTypeS(m_oFindDataA.dwFileAttributes) == SG_FILETYPE_DIR; + } + bool IsDirS(wchar_t) const { + return this->GetFileTypeS(m_oFindDataW.dwFileAttributes) == SG_FILETYPE_DIR; + } + + SG_FileType GetFileTypeS(const char * a_pszPath) { + return this->GetFileTypeS(GetFileAttributesA(a_pszPath)); + } + SG_FileType GetFileTypeS(const wchar_t * a_pszPath) { + return this->GetFileTypeS(GetFileAttributesW(a_pszPath)); + } + SG_FileType GetFileTypeS(DWORD a_dwAttribs) const { + if (a_dwAttribs == INVALID_FILE_ATTRIBUTES) { + return SG_FILETYPE_INVALID; + } + if (a_dwAttribs & FILE_ATTRIBUTE_DIRECTORY) { + return SG_FILETYPE_DIR; + } + return SG_FILETYPE_FILE; + } + +private: + HANDLE m_hFind; + WIN32_FIND_DATAA m_oFindDataA; + WIN32_FIND_DATAW m_oFindDataW; +}; + +#else // !_WIN32 + +#define SG_PATH_CHAR '/' + +/*! @brief Unix glob implementation. */ +template +struct SimpleGlobBase +{ + SimpleGlobBase() { + memset(&m_glob, 0, sizeof(m_glob)); + m_uiCurr = (size_t)-1; + } + + ~SimpleGlobBase() { + globfree(&m_glob); + } + + void FilePrep() { + m_bIsDir = false; + size_t len = strlen(m_glob.gl_pathv[m_uiCurr]); + if (m_glob.gl_pathv[m_uiCurr][len-1] == '/') { + m_bIsDir = true; + m_glob.gl_pathv[m_uiCurr][len-1] = 0; + } + } + + int FindFirstFileS(const char * a_pszFileSpec, unsigned int a_uiFlags) { + int nFlags = GLOB_MARK | GLOB_NOSORT; + if (a_uiFlags & SG_GLOB_ERR) nFlags |= GLOB_ERR; + if (a_uiFlags & SG_GLOB_TILDE) nFlags |= GLOB_TILDE; + int rc = glob(a_pszFileSpec, nFlags, NULL, &m_glob); + if (rc == GLOB_NOSPACE) return SG_ERR_MEMORY; + if (rc == GLOB_ABORTED) return SG_ERR_FAILURE; + if (rc == GLOB_NOMATCH) return SG_ERR_NOMATCH; + m_uiCurr = 0; + FilePrep(); + return SG_SUCCESS; + } + +#if SG_HAVE_ICU + int FindFirstFileS(const UChar * a_pszFileSpec, unsigned int a_uiFlags) { + char buf[PATH_MAX] = { 0 }; + UErrorCode status = U_ZERO_ERROR; + u_strToUTF8(buf, sizeof(buf), NULL, a_pszFileSpec, -1, &status); + if (U_FAILURE(status)) return SG_ERR_FAILURE; + return this->FindFirstFileS(buf, a_uiFlags); + } +#endif + + bool FindNextFileS(char) { + SG_ASSERT(m_uiCurr != (size_t)-1); + if (++m_uiCurr >= m_glob.gl_pathc) { + return false; + } + FilePrep(); + return true; + } + +#if SG_HAVE_ICU + bool FindNextFileS(UChar) { + return this->FindNextFileS((char)0); + } +#endif + + void FindDone() { + globfree(&m_glob); + memset(&m_glob, 0, sizeof(m_glob)); + m_uiCurr = (size_t)-1; + } + + const char * GetFileNameS(char) const { + SG_ASSERT(m_uiCurr != (size_t)-1); + return m_glob.gl_pathv[m_uiCurr]; + } + +#if SG_HAVE_ICU + const UChar * GetFileNameS(UChar) const { + const char * pszFile = this->GetFileNameS((char)0); + if (!pszFile) return NULL; + UErrorCode status = U_ZERO_ERROR; + memset(m_szBuf, 0, sizeof(m_szBuf)); + u_strFromUTF8(m_szBuf, PATH_MAX, NULL, pszFile, -1, &status); + if (U_FAILURE(status)) return NULL; + return m_szBuf; + } +#endif + + bool IsDirS(char) const { + SG_ASSERT(m_uiCurr != (size_t)-1); + return m_bIsDir; + } + +#if SG_HAVE_ICU + bool IsDirS(UChar) const { + return this->IsDirS((char)0); + } +#endif + + SG_FileType GetFileTypeS(const char * a_pszPath) const { + struct stat sb; + if (0 != stat(a_pszPath, &sb)) { + return SG_FILETYPE_INVALID; + } + if (S_ISDIR(sb.st_mode)) { + return SG_FILETYPE_DIR; + } + if (S_ISREG(sb.st_mode)) { + return SG_FILETYPE_FILE; + } + return SG_FILETYPE_INVALID; + } + +#if SG_HAVE_ICU + SG_FileType GetFileTypeS(const UChar * a_pszPath) const { + char buf[PATH_MAX] = { 0 }; + UErrorCode status = U_ZERO_ERROR; + u_strToUTF8(buf, sizeof(buf), NULL, a_pszPath, -1, &status); + if (U_FAILURE(status)) return SG_FILETYPE_INVALID; + return this->GetFileTypeS(buf); + } +#endif + +private: + glob_t m_glob; + size_t m_uiCurr; + bool m_bIsDir; +#if SG_HAVE_ICU + mutable UChar m_szBuf[PATH_MAX]; +#endif +}; + +#endif // _WIN32 + +#endif // DOXYGEN + +// --------------------------------------------------------------------------- +// MAIN TEMPLATE CLASS +// --------------------------------------------------------------------------- + +/*! @brief Implementation of the SimpleGlob class */ +template +class CSimpleGlobTempl : private SimpleGlobBase +{ +public: + /*! @brief Initialize the class. + + @param a_uiFlags Combination of SG_GLOB flags. + @param a_nReservedSlots Number of slots in the argv array that + should be reserved. In the returned array these slots + argv[0] ... argv[a_nReservedSlots-1] will be left empty for + the caller to fill in. + */ + CSimpleGlobTempl(unsigned int a_uiFlags = 0, int a_nReservedSlots = 0); + + /*! @brief Deallocate all memory buffers. */ + ~CSimpleGlobTempl(); + + /*! @brief Initialize (or re-initialize) the class in preparation for + adding new filespecs. + + All existing files are cleared. Note that allocated memory is only + deallocated at object destruction. + + @param a_uiFlags Combination of SG_GLOB flags. + @param a_nReservedSlots Number of slots in the argv array that + should be reserved. In the returned array these slots + argv[0] ... argv[a_nReservedSlots-1] will be left empty for + the caller to fill in. + */ + int Init(unsigned int a_uiFlags = 0, int a_nReservedSlots = 0); + + /*! @brief Add a new filespec to the glob. + + The filesystem will be immediately scanned for all matching files and + directories and they will be added to the glob. + + @param a_pszFileSpec Filespec to add to the glob. + + @return SG_SUCCESS Matching files were added to the glob. + @return SG_ERR_NOMATCH Nothing matched the pattern. To ignore this + error compare return value to >= SG_SUCCESS. + @return SG_ERR_MEMORY Out of memory failure. + @return SG_ERR_FAILURE General failure. + */ + int Add(const SOCHAR *a_pszFileSpec); + + /*! @brief Add an array of filespec to the glob. + + The filesystem will be immediately scanned for all matching files and + directories in each filespec and they will be added to the glob. + + @param a_nCount Number of filespec in the array. + @param a_rgpszFileSpec Array of filespec to add to the glob. + + @return SG_SUCCESS Matching files were added to the glob. + @return SG_ERR_NOMATCH Nothing matched the pattern. To ignore this + error compare return value to >= SG_SUCCESS. + @return SG_ERR_MEMORY Out of memory failure. + @return SG_ERR_FAILURE General failure. + */ + int Add(int a_nCount, const SOCHAR * const * a_rgpszFileSpec); + + /*! @brief Return the number of files in the argv array. + */ + inline int FileCount() const { return m_nArgsLen; } + + /*! @brief Return the full argv array. */ + inline SOCHAR ** Files() { + SetArgvArrayType(POINTERS); + return m_rgpArgs; + } + + /*! @brief Return the a single file. */ + inline SOCHAR * File(int n) { + SG_ASSERT(n >= 0 && n < m_nArgsLen); + return Files()[n]; + } + +private: + CSimpleGlobTempl(const CSimpleGlobTempl &); // disabled + CSimpleGlobTempl & operator=(const CSimpleGlobTempl &); // disabled + + /*! @brief The argv array has it's members stored as either an offset into + the string buffer, or as pointers to their string in the buffer. The + offsets are used because if the string buffer is dynamically resized, + all pointers into that buffer would become invalid. + */ + enum ARG_ARRAY_TYPE { OFFSETS, POINTERS }; + + /*! @brief Change the type of data stored in the argv array. */ + void SetArgvArrayType(ARG_ARRAY_TYPE a_nNewType); + + /*! @brief Add a filename to the array if it passes all requirements. */ + int AppendName(const SOCHAR *a_pszFileName, bool a_bIsDir); + + /*! @brief Grow the argv array to the required size. */ + bool GrowArgvArray(int a_nNewLen); + + /*! @brief Grow the string buffer to the required size. */ + bool GrowStringBuffer(size_t a_uiMinSize); + + /*! @brief Compare two (possible NULL) strings */ + static int fileSortCompare(const void *a1, const void *a2); + +private: + unsigned int m_uiFlags; + ARG_ARRAY_TYPE m_nArgArrayType; //!< argv is indexes or pointers + SOCHAR ** m_rgpArgs; //!< argv + int m_nReservedSlots; //!< # client slots in argv array + int m_nArgsSize; //!< allocated size of array + int m_nArgsLen; //!< used length + SOCHAR * m_pBuffer; //!< argv string buffer + size_t m_uiBufferSize; //!< allocated size of buffer + size_t m_uiBufferLen; //!< used length of buffer + SOCHAR m_szPathPrefix[MAX_PATH]; //!< wildcard path prefix +}; + +// --------------------------------------------------------------------------- +// IMPLEMENTATION +// --------------------------------------------------------------------------- + +template +CSimpleGlobTempl::CSimpleGlobTempl( + unsigned int a_uiFlags, + int a_nReservedSlots + ) +{ + m_rgpArgs = NULL; + m_nArgsSize = 0; + m_pBuffer = NULL; + m_uiBufferSize = 0; + + Init(a_uiFlags, a_nReservedSlots); +} + +template +CSimpleGlobTempl::~CSimpleGlobTempl() +{ + if (m_rgpArgs) free(m_rgpArgs); + if (m_pBuffer) free(m_pBuffer); +} + +template +int +CSimpleGlobTempl::Init( + unsigned int a_uiFlags, + int a_nReservedSlots + ) +{ + m_nArgArrayType = POINTERS; + m_uiFlags = a_uiFlags; + m_nArgsLen = a_nReservedSlots; + m_nReservedSlots = a_nReservedSlots; + m_uiBufferLen = 0; + + if (m_nReservedSlots > 0) { + if (!GrowArgvArray(m_nReservedSlots)) { + return SG_ERR_MEMORY; + } + for (int n = 0; n < m_nReservedSlots; ++n) { + m_rgpArgs[n] = NULL; + } + } + + return SG_SUCCESS; +} + +template +int +CSimpleGlobTempl::Add( + const SOCHAR *a_pszFileSpec + ) +{ +#ifdef _WIN32 + // Windows FindFirst/FindNext recognizes forward slash as the same as + // backward slash and follows the directories. We need to do the same + // when calculating the prefix and when we have no wildcards. + SOCHAR szFileSpec[MAX_PATH]; + SimpleGlobUtil::strcpy_s(szFileSpec, MAX_PATH, a_pszFileSpec); + const SOCHAR * pszPath = SimpleGlobUtil::strchr(szFileSpec, '/'); + while (pszPath) { + szFileSpec[pszPath - szFileSpec] = SG_PATH_CHAR; + pszPath = SimpleGlobUtil::strchr(pszPath + 1, '/'); + } + a_pszFileSpec = szFileSpec; +#endif + + // if this doesn't contain wildcards then we can just add it directly + m_szPathPrefix[0] = 0; + if (!SimpleGlobUtil::strchr(a_pszFileSpec, '*') && + !SimpleGlobUtil::strchr(a_pszFileSpec, '?')) + { + SG_FileType nType = this->GetFileTypeS(a_pszFileSpec); + if (nType == SG_FILETYPE_INVALID) { + if (m_uiFlags & SG_GLOB_NOCHECK) { + return AppendName(a_pszFileSpec, false); + } + return SG_ERR_NOMATCH; + } + return AppendName(a_pszFileSpec, nType == SG_FILETYPE_DIR); + } + +#ifdef _WIN32 + // Windows doesn't return the directory with the filename, so we need to + // extract the path from the search string ourselves and prefix it to the + // filename we get back. + const SOCHAR * pszFilename = + SimpleGlobUtil::strrchr(a_pszFileSpec, SG_PATH_CHAR); + if (pszFilename) { + SimpleGlobUtil::strcpy_s(m_szPathPrefix, MAX_PATH, a_pszFileSpec); + m_szPathPrefix[pszFilename - a_pszFileSpec + 1] = 0; + } +#endif + + // search for the first match on the file + int rc = this->FindFirstFileS(a_pszFileSpec, m_uiFlags); + if (rc != SG_SUCCESS) { + if (rc == SG_ERR_NOMATCH && (m_uiFlags & SG_GLOB_NOCHECK)) { + int ok = AppendName(a_pszFileSpec, false); + if (ok != SG_SUCCESS) rc = ok; + } + return rc; + } + + // add it and find all subsequent matches + int nError, nStartLen = m_nArgsLen; + bool bSuccess; + do { + nError = AppendName(this->GetFileNameS((SOCHAR)0), this->IsDirS((SOCHAR)0)); + bSuccess = this->FindNextFileS((SOCHAR)0); + } + while (nError == SG_SUCCESS && bSuccess); + SimpleGlobBase::FindDone(); + + // sort these files if required + if (m_nArgsLen > nStartLen && !(m_uiFlags & SG_GLOB_NOSORT)) { + if (m_uiFlags & SG_GLOB_FULLSORT) { + nStartLen = m_nReservedSlots; + } + SetArgvArrayType(POINTERS); + qsort( + m_rgpArgs + nStartLen, + m_nArgsLen - nStartLen, + sizeof(m_rgpArgs[0]), fileSortCompare); + } + + return nError; +} + +template +int +CSimpleGlobTempl::Add( + int a_nCount, + const SOCHAR * const * a_rgpszFileSpec + ) +{ + int nResult; + for (int n = 0; n < a_nCount; ++n) { + nResult = Add(a_rgpszFileSpec[n]); + if (nResult != SG_SUCCESS) { + return nResult; + } + } + return SG_SUCCESS; +} + +template +int +CSimpleGlobTempl::AppendName( + const SOCHAR * a_pszFileName, + bool a_bIsDir + ) +{ + // we need the argv array as offsets in case we resize it + SetArgvArrayType(OFFSETS); + + // check for special cases which cause us to ignore this entry + if ((m_uiFlags & SG_GLOB_ONLYDIR) && !a_bIsDir) { + return SG_SUCCESS; + } + if ((m_uiFlags & SG_GLOB_ONLYFILE) && a_bIsDir) { + return SG_SUCCESS; + } + if ((m_uiFlags & SG_GLOB_NODOT) && a_bIsDir) { + if (a_pszFileName[0] == '.') { + if (a_pszFileName[1] == '\0') { + return SG_SUCCESS; + } + if (a_pszFileName[1] == '.' && a_pszFileName[2] == '\0') { + return SG_SUCCESS; + } + } + } + + // ensure that we have enough room in the argv array + if (!GrowArgvArray(m_nArgsLen + 1)) { + return SG_ERR_MEMORY; + } + + // ensure that we have enough room in the string buffer (+1 for null) + size_t uiPrefixLen = SimpleGlobUtil::strlen(m_szPathPrefix); + size_t uiLen = uiPrefixLen + SimpleGlobUtil::strlen(a_pszFileName) + 1; + if (a_bIsDir && (m_uiFlags & SG_GLOB_MARK) == SG_GLOB_MARK) { + ++uiLen; // need space for the backslash + } + if (!GrowStringBuffer(m_uiBufferLen + uiLen)) { + return SG_ERR_MEMORY; + } + + // add this entry. m_uiBufferLen is offset from beginning of buffer. + m_rgpArgs[m_nArgsLen++] = (SOCHAR*)m_uiBufferLen; + SimpleGlobUtil::strcpy_s(m_pBuffer + m_uiBufferLen, + m_uiBufferSize - m_uiBufferLen, m_szPathPrefix); + SimpleGlobUtil::strcpy_s(m_pBuffer + m_uiBufferLen + uiPrefixLen, + m_uiBufferSize - m_uiBufferLen - uiPrefixLen, a_pszFileName); + m_uiBufferLen += uiLen; + + // add the directory slash if desired + if (a_bIsDir && (m_uiFlags & SG_GLOB_MARK) == SG_GLOB_MARK) { + const static SOCHAR szDirSlash[] = { SG_PATH_CHAR, 0 }; + SimpleGlobUtil::strcpy_s(m_pBuffer + m_uiBufferLen - 2, + m_uiBufferSize - (m_uiBufferLen - 2), szDirSlash); + } + + return SG_SUCCESS; +} + +template +void +CSimpleGlobTempl::SetArgvArrayType( + ARG_ARRAY_TYPE a_nNewType + ) +{ + if (m_nArgArrayType == a_nNewType) return; + if (a_nNewType == POINTERS) { + SG_ASSERT(m_nArgArrayType == OFFSETS); + for (int n = 0; n < m_nArgsLen; ++n) { + m_rgpArgs[n] = (m_rgpArgs[n] == (SOCHAR*)-1) ? + NULL : m_pBuffer + (size_t) m_rgpArgs[n]; + } + } + else { + SG_ASSERT(a_nNewType == OFFSETS); + SG_ASSERT(m_nArgArrayType == POINTERS); + for (int n = 0; n < m_nArgsLen; ++n) { + m_rgpArgs[n] = (m_rgpArgs[n] == NULL) ? + (SOCHAR*) -1 : (SOCHAR*) (m_rgpArgs[n] - m_pBuffer); + } + } + m_nArgArrayType = a_nNewType; +} + +template +bool +CSimpleGlobTempl::GrowArgvArray( + int a_nNewLen + ) +{ + if (a_nNewLen >= m_nArgsSize) { + static const int SG_ARGV_INITIAL_SIZE = 32; + int nNewSize = (m_nArgsSize > 0) ? + m_nArgsSize * 2 : SG_ARGV_INITIAL_SIZE; + while (a_nNewLen >= nNewSize) { + nNewSize *= 2; + } + void * pNewBuffer = realloc(m_rgpArgs, nNewSize * sizeof(SOCHAR*)); + if (!pNewBuffer) return false; + m_nArgsSize = nNewSize; + m_rgpArgs = (SOCHAR**) pNewBuffer; + } + return true; +} + +template +bool +CSimpleGlobTempl::GrowStringBuffer( + size_t a_uiMinSize + ) +{ + if (a_uiMinSize >= m_uiBufferSize) { + static const int SG_BUFFER_INITIAL_SIZE = 1024; + size_t uiNewSize = (m_uiBufferSize > 0) ? + m_uiBufferSize * 2 : SG_BUFFER_INITIAL_SIZE; + while (a_uiMinSize >= uiNewSize) { + uiNewSize *= 2; + } + void * pNewBuffer = realloc(m_pBuffer, uiNewSize * sizeof(SOCHAR)); + if (!pNewBuffer) return false; + m_uiBufferSize = uiNewSize; + m_pBuffer = (SOCHAR*) pNewBuffer; + } + return true; +} + +template +int +CSimpleGlobTempl::fileSortCompare( + const void *a1, + const void *a2 + ) +{ + const SOCHAR * s1 = *(const SOCHAR **)a1; + const SOCHAR * s2 = *(const SOCHAR **)a2; + if (s1 && s2) { + return SimpleGlobUtil::strcasecmp(s1, s2); + } + // NULL sorts first + return s1 == s2 ? 0 : (s1 ? 1 : -1); +} + +// --------------------------------------------------------------------------- +// TYPE DEFINITIONS +// --------------------------------------------------------------------------- + +/*! @brief ASCII/MBCS version of CSimpleGlob */ +typedef CSimpleGlobTempl CSimpleGlobA; + +/*! @brief wchar_t version of CSimpleGlob */ +typedef CSimpleGlobTempl CSimpleGlobW; + +#if SG_HAVE_ICU +/*! @brief UChar version of CSimpleGlob */ +typedef CSimpleGlobTempl CSimpleGlobU; +#endif + +#ifdef _UNICODE +/*! @brief TCHAR version dependent on if _UNICODE is defined */ +# if SG_HAVE_ICU +# define CSimpleGlob CSimpleGlobU +# else +# define CSimpleGlob CSimpleGlobW +# endif +#else +/*! @brief TCHAR version dependent on if _UNICODE is defined */ +# define CSimpleGlob CSimpleGlobA +#endif + +#endif // INCLUDED_SimpleGlob diff --git a/external/header_only/SimpleOpt.h b/external/header_only/SimpleOpt.h new file mode 100644 index 0000000000..ed27459fdc --- /dev/null +++ b/external/header_only/SimpleOpt.h @@ -0,0 +1,1046 @@ +/*! @file SimpleOpt.h + + @version 3.6 + + @brief A cross-platform command line library which can parse almost any + of the standard command line formats in use today. It is designed + explicitly to be portable to any platform and has been tested on Windows + and Linux. See CSimpleOptTempl for the class definition. + + @section features FEATURES + - MIT Licence allows free use in all software (including GPL + and commercial) + - multi-platform (Windows 95/98/ME/NT/2K/XP, Linux, Unix) + - supports all lengths of option names: + +
- + switch character only (e.g. use stdin for input) +
-o + short (single character) +
-long + long (multiple character, single switch character) +
--longer + long (multiple character, multiple switch characters) +
+ - supports all types of arguments for options: + +
--option + short/long option flag (no argument) +
--option ARG + short/long option with separate required argument +
--option=ARG + short/long option with combined required argument +
--option[=ARG] + short/long option with combined optional argument +
-oARG + short option with combined required argument +
-o[ARG] + short option with combined optional argument +
+ - supports options with multiple or variable numbers of arguments: + +
--multi ARG1 ARG2 + Multiple arguments +
--multi N ARG-1 ARG-2 ... ARG-N + Variable number of arguments +
+ - supports case-insensitive option matching on short, long and/or + word arguments. + - supports options which do not use a switch character. i.e. a special + word which is construed as an option. + e.g. "foo.exe open /directory/file.txt" + - supports clumping of multiple short options (no arguments) in a string + e.g. "foo.exe -abcdef file1" <==> "foo.exe -a -b -c -d -e -f file1" + - automatic recognition of a single slash as equivalent to a single + hyphen on Windows, e.g. "/f FILE" is equivalent to "-f FILE". + - file arguments can appear anywhere in the argument list: + "foo.exe file1.txt -a ARG file2.txt --flag file3.txt file4.txt" + files will be returned to the application in the same order they were + supplied on the command line + - short-circuit option matching: "--man" will match "--mandate" + invalid options can be handled while continuing to parse the command + line valid options list can be changed dynamically during command line + processing, i.e. accept different options depending on an option + supplied earlier in the command line. + - implemented with only a single C++ header file + - optionally use no C runtime or OS functions + - char, wchar_t and Windows TCHAR in the same program + - complete working examples included + - compiles cleanly at warning level 4 (Windows/VC.NET 2003), warning + level 3 (Windows/VC6) and -Wall (Linux/gcc) + + @section usage USAGE + The SimpleOpt class is used by following these steps: +
    +
  1. Include the SimpleOpt.h header file +
    +        \#include "SimpleOpt.h"
    +        
    +
  2. Define an array of valid options for your program. +
    +@link CSimpleOptTempl::SOption CSimpleOpt::SOption @endlink g_rgOptions[] = {
    +    { OPT_FLAG, _T("-a"),     SO_NONE    }, // "-a"
    +    { OPT_FLAG, _T("-b"),     SO_NONE    }, // "-b"
    +    { OPT_ARG,  _T("-f"),     SO_REQ_SEP }, // "-f ARG"
    +    { OPT_HELP, _T("-?"),     SO_NONE    }, // "-?"
    +    { OPT_HELP, _T("--help"), SO_NONE    }, // "--help"
    +    SO_END_OF_OPTIONS                       // END
    +};
    +
    + Note that all options must start with a hyphen even if the slash will + be accepted. This is because the slash character is automatically + converted into a hyphen to test against the list of options. + For example, the following line matches both "-?" and "/?" + (on Windows). +
    +    { OPT_HELP, _T("-?"),     SO_NONE    }, // "-?"
    +
    +
  3. Instantiate a CSimpleOpt object supplying argc, argv and the option + table +
    +@link CSimpleOptTempl CSimpleOpt @endlink args(argc, argv, g_rgOptions);
    +
    +
  4. Process the arguments by calling Next() until it returns false. + On each call, first check for an error by calling LastError(), then + either handle the error or process the argument. +
    +while (args.Next()) {
    +    if (args.LastError() == SO_SUCCESS) {
    +        handle option: use OptionId(), OptionText() and OptionArg()
    +    }
    +    else {
    +        handle error: see ESOError enums
    +    }
    +}
    +
    +
  5. Process all non-option arguments with File(), Files() and FileCount() +
    +ShowFiles(args.FileCount(), args.Files());
    +
    +
+ + @section notes NOTES + - In MBCS mode, this library is guaranteed to work correctly only when + all option names use only ASCII characters. + - Note that if case-insensitive matching is being used then the first + matching option in the argument list will be returned. + + @section licence MIT LICENCE +
+    The licence text below is the boilerplate "MIT Licence" used from:
+    http://www.opensource.org/licenses/mit-license.php
+
+    Copyright (c) 2006-2013, Brodie Thiesfield
+
+    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.
+
+*/ + +/*! @mainpage + + +
Library SimpleOpt +
Author Brodie Thiesfield [code at jellycan dot com] +
Source http://code.jellycan.com/simpleopt/ +
+ + @section SimpleOpt SimpleOpt + + A cross-platform library providing a simple method to parse almost any of + the standard command-line formats in use today. + + See the @link SimpleOpt.h SimpleOpt @endlink documentation for full + details. + + @section SimpleGlob SimpleGlob + + A cross-platform file globbing library providing the ability to + expand wildcards in command-line arguments to a list of all matching + files. + + See the @link SimpleGlob.h SimpleGlob @endlink documentation for full + details. +*/ + +#ifndef INCLUDED_SimpleOpt +#define INCLUDED_SimpleOpt + +// Default the max arguments to a fixed value. If you want to be able to +// handle any number of arguments, then predefine this to 0 and it will +// use an internal dynamically allocated buffer instead. +#ifdef SO_MAX_ARGS +# define SO_STATICBUF SO_MAX_ARGS +#else +# include // malloc, free +# include // memcpy +# define SO_STATICBUF 50 +#endif + +//! Error values +typedef enum _ESOError +{ + //! No error + SO_SUCCESS = 0, + + /*! It looks like an option (it starts with a switch character), but + it isn't registered in the option table. */ + SO_OPT_INVALID = -1, + + /*! Multiple options matched the supplied option text. + Only returned when NOT using SO_O_EXACT. */ + SO_OPT_MULTIPLE = -2, + + /*! Option doesn't take an argument, but a combined argument was + supplied. */ + SO_ARG_INVALID = -3, + + /*! SO_REQ_CMB style-argument was supplied to a SO_REQ_SEP option + Only returned when using SO_O_PEDANTIC. */ + SO_ARG_INVALID_TYPE = -4, + + //! Required argument was not supplied + SO_ARG_MISSING = -5, + + /*! Option argument looks like another option. + Only returned when NOT using SO_O_NOERR. */ + SO_ARG_INVALID_DATA = -6 +} ESOError; + +//! Option flags +enum _ESOFlags +{ + /*! Disallow partial matching of option names */ + SO_O_EXACT = 0x0001, + + /*! Disallow use of slash as an option marker on Windows. + Un*x only ever recognizes a hyphen. */ + SO_O_NOSLASH = 0x0002, + + /*! Permit arguments on single letter options with no equals sign. + e.g. -oARG or -o[ARG] */ + SO_O_SHORTARG = 0x0004, + + /*! Permit single character options to be clumped into a single + option string. e.g. "-a -b -c" <==> "-abc" */ + SO_O_CLUMP = 0x0008, + + /*! Process the entire argv array for options, including the + argv[0] entry. */ + SO_O_USEALL = 0x0010, + + /*! Do not generate an error for invalid options. errors for missing + arguments will still be generated. invalid options will be + treated as files. invalid options in clumps will be silently + ignored. */ + SO_O_NOERR = 0x0020, + + /*! Validate argument type pedantically. Return an error when a + separated argument "-opt arg" is supplied by the user as a + combined argument "-opt=arg". By default this is not considered + an error. */ + SO_O_PEDANTIC = 0x0040, + + /*! Case-insensitive comparisons for short arguments */ + SO_O_ICASE_SHORT = 0x0100, + + /*! Case-insensitive comparisons for long arguments */ + SO_O_ICASE_LONG = 0x0200, + + /*! Case-insensitive comparisons for word arguments + i.e. arguments without any hyphens at the start. */ + SO_O_ICASE_WORD = 0x0400, + + /*! Case-insensitive comparisons for all arg types */ + SO_O_ICASE = 0x0700 +}; + +/*! Types of arguments that options may have. Note that some of the _ESOFlags + are not compatible with all argument types. SO_O_SHORTARG requires that + relevant options use either SO_REQ_CMB or SO_OPT. SO_O_CLUMP requires + that relevant options use only SO_NONE. + */ +typedef enum _ESOArgType { + /*! No argument. Just the option flags. + e.g. -o --opt */ + SO_NONE, + + /*! Required separate argument. + e.g. -o ARG --opt ARG */ + SO_REQ_SEP, + + /*! Required combined argument. + e.g. -oARG -o=ARG --opt=ARG */ + SO_REQ_CMB, + + /*! Optional combined argument. + e.g. -o[ARG] -o[=ARG] --opt[=ARG] */ + SO_OPT, + + /*! Multiple separate arguments. The actual number of arguments is + determined programatically at the time the argument is processed. + e.g. -o N ARG1 ARG2 ... ARGN --opt N ARG1 ARG2 ... ARGN */ + SO_MULTI +} ESOArgType; + +//! this option definition must be the last entry in the table +#define SO_END_OF_OPTIONS { -1, NULL, SO_NONE } + +#ifdef _DEBUG +# ifdef _MSC_VER +# include +# define SO_ASSERT(b) _ASSERTE(b) +# else +# include +# define SO_ASSERT(b) assert(b) +# endif +#else +# define SO_ASSERT(b) //!< assertion used to test input data +#endif + +// --------------------------------------------------------------------------- +// MAIN TEMPLATE CLASS +// --------------------------------------------------------------------------- + +/*! @brief Implementation of the SimpleOpt class */ +template +class CSimpleOptTempl +{ +public: + /*! @brief Structure used to define all known options. */ + struct SOption { + /*! ID to return for this flag. Optional but must be >= 0 */ + int nId; + + /*! arg string to search for, e.g. "open", "-", "-f", "--file" + Note that on Windows the slash option marker will be converted + to a hyphen so that "-f" will also match "/f". */ + const SOCHAR * pszArg; + + /*! type of argument accepted by this option */ + ESOArgType nArgType; + }; + + /*! @brief Initialize the class. Init() must be called later. */ + CSimpleOptTempl() + : m_rgShuffleBuf(NULL) + { + Init(0, NULL, NULL, 0); + } + + /*! @brief Initialize the class in preparation for use. */ + CSimpleOptTempl( + int argc, + SOCHAR * argv[], + const SOption * a_rgOptions, + int a_nFlags = 0 + ) + : m_rgShuffleBuf(NULL) + { + Init(argc, argv, a_rgOptions, a_nFlags); + } + +#ifndef SO_MAX_ARGS + /*! @brief Deallocate any allocated memory. */ + ~CSimpleOptTempl() { if (m_rgShuffleBuf) free(m_rgShuffleBuf); } +#endif + + /*! @brief Initialize the class in preparation for calling Next. + + The table of options pointed to by a_rgOptions does not need to be + valid at the time that Init() is called. However on every call to + Next() the table pointed to must be a valid options table with the + last valid entry set to SO_END_OF_OPTIONS. + + NOTE: the array pointed to by a_argv will be modified by this + class and must not be used or modified outside of member calls to + this class. + + @param a_argc Argument array size + @param a_argv Argument array + @param a_rgOptions Valid option array + @param a_nFlags Optional flags to modify the processing of + the arguments + + @return true Successful + @return false if SO_MAX_ARGC > 0: Too many arguments + if SO_MAX_ARGC == 0: Memory allocation failure + */ + bool Init( + int a_argc, + SOCHAR * a_argv[], + const SOption * a_rgOptions, + int a_nFlags = 0 + ); + + /*! @brief Change the current options table during option parsing. + + @param a_rgOptions Valid option array + */ + inline void SetOptions(const SOption * a_rgOptions) { + m_rgOptions = a_rgOptions; + } + + /*! @brief Change the current flags during option parsing. + + Note that changing the SO_O_USEALL flag here will have no affect. + It must be set using Init() or the constructor. + + @param a_nFlags Flags to modify the processing of the arguments + */ + inline void SetFlags(int a_nFlags) { m_nFlags = a_nFlags; } + + /*! @brief Query if a particular flag is set */ + inline bool HasFlag(int a_nFlag) const { + return (m_nFlags & a_nFlag) == a_nFlag; + } + + /*! @brief Advance to the next option if available. + + When all options have been processed it will return false. When true + has been returned, you must check for an invalid or unrecognized + option using the LastError() method. This will be return an error + value other than SO_SUCCESS on an error. All standard data + (e.g. OptionText(), OptionArg(), OptionId(), etc) will be available + depending on the error. + + After all options have been processed, the remaining files from the + command line can be processed in same order as they were passed to + the program. + + @return true option or error available for processing + @return false all options have been processed + */ + bool Next(); + + /*! Stops processing of the command line and returns all remaining + arguments as files. The next call to Next() will return false. + */ + void Stop(); + + /*! @brief Return the last error that occurred. + + This function must always be called before processing the current + option. This function is available only when Next() has returned true. + */ + inline ESOError LastError() const { return m_nLastError; } + + /*! @brief Return the nId value from the options array for the current + option. + + This function is available only when Next() has returned true. + */ + inline int OptionId() const { return m_nOptionId; } + + /*! @brief Return the pszArg from the options array for the current + option. + + This function is available only when Next() has returned true. + */ + inline const SOCHAR * OptionText() const { return m_pszOptionText; } + + /*! @brief Return the argument for the current option where one exists. + + If there is no argument for the option, this will return NULL. + This function is available only when Next() has returned true. + */ + inline SOCHAR * OptionArg() const { return m_pszOptionArg; } + + /*! @brief Validate and return the desired number of arguments. + + This is only valid when OptionId() has return the ID of an option + that is registered as SO_MULTI. It may be called multiple times + each time returning the desired number of arguments. Previously + returned argument pointers are remain valid. + + If an error occurs during processing, NULL will be returned and + the error will be available via LastError(). + + @param n Number of arguments to return. + */ + SOCHAR ** MultiArg(int n); + + /*! @brief Returned the number of entries in the Files() array. + + After Next() has returned false, this will be the list of files (or + otherwise unprocessed arguments). + */ + inline int FileCount() const { return m_argc - m_nLastArg; } + + /*! @brief Return the specified file argument. + + @param n Index of the file to return. This must be between 0 + and FileCount() - 1; + */ + inline SOCHAR * File(int n) const { + SO_ASSERT(n >= 0 && n < FileCount()); + return m_argv[m_nLastArg + n]; + } + + /*! @brief Return the array of files. */ + inline SOCHAR ** Files() const { return &m_argv[m_nLastArg]; } + +private: + CSimpleOptTempl(const CSimpleOptTempl &); // disabled + CSimpleOptTempl & operator=(const CSimpleOptTempl &); // disabled + + SOCHAR PrepareArg(SOCHAR * a_pszString) const; + bool NextClumped(); + void ShuffleArg(int a_nStartIdx, int a_nCount); + int LookupOption(const SOCHAR * a_pszOption) const; + int CalcMatch(const SOCHAR *a_pszSource, const SOCHAR *a_pszTest) const; + + // Find the '=' character within a string. + inline SOCHAR * FindEquals(SOCHAR *s) const { + while (*s && *s != (SOCHAR)'=') ++s; + return *s ? s : NULL; + } + bool IsEqual(SOCHAR a_cLeft, SOCHAR a_cRight, int a_nArgType) const; + + inline void Copy(SOCHAR ** ppDst, SOCHAR ** ppSrc, int nCount) const { +#ifdef SO_MAX_ARGS + // keep our promise of no CLIB usage + while (nCount-- > 0) *ppDst++ = *ppSrc++; +#else + memcpy(ppDst, ppSrc, nCount * sizeof(SOCHAR*)); +#endif + } + +private: + const SOption * m_rgOptions; //!< pointer to options table + int m_nFlags; //!< flags + int m_nOptionIdx; //!< current argv option index + int m_nOptionId; //!< id of current option (-1 = invalid) + int m_nNextOption; //!< index of next option + int m_nLastArg; //!< last argument, after this are files + int m_argc; //!< argc to process + SOCHAR ** m_argv; //!< argv + const SOCHAR * m_pszOptionText; //!< curr option text, e.g. "-f" + SOCHAR * m_pszOptionArg; //!< curr option arg, e.g. "c:\file.txt" + SOCHAR * m_pszClump; //!< clumped single character options + SOCHAR m_szShort[3]; //!< temp for clump and combined args + ESOError m_nLastError; //!< error status from the last call + SOCHAR ** m_rgShuffleBuf; //!< shuffle buffer for large argc +}; + +// --------------------------------------------------------------------------- +// IMPLEMENTATION +// --------------------------------------------------------------------------- + +template +bool +CSimpleOptTempl::Init( + int a_argc, + SOCHAR * a_argv[], + const SOption * a_rgOptions, + int a_nFlags + ) +{ + m_argc = a_argc; + m_nLastArg = a_argc; + m_argv = a_argv; + m_rgOptions = a_rgOptions; + m_nLastError = SO_SUCCESS; + m_nOptionIdx = 0; + m_nOptionId = -1; + m_pszOptionText = NULL; + m_pszOptionArg = NULL; + m_nNextOption = (a_nFlags & SO_O_USEALL) ? 0 : 1; + m_szShort[0] = (SOCHAR)'-'; + m_szShort[2] = (SOCHAR)'\0'; + m_nFlags = a_nFlags; + m_pszClump = NULL; + +#ifdef SO_MAX_ARGS + if (m_argc > SO_MAX_ARGS) { + m_nLastError = SO_ARG_INVALID_DATA; + m_nLastArg = 0; + return false; + } +#else + if (m_rgShuffleBuf) { + free(m_rgShuffleBuf); + } + if (m_argc > SO_STATICBUF) { + m_rgShuffleBuf = (SOCHAR**) malloc(sizeof(SOCHAR*) * m_argc); + if (!m_rgShuffleBuf) { + return false; + } + } +#endif + + return true; +} + +template +bool +CSimpleOptTempl::Next() +{ +#ifdef SO_MAX_ARGS + if (m_argc > SO_MAX_ARGS) { + SO_ASSERT(!"Too many args! Check the return value of Init()!"); + return false; + } +#endif + + // process a clumped option string if appropriate + if (m_pszClump && *m_pszClump) { + // silently discard invalid clumped option + bool bIsValid = NextClumped(); + while (*m_pszClump && !bIsValid && HasFlag(SO_O_NOERR)) { + bIsValid = NextClumped(); + } + + // return this option if valid or we are returning errors + if (bIsValid || !HasFlag(SO_O_NOERR)) { + return true; + } + } + SO_ASSERT(!m_pszClump || !*m_pszClump); + m_pszClump = NULL; + + // init for the next option + m_nOptionIdx = m_nNextOption; + m_nOptionId = -1; + m_pszOptionText = NULL; + m_pszOptionArg = NULL; + m_nLastError = SO_SUCCESS; + + // find the next option + SOCHAR cFirst; + int nTableIdx = -1; + int nOptIdx = m_nOptionIdx; + while (nTableIdx < 0 && nOptIdx < m_nLastArg) { + SOCHAR * pszArg = m_argv[nOptIdx]; + m_pszOptionArg = NULL; + + // find this option in the options table + cFirst = PrepareArg(pszArg); + if (pszArg[0] == (SOCHAR)'-') { + // find any combined argument string and remove equals sign + m_pszOptionArg = FindEquals(pszArg); + if (m_pszOptionArg) { + *m_pszOptionArg++ = (SOCHAR)'\0'; + } + } + nTableIdx = LookupOption(pszArg); + + // if we didn't find this option but if it is a short form + // option then we try the alternative forms + if (nTableIdx < 0 + && !m_pszOptionArg + && pszArg[0] == (SOCHAR)'-' + && pszArg[1] + && pszArg[1] != (SOCHAR)'-' + && pszArg[2]) + { + // test for a short-form with argument if appropriate + if (HasFlag(SO_O_SHORTARG)) { + m_szShort[1] = pszArg[1]; + int nIdx = LookupOption(m_szShort); + if (nIdx >= 0 + && (m_rgOptions[nIdx].nArgType == SO_REQ_CMB + || m_rgOptions[nIdx].nArgType == SO_OPT)) + { + m_pszOptionArg = &pszArg[2]; + pszArg = m_szShort; + nTableIdx = nIdx; + } + } + + // test for a clumped short-form option string and we didn't + // match on the short-form argument above + if (nTableIdx < 0 && HasFlag(SO_O_CLUMP)) { + m_pszClump = &pszArg[1]; + ++m_nNextOption; + if (nOptIdx > m_nOptionIdx) { + ShuffleArg(m_nOptionIdx, nOptIdx - m_nOptionIdx); + } + return Next(); + } + } + + // The option wasn't found. If it starts with a switch character + // and we are not suppressing errors for invalid options then it + // is reported as an error, otherwise it is data. + if (nTableIdx < 0) { + if (!HasFlag(SO_O_NOERR) && pszArg[0] == (SOCHAR)'-') { + m_pszOptionText = pszArg; + break; + } + + pszArg[0] = cFirst; + ++nOptIdx; + if (m_pszOptionArg) { + *(--m_pszOptionArg) = (SOCHAR)'='; + } + } + } + + // end of options + if (nOptIdx >= m_nLastArg) { + if (nOptIdx > m_nOptionIdx) { + ShuffleArg(m_nOptionIdx, nOptIdx - m_nOptionIdx); + } + return false; + } + ++m_nNextOption; + + // get the option id + ESOArgType nArgType = SO_NONE; + if (nTableIdx < 0) { + m_nLastError = (ESOError) nTableIdx; // error code + } + else { + m_nOptionId = m_rgOptions[nTableIdx].nId; + m_pszOptionText = m_rgOptions[nTableIdx].pszArg; + + // ensure that the arg type is valid + nArgType = m_rgOptions[nTableIdx].nArgType; + switch (nArgType) { + case SO_NONE: + if (m_pszOptionArg) { + m_nLastError = SO_ARG_INVALID; + } + break; + + case SO_REQ_SEP: + if (m_pszOptionArg) { + // they wanted separate args, but we got a combined one, + // unless we are pedantic, just accept it. + if (HasFlag(SO_O_PEDANTIC)) { + m_nLastError = SO_ARG_INVALID_TYPE; + } + } + // more processing after we shuffle + break; + + case SO_REQ_CMB: + if (!m_pszOptionArg) { + m_nLastError = SO_ARG_MISSING; + } + break; + + case SO_OPT: + // nothing to do + break; + + case SO_MULTI: + // nothing to do. Caller must now check for valid arguments + // using GetMultiArg() + break; + } + } + + // shuffle the files out of the way + if (nOptIdx > m_nOptionIdx) { + ShuffleArg(m_nOptionIdx, nOptIdx - m_nOptionIdx); + } + + // we need to return the separate arg if required, just re-use the + // multi-arg code because it all does the same thing + if ( nArgType == SO_REQ_SEP + && !m_pszOptionArg + && m_nLastError == SO_SUCCESS) + { + SOCHAR ** ppArgs = MultiArg(1); + if (ppArgs) { + m_pszOptionArg = *ppArgs; + } + } + + return true; +} + +template +void +CSimpleOptTempl::Stop() +{ + if (m_nNextOption < m_nLastArg) { + ShuffleArg(m_nNextOption, m_nLastArg - m_nNextOption); + } +} + +template +SOCHAR +CSimpleOptTempl::PrepareArg( + SOCHAR * a_pszString + ) const +{ +#ifdef _WIN32 + // On Windows we can accept the forward slash as a single character + // option delimiter, but it cannot replace the '-' option used to + // denote stdin. On Un*x paths may start with slash so it may not + // be used to start an option. + if (!HasFlag(SO_O_NOSLASH) + && a_pszString[0] == (SOCHAR)'/' + && a_pszString[1] + && a_pszString[1] != (SOCHAR)'-') + { + a_pszString[0] = (SOCHAR)'-'; + return (SOCHAR)'/'; + } +#endif + return a_pszString[0]; +} + +template +bool +CSimpleOptTempl::NextClumped() +{ + // prepare for the next clumped option + m_szShort[1] = *m_pszClump++; + m_nOptionId = -1; + m_pszOptionText = NULL; + m_pszOptionArg = NULL; + m_nLastError = SO_SUCCESS; + + // lookup this option, ensure that we are using exact matching + int nSavedFlags = m_nFlags; + m_nFlags = SO_O_EXACT; + int nTableIdx = LookupOption(m_szShort); + m_nFlags = nSavedFlags; + + // unknown option + if (nTableIdx < 0) { + m_pszOptionText = m_szShort; // invalid option + m_nLastError = (ESOError) nTableIdx; // error code + return false; + } + + // valid option + m_pszOptionText = m_rgOptions[nTableIdx].pszArg; + ESOArgType nArgType = m_rgOptions[nTableIdx].nArgType; + if (nArgType == SO_NONE) { + m_nOptionId = m_rgOptions[nTableIdx].nId; + return true; + } + + if (nArgType == SO_REQ_CMB && *m_pszClump) { + m_nOptionId = m_rgOptions[nTableIdx].nId; + m_pszOptionArg = m_pszClump; + while (*m_pszClump) ++m_pszClump; // must point to an empty string + return true; + } + + // invalid option as it requires an argument + m_nLastError = SO_ARG_MISSING; + return true; +} + +// Shuffle arguments to the end of the argv array. +// +// For example: +// argv[] = { "0", "1", "2", "3", "4", "5", "6", "7", "8" }; +// +// ShuffleArg(1, 1) = { "0", "2", "3", "4", "5", "6", "7", "8", "1" }; +// ShuffleArg(5, 2) = { "0", "1", "2", "3", "4", "7", "8", "5", "6" }; +// ShuffleArg(2, 4) = { "0", "1", "6", "7", "8", "2", "3", "4", "5" }; +template +void +CSimpleOptTempl::ShuffleArg( + int a_nStartIdx, + int a_nCount + ) +{ + SOCHAR * staticBuf[SO_STATICBUF]; + SOCHAR ** buf = m_rgShuffleBuf ? m_rgShuffleBuf : staticBuf; + int nTail = m_argc - a_nStartIdx - a_nCount; + + // make a copy of the elements to be moved + Copy(buf, m_argv + a_nStartIdx, a_nCount); + + // move the tail down + Copy(m_argv + a_nStartIdx, m_argv + a_nStartIdx + a_nCount, nTail); + + // append the moved elements to the tail + Copy(m_argv + a_nStartIdx + nTail, buf, a_nCount); + + // update the index of the last unshuffled arg + m_nLastArg -= a_nCount; +} + +// match on the long format strings. partial matches will be +// accepted only if that feature is enabled. +template +int +CSimpleOptTempl::LookupOption( + const SOCHAR * a_pszOption + ) const +{ + int nBestMatch = -1; // index of best match so far + int nBestMatchLen = 0; // matching characters of best match + int nLastMatchLen = 0; // matching characters of last best match + + for (int n = 0; m_rgOptions[n].nId >= 0; ++n) { + // the option table must use hyphens as the option character, + // the slash character is converted to a hyphen for testing. + SO_ASSERT(m_rgOptions[n].pszArg[0] != (SOCHAR)'/'); + + int nMatchLen = CalcMatch(m_rgOptions[n].pszArg, a_pszOption); + if (nMatchLen == -1) { + return n; + } + if (nMatchLen > 0 && nMatchLen >= nBestMatchLen) { + nLastMatchLen = nBestMatchLen; + nBestMatchLen = nMatchLen; + nBestMatch = n; + } + } + + // only partial matches or no match gets to here, ensure that we + // don't return a partial match unless it is a clear winner + if (HasFlag(SO_O_EXACT) || nBestMatch == -1) { + return SO_OPT_INVALID; + } + return (nBestMatchLen > nLastMatchLen) ? nBestMatch : SO_OPT_MULTIPLE; +} + +// calculate the number of characters that match (case-sensitive) +// 0 = no match, > 0 == number of characters, -1 == perfect match +template +int +CSimpleOptTempl::CalcMatch( + const SOCHAR * a_pszSource, + const SOCHAR * a_pszTest + ) const +{ + if (!a_pszSource || !a_pszTest) { + return 0; + } + + // determine the argument type + int nArgType = SO_O_ICASE_LONG; + if (a_pszSource[0] != '-') { + nArgType = SO_O_ICASE_WORD; + } + else if (a_pszSource[1] != '-' && !a_pszSource[2]) { + nArgType = SO_O_ICASE_SHORT; + } + + // match and skip leading hyphens + while (*a_pszSource == (SOCHAR)'-' && *a_pszSource == *a_pszTest) { + ++a_pszSource; + ++a_pszTest; + } + if (*a_pszSource == (SOCHAR)'-' || *a_pszTest == (SOCHAR)'-') { + return 0; + } + + // find matching number of characters in the strings + int nLen = 0; + while (*a_pszSource && IsEqual(*a_pszSource, *a_pszTest, nArgType)) { + ++a_pszSource; + ++a_pszTest; + ++nLen; + } + + // if we have exhausted the source... + if (!*a_pszSource) { + // and the test strings, then it's a perfect match + if (!*a_pszTest) { + return -1; + } + + // otherwise the match failed as the test is longer than + // the source. i.e. "--mant" will not match the option "--man". + return 0; + } + + // if we haven't exhausted the test string then it is not a match + // i.e. "--mantle" will not best-fit match to "--mandate" at all. + if (*a_pszTest) { + return 0; + } + + // partial match to the current length of the test string + return nLen; +} + +template +bool +CSimpleOptTempl::IsEqual( + SOCHAR a_cLeft, + SOCHAR a_cRight, + int a_nArgType + ) const +{ + // if this matches then we are doing case-insensitive matching + if (m_nFlags & a_nArgType) { + if (a_cLeft >= 'A' && a_cLeft <= 'Z') a_cLeft += 'a' - 'A'; + if (a_cRight >= 'A' && a_cRight <= 'Z') a_cRight += 'a' - 'A'; + } + return a_cLeft == a_cRight; +} + +// calculate the number of characters that match (case-sensitive) +// 0 = no match, > 0 == number of characters, -1 == perfect match +template +SOCHAR ** +CSimpleOptTempl::MultiArg( + int a_nCount + ) +{ + // ensure we have enough arguments + if (m_nNextOption + a_nCount > m_nLastArg) { + m_nLastError = SO_ARG_MISSING; + return NULL; + } + + // our argument array + SOCHAR ** rgpszArg = &m_argv[m_nNextOption]; + + // Ensure that each of the following don't start with an switch character. + // Only make this check if we are returning errors for unknown arguments. + if (!HasFlag(SO_O_NOERR)) { + for (int n = 0; n < a_nCount; ++n) { + SOCHAR ch = PrepareArg(rgpszArg[n]); + if (rgpszArg[n][0] == (SOCHAR)'-') { + rgpszArg[n][0] = ch; + m_nLastError = SO_ARG_INVALID_DATA; + return NULL; + } + rgpszArg[n][0] = ch; + } + } + + // all good + m_nNextOption += a_nCount; + return rgpszArg; +} + + +// --------------------------------------------------------------------------- +// TYPE DEFINITIONS +// --------------------------------------------------------------------------- + +/*! @brief ASCII/MBCS version of CSimpleOpt */ +typedef CSimpleOptTempl CSimpleOptA; + +/*! @brief wchar_t version of CSimpleOpt */ +typedef CSimpleOptTempl CSimpleOptW; + +#if defined(_UNICODE) +/*! @brief TCHAR version dependent on if _UNICODE is defined */ +# define CSimpleOpt CSimpleOptW +#else +/*! @brief TCHAR version dependent on if _UNICODE is defined */ +# define CSimpleOpt CSimpleOptA +#endif + +#endif // INCLUDED_SimpleOpt diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 34799e4d73..67c9089957 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -12,6 +12,7 @@ set(SOURCE_FILES util/SignalMediator.hxx util/Singleton.hxx util/Meta.hxx + util/ParseCli.{hxx,cxx} util/Exception.{hxx,cxx} util/OSystem.{hxx,cxx} services/Randomizer.{hxx,cxx} diff --git a/src/Game.cxx b/src/Game.cxx index fc45566b47..675db47dd8 100644 --- a/src/Game.cxx +++ b/src/Game.cxx @@ -47,7 +47,7 @@ void Game::quit() #endif // USE_AUDIO } -bool Game::initialize(const char *videoDriver) +bool Game::initialize() { if (SDL_Init(0) != 0) { @@ -55,10 +55,13 @@ bool Game::initialize(const char *videoDriver) LOG(LOG_ERROR) << "SDL Error: " << SDL_GetError(); return false; } + const char *vd = nullptr; + if(Settings::instance().videoDriver != "Default") + vd = Settings::instance().videoDriver.c_str(); - if (SDL_VideoInit(videoDriver) != 0) + if (SDL_VideoInit(vd) != 0) { - LOG(LOG_ERROR) << "Unknown video driver " << videoDriver; + LOG(LOG_ERROR) << "Unknown video driver " << vd; int nbDriver = SDL_GetNumRenderDrivers(); for (int i = 0; i < nbDriver; i++) { @@ -282,7 +285,7 @@ bool Game::mainMenu() return quitGame; } -void Game::run(bool SkipMenu) +void Game::run() { LOG(LOG_INFO) << VERSION; diff --git a/src/Game.hxx b/src/Game.hxx index f935f67a51..78b6409503 100644 --- a/src/Game.hxx +++ b/src/Game.hxx @@ -42,13 +42,13 @@ public: /** @brief starts setting up the game * starts game initialization. */ - virtual bool initialize(const char *videoDriver); + virtual bool initialize(); /** @brief begins the game * starts running the game * @param SkipMenu if the main menu should be skipped or not */ - virtual void run(bool SkipMenu = false); + virtual void run(); /** @brief ends the game * shuts down the game diff --git a/src/engine/basics/Settings.hxx b/src/engine/basics/Settings.hxx index 02bfb66070..6235ccba6d 100644 --- a/src/engine/basics/Settings.hxx +++ b/src/engine/basics/Settings.hxx @@ -166,6 +166,16 @@ struct SettingsData /// Write errors to a log file bool writeErrorLogFile; + + // ================================== + // Command line options + // ================================== + + /// Sets a different video driver + std::string videoDriver = "Default"; + + bool skipMenu = false; + bool quitGame = false; }; /** diff --git a/src/main.cxx b/src/main.cxx index 95caa5bf43..88679a57f8 100644 --- a/src/main.cxx +++ b/src/main.cxx @@ -3,32 +3,14 @@ #include "Game.hxx" #include "Exception.hxx" #include "LOG.hxx" +#include "SimpleOpt.h" +#include "ParseCli.hxx" int protected_main(int argc, char **argv) { - (void)argc; - (void)argv; - - bool quitGame = false; - - // add commandline parameter to skipMenu - auto has_args = [argv, argc](const std::string ¶m) - { - for (int i = 1; i < argc; ++i) - if (param == argv[i]) - return i; - - LOG(LOG_DEBUG) << "Unknown game option " << param; - return 0; - }; - - bool skipMenu = has_args("--skipMenu"); - uint32_t videoOpt = has_args("--video"); - const char *videoDriver = nullptr; - if (videoOpt) - { - videoDriver = argv[videoOpt + 1]; - } + ParseCli cli; + if (!cli.ProcessCommandLine(argc,argv)) + return EXIT_FAILURE; LOG(LOG_DEBUG) << "Launching Cytopia"; @@ -36,10 +18,12 @@ int protected_main(int argc, char **argv) LOG(LOG_DEBUG) << "Initializing Cytopia"; - if (!game.initialize(videoDriver)) + if (!game.initialize()) return EXIT_FAILURE; - if (!skipMenu) + bool quitGame = Settings::instance().quitGame; + + if (!Settings::instance().skipMenu) { quitGame = game.mainMenu(); } @@ -47,7 +31,7 @@ int protected_main(int argc, char **argv) if (!quitGame) { LOG(LOG_DEBUG) << "Running the Game"; - game.run(skipMenu); + game.run(); } LOG(LOG_DEBUG) << "Closing the Game"; @@ -74,4 +58,4 @@ int main(int argc, char **argv) } return EXIT_FAILURE; -} +} \ No newline at end of file diff --git a/src/util/ParseCli.cxx b/src/util/ParseCli.cxx new file mode 100644 index 0000000000..824456a2d7 --- /dev/null +++ b/src/util/ParseCli.cxx @@ -0,0 +1,90 @@ +#include "ParseCli.hxx" +#include "LOG.hxx" +#include "Singleton.hxx" +#include "Settings.hxx" + +// option identifiers +enum +{ + OPT_HELP, + OPT_SKIPMENU, + OPT_VIDEODRIVER +}; + +// option array +CSimpleOpt::SOption cmdline_options[] = {{OPT_SKIPMENU, ("--skipMenu"), SO_NONE}, + {OPT_VIDEODRIVER, ("--video"), SO_REQ_SEP}, + {OPT_HELP, ("--help"), SO_NONE}, + {OPT_HELP, ("-h"), SO_NONE}, + SO_END_OF_OPTIONS}; + +bool ParseCli::ProcessCommandLine(int argc, char *argv[]) +{ + CSimpleOpt args(argc, argv, cmdline_options); + bool success = true; + while (args.Next()) + { + if (args.LastError() != SO_SUCCESS) + { + LOG(LOG_ERROR) << GetLastErrorText(args.LastError()) << " " << args.OptionText(); + success = false; + } + + switch (args.OptionId()) + { + case OPT_HELP: + ShowUsage(); + return false; + case OPT_SKIPMENU: + Settings::instance().skipMenu = true; + break; + case OPT_VIDEODRIVER: + if (args.OptionArg()) + { + Settings::instance().videoDriver = args.OptionArg(); + } + else + { + LOG(LOG_ERROR) << "videoDriver not set"; + ShowUsage(); + return false; + } + break; + default: + ShowUsage(); + } + } + + return success; +} + +void ParseCli::ShowUsage() +{ + LOG(LOG_INFO) << "Usage: Cytopia [OPTIONS]"; + LOG(LOG_INFO) << "\t--help (this)"; + LOG(LOG_INFO) << "\t--skipMenu (Skips the main menu)"; + LOG(LOG_INFO) << "\t--video (Sets a different video driver)"; +} + +std::string ParseCli::GetLastErrorText(int a_nError) +{ + switch (a_nError) + { + case SO_SUCCESS: + return "Success"; + case SO_OPT_INVALID: + return "Unrecognized option"; + case SO_OPT_MULTIPLE: + return "Option matched multiple strings"; + case SO_ARG_INVALID: + return "Option does not accept argument"; + case SO_ARG_INVALID_TYPE: + return "Invalid argument format"; + case SO_ARG_MISSING: + return "Required argument is missing"; + case SO_ARG_INVALID_DATA: + return "Invalid argument data"; + default: + return "Unknown error"; + } +} \ No newline at end of file diff --git a/src/util/ParseCli.hxx b/src/util/ParseCli.hxx new file mode 100644 index 0000000000..c2070a93c3 --- /dev/null +++ b/src/util/ParseCli.hxx @@ -0,0 +1,18 @@ +#ifndef CYTOPIA_PARSECLI_HXX +#define CYTOPIA_PARSECLI_HXX + +#include "SimpleOpt.h" +#include + +class ParseCli +{ +public: + bool ProcessCommandLine(int argc, char **argv); + +private: + std::string GetLastErrorText(int a_nError); + + void ShowUsage(); +}; + +#endif //CYTOPIA_PARSECLI_HXX