improve error message system
Move away from the exception-based system where we throw an Exception to stop the program (which is caught by main) and use a new die function that accesses args[0], prints an error and stops the program. Users should not notice a difference, I think this system is more lightweight and requires less code.
This commit is contained in:
parent
87826cba0d
commit
49e2606a2e
1 changed files with 84 additions and 78 deletions
162
esv.d
162
esv.d
|
@ -20,15 +20,13 @@
|
||||||
|
|
||||||
module esv;
|
module esv;
|
||||||
|
|
||||||
|
import std.algorithm : startsWith;
|
||||||
import std.conv : to, ConvException;
|
import std.conv : to, ConvException;
|
||||||
import std.exception : enforce;
|
|
||||||
import std.file : exists, mkdirRecurse, write, FileException;
|
import std.file : exists, mkdirRecurse, write, FileException;
|
||||||
import std.format : format;
|
import std.format : format;
|
||||||
import std.getopt : getopt, GetOptException;
|
import std.getopt : getopt, GetOptException;
|
||||||
import std.getopt : getoptConfig = config;
|
|
||||||
import std.path : baseName, dirName, expandTilde, isValidPath;
|
import std.path : baseName, dirName, expandTilde, isValidPath;
|
||||||
import std.process : environment, executeShell;
|
import std.process : environment, executeShell;
|
||||||
import std.regex : regex, matchFirst, replaceAll, replaceFirst;
|
|
||||||
import std.stdio : writef, writeln, writefln, File;
|
import std.stdio : writef, writeln, writefln, File;
|
||||||
import std.string : splitLines;
|
import std.string : splitLines;
|
||||||
|
|
||||||
|
@ -51,6 +49,7 @@ bool rFlag, RFlag; /* passage references */
|
||||||
string sFlag; /* search passages */
|
string sFlag; /* search passages */
|
||||||
bool VFlag; /* show version */
|
bool VFlag; /* show version */
|
||||||
|
|
||||||
|
string[] mainArgs;
|
||||||
File stderr;
|
File stderr;
|
||||||
|
|
||||||
version (OpenBSD) {
|
version (OpenBSD) {
|
||||||
|
@ -59,55 +58,31 @@ version (OpenBSD) {
|
||||||
|
|
||||||
int
|
int
|
||||||
main(string[] args)
|
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 apiKey;
|
||||||
string configPath;
|
string configPath;
|
||||||
INIUnit ini;
|
INIUnit ini;
|
||||||
ESVApi esv;
|
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 */
|
/* Parse command-line options */
|
||||||
try {
|
try {
|
||||||
|
import std.getopt : cfg = config;
|
||||||
getopt(args,
|
getopt(args,
|
||||||
getoptConfig.bundling,
|
cfg.bundling,
|
||||||
getoptConfig.caseSensitive,
|
cfg.caseSensitive,
|
||||||
"a", &aFlag,
|
"a", &aFlag,
|
||||||
"C", &CFlag,
|
"C", &CFlag,
|
||||||
"F", &FFlag, "f", &fFlag,
|
"F", &FFlag, "f", &fFlag,
|
||||||
|
@ -119,20 +94,19 @@ run(string[] args)
|
||||||
"V", &VFlag,
|
"V", &VFlag,
|
||||||
);
|
);
|
||||||
} catch (GetOptException e) {
|
} catch (GetOptException e) {
|
||||||
enforce(e.msg.matchFirst(regex("^Unrecognized option")).empty,
|
enforceDie(!e.msg.startsWith("Unrecognized option"),
|
||||||
"unknown option " ~ e.extractOpt());
|
"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());
|
"missing argument for option " ~ e.extractOpt());
|
||||||
|
|
||||||
throw new Exception(e.msg); /* catch-all */
|
die(e.msg); /* catch-all */
|
||||||
} catch (ConvException e) {
|
} catch (ConvException e) {
|
||||||
throw new Exception(
|
die("illegal argument to -l option -- must be integer");
|
||||||
"illegal argument to -l option -- must be integer");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (VFlag) {
|
if (VFlag) {
|
||||||
writeln("esv version " ~ VERSION);
|
writeln("esv version " ~ VERSION);
|
||||||
return true;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sFlag != "") {
|
if (sFlag != "") {
|
||||||
|
@ -142,26 +116,25 @@ run(string[] args)
|
||||||
|
|
||||||
if (args.length < 3) {
|
if (args.length < 3) {
|
||||||
stderr.writefln("usage: %s [-aFfHhNnRrV] [-C config] [-l length] [-s query] book verses", args[0].baseName());
|
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()),
|
enforceDie(bookValid(args[1].parseBook()),
|
||||||
format!"book '%s' does not exist"(args[1]));
|
"book '%s' does not exist", args[1]);
|
||||||
enforce(verseValid(args[2]),
|
enforceDie(verseValid(args[2]),
|
||||||
format!"invalid verse format '%s'"(args[2]));
|
"invalid verse format '%s'", args[2]);
|
||||||
|
|
||||||
/* determine configuration file
|
/* determine configuration file
|
||||||
* Options have first priority, then environment variables,
|
* Options have first priority, then environment variables,
|
||||||
* then the default path */
|
* then the default path */
|
||||||
config:
|
config:
|
||||||
configPath = environment.get(ENV_CONFIG, DEFAULT_CONFIGPATH)
|
configPath = environment.get(ENV_CONFIG, DEFAULT_CONFIGPATH).expandTilde();
|
||||||
.expandTilde();
|
|
||||||
try {
|
try {
|
||||||
if (CFlag != "") { /* if -C was given */
|
if (CFlag != "") { /* if -C was given */
|
||||||
enforce(isValidPath(CFlag), CFlag ~ ": invalid path");
|
enforceDie(isValidPath(CFlag), CFlag ~ ": invalid path");
|
||||||
configPath = CFlag.expandTilde();
|
configPath = CFlag.expandTilde();
|
||||||
} else {
|
} else {
|
||||||
enforce(isValidPath(configPath),
|
enforceDie(isValidPath(configPath),
|
||||||
configPath ~ ": invalid path");
|
configPath ~ ": invalid path");
|
||||||
|
|
||||||
if (!configPath.exists()) {
|
if (!configPath.exists()) {
|
||||||
|
@ -176,8 +149,8 @@ key = %s
|
||||||
# custom API parameters using `parameters`:
|
# custom API parameters using `parameters`:
|
||||||
#parameters = &my-parameter=value
|
#parameters = &my-parameter=value
|
||||||
|
|
||||||
# Some other settings that modify how the passages are displayed:
|
# Settings that modify how passages are displayed:
|
||||||
#[passage]
|
[passage]
|
||||||
#footnotes = false
|
#footnotes = false
|
||||||
#headings = false
|
#headings = false
|
||||||
#passage-references = false
|
#passage-references = false
|
||||||
|
@ -188,16 +161,17 @@ key = %s
|
||||||
readINIFile(ini, configPath);
|
readINIFile(ini, configPath);
|
||||||
} catch (FileException e) {
|
} catch (FileException e) {
|
||||||
/* filesystem syscall errors */
|
/* 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");
|
apiKey = ini["api"].key("key");
|
||||||
enforce(apiKey != null,
|
enforceDie(apiKey != null,
|
||||||
"API key not present in configuration file; cannot proceed");
|
"API key not present in configuration file; cannot proceed");
|
||||||
|
|
||||||
esv = new ESVApi(apiKey);
|
esv = new ESVApi(apiKey);
|
||||||
|
|
||||||
enforce(!(aFlag && sFlag), "cannot specify both -a and -s options");
|
|
||||||
if (aFlag) {
|
if (aFlag) {
|
||||||
string tmpf, mpegPlayer;
|
string tmpf, mpegPlayer;
|
||||||
|
|
||||||
|
@ -205,12 +179,11 @@ key = %s
|
||||||
mpegPlayer = environment.get(ENV_PLAYER, DEFAULT_MPEGPLAYER);
|
mpegPlayer = environment.get(ENV_PLAYER, DEFAULT_MPEGPLAYER);
|
||||||
|
|
||||||
/* check for an audio player */
|
/* check for an audio player */
|
||||||
enforce(
|
enforceDie(
|
||||||
executeShell(
|
executeShell(
|
||||||
format!"command -v %s >/dev/null 2>&1"(mpegPlayer)
|
format!"command -v %s >/dev/null 2>&1"(mpegPlayer)
|
||||||
).status == 0,
|
).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.
|
/* esv has built-in support for mpg123 and mpv.
|
||||||
* other players will work, just recompile with
|
* other players will work, just recompile with
|
||||||
|
@ -219,32 +192,37 @@ key = %s
|
||||||
mpegPlayer ~=
|
mpegPlayer ~=
|
||||||
mpegPlayer == "mpg123" ? " -q " :
|
mpegPlayer == "mpg123" ? " -q " :
|
||||||
mpegPlayer == "mpv" ? " --msg-level=all=no " : " ";
|
mpegPlayer == "mpv" ? " --msg-level=all=no " : " ";
|
||||||
/* spawn mpg123 */
|
/* spawn the player */
|
||||||
executeShell(mpegPlayer ~ tmpf);
|
executeShell(mpegPlayer ~ tmpf);
|
||||||
return true;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sFlag) {
|
if (sFlag) {
|
||||||
writeln(esv.searchFormat(sFlag));
|
writeln(esv.searchFormat(sFlag));
|
||||||
return true;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
esv.extraParameters = ini["api"].key("parameters", "");
|
esv.extraParameters = ini["api"].key("parameters", "");
|
||||||
|
|
||||||
/* Get [passage] keys */
|
/* Get [passage] keys */
|
||||||
foreach (string key; ["footnotes", "headings", "passage-references", "verse-numbers"]) {
|
foreach (string key; [
|
||||||
|
"footnotes",
|
||||||
|
"headings",
|
||||||
|
"passage-references",
|
||||||
|
"verse-numbers"
|
||||||
|
]) {
|
||||||
try
|
try
|
||||||
esv.opts.b["include-" ~ key] =
|
esv.opts.b["include-" ~ key] =
|
||||||
ini["passage"].keyAs!bool(key, "true");
|
ini["passage"].keyAs!bool(key, "true");
|
||||||
catch (INITypeException e)
|
catch (INITypeException e)
|
||||||
throw new Exception(configPath ~ ": " ~ e.msg);
|
die(configPath ~ ": " ~ e.msg);
|
||||||
}
|
}
|
||||||
/* Get line-length ([passage]) */
|
/* Get line-length ([passage]) */
|
||||||
try {
|
try
|
||||||
esv.opts.i["line-length"] =
|
esv.opts.i["line-length"] =
|
||||||
ini["passage"].keyAs!int("line-length", "0");
|
ini["passage"].keyAs!int("line-length", "0");
|
||||||
} catch (INITypeException e) {
|
catch (INITypeException e)
|
||||||
throw new Exception(configPath ~ ": " ~ e.msg);
|
die(configPath ~ ": " ~ e.msg);
|
||||||
}
|
|
||||||
|
|
||||||
if (fFlag) esv.opts.b["include-footnotes"] = true;
|
if (fFlag) esv.opts.b["include-footnotes"] = true;
|
||||||
if (hFlag) esv.opts.b["include-headings"] = true;
|
if (hFlag) esv.opts.b["include-headings"] = true;
|
||||||
|
@ -257,17 +235,45 @@ key = %s
|
||||||
if (lFlag != 0) esv.opts.i["line-length"] = lFlag;
|
if (lFlag != 0) esv.opts.i["line-length"] = lFlag;
|
||||||
|
|
||||||
writeln(esv.getPassage(args[1].parseBook(), args[2]));
|
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
|
private string
|
||||||
extractOpt(in GetOptException e) @safe
|
extractOpt(in GetOptException e)
|
||||||
{
|
{
|
||||||
|
import std.regex : matchFirst;
|
||||||
|
|
||||||
return e.msg.matchFirst("-.")[0];
|
return e.msg.matchFirst("-.")[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
private string
|
private string
|
||||||
parseBook(in string book) @safe
|
parseBook(in string book)
|
||||||
{
|
{
|
||||||
import std.string : tr;
|
import std.string : tr;
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue