├── tests ├── testExample ├── testExample.bat ├── testExample.sh └── testExample.d ├── dub.selections.json ├── dummy └── dummy.d ├── CHANGELOG.md ├── docs └── public │ ├── images │ └── ddox │ │ ├── alias.png │ │ ├── class.png │ │ ├── enum.png │ │ ├── module.png │ │ ├── struct.png │ │ ├── function.png │ │ ├── package.png │ │ ├── private.png │ │ ├── property.png │ │ ├── template.png │ │ ├── variable.png │ │ ├── enummember.png │ │ ├── inherited.png │ │ ├── interface.png │ │ └── protected.png │ ├── prettify │ ├── prettify.css │ └── prettify.js │ ├── scripts │ ├── ddox.js │ └── mousetrap.js │ └── styles │ └── ddox.css ├── examples ├── dub-project │ ├── dub.selections.json │ ├── dub.sdl │ ├── dub.json │ ├── myscript.d │ └── README.txt ├── features │ ├── AutomaticPhobosImport.d │ ├── README.txt │ ├── StringInterpolation.d │ ├── DryRunAssistance.d │ ├── UserInputPrompts.d │ ├── DisambiguatingWrite.d │ ├── Fail.d │ ├── CommandEchoing.d │ ├── TryAsFilesystemOperations.d │ ├── Filepaths.d │ └── ScriptStyleShellCommands.d └── single-file │ ├── myscript.d │ └── README.txt ├── .gitignore ├── dub.sdl ├── src └── scriptlike │ ├── only.d │ ├── std.d │ ├── package.d │ ├── fail.d │ ├── path │ ├── wrappers.d │ ├── extras.d │ └── package.d │ ├── file │ ├── package.d │ └── extras.d │ ├── interact.d │ ├── process.d │ └── core.d ├── .travis.yml ├── LICENSE.txt ├── makedocs ├── makedocs.bat ├── ddoc ├── macros.ddoc └── changelog.d ├── USAGE.md ├── appveyor.yml └── README.md /tests/testExample: -------------------------------------------------------------------------------- 1 | testExample.sh -------------------------------------------------------------------------------- /dub.selections.json: -------------------------------------------------------------------------------- 1 | { 2 | "fileVersion": 1, 3 | "versions": {} 4 | } 5 | -------------------------------------------------------------------------------- /dummy/dummy.d: -------------------------------------------------------------------------------- 1 | // This file/dir just keeps DUB from using it's own built-in buildsystem. 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Scriptlike's changelog has [moved here](http://semitwist.com/scriptlike/changelog.html). 2 | -------------------------------------------------------------------------------- /docs/public/images/ddox/alias.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abscissa/scriptlike/HEAD/docs/public/images/ddox/alias.png -------------------------------------------------------------------------------- /docs/public/images/ddox/class.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abscissa/scriptlike/HEAD/docs/public/images/ddox/class.png -------------------------------------------------------------------------------- /docs/public/images/ddox/enum.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abscissa/scriptlike/HEAD/docs/public/images/ddox/enum.png -------------------------------------------------------------------------------- /docs/public/images/ddox/module.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abscissa/scriptlike/HEAD/docs/public/images/ddox/module.png -------------------------------------------------------------------------------- /docs/public/images/ddox/struct.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abscissa/scriptlike/HEAD/docs/public/images/ddox/struct.png -------------------------------------------------------------------------------- /docs/public/images/ddox/function.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abscissa/scriptlike/HEAD/docs/public/images/ddox/function.png -------------------------------------------------------------------------------- /docs/public/images/ddox/package.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abscissa/scriptlike/HEAD/docs/public/images/ddox/package.png -------------------------------------------------------------------------------- /docs/public/images/ddox/private.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abscissa/scriptlike/HEAD/docs/public/images/ddox/private.png -------------------------------------------------------------------------------- /docs/public/images/ddox/property.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abscissa/scriptlike/HEAD/docs/public/images/ddox/property.png -------------------------------------------------------------------------------- /docs/public/images/ddox/template.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abscissa/scriptlike/HEAD/docs/public/images/ddox/template.png -------------------------------------------------------------------------------- /docs/public/images/ddox/variable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abscissa/scriptlike/HEAD/docs/public/images/ddox/variable.png -------------------------------------------------------------------------------- /docs/public/images/ddox/enummember.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abscissa/scriptlike/HEAD/docs/public/images/ddox/enummember.png -------------------------------------------------------------------------------- /docs/public/images/ddox/inherited.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abscissa/scriptlike/HEAD/docs/public/images/ddox/inherited.png -------------------------------------------------------------------------------- /docs/public/images/ddox/interface.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abscissa/scriptlike/HEAD/docs/public/images/ddox/interface.png -------------------------------------------------------------------------------- /docs/public/images/ddox/protected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Abscissa/scriptlike/HEAD/docs/public/images/ddox/protected.png -------------------------------------------------------------------------------- /examples/dub-project/dub.selections.json: -------------------------------------------------------------------------------- 1 | { 2 | "fileVersion": 1, 3 | "versions": { 4 | "scriptlike": "0.10.2" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /examples/dub-project/dub.sdl: -------------------------------------------------------------------------------- 1 | name "myscript" 2 | targetType "executable" 3 | mainSourceFile "myscript.d" 4 | dependency "scriptlike" version="~>0.10.2" 5 | -------------------------------------------------------------------------------- /tests/testExample.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | IF [%DMD%] == [] set DMD=dmd 3 | rdmd --compiler=%DMD% --force -debug -g -I%~dp0../src/ -of%~dp0.testExample %~dp0testExample.d %* 4 | -------------------------------------------------------------------------------- /examples/features/AutomaticPhobosImport.d: -------------------------------------------------------------------------------- 1 | import scriptlike; 2 | //import scriptlike.only; // In case you don't want Phobos auto-imported 3 | void main() { 4 | writeln("Works!"); 5 | } 6 | -------------------------------------------------------------------------------- /examples/dub-project/dub.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "myscript", 3 | "targetType": "executable", 4 | "mainSourceFile": "myscript.d", 5 | "dependencies": { 6 | "scriptlike": "~>0.10.2" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /examples/features/README.txt: -------------------------------------------------------------------------------- 1 | To build and run any of the examples in this directory: 2 | 3 | $ rdmd -I../../src (WhateverFile).d [program args] 4 | 5 | Examples: 6 | $ rdmd -I../../src StringInterpolation.d 7 | $ rdmd -I../../src Fail.d abc 123 8 | -------------------------------------------------------------------------------- /examples/dub-project/myscript.d: -------------------------------------------------------------------------------- 1 | import scriptlike; 2 | 3 | void main(string[] args) { 4 | writeln("This script is in directory: ", thisExePath.dirName); 5 | 6 | string name; 7 | if(args.length > 1) 8 | name = args[1]; 9 | else 10 | name = userInput!string("What's your name?"); 11 | 12 | writeln("Hello, ", name, "!"); 13 | } 14 | -------------------------------------------------------------------------------- /examples/dub-project/README.txt: -------------------------------------------------------------------------------- 1 | Note, you only need "dub.json" OR "dub.sdl", but both are provided here for convenience. 2 | 3 | To run this example: 4 | 5 | First, ensure you have DMD and DUB installed: 6 | - DMD: http://dlang.org/download.html#dmd 7 | - DUB: http://code.dlang.org/download 8 | 9 | And then: 10 | $ dub 11 | or 12 | $ dub -- Frank 13 | -------------------------------------------------------------------------------- /examples/single-file/myscript.d: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env dub 2 | /+ dub.sdl: 3 | name "myscript" 4 | dependency "scriptlike" version="~>0.10.2" 5 | +/ 6 | import scriptlike; 7 | 8 | void main(string[] args) { 9 | string name; 10 | if(args.length > 1) 11 | name = args[1]; 12 | else 13 | name = userInput!string("What's your name?"); 14 | 15 | writeln("Hello, ", name, "!"); 16 | } 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .dub/ 2 | bin/ 3 | deleteme/ 4 | docs/docs.json 5 | docs/public/file_hashes.json 6 | docs/public/*.html 7 | docs/public/sitemap.xml 8 | docs/public/symbols.js 9 | docs/public/scriptlike/ 10 | 11 | src/scriptlike/packageVersion.d 12 | ddoc/packageVersion.ddoc 13 | 14 | /examples/dub-project/myscript 15 | /examples/single-file/myscript 16 | tests/.testExample* 17 | tests/bin/ 18 | 19 | *.exe 20 | /tests/.o 21 | -------------------------------------------------------------------------------- /tests/testExample.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | SCRIPT_DIR="$(dirname "$(dirname "$0")/$(readlink "$0")")" 3 | if [ -z "$DMD" ]; then 4 | DMD=dmd 5 | fi 6 | # RDMD isn't built-in on travis-ci's LDC/GDC 7 | $DMD -debug -g -I$SCRIPT_DIR/../src $SCRIPT_DIR/../src/**/*.d $SCRIPT_DIR/../src/scriptlike/**/*.d -of$SCRIPT_DIR/.testExample $SCRIPT_DIR/testExample.d && $SCRIPT_DIR/.testExample "$@" 8 | 9 | TEST_STATUS=$? 10 | rm $SCRIPT_DIR/.o -f 11 | exit $TEST_STATUS 12 | -------------------------------------------------------------------------------- /examples/single-file/README.txt: -------------------------------------------------------------------------------- 1 | To run this example: 2 | 3 | First, ensure you have a DUB installed, and a D compiler such as DMD: 4 | - DMD: http://dlang.org/download.html#dmd 5 | - DUB: http://code.dlang.org/download 6 | 7 | DUB must be at least v1.0.0. 8 | 9 | And then... 10 | 11 | On Linux/OSX: 12 | ------------- 13 | $ chmod +x myscript.d 14 | $ ./myscript.d 15 | or 16 | $ ./myscript.d Frank 17 | 18 | On *any* OS: 19 | ------------- 20 | $ dub myscript.d 21 | or 22 | $ dub myscript.d Frank 23 | -------------------------------------------------------------------------------- /examples/features/StringInterpolation.d: -------------------------------------------------------------------------------- 1 | import scriptlike; 2 | 3 | void main() 4 | { 5 | // Output: The number 21 doubled is 42! 6 | int num = 21; 7 | writeln( mixin(interp!"The number ${num} doubled is ${num * 2}!") ); 8 | 9 | // Output: Empty braces output nothing. 10 | writeln( mixin(interp!"Empty ${}braces ${}output nothing.") ); 11 | 12 | // Output: Multiple params: John Doe. 13 | auto first = "John", last = "Doe"; 14 | writeln( mixin(interp!`Multiple params: ${first, " ", last}.`) ); 15 | } 16 | -------------------------------------------------------------------------------- /examples/features/DryRunAssistance.d: -------------------------------------------------------------------------------- 1 | import scriptlike; 2 | 3 | void main() 4 | { 5 | scriptlikeDryRun = true; 6 | 7 | // When dry-run is enabled, this echoes but doesn't actually copy or invoke DMD. 8 | copy("original.d", "app.d"); 9 | run("dmd app.d -ofbin/app"); 10 | 11 | // Works fine in dry-run, since it doesn't modify the filesystem. 12 | bool isItThere = exists("another-file"); 13 | 14 | if(!scriptlikeDryRun) 15 | { 16 | // This won't work right if we're running in dry-run mode, 17 | // since it'll be out-of-date, if it even exists at all. 18 | auto source = read("app.d"); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/features/UserInputPrompts.d: -------------------------------------------------------------------------------- 1 | import scriptlike; 2 | 3 | void main() 4 | { 5 | auto name = userInput!string("Please enter your name"); 6 | auto age = userInput!int("And your age"); 7 | 8 | if(userInput!bool("Do you want to continue?")) 9 | { 10 | string outputFolder = pathLocation("Where you do want to place the output?"); 11 | auto color = menu!string("What color would you like to use?", ["Blue", "Green"]); 12 | } 13 | 14 | auto num = require!(int, "a > 0 && a <= 10")("Enter a number from 1 to 10"); 15 | 16 | pause(); // Prompt "Press Enter to continue..."; 17 | pause("Hit Enter again, dood!!"); 18 | } 19 | -------------------------------------------------------------------------------- /examples/features/DisambiguatingWrite.d: -------------------------------------------------------------------------------- 1 | import scriptlike; 2 | import std.stdio; 3 | 4 | void main() 5 | { 6 | // Setup and cleanup 7 | chdir(thisExePath.dirName); 8 | scope(exit) 9 | tryRemove("filename.txt"); 10 | 11 | // Save file 12 | //write("filename.txt", "content"); // Error: Symbols conflict! 13 | // Change line above to... 14 | writeFile("filename.txt", "content"); // Convenience alias included in scriptlike 15 | 16 | // Output to stdout with no newline 17 | //write("Hello ", "world"); // Error: Symbols conflict! 18 | // Change line above to... 19 | std.stdio.write("Hello ", "world"); 20 | // or... 21 | stdout.write("Hello ", "world"); 22 | } 23 | -------------------------------------------------------------------------------- /examples/features/Fail.d: -------------------------------------------------------------------------------- 1 | /++ 2 | Example: 3 | -------- 4 | $ test 5 | test: ERROR: Need two args, not 0! 6 | $ test abc 123 7 | test: ERROR: First arg must be 'foobar', not 'abc'! 8 | -------- 9 | +/ 10 | 11 | import scriptlike; 12 | 13 | void main(string[] args) { 14 | helper(args); 15 | } 16 | 17 | // Throws a Fail exception on bad args: 18 | void helper(string[] args) { 19 | // Like std.exception.enforce, but bails with no ugly stack trace, 20 | // and if uncaught, outputs the program name and "ERROR: " 21 | failEnforce(args.length == 3, "Need two args, not ", args.length-1, "!"); 22 | 23 | if(args[1] != "foobar") 24 | fail("First arg must be 'foobar', not '", args[1], "'!"); 25 | } 26 | -------------------------------------------------------------------------------- /dub.sdl: -------------------------------------------------------------------------------- 1 | name "scriptlike" 2 | description "Utility library to help you write script-like programs." 3 | authors "Nick Sabalausky" "Jesse Phillips" 4 | homepage "https://github.com/abscissa/scriptlike" 5 | license "zlib/libpng" 6 | 7 | excludedSourceFiles "src/scriptlike/packageVersion.d" 8 | 9 | configuration "library" { 10 | targetType "library" 11 | targetPath "bin" 12 | importPaths "src" 13 | } 14 | 15 | configuration "unittest" { 16 | targetType "executable" 17 | targetPath "bin" 18 | targetName "scriptlike_unittest" 19 | versions "unittest_scriptlike_d" 20 | } 21 | 22 | configuration "no-build" { 23 | targetType "library" 24 | targetPath "deleteme" 25 | importPaths "dummy" 26 | sourcePaths "dummy" 27 | excludedSourceFiles "src/*" 28 | } 29 | -------------------------------------------------------------------------------- /src/scriptlike/only.d: -------------------------------------------------------------------------------- 1 | /++ 2 | $(H2 Scriptlike $(SCRIPTLIKE_VERSION)) 3 | Utility to aid in script-like programs. 4 | 5 | Written in the $(LINK2 http://dlang.org, D programming language). 6 | 7 | Import this `scriptlike.only` module instead of `scriptlike` if you want to 8 | import all of Scriptlike, but DON'T want to automatically import any of Phobos. 9 | 10 | Copyright: Copyright (C) 2014-2017 Nick Sabalausky 11 | License: $(LINK2 https://github.com/Abscissa/scriptlike/blob/master/LICENSE.txt, zlib/libpng) 12 | Authors: Nick Sabalausky 13 | +/ 14 | 15 | 16 | module scriptlike.only; 17 | 18 | public import scriptlike.core; 19 | public import scriptlike.interact; 20 | public import scriptlike.fail; 21 | public import scriptlike.file; 22 | public import scriptlike.path; 23 | public import scriptlike.process; 24 | -------------------------------------------------------------------------------- /examples/features/CommandEchoing.d: -------------------------------------------------------------------------------- 1 | import scriptlike; 2 | 3 | void main() 4 | { 5 | // Setup and cleanup 6 | chdir(thisExePath.dirName); 7 | scope(exit) 8 | { 9 | scriptlikeEcho = false; 10 | tryRemove("file.txt"); 11 | tryRmdirRecurse("some"); 12 | } 13 | 14 | /++ 15 | Output: 16 | -------- 17 | run: echo Hello > file.txt 18 | mkdirRecurse: some/new/dir 19 | copy: file.txt -> 'some/new/dir/target name.txt' 20 | Gonna run foo() now... 21 | foo: i = 42 22 | -------- 23 | +/ 24 | 25 | scriptlikeEcho = true; // Enable automatic echoing 26 | 27 | run("echo Hello > file.txt"); 28 | 29 | auto newDir = Path("some/new/dir"); 30 | mkdirRecurse(newDir.raw); // Even works with non-Path overloads 31 | copy("file.txt", newDir ~ "target name.txt"); 32 | 33 | void foo(int i = 42) { 34 | yapFunc("i = ", i); // Evaluated lazily 35 | } 36 | 37 | // yap and yapFunc ONLY output when echoing is enabled 38 | yap("Gonna run foo() now..."); 39 | foo(); 40 | } 41 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: d 2 | sudo: false 3 | 4 | before_script: 5 | - dub add-local . 6 | 7 | matrix: 8 | include: 9 | - d: dmd-2.080.0 10 | - d: dmd-2.079.1 11 | - d: dmd-2.078.2 12 | - d: dmd-2.077.1 13 | - d: dmd-2.076.1 14 | - d: dmd-2.075.1 15 | - d: dmd-2.074.1 16 | - d: dmd-2.073.1 17 | - d: dmd-2.072.2 18 | - d: dmd-2.071.2 19 | - d: dmd-2.070.2 20 | - d: dmd-2.069.2 21 | - d: dmd-2.068.1 22 | - d: dmd-2.068.0 23 | - d: dmd-2.067.1 24 | - d: dmd-2.067.0 25 | - d: dmd-2.066.1 26 | - d: dmd-2.066.0 27 | - d: ldc-1.9.0 28 | - d: ldc-1.8.0 29 | - d: ldc-1.7.0 30 | - d: ldc-1.6.0 31 | - d: ldc-1.5.0 32 | - d: ldc-1.4.0 33 | - d: ldc-1.3.0 34 | - d: ldc-1.2.0 35 | - d: ldc-1.1.1 36 | - d: ldc-1.1.0 37 | - d: ldc-1.0.0 38 | - d: ldc-0.17.5 39 | - d: ldc-0.17.1 40 | - d: ldc-0.16.1 41 | - d: gdc-5.2.0 42 | - d: gdc-4.9.3 43 | - d: gdc-4.9.2 44 | - d: gdc-4.8.5 45 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | ----------------------------------------------------- 2 | Scriptlike is licensed under The zlib/libpng License: 3 | ----------------------------------------------------- 4 | 5 | Copyright (c) 2014-2017 Nick Sabalausky 6 | Portions Copyright (C) 2010 Jesse Phillips 7 | 8 | This software is provided 'as-is', without any express or implied 9 | warranty. In no event will the authors be held liable for any damages 10 | arising from the use of this software. 11 | 12 | Permission is granted to anyone to use this software for any purpose, 13 | including commercial applications, and to alter it and redistribute it 14 | freely, subject to the following restrictions: 15 | 16 | 1. The origin of this software must not be misrepresented; you must not 17 | claim that you wrote the original software. If you use this software 18 | in a product, an acknowledgment in the product documentation would be 19 | appreciated but is not required. 20 | 21 | 2. Altered source versions must be plainly marked as such, and must not be 22 | misrepresented as being the original software. 23 | 24 | 3. This notice may not be removed or altered from any source 25 | distribution. 26 | -------------------------------------------------------------------------------- /makedocs: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo "Note, generating Scriptlike's docs requires that dub" 4 | echo " be available on your PATH." 5 | echo 6 | echo "You must also have ddox and gen-package-version available through dub:" 7 | echo "$ dub fetch ddox --version=0.15.18" 8 | echo "$ dub fetch gen-package-version --version=1.0.5" 9 | echo "or:" 10 | echo "$ dub add-local [path/to/ddox]" 11 | echo "$ dub add-local [path/to/gen-package-version]" 12 | echo 13 | echo "You may need to remove any older versions installed" 14 | echo "so they don't get run instead." 15 | echo 16 | echo "If you get errors, double-check you have dub, ddox and gen-package-version" 17 | echo "all installed as described above." 18 | echo 19 | 20 | dub run gen-package-version -- scriptlike --src=src --ddoc=ddoc 21 | rdmd -Isrc -Iddoc --build-only --force -c -Dddocs_tmp -X -Xfdocs/docs.json -version=docs_scriptlike_d src/scriptlike/package.d 22 | rm -rf docs_tmp 23 | rm src/scriptlike/package.o 24 | dub run ddox -- filter docs/docs.json --min-protection=Protected --ex=scriptlike.packageVersion 25 | dub run ddox -- generate-html docs/docs.json docs/public --navigation-type=ModuleTree --override-macros=ddoc/macros.ddoc --override-macros=ddoc/packageVersion.ddoc 26 | -------------------------------------------------------------------------------- /makedocs.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | echo Note, generating Scriptlike's docs requires that dub 4 | echo ^ be available on your PATH. 5 | echo. 6 | echo You must also have ddox and gen-package-version available through dub: 7 | echo ^> dub fetch ddox --version=0.15.18 8 | echo ^> dub fetch gen-package-version --version=1.0.5 9 | echo or: 10 | echo ^> dub add-local [path/to/ddox] 11 | echo ^> dub add-local [path/to/gen-package-version] 12 | echo. 13 | echo You may need to remove any older versions installed 14 | echo so they don't get run instead. 15 | echo. 16 | echo If you get errors, double-check you have dub, ddox and gen-package-version 17 | echo all installed as described above. 18 | echo. 19 | 20 | dub run gen-package-version -- scriptlike --src=src --ddoc=ddoc 21 | rdmd -Isrc -Iddoc --build-only --force -c -Dddocs_tmp -X -Xfdocs\docs.json -version=docs_scriptlike_d src\scriptlike\package.d 22 | rmdir /S /Q docs_tmp > NUL 2> NUL 23 | del src\scriptlike\package.obj 24 | dub run ddox -- filter docs\docs.json --min-protection=Protected --ex=scriptlike.packageVersion 25 | dub run ddox -- generate-html docs\docs.json docs\public --navigation-type=ModuleTree --override-macros=ddoc\macros.ddoc --override-macros=ddoc\packageVersion.ddoc 26 | -------------------------------------------------------------------------------- /examples/features/TryAsFilesystemOperations.d: -------------------------------------------------------------------------------- 1 | import scriptlike; 2 | 3 | void main() 4 | { 5 | // Setup 6 | chdir(thisExePath.dirName); 7 | 8 | // Just MAKE SURE this exists! If it's already there, then GREAT! 9 | tryMkdir("somedir"); 10 | assertThrown( mkdir("somedir") ); // Exception: Already exists! 11 | tryMkdir("somedir"); // Works fine! 12 | 13 | // Just MAKE SURE this is gone! If it's already gone, then GREAT! 14 | tryRmdir("somedir"); 15 | assertThrown( rmdir("somedir") ); // Exception: Already gone! 16 | tryRmdir("somedir"); // Works fine! 17 | 18 | // Just MAKE SURE it doesn't exist. Don't bother me if it doesn't! 19 | tryRemove("file"); 20 | 21 | // Copy if it exists, otherwise don't worry about it. 22 | tryCopy("file", "file-copy"); 23 | 24 | // Is this a directory? If it doesn't even exist, 25 | // then it's obviously NOT a directory. 26 | assertThrown( isDir("foo/bar") ); // Exception: Doesn't exist! 27 | if(existsAsDir("foo/bar")) // Works fine! 28 | {/+ ...do stuff... +/} 29 | 30 | // Bonus! Single function to delete files OR directories! 31 | writeFile("file.txt", "abc"); 32 | tryMkdirRecurse("foo/bar/dir"); 33 | writeFile("foo/bar/dir/file.txt", "123"); 34 | // Delete with the same function! 35 | removePath("file.txt"); // Calls 'remove' 36 | removePath("foo"); // Calls 'rmdirRecurse' 37 | tryRemovePath("file.txt"); // Also comes in try flavor! 38 | tryRemovePath("foo"); 39 | } 40 | -------------------------------------------------------------------------------- /examples/features/Filepaths.d: -------------------------------------------------------------------------------- 1 | import scriptlike; 2 | 3 | void main() 4 | { 5 | // Setup and cleanup 6 | chdir(thisExePath.dirName); 7 | scope(exit) 8 | { 9 | tryRmdirRecurse("path"); 10 | tryRmdirRecurse("target"); 11 | } 12 | mkdirRecurse("path/to"); 13 | mkdirRecurse("target/path"); 14 | import scriptlike.file.wrappers : write; 15 | write("path/to/file.txt", "abc"); 16 | 17 | // This is AUTOMATICALLY kept normalized (via std.path.buildNormalizedPath) 18 | auto dir = Path("foo/bar"); 19 | dir ~= "subdir"; // Append a subdirectory 20 | 21 | // No worries about trailing slashes! 22 | assert(Path("foo/bar") == Path("foo/bar/")); 23 | assert(Path("foo/bar/") == Path("foo/bar//")); 24 | 25 | 26 | // No worries about forward/backslashes! 27 | assert(dir == Path("foo/bar/subdir")); 28 | assert(dir == Path("foo\\bar\\subdir")); 29 | 30 | // No worries about spaces! 31 | auto file = dir.up ~ "different subdir\\Filename with spaces.txt"; 32 | assert(file == Path("foo/bar/different subdir/Filename with spaces.txt")); 33 | writeln(file); // Path.toString() always properly escapes for current platform! 34 | writeln(file.raw); // Don't escape! 35 | 36 | // Even file extentions are type-safe! 37 | Ext ext = file.extension; 38 | auto anotherFile = Path("path/to/file") ~ ext; 39 | assert(anotherFile.baseName == Path("file.txt")); 40 | 41 | // std.path and std.file are wrapped to offer Path/Ext support 42 | assert(dirName(anotherFile) == Path("path/to")); 43 | copy(anotherFile, Path("target/path/new file.txt")); 44 | } 45 | -------------------------------------------------------------------------------- /src/scriptlike/std.d: -------------------------------------------------------------------------------- 1 | /++ 2 | $(H2 Scriptlike $(SCRIPTLIKE_VERSION)) 3 | Utility to aid in script-like programs. 4 | 5 | Written in the $(LINK2 http://dlang.org, D programming language). 6 | 7 | Automatically pulls in anything from Phobos likely to be useful for scripts. 8 | 9 | The public `std.file` and `std.path` imports here are static imports to 10 | avoid name conflicts with the $(API_PATH_EXTR Path)-based wrappers in 11 | `scriptlike.file` and `scriptlike.path`. 12 | 13 | curl is omitted here because it involves an extra link dependency. 14 | 15 | Copyright: Copyright (C) 2014-2017 Nick Sabalausky 16 | License: $(LINK2 https://github.com/Abscissa/scriptlike/blob/master/LICENSE.txt, zlib/libpng) 17 | Authors: Nick Sabalausky 18 | +/ 19 | 20 | module scriptlike.std; 21 | 22 | public import std.algorithm; 23 | public import std.array; 24 | public import std.bigint; 25 | public import std.conv; 26 | public import std.datetime; 27 | public import std.exception; 28 | public import std.getopt; 29 | public import std.math; 30 | public import std.process; 31 | public import std.random; 32 | public import std.range; 33 | public import std.regex; 34 | public import std.stdio; 35 | public import std.string; 36 | public import std.system; 37 | public import std.traits; 38 | public import std.typecons; 39 | public import std.typetuple; 40 | public import std.uni; 41 | public import std.variant; 42 | 43 | public import std.path : dirSeparator, pathSeparator, isDirSeparator, 44 | buildPath, buildNormalizedPath; 45 | 46 | public static import std.file; 47 | public static import std.path; 48 | -------------------------------------------------------------------------------- /examples/features/ScriptStyleShellCommands.d: -------------------------------------------------------------------------------- 1 | import scriptlike; 2 | 3 | void main() 4 | { 5 | // Setup and cleanup 6 | chdir(thisExePath.dirName); 7 | scope(exit) 8 | tryRmdirRecurse("my"); 9 | mkdirRecurse("my/proj/dir/src"); 10 | import scriptlike.file.wrappers : write; 11 | write("my/proj/dir/src/main.d", `import std.stdio; void main() { writeln("Hello"); }`); 12 | 13 | run("dmd --help"); // Display DMD help screen 14 | pause(); // Wait for user to hit Enter 15 | 16 | // Automatically throws ErrorLevelException(1, "dmd --bad-flag") 17 | assertThrown!ErrorLevelException( run("dmd --bad-flag") ); 18 | 19 | // Automatically throws ErrorLevelException(-1, "this-cmd-does-not-exist") 20 | assertThrown!ErrorLevelException( run("this-cmd-does-not-exist") ); 21 | 22 | // Don't bail on error 23 | int statusCode = tryRun("dmd --bad-flag"); 24 | 25 | // Collect output instead of showing it 26 | string dmdHelp = runCollect("dmd --help"); 27 | auto isDMD_2_068_1 = dmdHelp.canFind("D Compiler v2.068.1"); 28 | 29 | // Don't bail on error 30 | auto result = tryRunCollect("dmd --help"); 31 | if(result.status == 0 && result.output.canFind("D Compiler v2.068.1")) 32 | writeln("Found DMD v2.068.1!"); 33 | 34 | // Use any working directory: 35 | auto myProjectDir = Path("my/proj/dir"); 36 | auto mainFile = Path("src/main.d"); 37 | myProjectDir.run(text("dmd ", mainFile, " -O")); // mainFile is properly escaped! 38 | 39 | // Verify it actually IS running from a different working directory: 40 | version(Posix) enum pwd = "pwd"; 41 | else version(Windows) enum pwd = "cd"; 42 | else static assert(0); 43 | auto output = myProjectDir.runCollect(pwd); 44 | auto expected = getcwd() ~ myProjectDir; 45 | assert( Path(output.strip()) == expected ); 46 | } 47 | -------------------------------------------------------------------------------- /src/scriptlike/package.d: -------------------------------------------------------------------------------- 1 | /++ 2 | $(H2 Scriptlike $(SCRIPTLIKE_VERSION)) 3 | Scriptlike is a utility library to help you write script-like programs in D. 4 | 5 | Written in the $(LINK2 http://dlang.org, D programming language) and licensed under 6 | The $(LINK2 https://github.com/Abscissa/scriptlike/blob/master/LICENSE.txt, zlib/libpng) License. 7 | 8 | For the list of officially supported compiler versions, see the 9 | $(LINK2 https://github.com/Abscissa/scriptlike/blob/master/.travis.yml, .travis.yml) 10 | file included with your version of Scriptlike. 11 | 12 | Links: 13 | $(UL 14 | $(LI $(LINK2 https://github.com/Abscissa/scriptlike, Scriptlike Homepage) ) 15 | $(LI $(LINK2 http://semitwist.com/scriptlike, Latest API Reference ) ) 16 | $(LI $(LINK2 http://semitwist.com/scriptlike-docs, Older API Reference Archives ) ) 17 | ) 18 | 19 | Import all (including anything from Phobos likely to be useful for scripts): 20 | ------------ 21 | import scriptlike; 22 | ------------ 23 | 24 | Import all of Scriptlike only, but no Phobos: 25 | ------------ 26 | import scriptlike.only; 27 | ------------ 28 | 29 | Homepage: 30 | $(LINK https://github.com/abscissa/scriptlike) 31 | 32 | Copyright: 33 | Copyright (C) 2014-2017 Nick Sabalausky. 34 | Portions Copyright (C) 2010 Jesse Phillips. 35 | 36 | License: $(LINK2 https://github.com/Abscissa/scriptlike/blob/master/LICENSE.txt, zlib/libpng) 37 | Authors: Nick Sabalausky, Jesse Phillips 38 | +/ 39 | 40 | module scriptlike; 41 | 42 | public import scriptlike.only; 43 | public import scriptlike.std; 44 | 45 | version(docs_scriptlike_d) import changelog; 46 | version(unittest_scriptlike_d) void main() {} 47 | 48 | // Run tests for sample programs in 'examples' 49 | version(unittest_scriptlike_d) 50 | unittest 51 | { 52 | version(Windows) 53 | // This Posix artifact gets in the way of calling .myscript.exe 54 | // Only an issue when Win/Posix machines are operating from the same directory. 55 | tryRemove("tests/.testExample"); 56 | 57 | writeln("Testing sample programs in 'examples':"); 58 | run(text( Path("tests/testExample"), " All" )); 59 | } 60 | -------------------------------------------------------------------------------- /docs/public/prettify/prettify.css: -------------------------------------------------------------------------------- 1 | /* Pretty printing styles. Used with prettify.js. */ 2 | 3 | /* SPAN elements with the classes below are added by prettyprint. */ 4 | .pln { color: #ffffff } /* plain text */ 5 | pre .pln { color: #ffffff } /* plain text */ 6 | 7 | @media screen { 8 | pre .str { color: #ffe7b6 } /* string content */ 9 | pre .typ { color: #9ad452 } /* a type name */ 10 | pre .lit { color: #ffe7b6 } /* a literal value */ 11 | pre .pun, .opn, .clo { color: #ddd } 12 | 13 | .str { color: #842 } /* string content */ 14 | .kwd { color: #ffaa00 } /* a keyword */ 15 | .com { color: #888 } /* a comment */ 16 | .typ { color: #693 } /* a type name */ 17 | .lit { color: #875 } /* a literal value */ 18 | /* punctuation, lisp open bracket, lisp close bracket */ 19 | .pun, .opn, .clo { color: #ddd } 20 | .tag { color: #ffaa00 } /* a markup tag name */ 21 | .atn { color: #9ad452 } /* a markup attribute name */ 22 | .atv { color: #ffe7b6 } /* a markup attribute value */ 23 | .dec, .var { color: #aaa } /* a declaration; a variable name */ 24 | .fun { color: red } /* a function name */ 25 | } 26 | 27 | /* Use higher contrast and text-weight for printable form. */ 28 | @media print, projection { 29 | .str { color: #060 } 30 | .kwd { color: #006; font-weight: bold } 31 | .com { color: #600; font-style: italic } 32 | .typ { color: #404; font-weight: bold } 33 | .lit { color: #044 } 34 | .pun, .opn, .clo { color: #440 } 35 | .tag { color: #006; font-weight: bold } 36 | .atn { color: #404 } 37 | .atv { color: #060 } 38 | } 39 | 40 | /* Put a border around prettyprinted code snippets. */ 41 | pre.prettyprint { 42 | padding: 0.4em 0.4em 0.4em 0.4em; 43 | color: #C0C0C0; 44 | background-color: #222; 45 | border: 1px solid black; 46 | } 47 | 48 | /* Specify class=linenums on a pre to get line numbering */ 49 | ol.linenums { margin-top: 0; margin-bottom: 0 } /* IE indents via margin-left */ 50 | li.L0, 51 | li.L1, 52 | li.L2, 53 | li.L3, 54 | li.L5, 55 | li.L6, 56 | li.L7, 57 | li.L8 { list-style-type: none } 58 | /* Alternate shading for lines */ 59 | li.L1, 60 | li.L3, 61 | li.L5, 62 | li.L7, 63 | li.L9 { background: #222222 } 64 | -------------------------------------------------------------------------------- /src/scriptlike/fail.d: -------------------------------------------------------------------------------- 1 | // Scriptlike: Utility to aid in script-like programs. 2 | // Written in the D programming language. 3 | 4 | /// Copyright: Copyright (C) 2014-2017 Nick Sabalausky 5 | /// License: $(LINK2 https://github.com/Abscissa/scriptlike/blob/master/LICENSE.txt, zlib/libpng) 6 | /// Authors: Nick Sabalausky 7 | 8 | module scriptlike.fail; 9 | 10 | import std.conv; 11 | import std.file; 12 | import std.path; 13 | import std.traits; 14 | 15 | /// This is the exception thrown by fail(). There's no need to create or throw 16 | /// this directly, but it's public in case you have reason to catch it. 17 | class Fail : Exception 18 | { 19 | private this() 20 | { 21 | super(null); 22 | } 23 | 24 | private static string msg; 25 | private static Fail opCall(string msg, string file=__FILE__, int line=__LINE__) 26 | { 27 | Fail.msg = msg; 28 | static if(__traits(compiles, Fail.classinfo.initializer)) 29 | // DMD 2.072 or 2.073 deprecates 'classinfo.init' 30 | throw cast(Fail) cast(void*) Fail.classinfo.initializer; 31 | else 32 | // DMD 2.069.2 and below lack 'classinfo.initializer' 33 | throw cast(Fail) cast(void*) Fail.classinfo.init; 34 | } 35 | 36 | private static string fullMessage(string msg = Fail.msg) 37 | { 38 | auto appName = thisExePath().baseName(); 39 | 40 | version(Windows) 41 | appName = appName.stripExtension(); 42 | 43 | return appName~": ERROR: "~msg; 44 | } 45 | 46 | override void toString(scope void delegate(in char[]) sink) const 47 | { 48 | sink(fullMessage()); 49 | } 50 | } 51 | 52 | /++ 53 | Call this to end your program with an error message for the user, and no 54 | ugly stack trace. The error message is sent to stderr and the errorlevel is 55 | set to non-zero. 56 | 57 | This is exception-safe, all cleanup code gets run. 58 | 59 | Your program's name is automatically detected from $(STD_FILE thisExePath). 60 | 61 | Example: 62 | ---------------- 63 | auto id = 3; 64 | fail("You forgot to provide a destination for id #", id, "!"); 65 | 66 | // Output: 67 | // yourProgramName: ERROR: You forgot to provide a destination for id #3! 68 | ---------------- 69 | +/ 70 | void fail(T...)(T args) 71 | { 72 | throw Fail( text(args) ); 73 | } 74 | 75 | /++ 76 | Calls fail() if the condition is false. 77 | 78 | This is much like $(FULL_STD_EXCEPTION enforce), but for for fail() instead of 79 | arbitrary exceptions. 80 | 81 | Example: 82 | ---------------- 83 | failEnforce(brokenSquareRoot(4)==2, "Reality broke! Expected 2, not ", brokenSquareRoot(4)); 84 | 85 | // Output: 86 | // yourProgramName: ERROR: Reality broke! Expected 2, not 555 87 | ---------------- 88 | +/ 89 | void failEnforce(T...)(bool cond, T args) 90 | { 91 | if(!cond) 92 | fail(args); 93 | } 94 | -------------------------------------------------------------------------------- /ddoc/macros.ddoc: -------------------------------------------------------------------------------- 1 | Ddoc 2 | 3 | Macros: 4 | H2 =

