├── .gitignore ├── CHANGELOG.md ├── LICENSE.txt ├── README.md ├── dub.json ├── helper └── bootstrap.d └── src └── genPackageVersion ├── fetchDubInfo.d ├── fetchVersionInfo.d ├── genAll.d ├── genDModule.d ├── genDdocMacros.d ├── getopt.d ├── ignoreFiles.d ├── main.d └── util.d /.gitignore: -------------------------------------------------------------------------------- 1 | .dub/ 2 | bin/ 3 | dub.selections.json 4 | src/genPackageVersion/packageVersion.d 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | gen-package-version - ChangeLog 2 | =============================== 3 | 4 | (Dates below are YYYY/MM/DD) 5 | 6 | v1.0.6 - 2020/05/13 7 | ------------------- 8 | - **Update:** [#6](https://github.com/Abscissa/gen-package-version/pull/6): 9 | Updated Scriptlike dependency from v0.9.1 to v0.10.3 (@schveiguy, thanks also to @logicfish) 10 | - **Fixed:** [#6](https://github.com/Abscissa/gen-package-version/pull/6): 11 | Fix missing imports. (@schveiguy, thanks also to @logicfish) 12 | - **Fixed:** [#6](https://github.com/Abscissa/gen-package-version/pull/6): 13 | Fix outdated auto concatenation. (@schveiguy) 14 | 15 | v1.0.5 - 2016/09/09 16 | ------------------- 17 | - **Fixed:** Fix import-related deprecation messages for DMD 2.071. 18 | 19 | v1.0.4 - 2016/08/23 20 | ------------------- 21 | - **Enhancement:** Include dub.sdl samples, not just dub.json. 22 | 23 | v1.0.3 - 2015/08/17 24 | ------------------- 25 | - **Fixed:** Compile error for unittest builds. 26 | 27 | v1.0.2 - 2015/07/01 28 | ------------------- 29 | - **Enhancement:** Now works on DMD 2.066.1 (previously required 2.067.0 or up). 30 | 31 | v1.0.1 - 2015/06/28 32 | ------------------- 33 | - **Fixed:** Don't use a broken scriptlike release (v0.9.0), use v0.9.1 instead. 34 | 35 | v1.0.0 - 2015/06/27 36 | ------------------- 37 | - **Change:** The generated ```packageTimestamp``` is changed from [ISOExt](http://dlang.org/phobos/std_datetime.html#toISOExtString) format to human readable. The ISOExt formatted version is now called ```packageTimestampISO```. 38 | - **Change:** Value for ```--module``` is no longer allowed to contain periods. 39 | - **Enhancement:** Basic ability to be used as a library. See the [README](https://github.com/Abscissa/gen-package-version/blob/master/README.md) for details. 40 | - **Enhancement:** Add ```-r|--root``` to support projects in any directory, not just the current directory. 41 | - **Enhancement:** Minor improvements to ```--verbose``` and ```--trace``` outputs. 42 | - **Fixed:** Don't update the version file (and thus trigger a project rebuild) if the version file doesn't need updated. Bypass this check with the new ```--force``` flag. 43 | - **Fixed:** Don't rebuild gen-package-version if not needed. 44 | - **Fixed:** Failure on Windows when target project is on a different drive letter from current working directory. 45 | 46 | v0.9.4 - 2015/06/16 47 | ------------------- 48 | - **Enhancement:** Support detecting the version number via Mercurial (hg). 49 | - **Enhancement:** Support .hgignore for Mercurial working directories. 50 | 51 | v0.9.3 - 2015/06/15 52 | ------------------- 53 | - **Enhancement:** If detecting the version number via git fails, attempt to detect it via the current directory name (ex, ```~/.dub/packages/[project-name]-[version-tag]```). 54 | - **Enhancement:** Don't bother running git if there's no ```.git``` directory. 55 | - **Enhancement:** Bootstraps itself, so gen-package-version itself enjoys the following fix: 56 | - **Fixed:** Fails to detect version number for packages fetched by dub (since they lack ```.git```). 57 | 58 | v0.9.2 - 2015/06/14 59 | ------------------- 60 | - **Fixed:** The old recommended "preGenerateCommands" led to problems (project dependencies that use gen-package-version would run it from the wrong directory). 61 | 62 | v0.9.1 - 2015/06/14 63 | ------------------- 64 | - **Fixed:** ```helper/gen_version.sh``` isn't set as executable when checked out through dub. 65 | 66 | v0.9.0 - 2015/06/14 67 | ------------------- 68 | - **New:** Initial release. 69 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | gen-package-version is licensed under The zlib/libpng License: 2 | -------------------------------------------------------------- 3 | 4 | Copyright (c) 2015-2016 Nick Sabalausky 5 | 6 | This software is provided 'as-is', without any express or implied 7 | warranty. In no event will the authors be held liable for any damages 8 | arising from the use of this software. 9 | 10 | Permission is granted to anyone to use this software for any purpose, 11 | including commercial applications, and to alter it and redistribute it 12 | freely, subject to the following restrictions: 13 | 14 | 1. The origin of this software must not be misrepresented; you must not 15 | claim that you wrote the original software. If you use this software 16 | in a product, an acknowledgment in the product documentation would be 17 | appreciated but is not required. 18 | 19 | 2. Altered source versions must be plainly marked as such, and must not be 20 | misrepresented as being the original software. 21 | 22 | 3. This notice may not be removed or altered from any source 23 | distribution. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | gen-package-version 2 | =================== 3 | 4 | **NOTE:** If using together with DUB, you need DUB v1.0.0 or later. This project doesn't work with earlier versions of DUB due to [this](https://github.com/D-Programming-Language/dub/issues/616). 5 | 6 | Automatically generate a [D](http://dlang.org) module with version and timestamp information (detected from git or Mercurial/hg) every time your program or library is built. You can also generate a DDOC macro file (using the ```--ddoc=dir``` switch.) 7 | 8 | Even better, all your in-between builds will automatically have *their own* VCS-generated version number, including the VCS commit hash (for example: ```v1.2.0-1-g78f5cf9```). So there's never any confusion as to which "version" of v1.2.0 you're running! 9 | 10 | If detecting the version number via git/hg fails, gen-package-version will attempt to detect it via the currect directory name (ex, ```~/.dub/packages/[project-name]-[version-tag]```). 11 | 12 | By default, gen-package-version will NOT re-generate the output files if the only difference is the build timestamp. So it won't trigger unnecessary rebuilds of your project. 13 | 14 | [ [Changelog](https://github.com/Abscissa/gen-package-version/blob/master/CHANGELOG.md) ] 15 | 16 | To Use: 17 | ------- 18 | 19 | It's recommended to use [dub](http://code.dlang.org/getting_started) ([get dub here](http://code.dlang.org/download)). But if you wish, you can also forgo dub entirely (see the next section below). 20 | 21 | First, add the following to your project's [dub.sdl](http://code.dlang.org/package-format?lang=sdl) or [dub.json](http://code.dlang.org/package-format?lang=json): 22 | 23 | ``` 24 | dependency "gen-package-version" version="~>1.0.6" 25 | preGenerateCommands "dub run gen-package-version -- your.package.name --root=$PACKAGE_DIR --src=path/to/src" 26 | ``` 27 | 28 | ```json 29 | { 30 | "dependencies": { 31 | "gen-package-version": "~>1.0.6" 32 | }, 33 | "preGenerateCommands": 34 | ["dub run gen-package-version -- your.package.name --root=$PACKAGE_DIR --src=path/to/src"] 35 | } 36 | ``` 37 | 38 | Replace ```path/to/src``` with the path to your project's sources (most likely ```src``` or ```source```). 39 | 40 | Replace ```your.package.name``` with the name of your project's D package (ex: ```std```, ```deimos```, ```coolsoft.coolproduct.component1```, etc...). 41 | 42 | Optionally, you can replace ```--src=path/to/src``` with ```--dub```. Then, gen-package-version will use dub (via ```dub describe```) to automatically detect your source path and add some extra info in the packageVersion module it generates. More options are also available (see "Help Screen" below). 43 | 44 | Finally, make sure your project is tagged with a version number (if using git, it must be an "annotated" [tag](https://git-scm.com/book/en/v2/Git-Basics-Tagging), ie a tag with a message - doesn't matter what the message is). Example: 45 | 46 | ```bash 47 | $ git tag -a v1.2.0 -m 'Tag v1.2.0' 48 | or 49 | $ hg tag v1.2.0 50 | ``` 51 | 52 | That's it. Now your program will always be able to access its own version number (auto-detected from git) and build timestamp: 53 | 54 | ```d 55 | module your.package.name.main; 56 | 57 | import std.stdio; 58 | import your.package.name.packageVersion; 59 | 60 | void main() 61 | { 62 | writeln("My Cool Program ", packageVersion); 63 | writeln("Built on ", packageTimestamp); 64 | 65 | // Only works of you used "--dub" 66 | //writeln(`The "name" field in my dub.json is: `, packageName); 67 | } 68 | ``` 69 | 70 | Every time you tag a new release (remember, annotated tag), your program will automatically know its new version number! Even builds from between releases will be easily distinguished. 71 | 72 | If your project is a library, your *library's users* can also query the version of your lib: 73 | 74 | ```d 75 | module myApp.main; 76 | 77 | import std.stdio; 78 | import myApp.packageVersion; 79 | static import coolLib.packageVersion; 80 | 81 | void main() 82 | { 83 | writeln("My App ", packageVersion, "(@ ", packageTimestamp, ")"); 84 | 85 | writeln("Using coolLib ", coolLib.packageVersion.packageVersion); 86 | writeln(" coolLib built @", coolLib.packageVersion.packageTimestamp); 87 | } 88 | ``` 89 | 90 | By default, gen-package-version automatically adds the generated ```packageVersion.d``` file to your ```.gitignore``` (or creates it if you don't have one). This helps ensure the file's changes don't clutter your project's pull requests. If you'd rather gen-package-version left your ```.gitignore``` file alone, just include the ```--no-ignore-file``` flag. 91 | 92 | Your project isn't built with dub? 93 | ---------------------------------- 94 | 95 | No prob! Just download and compile gen-package-version, then run it from your buildscript (or in your IDE's "Project Pre-Build Steps"). 96 | 97 | Download and compile using dub ([get dub](http://code.dlang.org/download)): 98 | ```bash 99 | $ dub fetch gen-package-version 100 | $ dub build gen-package-version 101 | 102 | # Add this to your project's buildscript: 103 | # dub run gen-package-version -- your.package.name --src=path/to/src 104 | ``` 105 | 106 | Or download and compile with no dub needed at all: 107 | ```bash 108 | $ git clone https://github.com/Abscissa/gen-package-version.git 109 | $ cd gen-package-version 110 | $ git checkout v1.0.6 # Or newer 111 | 112 | $ git clone https://github.com/Abscissa/scriptlike.git 113 | $ cd scriptlike 114 | $ git checkout v0.9.1 # Or newer 115 | $ cd .. 116 | 117 | $ rdmd --build-only -ofbin/gen-package-version -Isrc/ -Iscriptlike/src src/genPackageVersion/main.d 118 | 119 | # Add this to your project's buildscript: 120 | # [path/to/gen-package-version/]bin/gen-package-version your.package.name --src=path/to/src 121 | ``` 122 | 123 | Using as a library 124 | ------------------ 125 | Although support is very basic, gen-package-version can be used as a library instead of an executable: 126 | 127 | In your ```dub.sdl``` or ```dub.json``` (if using DUB): 128 | ``` 129 | dependency "gen-package-version" version="~>1.0.6" 130 | subConfiguration "gen-package-version" "library" 131 | ``` 132 | ```json 133 | "dependencies": { 134 | "gen-package-version": "~>1.0.6" 135 | }, 136 | "subConfigurations": { 137 | "gen-package-version": "library" 138 | } 139 | ``` 140 | 141 | In your source file: 142 | ```d 143 | import genPackageVersion.genAll; 144 | import scriptlike.fail : Fail; 145 | 146 | try { 147 | // Just like running: 148 | // gen-package-version foo.bar --src=source/dir 149 | genPackageVersionMain(["dummy-exe-name", "foo.bar", "--src=source/dir"]); 150 | } 151 | catch(Fail e) { 152 | // Do whatever. 153 | // 154 | // Or don't catch it at all to cleanly bail with appropriate 155 | // output message and no ugly stack trace. 156 | } 157 | ``` 158 | 159 | Or, you can bypass gen-package-version's commandline arg parsing and just set the flags directly in ```genPackageVersion.util```. Then call ```void genPackageVersion.genAll.generateAll(void)```. But unlike ```genPackageVersionMain(args)```, this approach is undocumented (unless you generate the docs with DDOC or DDOX yourself) and subject to change. 160 | 161 | Help Screen 162 | ----------- 163 | View this help screen with ```dub run gen-package-version -- --help``` or ```gen-package-version --help```: 164 | 165 | ``` 166 | gen-package-version v1.0.6 167 | 168 | ------------------------------------------------- 169 | Generates a D module with version information automatically-detected 170 | from git or hg and (optionally) dub. This generated D file is automatically 171 | added to .gitignore/.hgignore if necessary (unless using --no-ignore-file). 172 | 173 | It is recommended to run this via DUB's preGenerateCommands by copy/pasting the 174 | following lines into your project's dub.sdl (make sure to edit "path/to/src" 175 | and "your.package.name" as needed): 176 | 177 | dependency "gen-package-version" version="~>1.0.6" 178 | preGenerateCommands \ 179 | "dub run gen-package-version -- your.package.name --root=$PACKAGE_DIR --src=path/to/src" 180 | 181 | Or dub.json: 182 | 183 | "dependencies": { 184 | "gen-package-version": "~>1.0.6" 185 | }, 186 | "preGenerateCommands": 187 | ["dub run gen-package-version -- your.package.name --root=$PACKAGE_DIR --src=path/to/src"] 188 | 189 | USAGE: 190 | gen-package-version [options] your.package.name --src=path/to/src 191 | gen-package-version [options] your.package.name --dub 192 | 193 | EXAMPLES: 194 | gen-package-version foo.bar --src=source/dir 195 | Generates module "foo.bar.packageVersion" in the file: 196 | source/dir/foo/bar/packageVersion.d 197 | 198 | Access the info from your program via: 199 | 200 | import foo.bar.packageVersion; 201 | writeln("Version: ", packageVersion); 202 | writeln("Built on: ", packageTimestamp); 203 | 204 | gen-package-version foo.bar --src=source/dir --ddoc=ddoc/dir 205 | Same as above, but also generates a DDOC macro file: 206 | ddoc/dir/packageVersion.ddoc 207 | 208 | Which defines the macros: $(FOO_BAR_VERSION), $(FOO_BAR_TIMESTAMP) 209 | and $(FOO_BAR_TIMESTAMP_ISO). 210 | 211 | gen-package-version foo.bar --dub 212 | Generates module "foo.bar.packageVersion" in the file: 213 | (your_src_dir)/foo/bar/packageVersion.d 214 | 215 | Where (your_src_dir) above is auto-detected via "dub describe". 216 | The first path in "importPaths" is assumed to be (your_src_dir). 217 | 218 | Additional info is available when using --dub: 219 | 220 | writeln("This program's name is ", packageName); 221 | 222 | Note that even if --dub isn't used, gen-package-version might still run dub 223 | anyway if detecting the version through git/hg fails (for example, if the 224 | package is not in a VCS-controlled working directory, such as the case when 225 | a package is downloaded via dub). 226 | 227 | OPTIONS: 228 | --dub Use dub. May be slightly slower, but allows --src to be auto-detected, and adds extra info to the generated module. 229 | -s --src = VALUE Path to source files. Required unless --dub is used. 230 | -r --root = VALUE Path to root of project directory. Default: Current directory 231 | --module = VALUE Override the module name. Default: packageVersion 232 | --ddoc = VALUE Generate a DDOC macro file in the directory 'VALUE'. 233 | --no-ignore-file Do not attempt to update .gitignore/.hgignore 234 | --force Force overwriting the output file, even is it's up-to-date. 235 | --dry-run Dry run. Don't actually write or modify any files. Implies --trace 236 | -q --quiet Quiet mode 237 | -v --verbose Verbose mode 238 | --trace Extremely verbose mode (for debugging) 239 | --version Show this program's version number and exit 240 | -h --help This help information. 241 | ``` 242 | -------------------------------------------------------------------------------- /dub.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gen-package-version", 3 | "description": "Generate a D module with version information automatically-detected from git/hg.", 4 | "authors": ["Nick Sabalausky"], 5 | "homepage": "https://github.com/Abscissa/gen-package-version", 6 | "license": "zlib/libpng", 7 | 8 | "targetPath": "bin", 9 | "targetName": "gen-package-version", 10 | "sourcePaths": ["src"], 11 | "dependencies": { 12 | "scriptlike": "~>0.10.3" 13 | }, 14 | "preGenerateCommands-posix": 15 | ["cd $PACKAGE_DIR && rdmd helper/bootstrap.d $SCRIPTLIKE_PACKAGE_DIR"], 16 | "preGenerateCommands-windows": 17 | ["cd /D $PACKAGE_DIR && rdmd helper/bootstrap.d $SCRIPTLIKE_PACKAGE_DIR"], 18 | 19 | "configurations": [ 20 | { 21 | "name": "application", 22 | "targetType": "executable", 23 | "mainSourceFile": "src/genPackageVersion/main.d" 24 | }, 25 | { 26 | "name": "library", 27 | "targetType": "library", 28 | "excludedSourceFiles": ["src/genPackageVersion/main.d"] 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /helper/bootstrap.d: -------------------------------------------------------------------------------- 1 | import std.algorithm; 2 | import std.exception; 3 | import std.file; 4 | import std.path; 5 | import std.process; 6 | import std.stdio; 7 | 8 | immutable versionFile = "src/genPackageVersion/packageVersion.d"; 9 | 10 | immutable bootstrapVersionFileContent = 11 | q{module genPackageVersion.packageVersion; 12 | enum packageVersion = "bootstrap"; 13 | }; 14 | 15 | int main(string[] args) 16 | { 17 | enforce(args.length == 2, 18 | "Wrong number of args. Usage: (this_program) (path_to_scriptlike/)"); 19 | 20 | // Create default version file 21 | if(!versionFile.exists) 22 | std.file.write(versionFile, bootstrapVersionFileContent); 23 | 24 | // Ensure trailing slash 25 | auto scriptlikePath = args[1]; 26 | if(!scriptlikePath.endsWith(dirSeparator)) 27 | scriptlikePath ~= dirSeparator; 28 | 29 | // Bootstrap 30 | return spawnShell( 31 | `rdmd -ofbin/bootstrap -Isrc -I`~scriptlikePath~`src `~ 32 | `src/genPackageVersion/main.d genPackageVersion --src=src` 33 | ).wait(); 34 | } 35 | -------------------------------------------------------------------------------- /src/genPackageVersion/fetchDubInfo.d: -------------------------------------------------------------------------------- 1 | /// Fetch package information from DUB. 2 | module genPackageVersion.fetchDubInfo; 3 | 4 | import std.algorithm; 5 | import std.array; 6 | import std.json; 7 | 8 | import scriptlike.only; 9 | import genPackageVersion.util; 10 | 11 | /// Auto-detects srcDir if srcDir doesn't already have a value. 12 | /// Returns D source code to be appended to the output file. 13 | string generateDubExtras(ref string srcDir) 14 | { 15 | auto jsonRoot = getPackageJsonInfo(); 16 | auto rootPackageName = jsonRoot["rootPackage"].str; 17 | logTrace("rootPackageName: ", rootPackageName); 18 | 19 | auto packageInfo = jsonRoot.getPackageInfo(rootPackageName); 20 | auto targetName = packageInfo["targetName"].str; 21 | 22 | if(!srcDir) 23 | { 24 | // Auto-detect srcDir 25 | auto importPaths = packageInfo["importPaths"].array.map!(val => val.str).array(); 26 | logTrace("importPaths: ", importPaths); 27 | 28 | failEnforce(importPaths.length > 0, 29 | "Unable to autodetect source directory: Import path not found in 'dub describe'."); 30 | 31 | srcDir = importPaths[0]; 32 | logNormal("Detected source directory: ", srcDir); 33 | } 34 | 35 | return 36 | ` 37 | /++ 38 | DUB package name of this project. 39 | Ie, dub.json's "name" field. 40 | +/ 41 | enum packageName = "`~rootPackageName~`"; 42 | 43 | /++ 44 | Name of this project's target binary, minus extensions and prefixes. 45 | Ie, dub.json's "targetName" field. 46 | 47 | Note that depending on your needs, it may be better to 48 | use std.file.thisExePath() 49 | +/ 50 | enum packageTargetName = "`~targetName~`"; 51 | `; 52 | } 53 | 54 | /// Obtains package info via "dub describe". 55 | JSONValue getPackageJsonInfo() 56 | { 57 | static isCached = false; 58 | static JSONValue jsonRoot; 59 | 60 | if(!isCached) 61 | { 62 | auto rawJson = runCollect("dub describe"); 63 | jsonRoot = parseJSON( rawJson ); 64 | isCached = true; 65 | } 66 | 67 | return jsonRoot; 68 | } 69 | 70 | /// Get the JSON subtree for a specific package. 71 | JSONValue getPackageInfo(JSONValue dubInfo, string packageName) 72 | { 73 | auto packages = dubInfo["packages"].array; 74 | 75 | foreach(pack; packages) 76 | if(pack["name"].str) 77 | return pack; 78 | 79 | fail("Package '"~packageName~"' not found. Received bad data from 'dub describe'."); 80 | assert(0); 81 | } 82 | -------------------------------------------------------------------------------- /src/genPackageVersion/fetchVersionInfo.d: -------------------------------------------------------------------------------- 1 | /// Obtains package version string from various sources 2 | module genPackageVersion.fetchVersionInfo; 3 | 4 | import std.algorithm; 5 | import std.array; 6 | import std.json; 7 | 8 | import scriptlike.only; 9 | 10 | import genPackageVersion.fetchDubInfo; 11 | import genPackageVersion.util; 12 | 13 | /// Obtain the version 14 | string getVersionStr() 15 | { 16 | string ver; 17 | 18 | // Try "git describe" 19 | ver = getVersionStrGit(); 20 | 21 | // Try Mersurial 22 | if(ver.empty) 23 | ver = getVersionStrHg(); 24 | 25 | // Try checking the name of the directory (ex, for packages fetched by dub) 26 | if(ver.empty) 27 | ver = getVersionStrInferFromDir(); 28 | 29 | // Found nothing? 30 | if(ver.empty) 31 | ver = "unknown-ver"; 32 | 33 | return ver; 34 | } 35 | 36 | /// Attempt to get the version from git 37 | string getVersionStrGit() 38 | { 39 | import std.string : strip; 40 | 41 | // Don't bother running git if it's not even a git working directory 42 | if(!detectedGit) 43 | return null; 44 | 45 | auto result = tryRunCollect("git describe"); 46 | if(!result.status) 47 | return result.output.strip(); 48 | 49 | return null; 50 | } 51 | 52 | /// Attempt to get the version from Mercurial 53 | string getVersionStrHg() 54 | { 55 | // Don't bother running hg if it's not even an hg working directory 56 | if(!detectedHg) 57 | return null; 58 | 59 | auto result = tryRunCollect(`hg log -r . --template '{latesttag}-{latesttagdistance}-{node|short}'`); 60 | if(!result.status) 61 | { 62 | auto parts = result.output.split("-"); 63 | if(parts.length < 3) // Unexpected 64 | return null; 65 | 66 | // latesttagdistance == 0? 67 | if(parts[$-2] == "0") 68 | return parts[0..$-2].join("-"); // Return *only* the {latesttag} part 69 | else 70 | return result.output; // Return the whole thing 71 | } 72 | 73 | return null; 74 | } 75 | 76 | /// Attempt to get the version by inferring from current directory name. 77 | string getVersionStrInferFromDir() 78 | { 79 | import std.string : chompPrefix, isNumeric; 80 | 81 | JSONValue jsonRoot; 82 | try 83 | jsonRoot = getPackageJsonInfo(); 84 | catch(Exception e) // If "dub describe" failed 85 | return null; 86 | 87 | auto rootPackageName = jsonRoot["rootPackage"].str; 88 | auto currDir = getcwd().baseName().toString(); 89 | logTrace("rootPackageName: ", rootPackageName); 90 | logTrace("currDir: ", currDir); 91 | 92 | auto prefix = rootPackageName ~ "-"; 93 | if(currDir.startsWith(prefix)) 94 | { 95 | auto versionPortion = currDir.chompPrefix(prefix); 96 | if(!versionPortion.empty) 97 | { 98 | if(isNumeric(versionPortion[0..1])) 99 | return "v"~versionPortion; 100 | else 101 | return versionPortion; 102 | } 103 | } 104 | 105 | return null; 106 | } 107 | -------------------------------------------------------------------------------- /src/genPackageVersion/genAll.d: -------------------------------------------------------------------------------- 1 | /// Main entry point for gen-package-version 2 | module genPackageVersion.genAll; 3 | 4 | import std.algorithm; 5 | import std.array; 6 | import std.stdio; 7 | import genPackageVersion.getopt; 8 | 9 | import scriptlike.only; 10 | 11 | import genPackageVersion.fetchDubInfo; 12 | import genPackageVersion.fetchVersionInfo; 13 | import genPackageVersion.genDdocMacros; 14 | import genPackageVersion.genDModule; 15 | import genPackageVersion.ignoreFiles; 16 | import genPackageVersion.util; 17 | 18 | static import genPackageVersionInfo = genPackageVersion.packageVersion; 19 | 20 | /// The --help message 21 | immutable helpBanner = ( 22 | `gen-package-version `~genPackageVersionInfo.packageVersion~` 23 | 24 | ------------------------------------------------- 25 | Generates a D module with version information automatically-detected 26 | from git or hg and (optionally) dub. This generated D file is automatically 27 | added to .gitignore/.hgignore if necessary (unless using --no-ignore-file). 28 | 29 | It is recommended to run this via DUB's preGenerateCommands by copy/pasting the 30 | following lines into your project's dub.sdl (make sure to edit "path/to/src" 31 | and "your.package.name" as needed): 32 | 33 | dependency "gen-package-version" version="~>1.0.4" 34 | preGenerateCommands \ 35 | "dub run gen-package-version -- your.package.name --root=$PACKAGE_DIR --src=path/to/src" 36 | 37 | Or dub.json: 38 | 39 | "dependencies": { 40 | "gen-package-version": "~>1.0.4" 41 | }, 42 | "preGenerateCommands": 43 | ["dub run gen-package-version -- your.package.name --root=$PACKAGE_DIR --src=path/to/src"] 44 | 45 | USAGE: 46 | gen-package-version [options] your.package.name --src=path/to/src 47 | gen-package-version [options] your.package.name --dub 48 | 49 | EXAMPLES: 50 | gen-package-version foo.bar --src=source/dir 51 | Generates module "foo.bar.packageVersion" in the file: 52 | source/dir/foo/bar/packageVersion.d 53 | 54 | Access the info from your program via: 55 | 56 | import foo.bar.packageVersion; 57 | writeln("Version: ", packageVersion); 58 | writeln("Built on: ", packageTimestamp); 59 | 60 | gen-package-version foo.bar --src=source/dir --ddoc=ddoc/dir 61 | Same as above, but also generates a DDOC macro file: 62 | ddoc/dir/packageVersion.ddoc 63 | 64 | Which defines the macros: $(FOO_BAR_VERSION), $(FOO_BAR_TIMESTAMP) 65 | and $(FOO_BAR_TIMESTAMP_ISO). 66 | 67 | gen-package-version foo.bar --dub 68 | Generates module "foo.bar.packageVersion" in the file: 69 | (your_src_dir)/foo/bar/packageVersion.d 70 | 71 | Where (your_src_dir) above is auto-detected via "dub describe". 72 | The first path in "importPaths" is assumed to be (your_src_dir). 73 | 74 | Additional info is available when using --dub: 75 | 76 | writeln("This program's name is ", packageName); 77 | 78 | Note that even if --dub isn't used, gen-package-version might still run dub 79 | anyway if detecting the version through git/hg fails (for example, if the 80 | package is not in a VCS-controlled working directory, such as the case when 81 | a package is downloaded via dub). 82 | 83 | OPTIONS:`).replace("\t", " "); 84 | 85 | /// Main entry point for genPackageVersion 86 | void genPackageVersionMain(string[] args) 87 | { 88 | // Handle args 89 | if(!doGetOpt(args)) 90 | return; 91 | 92 | try 93 | generateAll(); 94 | catch(ErrorLevelException e) 95 | fail(e.msg); 96 | 97 | return; 98 | } 99 | 100 | /// Returns: Should program execution continue? 101 | bool doGetOpt(ref string[] args) 102 | { 103 | immutable usageHint = "For usage, run: gen-package-version --help"; 104 | bool showVersion; 105 | 106 | try 107 | { 108 | auto helpInfo = args.getopt( 109 | "dub", " Use dub. May be slightly slower, but allows --src to be auto-detected, and adds extra info to the generated module.", &useDub, 110 | "s|src", "= VALUE Path to source files. Required unless --dub is used.", &projectSourcePath, 111 | "r|root", "= VALUE Path to root of project directory. Default: Current directory", &rootPath, 112 | "module", "= VALUE Override the module name. Default: packageVersion", &outModuleName, 113 | "ddoc", "= VALUE Generate a DDOC macro file in the directory 'VALUE'.", &ddocDir, 114 | "no-ignore-file", " Do not attempt to update .gitignore/.hgignore", &noIgnoreFile, 115 | "force", " Force overwriting the output file, even is it's up-to-date.", &force, 116 | "dry-run", " Dry run. Don't actually write or modify any files. Implies --trace", 117 | { logLevel = LogLevel.trace; scriptlikeEcho = true; dryRun = true;}, 118 | //"silent", " Silence all non-error output", { logLevel = LogLevel.silent; }, 119 | "q|quiet", " Quiet mode", { logLevel = LogLevel.quiet; }, 120 | "v|verbose", " Verbose mode", { logLevel = LogLevel.verbose; }, 121 | "trace", " Extremely verbose mode (for debugging)", { logLevel = LogLevel.trace; scriptlikeEcho = true; }, 122 | //"log-level", " Verbosity level: --log-level=silent|quiet|normal|verbose|trace", &logLevel, 123 | "version", " Show this program's version number and exit", &showVersion, 124 | ); 125 | 126 | if(helpInfo.helpWanted) 127 | { 128 | defaultGetoptPrinter(helpBanner, helpInfo.options); 129 | return false; 130 | } 131 | } 132 | catch(GetOptException e) 133 | fail(e.msg, "\n", usageHint); 134 | 135 | if(showVersion) 136 | { 137 | writeln(genPackageVersionInfo.packageVersion); 138 | return false; 139 | } 140 | 141 | failEnforce(args.length == 2 && !args[1].empty, "Missing package name\n", usageHint); 142 | outPackageName = args[1]; 143 | 144 | failEnforce(projectSourcePath || useDub, 145 | "Missing --src= (Alternatively, you could use --dub to auto-detect --src=)\n", usageHint); 146 | 147 | failEnforce(!outModuleName.canFind("."), 148 | "Module name cannot include '.'\n", 149 | "Instead of --module=", outModuleName, ", try using --module=", 150 | outModuleName.replace(".", "_"), "\n", 151 | usageHint); 152 | 153 | return true; 154 | } 155 | 156 | /// After cmdline args have been processed, this does all the main work. 157 | void generateAll() 158 | { 159 | import std.datetime; 160 | 161 | auto originalWorkingDir = getcwd(); 162 | scope(exit) chdir(originalWorkingDir); 163 | chdir(rootPath); 164 | 165 | detectTools(); 166 | 167 | // Grab basic info 168 | auto versionStr = getVersionStr(); 169 | logTrace("versionStr: ", versionStr); 170 | 171 | auto now = Clock.currTime; 172 | auto nowStr = now.toString(); 173 | auto nowISOStr = now.toISOExtString(); 174 | 175 | // Generate dub extras 176 | string dubExtras; 177 | if(useDub) 178 | dubExtras = generateDubExtras(projectSourcePath); 179 | 180 | // Generate D module 181 | auto dModulePath = generateDModule(outPackageName, outModuleName, versionStr, nowStr, nowISOStr, dubExtras); 182 | 183 | // Generate DDOC macros 184 | string ddocPath; 185 | if(ddocDir) 186 | ddocPath = generateDdocMacros(ddocDir, outPackageName, outModuleName, versionStr, nowStr, nowISOStr); 187 | 188 | // Update VCS ignore files 189 | if(!noIgnoreFile) 190 | { 191 | addToIgnoreFiles(dModulePath); 192 | 193 | if(ddocPath) 194 | addToIgnoreFiles(ddocPath); 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /src/genPackageVersion/genDModule.d: -------------------------------------------------------------------------------- 1 | /// Generates a D module for package version info. 2 | module genPackageVersion.genDModule; 3 | 4 | import std.array; 5 | import scriptlike.only; 6 | 7 | import genPackageVersion.util; 8 | import genPackageVersion.packageVersion; 9 | 10 | /// Returns path to the output file that was (or would've been) written. 11 | string generateDModule(string packageName, string moduleName, 12 | string ver, string timestamp, string timestampIso, string dubExtras) 13 | { 14 | // Generate D source code 15 | auto dModule = 16 | `/++ 17 | Generated at `~timestamp~` 18 | by gen-package-version `~packageVersion~`: 19 | $(LINK https://github.com/Abscissa/gen-package-version) 20 | +/ 21 | module `~outPackageName~`.`~outModuleName~`; 22 | 23 | /++ 24 | Version of this package. 25 | +/ 26 | enum packageVersion = "`~ver~`"; 27 | 28 | /++ 29 | Human-readable timestamp of when this module was generated. 30 | +/ 31 | enum packageTimestamp = "`~timestamp~`"; 32 | 33 | /++ 34 | Timestamp of when this module was generated, as an ISO Ext string. 35 | Get a SysTime from this via: 36 | 37 | ------ 38 | std.datetime.fromISOExtString(packageTimestampISO) 39 | ------ 40 | +/ 41 | enum packageTimestampISO = "`~timestampIso~`"; 42 | `~dubExtras; 43 | //logTrace("--------------------------------------"); 44 | //logTrace(dModule); 45 | //logTrace("--------------------------------------"); 46 | 47 | import std.path : stdBuildPath = buildPath, stdDirName = dirName, dirSeparator; 48 | import scriptlike.file : scriptlikeRead = read, scriptlikeWrite = write; 49 | 50 | // Determine output filepath 51 | auto packagePath = outPackageName.replace(".", dirSeparator); 52 | auto outPath = stdBuildPath(projectSourcePath, packagePath, outModuleName) ~ ".d"; 53 | logTrace("outPath: ", outPath); 54 | 55 | // Ensure directory for output file exits 56 | auto outDir = stdDirName(outPath); 57 | failEnforce(exists(Path(outDir)), "Output directory doesn't exist: ", outDir); 58 | failEnforce(isDir(Path(outDir)), "Output directory isn't a directory: ", outDir); 59 | 60 | // Check whether output file should be updated 61 | if(force) 62 | logVerbose(`--force used, skipping "up-to-date" check`); 63 | else 64 | { 65 | if(existsAsFile(outPath)) 66 | { 67 | import std.regex; 68 | 69 | auto existingModule = cast(string) scriptlikeRead(Path(outPath)); 70 | auto adjustedExistingModule = existingModule 71 | .replaceFirst(regex(`Generated at [^\n]*\n`), `Generated at `~timestamp~"\n") 72 | .replaceFirst(regex(`packageTimestamp = "[^"]*";`), `packageTimestamp = "`~timestamp~`";`) 73 | .replaceFirst(regex(`packageTimestampISO = "[^"]*";`), `packageTimestampISO = "`~timestampIso~`";`); 74 | 75 | if(adjustedExistingModule == dModule) 76 | { 77 | logVerbose("Existing version file is up-to-date, skipping overwrite of ", outPath); 78 | return outPath; 79 | } 80 | } 81 | } 82 | 83 | // Write the file 84 | logVerbose("Saving to ", outPath); 85 | if(!dryRun) 86 | { 87 | try 88 | scriptlikeWrite(outPath, dModule); 89 | catch(FileException e) 90 | fail(e.msg); 91 | } 92 | 93 | return outPath; 94 | } 95 | -------------------------------------------------------------------------------- /src/genPackageVersion/genDdocMacros.d: -------------------------------------------------------------------------------- 1 | /// Generates a DDOC macros file for package version info. 2 | module genPackageVersion.genDdocMacros; 3 | 4 | import std.array; 5 | import std.string : toUpper; 6 | 7 | import scriptlike.only; 8 | import genPackageVersion.util; 9 | 10 | /// Returns path to the output file that was (or would've been) written. 11 | string generateDdocMacros(string outDir, string packageName, string moduleName, 12 | string ver, string timestamp, string timestampIso) 13 | { 14 | import std.path : buildPath; 15 | auto macroPrefix = packageName.toUpper().replace(".", "_"); 16 | 17 | // Ensure directory for output file exits 18 | failEnforce(exists(Path(outDir)), "DDOC output directory doesn't exist: ", outDir); 19 | failEnforce(isDir(Path(outDir)), "DDOC output directory isn't a directory: ", outDir); 20 | 21 | // Determine output filepath 22 | auto outPath = buildPath(outDir, moduleName) ~ ".ddoc"; 23 | logTrace("ddoc outPath: ", outPath); 24 | 25 | // Generate DDOC macro code 26 | auto newDdoc = 27 | `Ddoc 28 | 29 | Macros: 30 | `~macroPrefix~`_VERSION = `~ver~` 31 | `~macroPrefix~`_TIMESTAMP = `~timestamp~` 32 | `~macroPrefix~`_TIMESTAMP_ISO = `~timestampIso~` 33 | `; 34 | 35 | import scriptlike.file : scriptlikeRead = read, scriptlikeWrite = write; 36 | 37 | // Check whether output file should be updated 38 | if(force) 39 | logVerbose(`--force used, skipping ddoc "up-to-date" check`); 40 | else 41 | { 42 | if(existsAsFile(outPath)) 43 | { 44 | import std.regex; 45 | 46 | auto existingDdoc = cast(string) scriptlikeRead(Path(outPath)); 47 | auto adjustedExistingDdoc = existingDdoc 48 | .replaceFirst(regex(`_TIMESTAMP = [^\n]*\n`), `_TIMESTAMP = `~timestamp~"\n") 49 | .replaceFirst(regex(`_TIMESTAMP_ISO = [^\n]*\n`), `_TIMESTAMP_ISO = `~timestampIso~"\n"); 50 | 51 | if(adjustedExistingDdoc == newDdoc) 52 | { 53 | logVerbose("Existing ddoc version macro file is up-to-date, skipping overwrite of ", outPath); 54 | return outPath; 55 | } 56 | } 57 | } 58 | 59 | // Write the file 60 | logVerbose("Saving to ", outPath); 61 | if(!dryRun) 62 | { 63 | try 64 | scriptlikeWrite(outPath, newDdoc); 65 | catch(FileException e) 66 | fail(e.msg); 67 | } 68 | 69 | return outPath; 70 | } 71 | -------------------------------------------------------------------------------- /src/genPackageVersion/getopt.d: -------------------------------------------------------------------------------- 1 | // NOTE: This is borrowed from Phobos 2.067.1 for the sake of allowing 2 | // gen-package-version to compile on earlier versions of DMD. 3 | 4 | // Written in the D programming language. 5 | 6 | /** 7 | Processing of command line options. 8 | 9 | The getopt module implements a $(D getopt) function, which adheres to 10 | the POSIX syntax for command line options. GNU extensions are 11 | supported in the form of long options introduced by a double dash 12 | ("--"). Support for bundling of command line options, as was the case 13 | with the more traditional single-letter approach, is provided but not 14 | enabled by default. 15 | 16 | Macros: 17 | 18 | WIKI = Phobos/StdGetopt 19 | 20 | Copyright: Copyright Andrei Alexandrescu 2008 - 2009. 21 | License: $(WEB www.boost.org/LICENSE_1_0.txt, Boost License 1.0). 22 | Authors: $(WEB erdani.org, Andrei Alexandrescu) 23 | Credits: This module and its documentation are inspired by Perl's $(WEB 24 | perldoc.perl.org/Getopt/Long.html, Getopt::Long) module. The syntax of 25 | D's $(D getopt) is simpler than its Perl counterpart because $(D 26 | getopt) infers the expected parameter types from the static types of 27 | the passed-in pointers. 28 | Source: $(PHOBOSSRC std/_getopt.d) 29 | */ 30 | /* 31 | Copyright Andrei Alexandrescu 2008 - 2009. 32 | Distributed under the Boost Software License, Version 1.0. 33 | (See accompanying file LICENSE_1_0.txt or copy at 34 | http://www.boost.org/LICENSE_1_0.txt) 35 | */ 36 | module genPackageVersion.getopt; 37 | 38 | import std.traits; 39 | 40 | /** 41 | * Thrown on one of the following conditions: 42 | * - An unrecognized command-line argument is passed 43 | * and $(D std.getopt.config.passThrough) was not present. 44 | */ 45 | class GetOptException : Exception 46 | { 47 | @safe pure nothrow 48 | this(string msg, string file = __FILE__, size_t line = __LINE__) 49 | { 50 | super(msg, file, line); 51 | } 52 | } 53 | 54 | /** 55 | Parse and remove command line options from an string array. 56 | 57 | Synopsis: 58 | 59 | --------- 60 | import std.getopt; 61 | 62 | string data = "file.dat"; 63 | int length = 24; 64 | bool verbose; 65 | enum Color { no, yes }; 66 | Color color; 67 | 68 | void main(string[] args) 69 | { 70 | auto helpInformation = getopt( 71 | args, 72 | "length", &length, // numeric 73 | "file", &data, // string 74 | "verbose", &verbose, // flag 75 | "color", "Information about this color", &color); // enum 76 | ... 77 | 78 | if (helpInformation.helpWanted) 79 | { 80 | defaultGetoptPrinter("Some information about the program.", 81 | helpInformation.options); 82 | } 83 | } 84 | --------- 85 | 86 | The $(D getopt) function takes a reference to the command line 87 | (as received by $(D main)) as its first argument, and an 88 | unbounded number of pairs of strings and pointers. Each string is an 89 | option meant to "fill" the value pointed-to by the pointer to its 90 | right (the "bound" pointer). The option string in the call to 91 | $(D getopt) should not start with a dash. 92 | 93 | In all cases, the command-line options that were parsed and used by 94 | $(D getopt) are removed from $(D args). Whatever in the 95 | arguments did not look like an option is left in $(D args) for 96 | further processing by the program. Values that were unaffected by the 97 | options are not touched, so a common idiom is to initialize options 98 | to their defaults and then invoke $(D getopt). If a 99 | command-line argument is recognized as an option with a parameter and 100 | the parameter cannot be parsed properly (e.g. a number is expected 101 | but not present), a $(D ConvException) exception is thrown. 102 | If $(D std.getopt.config.passThrough) was not passed to getopt 103 | and an unrecognized command-line argument is found, a $(D GetOptException) 104 | is thrown. 105 | 106 | Depending on the type of the pointer being bound, $(D getopt) 107 | recognizes the following kinds of options: 108 | 109 | $(OL 110 | $(LI $(I Boolean options). A lone argument sets the option to $(D true). 111 | Additionally $(B true) or $(B false) can be set within the option separated 112 | with an "=" sign: 113 | 114 | --------- 115 | bool verbose = false, debugging = true; 116 | getopt(args, "verbose", &verbose, "debug", &debugging); 117 | --------- 118 | 119 | To set $(D verbose) to $(D true), invoke the program with either 120 | $(D --verbose) or $(D --verbose=true). 121 | 122 | To set $(D debugging) to $(D false), invoke the program with 123 | $(D --debugging=false). 124 | ) 125 | 126 | $(LI $(I Numeric options.) If an option is bound to a numeric type, a 127 | number is expected as the next option, or right within the option separated 128 | with an "=" sign: 129 | 130 | --------- 131 | uint timeout; 132 | getopt(args, "timeout", &timeout); 133 | --------- 134 | 135 | To set $(D timeout) to $(D 5), invoke the program with either 136 | $(D --timeout=5) or $(D --timeout 5). 137 | ) 138 | 139 | $(LI $(I Incremental options.) If an option name has a "+" suffix and is 140 | bound to a numeric type, then the option's value tracks the number of times 141 | the option occurred on the command line: 142 | 143 | --------- 144 | uint paranoid; 145 | getopt(args, "paranoid+", ¶noid); 146 | --------- 147 | 148 | Invoking the program with "--paranoid --paranoid --paranoid" will set $(D 149 | paranoid) to 3. Note that an incremental option never expects a parameter, 150 | e.g. in the command line "--paranoid 42 --paranoid", the "42" does not set 151 | $(D paranoid) to 42; instead, $(D paranoid) is set to 2 and "42" is not 152 | considered as part of the normal program arguments. 153 | ) 154 | 155 | $(LI $(I Enum options.) If an option is bound to an enum, an enum symbol as 156 | a string is expected as the next option, or right within the option 157 | separated with an "=" sign: 158 | 159 | --------- 160 | enum Color { no, yes }; 161 | Color color; // default initialized to Color.no 162 | getopt(args, "color", &color); 163 | --------- 164 | 165 | To set $(D color) to $(D Color.yes), invoke the program with either 166 | $(D --color=yes) or $(D --color yes). 167 | ) 168 | 169 | $(LI $(I String options.) If an option is bound to a string, a string is 170 | expected as the next option, or right within the option separated with an 171 | "=" sign: 172 | 173 | --------- 174 | string outputFile; 175 | getopt(args, "output", &outputFile); 176 | --------- 177 | 178 | Invoking the program with "--output=myfile.txt" or "--output myfile.txt" 179 | will set $(D outputFile) to "myfile.txt". If you want to pass a string 180 | containing spaces, you need to use the quoting that is appropriate to your 181 | shell, e.g. --output='my file.txt'. 182 | ) 183 | 184 | $(LI $(I Array options.) If an option is bound to an array, a new element 185 | is appended to the array each time the option occurs: 186 | 187 | --------- 188 | string[] outputFiles; 189 | getopt(args, "output", &outputFiles); 190 | --------- 191 | 192 | Invoking the program with "--output=myfile.txt --output=yourfile.txt" or 193 | "--output myfile.txt --output yourfile.txt" will set $(D outputFiles) to 194 | $(D [ "myfile.txt", "yourfile.txt" ]). 195 | 196 | Alternatively you can set $(LREF arraySep) as the element separator: 197 | 198 | --------- 199 | string[] outputFiles; 200 | arraySep = ","; // defaults to "", separation by whitespace 201 | getopt(args, "output", &outputFiles); 202 | --------- 203 | 204 | With the above code you can invoke the program with 205 | "--output=myfile.txt,yourfile.txt", or "--output myfile.txt,yourfile.txt".) 206 | 207 | $(LI $(I Hash options.) If an option is bound to an associative array, a 208 | string of the form "name=value" is expected as the next option, or right 209 | within the option separated with an "=" sign: 210 | 211 | --------- 212 | double[string] tuningParms; 213 | getopt(args, "tune", &tuningParms); 214 | --------- 215 | 216 | Invoking the program with e.g. "--tune=alpha=0.5 --tune beta=0.6" will set 217 | $(D tuningParms) to [ "alpha" : 0.5, "beta" : 0.6 ]. 218 | 219 | Alternatively you can set $(LREF arraySep) as the element separator: 220 | 221 | --------- 222 | double[string] tuningParms; 223 | arraySep = ","; // defaults to "", separation by whitespace 224 | getopt(args, "tune", &tuningParms); 225 | --------- 226 | 227 | With the above code you can invoke the program with 228 | "--tune=alpha=0.5,beta=0.6", or "--tune alpha=0.5,beta=0.6". 229 | 230 | In general, the keys and values can be of any parsable types. 231 | ) 232 | 233 | $(LI $(I Callback options.) An option can be bound to a function or 234 | delegate with the signature $(D void function()), $(D void function(string 235 | option)), $(D void function(string option, string value)), or their 236 | delegate equivalents. 237 | 238 | $(UL 239 | $(LI If the callback doesn't take any arguments, the callback is 240 | invoked whenever the option is seen. 241 | ) 242 | 243 | $(LI If the callback takes one string argument, the option string 244 | (without the leading dash(es)) is passed to the callback. After that, 245 | the option string is considered handled and removed from the options 246 | array. 247 | 248 | --------- 249 | void main(string[] args) 250 | { 251 | uint verbosityLevel = 1; 252 | void myHandler(string option) 253 | { 254 | if (option == "quiet") 255 | { 256 | verbosityLevel = 0; 257 | } 258 | else 259 | { 260 | assert(option == "verbose"); 261 | verbosityLevel = 2; 262 | } 263 | } 264 | getopt(args, "verbose", &myHandler, "quiet", &myHandler); 265 | } 266 | --------- 267 | 268 | ) 269 | 270 | $(LI If the callback takes two string arguments, the option string is 271 | handled as an option with one argument, and parsed accordingly. The 272 | option and its value are passed to the callback. After that, whatever 273 | was passed to the callback is considered handled and removed from the 274 | list. 275 | 276 | --------- 277 | void main(string[] args) 278 | { 279 | uint verbosityLevel = 1; 280 | void myHandler(string option, string value) 281 | { 282 | switch (value) 283 | { 284 | case "quiet": verbosityLevel = 0; break; 285 | case "verbose": verbosityLevel = 2; break; 286 | case "shouting": verbosityLevel = verbosityLevel.max; break; 287 | default : 288 | stderr.writeln("Dunno how verbose you want me to be by saying ", 289 | value); 290 | exit(1); 291 | } 292 | } 293 | getopt(args, "verbosity", &myHandler); 294 | } 295 | --------- 296 | ) 297 | )) 298 | ) 299 | 300 | $(B Options with multiple names) 301 | 302 | Sometimes option synonyms are desirable, e.g. "--verbose", 303 | "--loquacious", and "--garrulous" should have the same effect. Such 304 | alternate option names can be included in the option specification, 305 | using "|" as a separator: 306 | 307 | --------- 308 | bool verbose; 309 | getopt(args, "verbose|loquacious|garrulous", &verbose); 310 | --------- 311 | 312 | $(B Case) 313 | 314 | By default options are case-insensitive. You can change that behavior 315 | by passing $(D getopt) the $(D caseSensitive) directive like this: 316 | 317 | --------- 318 | bool foo, bar; 319 | getopt(args, 320 | std.getopt.config.caseSensitive, 321 | "foo", &foo, 322 | "bar", &bar); 323 | --------- 324 | 325 | In the example above, "--foo", "--bar", "--FOo", "--bAr" etc. are recognized. 326 | The directive is active til the end of $(D getopt), or until the 327 | converse directive $(D caseInsensitive) is encountered: 328 | 329 | --------- 330 | bool foo, bar; 331 | getopt(args, 332 | std.getopt.config.caseSensitive, 333 | "foo", &foo, 334 | std.getopt.config.caseInsensitive, 335 | "bar", &bar); 336 | --------- 337 | 338 | The option "--Foo" is rejected due to $(D 339 | std.getopt.config.caseSensitive), but not "--Bar", "--bAr" 340 | etc. because the directive $(D 341 | std.getopt.config.caseInsensitive) turned sensitivity off before 342 | option "bar" was parsed. 343 | 344 | $(B "Short" versus "long" options) 345 | 346 | Traditionally, programs accepted single-letter options preceded by 347 | only one dash (e.g. $(D -t)). $(D getopt) accepts such parameters 348 | seamlessly. When used with a double-dash (e.g. $(D --t)), a 349 | single-letter option behaves the same as a multi-letter option. When 350 | used with a single dash, a single-letter option is accepted. If the 351 | option has a parameter, that must be "stuck" to the option without 352 | any intervening space or "=": 353 | 354 | --------- 355 | uint timeout; 356 | getopt(args, "timeout|t", &timeout); 357 | --------- 358 | 359 | To set $(D timeout) to $(D 5), use either of the following: $(D --timeout=5), 360 | $(D --timeout 5), $(D --t=5), $(D --t 5), or $(D -t5). Forms such as $(D -t 5) 361 | and $(D -timeout=5) will be not accepted. 362 | 363 | For more details about short options, refer also to the next section. 364 | 365 | $(B Bundling) 366 | 367 | Single-letter options can be bundled together, i.e. "-abc" is the same as 368 | $(D "-a -b -c"). By default, this option is turned off. You can turn it on 369 | with the $(D std.getopt.config.bundling) directive: 370 | 371 | --------- 372 | bool foo, bar; 373 | getopt(args, 374 | std.getopt.config.bundling, 375 | "foo|f", &foo, 376 | "bar|b", &bar); 377 | --------- 378 | 379 | In case you want to only enable bundling for some of the parameters, 380 | bundling can be turned off with $(D std.getopt.config.noBundling). 381 | 382 | $(B Required) 383 | 384 | An option can be marked as required. If that option is not present in the 385 | arguments an exceptin will be thrown. 386 | 387 | --------- 388 | bool foo, bar; 389 | getopt(args, 390 | std.getopt.config.required, 391 | "foo|f", &foo, 392 | "bar|b", &bar); 393 | --------- 394 | 395 | Only the option direclty following $(D std.getopt.config.required) is 396 | required. 397 | 398 | $(B Passing unrecognized options through) 399 | 400 | If an application needs to do its own processing of whichever arguments 401 | $(D getopt) did not understand, it can pass the 402 | $(D std.getopt.config.passThrough) directive to $(D getopt): 403 | 404 | --------- 405 | bool foo, bar; 406 | getopt(args, 407 | std.getopt.config.passThrough, 408 | "foo", &foo, 409 | "bar", &bar); 410 | --------- 411 | 412 | An unrecognized option such as "--baz" will be found untouched in 413 | $(D args) after $(D getopt) returns. 414 | 415 | $(D Help Information Generation) 416 | 417 | If an option string is followed by another string, this string serves as an 418 | description for this option. The function $(D getopt) returns a struct of type 419 | $(D GetoptResult). This return value contains information about all passed options 420 | as well a bool indicating if information about these options where required by 421 | the passed arguments. 422 | 423 | $(B Options Terminator) 424 | 425 | A lonesome double-dash terminates $(D getopt) gathering. It is used to 426 | separate program options from other parameters (e.g. options to be passed 427 | to another program). Invoking the example above with $(D "--foo -- --bar") 428 | parses foo but leaves "--bar" in $(D args). The double-dash itself is 429 | removed from the argument array. 430 | */ 431 | GetoptResult getopt(T...)(ref string[] args, T opts) 432 | { 433 | import std.exception : enforce; 434 | enforce(args.length, 435 | "Invalid arguments string passed: program name missing"); 436 | configuration cfg; 437 | GetoptResult rslt; 438 | 439 | getoptImpl(args, cfg, rslt, opts); 440 | 441 | return rslt; 442 | } 443 | 444 | /// 445 | unittest 446 | { 447 | auto args = ["prog", "--foo", "-b"]; 448 | 449 | bool foo; 450 | bool bar; 451 | auto rslt = getopt(args, "foo|f", "Some information about foo.", &foo, "bar|b", 452 | "Some help message about bar.", &bar); 453 | 454 | if (rslt.helpWanted) 455 | { 456 | defaultGetoptPrinter("Some information about the program.", 457 | rslt.options); 458 | } 459 | } 460 | 461 | /** 462 | Configuration options for $(D getopt). 463 | 464 | You can pass them to $(D getopt) in any position, except in between an option 465 | string and its bound pointer. 466 | */ 467 | enum config { 468 | /// Turns case sensitivity on 469 | caseSensitive, 470 | /// Turns case sensitivity off 471 | caseInsensitive, 472 | /// Turns bundling on 473 | bundling, 474 | /// Turns bundling off 475 | noBundling, 476 | /// Pass unrecognized arguments through 477 | passThrough, 478 | /// Signal unrecognized arguments as errors 479 | noPassThrough, 480 | /// Stop at first argument that does not look like an option 481 | stopOnFirstNonOption, 482 | /// Do not erase the endOfOptions separator from args 483 | keepEndOfOptions, 484 | /// Makes the next option a required option 485 | required 486 | } 487 | 488 | /** The result of the $(D getopt) function. 489 | 490 | The $(D GetoptResult) contains two members. The first member is a boolean with 491 | the name $(D helpWanted). The second member is an array of $(D Option). The 492 | array is accessable by the name $(D options). 493 | */ 494 | struct GetoptResult { 495 | bool helpWanted; /// Flag indicating if help was requested 496 | Option[] options; /// All possible options 497 | } 498 | 499 | /** The result of the $(D getoptHelp) function. 500 | */ 501 | struct Option { 502 | string optShort; /// The short symbol for this option 503 | string optLong; /// The long symbol for this option 504 | string help; /// The description of this option 505 | bool required; /// If a option is required, not passing it will result in 506 | /// an error. 507 | } 508 | 509 | private pure Option splitAndGet(string opt) @trusted nothrow 510 | { 511 | import std.array : split; 512 | auto sp = split(opt, "|"); 513 | Option ret; 514 | if (sp.length > 1) 515 | { 516 | ret.optShort = "-" ~ (sp[0].length < sp[1].length ? 517 | sp[0] : sp[1]); 518 | ret.optLong = "--" ~ (sp[0].length > sp[1].length ? 519 | sp[0] : sp[1]); 520 | } 521 | else 522 | { 523 | ret.optLong = "--" ~ sp[0]; 524 | } 525 | 526 | return ret; 527 | } 528 | 529 | private void getoptImpl(T...)(ref string[] args, ref configuration cfg, 530 | ref GetoptResult rslt, T opts) 531 | { 532 | import std.algorithm : remove; 533 | import std.conv : to; 534 | static if (opts.length) 535 | { 536 | static if (is(typeof(opts[0]) : config)) 537 | { 538 | // it's a configuration flag, act on it 539 | setConfig(cfg, opts[0]); 540 | return getoptImpl(args, cfg, rslt, opts[1 .. $]); 541 | } 542 | else 543 | { 544 | // it's an option string 545 | auto option = to!string(opts[0]); 546 | Option optionHelp = splitAndGet(option); 547 | optionHelp.required = cfg.required; 548 | 549 | static if (is(typeof(opts[1]) : string)) 550 | { 551 | auto receiver = opts[2]; 552 | optionHelp.help = opts[1]; 553 | immutable lowSliceIdx = 3; 554 | } 555 | else 556 | { 557 | auto receiver = opts[1]; 558 | immutable lowSliceIdx = 2; 559 | } 560 | 561 | rslt.options ~= optionHelp; 562 | 563 | bool incremental; 564 | // Handle options of the form --blah+ 565 | if (option.length && option[$ - 1] == autoIncrementChar) 566 | { 567 | option = option[0 .. $ - 1]; 568 | incremental = true; 569 | } 570 | 571 | bool optWasHandled = handleOption(option, receiver, args, cfg, incremental); 572 | 573 | if (cfg.required && !optWasHandled) 574 | { 575 | throw new GetOptException("Required option " ~ option ~ 576 | "was not supplied"); 577 | } 578 | cfg.required = false; 579 | 580 | return getoptImpl(args, cfg, rslt, opts[lowSliceIdx .. $]); 581 | } 582 | } 583 | else 584 | { 585 | // no more options to look for, potentially some arguments left 586 | for (size_t i = 1; i < args.length;) 587 | { 588 | auto a = args[i]; 589 | if (endOfOptions.length && a == endOfOptions) 590 | { 591 | // Consume the "--" if keepEndOfOptions is not specified 592 | if (!cfg.keepEndOfOptions) 593 | args = args.remove(i); 594 | break; 595 | } 596 | if (!a.length || a[0] != optionChar) 597 | { 598 | // not an option 599 | if (cfg.stopOnFirstNonOption) break; 600 | ++i; 601 | continue; 602 | } 603 | if (a == "--help" || a == "-h") 604 | { 605 | rslt.helpWanted = true; 606 | args = args.remove(i); 607 | continue; 608 | } 609 | if (!cfg.passThrough) 610 | { 611 | throw new GetOptException("Unrecognized option "~a); 612 | } 613 | ++i; 614 | } 615 | 616 | Option helpOpt; 617 | helpOpt.optShort = "-h"; 618 | helpOpt.optLong = "--help"; 619 | helpOpt.help = "This help information."; 620 | rslt.options ~= helpOpt; 621 | } 622 | } 623 | 624 | private bool handleOption(R)(string option, R receiver, ref string[] args, 625 | ref configuration cfg, bool incremental) 626 | { 627 | import std.algorithm : map, splitter; 628 | import std.ascii : isAlpha; 629 | import std.conv : text, to; 630 | // Scan arguments looking for a match for this option 631 | bool ret = false; 632 | for (size_t i = 1; i < args.length; ) 633 | { 634 | auto a = args[i]; 635 | if (endOfOptions.length && a == endOfOptions) break; 636 | if (cfg.stopOnFirstNonOption && (!a.length || a[0] != optionChar)) 637 | { 638 | // first non-option is end of options 639 | break; 640 | } 641 | // Unbundle bundled arguments if necessary 642 | if (cfg.bundling && a.length > 2 && a[0] == optionChar && 643 | a[1] != optionChar) 644 | { 645 | string[] expanded; 646 | foreach (j, dchar c; a[1 .. $]) 647 | { 648 | // If the character is not alpha, stop right there. This allows 649 | // e.g. -j100 to work as "pass argument 100 to option -j". 650 | if (!isAlpha(c)) 651 | { 652 | expanded ~= a[j + 1 .. $]; 653 | break; 654 | } 655 | expanded ~= text(optionChar, c); 656 | } 657 | args = args[0 .. i] ~ expanded ~ args[i + 1 .. $]; 658 | continue; 659 | } 660 | 661 | string val; 662 | if (!optMatch(a, option, val, cfg)) 663 | { 664 | ++i; 665 | continue; 666 | } 667 | 668 | ret = true; 669 | 670 | // found it 671 | // from here on, commit to eat args[i] 672 | // (and potentially args[i + 1] too, but that comes later) 673 | args = args[0 .. i] ~ args[i + 1 .. $]; 674 | 675 | static if (is(typeof(*receiver) == bool)) 676 | { 677 | // parse '--b=true/false' 678 | if (val.length) 679 | { 680 | *receiver = to!(typeof(*receiver))(val); 681 | break; 682 | } 683 | 684 | // no argument means set it to true 685 | *receiver = true; 686 | break; 687 | } 688 | else 689 | { 690 | import std.exception : enforce; 691 | // non-boolean option, which might include an argument 692 | //enum isCallbackWithOneParameter = is(typeof(receiver("")) : void); 693 | enum isCallbackWithLessThanTwoParameters = 694 | (is(typeof(receiver) == delegate) || is(typeof(*receiver) == function)) && 695 | !is(typeof(receiver("", ""))); 696 | if (!isCallbackWithLessThanTwoParameters && !(val.length) && !incremental) 697 | { 698 | // Eat the next argument too. Check to make sure there's one 699 | // to be eaten first, though. 700 | enforce(i < args.length, 701 | "Missing value for argument " ~ a ~ "."); 702 | val = args[i]; 703 | args = args[0 .. i] ~ args[i + 1 .. $]; 704 | } 705 | static if (is(typeof(*receiver) == enum)) 706 | { 707 | *receiver = to!(typeof(*receiver))(val); 708 | } 709 | else static if (is(typeof(*receiver) : real)) 710 | { 711 | // numeric receiver 712 | if (incremental) ++*receiver; 713 | else *receiver = to!(typeof(*receiver))(val); 714 | } 715 | else static if (is(typeof(*receiver) == string)) 716 | { 717 | // string receiver 718 | *receiver = to!(typeof(*receiver))(val); 719 | } 720 | else static if (is(typeof(receiver) == delegate) || 721 | is(typeof(*receiver) == function)) 722 | { 723 | static if (is(typeof(receiver("", "")) : void)) 724 | { 725 | // option with argument 726 | receiver(option, val); 727 | } 728 | else static if (is(typeof(receiver("")) : void)) 729 | { 730 | static assert(is(typeof(receiver("")) : void)); 731 | // boolean-style receiver 732 | receiver(option); 733 | } 734 | else 735 | { 736 | static assert(is(typeof(receiver()) : void)); 737 | // boolean-style receiver without argument 738 | receiver(); 739 | } 740 | } 741 | else static if (isArray!(typeof(*receiver))) 742 | { 743 | // array receiver 744 | import std.range : ElementEncodingType; 745 | alias E = ElementEncodingType!(typeof(*receiver)); 746 | 747 | if (arraySep == "") 748 | { 749 | *receiver ~= to!E(val); 750 | } 751 | else 752 | { 753 | foreach (elem; val.splitter(arraySep).map!(a => to!E(a))()) 754 | *receiver ~= elem; 755 | } 756 | } 757 | else static if (isAssociativeArray!(typeof(*receiver))) 758 | { 759 | // hash receiver 760 | alias K = typeof(receiver.keys[0]); 761 | alias V = typeof(receiver.values[0]); 762 | 763 | import std.range : only; 764 | import std.typecons : Tuple, tuple; 765 | import std.string : indexOf; 766 | 767 | static Tuple!(K, V) getter(string input) 768 | { 769 | auto j = indexOf(input, assignChar); 770 | auto key = input[0 .. j]; 771 | auto value = input[j + 1 .. $]; 772 | return tuple(to!K(key), to!V(value)); 773 | } 774 | 775 | static void setHash(Range)(R receiver, Range range) 776 | { 777 | foreach (k, v; range.map!getter) 778 | (*receiver)[k] = v; 779 | } 780 | 781 | if (arraySep == "") 782 | setHash(receiver, val.only); 783 | else 784 | setHash(receiver, val.splitter(arraySep)); 785 | } 786 | else 787 | { 788 | static assert(false, "Dunno how to deal with type " ~ 789 | typeof(receiver).stringof); 790 | } 791 | } 792 | } 793 | 794 | return ret; 795 | } 796 | 797 | // 5316 - arrays with arraySep 798 | unittest 799 | { 800 | import std.conv; 801 | 802 | arraySep = ","; 803 | scope (exit) arraySep = ""; 804 | 805 | string[] names; 806 | auto args = ["program.name", "-nfoo,bar,baz"]; 807 | getopt(args, "name|n", &names); 808 | assert(names == ["foo", "bar", "baz"], to!string(names)); 809 | 810 | names = names.init; 811 | args = ["program.name", "-n", "foo,bar,baz"]; 812 | getopt(args, "name|n", &names); 813 | assert(names == ["foo", "bar", "baz"], to!string(names)); 814 | 815 | names = names.init; 816 | args = ["program.name", "--name=foo,bar,baz"]; 817 | getopt(args, "name|n", &names); 818 | assert(names == ["foo", "bar", "baz"], to!string(names)); 819 | 820 | names = names.init; 821 | args = ["program.name", "--name", "foo,bar,baz"]; 822 | getopt(args, "name|n", &names); 823 | assert(names == ["foo", "bar", "baz"], to!string(names)); 824 | } 825 | 826 | // 5316 - associative arrays with arraySep 827 | unittest 828 | { 829 | import std.conv; 830 | 831 | arraySep = ","; 832 | scope (exit) arraySep = ""; 833 | 834 | int[string] values; 835 | values = values.init; 836 | auto args = ["program.name", "-vfoo=0,bar=1,baz=2"]; 837 | getopt(args, "values|v", &values); 838 | assert(values == ["foo":0, "bar":1, "baz":2], to!string(values)); 839 | 840 | values = values.init; 841 | args = ["program.name", "-v", "foo=0,bar=1,baz=2"]; 842 | getopt(args, "values|v", &values); 843 | assert(values == ["foo":0, "bar":1, "baz":2], to!string(values)); 844 | 845 | values = values.init; 846 | args = ["program.name", "--values=foo=0,bar=1,baz=2"]; 847 | getopt(args, "values|t", &values); 848 | assert(values == ["foo":0, "bar":1, "baz":2], to!string(values)); 849 | 850 | values = values.init; 851 | args = ["program.name", "--values", "foo=0,bar=1,baz=2"]; 852 | getopt(args, "values|v", &values); 853 | assert(values == ["foo":0, "bar":1, "baz":2], to!string(values)); 854 | } 855 | 856 | /** 857 | The option character (default '-'). 858 | 859 | Defaults to '-' but it can be assigned to prior to calling $(D getopt). 860 | */ 861 | dchar optionChar = '-'; 862 | 863 | /** 864 | The string that conventionally marks the end of all options (default '--'). 865 | 866 | Defaults to "--" but can be assigned to prior to calling $(D getopt). Assigning an 867 | empty string to $(D endOfOptions) effectively disables it. 868 | */ 869 | string endOfOptions = "--"; 870 | 871 | /** 872 | The assignment character used in options with parameters (default '='). 873 | 874 | Defaults to '=' but can be assigned to prior to calling $(D getopt). 875 | */ 876 | dchar assignChar = '='; 877 | 878 | /** 879 | The string used to separate the elements of an array or associative array 880 | (default is "" which means the elements are separated by whitespace). 881 | 882 | Defaults to "" but can be assigned to prior to calling $(D getopt). 883 | */ 884 | string arraySep = ""; 885 | 886 | enum autoIncrementChar = '+'; 887 | 888 | private struct configuration 889 | { 890 | import std.bitmanip : bitfields; 891 | mixin(bitfields!( 892 | bool, "caseSensitive", 1, 893 | bool, "bundling", 1, 894 | bool, "passThrough", 1, 895 | bool, "stopOnFirstNonOption", 1, 896 | bool, "keepEndOfOptions", 1, 897 | bool, "required", 1, 898 | ubyte, "", 2)); 899 | } 900 | 901 | private bool optMatch(string arg, string optPattern, ref string value, 902 | configuration cfg) 903 | { 904 | import std.uni : toUpper; 905 | import std.string : indexOf; 906 | import std.array : split; 907 | //writeln("optMatch:\n ", arg, "\n ", optPattern, "\n ", value); 908 | //scope(success) writeln("optMatch result: ", value); 909 | if (!arg.length || arg[0] != optionChar) return false; 910 | // yank the leading '-' 911 | arg = arg[1 .. $]; 912 | immutable isLong = arg.length > 1 && arg[0] == optionChar; 913 | //writeln("isLong: ", isLong); 914 | // yank the second '-' if present 915 | if (isLong) arg = arg[1 .. $]; 916 | immutable eqPos = indexOf(arg, assignChar); 917 | if (isLong && eqPos >= 0) 918 | { 919 | // argument looks like --opt=value 920 | value = arg[eqPos + 1 .. $]; 921 | arg = arg[0 .. eqPos]; 922 | } 923 | else 924 | { 925 | if (!isLong && !cfg.bundling) 926 | { 927 | // argument looks like -ovalue and there's no bundling 928 | value = arg[1 .. $]; 929 | arg = arg[0 .. 1]; 930 | } 931 | else 932 | { 933 | // argument looks like --opt, or -oxyz with bundling 934 | value = null; 935 | } 936 | } 937 | //writeln("Arg: ", arg, " pattern: ", optPattern, " value: ", value); 938 | // Split the option 939 | const variants = split(optPattern, "|"); 940 | foreach (v ; variants) 941 | { 942 | //writeln("Trying variant: ", v, " against ", arg); 943 | if (arg == v || !cfg.caseSensitive && toUpper(arg) == toUpper(v)) 944 | return true; 945 | if (cfg.bundling && !isLong && v.length == 1 946 | && indexOf(arg, v) >= 0) 947 | { 948 | //writeln("success"); 949 | return true; 950 | } 951 | } 952 | return false; 953 | } 954 | 955 | private void setConfig(ref configuration cfg, config option) 956 | { 957 | switch (option) 958 | { 959 | case config.caseSensitive: cfg.caseSensitive = true; break; 960 | case config.caseInsensitive: cfg.caseSensitive = false; break; 961 | case config.bundling: cfg.bundling = true; break; 962 | case config.noBundling: cfg.bundling = false; break; 963 | case config.passThrough: cfg.passThrough = true; break; 964 | case config.noPassThrough: cfg.passThrough = false; break; 965 | case config.required: cfg.required = true; break; 966 | case config.stopOnFirstNonOption: 967 | cfg.stopOnFirstNonOption = true; break; 968 | case config.keepEndOfOptions: 969 | cfg.keepEndOfOptions = true; break; 970 | default: assert(false); 971 | } 972 | } 973 | 974 | unittest 975 | { 976 | import std.conv; 977 | import std.math; 978 | 979 | uint paranoid = 2; 980 | string[] args = ["program.name", "--paranoid", "--paranoid", "--paranoid"]; 981 | getopt(args, "paranoid+", ¶noid); 982 | assert(paranoid == 5, to!(string)(paranoid)); 983 | 984 | enum Color { no, yes } 985 | Color color; 986 | args = ["program.name", "--color=yes",]; 987 | getopt(args, "color", &color); 988 | assert(color, to!(string)(color)); 989 | 990 | color = Color.no; 991 | args = ["program.name", "--color", "yes",]; 992 | getopt(args, "color", &color); 993 | assert(color, to!(string)(color)); 994 | 995 | string data = "file.dat"; 996 | int length = 24; 997 | bool verbose = false; 998 | args = ["program.name", "--length=5", "--file", "dat.file", "--verbose"]; 999 | getopt( 1000 | args, 1001 | "length", &length, 1002 | "file", &data, 1003 | "verbose", &verbose); 1004 | assert(args.length == 1); 1005 | assert(data == "dat.file"); 1006 | assert(length == 5); 1007 | assert(verbose); 1008 | 1009 | // 1010 | string[] outputFiles; 1011 | args = ["program.name", "--output=myfile.txt", "--output", "yourfile.txt"]; 1012 | getopt(args, "output", &outputFiles); 1013 | assert(outputFiles.length == 2 1014 | && outputFiles[0] == "myfile.txt" && outputFiles[1] == "yourfile.txt"); 1015 | 1016 | outputFiles = []; 1017 | arraySep = ","; 1018 | args = ["program.name", "--output", "myfile.txt,yourfile.txt"]; 1019 | getopt(args, "output", &outputFiles); 1020 | assert(outputFiles.length == 2 1021 | && outputFiles[0] == "myfile.txt" && outputFiles[1] == "yourfile.txt"); 1022 | arraySep = ""; 1023 | 1024 | foreach (testArgs; 1025 | [["program.name", "--tune=alpha=0.5", "--tune", "beta=0.6"], 1026 | ["program.name", "--tune=alpha=0.5,beta=0.6"], 1027 | ["program.name", "--tune", "alpha=0.5,beta=0.6"]]) 1028 | { 1029 | arraySep = ","; 1030 | double[string] tuningParms; 1031 | getopt(testArgs, "tune", &tuningParms); 1032 | assert(testArgs.length == 1); 1033 | assert(tuningParms.length == 2); 1034 | assert(approxEqual(tuningParms["alpha"], 0.5)); 1035 | assert(approxEqual(tuningParms["beta"], 0.6)); 1036 | arraySep = ""; 1037 | } 1038 | 1039 | uint verbosityLevel = 1; 1040 | void myHandler(string option) 1041 | { 1042 | if (option == "quiet") 1043 | { 1044 | verbosityLevel = 0; 1045 | } 1046 | else 1047 | { 1048 | assert(option == "verbose"); 1049 | verbosityLevel = 2; 1050 | } 1051 | } 1052 | args = ["program.name", "--quiet"]; 1053 | getopt(args, "verbose", &myHandler, "quiet", &myHandler); 1054 | assert(verbosityLevel == 0); 1055 | args = ["program.name", "--verbose"]; 1056 | getopt(args, "verbose", &myHandler, "quiet", &myHandler); 1057 | assert(verbosityLevel == 2); 1058 | 1059 | verbosityLevel = 1; 1060 | void myHandler2(string option, string value) 1061 | { 1062 | assert(option == "verbose"); 1063 | verbosityLevel = 2; 1064 | } 1065 | args = ["program.name", "--verbose", "2"]; 1066 | getopt(args, "verbose", &myHandler2); 1067 | assert(verbosityLevel == 2); 1068 | 1069 | verbosityLevel = 1; 1070 | void myHandler3() 1071 | { 1072 | verbosityLevel = 2; 1073 | } 1074 | args = ["program.name", "--verbose"]; 1075 | getopt(args, "verbose", &myHandler3); 1076 | assert(verbosityLevel == 2); 1077 | 1078 | bool foo, bar; 1079 | args = ["program.name", "--foo", "--bAr"]; 1080 | getopt(args, 1081 | genPackageVersion.getopt.config.caseSensitive, 1082 | genPackageVersion.getopt.config.passThrough, 1083 | "foo", &foo, 1084 | "bar", &bar); 1085 | assert(args[1] == "--bAr"); 1086 | 1087 | // test stopOnFirstNonOption 1088 | 1089 | args = ["program.name", "--foo", "nonoption", "--bar"]; 1090 | foo = bar = false; 1091 | getopt(args, 1092 | genPackageVersion.getopt.config.stopOnFirstNonOption, 1093 | "foo", &foo, 1094 | "bar", &bar); 1095 | assert(foo && !bar && args[1] == "nonoption" && args[2] == "--bar"); 1096 | 1097 | args = ["program.name", "--foo", "nonoption", "--zab"]; 1098 | foo = bar = false; 1099 | getopt(args, 1100 | genPackageVersion.getopt.config.stopOnFirstNonOption, 1101 | "foo", &foo, 1102 | "bar", &bar); 1103 | assert(foo && !bar && args[1] == "nonoption" && args[2] == "--zab"); 1104 | 1105 | args = ["program.name", "--fb1", "--fb2=true", "--tb1=false"]; 1106 | bool fb1, fb2; 1107 | bool tb1 = true; 1108 | getopt(args, "fb1", &fb1, "fb2", &fb2, "tb1", &tb1); 1109 | assert(fb1 && fb2 && !tb1); 1110 | 1111 | // test keepEndOfOptions 1112 | 1113 | args = ["program.name", "--foo", "nonoption", "--bar", "--", "--baz"]; 1114 | getopt(args, 1115 | genPackageVersion.getopt.config.keepEndOfOptions, 1116 | "foo", &foo, 1117 | "bar", &bar); 1118 | assert(args == ["program.name", "nonoption", "--", "--baz"]); 1119 | 1120 | // Ensure old behavior without the keepEndOfOptions 1121 | 1122 | args = ["program.name", "--foo", "nonoption", "--bar", "--", "--baz"]; 1123 | getopt(args, 1124 | "foo", &foo, 1125 | "bar", &bar); 1126 | assert(args == ["program.name", "nonoption", "--baz"]); 1127 | 1128 | // test function callbacks 1129 | 1130 | static class MyEx : Exception 1131 | { 1132 | this() { super(""); } 1133 | this(string option) { this(); this.option = option; } 1134 | this(string option, string value) { this(option); this.value = value; } 1135 | 1136 | string option; 1137 | string value; 1138 | } 1139 | 1140 | static void myStaticHandler1() { throw new MyEx(); } 1141 | args = ["program.name", "--verbose"]; 1142 | try { getopt(args, "verbose", &myStaticHandler1); assert(0); } 1143 | catch (MyEx ex) { assert(ex.option is null && ex.value is null); } 1144 | 1145 | static void myStaticHandler2(string option) { throw new MyEx(option); } 1146 | args = ["program.name", "--verbose"]; 1147 | try { getopt(args, "verbose", &myStaticHandler2); assert(0); } 1148 | catch (MyEx ex) { assert(ex.option == "verbose" && ex.value is null); } 1149 | 1150 | static void myStaticHandler3(string option, string value) { throw new MyEx(option, value); } 1151 | args = ["program.name", "--verbose", "2"]; 1152 | try { getopt(args, "verbose", &myStaticHandler3); assert(0); } 1153 | catch (MyEx ex) { assert(ex.option == "verbose" && ex.value == "2"); } 1154 | } 1155 | 1156 | unittest 1157 | { 1158 | // From bugzilla 2142 1159 | bool f_linenum, f_filename; 1160 | string[] args = [ "", "-nl" ]; 1161 | getopt 1162 | ( 1163 | args, 1164 | genPackageVersion.getopt.config.bundling, 1165 | //genPackageVersion.getopt.config.caseSensitive, 1166 | "linenum|l", &f_linenum, 1167 | "filename|n", &f_filename 1168 | ); 1169 | assert(f_linenum); 1170 | assert(f_filename); 1171 | } 1172 | 1173 | unittest 1174 | { 1175 | // From bugzilla 6887 1176 | string[] p; 1177 | string[] args = ["", "-pa"]; 1178 | getopt(args, "p", &p); 1179 | assert(p.length == 1); 1180 | assert(p[0] == "a"); 1181 | } 1182 | 1183 | unittest 1184 | { 1185 | // From bugzilla 6888 1186 | int[string] foo; 1187 | auto args = ["", "-t", "a=1"]; 1188 | getopt(args, "t", &foo); 1189 | assert(foo == ["a":1]); 1190 | } 1191 | 1192 | unittest 1193 | { 1194 | // From bugzilla 9583 1195 | int opt; 1196 | auto args = ["prog", "--opt=123", "--", "--a", "--b", "--c"]; 1197 | getopt(args, "opt", &opt); 1198 | assert(args == ["prog", "--a", "--b", "--c"]); 1199 | } 1200 | 1201 | unittest 1202 | { 1203 | string foo, bar; 1204 | auto args = ["prog", "-thello", "-dbar=baz"]; 1205 | getopt(args, "t", &foo, "d", &bar); 1206 | assert(foo == "hello"); 1207 | assert(bar == "bar=baz"); 1208 | // From bugzilla 5762 1209 | string a; 1210 | args = ["prog", "-a-0x12"]; 1211 | getopt(args, config.bundling, "a|addr", &a); 1212 | assert(a == "-0x12", a); 1213 | args = ["prog", "--addr=-0x12"]; 1214 | getopt(args, config.bundling, "a|addr", &a); 1215 | assert(a == "-0x12"); 1216 | // From https://d.puremagic.com/issues/show_bug.cgi?id=11764 1217 | args = ["main", "-test"]; 1218 | bool opt; 1219 | args.getopt(config.passThrough, "opt", &opt); 1220 | assert(args == ["main", "-test"]); 1221 | } 1222 | 1223 | unittest // 5228 1224 | { 1225 | import std.exception; 1226 | import std.conv; 1227 | 1228 | auto args = ["prog", "--foo=bar"]; 1229 | int abc; 1230 | assertThrown!GetOptException(getopt(args, "abc", &abc)); 1231 | 1232 | args = ["prog", "--abc=string"]; 1233 | assertThrown!ConvException(getopt(args, "abc", &abc)); 1234 | } 1235 | 1236 | unittest // From bugzilla 7693 1237 | { 1238 | import std.exception; 1239 | 1240 | enum Foo { 1241 | bar, 1242 | baz 1243 | } 1244 | 1245 | auto args = ["prog", "--foo=barZZZ"]; 1246 | Foo foo; 1247 | assertThrown(getopt(args, "foo", &foo)); 1248 | args = ["prog", "--foo=bar"]; 1249 | assertNotThrown(getopt(args, "foo", &foo)); 1250 | args = ["prog", "--foo", "barZZZ"]; 1251 | assertThrown(getopt(args, "foo", &foo)); 1252 | args = ["prog", "--foo", "baz"]; 1253 | assertNotThrown(getopt(args, "foo", &foo)); 1254 | } 1255 | 1256 | unittest // same bug as 7693 only for bool 1257 | { 1258 | import std.exception; 1259 | 1260 | auto args = ["prog", "--foo=truefoobar"]; 1261 | bool foo; 1262 | assertThrown(getopt(args, "foo", &foo)); 1263 | args = ["prog", "--foo"]; 1264 | getopt(args, "foo", &foo); 1265 | assert(foo); 1266 | } 1267 | 1268 | unittest 1269 | { 1270 | bool foo; 1271 | auto args = ["prog", "--foo"]; 1272 | getopt(args, "foo", &foo); 1273 | assert(foo); 1274 | } 1275 | 1276 | unittest 1277 | { 1278 | bool foo; 1279 | bool bar; 1280 | auto args = ["prog", "--foo", "-b"]; 1281 | getopt(args, config.caseInsensitive,"foo|f", "Some foo", &foo, 1282 | config.caseSensitive, "bar|b", "Some bar", &bar); 1283 | assert(foo); 1284 | assert(bar); 1285 | } 1286 | 1287 | unittest 1288 | { 1289 | bool foo; 1290 | bool bar; 1291 | auto args = ["prog", "-b", "--foo", "-z"]; 1292 | getopt(args, config.caseInsensitive, config.required, "foo|f", "Some foo", 1293 | &foo, config.caseSensitive, "bar|b", "Some bar", &bar, 1294 | config.passThrough); 1295 | assert(foo); 1296 | assert(bar); 1297 | } 1298 | 1299 | unittest 1300 | { 1301 | import std.exception; 1302 | 1303 | bool foo; 1304 | bool bar; 1305 | auto args = ["prog", "-b", "-z"]; 1306 | assertThrown(getopt(args, config.caseInsensitive, config.required, "foo|f", 1307 | "Some foo", &foo, config.caseSensitive, "bar|b", "Some bar", &bar, 1308 | config.passThrough)); 1309 | } 1310 | 1311 | unittest 1312 | { 1313 | import std.exception; 1314 | 1315 | bool foo; 1316 | bool bar; 1317 | auto args = ["prog", "--foo", "-z"]; 1318 | assertNotThrown(getopt(args, config.caseInsensitive, config.required, 1319 | "foo|f", "Some foo", &foo, config.caseSensitive, "bar|b", "Some bar", 1320 | &bar, config.passThrough)); 1321 | assert(foo); 1322 | assert(!bar); 1323 | } 1324 | 1325 | unittest 1326 | { 1327 | bool foo; 1328 | auto args = ["prog", "-f"]; 1329 | auto r = getopt(args, config.caseInsensitive, "help|f", "Some foo", &foo); 1330 | assert(foo); 1331 | assert(!r.helpWanted); 1332 | } 1333 | 1334 | unittest // implicit help option without config.passThrough 1335 | { 1336 | string[] args = ["program", "--help"]; 1337 | auto r = getopt(args); 1338 | assert(r.helpWanted); 1339 | } 1340 | 1341 | // Issue 13316 - std.getopt: implicit help option breaks the next argument 1342 | unittest 1343 | { 1344 | string[] args = ["program", "--help", "--", "something"]; 1345 | getopt(args); 1346 | assert(args == ["program", "something"]); 1347 | 1348 | args = ["program", "--help", "--"]; 1349 | getopt(args); 1350 | assert(args == ["program"]); 1351 | 1352 | bool b; 1353 | args = ["program", "--help", "nonoption", "--option"]; 1354 | getopt(args, config.stopOnFirstNonOption, "option", &b); 1355 | assert(args == ["program", "nonoption", "--option"]); 1356 | } 1357 | 1358 | // Issue 13317 - std.getopt: endOfOptions broken when it doesn't look like an option 1359 | unittest 1360 | { 1361 | auto endOfOptionsBackup = endOfOptions; 1362 | scope(exit) endOfOptions = endOfOptionsBackup; 1363 | endOfOptions = "endofoptions"; 1364 | string[] args = ["program", "endofoptions", "--option"]; 1365 | bool b = false; 1366 | getopt(args, "option", &b); 1367 | assert(!b); 1368 | assert(args == ["program", "--option"]); 1369 | } 1370 | 1371 | /** This function prints the passed $(D Option) and text in an aligned manner. 1372 | 1373 | The passed text will be printed first, followed by a newline. Than the short 1374 | and long version of every option will be printed. The short and long version 1375 | will be aligned to the longest option of every $(D Option) passed. If a help 1376 | message is present it will be printed after the long version of the 1377 | $(D Option). 1378 | 1379 | ------------ 1380 | foreach(it; opt) 1381 | { 1382 | writefln("%*s %*s %s", lengthOfLongestShortOption, it.optShort, 1383 | lengthOfLongestLongOption, it.optLong, it.help); 1384 | } 1385 | ------------ 1386 | 1387 | Params: 1388 | text = The text to printed at the beginning of the help output. 1389 | opt = The $(D Option) extracted from the $(D getopt) parameter. 1390 | */ 1391 | void defaultGetoptPrinter(string text, Option[] opt) 1392 | { 1393 | import std.stdio : stdout; 1394 | 1395 | defaultGetoptFormatter(stdout.lockingTextWriter(), text, opt); 1396 | } 1397 | 1398 | /** This function writes the passed text and $(D Option) into an output range 1399 | in the manner, described in the documentation of function 1400 | $(D defaultGetoptPrinter). 1401 | 1402 | Params: 1403 | output = The output range used to write the help information. 1404 | text = The text to printed at the beginning of the help output. 1405 | opt = The $(D Option) extracted from the $(D getopt) parameter. 1406 | */ 1407 | void defaultGetoptFormatter(Output)(Output output, string text, Option[] opt) 1408 | { 1409 | import std.format : formattedWrite; 1410 | import std.algorithm : min, max; 1411 | 1412 | output.formattedWrite("%s\n", text); 1413 | 1414 | size_t ls, ll; 1415 | bool hasRequired = false; 1416 | foreach (it; opt) 1417 | { 1418 | ls = max(ls, it.optShort.length); 1419 | ll = max(ll, it.optLong.length); 1420 | 1421 | hasRequired = hasRequired || it.required; 1422 | } 1423 | 1424 | size_t argLength = ls + ll + 2; 1425 | 1426 | string re = " Required: "; 1427 | 1428 | foreach (it; opt) 1429 | { 1430 | output.formattedWrite("%*s %*s%*s%s\n", ls, it.optShort, ll, it.optLong, 1431 | hasRequired ? re.length : 1, it.required ? re : " ", it.help); 1432 | } 1433 | } 1434 | 1435 | unittest 1436 | { 1437 | import std.conv; 1438 | 1439 | import std.array; 1440 | import std.string; 1441 | bool a; 1442 | auto args = ["prog", "--foo"]; 1443 | auto t = getopt(args, "foo|f", "Help", &a); 1444 | string s; 1445 | auto app = appender!string(); 1446 | defaultGetoptFormatter(app, "Some Text", t.options); 1447 | 1448 | string helpMsg = app.data; 1449 | //writeln(helpMsg); 1450 | assert(helpMsg.length); 1451 | assert(helpMsg.count("\n") == 3, to!string(helpMsg.count("\n")) ~ " " 1452 | ~ helpMsg); 1453 | assert(helpMsg.indexOf("--foo") != -1); 1454 | assert(helpMsg.indexOf("-f") != -1); 1455 | assert(helpMsg.indexOf("-h") != -1); 1456 | assert(helpMsg.indexOf("--help") != -1); 1457 | assert(helpMsg.indexOf("Help") != -1); 1458 | 1459 | string wanted = "Some Text\n-f --foo Help\n-h --help This help " 1460 | ~ "information.\n"; 1461 | assert(wanted == helpMsg); 1462 | } 1463 | 1464 | unittest 1465 | { 1466 | import std.conv; 1467 | import std.string; 1468 | import std.array ; 1469 | bool a; 1470 | auto args = ["prog", "--foo"]; 1471 | auto t = getopt(args, config.required, "foo|f", "Help", &a); 1472 | string s; 1473 | auto app = appender!string(); 1474 | defaultGetoptFormatter(app, "Some Text", t.options); 1475 | 1476 | string helpMsg = app.data; 1477 | //writeln(helpMsg); 1478 | assert(helpMsg.length); 1479 | assert(helpMsg.count("\n") == 3, to!string(helpMsg.count("\n")) ~ " " 1480 | ~ helpMsg); 1481 | assert(helpMsg.indexOf("Required:") != -1); 1482 | assert(helpMsg.indexOf("--foo") != -1); 1483 | assert(helpMsg.indexOf("-f") != -1); 1484 | assert(helpMsg.indexOf("-h") != -1); 1485 | assert(helpMsg.indexOf("--help") != -1); 1486 | assert(helpMsg.indexOf("Help") != -1); 1487 | 1488 | string wanted = "Some Text\n-f --foo Required: Help\n-h --help " ~ 1489 | " This help information.\n"; 1490 | assert(wanted == helpMsg, helpMsg ~ wanted); 1491 | } 1492 | -------------------------------------------------------------------------------- /src/genPackageVersion/ignoreFiles.d: -------------------------------------------------------------------------------- 1 | /// Handles VCS ignore files. 2 | module genPackageVersion.ignoreFiles; 3 | 4 | import std.algorithm; 5 | import std.array; 6 | import std.stdio; 7 | import std.string : strip; 8 | 9 | import scriptlike.only; 10 | import genPackageVersion.util; 11 | 12 | /// Add `path` to ignore files for all VCSes detected. 13 | /// Does nothing if `path` is already in the ignore file. 14 | void addToIgnoreFiles(string path) 15 | { 16 | if(detectedGit) 17 | addToIgnore(".gitignore", path, false); 18 | 19 | if(detectedHg) 20 | addToIgnore(".hgignore", path, true); 21 | } 22 | 23 | /// Add `path` to a VCS ignore file, unless it's already in the ignore file. 24 | void addToIgnore(string ignoreFileName, string path, bool useRegex) 25 | { 26 | // Normalize to the standard git/hg ignore style. 27 | // Don't worry, this works on Windows just fine. 28 | path = path.replace("\\", "/"); 29 | 30 | if(useRegex) 31 | path = "^" ~ path ~ "$"; 32 | 33 | // Doesn't already exist? Create it. 34 | if(!exists(Path(ignoreFileName))) 35 | { 36 | import scriptlike.file : scriptlikeWrite = write; 37 | 38 | logVerbose("No existing ", ignoreFileName, " file. Creating it."); 39 | if(!dryRun) 40 | scriptlikeWrite(ignoreFileName, path~"\n"); 41 | 42 | return; 43 | } 44 | 45 | // Make sure it's actually a file 46 | if(!isFile(Path(ignoreFileName))) 47 | { 48 | logVerbose("Strange, ", ignoreFileName, " exists but isn't a file. Not updating it."); 49 | return; // Not a file? Don't even bother with it. 50 | } 51 | 52 | // Is 'path' already in the ignore file? 53 | auto isAlreadyInFile = 54 | File(ignoreFileName) 55 | .byLine() 56 | .map!(std.string.strip)() // Get rid of any trailing \r byLine might have left us on Windows 57 | .map!(a => a.replace("\\", "/")) 58 | .canFind(path); 59 | 60 | // Append 'path' to the ignore file 61 | if(!isAlreadyInFile) 62 | { 63 | logVerbose("Pattern '", path, "' not found in ", ignoreFileName, " file. Adding it."); 64 | 65 | if(!dryRun) 66 | { 67 | auto file = File(ignoreFileName, "a+"); 68 | scope(exit) file.close(); 69 | 70 | // Everything on Windows handles \n just fine, plus git is 71 | // heavily Linux-oriented so \n is more appropriate. 72 | file.rawWrite("\n"); // Just in case there isn't already a trailing newline 73 | file.rawWrite(path); 74 | file.rawWrite("\n"); 75 | } 76 | } 77 | else 78 | logVerbose("Pattern '", path, "' is already found in ", ignoreFileName, " file."); 79 | } 80 | -------------------------------------------------------------------------------- /src/genPackageVersion/main.d: -------------------------------------------------------------------------------- 1 | module genPackageVersion.main; 2 | 3 | import genPackageVersion.genAll; 4 | 5 | version(unittest) void main() {} else 6 | void main(string[] args) 7 | { 8 | genPackageVersionMain(args); 9 | } 10 | -------------------------------------------------------------------------------- /src/genPackageVersion/util.d: -------------------------------------------------------------------------------- 1 | /// Misc utility module 2 | module genPackageVersion.util; 3 | 4 | import std.stdio; 5 | import scriptlike.only; 6 | 7 | string outPackageName = null; /// Main cmd line argument to 'gen-package-version' 8 | string outModuleName = "packageVersion"; /// --module=... 9 | string projectSourcePath = null; /// --src=... 10 | string rootPath = "."; /// --root=... 11 | string ddocDir = null; /// --ddoc=... 12 | bool useDub = false; /// --dub 13 | bool noIgnoreFile = false; /// --no-ignore-file 14 | bool dryRun = false; /// --dry-run 15 | bool force = false; /// --force 16 | 17 | bool detectedGit; /// After running detectTools(): Is this a git working directory? 18 | bool detectedHg; /// After running detectTools(): Is this a Mercurial working directory? 19 | 20 | /// Populates the `detectedGit` and `detectedHg` bools. 21 | void detectTools() 22 | { 23 | detectedGit = existsAsDir(".git"); 24 | detectedHg = existsAsDir(".hg"); 25 | 26 | logVerbose("Git working directory?: ", detectedGit); 27 | logVerbose("Hg working directory?: ", detectedHg); 28 | } 29 | 30 | /// Logging level 31 | enum LogLevel 32 | { 33 | silent, 34 | quiet, 35 | normal, 36 | verbose, 37 | trace, 38 | } 39 | auto logLevel = LogLevel.normal; /// Current logging level 40 | 41 | /// Log a message at a specific logging level 42 | void logQuiet (T...)(T args) { log!(LogLevel.quiet)(args); } ///ditto 43 | void logNormal (T...)(T args) { log!(LogLevel.normal)(args); } ///ditto 44 | void logVerbose(T...)(T args) { log!(LogLevel.verbose)(args); } ///ditto 45 | void logTrace (T...)(T args) { log!(LogLevel.trace)(args); } ///ditto 46 | void log(LogLevel minimumLogLevel, T...)(T args) ///ditto 47 | { 48 | static assert(minimumLogLevel != LogLevel.silent); 49 | 50 | if(logLevel >= minimumLogLevel) 51 | writeln(args); 52 | } 53 | --------------------------------------------------------------------------------