Awesome
Scriptlike
Scriptlike is a utility library to help you write script-like programs in the D Programming Language.
Officially supported compiler versions are shown in .travis.yml.
Links:
- How to Use Scriptlike in Scripts
- API Reference
- Changelog
- DUB Package
- Small article explaining the original motivations behind scriptlike
Sections
- Automatic Phobos Import
- User Input Prompts
- String Interpolation
- Filepaths
- Try/As Filesystem Operations
- Script-Style Shell Commands
- Command Echoing
- Dry Run Assistance
- Fail
Disambiguating write and write
Features
Automatic Phobos Import
For most typical Phobos modules. Unless you don't want to. Who needs rows and rows of standard lib imports for a mere script?
import scriptlike;
//import scriptlike.only; // In case you don't want Phobos auto-imported
void main() {
writeln("Works!");
}
See: scriptlike
,
scriptlike.only
,
scriptlike.std
User Input Prompts
Easy prompting for and verifying command-line user input with the
interact
module:
auto name = userInput!string("Please enter your name");
auto age = userInput!int("And your age");
if(userInput!bool("Do you want to continue?"))
{
string outputFolder = pathLocation("Where you do want to place the output?");
auto color = menu!string("What color would you like to use?", ["Blue", "Green"]);
}
auto num = require!(int, "a > 0 && a <= 10")("Enter a number from 1 to 10");
pause(); // Prompt "Press Enter to continue...";
pause("Hit Enter again, dood!!");
See: userInput
,
pathLocation
,
menu
,
require
,
pause
String Interpolation
Variable (and expression) expansion inside strings:
// Output: The number 21 doubled is 42!
int num = 21;
writeln( mixin(interp!"The number ${num} doubled is ${num * 2}!") );
// Output: Empty braces output nothing.
writeln( mixin(interp!"Empty ${}braces ${}output nothing.") );
// Output: Multiple params: John Doe.
auto first = "John", last = "Doe";
writeln( mixin(interp!`Multiple params: ${first, " ", last}.`) );
See: interp
Filepaths
Simple, reliable, cross-platform. No more worrying about slashes, paths-with-spaces, buildPath, normalizing, or getting paths mixed up with ordinary strings:
// This is AUTOMATICALLY kept normalized (via std.path.buildNormalizedPath)
auto dir = Path("foo/bar");
dir ~= "subdir"; // Append a subdirectory
// No worries about trailing slashes!
assert(Path("foo/bar") == Path("foo/bar/"));
assert(Path("foo/bar/") == Path("foo/bar//"));
// No worries about forward/backslashes!
assert(dir == Path("foo/bar/subdir"));
assert(dir == Path("foo\\bar\\subdir"));
// No worries about spaces!
auto file = dir.up ~ "different subdir\\Filename with spaces.txt";
assert(file == Path("foo/bar/different subdir/Filename with spaces.txt"));
writeln(file); // Path.toString() always properly escapes for current platform!
writeln(file.toRawString()); // Don't escape!
// Even file extentions are type-safe!
Ext ext = file.extension;
auto anotherFile = Path("path/to/file") ~ ext;
assert(anotherFile.baseName == Path("file.txt"));
// std.path and std.file are wrapped to offer Path/Ext support
assert(dirName(anotherFile) == Path("path/to"));
copy(anotherFile, Path("target/path/new file.txt"));
See: Path
,
Path.toString
,
Path.toRawString
,
Path.up
,
Ext
,
dirName
,
copy
,
buildNormalizedPath
Try/As Filesystem Operations
Less pedantic, when you don't care if there's nothing to do:
// Just MAKE SURE this exists! If it's already there, then GREAT!
tryMkdir("somedir");
assertThrown( mkdir("somedir") ); // Exception: Already exists!
tryMkdir("somedir"); // Works fine!
// Just MAKE SURE this is gone! If it's already gone, then GREAT!
tryRmdir("somedir");
assertThrown( rmdir("somedir") ); // Exception: Already gone!
tryRmdir("somedir"); // Works fine!
// Just MAKE SURE it doesn't exist. Don't bother me if it doesn't!
tryRemove("file");
// Copy if it exists, otherwise don't worry about it.
tryCopy("file", "file-copy");
// Is this a directory? If it doesn't even exist,
// then it's obviously NOT a directory.
assertThrown( isDir("foo/bar") ); // Exception: Doesn't exist!
if(existsAsDir("foo/bar")) // Works fine!
{/+ ...do stuff... +/}
// Bonus! Single function to delete files OR directories!
writeFile("file.txt", "abc");
tryMkdirRecurse("foo/bar/dir");
writeFile("foo/bar/dir/file.txt", "123");
// Delete with the same function!
removePath("file.txt"); // Calls 'remove'
removePath("foo"); // Calls 'rmdirRecurse'
tryRemovePath("file.txt"); // Also comes in try flavor!
tryRemovePath("foo");
See: tryMkdir
,
mkdir
,
tryMkdirRecurse
,
mkdir
,
tryRmdir
,
rmdir
,
tryRemove
,
tryCopy
,
existsAsDir
,
removePath
,
tryRemovePath
,
writeFile
and more...
Script-Style Shell Commands
Invoke a command script-style: synchronously with forwarded stdout/in/err from any working directory. Or capture the output instead. Automatically throw on non-zero status code if you want.
One simple call, run
,
to run a shell command script-style (ie, synchronously with forwarded stdout/in/err)
from any working directory, and automatically throw if it fails. Or
runCollect
to capture the output instead of displaying it. Or
tryRun
/tryRunCollect
if you want to receive the status code instead of automatically throwing on non-zero.
run("dmd --help"); // Display DMD help screen
pause(); // Wait for user to hit Enter
// Automatically throws ErrorLevelException(1, "dmd --bad-flag")
assertThrown!ErrorLevelException( run("dmd --bad-flag") );
// Automatically throws ErrorLevelException(-1, "this-cmd-does-not-exist")
assertThrown!ErrorLevelException( run("this-cmd-does-not-exist") );
// Don't bail on error
int statusCode = tryRun("dmd --bad-flag");
// Collect output instead of showing it
string dmdHelp = runCollect("dmd --help");
auto isDMD_2_068_1 = dmdHelp.canFind("D Compiler v2.068.1");
// Don't bail on error
auto result = tryRunCollect("dmd --help");
if(result.status == 0 && result.output.canFind("D Compiler v2.068.1"))
writeln("Found DMD v2.068.1!");
// Use any working directory:
auto myProjectDir = Path("my/proj/dir");
auto mainFile = Path("src/main.d");
myProjectDir.run(text("dmd ", mainFile, " -O")); // mainFile is properly escaped!
// Verify it actually IS running from a different working directory:
version(Posix) enum pwd = "pwd";
else version(Windows) enum pwd = "cd";
else static assert(0);
auto output = myProjectDir.runCollect(pwd);
auto expected = getcwd() ~ myProjectDir;
assert( Path(output.strip()) == expected );
See: run
,
tryRun
,
runCollect
,
tryRunCollect
,
pause
,
Path
,
getcwd
,
ErrorLevelException
,
assertThrown
,
canFind
,
text
,
strip
Command Echoing
Optionally enable automatic command echoing (including shell commands,
changing/creating directories and deleting/copying/moving/linking/renaming
both directories and files) by setting one simple flag:
bool scriptlikeEcho
Echoing can be customized via
scriptlikeCustomEcho
.
/++
Output:
--------
run: echo Hello > file.txt
mkdirRecurse: some/new/dir
copy: file.txt -> 'some/new/dir/target name.txt'
Gonna run foo() now...
foo: i = 42
--------
+/
scriptlikeEcho = true; // Enable automatic echoing
run("echo Hello > file.txt");
auto newDir = Path("some/new/dir");
mkdirRecurse(newDir.toRawString()); // Even works with non-Path overloads
copy("file.txt", newDir ~ "target name.txt");
void foo(int i = 42) {
yapFunc("i = ", i); // Evaluated lazily
}
// yap and yapFunc ONLY output when echoing is enabled
yap("Gonna run foo() now...");
foo();
See: scriptlikeEcho
,
yap
,
yapFunc
,
run
,
Path
,
Path.toRawString
,
mkdirRecurse
,
copy
Dry Run Assistance
Scriptlike can help you create a dry-run mode, by automatically echoing (even if
scriptlikeEcho
is disabled) and disabling all functions that
launch external commands
or modify the filesystem.
Just enable the
scriptlikeDryRun
flag.
Note, if you choose to use this, you still must ensure your program logic behaves sanely in dry-run mode.
scriptlikeDryRun = true;
// When dry-run is enabled, this echoes but doesn't actually copy or invoke DMD.
copy("original.d", "app.d");
run("dmd app.d -ofbin/app");
// Works fine in dry-run, since it doesn't modify the filesystem.
bool isItThere = exists("another-file");
if(!scriptlikeDryRun)
{
// This won't work right if we're running in dry-run mode,
// since it'll be out-of-date, if it even exists at all.
auto source = read("app.d");
}
See: scriptlikeDryRun
,
copy
,
run
,
exists
,
read
Fail
Single function to bail out with an error message, exception-safe.
/++
Example:
--------
$ test
test: ERROR: Need two args, not 0!
$ test abc 123
test: ERROR: First arg must be 'foobar', not 'abc'!
--------
+/
import scriptlike;
void main(string[] args) {
helper(args);
}
// Throws a Fail exception on bad args:
void helper(string[] args) {
// Like std.exception.enforce, but bails with no ugly stack trace,
// and if uncaught, outputs the program name and "ERROR: "
failEnforce(args.length == 3, "Need two args, not ", args.length-1, "!");
if(args[1] != "foobar")
fail("First arg must be 'foobar', not '", args[1], "'!");
}
See: fail
,
failEnforce
,
Fail
Disambiguating write and write
Since they're both imported by default, you may get symbol conflict errors
when trying to use
scriptlike.file.wrappers.write
(which wraps std.file.write
)
or std.stdio.write
.
And unfortunately, DMD issue #11847
currently makes it impossible to use a qualified name lookup for
scriptlike.file.wrappers.write
.
Here's how to easily avoid symbol conflict errors with Scriptlike and write
:
// Save file
write("filename.txt", "content"); // Error: Symbols conflict!
// Change line above to...
writeFile("filename.txt", "content"); // Convenience alias included in scriptlike
// Output to stdout with no newline
write("Hello ", "world"); // Error: Symbols conflict!
// Change line above to...
std.stdio.write("Hello ", "world");
// or...
stdout.write("Hello ", "world");
See:
scriptlike.file.wrappers.writeFile
,
scriptlike.file.wrappers.readFile
,
scriptlike.file.wrappers.write
,
std.file.write
,
std.stdio.write