$0

5 | BR =
6 | DOLLAR = $ 7 | COLON = : 8 | EM = $(B $(I $0) ) 9 | ISSUE = $(LINK2 https://github.com/Abscissa/scriptlike/issues/$0, #$0) 10 | ENHANCE = $(LI $(B Enhancement$(COLON)) $0) 11 | CHANGE = $(LI $(B Change$(COLON)) $0) 12 | FIXED = $(LI $(B Fixed$(COLON)) $0) 13 | INTERNAL = $(LI $(B Internal$(COLON)) $0) 14 | TESTS = $(LI $(B Tests$(COLON)) $0) 15 | API_BASE = $(DDOX_ROOT_DIR)scriptlike/ 16 | API_CORE = $(D_INLINECODE $(LINK2 $(API_BASE)core/$0.html, $0)) 17 | API_FAIL = $(D_INLINECODE $(LINK2 $(API_BASE)fail/$0.html, $0)) 18 | API_FILE_EXTR = $(D_INLINECODE $(LINK2 $(API_BASE)file/extras/$0.html, $0)) 19 | API_FILE_WRAP = $(D_INLINECODE $(LINK2 $(API_BASE)file/wrappers/$0.html, $0)) 20 | API_INTERACT = $(D_INLINECODE $(LINK2 $(API_BASE)interact/$0.html, $0)) 21 | API_PATH_EXTR = $(D_INLINECODE $(LINK2 $(API_BASE)path/extras/$0.html, $0)) 22 | API_PATH_WRAP = $(D_INLINECODE $(LINK2 $(API_BASE)path/wrappers/$0.html, $0)) 23 | API_PROCESS = $(D_INLINECODE $(LINK2 $(API_BASE)process/$0.html, $0)) 24 | MODULE_CORE = $(D_INLINECODE $(LINK2 $(API_BASE)core.html, scriptlike.core)) 25 | MODULE_FAIL = $(D_INLINECODE $(LINK2 $(API_BASE)fail.html, scriptlike.fail)) 26 | MODULE_FILE = $(D_INLINECODE $(LINK2 $(API_BASE)file.html, scriptlike.file)) 27 | MODULE_FILE_EXTR = $(D_INLINECODE $(LINK2 $(API_BASE)file/extras.html, scriptlike.file.extras)) 28 | MODULE_FILE_WRAP = $(D_INLINECODE $(LINK2 $(API_BASE)file/wrappers.html, scriptlike.file.wrappers)) 29 | MODULE_INTERACT = $(D_INLINECODE $(LINK2 $(API_BASE)interact.html, scriptlike.interact)) 30 | MODULE_PATH = $(D_INLINECODE $(LINK2 $(API_BASE)path.html, scriptlike.path)) 31 | MODULE_PATH_EXTR = $(D_INLINECODE $(LINK2 $(API_BASE)path/extras.html, scriptlike.path.extras)) 32 | MODULE_PATH_WRAP = $(D_INLINECODE $(LINK2 $(API_BASE)path/wrappers.html, scriptlike.path.wrappers)) 33 | MODULE_PROCESS = $(D_INLINECODE $(LINK2 $(API_BASE)process.html, scriptlike.process)) 34 | STD_EXCEPTION = $(D_INLINECODE $(LINK2 http://dlang.org/phobos/std_exception.html#$0, $0)) 35 | STD_FILE = $(D_INLINECODE $(LINK2 http://dlang.org/phobos/std_file.html#$0, $0)) 36 | STD_PATH = $(D_INLINECODE $(LINK2 http://dlang.org/phobos/std_path.html#$0, $0)) 37 | STD_PROCESS = $(D_INLINECODE $(LINK2 http://dlang.org/phobos/std_process.html#$0, $0)) 38 | STD_STDIO = $(D_INLINECODE $(LINK2 http://dlang.org/phobos/std_stdio.html#$0, $0)) 39 | FULL_STD_EXCEPTION = $(D_INLINECODE $(LINK2 http://dlang.org/phobos/std_exception.html#$0, std.exception.$0)) 40 | FULL_STD_FILE = $(D_INLINECODE $(LINK2 http://dlang.org/phobos/std_file.html#$0, std.file.$0)) 41 | FULL_STD_PATH = $(D_INLINECODE $(LINK2 http://dlang.org/phobos/std_path.html#$0, std.path.$0)) 42 | FULL_STD_PROCESS = $(D_INLINECODE $(LINK2 http://dlang.org/phobos/std_process.html#$0, std.process.$0)) 43 | FULL_STD_STDIO = $(D_INLINECODE $(LINK2 http://dlang.org/phobos/std_stdio.html#$0, std.stdio.$0)) 44 | DMD = $(LINK2 http://dlang.org/, DMD) 45 | DDOX = $(LINK2 https://github.com/rejectedsoftware/ddox, ddox) 46 | MODULE_STD_FILE = $(D_INLINECODE $(LINK2 http://dlang.org/phobos/std_file.html, std.file)) 47 | MODULE_STD_PATH = $(D_INLINECODE $(LINK2 http://dlang.org/phobos/std_path.html, std.path)) 48 | LICENSE = $(LINK2 https://github.com/Abscissa/scriptlike/blob/master/LICENSE.txt, zlib/libpng) 49 | -------------------------------------------------------------------------------- /USAGE.md: -------------------------------------------------------------------------------- 1 | How to Use Scriptlike in Scripts 2 | ================================ 3 | 4 | These examples can be found in the 5 | "[examples](https://github.com/Abscissa/scriptlike/blob/master/examples)" directory. 6 | 7 | * [A Basic Script in D](#a-basic-script-in-d) 8 | * [In a DUB-based project](#in-a-dub-based-project) 9 | 10 | A Basic Script in D 11 | ------------------- 12 | Make sure you have [DUB](http://code.dlang.org/download) v1.0.0 or later installed, 13 | as well as a D compiler (DMD, LDC, or GDC). You can check your version of DUB 14 | by running `dub --help`. Then, using Scriptlike in a D script is easy: 15 | 16 | myscript.d: 17 | ```d 18 | #!/usr/bin/env dub 19 | /+ dub.sdl: 20 | name "myscript" 21 | dependency "scriptlike" version="~>0.10.2" 22 | +/ 23 | import scriptlike; 24 | 25 | void main(string[] args) { 26 | string name; 27 | if(args.length > 1) 28 | name = args[1]; 29 | else 30 | name = userInput!string("What's your name?"); 31 | 32 | writeln("Hello, ", name, "!"); 33 | } 34 | ``` 35 | 36 | You don't even need to download or install Scriptlike! That will be done 37 | automatically by DUB, thanks to the `dependency` line in the special 38 | `dub.sdl` comment section. (Learn 39 | [more about this feature of DUB](http://code.dlang.org/getting_started#single-file-packages), 40 | introduced in DUB v1.0.0.) 41 | 42 | On Linux/OSX, you can then run that script just like any shell script: 43 | 44 | ```bash 45 | $ chmod +x myscript.d 46 | $ ./myscript.d Frank 47 | Hello, Frank! 48 | ``` 49 | 50 | As long as you have a D compiler installed (DMD, LDC or GDC), that will 51 | cause DUB to automatically download/install all dependencies (in this case, 52 | just Scriptlike), recompile the script if necessary, and run it. 53 | 54 | Or if you're on Windows (this will also work on Linux/OSX, too): 55 | ```batch 56 | > dub myscript.d Frank 57 | Hello, Frank! 58 | ``` 59 | 60 | NOTE: Due to [an issue](https://github.com/dlang/dub/issues/907) in DUB, 61 | if you use this single-file approach and you need to use 62 | [`thisExePath`](http://semitwist.com/scriptlike/scriptlike/file/wrappers/thisExePath.html) 63 | (or the [Phobos equivalent](http://dlang.org/phobos/std_file.html#thisExePath)), 64 | then you won't get the expected result. The `thisExePath` function will NOT 65 | return the path to the `myscript.d` script, it will simply return the temporary 66 | path where DUB stored the compiled binary. I'm not aware of any way to work 67 | around this while using DUB's single-file feature, so if your script needs 68 | to obtain its own path (remember, `args[0]` is famously unreliable for this 69 | in any language), then try one of the older approaches below. 70 | 71 | In a DUB-based project 72 | ---------------------- 73 | If your project uses [DUB](http://code.dlang.org/getting_started), 74 | just include the scriptlike as a dependency in your 75 | [dub.json](http://code.dlang.org/package-format?lang=json) or 76 | [dub.sdl](http://code.dlang.org/package-format?lang=sdl) file like this: 77 | 78 | dub.json: 79 | ```json 80 | "dependencies": { 81 | "scriptlike": "~>0.10.2" 82 | } 83 | ``` 84 | 85 | dub.sdl: 86 | ``` 87 | dependency "scriptlike" version="~>0.10.2" 88 | ``` 89 | 90 | And then import with one of these: 91 | 92 | ```d 93 | // Imports all of Scriptlike, plus anything from Phobos likely to 94 | // be useful for scripts: 95 | import scriptlike; 96 | 97 | // Or import only Scriptlike and omit the automatic Phobos imports: 98 | import scriptlike.only; 99 | ``` 100 | 101 | Run your project with dub like normal: 102 | 103 | ```bash 104 | $ dub 105 | ``` 106 | -------------------------------------------------------------------------------- /docs/public/scripts/ddox.js: -------------------------------------------------------------------------------- 1 | function setupDdox() 2 | { 3 | $(".tree-view").children(".package").click(toggleTree); 4 | $(".tree-view.collapsed").children("ul").hide(); 5 | } 6 | 7 | function toggleTree() 8 | { 9 | node = $(this).parent(); 10 | node.toggleClass("collapsed"); 11 | if( node.hasClass("collapsed") ){ 12 | node.children("ul").hide(); 13 | } else { 14 | node.children("ul").show(); 15 | } 16 | return false; 17 | } 18 | 19 | var searchCounter = 0; 20 | var lastSearchString = ""; 21 | 22 | function performSymbolSearch(maxlen) 23 | { 24 | if (maxlen === 'undefined') maxlen = 26; 25 | 26 | var searchstring = $("#symbolSearch").val().toLowerCase(); 27 | 28 | if (searchstring == lastSearchString) return; 29 | lastSearchString = searchstring; 30 | 31 | var scnt = ++searchCounter; 32 | $('#symbolSearchResults').hide(); 33 | $('#symbolSearchResults').empty(); 34 | 35 | var terms = $.trim(searchstring).split(/\s+/); 36 | if (terms.length == 0 || (terms.length == 1 && terms[0].length < 2)) return; 37 | 38 | var results = []; 39 | for (i in symbols) { 40 | var sym = symbols[i]; 41 | var all_match = true; 42 | for (j in terms) 43 | if (sym.name.toLowerCase().indexOf(terms[j]) < 0) { 44 | all_match = false; 45 | break; 46 | } 47 | if (!all_match) continue; 48 | 49 | results.push(sym); 50 | } 51 | 52 | function compare(a, b) { 53 | // prefer non-deprecated matches 54 | var adep = a.attributes.indexOf("deprecated") >= 0; 55 | var bdep = b.attributes.indexOf("deprecated") >= 0; 56 | if (adep != bdep) return adep - bdep; 57 | 58 | // normalize the names 59 | var aname = a.name.toLowerCase(); 60 | var bname = b.name.toLowerCase(); 61 | 62 | var anameparts = aname.split("."); 63 | var bnameparts = bname.split("."); 64 | 65 | var asname = anameparts[anameparts.length-1]; 66 | var bsname = bnameparts[bnameparts.length-1]; 67 | 68 | // prefer exact matches 69 | var aexact = terms.indexOf(asname) >= 0; 70 | var bexact = terms.indexOf(bsname) >= 0; 71 | if (aexact != bexact) return bexact - aexact; 72 | 73 | // prefer elements with less nesting 74 | if (anameparts.length < bnameparts.length) return -1; 75 | if (anameparts.length > bnameparts.length) return 1; 76 | 77 | // prefer matches with a shorter name 78 | if (asname.length < bsname.length) return -1; 79 | if (asname.length > bsname.length) return 1; 80 | 81 | // sort the rest alphabetically 82 | if (aname < bname) return -1; 83 | if (aname > bname) return 1; 84 | return 0; 85 | } 86 | 87 | results.sort(compare); 88 | 89 | for (i = 0; i < results.length && i < 100; i++) { 90 | var sym = results[i]; 91 | 92 | var el = $(document.createElement("li")); 93 | el.addClass(sym.kind); 94 | for (j in sym.attributes) 95 | el.addClass(sym.attributes[j]); 96 | 97 | var name = sym.name; 98 | 99 | // compute a length limited representation of the full name 100 | var nameparts = name.split("."); 101 | var np = nameparts.length-1; 102 | var shortname = "." + nameparts[np]; 103 | while (np > 0 && nameparts[np-1].length + shortname.length <= maxlen) { 104 | np--; 105 | shortname = "." + nameparts[np] + shortname; 106 | } 107 | if (np > 0) shortname = ".." + shortname; 108 | else shortname = shortname.substr(1); 109 | 110 | el.append(''+shortname+''); 111 | $('#symbolSearchResults').append(el); 112 | } 113 | 114 | if (results.length > 100) { 115 | $('#symbolSearchResults').append("
  • …"+(results.length-100)+" additional results
  • "); 116 | } 117 | 118 | $('#symbolSearchResults').show(); 119 | } 120 | -------------------------------------------------------------------------------- /docs/public/scripts/mousetrap.js: -------------------------------------------------------------------------------- 1 | /* mousetrap v1.4.6 craig.is/killing/mice */ 2 | (function(J,r,f){function s(a,b,d){a.addEventListener?a.addEventListener(b,d,!1):a.attachEvent("on"+b,d)}function A(a){if("keypress"==a.type){var b=String.fromCharCode(a.which);a.shiftKey||(b=b.toLowerCase());return b}return h[a.which]?h[a.which]:B[a.which]?B[a.which]:String.fromCharCode(a.which).toLowerCase()}function t(a){a=a||{};var b=!1,d;for(d in n)a[d]?b=!0:n[d]=0;b||(u=!1)}function C(a,b,d,c,e,v){var g,k,f=[],h=d.type;if(!l[a])return[];"keyup"==h&&w(a)&&(b=[a]);for(g=0;gg||h.hasOwnProperty(g)&&(p[h[g]]=g)}e=p[d]?"keydown":"keypress"}"keypress"==e&&f.length&&(e="keydown");return{key:c,modifiers:f,action:e}}function F(a,b,d,c,e){q[a+":"+d]=b;a=a.replace(/\s+/g," ");var f=a.split(" ");1":".","?":"/","|":"\\"},G={option:"alt",command:"meta","return":"enter",escape:"esc",mod:/Mac|iPod|iPhone|iPad/.test(navigator.platform)?"meta":"ctrl"},p,l={},q={},n={},D,z=!1,I=!1,u=!1;for(f=1;20>f;++f)h[111+f]="f"+f;for(f=0;9>=f;++f)h[f+96]=f;s(r,"keypress",y);s(r,"keydown",y);s(r,"keyup",y);var m={bind:function(a,b,d){a=a instanceof Array?a:[a];for(var c=0;c $null; 169 | popd; 170 | echo "finished."; 171 | } 172 | - ps: SetUpDCompiler 173 | - powershell -Command Invoke-WebRequest https://code.dlang.org/files/dub-1.9.0-windows-x86.zip -OutFile dub.zip 174 | - 7z x dub.zip -odub > nul 175 | - set PATH=%CD%\%binpath%;%CD%\dub;%PATH% 176 | - dub --version 177 | # Some older LDC/GDC compilers don't come with rdmd 178 | - ps: function SetUpRDMD 179 | { 180 | if((Get-Command "rdmd.exe" -ErrorAction SilentlyContinue) -eq $null) 181 | { 182 | Invoke-WebRequest "http://downloads.dlang.org/releases/2.x/2.080.0/dmd.2.080.0.windows.7z" -OutFile "c:\rdmd-dmd.7z"; 183 | pushd c:\\; 184 | 7z x -ordmd-dmd rdmd-dmd.7z > $null; 185 | popd; 186 | } 187 | } 188 | - ps: SetUpRDMD 189 | - set PATH=%PATH%;c:\rdmd-dmd\dmd2\windows\bin 190 | 191 | before_build: 192 | - ps: if($env:arch -eq "x86"){ 193 | $env:compilersetupargs = "x86"; 194 | $env:Darch = "x86"; 195 | $env:DConf = "m32"; 196 | }elseif($env:arch -eq "x64"){ 197 | $env:compilersetupargs = "amd64"; 198 | $env:Darch = "x86_64"; 199 | $env:DConf = "m64"; 200 | } 201 | - ps: $env:compilersetup = "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall"; 202 | - '"%compilersetup%" %compilersetupargs%' 203 | 204 | build_script: 205 | - echo dummy build script - dont remove me 206 | 207 | test_script: 208 | - echo %PLATFORM% 209 | - echo %Darch% 210 | - echo %DC% 211 | - echo %DMD% 212 | - echo %PATH% 213 | - '%DC% --help' 214 | - dub add-local . 215 | - dub test --arch=%Darch% --compiler=%DC% 216 | -------------------------------------------------------------------------------- /src/scriptlike/path/extras.d: -------------------------------------------------------------------------------- 1 | /++ 2 | $(H2 Scriptlike $(SCRIPTLIKE_VERSION)) 3 | 4 | Extra Scriptlike-only functionality to complement $(MODULE_STD_PATH). 5 | 6 | Copyright: Copyright (C) 2014-2017 Nick Sabalausky 7 | License: zlib/libpng 8 | Authors: Nick Sabalausky 9 | +/ 10 | module scriptlike.path.extras; 11 | 12 | import std.algorithm; 13 | import std.conv; 14 | import std.datetime; 15 | import std.file; 16 | import std.process; 17 | import std.range; 18 | import std.stdio; 19 | import std.string; 20 | import std.traits; 21 | import std.typecons; 22 | import std.typetuple; 23 | 24 | static import std.path; 25 | import std.path : dirSeparator, pathSeparator, isDirSeparator, 26 | CaseSensitive, buildPath, buildNormalizedPath; 27 | 28 | import scriptlike.path.wrappers; 29 | 30 | /// Represents a file extension. 31 | struct Ext 32 | { 33 | private string str; 34 | 35 | /// Main constructor. 36 | this(string extension) pure @safe nothrow 37 | { 38 | this.str = extension; 39 | } 40 | 41 | /// Convert to string. 42 | string toString() pure @safe nothrow 43 | { 44 | return str; 45 | } 46 | 47 | /// No longer needed. Use Ext.toString() instead. 48 | deprecated("Use Ext.toString() instead.") 49 | string toRawString() pure @safe nothrow 50 | { 51 | return str; 52 | } 53 | 54 | /// Compare using OS-specific case-sensitivity rules. If you want to force 55 | /// case-sensitive or case-insensitive, then call filenameCmp instead. 56 | int opCmp(ref const Ext other) const 57 | { 58 | return std.path.filenameCmp(this.str, other.str); 59 | } 60 | 61 | ///ditto 62 | int opCmp(Ext other) const 63 | { 64 | return std.path.filenameCmp(this.str, other.str); 65 | } 66 | 67 | ///ditto 68 | int opCmp(string other) const 69 | { 70 | return std.path.filenameCmp(this.str, other); 71 | } 72 | 73 | /// Compare using OS-specific case-sensitivity rules. If you want to force 74 | /// case-sensitive or case-insensitive, then call filenameCmp instead. 75 | int opEquals(ref const Ext other) const 76 | { 77 | return opCmp(other) == 0; 78 | } 79 | 80 | ///ditto 81 | int opEquals(Ext other) const 82 | { 83 | return opCmp(other) == 0; 84 | } 85 | 86 | ///ditto 87 | int opEquals(string other) const 88 | { 89 | return opCmp(other) == 0; 90 | } 91 | 92 | /// Convert to bool 93 | T opCast(T)() if(is(T==bool)) 94 | { 95 | return !!str; 96 | } 97 | } 98 | 99 | /// Represents a filesystem path. The path is always kept normalized 100 | /// automatically (as performed by buildNormalizedPathFixed). 101 | struct Path 102 | { 103 | private string str = "."; 104 | 105 | /// Main constructor. 106 | this(string path) pure @safe nothrow 107 | { 108 | this.str = buildNormalizedPathFixed(path); 109 | } 110 | 111 | pure @trusted nothrow invariant() 112 | { 113 | assert(str == buildNormalizedPathFixed(str)); 114 | } 115 | 116 | /// Convert to string, quoting or escaping spaces if necessary. 117 | string toString() 118 | { 119 | return .escapeShellArg(str); 120 | } 121 | 122 | /// Returns the underlying string. Does NOT do any escaping, even if path contains spaces. 123 | string raw() const pure @safe nothrow 124 | { 125 | return str; 126 | } 127 | 128 | ///ditto 129 | deprecated("Use Path.raw instead.") 130 | alias toRawString = raw; 131 | 132 | /// Concatenates two paths, with a directory separator in between. 133 | Path opBinary(string op)(Path rhs) if(op=="~") 134 | { 135 | Path newPath; 136 | newPath.str = buildNormalizedPathFixed(this.str, rhs.str); 137 | return newPath; 138 | } 139 | 140 | ///ditto 141 | Path opBinary(string op)(string rhs) if(op=="~") 142 | { 143 | Path newPath; 144 | newPath.str = buildNormalizedPathFixed(this.str, rhs); 145 | return newPath; 146 | } 147 | 148 | ///ditto 149 | Path opBinaryRight(string op)(string lhs) if(op=="~") 150 | { 151 | Path newPath; 152 | newPath.str = buildNormalizedPathFixed(lhs, this.str); 153 | return newPath; 154 | } 155 | 156 | /// Appends an extension to a path. Naturally, a directory separator 157 | /// is NOT inserted in between. 158 | Path opBinary(string op)(Ext rhs) if(op=="~") 159 | { 160 | Path newPath; 161 | newPath.str = std.path.setExtension(this.str, rhs.str); 162 | return newPath; 163 | } 164 | 165 | /// Appends a path to this one, with a directory separator in between. 166 | Path opOpAssign(string op)(Path rhs) if(op=="~") 167 | { 168 | str = buildNormalizedPathFixed(str, rhs.str); 169 | return this; 170 | } 171 | 172 | ///ditto 173 | Path opOpAssign(string op)(string rhs) if(op=="~") 174 | { 175 | str = buildNormalizedPathFixed(str, rhs); 176 | return this; 177 | } 178 | 179 | /// Appends an extension to this path. Naturally, a directory separator 180 | /// is NOT inserted in between. 181 | Path opOpAssign(string op)(Ext rhs) if(op=="~") 182 | { 183 | str = std.path.setExtension(str, rhs.str); 184 | return this; 185 | } 186 | 187 | /// Compare using OS-specific case-sensitivity rules. If you want to force 188 | /// case-sensitive or case-insensitive, then call filenameCmp instead. 189 | int opCmp(ref const Path other) const 190 | { 191 | return std.path.filenameCmp(this.str, other.str); 192 | } 193 | 194 | ///ditto 195 | int opCmp(Path other) const 196 | { 197 | return std.path.filenameCmp(this.str, other.str); 198 | } 199 | 200 | ///ditto 201 | int opCmp(string other) const 202 | { 203 | return std.path.filenameCmp(this.str, other); 204 | } 205 | 206 | /// Compare using OS-specific case-sensitivity rules. If you want to force 207 | /// case-sensitive or case-insensitive, then call filenameCmp instead. 208 | int opEquals(ref const Path other) const 209 | { 210 | return opCmp(other) == 0; 211 | } 212 | 213 | ///ditto 214 | int opEquals(Path other) const 215 | { 216 | return opCmp(other) == 0; 217 | } 218 | 219 | ///ditto 220 | int opEquals(string other) const 221 | { 222 | return opCmp(other) == 0; 223 | } 224 | 225 | /// Convert to bool 226 | T opCast(T)() if(is(T==bool)) 227 | { 228 | return !!str; 229 | } 230 | 231 | /// Returns the parent path, according to $(FULL_STD_PATH dirName). 232 | @property Path up() 233 | { 234 | return this.dirName(); 235 | } 236 | 237 | /// Is this path equal to empty string? 238 | @property bool empty() 239 | { 240 | return str == ""; 241 | } 242 | } 243 | 244 | /// Convenience alias 245 | alias extOf = extension; 246 | alias stripExt = stripExtension; ///ditto 247 | alias setExt = setExtension; ///ditto 248 | alias defaultExt = defaultExtension; ///ditto 249 | 250 | /// Like buildNormalizedPath, but if the result is the current directory, 251 | /// this returns "." instead of "". However, if all the inputs are "", or there 252 | /// are no inputs, this still returns "" just like buildNormalizedPath. 253 | /// 254 | /// Also, unlike buildNormalizedPath, this converts back/forward slashes to 255 | /// native on BOTH Windows and Posix, not just on Windows. 256 | string buildNormalizedPathFixed(string[] paths...) 257 | @trusted pure nothrow 258 | { 259 | if(all!`a is null`(paths)) 260 | return null; 261 | 262 | if(all!`a==""`(paths)) 263 | return ""; 264 | 265 | auto result = std.path.buildNormalizedPath(paths); 266 | 267 | version(Posix) result = result.replace(`\`, `/`); 268 | else version(Windows) { /+ do nothing +/ } 269 | else static assert(0); 270 | 271 | return result==""? "." : result; 272 | } 273 | 274 | /// Properly escape arguments containing spaces for the command shell, if necessary. 275 | /// 276 | /// Although Path doesn't strictly need this (since Path.toString automatically 277 | /// calls this anyway), an overload of escapeShellArg which accepts a Path is 278 | /// provided for the sake of generic code. 279 | const(string) escapeShellArg(in string str) 280 | { 281 | if(str.canFind(' ')) 282 | { 283 | version(Windows) 284 | return escapeWindowsArgument(str); 285 | else version(Posix) 286 | return escapeShellFileName(str); 287 | else 288 | static assert(0, "This platform not supported."); 289 | } 290 | else 291 | return str; 292 | } 293 | 294 | ///ditto 295 | string escapeShellArg(Path path) 296 | { 297 | return path.toString(); 298 | } 299 | 300 | -------------------------------------------------------------------------------- /src/scriptlike/path/package.d: -------------------------------------------------------------------------------- 1 | /++ 2 | $(H2 Scriptlike $(SCRIPTLIKE_VERSION)) 3 | 4 | Extra Scriptlike-only functionality to complement and wrap $(MODULE_STD_PATH), 5 | providing extra functionality, such as no-fail "try*" alternatives, and support 6 | for Scriptlike's $(API_PATH_EXTR Path), command echoing and dry-run features. 7 | 8 | Modules: 9 | $(UL 10 | $(LI $(MODULE_PATH_EXTR) ) 11 | $(LI $(MODULE_PATH_WRAP) ) 12 | ) 13 | 14 | Copyright: Copyright (C) 2014-2017 Nick Sabalausky 15 | License: zlib/libpng 16 | Authors: Nick Sabalausky 17 | +/ 18 | module scriptlike.path; 19 | 20 | public import scriptlike.path.extras; 21 | public import scriptlike.path.wrappers; 22 | 23 | // The unittests in this module mainly check that all the templates compile 24 | // correctly and that the appropriate Phobos functions are correctly called. 25 | // 26 | // A completely thorough testing of the behavior of such functions is 27 | // occasionally left to Phobos itself as it is outside the scope of these tests. 28 | version(unittest_scriptlike_d) 29 | unittest 30 | { 31 | import std.algorithm; 32 | import std.conv; 33 | import std.datetime; 34 | import std.file; 35 | import std.path : dirSeparator; 36 | import std.process; 37 | import std.range; 38 | import std.stdio; 39 | import std.string; 40 | import std.traits; 41 | import std.typecons; 42 | import std.typetuple; 43 | 44 | import std.stdio : writeln; 45 | writeln("Running Scriptlike unittests: std.path wrappers"); 46 | 47 | alias dirSep = dirSeparator; 48 | 49 | { 50 | auto e = Ext(".txt"); 51 | assert(e != Ext(".dat")); 52 | assert(e == Ext(".txt")); 53 | version(Windows) 54 | assert(e == Ext(".TXT")); 55 | else version(OSX) 56 | assert(e == Ext(".TXT")); 57 | else version(Posix) 58 | assert(e != Ext(".TXT")); 59 | else 60 | static assert(0, "This platform not supported."); 61 | 62 | // Test the other comparison overloads 63 | assert(e != Ext(".dat")); 64 | assert(e == Ext(".txt")); 65 | assert(Ext(".dat") != e); 66 | assert(Ext(".txt") == e); 67 | assert(".dat" != e); 68 | assert(".txt" == e); 69 | 70 | assert(Ext("foo")); 71 | assert(Ext("")); 72 | assert(Ext(null).toString() is null); 73 | assert(!Ext(null)); 74 | } 75 | 76 | auto p = Path(); 77 | assert(p.raw == "."); 78 | assert(!p.empty); 79 | 80 | assert(Path("").empty); 81 | 82 | assert(Path("foo")); 83 | assert(Path("")); 84 | assert(Path(null).raw is null); 85 | assert(!Path(null)); 86 | 87 | version(Windows) 88 | auto testStrings = ["/foo/bar", "/foo/bar/", `\foo\bar`, `\foo\bar\`]; 89 | else version(Posix) 90 | auto testStrings = ["/foo/bar", "/foo/bar/"]; 91 | else 92 | static assert(0, "This platform not supported."); 93 | 94 | foreach(str; testStrings) 95 | { 96 | writeln(" testing str: ", str); 97 | 98 | p = Path(str); 99 | assert(!p.empty); 100 | assert(p.raw == dirSep~"foo"~dirSep~"bar"); 101 | 102 | p = Path(str); 103 | assert(p.raw == dirSep~"foo"~dirSep~"bar"); 104 | assert(p.raw == p.raw); 105 | assert(p.toString() == p.raw.to!string()); 106 | 107 | assert(p.up.toString() == dirSep~"foo"); 108 | assert(p.up.up.toString() == dirSep); 109 | 110 | assert((p~"sub").toString() == dirSep~"foo"~dirSep~"bar"~dirSep~"sub"); 111 | assert((p~"sub"~"2").toString() == dirSep~"foo"~dirSep~"bar"~dirSep~"sub"~dirSep~"2"); 112 | assert((p~Path("sub")).toString() == dirSep~"foo"~dirSep~"bar"~dirSep~"sub"); 113 | 114 | version(Windows) 115 | assert((p~"sub dir").toString() == `"`~dirSep~"foo"~dirSep~"bar"~dirSep~"sub dir"~`"`); 116 | else version(Posix) 117 | assert((p~"sub dir").toString() == `'`~dirSep~"foo"~dirSep~"bar"~dirSep~`sub dir'`); 118 | else 119 | static assert(0, "This platform not supported."); 120 | 121 | assert(("dir"~p).toString() == dirSep~"foo"~dirSep~"bar"); 122 | assert(("dir"~Path(str[1..$])).toString() == "dir"~dirSep~"foo"~dirSep~"bar"); 123 | 124 | p ~= "blah"; 125 | assert(p.toString() == dirSep~"foo"~dirSep~"bar"~dirSep~"blah"); 126 | 127 | p ~= Path("more"); 128 | assert(p.toString() == dirSep~"foo"~dirSep~"bar"~dirSep~"blah"~dirSep~"more"); 129 | 130 | p ~= ".."; 131 | assert(p.toString() == dirSep~"foo"~dirSep~"bar"~dirSep~"blah"); 132 | 133 | p ~= Path(".."); 134 | assert(p.toString() == dirSep~"foo"~dirSep~"bar"); 135 | 136 | p ~= "sub dir"; 137 | p ~= ".."; 138 | assert(p.toString() == dirSep~"foo"~dirSep~"bar"); 139 | 140 | p ~= "filename"; 141 | assert((p~Ext(".txt")).toString() == dirSep~"foo"~dirSep~"bar"~dirSep~"filename.txt"); 142 | assert((p~Ext("txt")).toString() == dirSep~"foo"~dirSep~"bar"~dirSep~"filename.txt"); 143 | assert((p~Ext("")).toString() == dirSep~"foo"~dirSep~"bar"~dirSep~"filename"); 144 | 145 | p ~= Ext(".ext"); 146 | assert(p.toString() == dirSep~"foo"~dirSep~"bar"~dirSep~"filename.ext"); 147 | assert(p.baseName().toString() == "filename.ext"); 148 | assert(p.dirName().toString() == dirSep~"foo"~dirSep~"bar"); 149 | assert(p.rootName().toString() == dirSep); 150 | assert(p.driveName().toString() == ""); 151 | assert(p.stripDrive().toString() == dirSep~"foo"~dirSep~"bar"~dirSep~"filename.ext"); 152 | version(Windows) 153 | { 154 | assert(( Path("C:"~p.raw) ).toString() == "C:"~dirSep~"foo"~dirSep~"bar"~dirSep~"filename.ext"); 155 | assert(( Path("C:"~p.raw) ).stripDrive().toString() == dirSep~"foo"~dirSep~"bar"~dirSep~"filename.ext"); 156 | } 157 | assert(p.extension().toString() == ".ext"); 158 | assert(p.stripExtension().toString() == dirSep~"foo"~dirSep~"bar"~dirSep~"filename"); 159 | assert(p.setExtension(".txt").toString() == dirSep~"foo"~dirSep~"bar"~dirSep~"filename.txt"); 160 | assert(p.setExtension("txt").toString() == dirSep~"foo"~dirSep~"bar"~dirSep~"filename.txt"); 161 | assert(p.setExtension("").toString() == dirSep~"foo"~dirSep~"bar"~dirSep~"filename"); 162 | assert(p.setExtension(Ext(".txt")).toString() == dirSep~"foo"~dirSep~"bar"~dirSep~"filename.txt"); 163 | assert(p.setExtension(Ext("txt")).toString() == dirSep~"foo"~dirSep~"bar"~dirSep~"filename.txt"); 164 | assert(p.setExtension(Ext("")).toString() == dirSep~"foo"~dirSep~"bar"~dirSep~"filename"); 165 | 166 | assert(p.defaultExtension(".dat").toString() == dirSep~"foo"~dirSep~"bar"~dirSep~"filename.ext"); 167 | assert(p.stripExtension().defaultExtension(".dat").toString() == dirSep~"foo"~dirSep~"bar"~dirSep~"filename.dat"); 168 | 169 | assert(equal(p.pathSplitter(), [dirSep, "foo", "bar", "filename.ext"])); 170 | 171 | assert(p.isRooted()); 172 | version(Windows) 173 | assert(!p.isAbsolute()); 174 | else version(Posix) 175 | assert(p.isAbsolute()); 176 | else 177 | static assert(0, "This platform not supported."); 178 | 179 | assert(!( Path("dir"~p.raw) ).isRooted()); 180 | assert(!( Path("dir"~p.raw) ).isAbsolute()); 181 | 182 | version(Windows) 183 | { 184 | assert(( Path("dir"~p.raw) ).absolutePath("C:/main").toString() == "C:"~dirSep~"main"~dirSep~"dir"~dirSep~"foo"~dirSep~"bar"~dirSep~"filename.ext"); 185 | assert(( Path("C:"~p.raw) ).relativePath("C:/foo").toString() == "bar"~dirSep~"filename.ext"); 186 | assert(( Path("C:"~p.raw) ).relativePath("C:/foo/bar").toString() == "filename.ext"); 187 | } 188 | else version(Posix) 189 | { 190 | assert(( Path("dir"~p.raw) ).absolutePath("/main").toString() == dirSep~"main"~dirSep~"dir"~dirSep~"foo"~dirSep~"bar"~dirSep~"filename.ext"); 191 | assert(p.relativePath("/foo").toString() == "bar"~dirSep~"filename.ext"); 192 | assert(p.relativePath("/foo/bar").toString() == "filename.ext"); 193 | } 194 | else 195 | static assert(0, "This platform not supported."); 196 | 197 | assert(p.filenameCmp(dirSep~"foo"~dirSep~"bar"~dirSep~"filename.ext") == 0); 198 | assert(p.filenameCmp(dirSep~"faa"~dirSep~"bat"~dirSep~"filename.ext") != 0); 199 | assert(p.globMatch("*foo*name.ext")); 200 | assert(!p.globMatch("*foo*Bname.ext")); 201 | 202 | assert(!p.isValidFilename()); 203 | assert(p.baseName().isValidFilename()); 204 | assert(p.isValidPath()); 205 | 206 | assert(p.expandTilde().toString() == dirSep~"foo"~dirSep~"bar"~dirSep~"filename.ext"); 207 | 208 | assert(p != Path("/dir/subdir/filename.ext")); 209 | assert(p == Path("/foo/bar/filename.ext")); 210 | version(Windows) 211 | assert(p == Path("/FOO/BAR/FILENAME.EXT")); 212 | else version(OSX) 213 | assert(p == Path("/FOO/BAR/FILENAME.EXT")); 214 | else version(Posix) 215 | assert(p != Path("/FOO/BAR/FILENAME.EXT")); 216 | else 217 | static assert(0, "This platform not supported."); 218 | 219 | // Test the other comparison overloads 220 | assert(p != Path("/dir/subdir/filename.ext")); 221 | assert(p == Path("/foo/bar/filename.ext")); 222 | assert(Path("/dir/subdir/filename.ext") != p); 223 | assert(Path("/foo/bar/filename.ext") == p); 224 | assert("/dir/subdir/filename.ext" != p); 225 | assert("/foo/bar/filename.ext" == p); 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /src/scriptlike/file/package.d: -------------------------------------------------------------------------------- 1 | /++ 2 | $(H2 Scriptlike $(SCRIPTLIKE_VERSION)) 3 | 4 | Extra Scriptlike-only functionality to complement and wrap $(MODULE_STD_FILE), 5 | providing extra functionality, such as no-fail "try*" alternatives, and support 6 | for Scriptlike's $(API_PATH_EXTR Path), command echoing and dry-run features. 7 | 8 | Modules: 9 | $(UL 10 | $(LI $(MODULE_FILE_EXTR) ) 11 | $(LI $(MODULE_FILE_WRAP) ) 12 | ) 13 | 14 | Copyright: Copyright (C) 2014-2017 Nick Sabalausky 15 | License: zlib/libpng 16 | Authors: Nick Sabalausky 17 | +/ 18 | module scriptlike.file; 19 | 20 | public import scriptlike.file.extras; 21 | public import scriptlike.file.wrappers; 22 | 23 | version(unittest_scriptlike_d) 24 | unittest 25 | { 26 | import std.algorithm : equal; 27 | import std.conv; 28 | import std.datetime : SysTime; 29 | static import std.file; 30 | static import std.path; 31 | import std.string; 32 | import std.traits; 33 | import std.typecons; 34 | 35 | import scriptlike.path; 36 | import scriptlike.core : tmpName; 37 | 38 | import std.stdio : writeln; 39 | import std.process : thisProcessID; 40 | alias copy = scriptlike.file.wrappers.copy; 41 | 42 | writeln("Running Scriptlike unittests: std.file wrappers"); 43 | 44 | immutable tempname1 = tmpName("1"); 45 | immutable tempname2 = tmpName("2"); 46 | immutable tempname3 = tmpName("3", "somefile"); 47 | auto tempPath = Path(tempname1); 48 | auto tempPath2 = Path(tempname2); 49 | auto tempPath3 = Path(tempname3); 50 | 51 | void testA(T)(T tempPath) 52 | { 53 | scope(exit) 54 | { 55 | if(std.file.exists(tempname1)) std.file.remove(tempname1); 56 | } 57 | 58 | tempPath.write("stuff"); 59 | 60 | tempPath.append(" more"); 61 | assert(tempPath.read(3) == "stu"); 62 | assert(tempPath.read() == "stuff more"); 63 | assert(tempPath.readText() == "stuff more"); 64 | assert(tempPath.getSize() == 10); 65 | 66 | auto parsed = tempPath.slurp!(string, string)("%s %s"); 67 | assert(equal(parsed, [tuple("stuff", "more")])); 68 | 69 | SysTime timeA, timeB, timeC; 70 | tempPath.getTimes(timeA, timeB); 71 | version(Windows) 72 | tempPath.getTimesWin(timeA, timeB, timeC); 73 | tempPath.setTimes(timeA, timeB); 74 | timeA = tempPath.timeLastModified(); 75 | timeA = tempPath.timeLastModified(timeB); 76 | 77 | uint attr; 78 | attr = tempPath.getAttributes(); 79 | attr = tempPath.getLinkAttributes(); 80 | 81 | assert(tempPath.exists()); 82 | assert(tempPath.isFile()); 83 | assert(tempPath.existsAsFile()); 84 | assert(!tempPath.isDir()); 85 | assert(!tempPath.existsAsDir()); 86 | assert(!tempPath.isSymlink()); 87 | assert(!tempPath.existsAsSymlink()); 88 | tempPath.remove(); 89 | assert(!tempPath.exists()); 90 | assert(!tempPath.existsAsFile()); 91 | assert(!tempPath.existsAsDir()); 92 | assert(!tempPath.existsAsSymlink()); 93 | } 94 | 95 | import std.stdio : stdout; 96 | writeln(" testA with string"); stdout.flush(); 97 | testA(tempPath.raw); // Test with string 98 | 99 | writeln(" testA with Path"); stdout.flush(); 100 | testA(tempPath); // Test with Path 101 | 102 | writeln(" more..."); stdout.flush(); 103 | { 104 | assert(!tempPath.exists()); 105 | assert(!tempPath2.exists()); 106 | 107 | scope(exit) 108 | { 109 | if(std.file.exists(tempname1)) std.file.remove(tempname1); 110 | if(std.file.exists(tempname2)) std.file.remove(tempname2); 111 | } 112 | tempPath.write("ABC"); 113 | 114 | assert(tempPath.existsAsFile()); 115 | assert(!tempPath2.exists()); 116 | 117 | tempPath.rename(tempPath2); 118 | 119 | assert(!tempPath.exists()); 120 | assert(tempPath2.existsAsFile()); 121 | 122 | tempPath2.copy(tempPath); 123 | 124 | assert(tempPath.existsAsFile()); 125 | assert(tempPath2.existsAsFile()); 126 | } 127 | 128 | { 129 | scope(exit) 130 | { 131 | if(std.file.exists(tempname1)) std.file.rmdir(tempname1); 132 | if(std.file.exists(tempname3)) std.file.rmdir(tempname3); 133 | if(std.file.exists( std.path.dirName(tempname3) )) std.file.rmdir( std.path.dirName(tempname3) ); 134 | } 135 | 136 | assert(!tempPath.exists()); 137 | assert(!tempPath3.exists()); 138 | 139 | tempPath.mkdir(); 140 | assert(tempPath.exists()); 141 | assert(!tempPath.isFile()); 142 | assert(!tempPath.existsAsFile()); 143 | assert(tempPath.isDir()); 144 | assert(tempPath.existsAsDir()); 145 | assert(!tempPath.isSymlink()); 146 | assert(!tempPath.existsAsSymlink()); 147 | 148 | tempPath3.mkdirRecurse(); 149 | assert(tempPath3.exists()); 150 | assert(!tempPath3.isFile()); 151 | assert(!tempPath3.existsAsFile()); 152 | assert(tempPath3.isDir()); 153 | assert(tempPath3.existsAsDir()); 154 | assert(!tempPath3.isSymlink()); 155 | assert(!tempPath3.existsAsSymlink()); 156 | 157 | auto saveDirName = std.file.getcwd(); 158 | auto saveDir = Path(saveDirName); 159 | scope(exit) chdir(saveDirName); 160 | 161 | tempPath.chdir(); 162 | assert(getcwd() == tempname1); 163 | saveDir.chdir(); 164 | assert(getcwd() == saveDirName); 165 | 166 | auto entries1 = (tempPath3~"..").dirEntries(SpanMode.shallow); 167 | assert(!entries1.empty); 168 | auto entries2 = (tempPath3~"..").dirEntries("*", SpanMode.shallow); 169 | assert(!entries2.empty); 170 | auto entries3 = (tempPath3~"..").dirEntries("TUNA TUNA THIS DOES NOT EXIST TUNA WHEE", SpanMode.shallow); 171 | assert(entries3.empty); 172 | 173 | tempPath.rmdir(); 174 | assert(!tempPath.exists()); 175 | assert(!tempPath.existsAsFile()); 176 | assert(!tempPath.existsAsDir()); 177 | assert(!tempPath.existsAsSymlink()); 178 | 179 | tempPath3.rmdirRecurse(); 180 | assert(!tempPath.exists()); 181 | assert(!tempPath.existsAsFile()); 182 | assert(!tempPath.existsAsDir()); 183 | assert(!tempPath.existsAsSymlink()); 184 | } 185 | 186 | { 187 | version(Posix) 188 | { 189 | assert(!tempPath.exists()); 190 | assert(!tempPath2.exists()); 191 | 192 | scope(exit) 193 | { 194 | if(std.file.exists(tempname2)) std.file.remove(tempname2); 195 | if(std.file.exists(tempname1)) std.file.remove(tempname1); 196 | } 197 | tempPath.write("DEF"); 198 | 199 | tempPath.symlink(tempPath2); 200 | assert(tempPath2.exists()); 201 | assert(tempPath2.isFile()); 202 | assert(tempPath2.existsAsFile()); 203 | assert(!tempPath2.isDir()); 204 | assert(!tempPath2.existsAsDir()); 205 | assert(tempPath2.isSymlink()); 206 | assert(tempPath2.existsAsSymlink()); 207 | 208 | auto linkTarget = tempPath2.readLink(); 209 | assert(linkTarget.raw == tempname1); 210 | } 211 | } 212 | 213 | { 214 | assert(!tempPath.exists()); 215 | 216 | scope(exit) 217 | { 218 | if(std.file.exists(tempname1)) std.file.remove(tempname1); 219 | } 220 | 221 | import scriptlike.process; 222 | run(`echo TestScriptStuff > `~tempPath.to!string()); 223 | assert(tempPath.exists()); 224 | assert(tempPath.isFile()); 225 | assert((cast(string)tempPath.read()).strip() == "TestScriptStuff"); 226 | tempPath.remove(); 227 | assert(!tempPath.exists()); 228 | 229 | auto errlevel = tryRun(`echo TestScriptStuff > `~tempPath.to!string()); 230 | assert(tempPath.exists()); 231 | assert(tempPath.isFile()); 232 | assert((cast(string)tempPath.read()).strip() == "TestScriptStuff"); 233 | assert(errlevel == 0); 234 | tempPath.remove(); 235 | assert(!tempPath.exists()); 236 | 237 | import scriptlike.process; 238 | getcwd().run(`echo TestScriptStuff > `~tempPath.to!string()); 239 | getcwd().tryRun(`echo TestScriptStuff > `~tempPath.to!string()); 240 | } 241 | 242 | { 243 | assert(!tempPath3.exists()); 244 | assert(!tempPath3.up.exists()); 245 | 246 | scope(exit) 247 | { 248 | if(std.file.exists(tempname3)) std.file.remove(tempname3); 249 | if(std.file.exists( std.path.dirName(tempname3) )) std.file.rmdir( std.path.dirName(tempname3) ); 250 | } 251 | 252 | tempPath3.up.mkdir(); 253 | assert(tempPath3.up.exists()); 254 | assert(tempPath3.up.isDir()); 255 | 256 | import scriptlike.process; 257 | tempPath3.up.run(`echo MoreTestStuff > `~tempPath3.baseName().to!string()); 258 | assert(tempPath3.exists()); 259 | assert(tempPath3.isFile()); 260 | assert((cast(string)tempPath3.read()).strip() == "MoreTestStuff"); 261 | } 262 | 263 | { 264 | scope(exit) 265 | { 266 | if(std.file.exists(tempname1)) std.file.rmdir(tempname1); 267 | if(std.file.exists(tempname3)) std.file.rmdir(tempname3); 268 | if(std.file.exists( std.path.dirName(tempname3) )) std.file.rmdir( std.path.dirName(tempname3) ); 269 | } 270 | 271 | assert(!tempPath.exists()); 272 | assert(!tempPath3.exists()); 273 | 274 | assert(!tempPath.tryRmdir()); 275 | assert(!tempPath.tryRmdirRecurse()); 276 | assert(!tempPath.tryRemove()); 277 | assert(!tempPath.tryRename(tempPath3)); 278 | version(Posix) assert(!tempPath.trySymlink(tempPath3)); 279 | assert(!tempPath.tryCopy(tempPath3)); 280 | 281 | assert(tempPath.tryMkdir()); 282 | assert(tempPath.exists()); 283 | assert(!tempPath.tryMkdir()); 284 | assert(!tempPath.tryMkdirRecurse()); 285 | 286 | assert(tempPath.tryRmdir()); 287 | assert(!tempPath.exists()); 288 | 289 | assert(tempPath.tryMkdirRecurse()); 290 | assert(tempPath.exists()); 291 | assert(!tempPath.tryMkdirRecurse()); 292 | } 293 | } 294 | -------------------------------------------------------------------------------- /src/scriptlike/interact.d: -------------------------------------------------------------------------------- 1 | /** 2 | * Handling of interaction with users via standard input. 3 | * 4 | * Provides functions for simple and common interactions with users in 5 | * the form of question and answer. 6 | * 7 | * Copyright: Copyright Jesse Phillips 2010 8 | * License: $(LINK2 https://github.com/Abscissa/scriptlike/blob/master/LICENSE.txt, zlib/libpng) 9 | * Authors: Jesse Phillips 10 | * 11 | * Synopsis: 12 | * 13 | * -------- 14 | * import scriptlike.interact; 15 | * 16 | * auto age = userInput!int("Please Enter your age"); 17 | * 18 | * if(userInput!bool("Do you want to continue?")) 19 | * { 20 | * auto outputFolder = pathLocation("Where you do want to place the output?"); 21 | * auto color = menu!string("What color would you like to use?", ["Blue", "Green"]); 22 | * } 23 | * 24 | * auto num = require!(int, "a > 0 && a <= 10")("Enter a number from 1 to 10"); 25 | * -------- 26 | */ 27 | module scriptlike.interact; 28 | 29 | import std.conv; 30 | import std.file; 31 | import std.functional; 32 | import std.range; 33 | import std.stdio; 34 | import std.string; 35 | import std.traits; 36 | 37 | /** 38 | * The $(D userInput) function provides a means to accessing a single 39 | * value from the user. Each invocation outputs a provided 40 | * statement/question and takes an entire line of input. The result is then 41 | * converted to the requested type; default is a string. 42 | * 43 | * -------- 44 | * auto name = userInput("What is your name"); 45 | * //or 46 | * string name; 47 | * userInput("What is your name", name); 48 | * -------- 49 | * 50 | * Returns: User response as type T. 51 | * 52 | * Where type is bool: 53 | * 54 | * true on "ok", "continue", 55 | * and if the response starts with 'y' or 'Y'. 56 | * 57 | * false on all other input, include no response (will not throw). 58 | * 59 | * Throws: $(D NoInputException) if the user does not enter anything. 60 | * $(D ConvError) when the string could not be converted to the desired type. 61 | */ 62 | T userInput(T = string)(string question = "") 63 | { 64 | write(question ~ "\n> "); 65 | stdout.flush; 66 | auto ans = readln(); 67 | 68 | static if(is(T == bool)) 69 | { 70 | switch(ans.front) 71 | { 72 | case 'y', 'Y': 73 | return true; 74 | default: 75 | } 76 | switch(ans.strip()) 77 | { 78 | case "continue": 79 | case "ok": 80 | return true; 81 | default: 82 | return false; 83 | } 84 | } else 85 | { 86 | if(ans == "\x0a") 87 | throw new NoInputException("Value required, "~ 88 | "cannot continue operation."); 89 | static if(isSomeChar!T) 90 | { 91 | return to!(T)(ans[0]); 92 | } else 93 | return to!(T)(ans.strip()); 94 | } 95 | } 96 | 97 | ///ditto 98 | void userInput(T = string)(string question, ref T result) 99 | { 100 | result = userInput!T(question); 101 | } 102 | 103 | version(unittest_scriptlike_d) 104 | unittest 105 | { 106 | mixin(selfCom(["10PM", "9PM"])); 107 | mixin(selfCom()); 108 | auto s = userInput("What time is it?"); 109 | assert(s == "10PM", "Expected 10PM got" ~ s); 110 | outfile.rewind; 111 | assert(outfile.readln().strip == "What time is it?"); 112 | 113 | outfile.rewind; 114 | userInput("What time?", s); 115 | assert(s == "9PM", "Expected 9PM got" ~ s); 116 | outfile.rewind; 117 | assert(outfile.readln().strip == "What time?"); 118 | } 119 | 120 | /** 121 | * Pauses and prompts the user to press Enter (or "Return" on OSX). 122 | * 123 | * This is similar to the Windows command line's PAUSE command. 124 | * 125 | * -------- 126 | * pause(); 127 | * pause("Thanks. Please press Enter again..."); 128 | * -------- 129 | */ 130 | void pause(string prompt = defaultPausePrompt) 131 | { 132 | //TODO: This works, but needs a little work. Currently, it echoes all 133 | // input until Enter is pressed. Fixing that requires some low-level 134 | // os-specific work. 135 | // 136 | // For reference: 137 | // http://stackoverflow.com/questions/6856635/hide-password-input-on-terminal 138 | // http://linux.die.net/man/3/termios 139 | 140 | write(prompt); 141 | stdout.flush(); 142 | getchar(); 143 | } 144 | 145 | version(OSX) 146 | enum defaultPausePrompt = "Press Return to continue..."; /// 147 | else 148 | enum defaultPausePrompt = "Press Enter to continue..."; /// 149 | 150 | 151 | /** 152 | * Gets a valid path folder from the user. The string will not contain 153 | * quotes, if you are using in a system call and the path contain spaces 154 | * wrapping in quotes may be required. 155 | * 156 | * -------- 157 | * auto confFile = pathLocation("Where is the configuration file?"); 158 | * -------- 159 | * 160 | * Throws: NoInputException if the user does not provide a path. 161 | */ 162 | string pathLocation(string action) 163 | { 164 | import std.algorithm; 165 | import std.utf : byChar; 166 | string ans; 167 | 168 | do 169 | { 170 | if(ans !is null) 171 | writeln("Could not locate that file."); 172 | ans = userInput(action); 173 | // Quotations will generally cause problems when 174 | // using the path with std.file and Windows. This removes the quotes. 175 | ans = std.string.strip( ans.byChar.filter!(a => a != '"' && a != ';').array ); 176 | //ans = ans.removechars("\";").strip(); 177 | //ans = ans[0] == '"' ? ans[1..$] : ans; // removechars skips first char 178 | } while(!exists(ans)); 179 | 180 | return ans; 181 | } 182 | 183 | /** 184 | * Creates a menu from a Range of strings. 185 | * 186 | * It will require that a number is selected within the number of options. 187 | * 188 | * If the the return type is a string, the string in the options parameter will 189 | * be returned. 190 | * 191 | * Throws: NoInputException if the user wants to quit. 192 | */ 193 | T menu(T = ElementType!(Range), Range) (string question, Range options) 194 | if((is(T==ElementType!(Range)) || is(T==int)) && 195 | isForwardRange!(Range)) 196 | { 197 | string ans; 198 | int maxI; 199 | int i; 200 | 201 | while(true) 202 | { 203 | writeln(question); 204 | i = 0; 205 | foreach(str; options) 206 | { 207 | writefln("%8s. %s", i+1, str); 208 | i++; 209 | } 210 | maxI = i+1; 211 | 212 | writefln("%8s. %s", "No Input", "Quit"); 213 | ans = userInput!(string)("").strip(); 214 | int ians; 215 | 216 | try 217 | { 218 | ians = to!(int)(ans); 219 | } catch(ConvException ce) 220 | { 221 | bool found; 222 | i = 0; 223 | foreach(o; options) 224 | { 225 | if(ans.toLower() == to!string(o).toLower()) 226 | { 227 | found = true; 228 | ians = i+1; 229 | break; 230 | } 231 | i++; 232 | } 233 | if(!found) 234 | throw ce; 235 | 236 | } 237 | 238 | if(ians > 0 && ians <= maxI) 239 | static if(is(T==ElementType!(Range))) 240 | static if(isRandomAccessRange!(Range)) 241 | return options[ians-1]; 242 | else 243 | { 244 | take!(ians-1)(options); 245 | return options.front; 246 | } 247 | else 248 | return ians; 249 | else 250 | writeln("You did not select a valid entry."); 251 | } 252 | } 253 | 254 | version(unittest_scriptlike_d) 255 | unittest 256 | { 257 | mixin(selfCom(["1","Green", "green","2"])); 258 | mixin(selfCom()); 259 | auto color = menu!string("What color?", ["Blue", "Green"]); 260 | assert(color == "Blue", "Expected Blue got " ~ color); 261 | 262 | auto ic = menu!int("What color?", ["Blue", "Green"]); 263 | assert(ic == 2, "Expected 2 got " ~ ic.to!string); 264 | 265 | color = menu!string("What color?", ["Blue", "Green"]); 266 | assert(color == "Green", "Expected Green got " ~ color); 267 | 268 | color = menu!string("What color?", ["Blue", "Green"]); 269 | assert(color == "Green", "Expected Green got " ~ color); 270 | outfile.rewind; 271 | assert(outfile.readln().strip == "What color?"); 272 | } 273 | 274 | 275 | /** 276 | * Requires that a value be provided and valid based on 277 | * the delegate passed in. It must also check against null input. 278 | * 279 | * -------- 280 | * auto num = require!(int, "a > 0 && a <= 10")("Enter a number from 1 to 10"); 281 | * -------- 282 | * 283 | * Throws: NoInputException if the user does not provide any value. 284 | * ConvError if the user does not provide any value. 285 | */ 286 | T require(T, alias cond)(in string question, in string failure = null) 287 | { 288 | alias unaryFun!(cond) call; 289 | T ans; 290 | while(1) 291 | { 292 | ans = userInput!T(question); 293 | if(call(ans)) 294 | break; 295 | if(failure) 296 | writeln(failure); 297 | } 298 | 299 | return ans; 300 | } 301 | 302 | version(unittest_scriptlike_d) 303 | unittest 304 | { 305 | mixin(selfCom(["1","11","3"])); 306 | mixin(selfCom()); 307 | auto num = require!(int, "a > 0 && a <= 10")("Enter a number from 1 to 10"); 308 | assert(num == 1, "Expected 1 got" ~ num.to!string); 309 | num = require!(int, "a > 0 && a <= 10")("Enter a number from 1 to 10"); 310 | assert(num == 3, "Expected 1 got" ~ num.to!string); 311 | outfile.rewind; 312 | assert(outfile.readln().strip == "Enter a number from 1 to 10"); 313 | } 314 | 315 | 316 | /** 317 | * Used when input was not provided. 318 | */ 319 | class NoInputException: Exception 320 | { 321 | this(string msg) 322 | { 323 | super(msg); 324 | } 325 | } 326 | 327 | version(unittest_scriptlike_d) 328 | private string selfCom() 329 | { 330 | string ans = q{ 331 | auto outfile = File.tmpfile(); 332 | auto origstdout = stdout; 333 | scope(exit) stdout = origstdout; 334 | stdout = outfile;}; 335 | 336 | return ans; 337 | } 338 | 339 | version(unittest_scriptlike_d) 340 | private string selfCom(string[] input) 341 | { 342 | string ans = q{ 343 | auto infile = File.tmpfile(); 344 | auto origstdin = stdin; 345 | scope(exit) stdin = origstdin; 346 | stdin = infile;}; 347 | 348 | foreach(i; input) 349 | ans ~= "infile.writeln(`"~i~"`);"; 350 | ans ~= "infile.rewind;"; 351 | 352 | return ans; 353 | } 354 | -------------------------------------------------------------------------------- /ddoc/changelog.d: -------------------------------------------------------------------------------- 1 | /++ 2 | Scriptlike_Changelog: 3 | 4 | The latest version of this changelog is always available at:$(BR) 5 | $(LINK http://semitwist.com/scriptlike/changelog.html) 6 | 7 | (Dates below are YYYY/MM/DD) 8 | 9 | $(H2 v0.10.3 - 2019/07/15) 10 | 11 | Maintenance release. 12 | 13 | $(UL 14 | $(CHANGE 15 | Fixed deprecations for DMD 2.075+ and 2.079+. Dropped support for LDC 0.15.1. 16 | ) 17 | $(TESTS 18 | Added Windows CI testing via 19 | $(LINK2 https://ci.appveyor.com/project/Abscissa/scriptlike, AppVeyor). 20 | ) 21 | ) 22 | 23 | $(H2 v0.10.2 - 2017/03/03) 24 | 25 | $(UL 26 | $(ENHANCE 27 | Added $(API_CORE trace) functions as debugging aid. Outputs 28 | file/line info and optionally a variable name/value. 29 | ) 30 | $(ENHANCE 31 | Added $(API_FILE_EXTR isUserExec), $(API_FILE_EXTR isGroupExec) 32 | and $(API_FILE_EXTR isWorldExec) to check a file's executable bits on Posix. 33 | ) 34 | $(FIXED 35 | $(ISSUE 34): Unable to build docs of own project with DUB. 36 | ) 37 | $(FIXED 38 | Make sure the example tests, when run in travis-ci, always use 39 | the current scriptlike commit, instead of using a scriptlike release 40 | from the published dub repos. 41 | ) 42 | $(FIXED 43 | Docs weren't being correctly built for $(API_FILE_WRAP symlink), 44 | $(API_FILE_WRAP readLink), $(API_FILE_WRAP getTimesWin) and $(API_FILE_EXTR trySymlink). 45 | ) 46 | $(CHANGE 47 | Removed outdated, messy and problematic "plain script" example. 48 | ) 49 | ) 50 | 51 | $(H2 v0.10.1 - 2017/02/25) 52 | 53 | $(UL 54 | $(FIXED 55 | Fix some minor doc and travis-ci issues with v0.10.0's release. 56 | ) 57 | ) 58 | 59 | $(H2 v0.10.0 - 2017/02/25) 60 | 61 | $(UL 62 | $(CHANGE 63 | $(ISSUE 33): Rename `Path.toRawString` to `Path.raw`. 64 | ) 65 | $(CHANGE 66 | Deprecated `Ext.toRawString`. It didn't do anything 67 | different from `Ext.toString` and thus wasn't needed. 68 | ) 69 | $(FIXED 70 | $(ISSUE 19): Compile error with DMDFE 2.065. Note, Scriptlike 71 | still $(I officially) requires at least DMDFE 2.066, mainly because 72 | of a bugfix for Windows, but DMDFE 2.065 appears to still be 73 | important for Debian's GDC. 74 | ) 75 | $(FIXED 76 | Excess blank lines and malformed `

    ` in this changelog. 77 | ) 78 | ) 79 | 80 | $(H2 v0.9.7 - 2017/01/23) 81 | 82 | $(UL 83 | $(ENHANCE 84 | Docs/Examples: Now recommend DUB v1.0.0+'s single-file package support, 85 | and test the provided example. 86 | ) 87 | $(FIXED 88 | $(ISSUE 31): Deprecation warnings on DMD 2.072 and up. 89 | ) 90 | ) 91 | 92 | $(H2 v0.9.6 - 2016/05/28) 93 | 94 | (Note: This was going to be v0.9.5, but the release got borked, so it's released as v0.9.6 instead.) 95 | 96 | $(UL 97 | $(FIXED 98 | $(ISSUE 26): Deprecation warnings on DMD 2.070 and 2.071. 99 | ) 100 | $(FIXED 101 | $(ISSUE 27): Flush stdout when requesting input. 102 | [$(LINK2 https://github.com/JesseKPhillips, Jesse Phillips)] 103 | ) 104 | $(FIXED 105 | $(LINK2 https://github.com/Abscissa/scriptlike/blob/master/USAGE.md#in-a-plain-script, Plain script) 106 | example fails on DUB 0.9.25 (due to a change in dub's package cache directory structure). 107 | ) 108 | $(FIXED 109 | Testing any pull request on 110 | $(LINK2 https://travis-ci.org/Abscissa/scriptlike/, Travis-CI) 111 | fails. 112 | ) 113 | $(FIXED 114 | Unittests fail to build on DMD 2.071. 115 | ) 116 | ) 117 | 118 | $(H2 v0.9.4 - 2015/09/22) 119 | 120 | $(UL 121 | $(FIXED 122 | Previous release broke the `unittest` script when `dub test` support was added. 123 | ) 124 | $(FIXED 125 | In echo mode, several functions would echo the wrong "try*" or 126 | non-"try*" version. Ex: $(API_PROCESS run) echoed $(API_PROCESS tryRun), 127 | and $(API_FILE_EXTR tryRename) echoed $(API_FILE_WRAP rename). 128 | ) 129 | $(FIXED 130 | $(API_PATH_EXTR Path) and $(API_PATH_EXTR buildNormalizedPathFixed) now 131 | convert back/forward slashes to native on BOTH Windows and Posix, not 132 | just on Windows. 133 | ) 134 | $(FIXED 135 | Some links within changelog and API reference were pointing to the 136 | reference docs for Scriptlike's latest version, instead of staying 137 | within the same documentation version. This made 138 | $(LINK2 http://semitwist.com/scriptlike-docs/, archived docs for previous versions) 139 | difficult to navigate. 140 | ) 141 | $(ENHANCE 142 | $(ISSUE 17),$(ISSUE 20): Added usage examples to readme. 143 | ) 144 | $(ENHANCE 145 | Add $(API_CORE interp) for interpolated strings:$(BR) 146 | `string s = mixin( interp!"Value is ${variableOrExpression}" )` 147 | ) 148 | $(ENHANCE 149 | Add $(API_FILE_EXTR removePath)/$(API_FILE_EXTR tryRemovePath) for 150 | deleting a path regardless of whether it's a file or directory. (Calls 151 | $(API_FILE_WRAP remove) for files and $(API_FILE_WRAP rmdirRecurse) for 152 | directories.) 153 | ) 154 | $(ENHANCE 155 | Add a Path-accepting overload of $(API_PATH_EXTR escapeShellArg) for 156 | the sake of generic code. 157 | ) 158 | $(ENHANCE 159 | When $(API_PROCESS runCollect) throws, the $(API_PROCESS ErrorLevelException) 160 | now includes and displays the command's output (otherwise there'd be no 161 | way to inspect the command's output for diagnostic purposes). 162 | ) 163 | $(ENHANCE 164 | Greatly extended and improved set of tests. 165 | ) 166 | ) 167 | 168 | $(H2 v0.9.3 - 2015/08/19) 169 | 170 | $(UL 171 | $(FIXED 172 | $(ISSUE 16): Access to standard Phobos function hampered. 173 | ) 174 | $(ENHANCE 175 | Support running unittests through DUB: `dub test` 176 | ) 177 | $(ENHANCE 178 | Uses $(LINK2 https://travis-ci.org, travis-ci.org) for continuous integration testing. 179 | ) 180 | ) 181 | 182 | $(H2 v0.9.2 - 2015/07/10) 183 | 184 | $(UL 185 | $(FIXED 186 | Properly flush all command echoing output 187 | (ie, in $(API_CORE yap) and $(API_CORE yapFunc)). 188 | ) 189 | $(ENHANCE 190 | Add a "no-build" configuration for projects that need to import/depend 191 | on Scriptlike through DUB, but use their own buildsystem. 192 | ) 193 | ) 194 | 195 | $(H2 v0.9.1 - 2015/06/28) 196 | 197 | $(UL 198 | $(FIXED Fails to compile unless the `makedocs` script has been run.) 199 | ) 200 | 201 | $(H2 v0.9.0 - 2015/06/27) 202 | 203 | $(UL 204 | $(CHANGE Split $(MODULE_FILE) and $(MODULE_PATH) into the following:$(BR) 205 | $(UL 206 | $(LI $(MODULE_CORE) ) 207 | $(LI $(MODULE_FILE_EXTR) ) 208 | $(LI $(MODULE_FILE_WRAP) ) 209 | $(LI $(MODULE_PATH_EXTR) ) 210 | $(LI $(MODULE_PATH_WRAP) ) 211 | ) 212 | Utilizes `package.d` to retain ability to import $(MODULE_FILE) and $(MODULE_PATH). 213 | ) 214 | $(CHANGE Convert changelog from markdown to $(DDOX) so links are more readable. ) 215 | $(ENHANCE Add (opt-in) command echoing to most functions in $(MODULE_FILE). ) 216 | $(ENHANCE 217 | Add $(API_CORE yap) and $(API_CORE yapFunc) as improved versions 218 | of to-be-deprecated $(API_CORE echoCommand). 219 | ) 220 | $(FIXED Make $(API_PATH_EXTR escapeShellArg) const-correct. ) 221 | $(FIXED 222 | Make $(API_PATH_EXTR Path.toRawString) and $(API_PATH_EXTR Ext.toRawString) 223 | both be `pure @safe nothrow`. 224 | ) 225 | ) 226 | 227 | $(H2 v0.8.1 - 2015/06/22) 228 | 229 | $(UL 230 | $(ENHANCE 231 | New overload for $(API_INTERACT userInput) to allow type inference:$(BR) 232 | `void userInput(T=string)(string question, ref T result);` 233 | (suggestion from 234 | $(LINK2 http://forum.dlang.org/post/povoxkcogcmbvhwlxqbc@forum.dlang.org, Per Nordlöw)). 235 | ) 236 | ) 237 | 238 | $(H2 v0.8.0 - 2015/06/13) 239 | 240 | $(UL 241 | $(CHANGE 242 | Minimum officially supported $(DMD) increased from v2.064.2 to v2.066.0. 243 | Versions below v2.066.0 may still work, but there will now be certain 244 | problems when dealing with paths that contain spaces, particularly 245 | on Windows. 246 | ) 247 | $(CHANGE 248 | Removed unnecessary non-$(API_PATH_EXTR Path) wrappers around $(MODULE_STD_FILE)/$(MODULE_STD_PATH). 249 | Things not wrapped (like $(STD_PATH dirSeparator) and $(STD_FILE SpanMode)) 250 | are now selective public imports instead of aliases. These changes should 251 | reduce issues with symbol conflicts. 252 | ) 253 | $(CHANGE 254 | $(LINK2 http://semitwist.com/scriptlike/, API reference) now built 255 | using $(DDOX) and uses much improved styling (actually uses a stylesheet now). 256 | ) 257 | $(CHANGE 258 | Eliminate remnants of the "planned but never enabled" wstring/dstring 259 | versions of $(API_PATH_EXTR Path)/$(API_PATH_EXTR Ext)/$(API_PROCESS Args). There 260 | turned out not to be much need for them, and even $(MODULE_STD_FILE) 261 | doesn't support wstring/dstring either. 262 | ) 263 | $(CHANGE Put output binaries in "bin" subdirectory, instead of Scriptlike's root. ) 264 | $(ENHANCE 265 | Add module scriptlike.only to import all of scriptlike, but omit the 266 | helper Phobos imports in scriptlike.std. 267 | ) 268 | $(ENHANCE 269 | $(API_FAIL fail) now accepts an arbitrary list of args of any type, 270 | just like $(STD_STDIO writeln), 271 | ) 272 | $(ENHANCE 273 | Added $(API_FAIL failEnforce), like Phobos's $(STD_EXCEPTION enforce), 274 | but for $(API_FAIL fail). 275 | ) 276 | $(ENHANCE 277 | Added $(API_PROCESS runCollect) and $(API_PROCESS tryRunCollect), to 278 | capture a command's output instead of displaying it. 279 | ) 280 | $(ENHANCE Added $(API_INTERACT pause) to pause and prompt the user to press Enter. ) 281 | $(ENHANCE $(API_CORE echoCommand) is no longer private. ) 282 | $(ENHANCE 283 | Added $(API_PATH_EXTR Path)-based wrappers for $(MODULE_STD_FILE)'s 284 | $(STD_FILE getcwd), $(STD_FILE thisExePath) and $(STD_FILE tempDir). 285 | ) 286 | $(FIXED No longer uses Phobos's deprecated $(STD_PROCESS system) function.) 287 | ) 288 | 289 | $(H2 v0.7.0 - 2015/04/02) 290 | 291 | $(UL 292 | $(ENHANCE 293 | $(ISSUE 14): Added scriptlike.interact module for easy user-input prompts. 294 | [$(LINK2 https://github.com/JesseKPhillips, Jesse Phillips)] 295 | ) 296 | $(FIXED Unittest compile failure on $(DMD) v2.067.0. ) 297 | ) 298 | 299 | $(H2 v0.6.0 - 2014/02/16) 300 | 301 | $(UL 302 | $(CHANGE 303 | $(API_PATH_EXTR Path) and $(API_PATH_EXTR Ext) are now aliases for the UTF-8 304 | instantiations, and the template structs are now named `PathT` and `ExtT`. 305 | ) 306 | $(CHANGE 307 | Removed `path()` and `ext()` helper functions to free up useful names 308 | from the namespace, since they are no longer needed. Use `Path()` and 309 | `Ext()` instead. 310 | ) 311 | $(CHANGE 312 | Internally split into separate modules, but uses `package.d` to 313 | preserve `import scriptlike;`. 314 | ) 315 | $(CHANGE Rename `escapeShellPath` -> $(API_PATH_EXTR escapeShellArg). ) 316 | $(CHANGE 317 | Rename $(API_PROCESS runShell) -> $(API_PROCESS tryRun). Temporarily keep 318 | $(API_PROCESS runShell) as an alias. 319 | ) 320 | $(CHANGE 321 | Rename $(API_CORE scriptlikeTraceCommands) -> $(API_CORE scriptlikeEcho). 322 | Temporarily keep $(API_CORE scriptlikeTraceCommands) as an alias. 323 | ) 324 | $(ENHANCE Added scripts to run unittests and build API docs. ) 325 | $(ENHANCE 326 | Added $(API_PATH_EXTR Path.opCast) and $(API_PATH_EXTR Ext.opCast) for 327 | converting to bool. 328 | ) 329 | $(ENHANCE 330 | $(API_FAIL fail) no longer requires any boilerplate in `main()`. 331 | ($(LINK2 http://forum.dlang.org/thread/ldc6qt$(DOLLAR)22tv$(DOLLAR)1@digitalmars.com, Newsgroup link)) 332 | ) 333 | $(ENHANCE 334 | Added $(API_PROCESS run) to run a shell command like $(API_PROCESS tryRun), 335 | but automatically throw if the process returns a non-zero error level. 336 | ) 337 | $(ENHANCE $(ISSUE 2): Optional callback sink for command echoing: $(API_CORE scriptlikeCustomEcho). ) 338 | $(ENHANCE $(ISSUE 8): Dry run support via bool $(API_CORE scriptlikeDryRun). ) 339 | $(ENHANCE 340 | $(ISSUE 13): Added `ArgsT` (and $(API_PROCESS Args) helper alias) 341 | to safely build command strings from parts. 342 | ) 343 | $(ENHANCE Added this changelog. ) 344 | $(FIXED 345 | $(API_PATH_EXTR Path)(null) and $(API_PATH_EXTR Ext)(null) were automatically 346 | changed to empty string. 347 | ) 348 | $(FIXED $(ISSUE 10): Docs should include all OS-specific functions. ) 349 | ) 350 | 351 | $(H2 v0.5.0 - 2014/02/11) 352 | 353 | $(UL 354 | $(LI Initial release. ) 355 | ) 356 | 357 | Copyright: 358 | Copyright (C) 2014-2017 Nick Sabalausky. 359 | Portions Copyright (C) 2010 Jesse Phillips. 360 | 361 | License: zlib/libpng 362 | Authors: Nick Sabalausky, Jesse Phillips 363 | +/ 364 | module changelog; 365 | -------------------------------------------------------------------------------- /tests/testExample.d: -------------------------------------------------------------------------------- 1 | /++ 2 | This program runs and tests one or all of the "features" examples 3 | in this directory. 4 | +/ 5 | import scriptlike; 6 | 7 | void function()[string] lookupTest; // Lookup test by name 8 | string testName; // Name of test being run 9 | 10 | void main(string[] args) 11 | { 12 | // Init test lookup 13 | lookupTest = [ 14 | "All": &testAll, 15 | 16 | "features/AutomaticPhobosImport": &testAutomaticPhobosImport, 17 | "features/CommandEchoing": &testCommandEchoing, 18 | "features/DisambiguatingWrite": &testDisambiguatingWrite, 19 | "features/DryRunAssistance": &testDryRunAssistance, 20 | "features/Fail": &testFail, 21 | "features/Filepaths": &testFilepaths, 22 | "features/ScriptStyleShellCommands": &testScriptStyleShellCommands, 23 | "features/StringInterpolation": &testStringInterpolation, 24 | "features/TryAsFilesystemOperations": &testTryAsFilesystemOperations, 25 | "features/UserInputPrompts": &testUserInputPrompts, 26 | 27 | "DubProject": &testDubProject, 28 | "SingleFile": &testSingleFile, 29 | ]; 30 | 31 | // Check args 32 | getopt(args, "v", &scriptlikeEcho); 33 | 34 | failEnforce( 35 | args.length == 2, 36 | "Invalid args.\n", 37 | "\n", 38 | "Usage: testExample [-v] NAME\n", 39 | "\n", 40 | "Options:\n", 41 | "-v Verbose\n", 42 | "\n", 43 | "Examples:\n", 44 | " testExample All\n", 45 | " testExample features/UserInputPrompts\n", 46 | "\n", 47 | "Available Test Names:\n", 48 | " ", lookupTest.keys.sort().join("\n "), 49 | ); 50 | 51 | testName = args[1]; 52 | failEnforce( 53 | (testName in lookupTest) != null, 54 | "No such test '", testName, "'.\n", 55 | "Available Test Names:\n", 56 | " ", lookupTest.keys.sort().join("\n "), 57 | ); 58 | 59 | // Setup for test 60 | chdir(thisExePath.dirName); 61 | tryMkdirRecurse("bin/features"); // gdmd doesn't automatically create the output directory. 62 | 63 | // Run test 64 | writeln("Testing ", testName); stdout.flush(); 65 | lookupTest[testName](); 66 | } 67 | 68 | alias RunResult = Tuple!(int, "status", string, "output"); 69 | 70 | /++ 71 | Compiles and runs a test, returning the test's output. 72 | 73 | Always displays, but does not return, the compiler output. 74 | 75 | Throws upon failure. 76 | +/ 77 | string compileAndRun(string testName, string runCmdSuffix=null) 78 | { 79 | return _compileAndRunImpl(true, testName, runCmdSuffix).output; 80 | } 81 | 82 | /++ 83 | Compiles and runs a test, returning the status code and the test's output. 84 | 85 | Always displays, but does not return, the compiler output. 86 | +/ 87 | RunResult tryCompileAndRun(string testName, string runCmdSuffix=null) 88 | { 89 | return _compileAndRunImpl(false, testName, runCmdSuffix); 90 | } 91 | 92 | /++ 93 | Separating the compile & build steps is important here because on 94 | AppVeyor/Windows the linker outputs a non-fatal message: 95 | "[...] not found or not built by the last incremental link; performing full link)" 96 | 97 | Any such non-fatal compilation messages MUST NOT be included in this 98 | function's return value or they will cause the tests to fail. 99 | +/ 100 | RunResult _compileAndRunImpl(bool throwOnError, string testName, string runCmdSuffix) 101 | { 102 | version(Windows) auto exeSuffix = ".exe"; 103 | else auto exeSuffix = ""; 104 | 105 | auto compileCmd = compilerCommand(testName); 106 | auto runBinary = fixSlashes("bin/"~testName~exeSuffix); 107 | auto runCmd = runBinary~runCmdSuffix; 108 | 109 | writeln("compileCmd: ", compileCmd); stdout.flush(); 110 | writeln("runCmd: ", runCmd); stdout.flush(); 111 | 112 | if(throwOnError) 113 | { 114 | run(compileCmd); 115 | auto output = runCollect(runCmd); 116 | return RunResult(0, output); 117 | } 118 | else 119 | { 120 | auto status = tryRun(compileCmd); 121 | if(status != 0) 122 | return RunResult(status, null); 123 | 124 | return tryRunCollect(runCmd); 125 | } 126 | } 127 | 128 | string compilerCommand(string testName) 129 | { 130 | string archFlag = ""; 131 | auto envArch = environment.get("Darch", ""); 132 | if(envArch == "x86_64") archFlag = "-m64"; 133 | if(envArch == "x86") archFlag = "-m32"; 134 | 135 | auto libSourceFiles = cast(string) 136 | dirEntries("../src", "*.d", SpanMode.breadth). 137 | map!(a => cast(const(ubyte)[]) escapeShellArg(a)). 138 | joiner(cast(const(ubyte)[]) " "). 139 | array; 140 | 141 | version(Windows) auto execName = testName~".exe"; 142 | else auto execName = testName; 143 | 144 | auto envDmd = environment.get("DMD", "dmd"); 145 | return envDmd~" "~archFlag~" -debug -g -I../src "~libSourceFiles~" -ofbin/"~execName~" ../examples/"~testName~".d"; 146 | } 147 | 148 | string normalizeNewlines(string str) 149 | { 150 | version(Windows) return str.replace("\r\n", "\n"); 151 | else return str; 152 | } 153 | 154 | string fixSlashes(string path) 155 | { 156 | version(Windows) return path.replace(`/`, `\`); 157 | else version(Posix) return path.replace(`\`, `/`); 158 | else static assert(0); 159 | } 160 | 161 | string quote(string str) 162 | { 163 | version(Windows) return `"` ~ str ~ `"`; 164 | else version(Posix) return `'` ~ str ~ `'`; 165 | else static assert(0); 166 | } 167 | 168 | void testAll() 169 | { 170 | bool failed = false; // Have any tests failed? 171 | 172 | foreach(name; lookupTest.keys.sort()) 173 | if(lookupTest[name] != &testAll) 174 | { 175 | // Instead of running the test function directly, run it as a separate 176 | // process. This way, we can safely continue running all the tests 177 | // even if one throws an AssertError or other Error. 178 | auto verbose = scriptlikeEcho? "-v " : ""; 179 | auto status = tryRun("." ~ dirSeparator ~ "testExample " ~ verbose ~ name); 180 | if(status != 0) 181 | failed = true; 182 | } 183 | writeln("Done running tests for examples."); stdout.flush(); 184 | 185 | failEnforce(!failed, "Not all tests succeeded."); 186 | } 187 | 188 | void testAutomaticPhobosImport() 189 | { 190 | auto output = compileAndRun(testName).normalizeNewlines; 191 | assert(output == "Works!\n"); 192 | } 193 | 194 | void testCommandEchoing() 195 | { 196 | immutable expected = 197 | "run: echo Hello > file.txt 198 | mkdirRecurse: "~("some/new/dir".fixSlashes)~" 199 | copy: file.txt -> "~("some/new/dir/target name.txt".fixSlashes.quote)~" 200 | Gonna run foo() now... 201 | foo: i = 42 202 | "; 203 | 204 | auto output = compileAndRun(testName).normalizeNewlines; 205 | assert(output == expected); 206 | } 207 | 208 | void testDisambiguatingWrite() 209 | { 210 | immutable expected = "Hello worldHello world"; 211 | 212 | auto output = compileAndRun(testName).normalizeNewlines; 213 | assert(output == expected); 214 | } 215 | 216 | void testDryRunAssistance() 217 | { 218 | immutable expected = 219 | "copy: original.d -> app.d 220 | run: dmd app.d -ofbin/app 221 | exists: another-file 222 | "; 223 | 224 | auto output = compileAndRun(testName).normalizeNewlines; 225 | assert(output == expected); 226 | } 227 | 228 | void testFail() 229 | { 230 | auto result = tryCompileAndRun(testName); 231 | assert(result.status > 0); 232 | assert(result.output.normalizeNewlines.strip == "Fail: ERROR: Need two args, not 0!"); 233 | 234 | result = tryCompileAndRun(testName, " abc 123"); 235 | assert(result.status > 0); 236 | assert(result.output.normalizeNewlines.strip == "Fail: ERROR: First arg must be 'foobar', not 'abc'!"); 237 | 238 | auto output = compileAndRun(testName, " foobar 123"); 239 | assert(output == ""); 240 | } 241 | 242 | void testFilepaths() 243 | { 244 | immutable expected = 245 | ("foo/bar/different subdir/Filename with spaces.txt".fixSlashes.quote) ~ "\n" ~ 246 | ("foo/bar/different subdir/Filename with spaces.txt".fixSlashes) ~ "\n"; 247 | 248 | auto output = compileAndRun(testName).normalizeNewlines; 249 | assert(output == expected); 250 | } 251 | 252 | void testScriptStyleShellCommands() 253 | { 254 | // This test relies on "dmd" being available on the PATH 255 | auto dmdResult = tryRunCollect("dmd --help"); 256 | if(dmdResult.status != 0) 257 | { 258 | writeln(`Skipping `, testName, `: Couldn't find 'dmd' on the PATH.`); stdout.flush(); 259 | return; 260 | } 261 | 262 | immutable inFile = "testinput.txt"; 263 | scope(exit) 264 | tryRemove(inFile); 265 | 266 | writeFile(inFile, "\n"); 267 | 268 | version(OSX) enum key = "Return"; 269 | else enum key = "Enter"; 270 | 271 | immutable expectedExcerpt = 272 | "Press "~key~" to continue...Error: unrecognized switch '--bad-flag'\n"; 273 | 274 | auto output = compileAndRun(testName, " < " ~ inFile).normalizeNewlines; 275 | assert(output.canFind(expectedExcerpt)); 276 | } 277 | 278 | void testStringInterpolation() 279 | { 280 | immutable expected = 281 | "The number 21 doubled is 42! 282 | Empty braces output nothing. 283 | Multiple params: John Doe. 284 | "; 285 | 286 | auto output = compileAndRun(testName).normalizeNewlines; 287 | assert(output == expected); 288 | } 289 | 290 | void testTryAsFilesystemOperations() 291 | { 292 | auto output = compileAndRun(testName).normalizeNewlines; 293 | assert(output == ""); 294 | } 295 | 296 | void testUserInputPrompts() 297 | { 298 | immutable inFile = "testinput.txt"; 299 | scope(exit) 300 | tryRemove(inFile); 301 | 302 | writeFile(inFile, 303 | "Nana 304 | 20 305 | y 306 | testExample.d 307 | 2 308 | 7 309 | \n\n" 310 | ); 311 | 312 | version(OSX) enum key = "Return"; 313 | else enum key = "Enter"; 314 | 315 | immutable expectedExcerpt = 316 | "Please enter your name 317 | > And your age 318 | > Do you want to continue? 319 | > Where you do want to place the output? 320 | > What color would you like to use? 321 | 1. Blue 322 | 2. Green 323 | No Input. Quit 324 | 325 | > Enter a number from 1 to 10 326 | > Press "~key~" to continue...Hit Enter again, dood!!"; 327 | 328 | auto output = compileAndRun(testName, " < " ~ inFile).normalizeNewlines; 329 | assert(output.canFind(expectedExcerpt)); 330 | } 331 | 332 | void testUseInScripts(string subdir, Path workingDir, string command, bool checkReportedDir=true) 333 | { 334 | auto projDir = Path("../examples/"~subdir); 335 | 336 | // Test with cmdline arg 337 | { 338 | string expected; 339 | if(checkReportedDir) 340 | { 341 | expected = text( 342 | "This script is in directory: ", (thisExePath.dirName ~ projDir), " 343 | Hello, Frank! 344 | "); 345 | } 346 | else 347 | { 348 | expected = text( 349 | "Hello, Frank! 350 | "); 351 | } 352 | auto output = workingDir.runCollect( command~" Frank" ).normalizeNewlines; 353 | if(output != expected) 354 | { 355 | writeln("expected:========================"); 356 | writeln(expected); 357 | writeln("output:========================"); 358 | writeln(output); 359 | writeln("========================"); 360 | stdout.flush(); 361 | } 362 | assert(output.endsWith(expected)); 363 | } 364 | 365 | // Test interactive 366 | { 367 | immutable inFile = "testinput.txt"; 368 | scope(exit) 369 | tryRemove(workingDir ~ inFile); 370 | 371 | writeFile(workingDir ~ inFile, "George\n"); 372 | 373 | string expected; 374 | if(checkReportedDir) 375 | { 376 | expected = text( 377 | "This script is in directory: ", (thisExePath.dirName ~ projDir), " 378 | What's your name? 379 | > Hello, George! 380 | "); 381 | } 382 | else 383 | { 384 | expected = text( 385 | "What's your name? 386 | > Hello, George! 387 | "); 388 | } 389 | 390 | auto output = workingDir.runCollect( command~" < "~inFile ).normalizeNewlines; 391 | if(output != expected) 392 | { 393 | writeln("expected:========================"); 394 | writeln(expected); 395 | writeln("output:========================"); 396 | writeln(output); 397 | writeln("========================"); 398 | stdout.flush(); 399 | } 400 | assert(output.endsWith(expected)); 401 | } 402 | } 403 | 404 | string getDubEnvArgs() 405 | { 406 | string args; 407 | 408 | if(environment.get("Darch") !is null) 409 | args ~= " --arch=" ~ environment["Darch"]; 410 | 411 | if(environment.get("DC") !is null) 412 | args ~= " --compiler=" ~ environment["DC"]; 413 | 414 | return args; 415 | } 416 | 417 | void testDubProject() 418 | { 419 | // Force rebuild 420 | tryRemove("../examples/dub-project/myscript"); 421 | tryRemove("../examples/dub-project/myscript.exe"); 422 | 423 | // Do test 424 | testUseInScripts("dub-project", Path("../examples/dub-project"), "dub --vquiet "~getDubEnvArgs~" -- "); 425 | } 426 | 427 | void testSingleFile() 428 | { 429 | // Do tests 430 | writeln(" Testing from its own directory..."); stdout.flush(); 431 | testUseInScripts("single-file", Path("../examples/single-file"), "dub --vquiet --single "~getDubEnvArgs~" myscript.d -- ", false); 432 | 433 | writeln(" Testing from different directory..."); stdout.flush(); 434 | testUseInScripts( 435 | "single-file", 436 | Path("../tests/bin"), 437 | "dub --vquiet --single "~getDubEnvArgs~" "~Path("../../examples/single-file/myscript.d").raw~" -- ", 438 | false 439 | ); 440 | } 441 | -------------------------------------------------------------------------------- /docs/public/prettify/prettify.js: -------------------------------------------------------------------------------- 1 | var o=!0,r=null,A=!1;window.PR_SHOULD_USE_CONTINUATION=o; (function(){function O(a){function m(a){var f=a.charCodeAt(0);if(92!==f)return f;var b=a.charAt(1);return(f=s[b])?f:"0"<=b&&"7">=b?parseInt(a.substring(1),8):"u"===b||"x"===b?parseInt(a.substring(2),16):a.charCodeAt(1)}function e(a){if(32>a)return(16>a?"\\x0":"\\x")+a.toString(16);a=String.fromCharCode(a);if("\\"===a||"-"===a||"["===a||"]"===a)a="\\"+a;return a}function h(a){for(var f=a.substring(1,a.length-1).match(RegExp("\\\\u[0-9A-Fa-f]{4}|\\\\x[0-9A-Fa-f]{2}|\\\\[0-3][0-7]{0,2}|\\\\[0-7]{1,2}|\\\\[\\s\\S]|-|[^-\\\\]","g")), a=[],b=[],p="^"===f[0],c=p?1:0,i=f.length;cd||122d||90d||122i[0]&&(i[1]+1>i[0]&&b.push("-"),b.push(e(i[1])));b.push("]");return b.join("")}function z(a){for(var f=a.source.match(RegExp("(?:\\[(?:[^\\x5C\\x5D]|\\\\[\\s\\S])*\\]|\\\\u[A-Fa-f0-9]{4}|\\\\x[A-Fa-f0-9]{2}|\\\\[0-9]+|\\\\[^ux0-9]|\\(\\?[:!=]|[\\(\\)\\^]|[^\\x5B\\x5C\\(\\)\\^]+)","g")),b=f.length,d=[],c=0,i=0;c/,r])):m.push(["com",/^#[^\r\n]*/,r,"#"]));a.cStyleComments&&(e.push(["com",/^\/\/[^\r\n]*/,r]),e.push(["com",/^\/\*[\s\S]*?(?:\*\/|$)/,r]));a.regexLiterals&&e.push(["lang-regex",RegExp("^(?:^^\\.?|[+-]|\\!|\\!=|\\!==|\\#|\\%|\\%=|&|&&|&&=|&=|\\(|\\*|\\*=|\\+=|\\,|\\-=|\\->|\\/|\\/=|:|::|\\;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|\\?|\\@|\\[|\\^|\\^=|\\^\\^|\\^\\^=|\\{|\\||\\|=|\\|\\||\\|\\|=|\\~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\\s*(/(?=[^/*])(?:[^/\\x5B\\x5C]|\\x5C[\\s\\S]|\\x5B(?:[^\\x5C\\x5D]|\\x5C[\\s\\S])*(?:\\x5D|$))+/)")]); (h=a.types)&&e.push(["typ",h]);a=(""+a.keywords).replace(/^ | $/g,"");a.length&&e.push(["kwd",RegExp("^(?:"+a.replace(/[\s,]+/g,"|")+")\\b"),r]);m.push(["pln",/^\s+/,r," \r\n\t\u00a0"]);e.push(["lit",/^@[a-z_$][a-z_$@0-9]*/i,r],["typ",/^(?:[@_]?[A-Z]+[a-z][A-Za-z_$@0-9]*|\w+_t\b)/,r],["pln",/^[a-z_$][a-z_$@0-9]*/i,r],["lit",/^(?:0x[a-f0-9]+|(?:\d(?:_\d+)*\d*(?:\.\d*)?|\.\d\+)(?:e[+\-]?\d+)?)[a-z]*/i,r,"0123456789"],["pln",/^\\[\s\S]?/,r],["pun",/^.[^\s\w\.$@\'\"\`\/\#\\]*/,r]);return x(m,e)}function F(a, m){function e(a){switch(a.nodeType){case 1:if(k.test(a.className))break;if("BR"===a.nodeName)h(a),a.parentNode&&a.parentNode.removeChild(a);else for(a=a.firstChild;a;a=a.nextSibling)e(a);break;case 3:case 4:if(q){var b=a.nodeValue,d=b.match(u);if(d){var c=b.substring(0,d.index);a.nodeValue=c;(b=b.substring(d.index+d[0].length))&&a.parentNode.insertBefore(t.createTextNode(b),a.nextSibling);h(a);c||a.parentNode.removeChild(a)}}}}function h(a){function b(a,d){var e=d?a.cloneNode(A):a,f=a.parentNode; if(f){var f=b(f,1),g=a.nextSibling;f.appendChild(e);for(var h=g;h;h=g)g=h.nextSibling,f.appendChild(h)}return e}for(;!a.nextSibling;)if(a=a.parentNode,!a)return;for(var a=b(a.nextSibling,0),e;(e=a.parentNode)&&1===e.nodeType;)a=e;d.push(a)}var k=/(?:^|\s)nocode(?:\s|$)/,u=/\r\n?|\n/,t=a.ownerDocument,l;a.currentStyle?l=a.currentStyle.whiteSpace:window.getComputedStyle&&(l=t.defaultView.getComputedStyle(a,r).getPropertyValue("white-space"));var q=l&&"pre"===l.substring(0,3);for(l=t.createElement("LI");a.firstChild;)l.appendChild(a.firstChild); for(var d=[l],g=0;g=p&&(h+=2);e>=c&&(a+=2)}}catch(x){"console"in window&&console.log(x&&x.stack?x.stack:x)}}var w=["break,continue,do,else,for,if,return,while"],y=[[w,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"], "catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"],H=[y,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"],I=[y,"alias,align,asm,bool,cast,export,foreach,foreach_reverse,import,interface,module,null,assert,template,typeid,byte,ubyte,ushort,uint,ulong"], J=[y,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"],K=[J,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"],y=[y,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"],L= [w,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"],M=[w,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"],w=[w,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"],N=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/, Q=/\S/,R=v({keywords:[H,I,K,y,"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END"+L,M,w],hashComments:o,cStyleComments:o,multiLineStrings:o,regexLiterals:o}),C={};k(R,["default-code"]);k(x([],[["pln",/^[^]*(?:>|$)/],["com",/^<\!--[\s\S]*?(?:-\->|$)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],["pun",/^(?:<[%?]|[%?]>)/],["lang-", /^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]),"default-markup htm html mxml xhtml xml xsl".split(" "));k(x([["pln",/^[\s]+/,r," \t\r\n"],["atv",/^(?:\"[^\"]*\"?|\'[^\']*\'?)/,r,"\"'"]],[["tag",/^^<\/?[a-z](?:[\w.:-]*\w)?|\/?>$/i],["atn",/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^>\'\"\s]*(?:[^>\'\"\s\/]|\/(?=\s)))/],["pun", /^[=<>\/]+/],["lang-js",/^on\w+\s*=\s*\"([^\"]+)\"/i],["lang-js",/^on\w+\s*=\s*\'([^\']+)\'/i],["lang-js",/^on\w+\s*=\s*([^\"\'>\s]+)/i],["lang-css",/^style\s*=\s*\"([^\"]+)\"/i],["lang-css",/^style\s*=\s*\'([^\']+)\'/i],["lang-css",/^style\s*=\s*([^\"\'>\s]+)/i]]),["in.tag"]);k(x([],[["atv",/^[\s\S]+/]]),["uq.val"]);k(v({keywords:H,hashComments:o,cStyleComments:o,types:N}),"c cc cpp cxx cyc m".split(" "));k(v({keywords:I,hashComments:o,cStyleComments:o,types:/^(Object|string)/}),["d"]);k(v({keywords:"null,true,false"}), ["json"]);k(v({keywords:K,hashComments:o,cStyleComments:o,verbatimStrings:o,types:N}),["cs"]);k(v({keywords:J,cStyleComments:o}),["java"]);k(v({keywords:w,hashComments:o,multiLineStrings:o}),["bsh","csh","sh"]);k(v({keywords:L,hashComments:o,multiLineStrings:o,tripleQuotedStrings:o}),["cv","py"]);k(v({keywords:"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END",hashComments:o, multiLineStrings:o,regexLiterals:o}),["perl","pl","pm"]);k(v({keywords:M,hashComments:o,multiLineStrings:o,regexLiterals:o}),["rb"]);k(v({keywords:y,cStyleComments:o,regexLiterals:o}),["js"]);k(v({keywords:"all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes",hashComments:3,cStyleComments:o,multilineStrings:o,tripleQuotedStrings:o,regexLiterals:o}),["coffee"]);k(x([],[["str",/^[\s\S]+/]]),["regex"]); window.prettyPrintOne=function(a,m,e){var h=document.createElement("PRE");h.innerHTML=a;e&&F(h,e);G({g:m,i:e,h:h});return h.innerHTML};window.prettyPrint=function(a){function m(){for(var e=window.PR_SHOULD_USE_CONTINUATION?l.now()+250:Infinity;q ", Path(targetFile))); 119 | mixin(checkResult); 120 | }); 121 | 122 | testFileOperation!("run", "custom dir")(() { 123 | mixin(useTmpName!"scratchDir"); 124 | mixin(useTmpName!("targetFile", "dummy")); 125 | auto origDir = std.file.getcwd(); 126 | scope(exit) std.file.chdir(origDir); 127 | std.file.mkdir(scratchDir); 128 | std.file.chdir(scratchDir); 129 | std.file.mkdir(std.path.dirName(targetFile)); 130 | expectedContent = std.path.dirName(targetFile); 131 | 132 | checkPre(); 133 | run(Path(std.path.dirName(targetFile)), text(pwd, " > dummy")); 134 | mixin(checkResult); 135 | }); 136 | 137 | testFileOperation!("run", "bad command")(() { 138 | import std.exception : assertThrown; 139 | 140 | void doIt() 141 | { 142 | run("cd this-path-does-not-exist-scriptlike"~quiet); 143 | } 144 | 145 | if(scriptlikeDryRun) 146 | doIt(); 147 | else 148 | assertThrown!ErrorLevelException( doIt() ); 149 | }); 150 | } 151 | 152 | /++ 153 | Runs a command, through the system's command shell interpreter, 154 | in typical shell-script style: Synchronously, with the command's 155 | stdout/in/err automatically forwarded through your 156 | program's stdout/in/err. 157 | 158 | Optionally takes a working directory to run the command from. 159 | 160 | The command is echoed if scriptlikeEcho is true. 161 | 162 | Returns: The error level the process exited with. Or -1 upon failure to 163 | start the process. 164 | 165 | Example: 166 | --------------------- 167 | Args cmd; 168 | cmd ~= Path("some tool"); 169 | cmd ~= "-o"; 170 | cmd ~= Path(`dir/out file.txt`); 171 | cmd ~= ["--abc", "--def", "-g"]; 172 | auto errLevel = Path("some working dir").run(cmd.data); 173 | --------------------- 174 | +/ 175 | int tryRun(string command) 176 | { 177 | yapFunc(command); 178 | 179 | if(scriptlikeDryRun) 180 | return 0; 181 | else 182 | { 183 | try 184 | return spawnShell(command).wait(); 185 | catch(Exception e) 186 | return -1; 187 | } 188 | } 189 | 190 | ///ditto 191 | int tryRun(Path workingDirectory, string command) 192 | { 193 | auto saveDir = getcwd(); 194 | workingDirectory.chdir(); 195 | scope(exit) saveDir.chdir(); 196 | 197 | return tryRun(command); 198 | } 199 | 200 | version(unittest_scriptlike_d) 201 | unittest 202 | { 203 | import std.string : strip; 204 | 205 | string scratchDir; 206 | string targetFile; 207 | string expectedContent; 208 | void checkPre() 209 | { 210 | assert(!std.file.exists(targetFile)); 211 | } 212 | 213 | void checkPost() 214 | { 215 | assert(std.file.exists(targetFile)); 216 | assert(std.file.isFile(targetFile)); 217 | assert(strip(cast(string) std.file.read(targetFile)) == expectedContent); 218 | } 219 | 220 | testFileOperation!("tryRun", "default dir")(() { 221 | mixin(useTmpName!"scratchDir"); 222 | mixin(useTmpName!("targetFile", "dummy")); 223 | auto origDir = std.file.getcwd(); 224 | scope(exit) std.file.chdir(origDir); 225 | std.file.mkdir(scratchDir); 226 | std.file.chdir(scratchDir); 227 | std.file.mkdir(std.path.dirName(targetFile)); 228 | expectedContent = scratchDir; 229 | 230 | checkPre(); 231 | tryRun(text(pwd, " > ", Path(targetFile))); 232 | mixin(checkResult); 233 | }); 234 | 235 | testFileOperation!("tryRun", "custom dir")(() { 236 | mixin(useTmpName!"scratchDir"); 237 | mixin(useTmpName!("targetFile", "dummy")); 238 | auto origDir = std.file.getcwd(); 239 | scope(exit) std.file.chdir(origDir); 240 | std.file.mkdir(scratchDir); 241 | std.file.chdir(scratchDir); 242 | std.file.mkdir(std.path.dirName(targetFile)); 243 | expectedContent = std.path.dirName(targetFile); 244 | 245 | checkPre(); 246 | tryRun(Path(std.path.dirName(targetFile)), text(pwd, " > dummy")); 247 | mixin(checkResult); 248 | }); 249 | 250 | testFileOperation!("tryRun", "bad command")(() { 251 | import std.exception : assertNotThrown; 252 | mixin(useTmpName!"scratchDir"); 253 | auto origDir = std.file.getcwd(); 254 | scope(exit) std.file.chdir(origDir); 255 | std.file.mkdir(scratchDir); 256 | std.file.chdir(scratchDir); 257 | 258 | assertNotThrown( tryRun("cd this-path-does-not-exist-scriptlike"~quiet) ); 259 | }); 260 | } 261 | 262 | /// Backwards-compatibility alias. runShell may become deprecated in the 263 | /// future, so you should use tryRun or run insetad. 264 | alias runShell = tryRun; 265 | 266 | /// Similar to run(), but (like std.process.executeShell) captures and returns 267 | /// the output instead of displaying it. 268 | string runCollect(string command) 269 | { 270 | yapFunc(command); 271 | mixin(gagEcho); 272 | 273 | auto result = tryRunCollect(command); 274 | if(result.status != 0) 275 | throw new ErrorLevelException(result.status, command, result.output); 276 | 277 | return result.output; 278 | } 279 | 280 | ///ditto 281 | string runCollect(Path workingDirectory, string command) 282 | { 283 | auto saveDir = getcwd(); 284 | workingDirectory.chdir(); 285 | scope(exit) saveDir.chdir(); 286 | 287 | return runCollect(command); 288 | } 289 | 290 | version(unittest_scriptlike_d) 291 | unittest 292 | { 293 | import std.string : strip; 294 | string dir; 295 | 296 | testFileOperation!("runCollect", "default dir")(() { 297 | auto result = runCollect(pwd); 298 | 299 | if(scriptlikeDryRun) 300 | assert(result == ""); 301 | else 302 | assert(strip(result) == std.file.getcwd()); 303 | }); 304 | 305 | testFileOperation!("runCollect", "custom dir")(() { 306 | mixin(useTmpName!"dir"); 307 | std.file.mkdir(dir); 308 | 309 | auto result = Path(dir).runCollect(pwd); 310 | 311 | if(scriptlikeDryRun) 312 | assert(result == ""); 313 | else 314 | assert(strip(result) == dir); 315 | }); 316 | 317 | testFileOperation!("runCollect", "bad command")(() { 318 | import std.exception : assertThrown; 319 | 320 | void doIt() 321 | { 322 | runCollect("cd this-path-does-not-exist-scriptlike"~quiet); 323 | } 324 | 325 | if(scriptlikeDryRun) 326 | doIt(); 327 | else 328 | assertThrown!ErrorLevelException( doIt() ); 329 | }); 330 | } 331 | 332 | /// Similar to tryRun(), but (like $(FULL_STD_PROCESS executeShell)) captures 333 | /// and returns the output instead of displaying it. 334 | /// 335 | /// Returns the same tuple as $(FULL_STD_PROCESS executeShell): 336 | /// `std.typecons.Tuple!(int, "status", string, "output")` 337 | /// 338 | /// Returns: The `status` field will be -1 upon failure to 339 | /// start the process. 340 | auto tryRunCollect(string command) 341 | { 342 | import std.typecons : Tuple; 343 | import std.traits : ReturnType; 344 | 345 | yapFunc(command); 346 | // Tuple!(int, "status", string, "output") on DMD 2.066 and up 347 | // ProcessOutput on DMD 2.065 348 | auto result = ReturnType!executeShell(0, null); 349 | 350 | if(scriptlikeDryRun) 351 | return result; 352 | else 353 | { 354 | try 355 | return executeShell(command); 356 | catch(Exception e) 357 | { 358 | result.status = -1; 359 | return result; 360 | } 361 | } 362 | } 363 | 364 | ///ditto 365 | auto tryRunCollect(Path workingDirectory, string command) 366 | { 367 | auto saveDir = getcwd(); 368 | workingDirectory.chdir(); 369 | scope(exit) saveDir.chdir(); 370 | 371 | return tryRunCollect(command); 372 | } 373 | 374 | version(unittest_scriptlike_d) 375 | unittest 376 | { 377 | import std.string : strip; 378 | string dir; 379 | 380 | testFileOperation!("tryRunCollect", "default dir")(() { 381 | auto result = tryRunCollect(pwd); 382 | 383 | assert(result.status == 0); 384 | if(scriptlikeDryRun) 385 | assert(result.output == ""); 386 | else 387 | assert(strip(result.output) == std.file.getcwd()); 388 | }); 389 | 390 | testFileOperation!("tryRunCollect", "custom dir")(() { 391 | mixin(useTmpName!"dir"); 392 | std.file.mkdir(dir); 393 | 394 | auto result = Path(dir).tryRunCollect(pwd); 395 | 396 | assert(result.status == 0); 397 | if(scriptlikeDryRun) 398 | assert(result.output == ""); 399 | else 400 | assert(strip(result.output) == dir); 401 | }); 402 | 403 | testFileOperation!("tryRunCollect", "bad command")(() { 404 | import std.exception : assertThrown; 405 | 406 | auto result = tryRunCollect("cd this-path-does-not-exist-scriptlike"~quiet); 407 | if(scriptlikeDryRun) 408 | assert(result.status == 0); 409 | else 410 | assert(result.status != 0); 411 | assert(result.output == ""); 412 | }); 413 | } 414 | 415 | /++ 416 | Much like std.array.Appender!string, but specifically geared towards 417 | building a command string out of arguments. String and Path can both 418 | be appended. All elements added will automatically be escaped, 419 | and separated by spaces, as necessary. 420 | 421 | Example: 422 | ------------------- 423 | Args args; 424 | args ~= Path(`some/big path/here/foobar`); 425 | args ~= "-A"; 426 | args ~= "--bcd"; 427 | args ~= "Hello World"; 428 | args ~= Path("file.ext"); 429 | 430 | // On windows: 431 | assert(args.data == `"some\big path\here\foobar" -A --bcd "Hello World" file.ext`); 432 | // On linux: 433 | assert(args.data == `'some/big path/here/foobar' -A --bcd 'Hello World' file.ext`); 434 | ------------------- 435 | +/ 436 | struct Args 437 | { 438 | // Internal note: For every element the user adds to ArgsT, 439 | // *two* elements will be added to this internal buf: first a spacer 440 | // (normally a space, or an empty string in the case of the very first 441 | // element the user adds) and then the actual element the user added. 442 | private Appender!(string) buf; 443 | private size_t _length = 0; 444 | 445 | void reserve(size_t newCapacity) @safe pure nothrow 446 | { 447 | // "*2" to account for the spacers 448 | buf.reserve(newCapacity * 2); 449 | } 450 | 451 | 452 | @property size_t capacity() const @safe pure nothrow 453 | { 454 | // "/2" to account for the spacers 455 | return buf.capacity / 2; 456 | } 457 | 458 | @property string data() inout @trusted pure nothrow 459 | { 460 | return buf.data; 461 | } 462 | 463 | @property size_t length() 464 | { 465 | return _length; 466 | } 467 | 468 | private void putSpacer() 469 | { 470 | buf.put(_length==0? "" : " "); 471 | } 472 | 473 | void put(string item) 474 | { 475 | putSpacer(); 476 | buf.put(escapeShellArg(item)); 477 | _length += 2; 478 | } 479 | 480 | void put(Path item) 481 | { 482 | put(item.raw); 483 | } 484 | 485 | void put(Range)(Range items) 486 | if( 487 | isInputRange!Range && 488 | (is(ElementType!Range == string) || is(ElementType!Range == Path)) 489 | ) 490 | { 491 | for(; !items.empty; items.popFront()) 492 | put(items.front); 493 | } 494 | 495 | void opOpAssign(string op)(string item) if(op == "~") 496 | { 497 | put(item); 498 | } 499 | 500 | void opOpAssign(string op)(Path item) if(op == "~") 501 | { 502 | put(item); 503 | } 504 | 505 | void opOpAssign(string op, Range)(Range items) 506 | if( 507 | op == "~" && 508 | isInputRange!Range && 509 | (is(ElementType!Range == string) || is(ElementType!Range == Path)) 510 | ) 511 | { 512 | put(items); 513 | } 514 | } 515 | 516 | version(unittest_scriptlike_d) 517 | unittest 518 | { 519 | import std.stdio : writeln; 520 | writeln("Running Scriptlike unittests: Args"); 521 | 522 | Args args; 523 | args ~= Path(`some/big path/here/foobar`); 524 | args ~= "-A"; 525 | args ~= "--bcd"; 526 | args ~= "Hello World"; 527 | args ~= Path("file.ext"); 528 | 529 | version(Windows) 530 | assert(args.data == `"some\big path\here\foobar" -A --bcd "Hello World" file.ext`); 531 | else version(Posix) 532 | assert(args.data == `'some/big path/here/foobar' -A --bcd 'Hello World' file.ext`); 533 | } 534 | -------------------------------------------------------------------------------- /src/scriptlike/core.d: -------------------------------------------------------------------------------- 1 | // Scriptlike: Utility to aid in script-like programs. 2 | // Written in the D programming language. 3 | 4 | /// Copyright: Copyright (C) 2014-2017 Nick Sabalausky 5 | /// License: $(LINK2 https://github.com/Abscissa/scriptlike/blob/master/LICENSE.txt, zlib/libpng) 6 | /// Authors: Nick Sabalausky 7 | 8 | module scriptlike.core; 9 | 10 | import std.conv; 11 | static import std.file; 12 | static import std.path; 13 | import std.string; 14 | 15 | /// If true, all commands will be echoed. By default, they will be 16 | /// echoed to stdout, but you can override this with scriptlikeCustomEcho. 17 | bool scriptlikeEcho = false; 18 | 19 | /// Alias for backwards-compatibility. This will be deprecated in the future. 20 | /// You should use scriptlikeEcho insetad. 21 | alias scriptlikeTraceCommands = scriptlikeEcho; 22 | 23 | /++ 24 | If true, then run, tryRun, file write, file append, and all the echoable 25 | commands that modify the filesystem will be echoed to stdout (regardless 26 | of scriptlikeEcho) and NOT actually executed. 27 | 28 | Warning! This is NOT a "set it and forget it" switch. You must still take 29 | care to write your script in a way that's dryrun-safe. Two things to remember: 30 | 31 | 1. ONLY Scriptlike's functions will obey this setting. Calling Phobos 32 | functions directly will BYPASS this setting. 33 | 34 | 2. If part of your script relies on a command having ACTUALLY been run, then 35 | that command will fail. You must avoid that situation or work around it. 36 | For example: 37 | 38 | --------------------- 39 | run(`date > tempfile`); 40 | 41 | // The following will FAIL or behave INCORRECTLY in dryrun mode: 42 | auto data = cast(string)read("tempfile"); 43 | run("echo "~data); 44 | --------------------- 45 | 46 | That may be an unrealistic example, but it demonstrates the problem: Normally, 47 | the code above should run fine (at least on posix). But in dryrun mode, 48 | "date" will not actually be run. Therefore, tempfile will neither be created 49 | nor overwritten. Result: Either an exception reading a non-existent file, 50 | or outdated information will be displayed. 51 | 52 | Scriptlike cannot anticipate or handle such situations. So it's up to you to 53 | make sure your script is dryrun-safe. 54 | +/ 55 | bool scriptlikeDryRun = false; 56 | 57 | /++ 58 | By default, scriptlikeEcho and scriptlikeDryRun echo to stdout. 59 | You can override this behavior by setting scriptlikeCustomEcho to your own 60 | sink delegate. Since this is used for logging, don't forget to flush your output. 61 | 62 | Reset this to null to go back to Scriptlike's default of "echo to stdout" again. 63 | 64 | Note, setting this does not automatically enable echoing. You still need to 65 | set either scriptlikeEcho or scriptlikeDryRun to true. 66 | +/ 67 | void delegate(string) scriptlikeCustomEcho; 68 | 69 | /++ 70 | Output text lazily through scriptlike's echo logger. 71 | Does nothing if scriptlikeEcho and scriptlikeDryRun are both false. 72 | 73 | The yapFunc version automatically prepends the output with the 74 | name of the calling function. Ex: 75 | 76 | ---------------- 77 | void foo(int i = 42) { 78 | // Outputs: 79 | // foo: i = 42 80 | yapFunc("i = ", i); 81 | } 82 | ---------------- 83 | +/ 84 | void yap(T...)(lazy T args) 85 | { 86 | import std.stdio; 87 | 88 | if(scriptlikeEcho || scriptlikeDryRun) 89 | { 90 | if(scriptlikeCustomEcho) 91 | scriptlikeCustomEcho(text(args)); 92 | else 93 | { 94 | writeln(args); 95 | stdout.flush(); 96 | } 97 | } 98 | } 99 | 100 | ///ditto 101 | void yapFunc(string funcName=__FUNCTION__, T...)(lazy T args) 102 | { 103 | static assert(funcName != ""); 104 | 105 | auto funcNameSimple = funcName.split(".")[$-1]; 106 | yap(funcNameSimple, ": ", args); 107 | } 108 | 109 | /// Maintained for backwards-compatibility. Will be deprecated. 110 | /// Use 'yap' instead. 111 | void echoCommand(lazy string msg) 112 | { 113 | yap(msg); 114 | } 115 | 116 | /++ 117 | Interpolated string (ie, variable expansion). 118 | 119 | Any D expression can be placed inside ${ and }. Everything between the curly 120 | braces will be evaluated inside your current scope, and passed as a parameter 121 | (or parameters) to std.conv.text. 122 | 123 | The curly braces do NOT nest, so variable expansion will end at the first 124 | closing brace. If the closing brace is missing, an Exception will be thrown 125 | at compile-time. 126 | 127 | Example: 128 | ------------ 129 | // Output: The number 21 doubled is 42! 130 | int num = 21; 131 | writeln( mixin(interp!"The number ${num} doubled is ${num * 2}!") ); 132 | 133 | // Output: Empty braces output nothing. 134 | writeln( mixin(interp!"Empty ${}braces ${}output nothing.") ); 135 | 136 | // Output: Multiple params: John Doe. 137 | auto first = "John", last = "Doe"; 138 | writeln( mixin(interp!`Multiple params: ${first, " ", last}.`) ); 139 | ------------ 140 | +/ 141 | string interp(string str)() 142 | { 143 | enum State 144 | { 145 | normal, 146 | dollar, 147 | code, 148 | } 149 | 150 | auto state = State.normal; 151 | 152 | string buf; 153 | buf ~= '`'; 154 | 155 | foreach(char c; str) 156 | final switch(state) 157 | { 158 | case State.normal: 159 | if(c == '$') 160 | // Delay copying the $ until we find out whether it's 161 | // the start of an escape sequence. 162 | state = State.dollar; 163 | else if(c == '`') 164 | buf ~= "`~\"`\"~`"; 165 | else 166 | buf ~= c; 167 | break; 168 | 169 | case State.dollar: 170 | if(c == '{') 171 | { 172 | state = State.code; 173 | buf ~= "`~_interp_text("; 174 | } 175 | else if(c == '$') 176 | buf ~= '$'; // Copy the previous $ 177 | else 178 | { 179 | buf ~= '$'; // Copy the previous $ 180 | buf ~= c; 181 | state = State.normal; 182 | } 183 | break; 184 | 185 | case State.code: 186 | if(c == '}') 187 | { 188 | buf ~= ")~`"; 189 | state = State.normal; 190 | } 191 | else 192 | buf ~= c; 193 | break; 194 | } 195 | 196 | // Finish up 197 | final switch(state) 198 | { 199 | case State.normal: 200 | buf ~= '`'; 201 | break; 202 | 203 | case State.dollar: 204 | buf ~= "$`"; // Copy the previous $ 205 | break; 206 | 207 | case State.code: 208 | throw new Exception( 209 | "Interpolated string contains an unterminated expansion. "~ 210 | "You're missing a closing curly brace." 211 | ); 212 | } 213 | 214 | return buf; 215 | } 216 | string _interp_text(T...)(T args) 217 | { 218 | static if(T.length == 0) 219 | return null; 220 | else 221 | return std.conv.text(args); 222 | } 223 | 224 | version(unittest_scriptlike_d) 225 | unittest 226 | { 227 | import std.stdio; 228 | writeln("Running Scriptlike unittests: interp"); stdout.flush(); 229 | 230 | assert(mixin(interp!"hello") == "hello"); 231 | assert(mixin(interp!"$") == "$"); 232 | 233 | int num = 21; 234 | assert( 235 | mixin(interp!"The number ${num} doubled is ${num * 2}!") == 236 | "The number 21 doubled is 42!" 237 | ); 238 | 239 | assert( 240 | mixin(interp!"Empty ${}braces ${}output nothing.") == 241 | "Empty braces output nothing." 242 | ); 243 | 244 | auto first = "John", last = "Doe"; 245 | assert( 246 | mixin(interp!`Multiple params: ${first, " ", last}.`) == 247 | "Multiple params: John Doe." 248 | ); 249 | } 250 | 251 | immutable gagEcho = q{ 252 | auto _gagEcho_saveCustomEcho = scriptlikeCustomEcho; 253 | 254 | scriptlikeCustomEcho = delegate(string str) {}; 255 | scope(exit) 256 | scriptlikeCustomEcho = _gagEcho_saveCustomEcho; 257 | }; 258 | 259 | version(unittest_scriptlike_d) 260 | unittest 261 | { 262 | import std.stdio; 263 | writeln("Running Scriptlike unittests: gagecho"); stdout.flush(); 264 | 265 | // Test 1 266 | scriptlikeEcho = true; 267 | scriptlikeDryRun = true; 268 | scriptlikeCustomEcho = null; 269 | { 270 | mixin(gagEcho); 271 | assert(scriptlikeEcho == true); 272 | assert(scriptlikeDryRun == true); 273 | assert(scriptlikeCustomEcho != null); 274 | } 275 | assert(scriptlikeEcho == true); 276 | assert(scriptlikeDryRun == true); 277 | assert(scriptlikeCustomEcho == null); 278 | 279 | // Test 2 280 | scriptlikeEcho = false; 281 | scriptlikeDryRun = false; 282 | scriptlikeCustomEcho = null; 283 | { 284 | mixin(gagEcho); 285 | assert(scriptlikeEcho == false); 286 | assert(scriptlikeDryRun == false); 287 | assert(scriptlikeCustomEcho != null); 288 | } 289 | assert(scriptlikeEcho == false); 290 | assert(scriptlikeDryRun == false); 291 | assert(scriptlikeCustomEcho == null); 292 | 293 | // Test 3 294 | void testEcho(string str) 295 | { 296 | import std.stdio; 297 | writeln(str); 298 | } 299 | scriptlikeEcho = false; 300 | scriptlikeDryRun = false; 301 | scriptlikeCustomEcho = &testEcho; 302 | { 303 | mixin(gagEcho); 304 | assert(scriptlikeEcho == false); 305 | assert(scriptlikeDryRun == false); 306 | assert(scriptlikeCustomEcho != null); 307 | assert(scriptlikeCustomEcho != &testEcho); 308 | } 309 | assert(scriptlikeEcho == false); 310 | assert(scriptlikeDryRun == false); 311 | assert(scriptlikeCustomEcho == &testEcho); 312 | } 313 | 314 | /++ 315 | Debugging aid: Output current file/line to stderr. 316 | 317 | Also flushes stderr to ensure buffering and a subsequent crash don't 318 | cause the message to get lost. 319 | 320 | Example: 321 | -------- 322 | // Output example: 323 | // src/myproj/myfile.d(42): trace 324 | trace(); 325 | -------- 326 | +/ 327 | template trace() 328 | { 329 | void trace(string file = __FILE__, size_t line = __LINE__)() 330 | { 331 | stderr.writeln(file, "(", line, "): trace"); 332 | stderr.flush(); 333 | } 334 | } 335 | 336 | /++ 337 | Debugging aid: Output variable name/value and file/line info to stderr. 338 | 339 | Also flushes stderr to ensure buffering and a subsequent crash don't 340 | cause the message to get lost. 341 | 342 | Example: 343 | -------- 344 | auto x = 5; 345 | auto str = "Hello"; 346 | 347 | // Output example: 348 | // src/myproj/myfile.d(42): x: 5 349 | // src/myproj/myfile.d(43): str: Hello 350 | trace!x; 351 | trace!str; 352 | -------- 353 | +/ 354 | template trace(alias var) 355 | { 356 | void trace(string file = __FILE__, size_t line = __LINE__)() 357 | { 358 | stderr.writeln(file, "(", line, "): ", var.stringof, ": ", var); 359 | stderr.flush(); 360 | } 361 | } 362 | 363 | // Some tools for Scriptlike's unittests 364 | version(unittest_scriptlike_d) 365 | { 366 | version(Posix) enum pwd = "pwd"; 367 | else version(Windows) enum pwd = "cd"; 368 | else static assert(0); 369 | 370 | version(Posix) enum quiet = " >/dev/null 2>/dev/null"; 371 | else version(Windows) enum quiet = " > NUL 2> NUL"; 372 | else static assert(0); 373 | 374 | string openSandbox(string func=__FUNCTION__)() 375 | { 376 | import scriptlike.file.wrappers; 377 | import scriptlike.file.extras; 378 | import scriptlike.path; 379 | 380 | // Space in path is deliberate 381 | auto sandboxDir = tempDir() ~ "scriptlike-d/test sandboxes" ~ func; 382 | //import std.stdio; writeln("sandboxDir: ", sandboxDir.raw); 383 | 384 | tryRmdirRecurse(sandboxDir); 385 | mkdirRecurse(sandboxDir); 386 | chdir(sandboxDir); 387 | return sandboxDir.raw; 388 | } 389 | 390 | enum useSandbox = q{ 391 | import std.stdio; 392 | 393 | auto oldCwd = std.file.getcwd(); 394 | auto sandboxDir = openSandbox(); 395 | scope(success) // Don't cleanup upon failure, so the remains can be manually insepcted. 396 | tryRmdirRecurse(sandboxDir); 397 | scope(failure) 398 | writeln("Sandbox directory: '", sandboxDir, "'"); 399 | scope(exit) 400 | std.file.chdir(oldCwd); 401 | }; 402 | 403 | immutable initTest(string testName, string msg = null, string module_ = __MODULE__) = ` 404 | import std.stdio: writeln; 405 | import std.exception; 406 | import core.exception; 407 | import scriptlike.core; 408 | 409 | writeln("Testing `~module_~`: `~testName~`"); 410 | scriptlikeEcho = false; 411 | scriptlikeDryRun = false; 412 | scriptlikeCustomEcho = null; 413 | `; 414 | 415 | // Generate a temporary filepath unique to the current process and current 416 | // unittest block. Takes optional id number and path suffix. 417 | // Guaranteed not to already exist. 418 | // 419 | // Path received can be used as either a file or dir, doesn't matter. 420 | string tmpName(string id = null, string suffix = null, string func = __FUNCTION__) 421 | out(result) 422 | { 423 | assert(!std.file.exists(result)); 424 | } 425 | body 426 | { 427 | import std.conv : text; 428 | import std.process : thisProcessID; 429 | 430 | // Include some spaces in the path, too: 431 | auto withoutSuffix = std.path.buildPath( 432 | std.file.tempDir(), 433 | text("deleteme.script like.unit test.pid", thisProcessID, ".", func, ".", id) 434 | ); 435 | unittest_tryRemovePath(withoutSuffix); 436 | 437 | // Add suffix 438 | return std.path.buildPath(withoutSuffix, suffix); 439 | } 440 | 441 | // Get a unique temp pathname (guaranteed not to exist or collide), and 442 | // clean up at the end up scope, deleting it if it exists. 443 | // Path received can be used as either a file or dir, doesn't matter. 444 | immutable useTmpName(string name, string suffix=null) = 445 | name~" = tmpName(`"~name~"`, `"~suffix~"`); 446 | scope(exit) unittest_tryRemovePath(tmpName(`"~name~"`)); 447 | "; 448 | 449 | // Delete if it already exists, regardless of whether it's a file or directory. 450 | // Just like `tryRemovePath`, but intentionally ignores echo and dryrun modes. 451 | void unittest_tryRemovePath(string path) 452 | out 453 | { 454 | assert(!std.file.exists(path)); 455 | } 456 | body 457 | { 458 | if(std.file.exists(path)) 459 | { 460 | if(std.file.isDir(path)) 461 | std.file.rmdirRecurse(path); 462 | else 463 | std.file.remove(path); 464 | } 465 | } 466 | 467 | immutable checkResult = q{ 468 | if(scriptlikeDryRun) 469 | checkPre(); 470 | else 471 | checkPost(); 472 | }; 473 | 474 | // Runs the provided test in both normal and dryrun modes. 475 | // The provided test can read scriptlikeDryRun and assert appropriately. 476 | // 477 | // Automatically ensures the test echoes in the echo and dryrun modes, 478 | // and doesn't echo otherwise. 479 | void testFileOperation(string funcName, string msg = null, string module_ = __MODULE__) 480 | (void delegate() test) 481 | { 482 | static import std.stdio; 483 | import std.stdio : writeln, stdout; 484 | import std.algorithm : canFind; 485 | 486 | string capturedEcho; 487 | void captureEcho(string str) 488 | { 489 | capturedEcho ~= '\n'; 490 | capturedEcho ~= str; 491 | } 492 | 493 | auto originalCurrentDir = std.file.getcwd(); 494 | 495 | scope(exit) 496 | { 497 | scriptlikeEcho = false; 498 | scriptlikeDryRun = false; 499 | scriptlikeCustomEcho = null; 500 | } 501 | 502 | // Test normally 503 | { 504 | std.stdio.write("Testing ", module_, ".", funcName, (msg? ": " : ""), msg, "\t[normal]"); 505 | stdout.flush(); 506 | scriptlikeEcho = false; 507 | scriptlikeDryRun = false; 508 | capturedEcho = null; 509 | scriptlikeCustomEcho = &captureEcho; 510 | 511 | scope(failure) writeln(); 512 | scope(exit) std.file.chdir(originalCurrentDir); 513 | test(); 514 | assert( 515 | capturedEcho == "", 516 | "Expected the test not to echo, but it echoed this:\n------------\n"~capturedEcho~"------------" 517 | ); 518 | } 519 | 520 | // Test in echo mode 521 | { 522 | std.stdio.write(" [echo]"); 523 | stdout.flush(); 524 | scriptlikeEcho = true; 525 | scriptlikeDryRun = false; 526 | capturedEcho = null; 527 | scriptlikeCustomEcho = &captureEcho; 528 | 529 | scope(failure) writeln(); 530 | scope(exit) std.file.chdir(originalCurrentDir); 531 | test(); 532 | assert(capturedEcho != "", "Expected the test to echo, but it didn't."); 533 | assert( 534 | capturedEcho.canFind("\n"~funcName~": "), 535 | "Couldn't find '"~funcName~": ' in test's echo output:\n------------\n"~capturedEcho~"------------" 536 | ); 537 | } 538 | 539 | // Test in dry run mode 540 | { 541 | std.stdio.write(" [dryrun]"); 542 | stdout.flush(); 543 | scriptlikeEcho = false; 544 | scriptlikeDryRun = true; 545 | capturedEcho = null; 546 | scriptlikeCustomEcho = &captureEcho; 547 | 548 | scope(failure) writeln(); 549 | scope(exit) std.file.chdir(originalCurrentDir); 550 | test(); 551 | assert(capturedEcho != "", "Expected the test to echo, but it didn't."); 552 | assert( 553 | capturedEcho.canFind("\n"~funcName~": "), 554 | "Couldn't find '"~funcName~": ' in the test's echo output:\n------------"~capturedEcho~"------------" 555 | ); 556 | } 557 | 558 | writeln(); 559 | } 560 | 561 | unittest 562 | { 563 | mixin(initTest!"testFileOperation"); 564 | 565 | testFileOperation!("testFileOperation", "Echo works 1")(() { 566 | void testFileOperation() 567 | { 568 | yapFunc(); 569 | } 570 | testFileOperation(); 571 | }); 572 | 573 | testFileOperation!("testFileOperation", "Echo works 2")(() { 574 | if(scriptlikeEcho) scriptlikeCustomEcho("testFileOperation: "); 575 | else if(scriptlikeDryRun) scriptlikeCustomEcho("testFileOperation: "); 576 | else {} 577 | }); 578 | 579 | { 580 | auto countNormal = 0; 581 | auto countEcho = 0; 582 | auto countDryRun = 0; 583 | testFileOperation!("testFileOperation", "Gets run in each mode")(() { 584 | if(scriptlikeEcho) 585 | { 586 | countEcho++; 587 | scriptlikeCustomEcho("testFileOperation: "); 588 | } 589 | else if(scriptlikeDryRun) 590 | { 591 | countDryRun++; 592 | scriptlikeCustomEcho("testFileOperation: "); 593 | } 594 | else 595 | countNormal++; 596 | }); 597 | assert(countNormal == 1); 598 | assert(countEcho == 1); 599 | assert(countDryRun == 1); 600 | } 601 | 602 | assertThrown!AssertError( 603 | testFileOperation!("testFileOperation", "Echoing even with both echo and dryrun disabled")(() { 604 | scriptlikeCustomEcho("testFileOperation: "); 605 | }) 606 | ); 607 | 608 | assertThrown!AssertError( 609 | testFileOperation!("testFileOperation", "No echo in echo mode")(() { 610 | if(scriptlikeEcho) {} 611 | else if(scriptlikeDryRun) scriptlikeCustomEcho("testFileOperation: "); 612 | else {} 613 | }) 614 | ); 615 | 616 | assertThrown!AssertError( 617 | testFileOperation!("testFileOperation", "No echo in dryrun mode")(() { 618 | if(scriptlikeEcho) scriptlikeCustomEcho("testFileOperation: "); 619 | else if(scriptlikeDryRun) {} 620 | else {} 621 | }) 622 | ); 623 | } 624 | } 625 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Scriptlike [![Build Status](https://travis-ci.org/Abscissa/scriptlike.svg?branch=master)](https://travis-ci.org/Abscissa/scriptlike) [![Build status](https://ci.appveyor.com/api/projects/status/yvel4q7dekn0k1cb/branch/master?svg=true)](https://ci.appveyor.com/project/Abscissa/scriptlike/branch/master) 2 | ========== 3 | 4 | Scriptlike is a utility library to help you write script-like programs in the 5 | [D Programming Language](http://dlang.org). 6 | 7 | Officially supported compiler versions are shown in 8 | [.travis.yml](https://github.com/Abscissa/scriptlike/blob/master/.travis.yml). 9 | 10 | Links: 11 | * [How to Use Scriptlike in Scripts](https://github.com/Abscissa/scriptlike/blob/master/USAGE.md) 12 | * [API Reference](http://semitwist.com/scriptlike) 13 | * [Changelog](http://semitwist.com/scriptlike/changelog.html) 14 | * [DUB](http://code.dlang.org/about) [Package](http://code.dlang.org/packages/scriptlike) 15 | * [Small article explaining the original motivations behind scriptlike](http://semitwist.com/articles/article/view/scriptlike-shell-scripting-in-d-annoyances-and-a-library-solution) 16 | 17 | Sections 18 | -------- 19 | 20 | **[Features](#features)** 21 | * [Automatic Phobos Import](#automatic-phobos-import) 22 | * [User Input Prompts](#user-input-prompts) 23 | * [String Interpolation](#string-interpolation) 24 | * [Filepaths](#filepaths) 25 | * [Try/As Filesystem Operations](#tryas-filesystem-operations) 26 | * [Script-Style Shell Commands](#script-style-shell-commands) 27 | * [Command Echoing](#command-echoing) 28 | * [Dry Run Assistance](#dry-run-assistance) 29 | * [Fail](#fail) 30 | 31 | **[Disambiguating write and write](#disambiguating-write-and-write)** 32 | 33 | Features 34 | -------- 35 | 36 | ### Automatic Phobos Import 37 | 38 | For most typical Phobos modules. Unless you 39 | [don't want to](http://semitwist.com/scriptlike/scriptlike/only.html). 40 | Who needs rows and rows of standard lib imports for a mere script? 41 | 42 | ```d 43 | import scriptlike; 44 | //import scriptlike.only; // In case you don't want Phobos auto-imported 45 | void main() { 46 | writeln("Works!"); 47 | } 48 | ``` 49 | 50 | See: [```scriptlike```](https://github.com/Abscissa/scriptlike/blob/examples/src/scriptlike/package.d), 51 | [```scriptlike.only```](https://github.com/Abscissa/scriptlike/blob/examples/src/scriptlike/only.d), 52 | [```scriptlike.std```](https://github.com/Abscissa/scriptlike/blob/examples/src/scriptlike/std.d) 53 | 54 | ### User Input Prompts 55 | 56 | Easy prompting for and verifying command-line user input with the 57 | [```interact```](http://semitwist.com/scriptlike/scriptlike/interact.html) module: 58 | 59 | ```d 60 | auto name = userInput!string("Please enter your name"); 61 | auto age = userInput!int("And your age"); 62 | 63 | if(userInput!bool("Do you want to continue?")) 64 | { 65 | string outputFolder = pathLocation("Where you do want to place the output?"); 66 | auto color = menu!string("What color would you like to use?", ["Blue", "Green"]); 67 | } 68 | 69 | auto num = require!(int, "a > 0 && a <= 10")("Enter a number from 1 to 10"); 70 | 71 | pause(); // Prompt "Press Enter to continue..."; 72 | pause("Hit Enter again, dood!!"); 73 | ``` 74 | 75 | See: [```userInput```](http://semitwist.com/scriptlike/scriptlike/interact/userInput.html), 76 | [```pathLocation```](http://semitwist.com/scriptlike/scriptlike/interact/pathLocation.html), 77 | [```menu```](http://semitwist.com/scriptlike/scriptlike/interact/menu.html), 78 | [```require```](http://semitwist.com/scriptlike/scriptlike/interact/require.html), 79 | [```pause```](http://semitwist.com/scriptlike/scriptlike/interact/pause.html) 80 | 81 | ### String Interpolation 82 | 83 | Variable (and expression) expansion inside strings: 84 | 85 | ```d 86 | // Output: The number 21 doubled is 42! 87 | int num = 21; 88 | writeln( mixin(interp!"The number ${num} doubled is ${num * 2}!") ); 89 | 90 | // Output: Empty braces output nothing. 91 | writeln( mixin(interp!"Empty ${}braces ${}output nothing.") ); 92 | 93 | // Output: Multiple params: John Doe. 94 | auto first = "John", last = "Doe"; 95 | writeln( mixin(interp!`Multiple params: ${first, " ", last}.`) ); 96 | ``` 97 | 98 | See: [```interp```](http://semitwist.com/scriptlike\/scriptlike/core/interp.html) 99 | 100 | ### Filepaths 101 | 102 | Simple, reliable, cross-platform. No more worrying about slashes, paths-with-spaces, 103 | [buildPath](http://dlang.org/phobos/std_path.html#buildPath), 104 | [normalizing](http://dlang.org/phobos/std_path.html#buildNormalizedPath), 105 | or getting paths mixed up with ordinary strings: 106 | 107 | ```d 108 | // This is AUTOMATICALLY kept normalized (via std.path.buildNormalizedPath) 109 | auto dir = Path("foo/bar"); 110 | dir ~= "subdir"; // Append a subdirectory 111 | 112 | // No worries about trailing slashes! 113 | assert(Path("foo/bar") == Path("foo/bar/")); 114 | assert(Path("foo/bar/") == Path("foo/bar//")); 115 | 116 | 117 | // No worries about forward/backslashes! 118 | assert(dir == Path("foo/bar/subdir")); 119 | assert(dir == Path("foo\\bar\\subdir")); 120 | 121 | // No worries about spaces! 122 | auto file = dir.up ~ "different subdir\\Filename with spaces.txt"; 123 | assert(file == Path("foo/bar/different subdir/Filename with spaces.txt")); 124 | writeln(file); // Path.toString() always properly escapes for current platform! 125 | writeln(file.toRawString()); // Don't escape! 126 | 127 | // Even file extentions are type-safe! 128 | Ext ext = file.extension; 129 | auto anotherFile = Path("path/to/file") ~ ext; 130 | assert(anotherFile.baseName == Path("file.txt")); 131 | 132 | // std.path and std.file are wrapped to offer Path/Ext support 133 | assert(dirName(anotherFile) == Path("path/to")); 134 | copy(anotherFile, Path("target/path/new file.txt")); 135 | ``` 136 | 137 | See: [```Path```](http://semitwist.com/scriptlike/scriptlike/path/extras/Path.html), 138 | [```Path.toString```](http://semitwist.com/scriptlike/scriptlike/path/extras/Path.toString.html), 139 | [```Path.toRawString```](http://semitwist.com/scriptlike/scriptlike/path/extras/Path.toRawString.html), 140 | [```Path.up```](http://semitwist.com/scriptlike/scriptlike/path/extras/Path.up.html), 141 | [```Ext```](http://semitwist.com/scriptlike/scriptlike/path/extras/Ext.html), 142 | [```dirName```](http://semitwist.com/scriptlike/scriptlike/path/wrappers/dirName.html), 143 | [```copy```](http://semitwist.com/scriptlike/scriptlike/file/wrappers/copy.html), 144 | [```buildNormalizedPath```](http://dlang.org/phobos/std_path.html#buildNormalizedPath) 145 | 146 | ### Try/As Filesystem Operations 147 | 148 | Less pedantic, when you don't care if there's nothing to do: 149 | 150 | ```d 151 | // Just MAKE SURE this exists! If it's already there, then GREAT! 152 | tryMkdir("somedir"); 153 | assertThrown( mkdir("somedir") ); // Exception: Already exists! 154 | tryMkdir("somedir"); // Works fine! 155 | 156 | // Just MAKE SURE this is gone! If it's already gone, then GREAT! 157 | tryRmdir("somedir"); 158 | assertThrown( rmdir("somedir") ); // Exception: Already gone! 159 | tryRmdir("somedir"); // Works fine! 160 | 161 | // Just MAKE SURE it doesn't exist. Don't bother me if it doesn't! 162 | tryRemove("file"); 163 | 164 | // Copy if it exists, otherwise don't worry about it. 165 | tryCopy("file", "file-copy"); 166 | 167 | // Is this a directory? If it doesn't even exist, 168 | // then it's obviously NOT a directory. 169 | assertThrown( isDir("foo/bar") ); // Exception: Doesn't exist! 170 | if(existsAsDir("foo/bar")) // Works fine! 171 | {/+ ...do stuff... +/} 172 | 173 | // Bonus! Single function to delete files OR directories! 174 | writeFile("file.txt", "abc"); 175 | tryMkdirRecurse("foo/bar/dir"); 176 | writeFile("foo/bar/dir/file.txt", "123"); 177 | // Delete with the same function! 178 | removePath("file.txt"); // Calls 'remove' 179 | removePath("foo"); // Calls 'rmdirRecurse' 180 | tryRemovePath("file.txt"); // Also comes in try flavor! 181 | tryRemovePath("foo"); 182 | ``` 183 | 184 | See: [```tryMkdir```](http://semitwist.com/scriptlike/scriptlike/file/extras/tryMkdir.html), 185 | [```mkdir```](http://semitwist.com/scriptlike/scriptlike/file/wrappers/mkdir.html), 186 | [```tryMkdirRecurse```](http://semitwist.com/scriptlike/scriptlike/file/extras/tryMkdirRecurse.html), 187 | [```mkdir```](http://semitwist.com/scriptlike/scriptlike/file/wrappers/mkdir.html), 188 | [```tryRmdir```](http://semitwist.com/scriptlike/scriptlike/file/extras/tryRmdir.html), 189 | [```rmdir```](http://semitwist.com/scriptlike/scriptlike/file/wrappers/rmdir.html), 190 | [```tryRemove```](http://semitwist.com/scriptlike/scriptlike/file/extras/tryRemove.html), 191 | [```tryCopy```](http://semitwist.com/scriptlike/scriptlike/file/extras/tryCopy.html), 192 | [```existsAsDir```](http://semitwist.com/scriptlike/scriptlike/file/extras/existsAsDir.html), 193 | [```removePath```](http://semitwist.com/scriptlike/scriptlike/file/extras/removePath.html), 194 | [```tryRemovePath```](http://semitwist.com/scriptlike/scriptlike/file/extras/tryRemovePath.html), 195 | [```writeFile```](http://semitwist.com/scriptlike/scriptlike/file/extras/writeFile.html) 196 | and [more...](http://semitwist.com/scriptlike/scriptlike/file/extras.html) 197 | 198 | ### Script-Style Shell Commands 199 | 200 | Invoke a command script-style: synchronously with forwarded stdout/in/err 201 | from any working directory. Or capture the output instead. Automatically 202 | throw on non-zero status code if you want. 203 | 204 | One simple call, [```run```](http://semitwist.com/scriptlike/scriptlike/process/run.html), 205 | to run a shell command script-style (ie, synchronously with forwarded stdout/in/err) 206 | from any working directory, and automatically throw if it fails. Or 207 | [```runCollect```](http://semitwist.com/scriptlike/scriptlike/process/runCollect.html) 208 | to capture the output instead of displaying it. Or 209 | [```tryRun```](http://semitwist.com/scriptlike/scriptlike/process/tryRun.html)/[```tryRunCollect```](http://semitwist.com/scriptlike/scriptlike/process/tryRunCollect.html) 210 | if you want to receive the status code instead of automatically throwing on non-zero. 211 | 212 | ```d 213 | run("dmd --help"); // Display DMD help screen 214 | pause(); // Wait for user to hit Enter 215 | 216 | // Automatically throws ErrorLevelException(1, "dmd --bad-flag") 217 | assertThrown!ErrorLevelException( run("dmd --bad-flag") ); 218 | 219 | // Automatically throws ErrorLevelException(-1, "this-cmd-does-not-exist") 220 | assertThrown!ErrorLevelException( run("this-cmd-does-not-exist") ); 221 | 222 | // Don't bail on error 223 | int statusCode = tryRun("dmd --bad-flag"); 224 | 225 | // Collect output instead of showing it 226 | string dmdHelp = runCollect("dmd --help"); 227 | auto isDMD_2_068_1 = dmdHelp.canFind("D Compiler v2.068.1"); 228 | 229 | // Don't bail on error 230 | auto result = tryRunCollect("dmd --help"); 231 | if(result.status == 0 && result.output.canFind("D Compiler v2.068.1")) 232 | writeln("Found DMD v2.068.1!"); 233 | 234 | // Use any working directory: 235 | auto myProjectDir = Path("my/proj/dir"); 236 | auto mainFile = Path("src/main.d"); 237 | myProjectDir.run(text("dmd ", mainFile, " -O")); // mainFile is properly escaped! 238 | 239 | // Verify it actually IS running from a different working directory: 240 | version(Posix) enum pwd = "pwd"; 241 | else version(Windows) enum pwd = "cd"; 242 | else static assert(0); 243 | auto output = myProjectDir.runCollect(pwd); 244 | auto expected = getcwd() ~ myProjectDir; 245 | assert( Path(output.strip()) == expected ); 246 | ``` 247 | 248 | See: [```run```](http://semitwist.com/scriptlike/scriptlike/process/run.html), 249 | [```tryRun```](http://semitwist.com/scriptlike/scriptlike/process/tryRun.html), 250 | [```runCollect```](http://semitwist.com/scriptlike/scriptlike/process/runCollect.html), 251 | [```tryRunCollect```](http://semitwist.com/scriptlike/scriptlike/process/tryRunCollect.html), 252 | [```pause```](http://semitwist.com/scriptlike/scriptlike/interact/pause.html), 253 | [```Path```](http://semitwist.com/scriptlike/scriptlike/path/extras/Path.html), 254 | [```getcwd```](http://semitwist.com/scriptlike/scriptlike/file/wrappers/getcwd.html), 255 | [```ErrorLevelException```](http://semitwist.com/scriptlike/scriptlike/process/ErrorLevelException.html), 256 | [```assertThrown```](http://dlang.org/phobos/std_exception.html#assertThrown), 257 | [```canFind```](http://dlang.org/phobos/std_algorithm_searching.html#.canFind), 258 | [```text```](http://dlang.org/phobos/std_conv.html#text), 259 | [```strip```](http://dlang.org/phobos/std_string.html#.strip) 260 | 261 | ### Command Echoing 262 | 263 | Optionally enable automatic command echoing (including shell commands, 264 | changing/creating directories and deleting/copying/moving/linking/renaming 265 | both directories and files) by setting one simple flag: 266 | [```bool scriptlikeEcho```](http://semitwist.com/scriptlike/scriptlike/core/scriptlikeEcho.html) 267 | 268 | Echoing can be customized via 269 | [```scriptlikeCustomEcho```](http://semitwist.com/scriptlike/scriptlike/core/scriptlikeCustomEcho.html). 270 | 271 | ```d 272 | /++ 273 | Output: 274 | -------- 275 | run: echo Hello > file.txt 276 | mkdirRecurse: some/new/dir 277 | copy: file.txt -> 'some/new/dir/target name.txt' 278 | Gonna run foo() now... 279 | foo: i = 42 280 | -------- 281 | +/ 282 | 283 | scriptlikeEcho = true; // Enable automatic echoing 284 | 285 | run("echo Hello > file.txt"); 286 | 287 | auto newDir = Path("some/new/dir"); 288 | mkdirRecurse(newDir.toRawString()); // Even works with non-Path overloads 289 | copy("file.txt", newDir ~ "target name.txt"); 290 | 291 | void foo(int i = 42) { 292 | yapFunc("i = ", i); // Evaluated lazily 293 | } 294 | 295 | // yap and yapFunc ONLY output when echoing is enabled 296 | yap("Gonna run foo() now..."); 297 | foo(); 298 | ``` 299 | 300 | See: [```scriptlikeEcho```](http://semitwist.com/scriptlike/scriptlike/core/scriptlikeEcho.html), 301 | [```yap```](http://semitwist.com/scriptlike/scriptlike/core/yap.html), 302 | [```yapFunc```](http://semitwist.com/scriptlike/scriptlike/core/yapFunc.html), 303 | [```run```](http://semitwist.com/scriptlike/scriptlike/process/run.html), 304 | [```Path```](http://semitwist.com/scriptlike/scriptlike/path/extras/Path.html), 305 | [```Path.toRawString```](http://semitwist.com/scriptlike/scriptlike/path/extras/Path.toRawString.html), 306 | [```mkdirRecurse```](http://semitwist.com/scriptlike/scriptlike/file/wrappers/mkdirRecurse.html), 307 | [```copy```](http://semitwist.com/scriptlike/scriptlike/file/wrappers/copy.html) 308 | 309 | ### Dry Run Assistance 310 | 311 | Scriptlike can help you create a dry-run mode, by automatically echoing (even if 312 | [```scriptlikeEcho```](http://semitwist.com/scriptlike/scriptlike/core/scriptlikeEcho.html) 313 | is disabled) and disabling all functions that 314 | [launch external commands](http://semitwist.com/scriptlike/scriptlike/process.html) 315 | or [modify the filesystem](http://semitwist.com/scriptlike/scriptlike/file.html). 316 | Just enable the 317 | [```scriptlikeDryRun```](http://semitwist.com/scriptlike/scriptlike/core/scriptlikeDryRun.html) flag. 318 | 319 | Note, if you choose to use this, you still must ensure your program logic 320 | behaves sanely in dry-run mode. 321 | 322 | ```d 323 | scriptlikeDryRun = true; 324 | 325 | // When dry-run is enabled, this echoes but doesn't actually copy or invoke DMD. 326 | copy("original.d", "app.d"); 327 | run("dmd app.d -ofbin/app"); 328 | 329 | // Works fine in dry-run, since it doesn't modify the filesystem. 330 | bool isItThere = exists("another-file"); 331 | 332 | if(!scriptlikeDryRun) 333 | { 334 | // This won't work right if we're running in dry-run mode, 335 | // since it'll be out-of-date, if it even exists at all. 336 | auto source = read("app.d"); 337 | } 338 | ``` 339 | 340 | See: [```scriptlikeDryRun```](http://semitwist.com/scriptlike/scriptlike/core/scriptlikeDryRun.html), 341 | [```copy```](semitwist.com/scriptlike/scriptlike/file/wrappers/copy.html), 342 | [```run```](http://semitwist.com/scriptlike/scriptlike/process/run.html), 343 | [```exists```](http://semitwist.com/scriptlike/scriptlike/file/wrappers/exists.html), 344 | [```read```](http://semitwist.com/scriptlike/scriptlike/file/wrappers/read.html) 345 | 346 | ### Fail 347 | 348 | Single function to bail out with an error message, exception-safe. 349 | 350 | ```d 351 | /++ 352 | Example: 353 | -------- 354 | $ test 355 | test: ERROR: Need two args, not 0! 356 | $ test abc 123 357 | test: ERROR: First arg must be 'foobar', not 'abc'! 358 | -------- 359 | +/ 360 | 361 | import scriptlike; 362 | 363 | void main(string[] args) { 364 | helper(args); 365 | } 366 | 367 | // Throws a Fail exception on bad args: 368 | void helper(string[] args) { 369 | // Like std.exception.enforce, but bails with no ugly stack trace, 370 | // and if uncaught, outputs the program name and "ERROR: " 371 | failEnforce(args.length == 3, "Need two args, not ", args.length-1, "!"); 372 | 373 | if(args[1] != "foobar") 374 | fail("First arg must be 'foobar', not '", args[1], "'!"); 375 | } 376 | ``` 377 | 378 | See: [```fail```](http://semitwist.com/scriptlike/scriptlike/fail/fail.html), 379 | [```failEnforce```](http://semitwist.com/scriptlike/scriptlike/fail/failEnforce.html), 380 | [```Fail```](http://semitwist.com/scriptlike/scriptlike/fail/Fail.html) 381 | 382 | Disambiguating write and write 383 | ------------------------------ 384 | 385 | Since they're both imported by default, you may get symbol conflict errors 386 | when trying to use 387 | [```scriptlike.file.wrappers.write```](http://semitwist.com/scriptlike/scriptlike/file/wrappers/write.html) 388 | (which wraps [```std.file.write```](http://dlang.org/phobos/std_file.html#write)) 389 | or [```std.stdio.write```](http://dlang.org/phobos/std_stdio.html#.write). 390 | And unfortunately, DMD issue [#11847](https://issues.dlang.org/show_bug.cgi?id=11847) 391 | currently makes it impossible to use a qualified name lookup for 392 | [```scriptlike.file.wrappers.write```](http://semitwist.com/scriptlike/scriptlike/file/wrappers/write.html). 393 | 394 | Here's how to easily avoid symbol conflict errors with Scriptlike and ```write```: 395 | 396 | ```d 397 | // Save file 398 | write("filename.txt", "content"); // Error: Symbols conflict! 399 | // Change line above to... 400 | writeFile("filename.txt", "content"); // Convenience alias included in scriptlike 401 | 402 | // Output to stdout with no newline 403 | write("Hello ", "world"); // Error: Symbols conflict! 404 | // Change line above to... 405 | std.stdio.write("Hello ", "world"); 406 | // or... 407 | stdout.write("Hello ", "world"); 408 | ``` 409 | 410 | See: 411 | [```scriptlike.file.wrappers.writeFile```](http://semitwist.com/scriptlike/scriptlike/file/wrappers/writeFile.html), 412 | [```scriptlike.file.wrappers.readFile```](http://semitwist.com/scriptlike/scriptlike/file/wrappers/readFile.html), 413 | [```scriptlike.file.wrappers.write```](http://semitwist.com/scriptlike/scriptlike/file/wrappers/write.html), 414 | [```std.file.write```](http://dlang.org/phobos/std_file.html#write), 415 | [```std.stdio.write```](http://dlang.org/phobos/std_stdio.html#.write) 416 | -------------------------------------------------------------------------------- /src/scriptlike/file/extras.d: -------------------------------------------------------------------------------- 1 | /++ 2 | $(H2 Scriptlike $(SCRIPTLIKE_VERSION)) 3 | 4 | Extra Scriptlike-only functionality to complement $(MODULE_STD_FILE). 5 | 6 | Copyright: Copyright (C) 2014-2017 Nick Sabalausky 7 | License: zlib/libpng 8 | Authors: Nick Sabalausky 9 | +/ 10 | module scriptlike.file.extras; 11 | 12 | import std.algorithm; 13 | import std.datetime; 14 | import std.traits; 15 | import std.typecons; 16 | 17 | static import std.file; 18 | static import std.path; 19 | 20 | import scriptlike.core; 21 | import scriptlike.path; 22 | import scriptlike.file.wrappers; 23 | 24 | /// Checks if the path exists as a directory. 25 | /// 26 | /// This is like $(FULL_STD_FILE isDir), but returns false instead of 27 | /// throwing if the path doesn't exist. 28 | bool existsAsDir(in string path) @trusted 29 | { 30 | yapFunc(path.escapeShellArg()); 31 | return std.file.exists(path) && std.file.isDir(path); 32 | } 33 | ///ditto 34 | bool existsAsDir(in Path path) @trusted 35 | { 36 | return existsAsDir(path.raw); 37 | } 38 | 39 | version(unittest_scriptlike_d) 40 | unittest 41 | { 42 | string file, dir, notExist; 43 | 44 | testFileOperation!("existsAsDir", "string")(() { 45 | mixin(useTmpName!"file"); 46 | mixin(useTmpName!"dir"); 47 | mixin(useTmpName!"notExist"); 48 | std.file.write(file, "abc123"); 49 | std.file.mkdir(dir); 50 | 51 | assert( !existsAsDir(file) ); 52 | assert( existsAsDir(dir) ); 53 | assert( !existsAsDir(notExist) ); 54 | }); 55 | 56 | testFileOperation!("existsAsDir", "Path")(() { 57 | mixin(useTmpName!"file"); 58 | mixin(useTmpName!"dir"); 59 | mixin(useTmpName!"notExist"); 60 | std.file.write(file, "abc123"); 61 | std.file.mkdir(dir); 62 | 63 | assert( !existsAsDir(Path(file)) ); 64 | assert( existsAsDir(Path(dir)) ); 65 | assert( !existsAsDir(Path(notExist)) ); 66 | }); 67 | } 68 | 69 | /// Checks if the path exists as a file. 70 | /// 71 | /// This is like $(FULL_STD_FILE isFile), but returns false instead of 72 | /// throwing if the path doesn't exist. 73 | bool existsAsFile(in string path) @trusted 74 | { 75 | yapFunc(path.escapeShellArg()); 76 | return std.file.exists(path) && std.file.isFile(path); 77 | } 78 | ///ditto 79 | bool existsAsFile(in Path path) @trusted 80 | { 81 | return existsAsFile(path.raw); 82 | } 83 | 84 | version(unittest_scriptlike_d) 85 | unittest 86 | { 87 | string file, dir, notExist; 88 | 89 | testFileOperation!("existsAsFile", "string")(() { 90 | mixin(useTmpName!"file"); 91 | mixin(useTmpName!"dir"); 92 | mixin(useTmpName!"notExist"); 93 | std.file.write(file, "abc123"); 94 | std.file.mkdir(dir); 95 | 96 | assert( existsAsFile(file) ); 97 | assert( !existsAsFile(dir) ); 98 | assert( !existsAsFile(notExist) ); 99 | }); 100 | 101 | testFileOperation!("existsAsFile", "Path")(() { 102 | mixin(useTmpName!"file"); 103 | mixin(useTmpName!"dir"); 104 | mixin(useTmpName!"notExist"); 105 | std.file.write(file, "abc123"); 106 | std.file.mkdir(dir); 107 | 108 | assert( existsAsFile(Path(file)) ); 109 | assert( !existsAsFile(Path(dir)) ); 110 | assert( !existsAsFile(Path(notExist)) ); 111 | }); 112 | } 113 | 114 | /// Checks if the path exists as a symlink. 115 | /// 116 | /// This is like $(FULL_STD_FILE isSymlink), but returns false instead of 117 | /// throwing if the path doesn't exist. 118 | bool existsAsSymlink()(in string path) @trusted 119 | { 120 | yapFunc(path.escapeShellArg()); 121 | return std.file.exists(path) && std.file.isSymlink(path); 122 | } 123 | ///ditto 124 | bool existsAsSymlink(in Path path) @trusted 125 | { 126 | return existsAsSymlink(path.raw); 127 | } 128 | 129 | version(unittest_scriptlike_d) 130 | unittest 131 | { 132 | string file, dir, fileLink, dirLink, notExist; 133 | 134 | testFileOperation!("existsAsSymlink", "string")(() { 135 | mixin(useTmpName!"file"); 136 | mixin(useTmpName!"dir"); 137 | mixin(useTmpName!"fileLink"); 138 | mixin(useTmpName!"dirLink"); 139 | mixin(useTmpName!"notExist"); 140 | std.file.write(file, "abc123"); 141 | std.file.mkdir(dir); 142 | version(Posix) 143 | { 144 | std.file.symlink(file, fileLink); 145 | std.file.symlink(dir, dirLink); 146 | } 147 | 148 | assert( !existsAsSymlink(file) ); 149 | assert( !existsAsSymlink(dir) ); 150 | assert( !existsAsSymlink(notExist) ); 151 | version(Posix) 152 | { 153 | assert( existsAsSymlink(fileLink) ); 154 | assert( existsAsSymlink(dirLink) ); 155 | } 156 | }); 157 | 158 | testFileOperation!("existsAsSymlink", "Path")(() { 159 | mixin(useTmpName!"file"); 160 | mixin(useTmpName!"dir"); 161 | mixin(useTmpName!"fileLink"); 162 | mixin(useTmpName!"dirLink"); 163 | mixin(useTmpName!"notExist"); 164 | std.file.write(file, "abc123"); 165 | std.file.mkdir(dir); 166 | version(Posix) 167 | { 168 | std.file.symlink(file, fileLink); 169 | std.file.symlink(dir, dirLink); 170 | } 171 | 172 | assert( !existsAsSymlink(Path(file)) ); 173 | assert( !existsAsSymlink(Path(dir)) ); 174 | assert( !existsAsSymlink(Path(notExist)) ); 175 | version(Posix) 176 | { 177 | assert( existsAsSymlink(Path(fileLink)) ); 178 | assert( existsAsSymlink(Path(dirLink)) ); 179 | } 180 | }); 181 | } 182 | 183 | /// If 'from' exists, then rename. Otherwise, do nothing and return false. 184 | /// 185 | /// Supports Path and command echoing. 186 | /// 187 | /// Returns: Success? 188 | bool tryRename(T1, T2)(T1 from, T2 to) 189 | if( 190 | (is(T1==string) || is(T1==Path)) && 191 | (is(T2==string) || is(T2==Path)) 192 | ) 193 | { 194 | yapFunc(from.escapeShellArg(), " -> ", to.escapeShellArg()); 195 | mixin(gagEcho); 196 | 197 | if(from.exists()) 198 | { 199 | rename(from, to); 200 | return true; 201 | } 202 | 203 | return false; 204 | } 205 | 206 | version(unittest_scriptlike_d) 207 | unittest 208 | { 209 | string file1, file2, notExist1, notExist2; 210 | void checkPre() 211 | { 212 | assert(!std.file.exists(notExist1)); 213 | assert(!std.file.exists(notExist2)); 214 | 215 | assert(!std.file.exists(file2)); 216 | assert(std.file.exists(file1)); 217 | assert(std.file.isFile(file1)); 218 | assert(cast(string) std.file.read(file1) == "abc"); 219 | } 220 | 221 | void checkPost() 222 | { 223 | assert(!std.file.exists(notExist1)); 224 | assert(!std.file.exists(notExist2)); 225 | 226 | assert(!std.file.exists(file1)); 227 | assert(std.file.exists(file2)); 228 | assert(std.file.isFile(file2)); 229 | assert(cast(string) std.file.read(file2) == "abc"); 230 | } 231 | 232 | testFileOperation!("tryRename", "string,string")(() { 233 | mixin(useTmpName!"file1"); 234 | mixin(useTmpName!"file2"); 235 | mixin(useTmpName!"notExist1"); 236 | mixin(useTmpName!"notExist2"); 237 | std.file.write(file1, "abc"); 238 | 239 | checkPre(); 240 | assert( tryRename(file1, file2) ); 241 | assert( !tryRename(notExist1, notExist2) ); 242 | mixin(checkResult); 243 | }); 244 | 245 | testFileOperation!("tryRename", "string,Path")(() { 246 | mixin(useTmpName!"file1"); 247 | mixin(useTmpName!"file2"); 248 | mixin(useTmpName!"notExist1"); 249 | mixin(useTmpName!"notExist2"); 250 | std.file.write(file1, "abc"); 251 | 252 | checkPre(); 253 | assert( tryRename(file1, Path(file2)) ); 254 | assert( !tryRename(notExist1, Path(notExist2)) ); 255 | mixin(checkResult); 256 | }); 257 | 258 | testFileOperation!("tryRename", "Path,string")(() { 259 | mixin(useTmpName!"file1"); 260 | mixin(useTmpName!"file2"); 261 | mixin(useTmpName!"notExist1"); 262 | mixin(useTmpName!"notExist2"); 263 | std.file.write(file1, "abc"); 264 | 265 | checkPre(); 266 | assert( tryRename(Path(file1), file2) ); 267 | assert( !tryRename(Path(notExist1), notExist2) ); 268 | mixin(checkResult); 269 | }); 270 | 271 | testFileOperation!("tryRename", "Path,Path")(() { 272 | mixin(useTmpName!"file1"); 273 | mixin(useTmpName!"file2"); 274 | mixin(useTmpName!"notExist1"); 275 | mixin(useTmpName!"notExist2"); 276 | std.file.write(file1, "abc"); 277 | 278 | checkPre(); 279 | assert( tryRename(Path(file1), Path(file2)) ); 280 | assert( !tryRename(Path(notExist1), Path(notExist2)) ); 281 | mixin(checkResult); 282 | }); 283 | } 284 | 285 | /// If 'name' exists, then remove. Otherwise, do nothing and return false. 286 | /// 287 | /// Supports Path, command echoing and dryrun. 288 | /// 289 | /// Returns: Success? 290 | bool tryRemove(T)(T name) if(is(T==string) || is(T==Path)) 291 | { 292 | yapFunc(name.escapeShellArg()); 293 | mixin(gagEcho); 294 | 295 | if(name.exists()) 296 | { 297 | remove(name); 298 | return true; 299 | } 300 | 301 | return false; 302 | } 303 | 304 | version(unittest_scriptlike_d) 305 | unittest 306 | { 307 | string file, notExist; 308 | void checkPre() 309 | { 310 | assert(std.file.exists(file)); 311 | assert(std.file.isFile(file)); 312 | assert(cast(string) std.file.read(file) == "abc"); 313 | 314 | assert(!std.file.exists(notExist)); 315 | } 316 | 317 | void checkPost() 318 | { 319 | assert(!std.file.exists(file)); 320 | assert(!std.file.exists(notExist)); 321 | } 322 | 323 | testFileOperation!("tryRemove", "string")(() { 324 | mixin(useTmpName!"file"); 325 | mixin(useTmpName!"notExist"); 326 | std.file.write(file, "abc"); 327 | 328 | checkPre(); 329 | assert( tryRemove(file) ); 330 | assert( !tryRemove(notExist) ); 331 | mixin(checkResult); 332 | }); 333 | 334 | testFileOperation!("tryRemove", "Path")(() { 335 | mixin(useTmpName!"file"); 336 | mixin(useTmpName!"notExist"); 337 | std.file.write(file, "abc"); 338 | 339 | checkPre(); 340 | assert( tryRemove(Path(file)) ); 341 | assert( !tryRemove(Path(notExist)) ); 342 | mixin(checkResult); 343 | }); 344 | } 345 | 346 | /// If 'name' doesn't already exist, then mkdir. Otherwise, do nothing and return false. 347 | /// 348 | /// Supports Path and command echoing. 349 | /// 350 | /// Returns: Success? 351 | bool tryMkdir(T)(T name) if(is(T==string) || is(T==Path)) 352 | { 353 | yapFunc(name.escapeShellArg()); 354 | mixin(gagEcho); 355 | 356 | if(!name.exists()) 357 | { 358 | mkdir(name); 359 | return true; 360 | } 361 | 362 | return false; 363 | } 364 | 365 | version(unittest_scriptlike_d) 366 | unittest 367 | { 368 | string dir, alreadyExist; 369 | void checkPre() 370 | { 371 | assert(!std.file.exists(dir)); 372 | assert(std.file.exists(alreadyExist)); 373 | } 374 | 375 | void checkPost() 376 | { 377 | assert(std.file.exists(dir)); 378 | assert(std.file.isDir(dir)); 379 | assert(std.file.exists(alreadyExist)); 380 | } 381 | 382 | testFileOperation!("tryMkdir", "string")(() { 383 | mixin(useTmpName!"dir"); 384 | mixin(useTmpName!"alreadyExist"); 385 | std.file.mkdir(alreadyExist); 386 | 387 | checkPre(); 388 | assert( tryMkdir(dir) ); 389 | assert( !tryMkdir(alreadyExist) ); 390 | mixin(checkResult); 391 | }); 392 | 393 | testFileOperation!("tryMkdir", "Path")(() { 394 | mixin(useTmpName!"dir"); 395 | mixin(useTmpName!"alreadyExist"); 396 | std.file.mkdir(alreadyExist); 397 | 398 | checkPre(); 399 | assert( tryMkdir(Path(dir)) ); 400 | assert( !tryMkdir(Path(alreadyExist)) ); 401 | mixin(checkResult); 402 | }); 403 | } 404 | 405 | /// If 'name' doesn't already exist, then mkdirRecurse. Otherwise, do nothing and return false. 406 | /// 407 | /// Supports Path and command echoing. 408 | /// 409 | /// Returns: Success? 410 | bool tryMkdirRecurse(T)(T name) if(is(T==string) || is(T==Path)) 411 | { 412 | yapFunc(name.escapeShellArg()); 413 | mixin(gagEcho); 414 | 415 | if(!name.exists()) 416 | { 417 | mkdirRecurse(name); 418 | return true; 419 | } 420 | 421 | return false; 422 | } 423 | 424 | version(unittest_scriptlike_d) 425 | unittest 426 | { 427 | string dir, alreadyExist; 428 | void checkPre() 429 | { 430 | assert(!std.file.exists(dir)); 431 | assert(std.file.exists(alreadyExist)); 432 | } 433 | 434 | void checkPost() 435 | { 436 | assert(std.file.exists(dir)); 437 | assert(std.file.isDir(dir)); 438 | assert(std.file.exists(alreadyExist)); 439 | } 440 | 441 | testFileOperation!("tryMkdirRecurse", "string")(() { 442 | mixin(useTmpName!("dir", "subdir")); 443 | mixin(useTmpName!"alreadyExist"); 444 | std.file.mkdir(alreadyExist); 445 | 446 | checkPre(); 447 | assert( tryMkdirRecurse(dir) ); 448 | assert( !tryMkdirRecurse(alreadyExist) ); 449 | mixin(checkResult); 450 | }); 451 | 452 | testFileOperation!("tryMkdirRecurse", "Path")(() { 453 | mixin(useTmpName!("dir", "subdir")); 454 | mixin(useTmpName!"alreadyExist"); 455 | std.file.mkdir(alreadyExist); 456 | 457 | checkPre(); 458 | assert( tryMkdirRecurse(Path(dir)) ); 459 | assert( !tryMkdirRecurse(Path(alreadyExist)) ); 460 | mixin(checkResult); 461 | }); 462 | } 463 | 464 | /// If 'name' exists, then rmdir. Otherwise, do nothing and return false. 465 | /// 466 | /// Supports Path and command echoing. 467 | /// 468 | /// Returns: Success? 469 | bool tryRmdir(T)(T name) if(is(T==string) || is(T==Path)) 470 | { 471 | yapFunc(name.escapeShellArg()); 472 | mixin(gagEcho); 473 | 474 | if(name.exists()) 475 | { 476 | rmdir(name); 477 | return true; 478 | } 479 | 480 | return false; 481 | } 482 | 483 | version(unittest_scriptlike_d) 484 | unittest 485 | { 486 | string dir, notExist; 487 | void checkPre() 488 | { 489 | assert(std.file.exists(dir)); 490 | assert(std.file.isDir(dir)); 491 | } 492 | 493 | void checkPost() 494 | { 495 | assert(!std.file.exists(dir)); 496 | } 497 | 498 | testFileOperation!("tryRmdir", "string")(() { 499 | mixin(useTmpName!"dir"); 500 | mixin(useTmpName!"notExist"); 501 | std.file.mkdir(dir); 502 | 503 | checkPre(); 504 | assert( tryRmdir(dir) ); 505 | assert( !tryRmdir(notExist) ); 506 | mixin(checkResult); 507 | }); 508 | 509 | testFileOperation!("tryRmdir", "Path")(() { 510 | mixin(useTmpName!"dir"); 511 | mixin(useTmpName!"notExist"); 512 | std.file.mkdir(dir); 513 | 514 | checkPre(); 515 | assert( tryRmdir(Path(dir)) ); 516 | assert( !tryRmdir(Path(notExist)) ); 517 | mixin(checkResult); 518 | }); 519 | } 520 | 521 | version(docs_scriptlike_d) 522 | { 523 | /// Posix-only. If 'original' exists, then symlink. Otherwise, do nothing and return false. 524 | /// 525 | /// Supports Path and command echoing. 526 | /// 527 | /// Returns: Success? 528 | bool trySymlink(T1, T2)(T1 original, T2 link) 529 | if( 530 | (is(T1==string) || is(T1==Path)) && 531 | (is(T2==string) || is(T2==Path)) 532 | ); 533 | } 534 | else version(Posix) 535 | { 536 | bool trySymlink(T1, T2)(T1 original, T2 link) 537 | if( 538 | (is(T1==string) || is(T1==Path)) && 539 | (is(T2==string) || is(T2==Path)) 540 | ) 541 | { 542 | yapFunc("[original] ", original.escapeShellArg(), " : [symlink] ", link.escapeShellArg()); 543 | mixin(gagEcho); 544 | 545 | if(original.exists()) 546 | { 547 | symlink(original, link); 548 | return true; 549 | } 550 | 551 | return false; 552 | } 553 | 554 | version(unittest_scriptlike_d) 555 | unittest 556 | { 557 | string file, link, notExistFile, notExistLink; 558 | void checkPre() 559 | { 560 | assert(std.file.exists(file)); 561 | assert(std.file.isFile(file)); 562 | assert(cast(string) std.file.read(file) == "abc123"); 563 | 564 | assert(!std.file.exists(link)); 565 | 566 | assert(!std.file.exists(notExistFile)); 567 | assert(!std.file.exists(notExistLink)); 568 | } 569 | 570 | void checkPost() 571 | { 572 | assert(std.file.exists(file)); 573 | assert(std.file.isFile(file)); 574 | assert(cast(string) std.file.read(file) == "abc123"); 575 | 576 | assert(std.file.exists(link)); 577 | assert(std.file.isSymlink(link)); 578 | assert(std.file.readLink(link) == file); 579 | assert(cast(string) std.file.read(link) == "abc123"); 580 | 581 | assert(!std.file.exists(notExistFile)); 582 | assert(!std.file.exists(notExistLink)); 583 | } 584 | 585 | testFileOperation!("trySymlink", "string,string")(() { 586 | mixin(useTmpName!"file"); 587 | mixin(useTmpName!"link"); 588 | mixin(useTmpName!"notExistFile"); 589 | mixin(useTmpName!"notExistLink"); 590 | std.file.write(file, "abc123"); 591 | 592 | checkPre(); 593 | assert( trySymlink(file, link) ); 594 | assert( !trySymlink(notExistFile, notExistLink) ); 595 | mixin(checkResult); 596 | }); 597 | 598 | testFileOperation!("trySymlink", "string,Path")(() { 599 | mixin(useTmpName!"file"); 600 | mixin(useTmpName!"link"); 601 | mixin(useTmpName!"notExistFile"); 602 | mixin(useTmpName!"notExistLink"); 603 | std.file.write(file, "abc123"); 604 | 605 | checkPre(); 606 | assert( trySymlink(file, Path(link)) ); 607 | assert( !trySymlink(notExistFile, Path(notExistLink)) ); 608 | mixin(checkResult); 609 | }); 610 | 611 | testFileOperation!("trySymlink", "Path,string")(() { 612 | mixin(useTmpName!"file"); 613 | mixin(useTmpName!"link"); 614 | mixin(useTmpName!"notExistFile"); 615 | mixin(useTmpName!"notExistLink"); 616 | std.file.write(file, "abc123"); 617 | 618 | checkPre(); 619 | assert( trySymlink(Path(file), link) ); 620 | assert( !trySymlink(Path(notExistFile), notExistLink) ); 621 | mixin(checkResult); 622 | }); 623 | 624 | testFileOperation!("trySymlink", "Path,Path")(() { 625 | mixin(useTmpName!"file"); 626 | mixin(useTmpName!"link"); 627 | mixin(useTmpName!"notExistFile"); 628 | mixin(useTmpName!"notExistLink"); 629 | std.file.write(file, "abc123"); 630 | 631 | checkPre(); 632 | assert( trySymlink(Path(file), Path(link)) ); 633 | assert( !trySymlink(Path(notExistFile), Path(notExistLink)) ); 634 | mixin(checkResult); 635 | }); 636 | } 637 | } 638 | 639 | /// If 'from' exists, then copy. Otherwise, do nothing and return false. 640 | /// 641 | /// Supports Path and command echoing. 642 | /// 643 | /// Returns: Success? 644 | bool tryCopy(T1, T2)(T1 from, T2 to) 645 | if( 646 | (is(T1==string) || is(T1==Path)) && 647 | (is(T2==string) || is(T2==Path)) 648 | ) 649 | { 650 | yapFunc(from.escapeShellArg(), " -> ", to.escapeShellArg()); 651 | mixin(gagEcho); 652 | 653 | if(from.exists()) 654 | { 655 | copy(from, to); 656 | return true; 657 | } 658 | 659 | return false; 660 | } 661 | 662 | version(unittest_scriptlike_d) 663 | unittest 664 | { 665 | string file1, file2, notExist1, notExist2; 666 | void checkPre() 667 | { 668 | assert(!std.file.exists(notExist1)); 669 | assert(!std.file.exists(notExist2)); 670 | 671 | assert(std.file.exists(file1)); 672 | assert(std.file.isFile(file1)); 673 | assert(cast(string) std.file.read(file1) == "abc"); 674 | 675 | assert(!std.file.exists(file2)); 676 | } 677 | 678 | void checkPost() 679 | { 680 | assert(!std.file.exists(notExist1)); 681 | assert(!std.file.exists(notExist2)); 682 | 683 | assert(std.file.exists(file1)); 684 | assert(std.file.isFile(file1)); 685 | assert(cast(string) std.file.read(file1) == "abc"); 686 | 687 | assert(std.file.exists(file2)); 688 | assert(std.file.isFile(file2)); 689 | assert(cast(string) std.file.read(file2) == "abc"); 690 | } 691 | 692 | testFileOperation!("tryCopy", "string,string")(() { 693 | mixin(useTmpName!"file1"); 694 | mixin(useTmpName!"file2"); 695 | mixin(useTmpName!"notExist1"); 696 | mixin(useTmpName!"notExist2"); 697 | std.file.write(file1, "abc"); 698 | 699 | checkPre(); 700 | assert( tryCopy(file1, file2) ); 701 | assert( !tryCopy(notExist1, notExist2) ); 702 | mixin(checkResult); 703 | }); 704 | 705 | testFileOperation!("tryCopy", "string,Path")(() { 706 | mixin(useTmpName!"file1"); 707 | mixin(useTmpName!"file2"); 708 | mixin(useTmpName!"notExist1"); 709 | mixin(useTmpName!"notExist2"); 710 | std.file.write(file1, "abc"); 711 | 712 | checkPre(); 713 | assert( tryCopy(file1, Path(file2)) ); 714 | assert( !tryCopy(notExist1, Path(notExist2)) ); 715 | mixin(checkResult); 716 | }); 717 | 718 | testFileOperation!("tryCopy", "Path,string")(() { 719 | mixin(useTmpName!"file1"); 720 | mixin(useTmpName!"file2"); 721 | mixin(useTmpName!"notExist1"); 722 | mixin(useTmpName!"notExist2"); 723 | std.file.write(file1, "abc"); 724 | 725 | checkPre(); 726 | assert( tryCopy(Path(file1), file2) ); 727 | assert( !tryCopy(Path(notExist1), notExist2) ); 728 | mixin(checkResult); 729 | }); 730 | 731 | testFileOperation!("tryCopy", "Path,Path")(() { 732 | mixin(useTmpName!"file1"); 733 | mixin(useTmpName!"file2"); 734 | mixin(useTmpName!"notExist1"); 735 | mixin(useTmpName!"notExist2"); 736 | std.file.write(file1, "abc"); 737 | 738 | checkPre(); 739 | assert( tryCopy(Path(file1), Path(file2)) ); 740 | assert( !tryCopy(Path(notExist1), Path(notExist2)) ); 741 | mixin(checkResult); 742 | }); 743 | } 744 | 745 | /// If 'name' exists, then rmdirRecurse. Otherwise, do nothing and return false. 746 | /// 747 | /// Supports Path and command echoing. 748 | /// 749 | /// Returns: Success? 750 | bool tryRmdirRecurse(T)(T name) if(is(T==string) || is(T==Path)) 751 | { 752 | yapFunc(name.escapeShellArg()); 753 | mixin(gagEcho); 754 | 755 | if(name.exists()) 756 | { 757 | rmdirRecurse(name); 758 | return true; 759 | } 760 | 761 | return false; 762 | } 763 | 764 | version(unittest_scriptlike_d) 765 | unittest 766 | { 767 | string dir, notExist; 768 | void checkPre() 769 | { 770 | assert(std.file.exists(dir)); 771 | assert(std.file.isDir(dir)); 772 | 773 | assert(!std.file.exists( notExist )); 774 | } 775 | 776 | void checkPost() 777 | { 778 | assert(!std.file.exists( std.path.dirName(dir) )); 779 | 780 | assert(!std.file.exists( notExist )); 781 | } 782 | 783 | testFileOperation!("tryRmdirRecurse", "string")(() { 784 | mixin(useTmpName!("dir", "subdir")); 785 | mixin(useTmpName!"notExist"); 786 | std.file.mkdirRecurse(dir); 787 | 788 | checkPre(); 789 | assert(tryRmdirRecurse( std.path.dirName(dir) )); 790 | assert(!tryRmdirRecurse( notExist )); 791 | mixin(checkResult); 792 | }); 793 | 794 | testFileOperation!("tryRmdirRecurse", "Path")(() { 795 | mixin(useTmpName!("dir", "subdir")); 796 | mixin(useTmpName!"notExist"); 797 | std.file.mkdirRecurse(dir); 798 | 799 | checkPre(); 800 | assert(tryRmdirRecurse(Path( std.path.dirName(dir) ))); 801 | assert(!tryRmdirRecurse(Path( notExist ) )); 802 | mixin(checkResult); 803 | }); 804 | } 805 | 806 | /// Delete `name` regardless of whether it's a file or directory. 807 | /// If it's a directory, it's deleted recursively, via 808 | /// $(API_FILE_WRAP rmdirRecurse). Throws if the file/directory doesn't exist. 809 | /// 810 | /// If you just want to make sure a file/dir is gone, and don't care whether 811 | /// it already exists or not, consider using `tryRemovePath` instead. 812 | /// 813 | /// Supports Path and command echoing. 814 | void removePath(T)(T name) if(is(T==string) || is(T==Path)) 815 | { 816 | yapFunc(name.escapeShellArg()); 817 | 818 | if(name.exists() && name.isDir()) 819 | rmdirRecurse(name); 820 | else 821 | remove(name); 822 | } 823 | 824 | version(unittest_scriptlike_d) 825 | unittest 826 | { 827 | import std.exception; 828 | string file, dir, notExist; 829 | 830 | void checkPre() 831 | { 832 | assert(std.file.exists(file)); 833 | assert(std.file.isFile(file)); 834 | 835 | assert(std.file.exists(dir)); 836 | assert(std.file.isDir(dir)); 837 | 838 | assert(!std.file.exists( notExist )); 839 | } 840 | 841 | void checkPost() 842 | { 843 | assert(!std.file.exists( file )); 844 | assert(!std.file.exists( std.path.dirName(dir) )); 845 | assert(!std.file.exists( notExist )); 846 | } 847 | 848 | testFileOperation!("removePath", "string")(() { 849 | mixin(useTmpName!"file"); 850 | mixin(useTmpName!("dir", "subdir")); 851 | mixin(useTmpName!"notExist"); 852 | std.file.write(file, "abc"); 853 | std.file.mkdirRecurse(dir); 854 | 855 | checkPre(); 856 | removePath( file ); 857 | removePath( std.path.dirName(dir) ); 858 | if(scriptlikeDryRun) 859 | removePath( notExist ); 860 | else 861 | assertThrown(removePath( notExist )); 862 | mixin(checkResult); 863 | }); 864 | 865 | testFileOperation!("removePath", "Path")(() { 866 | mixin(useTmpName!"file"); 867 | mixin(useTmpName!("dir", "subdir")); 868 | mixin(useTmpName!"notExist"); 869 | std.file.write(file, "abc"); 870 | std.file.mkdirRecurse(dir); 871 | 872 | checkPre(); 873 | removePath(Path( file )); 874 | removePath(Path( std.path.dirName(dir) )); 875 | if(scriptlikeDryRun) 876 | removePath(Path( notExist )); 877 | else 878 | assertThrown(removePath(Path( notExist ) )); 879 | mixin(checkResult); 880 | }); 881 | } 882 | 883 | /// If `name` exists, then delete it regardless of whether it's a file or 884 | /// directory. If it doesn't already exist, do nothing and return false. 885 | /// 886 | /// If you want an exception to be thrown if `name` doesn't already exist, 887 | /// use `removePath` instead. 888 | /// 889 | /// Supports Path and command echoing. 890 | /// 891 | /// Returns: Success? 892 | bool tryRemovePath(T)(T name) if(is(T==string) || is(T==Path)) 893 | { 894 | yapFunc(name.escapeShellArg()); 895 | mixin(gagEcho); 896 | 897 | if(name.exists()) 898 | { 899 | removePath(name); 900 | return true; 901 | } 902 | 903 | return false; 904 | } 905 | 906 | version(unittest_scriptlike_d) 907 | unittest 908 | { 909 | string file, dir, notExist; 910 | 911 | void checkPre() 912 | { 913 | assert(std.file.exists(file)); 914 | assert(std.file.isFile(file)); 915 | 916 | assert(std.file.exists(dir)); 917 | assert(std.file.isDir(dir)); 918 | 919 | assert(!std.file.exists( notExist )); 920 | } 921 | 922 | void checkPost() 923 | { 924 | assert(!std.file.exists( file )); 925 | assert(!std.file.exists( std.path.dirName(dir) )); 926 | assert(!std.file.exists( notExist )); 927 | } 928 | 929 | testFileOperation!("tryRemovePath", "string")(() { 930 | mixin(useTmpName!"file"); 931 | mixin(useTmpName!("dir", "subdir")); 932 | mixin(useTmpName!"notExist"); 933 | std.file.write(file, "abc"); 934 | std.file.mkdirRecurse(dir); 935 | 936 | checkPre(); 937 | assert(tryRemovePath( file )); 938 | assert(tryRemovePath( std.path.dirName(dir) )); 939 | assert(!tryRemovePath( notExist )); 940 | mixin(checkResult); 941 | }); 942 | 943 | testFileOperation!("tryRemovePath", "Path")(() { 944 | mixin(useTmpName!"file"); 945 | mixin(useTmpName!("dir", "subdir")); 946 | mixin(useTmpName!"notExist"); 947 | std.file.write(file, "abc"); 948 | std.file.mkdirRecurse(dir); 949 | 950 | checkPre(); 951 | assert(tryRemovePath(Path( file ))); 952 | assert(tryRemovePath(Path( std.path.dirName(dir) ))); 953 | assert(!tryRemovePath(Path( notExist ) )); 954 | mixin(checkResult); 955 | }); 956 | } 957 | 958 | version(docs_scriptlike_d) 959 | { 960 | /// Posix-only. Check the user (ie "owner") executable bit of a file. File must exist. 961 | bool isUserExec(Path path); 962 | ///ditto 963 | bool isUserExec(string path); 964 | 965 | /// Posix-only. Check the group executable bit of a file. File must exist. 966 | bool isGroupExec(Path path); 967 | ///ditto 968 | bool isGroupExec(string path); 969 | 970 | /// Posix-only. Check the world (ie "other") executable bit of a file. File must exist. 971 | bool isWorldExec(Path path); 972 | ///ditto 973 | bool isWorldExec(string path); 974 | } 975 | else version(Posix) 976 | { 977 | bool isUserExec(Path path) 978 | { 979 | return isUserExec(path.raw); 980 | } 981 | 982 | bool isUserExec(string path) 983 | { 984 | import core.sys.posix.sys.stat; 985 | return !!(getAttributes(path) & S_IXUSR); 986 | } 987 | 988 | bool isGroupExec(Path path) 989 | { 990 | return isGroupExec(path.raw); 991 | } 992 | 993 | bool isGroupExec(string path) 994 | { 995 | import core.sys.posix.sys.stat; 996 | return !!(getAttributes(path) & S_IXGRP); 997 | } 998 | 999 | bool isWorldExec(Path path) 1000 | { 1001 | return isUserExec(path.raw); 1002 | } 1003 | 1004 | bool isWorldExec(string path) 1005 | { 1006 | import core.sys.posix.sys.stat; 1007 | return !!(getAttributes(path) & S_IXOTH); 1008 | } 1009 | } 1010 | 1011 | version(Posix) 1012 | version(unittest_scriptlike_d) 1013 | unittest 1014 | { 1015 | import std.stdio; 1016 | writeln("Running Scriptlike unittests: isUserExec / isGroupExec / isWorldExec"); stdout.flush(); 1017 | 1018 | mixin(useSandbox); 1019 | 1020 | import scriptlike.process : run; 1021 | 1022 | writeFile("noX.txt", "Hi"); 1023 | writeFile("userX.txt", "Hi"); 1024 | writeFile("groupX.txt", "Hi"); 1025 | writeFile("otherX.txt", "Hi"); 1026 | writeFile("allX.txt", "Hi"); 1027 | run("chmod -x noX.txt"); 1028 | run("chmod u+x,go-x userX.txt"); 1029 | run("chmod g+x,uo-x groupX.txt"); 1030 | run("chmod o+x,ug-x otherX.txt"); 1031 | run("chmod +x allX.txt"); 1032 | 1033 | assert(!isUserExec("noX.txt")); 1034 | assert(isUserExec("userX.txt")); 1035 | assert(!isUserExec("groupX.txt")); 1036 | assert(!isUserExec("otherX.txt")); 1037 | assert(isUserExec("allX.txt")); 1038 | 1039 | assert(!isGroupExec("noX.txt")); 1040 | assert(!isGroupExec("userX.txt")); 1041 | assert(isGroupExec("groupX.txt")); 1042 | assert(!isGroupExec("otherX.txt")); 1043 | assert(isGroupExec("allX.txt")); 1044 | 1045 | assert(!isWorldExec("noX.txt")); 1046 | assert(!isWorldExec("userX.txt")); 1047 | assert(!isWorldExec("groupX.txt")); 1048 | assert(isWorldExec("otherX.txt")); 1049 | assert(isWorldExec("allX.txt")); 1050 | } 1051 | --------------------------------------------------------------------------------