├── .gitmodules ├── LICENSE ├── CMakeLists.txt ├── README.md ├── main.cpp └── include └── SimpleOpt.h /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "CascLib"] 2 | path = CascLib 3 | url = https://github.com/ladislav-zezula/CascLib.git 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | CASCExtractor is made available under the MIT License. 2 | 3 | Copyright (c) 2014 Philip Abbet 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | project(CASCEXTRACTOR) 2 | cmake_minimum_required(VERSION 2.6) 3 | 4 | set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CASCEXTRACTOR_BINARY_DIR}/bin") 5 | set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${CASCEXTRACTOR_BINARY_DIR}/lib") 6 | set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${CASCEXTRACTOR_BINARY_DIR}/bin") 7 | 8 | 9 | if (NOT EXISTS "${CASCEXTRACTOR_SOURCE_DIR}/CascLib/CMakeLists.txt") 10 | message(FATAL_ERROR 11 | "Missing dependency: CascLib 12 | CASCExtractor requires the CascLib library. 13 | It is provided as a GIT submodule of this repository. 14 | Did you forgot to execute the following commands? 15 | git submodule init 16 | git submodule update") 17 | endif() 18 | 19 | 20 | add_subdirectory(CascLib) 21 | 22 | include_directories("${CASCEXTRACTOR_SOURCE_DIR}/CascLib/src/" 23 | "${CASCEXTRACTOR_SOURCE_DIR}/include/" 24 | ) 25 | 26 | add_executable(CASCExtractor main.cpp) 27 | target_link_libraries(CASCExtractor casc) 28 | 29 | # Set the RPATH 30 | if (APPLE) 31 | set_target_properties(CASCExtractor PROPERTIES LINK_FLAGS "-Wl,-rpath,@loader_path/.") 32 | elseif (UNIX) 33 | set_target_properties(CASCExtractor PROPERTIES INSTALL_RPATH ".") 34 | endif() 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CASCExtractor 2 | 3 | A command-line tool to extract files from CASC (Content Addressable Storage Container) 4 | storages (used by Blizzard games since 2014). 5 | 6 | Works on MacOS X and Linux. 7 | 8 | 9 | ## Dependencies 10 | 11 | The following libraries are necessary to build the extractor: 12 | 13 | * CascLib (http://www.zezula.net/en/casc/main.html), MIT license 14 | * SimpleOpt 3.4 (http://code.jellycan.com/simpleopt/), MIT License - part of the 15 | distribution 16 | 17 | To download the CascLib submodule, do: 18 | 19 | somewhere$ cd 20 | CASCExtractor$ git submodule init 21 | CASCExtractor$ git submodule update 22 | 23 | 24 | ## Compilation 25 | 26 | Requires cmake to build: 27 | 28 | $ mkdir build 29 | $ cd build 30 | $ cmake 31 | $ make 32 | 33 | The executable will be put in build/bin/ 34 | 35 | 36 | ## Usage 37 | 38 | See `CASCExtractor --help` for more details. All the examples below are shown using the 39 | World of Warcraft: Warlords of Draenor Beta files, as of July 2014. 40 | 41 | Note that the CASC format doesn't store the name of the files in plain text, but only in a 42 | hashed version. Thus, a *listfile* is needed if you don't know the exact name of the file 43 | you want to extract. One such *listfile* for WoW:WoD is provided with CascLib in 44 | *CascLib/listfile/listfile-wow6.txt*. 45 | 46 | 47 | **Extract all the *.M2 files from a CASC storage:** 48 | 49 | $ CASCExtractor -l /path/to/listfile-wow6.txt "/Applications/World of Warcraft Beta/Data/" *.M2 50 | Opening '/Applications/World of Warcraft Beta/Data'... 51 | 52 | Searching for '*.M2'... 53 | 54 | Found files: 55 | - Cameras\Abyssal_Maw_CameraFly_01.M2 56 | - Cameras\Firelands_Bridge_Camera_01.M2 57 | - Cameras\FlyBy_Maelstrom.M2 58 | - Cameras\FlyBy_MoguBridge_Collapse.M2 59 | - Cameras\FlyBy_MoguBridge_Collapse02.M2 60 | - Cameras\FlyBy_MoguSecret_Door.M2 61 | - Cameras\FlyBy_MoguSecret_Door02.M2 62 | - Cameras\FlyByBloodElf.m2 63 | - Cameras\FlyByDeathKnight.m2 64 | .... 65 | 66 | 67 | **Extract a specific file from a CASC storage:** 68 | 69 | **IMPORTANT:** Note that the file name is enclosed in "". This is to prevent the shell 70 | to try to interpret the backslashes (\\) as the start of an escape sequence, which would 71 | result in invalid file names. 72 | 73 | $ mkdir out 74 | $ CASCExtractor -o out "/Applications/World of Warcraft Beta/Data/" "Cameras\Abyssal_Maw_CameraFly_01.M2" 75 | Opening '/Applications/World of Warcraft Beta/Data'... 76 | 77 | Extracting files... 78 | 79 | $ ls out/ 80 | Abyssal_Maw_CameraFly_01.M2 81 | 82 | 83 | **Extract some specific files from a CASC storage, preserving the path hierarchy:** 84 | 85 | $ mkdir out 86 | $ CASCExtractor -f -o out "/Applications/World of Warcraft Beta/Data/" "Cameras\FlyBy_*.M2" 87 | Opening '/Applications/World of Warcraft Beta/Data'... 88 | 89 | Searching for 'Cameras\FlyBy_*.M2'... 90 | 91 | Found files: 92 | - Cameras\FlyBy_Maelstrom.M2 93 | - Cameras\FlyBy_MoguBridge_Collapse.M2 94 | - Cameras\FlyBy_MoguBridge_Collapse02.M2 95 | - Cameras\FlyBy_MoguSecret_Door.M2 96 | - Cameras\FlyBy_MoguSecret_Door02.M2 97 | - Cameras\FlyByBloodElf.m2 98 | ... 99 | 100 | Extracting files... 101 | 102 | $ ls out/Cameras/ 103 | FlyBy_Maelstrom.M2 104 | FlyBy_MoguBridge_Collapse.M2 105 | FlyBy_MoguBridge_Collapse02.M2 106 | FlyBy_MoguSecret_Door.M2 107 | FlyBy_MoguSecret_Door02.M2 108 | FlyByBloodElf.m2 109 | ... 110 | 111 | 112 | ## License 113 | 114 | CASCExtractor is is made available under the MIT License. The text of the license is in 115 | the file 'LICENSE'. 116 | 117 | Under the MIT License you may use CASCExtractor for any purpose you wish, without warranty, 118 | and modify it if you require, subject to one condition: 119 | 120 | > "The above copyright notice and this permission notice shall be included in 121 | > all copies or substantial portions of the Software." 122 | 123 | In practice this means that whenever you distribute your application, whether as binary or 124 | as source code, you must include somewhere in your distribution the text in the file 125 | 'LICENSE'. This might be in the printed documentation, as a file on delivered media, or 126 | even on the credits / acknowledgements of the runtime application itself; any of those 127 | would satisfy the requirement. 128 | 129 | Even if the license doesn't require it, please consider to contribute your modifications 130 | back to the community. 131 | -------------------------------------------------------------------------------- /main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | 14 | using namespace std; 15 | 16 | 17 | struct tSearchResult 18 | { 19 | string strFileName; 20 | string strFullPath; 21 | }; 22 | 23 | 24 | /**************************** COMMAND-LINE PARSING ****************************/ 25 | 26 | // The valid options 27 | enum 28 | { 29 | OPT_HELP, 30 | OPT_LISTFILE, 31 | OPT_SEARCH, 32 | OPT_EXTRACT, 33 | OPT_DEST, 34 | OPT_FULLPATH, 35 | OPT_LOWERCASE, 36 | }; 37 | 38 | 39 | const CSimpleOpt::SOption COMMAND_LINE_OPTIONS[] = { 40 | { OPT_HELP, "-h", SO_NONE }, 41 | { OPT_HELP, "--help", SO_NONE }, 42 | { OPT_LISTFILE, "-l", SO_REQ_SEP }, 43 | { OPT_LISTFILE, "--listfile", SO_REQ_SEP }, 44 | { OPT_SEARCH, "-s", SO_REQ_SEP }, 45 | { OPT_SEARCH, "--search", SO_REQ_SEP }, 46 | { OPT_EXTRACT, "-e", SO_REQ_SEP }, 47 | { OPT_EXTRACT, "--extract", SO_REQ_SEP }, 48 | { OPT_DEST, "-o", SO_REQ_SEP }, 49 | { OPT_DEST, "--dest", SO_REQ_SEP }, 50 | { OPT_FULLPATH, "-f", SO_NONE }, 51 | { OPT_FULLPATH, "--fullpath", SO_NONE }, 52 | { OPT_LOWERCASE, "-c", SO_NONE }, 53 | { OPT_LOWERCASE, "--lowercase", SO_NONE }, 54 | 55 | SO_END_OF_OPTIONS 56 | }; 57 | 58 | 59 | /********************************** FUNCTIONS *********************************/ 60 | 61 | void showUsage(const std::string& strApplicationName) 62 | { 63 | cout << "CASCExtractor" << endl 64 | << "Usage: " << strApplicationName << " [options] " << endl 65 | << endl 66 | << "This program can extract files from a CASC storage" << endl 67 | << endl 68 | << "Options:" << endl 69 | << " --help, -h: Display this help" << endl 70 | << " --listfile " << endl 71 | << " -l Use the list file FILE [required if PATTERN is not a full path]" << endl 72 | << " --dest ," << endl 73 | << " -o : The folder where the files are extracted (default: the" << endl 74 | << " current one)" << endl 75 | << " --fullpath, -f: During extraction, preserve the path hierarchy found" << endl 76 | << " inside the storage" << endl 77 | << " --lowercase, -c: Convert extracted file paths to lowercase" < searchResults; 106 | bool bUseFullPath = false; 107 | bool bLowerCase = false; 108 | 109 | 110 | // Parse the command-line parameters 111 | CSimpleOpt args(argc, argv, COMMAND_LINE_OPTIONS); 112 | while (args.Next()) 113 | { 114 | if (args.LastError() == SO_SUCCESS) 115 | { 116 | switch (args.OptionId()) 117 | { 118 | case OPT_HELP: 119 | showUsage(argv[0]); 120 | return 0; 121 | 122 | case OPT_LISTFILE: 123 | strListFile = args.OptionArg(); 124 | break; 125 | 126 | case OPT_DEST: 127 | strDestination = args.OptionArg(); 128 | break; 129 | 130 | case OPT_FULLPATH: 131 | bUseFullPath = true; 132 | break; 133 | 134 | case OPT_LOWERCASE: 135 | bLowerCase = true; 136 | break; 137 | } 138 | } 139 | else 140 | { 141 | cerr << "Invalid argument: " << args.OptionText() << endl; 142 | return -1; 143 | } 144 | } 145 | 146 | if (args.FileCount() != 2) 147 | { 148 | cerr << "Must specify both the path to a CASC storage and a search pattern" << endl; 149 | return -1; 150 | } 151 | 152 | strStorage = args.File(0); 153 | strSearchPattern = args.File(1); 154 | 155 | // Remove trailing slashes at the end of the storage path (CascLib doesn't like that) 156 | if ((strStorage[strStorage.size() - 1] == '/') || (strStorage[strStorage.size() - 1] == '\\')) 157 | strStorage = strStorage.substr(0, strStorage.size() - 1); 158 | 159 | cout << "Opening '" << strStorage << "'..." << endl; 160 | if (!CascOpenStorage(strStorage.c_str(), 0, &hStorage)) 161 | { 162 | cerr << "Failed to open the storage '" << strStorage << "'" << endl; 163 | return -1; 164 | } 165 | 166 | 167 | // Search the files 168 | if ((strSearchPattern.find("*") == string::npos) && (strSearchPattern.find("?") == string::npos)) 169 | { 170 | tSearchResult r; 171 | r.strFileName = strSearchPattern.substr(strSearchPattern.find_last_of("\\") + 1); 172 | r.strFullPath = strSearchPattern; 173 | 174 | searchResults.push_back(r); 175 | } 176 | else 177 | { 178 | if (strListFile.empty()) 179 | { 180 | cerr << "No listfile specified, use the --listfile option" << endl; 181 | return -1; 182 | } 183 | 184 | cout << endl; 185 | cout << "Searching for '" << strSearchPattern << "'..." << endl; 186 | 187 | CASC_FIND_DATA findData; 188 | HANDLE handle = CascFindFirstFile(hStorage, strSearchPattern.c_str(), &findData, strListFile.c_str()); 189 | 190 | if (handle) 191 | { 192 | cout << endl; 193 | cout << "Found files:" << endl; 194 | 195 | do { 196 | cout << " - " << findData.szFileName << endl; 197 | 198 | tSearchResult r; 199 | r.strFileName = findData.szPlainName; 200 | r.strFullPath = findData.szFileName; 201 | 202 | searchResults.push_back(r); 203 | } while (CascFindNextFile(handle, &findData) && findData.szPlainName); 204 | 205 | CascFindClose(handle); 206 | } 207 | else 208 | { 209 | cout << "No file found!" << endl; 210 | } 211 | } 212 | 213 | // Extraction 214 | if (!searchResults.empty()) 215 | { 216 | char buffer[1000000]; 217 | 218 | cout << endl; 219 | cout << "Extracting files..." << endl; 220 | cout << endl; 221 | 222 | if (strDestination.at(strDestination.size() - 1) != '/') 223 | strDestination += "/"; 224 | 225 | vector::iterator iter, iterEnd; 226 | for (iter = searchResults.begin(), iterEnd = searchResults.end(); iter != iterEnd; ++iter) 227 | { 228 | string strDestName = strDestination; 229 | 230 | if (bUseFullPath) 231 | { 232 | if (bLowerCase){ 233 | transform(iter->strFullPath.begin(), iter->strFullPath.end(), iter->strFullPath.begin(), ::tolower); 234 | } 235 | 236 | strDestName += iter->strFullPath; 237 | 238 | size_t offset = strDestName.find("\\"); 239 | while (offset != string::npos) 240 | { 241 | strDestName = strDestName.substr(0, offset) + "/" + strDestName.substr(offset + 1); 242 | offset = strDestName.find("\\"); 243 | } 244 | 245 | offset = strDestName.find_last_of("/"); 246 | if (offset != string::npos) 247 | { 248 | string dest = strDestName.substr(0, offset + 1); 249 | 250 | size_t start = dest.find("/", 0); 251 | while (start != string::npos) 252 | { 253 | string dirname = dest.substr(0, start); 254 | 255 | DIR* d = opendir(dirname.c_str()); 256 | if (!d) 257 | mkdir(dirname.c_str(), S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH); 258 | else 259 | closedir(d); 260 | 261 | start = dest.find("/", start + 1); 262 | } 263 | } 264 | } 265 | else 266 | { 267 | if (bLowerCase){ 268 | transform(iter->strFileName.begin(), iter->strFileName.end(), iter->strFileName.begin(), ::tolower); 269 | } 270 | 271 | strDestName += iter->strFileName; 272 | } 273 | 274 | HANDLE hFile; 275 | if (CascOpenFile(hStorage, iter->strFullPath.c_str(), CASC_LOCALE_ALL, 0, &hFile)) 276 | { 277 | DWORD read; 278 | FILE* dest = fopen(strDestName.c_str(), "wb"); 279 | if (dest) 280 | { 281 | do { 282 | if (CascReadFile(hFile, &buffer, 1000000, &read)) 283 | fwrite(&buffer, read, 1, dest); 284 | } while (read > 0); 285 | 286 | fclose(dest); 287 | } 288 | else 289 | { 290 | cerr << "Failed to extract the file '" << iter->strFullPath << "' in " << strDestName << endl; 291 | } 292 | 293 | CascCloseFile(hFile); 294 | } 295 | else 296 | { 297 | cerr << "Failed to extract the file '" << iter->strFullPath << "' in " << strDestName << endl; 298 | } 299 | } 300 | } 301 | 302 | CascCloseStorage(hStorage); 303 | 304 | return 0; 305 | } 306 | -------------------------------------------------------------------------------- /include/SimpleOpt.h: -------------------------------------------------------------------------------- 1 | /*! @file SimpleOpt.h 2 | 3 | @version 3.4 4 | 5 | @brief A cross-platform command line library which can parse almost any 6 | of the standard command line formats in use today. It is designed 7 | explicitly to be portable to any platform and has been tested on Windows 8 | and Linux. See CSimpleOptTempl for the class definition. 9 | 10 | @section features FEATURES 11 | 12 | - MIT Licence allows free use in all software (including GPL 13 | and commercial) 14 | - multi-platform (Windows 95/98/ME/NT/2K/XP, Linux, Unix) 15 | - supports all lengths of option names: 16 | 17 |
- 18 | switch character only (e.g. use stdin for input) 19 |
-o 20 | short (single character) 21 |
-long 22 | long (multiple character, single switch character) 23 |
--longer 24 | long (multiple character, multiple switch characters) 25 |
26 | - supports all types of arguments for options: 27 | 28 |
--option 29 | short/long option flag (no argument) 30 |
--option ARG 31 | short/long option with separate required argument 32 |
--option=ARG 33 | short/long option with combined required argument 34 |
--option[=ARG] 35 | short/long option with combined optional argument 36 |
-oARG 37 | short option with combined required argument 38 |
-o[ARG] 39 | short option with combined optional argument 40 |
41 | - supports options with multiple or variable numbers of arguments: 42 | 43 |
--multi ARG1 ARG2 44 | Multiple arguments 45 |
--multi N ARG-1 ARG-2 ... ARG-N 46 | Variable number of arguments 47 |
48 | - supports case-insensitive option matching on short, long and/or 49 | word arguments. 50 | - supports options which do not use a switch character. i.e. a special 51 | word which is construed as an option. 52 | e.g. "foo.exe open /directory/file.txt" 53 | - supports clumping of multiple short options (no arguments) in a string 54 | e.g. "foo.exe -abcdef file1" <==> "foo.exe -a -b -c -d -e -f file1" 55 | - automatic recognition of a single slash as equivalent to a single 56 | hyphen on Windows, e.g. "/f FILE" is equivalent to "-f FILE". 57 | - file arguments can appear anywhere in the argument list: 58 | "foo.exe file1.txt -a ARG file2.txt --flag file3.txt file4.txt" 59 | files will be returned to the application in the same order they were 60 | supplied on the command line 61 | - short-circuit option matching: "--man" will match "--mandate" 62 | invalid options can be handled while continuing to parse the command 63 | line valid options list can be changed dynamically during command line 64 | processing, i.e. accept different options depending on an option 65 | supplied earlier in the command line. 66 | - implemented with only a single C++ header file 67 | - optionally use no C runtime or OS functions 68 | - char, wchar_t and Windows TCHAR in the same program 69 | - complete working examples included 70 | - compiles cleanly at warning level 4 (Windows/VC.NET 2003), warning 71 | level 3 (Windows/VC6) and -Wall (Linux/gcc) 72 | 73 | @section usage USAGE 74 | 75 | The SimpleOpt class is used by following these steps: 76 | 77 |
    78 |
  1. Include the SimpleOpt.h header file 79 | 80 |
      81 |         \#include "SimpleOpt.h"
      82 |         
    83 | 84 |
  2. Define an array of valid options for your program. 85 | 86 |
      87 | @link CSimpleOptTempl::SOption CSimpleOpt::SOption @endlink g_rgOptions[] = {
      88 |     { OPT_FLAG, _T("-a"),     SO_NONE    }, // "-a"
      89 |     { OPT_FLAG, _T("-b"),     SO_NONE    }, // "-b"
      90 |     { OPT_ARG,  _T("-f"),     SO_REQ_SEP }, // "-f ARG"
      91 |     { OPT_HELP, _T("-?"),     SO_NONE    }, // "-?"
      92 |     { OPT_HELP, _T("--help"), SO_NONE    }, // "--help"
      93 |     SO_END_OF_OPTIONS                       // END
      94 | };
      95 | 
    96 | 97 | Note that all options must start with a hyphen even if the slash will 98 | be accepted. This is because the slash character is automatically 99 | converted into a hyphen to test against the list of options. 100 | For example, the following line matches both "-?" and "/?" 101 | (on Windows). 102 | 103 |
     104 |         { OPT_HELP, _T("-?"),     SO_NONE    }, // "-?"
     105 |         
    106 | 107 |
  3. Instantiate a CSimpleOpt object supplying argc, argv and the option 108 | table 109 | 110 |
     111 | @link CSimpleOptTempl CSimpleOpt @endlink args(argc, argv, g_rgOptions);
     112 | 
    113 | 114 |
  4. Process the arguments by calling Next() until it returns false. 115 | On each call, first check for an error by calling LastError(), then 116 | either handle the error or process the argument. 117 | 118 |
     119 | while (args.Next()) {
     120 |     if (args.LastError() == SO_SUCCESS) {
     121 |         handle option: use OptionId(), OptionText() and OptionArg()
     122 |     }
     123 |     else {
     124 |         handle error: see ESOError enums
     125 |     }
     126 | }
     127 | 
    128 | 129 |
  5. Process all non-option arguments with File(), Files() and FileCount() 130 | 131 |
     132 | ShowFiles(args.FileCount(), args.Files());
     133 | 
    134 | 135 |
