diff --git a/esv.d b/esv.d index fc8c3b1..f9429ed 100644 --- a/esv.d +++ b/esv.d @@ -20,15 +20,13 @@ module esv; +import std.algorithm : startsWith; import std.conv : to, ConvException; -import std.exception : enforce; import std.file : exists, mkdirRecurse, write, FileException; import std.format : format; import std.getopt : getopt, GetOptException; -import std.getopt : getoptConfig = config; import std.path : baseName, dirName, expandTilde, isValidPath; import std.process : environment, executeShell; -import std.regex : regex, matchFirst, replaceAll, replaceFirst; import std.stdio : writef, writeln, writefln, File; import std.string : splitLines; @@ -51,6 +49,7 @@ bool rFlag, RFlag; /* passage references */ string sFlag; /* search passages */ bool VFlag; /* show version */ +string[] mainArgs; File stderr; version (OpenBSD) { @@ -59,55 +58,31 @@ version (OpenBSD) { int main(string[] args) -{ - bool success; - - /* @safe way of opening stderr on Unix */ - stderr = File("/dev/stderr", "w"); - - version (OpenBSD) { - import core.sys.openbsd.unistd : pledge; - import std.string : toStringz; - - promises = toStringz("stdio rpath wpath cpath inet dns proc exec prot_exec"); - pledge(promises, null); - } - - debug { - return run(args) ? 0 : 1; - } - - try { - success = run(args); - } catch (Exception e) { - if (typeid(e) == typeid(Exception)) { - stderr.writefln("%s: %s", args[0].baseName(), e.msg); - } else { - stderr.writefln("%s: uncaught %s in %s:%d: %s", - args[0].baseName(), - typeid(e).name, - e.file, e.line, e.msg); - stderr.writefln("this is probably a bug; it would be greatly appreciated if you reported it at:\n\n %s", - BUGREPORTURL); - } - } - - return success ? 0 : 1; -} - -bool -run(string[] args) { string apiKey; string configPath; INIUnit ini; ESVApi esv; + mainArgs = args; + + version (OpenBSD) () @trusted { + import core.sys.openbsd.unistd : pledge; + import std.string : toStringz; + + promises = toStringz("stdio rpath wpath cpath inet dns proc exec prot_exec"); + pledge(promises, null); + }(); + + /* @safe way of opening stderr on Unix */ + stderr = File("/dev/stderr", "w"); + /* Parse command-line options */ try { + import std.getopt : cfg = config; getopt(args, - getoptConfig.bundling, - getoptConfig.caseSensitive, + cfg.bundling, + cfg.caseSensitive, "a", &aFlag, "C", &CFlag, "F", &FFlag, "f", &fFlag, @@ -119,20 +94,19 @@ run(string[] args) "V", &VFlag, ); } catch (GetOptException e) { - enforce(e.msg.matchFirst(regex("^Unrecognized option")).empty, + enforceDie(!e.msg.startsWith("Unrecognized option"), "unknown option " ~ e.extractOpt()); - enforce(e.msg.matchFirst(regex("^Missing value for argument")).empty, + enforceDie(!e.msg.startsWith("Missing value for argument"), "missing argument for option " ~ e.extractOpt()); - throw new Exception(e.msg); /* catch-all */ + die(e.msg); /* catch-all */ } catch (ConvException e) { - throw new Exception( - "illegal argument to -l option -- must be integer"); + die("illegal argument to -l option -- must be integer"); } if (VFlag) { writeln("esv version " ~ VERSION); - return true; + return 0; } if (sFlag != "") { @@ -142,26 +116,25 @@ run(string[] args) if (args.length < 3) { stderr.writefln("usage: %s [-aFfHhNnRrV] [-C config] [-l length] [-s query] book verses", args[0].baseName()); - return false; + return 1; } - enforce(bookValid(args[1].parseBook()), - format!"book '%s' does not exist"(args[1])); - enforce(verseValid(args[2]), - format!"invalid verse format '%s'"(args[2])); + enforceDie(bookValid(args[1].parseBook()), + "book '%s' does not exist", args[1]); + enforceDie(verseValid(args[2]), + "invalid verse format '%s'", args[2]); /* determine configuration file * Options have first priority, then environment variables, * then the default path */ config: - configPath = environment.get(ENV_CONFIG, DEFAULT_CONFIGPATH) - .expandTilde(); + configPath = environment.get(ENV_CONFIG, DEFAULT_CONFIGPATH).expandTilde(); try { if (CFlag != "") { /* if -C was given */ - enforce(isValidPath(CFlag), CFlag ~ ": invalid path"); + enforceDie(isValidPath(CFlag), CFlag ~ ": invalid path"); configPath = CFlag.expandTilde(); } else { - enforce(isValidPath(configPath), + enforceDie(isValidPath(configPath), configPath ~ ": invalid path"); if (!configPath.exists()) { @@ -176,8 +149,8 @@ key = %s # custom API parameters using `parameters`: #parameters = &my-parameter=value -# Some other settings that modify how the passages are displayed: -#[passage] +# Settings that modify how passages are displayed: +[passage] #footnotes = false #headings = false #passage-references = false @@ -188,16 +161,17 @@ key = %s readINIFile(ini, configPath); } catch (FileException e) { /* filesystem syscall errors */ - throw new Exception(e.msg); + die(e.msg); } + enforceDie(!(aFlag && sFlag), "cannot specify both -a and -s flags"); + apiKey = ini["api"].key("key"); - enforce(apiKey != null, + enforceDie(apiKey != null, "API key not present in configuration file; cannot proceed"); esv = new ESVApi(apiKey); - enforce(!(aFlag && sFlag), "cannot specify both -a and -s options"); if (aFlag) { string tmpf, mpegPlayer; @@ -205,12 +179,11 @@ key = %s mpegPlayer = environment.get(ENV_PLAYER, DEFAULT_MPEGPLAYER); /* check for an audio player */ - enforce( + enforceDie( executeShell( format!"command -v %s >/dev/null 2>&1"(mpegPlayer) ).status == 0, - format!"%s is required for audio mode; cannot continue"(mpegPlayer) - ); + mpegPlayer ~ " is required for audio mode; cannot continue"); /* esv has built-in support for mpg123 and mpv. * other players will work, just recompile with @@ -219,32 +192,37 @@ key = %s mpegPlayer ~= mpegPlayer == "mpg123" ? " -q " : mpegPlayer == "mpv" ? " --msg-level=all=no " : " "; - /* spawn mpg123 */ + /* spawn the player */ executeShell(mpegPlayer ~ tmpf); - return true; + return 0; } + if (sFlag) { writeln(esv.searchFormat(sFlag)); - return true; + return 0; } esv.extraParameters = ini["api"].key("parameters", ""); /* Get [passage] keys */ - foreach (string key; ["footnotes", "headings", "passage-references", "verse-numbers"]) { + foreach (string key; [ + "footnotes", + "headings", + "passage-references", + "verse-numbers" + ]) { try esv.opts.b["include-" ~ key] = ini["passage"].keyAs!bool(key, "true"); catch (INITypeException e) - throw new Exception(configPath ~ ": " ~ e.msg); + die(configPath ~ ": " ~ e.msg); } /* Get line-length ([passage]) */ - try { + try esv.opts.i["line-length"] = ini["passage"].keyAs!int("line-length", "0"); - } catch (INITypeException e) { - throw new Exception(configPath ~ ": " ~ e.msg); - } + catch (INITypeException e) + die(configPath ~ ": " ~ e.msg); if (fFlag) esv.opts.b["include-footnotes"] = true; if (hFlag) esv.opts.b["include-headings"] = true; @@ -257,17 +235,45 @@ key = %s if (lFlag != 0) esv.opts.i["line-length"] = lFlag; writeln(esv.getPassage(args[1].parseBook(), args[2])); - return true; + return 0; +} + +private void +warn(string mesg) +{ + stderr.writeln(baseName(mainArgs[0]) ~ ": " ~ mesg); +} + +private void +die(string mesg) @trusted +{ + import core.runtime : Runtime; + import core.stdc.stdlib : exit; + + warn(mesg); + Runtime.terminate(); + exit(1); +} + +private void +enforceDie(A...)(bool cond, string fmt, A a) +{ + import std.format : format; + + if (!cond) + die(format(fmt, a)); } private string -extractOpt(in GetOptException e) @safe +extractOpt(in GetOptException e) { + import std.regex : matchFirst; + return e.msg.matchFirst("-.")[0]; } private string -parseBook(in string book) @safe +parseBook(in string book) { import std.string : tr;