Abscissa / scriptlike

Utility library to help you write script-like programs in D
Other
93 stars 10 forks source link

Scriptlike Build Status Build status

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:

Sections

Features

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