136 | 137 | @section notes NOTES 138 | 139 | - In MBCS mode, this library is guaranteed to work correctly only when 140 | all option names use only ASCII characters. 141 | - Note that if case-insensitive matching is being used then the first 142 | matching option in the argument list will be returned. 143 | 144 | @section licence MIT LICENCE 145 | 146 | The licence text below is the boilerplate "MIT Licence" used from: 147 | http://www.opensource.org/licenses/mit-license.php 148 | 149 | Copyright (c) 2006-2007, Brodie Thiesfield 150 | 151 | Permission is hereby granted, free of charge, to any person obtaining a 152 | copy of this software and associated documentation files (the "Software"), 153 | to deal in the Software without restriction, including without limitation 154 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 155 | and/or sell copies of the Software, and to permit persons to whom the 156 | Software is furnished to do so, subject to the following conditions: 157 | 158 | The above copyright notice and this permission notice shall be included 159 | in all copies or substantial portions of the Software. 160 | 161 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 162 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 163 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 164 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 165 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 166 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 167 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 168 | */ 169 | 170 | /*! @mainpage 171 | 172 | 173 |
Library SimpleOpt 174 |
Author Brodie Thiesfield [code at jellycan dot com] 175 |
Source http://code.jellycan.com/simpleopt/ 176 |
177 | 178 | @section SimpleOpt SimpleOpt 179 | 180 | A cross-platform library providing a simple method to parse almost any of 181 | the standard command-line formats in use today. 182 | 183 | See the @link SimpleOpt.h SimpleOpt @endlink documentation for full 184 | details. 185 | 186 | @section SimpleGlob SimpleGlob 187 | 188 | A cross-platform file globbing library providing the ability to 189 | expand wildcards in command-line arguments to a list of all matching 190 | files. 191 | 192 | See the @link SimpleGlob.h SimpleGlob @endlink documentation for full 193 | details. 194 | */ 195 | 196 | #ifndef INCLUDED_SimpleOpt 197 | #define INCLUDED_SimpleOpt 198 | 199 | // Default the max arguments to a fixed value. If you want to be able to 200 | // handle any number of arguments, then predefine this to 0 and it will 201 | // use an internal dynamically allocated buffer instead. 202 | #ifdef SO_MAX_ARGS 203 | # define SO_STATICBUF SO_MAX_ARGS 204 | #else 205 | # include // malloc, free 206 | # include // memcpy 207 | # define SO_STATICBUF 50 208 | #endif 209 | 210 | //! Error values 211 | typedef enum _ESOError 212 | { 213 | //! No error 214 | SO_SUCCESS = 0, 215 | 216 | /*! It looks like an option (it starts with a switch character), but 217 | it isn't registered in the option table. */ 218 | SO_OPT_INVALID = -1, 219 | 220 | /*! Multiple options matched the supplied option text. 221 | Only returned when NOT using SO_O_EXACT. */ 222 | SO_OPT_MULTIPLE = -2, 223 | 224 | /*! Option doesn't take an argument, but a combined argument was 225 | supplied. */ 226 | SO_ARG_INVALID = -3, 227 | 228 | /*! SO_REQ_CMB style-argument was supplied to a SO_REQ_SEP option 229 | Only returned when using SO_O_PEDANTIC. */ 230 | SO_ARG_INVALID_TYPE = -4, 231 | 232 | //! Required argument was not supplied 233 | SO_ARG_MISSING = -5, 234 | 235 | /*! Option argument looks like another option. 236 | Only returned when NOT using SO_O_NOERR. */ 237 | SO_ARG_INVALID_DATA = -6 238 | } ESOError; 239 | 240 | //! Option flags 241 | enum _ESOFlags 242 | { 243 | /*! Disallow partial matching of option names */ 244 | SO_O_EXACT = 0x0001, 245 | 246 | /*! Disallow use of slash as an option marker on Windows. 247 | Un*x only ever recognizes a hyphen. */ 248 | SO_O_NOSLASH = 0x0002, 249 | 250 | /*! Permit arguments on single letter options with no equals sign. 251 | e.g. -oARG or -o[ARG] */ 252 | SO_O_SHORTARG = 0x0004, 253 | 254 | /*! Permit single character options to be clumped into a single 255 | option string. e.g. "-a -b -c" <==> "-abc" */ 256 | SO_O_CLUMP = 0x0008, 257 | 258 | /*! Process the entire argv array for options, including the 259 | argv[0] entry. */ 260 | SO_O_USEALL = 0x0010, 261 | 262 | /*! Do not generate an error for invalid options. errors for missing 263 | arguments will still be generated. invalid options will be 264 | treated as files. invalid options in clumps will be silently 265 | ignored. */ 266 | SO_O_NOERR = 0x0020, 267 | 268 | /*! Validate argument type pedantically. Return an error when a 269 | separated argument "-opt arg" is supplied by the user as a 270 | combined argument "-opt=arg". By default this is not considered 271 | an error. */ 272 | SO_O_PEDANTIC = 0x0040, 273 | 274 | /*! Case-insensitive comparisons for short arguments */ 275 | SO_O_ICASE_SHORT = 0x0100, 276 | 277 | /*! Case-insensitive comparisons for long arguments */ 278 | SO_O_ICASE_LONG = 0x0200, 279 | 280 | /*! Case-insensitive comparisons for word arguments 281 | i.e. arguments without any hyphens at the start. */ 282 | SO_O_ICASE_WORD = 0x0400, 283 | 284 | /*! Case-insensitive comparisons for all arg types */ 285 | SO_O_ICASE = 0x0700 286 | }; 287 | 288 | /*! Types of arguments that options may have. Note that some of the _ESOFlags 289 | are not compatible with all argument types. SO_O_SHORTARG requires that 290 | relevant options use either SO_REQ_CMB or SO_OPT. SO_O_CLUMP requires 291 | that relevant options use only SO_NONE. 292 | */ 293 | typedef enum _ESOArgType { 294 | /*! No argument. Just the option flags. 295 | e.g. -o --opt */ 296 | SO_NONE, 297 | 298 | /*! Required separate argument. 299 | e.g. -o ARG --opt ARG */ 300 | SO_REQ_SEP, 301 | 302 | /*! Required combined argument. 303 | e.g. -oARG -o=ARG --opt=ARG */ 304 | SO_REQ_CMB, 305 | 306 | /*! Optional combined argument. 307 | e.g. -o[ARG] -o[=ARG] --opt[=ARG] */ 308 | SO_OPT, 309 | 310 | /*! Multiple separate arguments. The actual number of arguments is 311 | determined programatically at the time the argument is processed. 312 | e.g. -o N ARG1 ARG2 ... ARGN --opt N ARG1 ARG2 ... ARGN */ 313 | SO_MULTI 314 | } ESOArgType; 315 | 316 | //! this option definition must be the last entry in the table 317 | #define SO_END_OF_OPTIONS { -1, NULL, SO_NONE } 318 | 319 | #ifdef _DEBUG 320 | # ifdef _MSC_VER 321 | # include 322 | # define SO_ASSERT(b) _ASSERTE(b) 323 | # else 324 | # include 325 | # define SO_ASSERT(b) assert(b) 326 | # endif 327 | #else 328 | # define SO_ASSERT(b) //!< assertion used to test input data 329 | #endif 330 | 331 | // --------------------------------------------------------------------------- 332 | // MAIN TEMPLATE CLASS 333 | // --------------------------------------------------------------------------- 334 | 335 | /*! @brief Implementation of the SimpleOpt class */ 336 | template 337 | class CSimpleOptTempl 338 | { 339 | public: 340 | /*! @brief Structure used to define all known options. */ 341 | struct SOption { 342 | /*! ID to return for this flag. Optional but must be >= 0 */ 343 | int nId; 344 | 345 | /*! arg string to search for, e.g. "open", "-", "-f", "--file" 346 | Note that on Windows the slash option marker will be converted 347 | to a hyphen so that "-f" will also match "/f". */ 348 | const SOCHAR * pszArg; 349 | 350 | /*! type of argument accepted by this option */ 351 | ESOArgType nArgType; 352 | }; 353 | 354 | /*! @brief Initialize the class. Init() must be called later. */ 355 | CSimpleOptTempl() 356 | : m_rgShuffleBuf(NULL) 357 | { 358 | Init(0, NULL, NULL, 0); 359 | } 360 | 361 | /*! @brief Initialize the class in preparation for use. */ 362 | CSimpleOptTempl( 363 | int argc, 364 | SOCHAR * argv[], 365 | const SOption * a_rgOptions, 366 | int a_nFlags = 0 367 | ) 368 | : m_rgShuffleBuf(NULL) 369 | { 370 | Init(argc, argv, a_rgOptions, a_nFlags); 371 | } 372 | 373 | #ifndef SO_MAX_ARGS 374 | /*! @brief Deallocate any allocated memory. */ 375 | ~CSimpleOptTempl() { if (m_rgShuffleBuf) free(m_rgShuffleBuf); } 376 | #endif 377 | 378 | /*! @brief Initialize the class in preparation for calling Next. 379 | 380 | The table of options pointed to by a_rgOptions does not need to be 381 | valid at the time that Init() is called. However on every call to 382 | Next() the table pointed to must be a valid options table with the 383 | last valid entry set to SO_END_OF_OPTIONS. 384 | 385 | NOTE: the array pointed to by a_argv will be modified by this 386 | class and must not be used or modified outside of member calls to 387 | this class. 388 | 389 | @param a_argc Argument array size 390 | @param a_argv Argument array 391 | @param a_rgOptions Valid option array 392 | @param a_nFlags Optional flags to modify the processing of 393 | the arguments 394 | 395 | @return true Successful 396 | @return false if SO_MAX_ARGC > 0: Too many arguments 397 | if SO_MAX_ARGC == 0: Memory allocation failure 398 | */ 399 | bool Init( 400 | int a_argc, 401 | SOCHAR * a_argv[], 402 | const SOption * a_rgOptions, 403 | int a_nFlags = 0 404 | ); 405 | 406 | /*! @brief Change the current options table during option parsing. 407 | 408 | @param a_rgOptions Valid option array 409 | */ 410 | inline void SetOptions(const SOption * a_rgOptions) { 411 | m_rgOptions = a_rgOptions; 412 | } 413 | 414 | /*! @brief Change the current flags during option parsing. 415 | 416 | Note that changing the SO_O_USEALL flag here will have no affect. 417 | It must be set using Init() or the constructor. 418 | 419 | @param a_nFlags Flags to modify the processing of the arguments 420 | */ 421 | inline void SetFlags(int a_nFlags) { m_nFlags = a_nFlags; } 422 | 423 | /*! @brief Query if a particular flag is set */ 424 | inline bool HasFlag(int a_nFlag) const { 425 | return (m_nFlags & a_nFlag) == a_nFlag; 426 | } 427 | 428 | /*! @brief Advance to the next option if available. 429 | 430 | When all options have been processed it will return false. When true 431 | has been returned, you must check for an invalid or unrecognized 432 | option using the LastError() method. This will be return an error 433 | value other than SO_SUCCESS on an error. All standard data 434 | (e.g. OptionText(), OptionArg(), OptionId(), etc) will be available 435 | depending on the error. 436 | 437 | After all options have been processed, the remaining files from the 438 | command line can be processed in same order as they were passed to 439 | the program. 440 | 441 | @return true option or error available for processing 442 | @return false all options have been processed 443 | */ 444 | bool Next(); 445 | 446 | /*! Stops processing of the command line and returns all remaining 447 | arguments as files. The next call to Next() will return false. 448 | */ 449 | void Stop(); 450 | 451 | /*! @brief Return the last error that occurred. 452 | 453 | This function must always be called before processing the current 454 | option. This function is available only when Next() has returned true. 455 | */ 456 | inline ESOError LastError() const { return m_nLastError; } 457 | 458 | /*! @brief Return the nId value from the options array for the current 459 | option. 460 | 461 | This function is available only when Next() has returned true. 462 | */ 463 | inline int OptionId() const { return m_nOptionId; } 464 | 465 | /*! @brief Return the pszArg from the options array for the current 466 | option. 467 | 468 | This function is available only when Next() has returned true. 469 | */ 470 | inline const SOCHAR * OptionText() const { return m_pszOptionText; } 471 | 472 | /*! @brief Return the argument for the current option where one exists. 473 | 474 | If there is no argument for the option, this will return NULL. 475 | This function is available only when Next() has returned true. 476 | */ 477 | inline SOCHAR * OptionArg() const { return m_pszOptionArg; } 478 | 479 | /*! @brief Validate and return the desired number of arguments. 480 | 481 | This is only valid when OptionId() has return the ID of an option 482 | that is registered as SO_MULTI. It may be called multiple times 483 | each time returning the desired number of arguments. Previously 484 | returned argument pointers are remain valid. 485 | 486 | If an error occurs during processing, NULL will be returned and 487 | the error will be available via LastError(). 488 | 489 | @param n Number of arguments to return. 490 | */ 491 | SOCHAR ** MultiArg(int n); 492 | 493 | /*! @brief Returned the number of entries in the Files() array. 494 | 495 | After Next() has returned false, this will be the list of files (or 496 | otherwise unprocessed arguments). 497 | */ 498 | inline int FileCount() const { return m_argc - m_nLastArg; } 499 | 500 | /*! @brief Return the specified file argument. 501 | 502 | @param n Index of the file to return. This must be between 0 503 | and FileCount() - 1; 504 | */ 505 | inline SOCHAR * File(int n) const { 506 | SO_ASSERT(n >= 0 && n < FileCount()); 507 | return m_argv[m_nLastArg + n]; 508 | } 509 | 510 | /*! @brief Return the array of files. */ 511 | inline SOCHAR ** Files() const { return &m_argv[m_nLastArg]; } 512 | 513 | private: 514 | CSimpleOptTempl(const CSimpleOptTempl &); // disabled 515 | CSimpleOptTempl & operator=(const CSimpleOptTempl &); // disabled 516 | 517 | SOCHAR PrepareArg(SOCHAR * a_pszString) const; 518 | bool NextClumped(); 519 | void ShuffleArg(int a_nStartIdx, int a_nCount); 520 | int LookupOption(const SOCHAR * a_pszOption) const; 521 | int CalcMatch(const SOCHAR *a_pszSource, const SOCHAR *a_pszTest) const; 522 | 523 | // Find the '=' character within a string. 524 | inline SOCHAR * FindEquals(SOCHAR *s) const { 525 | while (*s && *s != (SOCHAR)'=') ++s; 526 | return *s ? s : NULL; 527 | } 528 | bool IsEqual(SOCHAR a_cLeft, SOCHAR a_cRight, int a_nArgType) const; 529 | 530 | inline void Copy(SOCHAR ** ppDst, SOCHAR ** ppSrc, int nCount) const { 531 | #ifdef SO_MAX_ARGS 532 | // keep our promise of no CLIB usage 533 | while (nCount-- > 0) *ppDst++ = *ppSrc++; 534 | #else 535 | memcpy(ppDst, ppSrc, nCount * sizeof(SOCHAR*)); 536 | #endif 537 | } 538 | 539 | private: 540 | const SOption * m_rgOptions; //!< pointer to options table 541 | int m_nFlags; //!< flags 542 | int m_nOptionIdx; //!< current argv option index 543 | int m_nOptionId; //!< id of current option (-1 = invalid) 544 | int m_nNextOption; //!< index of next option 545 | int m_nLastArg; //!< last argument, after this are files 546 | int m_argc; //!< argc to process 547 | SOCHAR ** m_argv; //!< argv 548 | const SOCHAR * m_pszOptionText; //!< curr option text, e.g. "-f" 549 | SOCHAR * m_pszOptionArg; //!< curr option arg, e.g. "c:\file.txt" 550 | SOCHAR * m_pszClump; //!< clumped single character options 551 | SOCHAR m_szShort[3]; //!< temp for clump and combined args 552 | ESOError m_nLastError; //!< error status from the last call 553 | SOCHAR ** m_rgShuffleBuf; //!< shuffle buffer for large argc 554 | }; 555 | 556 | // --------------------------------------------------------------------------- 557 | // IMPLEMENTATION 558 | // --------------------------------------------------------------------------- 559 | 560 | template 561 | bool 562 | CSimpleOptTempl::Init( 563 | int a_argc, 564 | SOCHAR * a_argv[], 565 | const SOption * a_rgOptions, 566 | int a_nFlags 567 | ) 568 | { 569 | m_argc = a_argc; 570 | m_nLastArg = a_argc; 571 | m_argv = a_argv; 572 | m_rgOptions = a_rgOptions; 573 | m_nLastError = SO_SUCCESS; 574 | m_nOptionIdx = 0; 575 | m_nOptionId = -1; 576 | m_pszOptionText = NULL; 577 | m_pszOptionArg = NULL; 578 | m_nNextOption = (a_nFlags & SO_O_USEALL) ? 0 : 1; 579 | m_szShort[0] = (SOCHAR)'-'; 580 | m_szShort[2] = (SOCHAR)'\0'; 581 | m_nFlags = a_nFlags; 582 | m_pszClump = NULL; 583 | 584 | #ifdef SO_MAX_ARGS 585 | if (m_argc > SO_MAX_ARGS) { 586 | m_nLastError = SO_ARG_INVALID_DATA; 587 | m_nLastArg = 0; 588 | return false; 589 | } 590 | #else 591 | if (m_rgShuffleBuf) { 592 | free(m_rgShuffleBuf); 593 | } 594 | if (m_argc > SO_STATICBUF) { 595 | m_rgShuffleBuf = (SOCHAR**) malloc(sizeof(SOCHAR*) * m_argc); 596 | if (!m_rgShuffleBuf) { 597 | return false; 598 | } 599 | } 600 | #endif 601 | 602 | return true; 603 | } 604 | 605 | template 606 | bool 607 | CSimpleOptTempl::Next() 608 | { 609 | #ifdef SO_MAX_ARGS 610 | if (m_argc > SO_MAX_ARGS) { 611 | SO_ASSERT(!"Too many args! Check the return value of Init()!"); 612 | return false; 613 | } 614 | #endif 615 | 616 | // process a clumped option string if appropriate 617 | if (m_pszClump && *m_pszClump) { 618 | // silently discard invalid clumped option 619 | bool bIsValid = NextClumped(); 620 | while (*m_pszClump && !bIsValid && HasFlag(SO_O_NOERR)) { 621 | bIsValid = NextClumped(); 622 | } 623 | 624 | // return this option if valid or we are returning errors 625 | if (bIsValid || !HasFlag(SO_O_NOERR)) { 626 | return true; 627 | } 628 | } 629 | SO_ASSERT(!m_pszClump || !*m_pszClump); 630 | m_pszClump = NULL; 631 | 632 | // init for the next option 633 | m_nOptionIdx = m_nNextOption; 634 | m_nOptionId = -1; 635 | m_pszOptionText = NULL; 636 | m_pszOptionArg = NULL; 637 | m_nLastError = SO_SUCCESS; 638 | 639 | // find the next option 640 | SOCHAR cFirst; 641 | int nTableIdx = -1; 642 | int nOptIdx = m_nOptionIdx; 643 | while (nTableIdx < 0 && nOptIdx < m_nLastArg) { 644 | SOCHAR * pszArg = m_argv[nOptIdx]; 645 | m_pszOptionArg = NULL; 646 | 647 | // find this option in the options table 648 | cFirst = PrepareArg(pszArg); 649 | if (pszArg[0] == (SOCHAR)'-') { 650 | // find any combined argument string and remove equals sign 651 | m_pszOptionArg = FindEquals(pszArg); 652 | if (m_pszOptionArg) { 653 | *m_pszOptionArg++ = (SOCHAR)'\0'; 654 | } 655 | } 656 | nTableIdx = LookupOption(pszArg); 657 | 658 | // if we didn't find this option but if it is a short form 659 | // option then we try the alternative forms 660 | if (nTableIdx < 0 661 | && !m_pszOptionArg 662 | && pszArg[0] == (SOCHAR)'-' 663 | && pszArg[1] 664 | && pszArg[1] != (SOCHAR)'-' 665 | && pszArg[2]) 666 | { 667 | // test for a short-form with argument if appropriate 668 | if (HasFlag(SO_O_SHORTARG)) { 669 | m_szShort[1] = pszArg[1]; 670 | int nIdx = LookupOption(m_szShort); 671 | if (nIdx >= 0 672 | && (m_rgOptions[nIdx].nArgType == SO_REQ_CMB 673 | || m_rgOptions[nIdx].nArgType == SO_OPT)) 674 | { 675 | m_pszOptionArg = &pszArg[2]; 676 | pszArg = m_szShort; 677 | nTableIdx = nIdx; 678 | } 679 | } 680 | 681 | // test for a clumped short-form option string and we didn't 682 | // match on the short-form argument above 683 | if (nTableIdx < 0 && HasFlag(SO_O_CLUMP)) { 684 | m_pszClump = &pszArg[1]; 685 | ++m_nNextOption; 686 | if (nOptIdx > m_nOptionIdx) { 687 | ShuffleArg(m_nOptionIdx, nOptIdx - m_nOptionIdx); 688 | } 689 | return Next(); 690 | } 691 | } 692 | 693 | // The option wasn't found. If it starts with a switch character 694 | // and we are not suppressing errors for invalid options then it 695 | // is reported as an error, otherwise it is data. 696 | if (nTableIdx < 0) { 697 | if (!HasFlag(SO_O_NOERR) && pszArg[0] == (SOCHAR)'-') { 698 | m_pszOptionText = pszArg; 699 | break; 700 | } 701 | 702 | pszArg[0] = cFirst; 703 | ++nOptIdx; 704 | if (m_pszOptionArg) { 705 | *(--m_pszOptionArg) = (SOCHAR)'='; 706 | } 707 | } 708 | } 709 | 710 | // end of options 711 | if (nOptIdx >= m_nLastArg) { 712 | if (nOptIdx > m_nOptionIdx) { 713 | ShuffleArg(m_nOptionIdx, nOptIdx - m_nOptionIdx); 714 | } 715 | return false; 716 | } 717 | ++m_nNextOption; 718 | 719 | // get the option id 720 | ESOArgType nArgType = SO_NONE; 721 | if (nTableIdx < 0) { 722 | m_nLastError = (ESOError) nTableIdx; // error code 723 | } 724 | else { 725 | m_nOptionId = m_rgOptions[nTableIdx].nId; 726 | m_pszOptionText = m_rgOptions[nTableIdx].pszArg; 727 | 728 | // ensure that the arg type is valid 729 | nArgType = m_rgOptions[nTableIdx].nArgType; 730 | switch (nArgType) { 731 | case SO_NONE: 732 | if (m_pszOptionArg) { 733 | m_nLastError = SO_ARG_INVALID; 734 | } 735 | break; 736 | 737 | case SO_REQ_SEP: 738 | if (m_pszOptionArg) { 739 | // they wanted separate args, but we got a combined one, 740 | // unless we are pedantic, just accept it. 741 | if (HasFlag(SO_O_PEDANTIC)) { 742 | m_nLastError = SO_ARG_INVALID_TYPE; 743 | } 744 | } 745 | // more processing after we shuffle 746 | break; 747 | 748 | case SO_REQ_CMB: 749 | if (!m_pszOptionArg) { 750 | m_nLastError = SO_ARG_MISSING; 751 | } 752 | break; 753 | 754 | case SO_OPT: 755 | // nothing to do 756 | break; 757 | 758 | case SO_MULTI: 759 | // nothing to do. Caller must now check for valid arguments 760 | // using GetMultiArg() 761 | break; 762 | } 763 | } 764 | 765 | // shuffle the files out of the way 766 | if (nOptIdx > m_nOptionIdx) { 767 | ShuffleArg(m_nOptionIdx, nOptIdx - m_nOptionIdx); 768 | } 769 | 770 | // we need to return the separate arg if required, just re-use the 771 | // multi-arg code because it all does the same thing 772 | if ( nArgType == SO_REQ_SEP 773 | && !m_pszOptionArg 774 | && m_nLastError == SO_SUCCESS) 775 | { 776 | SOCHAR ** ppArgs = MultiArg(1); 777 | if (ppArgs) { 778 | m_pszOptionArg = *ppArgs; 779 | } 780 | } 781 | 782 | return true; 783 | } 784 | 785 | template 786 | void 787 | CSimpleOptTempl::Stop() 788 | { 789 | if (m_nNextOption < m_nLastArg) { 790 | ShuffleArg(m_nNextOption, m_nLastArg - m_nNextOption); 791 | } 792 | } 793 | 794 | template 795 | SOCHAR 796 | CSimpleOptTempl::PrepareArg( 797 | SOCHAR * a_pszString 798 | ) const 799 | { 800 | #ifdef _WIN32 801 | // On Windows we can accept the forward slash as a single character 802 | // option delimiter, but it cannot replace the '-' option used to 803 | // denote stdin. On Un*x paths may start with slash so it may not 804 | // be used to start an option. 805 | if (!HasFlag(SO_O_NOSLASH) 806 | && a_pszString[0] == (SOCHAR)'/' 807 | && a_pszString[1] 808 | && a_pszString[1] != (SOCHAR)'-') 809 | { 810 | a_pszString[0] = (SOCHAR)'-'; 811 | return (SOCHAR)'/'; 812 | } 813 | #endif 814 | return a_pszString[0]; 815 | } 816 | 817 | template 818 | bool 819 | CSimpleOptTempl::NextClumped() 820 | { 821 | // prepare for the next clumped option 822 | m_szShort[1] = *m_pszClump++; 823 | m_nOptionId = -1; 824 | m_pszOptionText = NULL; 825 | m_pszOptionArg = NULL; 826 | m_nLastError = SO_SUCCESS; 827 | 828 | // lookup this option, ensure that we are using exact matching 829 | int nSavedFlags = m_nFlags; 830 | m_nFlags = SO_O_EXACT; 831 | int nTableIdx = LookupOption(m_szShort); 832 | m_nFlags = nSavedFlags; 833 | 834 | // unknown option 835 | if (nTableIdx < 0) { 836 | m_nLastError = (ESOError) nTableIdx; // error code 837 | return false; 838 | } 839 | 840 | // valid option 841 | m_pszOptionText = m_rgOptions[nTableIdx].pszArg; 842 | ESOArgType nArgType = m_rgOptions[nTableIdx].nArgType; 843 | if (nArgType == SO_NONE) { 844 | m_nOptionId = m_rgOptions[nTableIdx].nId; 845 | return true; 846 | } 847 | 848 | if (nArgType == SO_REQ_CMB && *m_pszClump) { 849 | m_nOptionId = m_rgOptions[nTableIdx].nId; 850 | m_pszOptionArg = m_pszClump; 851 | while (*m_pszClump) ++m_pszClump; // must point to an empty string 852 | return true; 853 | } 854 | 855 | // invalid option as it requires an argument 856 | m_nLastError = SO_ARG_MISSING; 857 | return true; 858 | } 859 | 860 | // Shuffle arguments to the end of the argv array. 861 | // 862 | // For example: 863 | // argv[] = { "0", "1", "2", "3", "4", "5", "6", "7", "8" }; 864 | // 865 | // ShuffleArg(1, 1) = { "0", "2", "3", "4", "5", "6", "7", "8", "1" }; 866 | // ShuffleArg(5, 2) = { "0", "1", "2", "3", "4", "7", "8", "5", "6" }; 867 | // ShuffleArg(2, 4) = { "0", "1", "6", "7", "8", "2", "3", "4", "5" }; 868 | template 869 | void 870 | CSimpleOptTempl::ShuffleArg( 871 | int a_nStartIdx, 872 | int a_nCount 873 | ) 874 | { 875 | SOCHAR * staticBuf[SO_STATICBUF]; 876 | SOCHAR ** buf = m_rgShuffleBuf ? m_rgShuffleBuf : staticBuf; 877 | int nTail = m_argc - a_nStartIdx - a_nCount; 878 | 879 | // make a copy of the elements to be moved 880 | Copy(buf, m_argv + a_nStartIdx, a_nCount); 881 | 882 | // move the tail down 883 | Copy(m_argv + a_nStartIdx, m_argv + a_nStartIdx + a_nCount, nTail); 884 | 885 | // append the moved elements to the tail 886 | Copy(m_argv + a_nStartIdx + nTail, buf, a_nCount); 887 | 888 | // update the index of the last unshuffled arg 889 | m_nLastArg -= a_nCount; 890 | } 891 | 892 | // match on the long format strings. partial matches will be 893 | // accepted only if that feature is enabled. 894 | template 895 | int 896 | CSimpleOptTempl::LookupOption( 897 | const SOCHAR * a_pszOption 898 | ) const 899 | { 900 | int nBestMatch = -1; // index of best match so far 901 | int nBestMatchLen = 0; // matching characters of best match 902 | int nLastMatchLen = 0; // matching characters of last best match 903 | 904 | for (int n = 0; m_rgOptions[n].nId >= 0; ++n) { 905 | // the option table must use hyphens as the option character, 906 | // the slash character is converted to a hyphen for testing. 907 | SO_ASSERT(m_rgOptions[n].pszArg[0] != (SOCHAR)'/'); 908 | 909 | int nMatchLen = CalcMatch(m_rgOptions[n].pszArg, a_pszOption); 910 | if (nMatchLen == -1) { 911 | return n; 912 | } 913 | if (nMatchLen > 0 && nMatchLen >= nBestMatchLen) { 914 | nLastMatchLen = nBestMatchLen; 915 | nBestMatchLen = nMatchLen; 916 | nBestMatch = n; 917 | } 918 | } 919 | 920 | // only partial matches or no match gets to here, ensure that we 921 | // don't return a partial match unless it is a clear winner 922 | if (HasFlag(SO_O_EXACT) || nBestMatch == -1) { 923 | return SO_OPT_INVALID; 924 | } 925 | return (nBestMatchLen > nLastMatchLen) ? nBestMatch : SO_OPT_MULTIPLE; 926 | } 927 | 928 | // calculate the number of characters that match (case-sensitive) 929 | // 0 = no match, > 0 == number of characters, -1 == perfect match 930 | template 931 | int 932 | CSimpleOptTempl::CalcMatch( 933 | const SOCHAR * a_pszSource, 934 | const SOCHAR * a_pszTest 935 | ) const 936 | { 937 | if (!a_pszSource || !a_pszTest) { 938 | return 0; 939 | } 940 | 941 | // determine the argument type 942 | int nArgType = SO_O_ICASE_LONG; 943 | if (a_pszSource[0] != '-') { 944 | nArgType = SO_O_ICASE_WORD; 945 | } 946 | else if (a_pszSource[1] != '-' && !a_pszSource[2]) { 947 | nArgType = SO_O_ICASE_SHORT; 948 | } 949 | 950 | // match and skip leading hyphens 951 | while (*a_pszSource == (SOCHAR)'-' && *a_pszSource == *a_pszTest) { 952 | ++a_pszSource; 953 | ++a_pszTest; 954 | } 955 | if (*a_pszSource == (SOCHAR)'-' || *a_pszTest == (SOCHAR)'-') { 956 | return 0; 957 | } 958 | 959 | // find matching number of characters in the strings 960 | int nLen = 0; 961 | while (*a_pszSource && IsEqual(*a_pszSource, *a_pszTest, nArgType)) { 962 | ++a_pszSource; 963 | ++a_pszTest; 964 | ++nLen; 965 | } 966 | 967 | // if we have exhausted the source... 968 | if (!*a_pszSource) { 969 | // and the test strings, then it's a perfect match 970 | if (!*a_pszTest) { 971 | return -1; 972 | } 973 | 974 | // otherwise the match failed as the test is longer than 975 | // the source. i.e. "--mant" will not match the option "--man". 976 | return 0; 977 | } 978 | 979 | // if we haven't exhausted the test string then it is not a match 980 | // i.e. "--mantle" will not best-fit match to "--mandate" at all. 981 | if (*a_pszTest) { 982 | return 0; 983 | } 984 | 985 | // partial match to the current length of the test string 986 | return nLen; 987 | } 988 | 989 | template 990 | bool 991 | CSimpleOptTempl::IsEqual( 992 | SOCHAR a_cLeft, 993 | SOCHAR a_cRight, 994 | int a_nArgType 995 | ) const 996 | { 997 | // if this matches then we are doing case-insensitive matching 998 | if (m_nFlags & a_nArgType) { 999 | if (a_cLeft >= 'A' && a_cLeft <= 'Z') a_cLeft += 'a' - 'A'; 1000 | if (a_cRight >= 'A' && a_cRight <= 'Z') a_cRight += 'a' - 'A'; 1001 | } 1002 | return a_cLeft == a_cRight; 1003 | } 1004 | 1005 | // calculate the number of characters that match (case-sensitive) 1006 | // 0 = no match, > 0 == number of characters, -1 == perfect match 1007 | template 1008 | SOCHAR ** 1009 | CSimpleOptTempl::MultiArg( 1010 | int a_nCount 1011 | ) 1012 | { 1013 | // ensure we have enough arguments 1014 | if (m_nNextOption + a_nCount > m_nLastArg) { 1015 | m_nLastError = SO_ARG_MISSING; 1016 | return NULL; 1017 | } 1018 | 1019 | // our argument array 1020 | SOCHAR ** rgpszArg = &m_argv[m_nNextOption]; 1021 | 1022 | // Ensure that each of the following don't start with an switch character. 1023 | // Only make this check if we are returning errors for unknown arguments. 1024 | if (!HasFlag(SO_O_NOERR)) { 1025 | for (int n = 0; n < a_nCount; ++n) { 1026 | SOCHAR ch = PrepareArg(rgpszArg[n]); 1027 | if (rgpszArg[n][0] == (SOCHAR)'-') { 1028 | rgpszArg[n][0] = ch; 1029 | m_nLastError = SO_ARG_INVALID_DATA; 1030 | return NULL; 1031 | } 1032 | rgpszArg[n][0] = ch; 1033 | } 1034 | } 1035 | 1036 | // all good 1037 | m_nNextOption += a_nCount; 1038 | return rgpszArg; 1039 | } 1040 | 1041 | 1042 | // --------------------------------------------------------------------------- 1043 | // TYPE DEFINITIONS 1044 | // --------------------------------------------------------------------------- 1045 | 1046 | /*! @brief ASCII/MBCS version of CSimpleOpt */ 1047 | typedef CSimpleOptTempl CSimpleOptA; 1048 | 1049 | /*! @brief wchar_t version of CSimpleOpt */ 1050 | typedef CSimpleOptTempl CSimpleOptW; 1051 | 1052 | #if defined(_UNICODE) 1053 | /*! @brief TCHAR version dependent on if _UNICODE is defined */ 1054 | # define CSimpleOpt CSimpleOptW 1055 | #else 1056 | /*! @brief TCHAR version dependent on if _UNICODE is defined */ 1057 | # define CSimpleOpt CSimpleOptA 1058 | #endif 1059 | 1060 | #endif // INCLUDED_SimpleOpt 1061 | --------------------------------------------------------------------------------