From 06a4dc286d90717a5695f90655c0ce46a8a3882f Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Sat, 23 Sep 2023 17:10:38 +1200 Subject: [PATCH] Refactor most of the code - Replace panic()-based error handling in main() with Exception-based error handling - Put hardcoded defaults in config.di - Make general optimisations and readability improvements for a lot of the code - Change some of the ESVApi's interface --- config.di | 18 +++ esvapi.d | 410 ++++++++++++++++++++++++++++-------------------------- main.d | 331 +++++++++++++++++++++---------------------- 3 files changed, 397 insertions(+), 362 deletions(-) create mode 100644 config.di diff --git a/config.di b/config.di new file mode 100644 index 0000000..e40261f --- /dev/null +++ b/config.di @@ -0,0 +1,18 @@ +/* default configuration for esv */ + +module config; + +public: + +enum DEFAULT_APIKEY = "abfb7456fa52ec4292c79e435890cfa3df14dc2b"; +enum DEFAULT_CONFIGPATH = "~/.config/esv.conf"; +enum DEFAULT_MPEGPLAYER = "mpg123"; +enum DEFAULT_PAGER = "less"; + +enum ENV_CONFIG = "ESV_CONFIG"; +enum ENV_PAGER = "ESV_PAGER"; +enum ENV_PLAYER = "ESV_PLAYER"; + +enum BUGREPORTURL = "https://codeberg.org/jtbx/esv/issues"; + +// vi: ft=d diff --git a/esvapi.d b/esvapi.d index 573a71b..84d17a7 100644 --- a/esvapi.d +++ b/esvapi.d @@ -20,31 +20,25 @@ module esvapi; -import std.algorithm : filter, map; -import std.array : appender; -import std.ascii : isAlphaNum; -import std.base64 : Base64; import std.conv : to; -import std.file : mkdirRecurse, tempDir, write; +import std.exception : basicExceptionCtors, enforce; +import std.file : tempDir, write; import std.format : format; import std.json : JSONValue, parseJSON; -import std.random : rndGen; -import std.range : take; -import std.regex : matchAll, replaceAll, replaceFirst, regex; +import std.regex : regex, matchAll, replaceAll; +import std.stdio : File; import std.string : capitalize; -import std.utf : toUTF8; -import std.net.curl; +import std.net.curl : HTTP; -public enum ESVMode +enum ESVIndent { - TEXT, - AUDIO + SPACE, + TAB } -const enum ESVAPI_KEY = "abfb7456fa52ec4292c79e435890cfa3df14dc2b"; -const enum ESVAPI_URL = "https://api.esv.org/v3/passage"; +const enum string ESVAPI_URL = "https://api.esv.org/v3/passage"; const string[] BIBLE_BOOKS = [ - // Old Testament + /* Old Testament */ "Genesis", "Exodus", "Leviticus", @@ -64,7 +58,7 @@ const string[] BIBLE_BOOKS = [ "Esther", "Job", "Psalm", - "Psalms", // both are valid + "Psalms", /* both are valid */ "Proverbs", "Ecclesiastes", "Song of Solomon", @@ -85,7 +79,7 @@ const string[] BIBLE_BOOKS = [ "Haggai", "Zechariah", "Malachi", - // New Testament + /* New Testament */ "Matthew", "Mark", "Luke", @@ -114,154 +108,167 @@ const string[] BIBLE_BOOKS = [ "Jude", "Revelation" ]; +/* All boolean API parameters */ +const string[] ESVAPI_PARAMETERS = [ + "include-passage-references", + "include-verse-numbers", + "include-first-verse-numbers", + "include-footnotes", + "include-footnote-body", + "include-headings", + "include-short-copyright", + "include-copyright", + "include-passage-horizontal-lines", + "include-heading-horizontal-lines", + "include-selahs", + "indent-poetry", + "horizontal-line-length", + "indent-paragraphs", + "indent-poetry-lines", + "indent-declares", + "indent-psalm-doxology", + "line-length", + "indent-using", +]; + +/* + * Returns true if the argument book is a valid book of the Bible. + * Otherwise, returns false. + */ +bool bookValid(in char[] book) nothrow @safe +{ + foreach (string b; BIBLE_BOOKS) { + if (book.capitalize() == b.capitalize()) + return true; + } + return false; +} +/* + * Returns true if the argument book is a valid verse format. + * Otherwise, returns false. + */ +bool verseValid(in char[] verse) @safe +{ + bool vMatch(in string re) @safe + { + return !verse.matchAll(regex(re)).empty; + } + if (vMatch("^\\d{1,3}$") || + vMatch("^\\d{1,3}-\\d{1,3}$") || + vMatch("^\\d{1,3}:\\d{1,3}$") || + vMatch("^\\d{1,3}:\\d{1,3}-\\d{1,3}$")) + return true; + + return false; +} + +} class ESVApi { - private { - int _mode; + protected { string _key; string _tmp; string _url; } - ESVApiOptions opts; + string extraParameters; int delegate(size_t, size_t, size_t, size_t) onProgress; - this(immutable(string) key = ESVAPI_KEY, bool audio = false) + ESVApiOptions opts; + + this(string key, string tmpName = "esv") { - _key = key; - _mode = audio ? ESVMode.AUDIO : ESVMode.TEXT; - _tmp = tempDir() ~ "esv"; - _url = ESVAPI_URL; - opts.defaults(); + _key = key; + _tmp = tempDir() ~ tmpName; + _url = ESVAPI_URL; + opts = ESVApiOptions(true); extraParameters = ""; onProgress = (size_t dlTotal, size_t dlNow, size_t ulTotal, size_t ulNow) { return 0; }; - tmpName = "esv"; } + /* - * Returns the API authentication key that was given when the API object was instantiated. - * This authentication key cannot be changed after instantiation. + * Returns the API authentication key that was given when the object + * was constructed. This authentication key cannot be changed. */ - @property string key() const nothrow pure @nogc @safe + @property string key() const nothrow pure @safe { return _key; } /* - * Returns the API authentication key currently in use. + * Returns the subdirectory used to store temporary audio passages. */ - @property int mode() const nothrow pure @nogc @safe + @property string tmpDir() const nothrow pure @safe { - return _mode; - } - /* - * If the mode argument is either "text" or "html", - * sets the text API mode to the given mode argument. - * If the mode argument is not one of those, - * throws an ESVException. - */ - @property void mode(immutable(int) mode) pure @safe - { - if (mode == ESVMode.TEXT || mode == ESVMode.AUDIO) - _mode = mode; - else - throw new ESVException("Invalid mode"); + return _tmp; } /* * Returns the API URL currently in use. */ - @property string url() const nothrow pure @nogc @safe + @property string url() const nothrow pure @safe { return _url; } /* - * If the url argument is a valid HTTP URL, sets the API URL currently in use - * to the given url argument. Otherwise, throws an ESVException. + * Sets the API URL currently in use to the given url argument. */ @property void url(immutable(string) url) @safe + in (!url.matchAll(`^https?://.+\\..+(/.+)?`).empty, "Invalid URL format") { - if (url.matchAll("^https?://.+\\..+(/.+)?").empty) - throw new ESVException("Invalid URL format"); - else - _url = url; + _url = url; } /* - * Returns the temp directory name. - */ - @property tmpName() const @safe - { - return _tmp.replaceFirst(regex('^' ~ tempDir()), ""); - } - /* - * Sets the temp directory name to the given string. - */ - @property void tmpName(immutable(string) name) @safe - { - _tmp = tempDir() ~ name; - } - /* - * Returns true if the argument book is a valid book of the Bible. - * Otherwise, returns false. - */ - bool validateBook(in char[] book) const nothrow @safe - { - foreach (b; BIBLE_BOOKS) { - if (book.capitalize() == b.capitalize()) - return true; - } - return false; - } - /* - * Returns true if the argument book is a valid verse format. - * Otherwise, returns false. - */ - bool validateVerse(in char[] verse) const @safe - { - bool attemptRegex(string re) const @safe - { - return !verse.matchAll(re).empty; - } - if (attemptRegex("^\\d{1,3}$") || - attemptRegex("^\\d{1,3}-\\d{1,3}$") || - attemptRegex("^\\d{1,3}:\\d{1,3}$") || - attemptRegex("^\\d{1,3}:\\d{1,3}-\\d{1,3}$")) - return true; - else - return false; - } - /* - * Requests the verse(s) from the API and returns it. + * Requests the verse(s) in text format from the API and returns it. * The (case-insensitive) name of the book being searched are * contained in the argument book. The verse(s) being looked up are * contained in the argument verses. * - * If the mode is ESVMode.AUDIO, requests an audio passage instead. - * A file path to an MP3 audio track is returned. - * To explicitly get an audio passage without setting the mode, - * use getAudioVerses(). - * * Example: getVerses("John", "3:16-21") */ string getVerses(in char[] book, in char[] verse) const + in (bookValid(book), "Invalid book") + in (verseValid(verse), "Invalid verse format") { - if (_mode == ESVMode.AUDIO) { - return getAudioVerses(book, verse); + char[] params, response; + HTTP request; + + params = []; + + { + (char[])[] parambuf; + foreach (string opt; ESVAPI_PARAMETERS) { + bool bo; + int io; + + switch (opt) { + case "indent-using": + o = opts.indent_using; + break; + case "indent-poetry": + } + !opt.matchAll("^include-").empty) { + o = opts.b[opt]; + } else if (opt == "line-length" || + opt == "horizontal-line-length" || + !opt.matchAll("^indent-").empty) { + o = opts.i[opt]; + } + params = format!"%s&%s=%s"(params, opt, o.to!string()); + } } - if (!validateBook(book)) - throw new ESVException("Invalid book"); - if (!validateVerse(verse)) - throw new ESVException("Invalid verse format"); - - string apiURL = format!"%s/%s/?q=%s+%s%s%s"(_url, _mode, - book.capitalize().replaceAll(regex(" "), "+"), verse, - assembleParameters(), extraParameters); - auto request = HTTP(apiURL); - string response; + request = HTTP( + format!"%s/text/?q=%s+%s%s%s"(_url, book + .capitalize() + .replaceAll(regex(" "), "+"), + verse, params, extraParameters) + ); request.onProgress = onProgress; - request.onReceive = (ubyte[] data) - { - response = cast(string)data; - return data.length; - }; + request.onReceive = + (ubyte[] data) + { + response ~= data; + return data.length; + }; request.addRequestHeader("Authorization", "Token " ~ _key); request.perform(); return response.parseJSON()["passages"][0].str; @@ -273,103 +280,110 @@ class ESVApi * contained in the argument book. The verse(s) being looked up are * contained in the argument verses. * - * Example: getVerses("John", "3:16-21") + * Example: getAudioVerses("John", "3:16-21") */ string getAudioVerses(in char[] book, in char[] verse) const + in (bookValid(book), "Invalid book") + in (verseValid(verse), "Invalid verse format") { - if (!validateBook(book)) - throw new ESVException("Invalid book"); - if (!validateVerse(verse)) - throw new ESVException("Invalid verse format"); + char[] response; + File tmpFile; - string apiURL = format!"%s/audio/?q=%s+%s"(_url, book.capitalize().replaceAll(regex(" "), "+"), verse); - auto request = HTTP(apiURL); - ubyte[] response; + auto request = HTTP(format!"%s/audio/?q=%s+%s"( + _url, book.capitalize().replaceAll(regex(" "), "+"), verse)); request.onProgress = onProgress; - request.onReceive = (ubyte[] data) - { - response ~= data; - return data.length; - }; + request.onReceive = + (ubyte[] data) + { + response ~= data; + return data.length; + }; request.addRequestHeader("Authorization", "Token " ~ _key); request.perform(); - string tmpFile = tempFile(); + tmpFile = File(_tmp, "w"); tmpFile.write(response); - return tmpFile; + return _tmp; } - private: - string assembleParameters() const pure @safe + /* + * Requests a passage search for the given query. + * If raw is false, formats the passage search as + * plain text, otherwise returns JSON from the API. + * + * Example: search("It is finished") + */ + char[] search(in string query, in bool raw = true) { - string params = ""; - string addParam(string param, string value) const pure @safe - { - return format!"%s&%s=%s"(params, param, value); + ulong i; + char[] response; + char[] layout; + HTTP request; + JSONValue json; + + request = HTTP(format!"%s/search/?q=%s"( + _url, query.replaceAll(regex(" "), "+"))); + request.onProgress = onProgress; + request.onReceive = + (ubyte[] data) + { + response ~= cast(char[])data; + return data.length; + }; + request.addRequestHeader("Authorization", "Token " ~ _key); + request.perform(); + + if (raw) + return response; + + json = response.parseJSON(); + layout = cast(char[])""; + + enforce!ESVException(json["total_results"].integer == 0, "No results for search"); + + foreach (ulong i, JSONValue result; json["results"]) { + layout ~= format!"%s\n %s\n"( + result["reference"].str, + result["content"].str + ); } - params = addParam("include-passage-references", opts.boolOpts["include_passage_references"].to!string); - params = addParam("include-verse-numbers", opts.boolOpts["include_verse_numbers"].to!string); - params = addParam("include-first-verse-numbers", opts.boolOpts["include_first_verse_numbers"].to!string); - params = addParam("include-footnotes", opts.boolOpts["include_footnotes"].to!string); - params = addParam("include-footnote-body", opts.boolOpts["include_footnote_body"].to!string); - params = addParam("include-headings", opts.boolOpts["include_headings"].to!string); - params = addParam("include-short-copyright", opts.boolOpts["include_short_copyright"].to!string); - params = addParam("include-copyright", opts.boolOpts["include_copyright"].to!string); - params = addParam("include-passage-horizontal-lines", opts.boolOpts["include_passage_horizontal_lines"].to!string); - params = addParam("include-heading-horizontal-lines", opts.boolOpts["include_heading_horizontal_lines"].to!string); - params = addParam("include-selahs", opts.boolOpts["include_selahs"].to!string); - params = addParam("indent-poetry", opts.boolOpts["indent_poetry"].to!string); - params = addParam("horizontal-line-length", opts.intOpts ["horizontal_line_length"].to!string); - params = addParam("indent-paragraphs", opts.intOpts ["indent_paragraphs"].to!string); - params = addParam("indent-poetry-lines", opts.intOpts ["indent_poetry_lines"].to!string); - params = addParam("indent-declares", opts.intOpts ["indent_declares"].to!string); - params = addParam("indent-psalm-doxology", opts.intOpts ["indent_psalm_doxology"].to!string); - params = addParam("line-length", opts.intOpts ["line_length"].to!string); - params = addParam("indent-using", opts.indent_using.to!string); - return params; - } - string tempFile() const @safe - { - auto rndNums = rndGen().map!(a => cast(ubyte)a)().take(32); - auto result = appender!string(); - Base64.encode(rndNums, result); - _tmp.mkdirRecurse(); - string f = _tmp ~ "/" ~ result.data.filter!isAlphaNum().to!string(); - return f; + + return layout; } } struct ESVApiOptions { - bool[string] boolOpts; - int[string] intOpts; - string indent_using; - void defaults() nothrow @safe + bool[string] b; + int[string] i; + ESVIndent indent_using; + + this(bool initialise) nothrow @safe { - boolOpts["include_passage_references"] = true; - boolOpts["include_verse_numbers"] = true; - boolOpts["include_first_verse_numbers"] = true; - boolOpts["include_footnotes"] = true; - boolOpts["include_footnote_body"] = true; - boolOpts["include_headings"] = true; - boolOpts["include_short_copyright"] = true; - boolOpts["include_copyright"] = false; - boolOpts["include_passage_horizontal_lines"] = false; - boolOpts["include_heading_horizontal_lines"] = false; - boolOpts["include_selahs"] = true; - boolOpts["indent_poetry"] = true; - intOpts["horizontal_line_length"] = 55; - intOpts["indent_paragraphs"] = 2; - intOpts["indent_poetry_lines"] = 4; - intOpts["indent_declares"] = 40; - intOpts["indent_psalm_doxology"] = 30; - intOpts["line_length"] = 0; - indent_using = "space"; + if (!initialise) + return; + + b["include-passage-references"] = true; + b["include-verse-numbers"] = true; + b["include-first-verse-numbers"] = true; + b["include-footnotes"] = true; + b["include-footnote-body"] = true; + b["include-headings"] = true; + b["include-short-copyright"] = true; + b["include-copyright"] = false; + b["include-passage-horizontal-lines"] = false; + b["include-heading-horizontal-lines"] = false; + b["include-selahs"] = true; + b["indent-poetry"] = true; + i["horizontal-line-length"] = 55; + i["indent-paragraphs"] = 2; + i["indent-poetry-lines"] = 4; + i["indent-declares"] = 40; + i["indent-psalm-doxology"] = 30; + i["line-length"] = 0; + indent_using = ESVIndent.TAB; } } class ESVException : Exception { - @safe this(string msg, string file = __FILE__, size_t line = __LINE__) pure - { - super(msg, file, line); - } + mixin basicExceptionCtors; } diff --git a/main.d b/main.d index c1111b3..56686d1 100644 --- a/main.d +++ b/main.d @@ -19,123 +19,139 @@ */ import std.conv : to, ConvException; -import std.file : exists, write, FileException; +import std.exception : enforce; +import std.file : exists, mkdirRecurse, write, FileException; import std.format : format; -import std.getopt : getopt, GetOptException, config; -import std.path : baseName, expandTilde, isValidPath; +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, stderr; import std.string : splitLines; +import config; import esvapi; import dini; enum VERSION = "0.2.0"; -enum DEFAULT_APIKEY = "abfb7456fa52ec4292c79e435890cfa3df14dc2b"; -enum DEFAULT_CONFIGPATH = "~/.config/esv.conf"; -enum DEFAULT_MPEGPLAYER = "mpg123"; -enum DEFAULT_PAGER = "less"; +bool aFlag; /* audio */ +string CFlag; /* config */ +bool fFlag, FFlag; /* footnotes */ +bool hFlag, HFlag; /* headings */ +int lFlag; /* line length */ +bool nFlag, NFlag; /* verse numbers */ +bool PFlag; /* disable pager */ +bool rFlag, RFlag; /* passage references */ +bool VFlag; /* show version */ -enum ENV_CONFIG = "ESV_CONFIG"; -enum ENV_PAGER = "ESV_PAGER"; -enum ENV_PLAYER = "ESV_PLAYER"; - -bool optAudio; -string optConfigPath; -bool optFootnotes; -bool optNoFootnotes; -bool optHeadings; -bool optNoHeadings; -int optLineLength = 0; -bool optVerseNumbers; -bool optNoVerseNumbers; -bool optNoPager; -bool optPassageReferences; -bool optNoPassageReferences; -bool optVersion; - -int main(string[] args) +int +main(string[] args) { - void msg(string s) - { - stderr.writefln("%s: %s", args[0].baseName(), s); + bool success; + + debug { + return run(args); } - void panic(string s) - { - import core.runtime : Runtime; - import core.stdc.stdlib : exit; - - msg(s); - scope(exit) { - Runtime.terminate(); - exit(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); } } - // Parse command-line options + return success ? 0 : 1; +} + +bool +run(string[] args) +{ + ushort lines; + string apiKey; + string configPath; + string configEnvVar; + string pager; + string verses; + Ini iniData; + ESVApi esv; + + /* Parse command-line options */ try { args.getopt( - config.bundling, - config.caseSensitive, - "a", &optAudio, - "C", &optConfigPath, - "F", &optNoFootnotes, - "f", &optFootnotes, - "H", &optNoHeadings, - "h", &optHeadings, - "l", &optLineLength, - "N", &optNoVerseNumbers, - "n", &optVerseNumbers, - "P", &optNoPager, - "R", &optNoPassageReferences, - "r", &optNoPassageReferences, - "V", &optVersion, + getoptConfig.bundling, + getoptConfig.caseSensitive, + "a", &aFlag, + "C", &CFlag, + "F", &FFlag, + "f", &fFlag, + "H", &HFlag, + "h", &hFlag, + "l", &lFlag, + "N", &NFlag, + "n", &nFlag, + "P", &PFlag, + "R", &RFlag, + "r", &rFlag, + "V", &VFlag, ); } catch (GetOptException e) { - if (!e.msg.matchFirst(regex("^Unrecognized option")).empty) - panic("unknown option " ~ e.extractOpt()); - else if (!e.msg.matchFirst(regex("^Missing value for argument")).empty) - panic("missing argument for option " ~ e.extractOpt()); - } catch (ConvException e) - panic("value provided by option -l is not convertible to an integer value; must be a non-decimal number"); + enforce(e.msg.matchFirst(regex("^Unrecognized option")).empty, + "unknown option " ~ e.extractOpt()); + enforce(e.msg.matchFirst(regex("^Missing value for argument")).empty, + "missing argument for option " ~ e.extractOpt()); - if (optVersion) { + throw new Exception(e.msg); /* catch-all */ + } catch (ConvException e) { + throw new Exception( + "illegal argument to -l option -- integer required"); + } + + if (VFlag) { writeln("esv version " ~ VERSION); - return 0; + return true; } if (args.length < 3) { stderr.writefln("usage: %s [-C config] [-l length] [-aFfHhNnPRrV] book verses", args[0].baseName()); - return 1; + return false; } - // Determine configuration file - // Options have first priority, then environment variables, then the default path - string configPath; - string configEnvVar = environment.get(ENV_CONFIG); - Ini iniData; + enforce(bookValid(args[1].parseBook()), + format!"book '%s' does not exist"(args[1])); + enforce(verseValid(args[2]), + format!"invalid verse format '%s'"(args[2])); + + /* determine configuration file + * Options have first priority, then environment variables, + * then the default path */ + configPath = environment.get(ENV_CONFIG, DEFAULT_CONFIGPATH) + .expandTilde(); try { - if (optConfigPath != "") { - if (optConfigPath.isValidPath()) - configPath = optConfigPath.expandTilde(); - else - panic(optConfigPath ~ ": invalid file path"); + if (CFlag != "") { /* if -C was given */ + enforce(isValidPath(CFlag), CFlag ~ ": invalid path"); + configPath = CFlag.expandTilde(); } else { - configPath = environment.get(ENV_CONFIG, DEFAULT_CONFIGPATH); - if (configPath.isValidPath()) - configPath = configPath.expandTilde(); - else - panic(configEnvVar ~ ": invalid file path"); + enforce(isValidPath(configPath), + configPath ~ ": invalid path"); + if (!configPath.exists()) { - configPath.write( + mkdirRecurse(configPath.dirName()); + configPath.write(format! "# Default esv configuration file. # An API key is required to access the ESV Bible API. [api] -key = " ~ DEFAULT_APIKEY ~ " +key = %s # If you really need to, you can specify # custom API parameters using `parameters`: #parameters = &my-parameter=value @@ -146,136 +162,123 @@ key = " ~ DEFAULT_APIKEY ~ " #headings = false #passage_references = false #verse_numbers = false -"); +"(DEFAULT_APIKEY)); } } iniData = Ini.Parse(configPath); } catch (FileException e) { - // filesystem syscall errors - if (!e.msg.matchFirst(regex("^" ~ configPath ~ ": [Ii]s a directory")).empty || - !e.msg.matchFirst(regex("^" ~ configPath ~ ": [Nn]o such file or directory")).empty || - !e.msg.matchFirst(regex("^" ~ configPath ~ ": [Pp]ermission denied")).empty) - panic(e.msg); + /* filesystem syscall errors */ + throw new Exception(e.msg); } - string apiKey; - try + try { apiKey = iniData["api"].getKey("key"); - catch (IniException e) - panic("API key not present in configuration file; cannot proceed"); - if (apiKey == "") - panic("API key not present in configuration file; cannot proceed"); + } catch (IniException e) { + apiKey = ""; + } + enforce(apiKey != "", + "API key not present in configuration file; cannot proceed"); - // Initialise API object and validate the book and verse - ESVApi esv = new ESVApi(apiKey, optAudio); - if (!esv.validateBook(args[1].extractBook())) - panic("book '" ~ args[1] ~ "' does not exist"); - if (!esv.validateVerse(args[2])) - panic("invalid verse format '" ~ args[2] ~ "'"); + esv = new ESVApi(apiKey); - if (optAudio) { - // check for mpg123 - if (executeShell("which " ~ DEFAULT_MPEGPLAYER ~ " >/dev/null 2>&1").status > 0) { - panic(DEFAULT_MPEGPLAYER ~ " is required for audio mode; cannot continue"); - return 1; - } else { - string tmpf = esv.getAudioVerses(args[1], args[2]); - string mpegPlayer = environment.get(ENV_PLAYER, DEFAULT_MPEGPLAYER); - /* - * esv has built-in support for mpg123 and mpv; - * other players will work, just recompile with - * the DEFAULT_MPEGPLAYER enum set differently - * or use the ESV_PLAYER environment variable - */ - if (mpegPlayer == "mpg123") - mpegPlayer = mpegPlayer ~ " -q "; - else if (mpegPlayer == "mpv") - mpegPlayer = mpegPlayer ~ " --msg-level=all=no "; - else - mpegPlayer = DEFAULT_MPEGPLAYER ~ " "; - // spawn mpg123 - executeShell(mpegPlayer ~ tmpf); - } - return 0; + if (aFlag) { + string tmpf, mpegPlayer; + + /* check for mpg123 */ + enforce(executeShell( + format!"command -v %s >/dev/null 2>&1"(DEFAULT_MPEGPLAYER)).status == 0, + DEFAULT_MPEGPLAYER ~ " is required for audio mode; cannot continue"); + + tmpf = esv.getAudioVerses(args[1], args[2]); + mpegPlayer = environment.get(ENV_PLAYER, DEFAULT_MPEGPLAYER); + /* esv has built-in support for mpg123 and mpv. + * other players will work, just recompile with + * the DEFAULT_MPEGPLAYER enum set differently + * or use the ESV_PLAYER environment variable */ + mpegPlayer ~= + mpegPlayer == "mpg123" ? " -q " : + mpegPlayer == "mpv" ? " --msg-level=all=no " : " "; + /* spawn mpg123 */ + executeShell(mpegPlayer ~ tmpf); + return true; } esv.extraParameters = iniData["api"].getKey("parameters"); string returnValid(string def, string val) { - if (val == "") - return def; - else - return val; + return val == "" ? def : val; } - // Get [passage] keys + /* Get [passage] keys */ foreach (string key; ["footnotes", "headings", "passage_references", "verse_numbers"]) { try { - esv.opts.boolOpts["include_" ~ key] = + esv.opts.b["include_" ~ key] = returnValid("true", iniData["passage"].getKey(key)).to!bool(); } catch (ConvException e) { - panic( - format!"%s: key '%s' of section 'passage' is not a boolean value (must be either 'true' or 'false')"( - configPath, key - ) + throw new Exception(format! + "%s: key '%s' of section 'passage' is not a boolean value (must be either 'true' or 'false')" + (configPath, key) ); } catch (IniException e) {} // just do nothing; use the default settings } - // Get line_length ([passage]) - try - esv.opts.intOpts["line_length"] = returnValid("0", iniData["passage"].getKey("line_length")).to!int(); + /* Get line_length ([passage]) */ + try esv.opts.i["line_length"] = returnValid("0", iniData["passage"].getKey("line_length")).to!int(); catch (ConvException e) { - panic(configPath ~ ": value '" ~ iniData["passage"].getKey("line_length") - ~ "' is not convertible to an integer value; must be a non-decimal number"); + throw new Exception( + format!"%s: illegal value '%s' -- must be an integer"( + configPath, + iniData["passage"].getKey("line_length")) + ); } catch (IniException e) {} // just do nothing; use the default setting - if (optFootnotes) esv.opts.boolOpts["include_footnotes"] = true; - if (optHeadings) esv.opts.boolOpts["include_headings"] = true; - if (optVerseNumbers) esv.opts.boolOpts["include_verse_numbers"] = true; - if (optPassageReferences) esv.opts.boolOpts["include_passage_references"] = true; - if (optNoFootnotes) esv.opts.boolOpts["include_footnotes"] = false; - if (optNoHeadings) esv.opts.boolOpts["include_headings"] = false; - if (optNoVerseNumbers) esv.opts.boolOpts["include_verse_numbers"] = false; - if (optNoPassageReferences) esv.opts.boolOpts["include_passage_references"] = false; - if (optLineLength != 0) esv.opts.intOpts ["line_length"] = optLineLength; + if (fFlag) esv.opts.b["include_footnotes"] = true; + if (hFlag) esv.opts.b["include_headings"] = true; + if (nFlag) esv.opts.b["include_verse_numbers"] = true; + if (rFlag) esv.opts.b["include_passage_references"] = true; + if (FFlag) esv.opts.b["include_footnotes"] = false; + if (HFlag) esv.opts.b["include_headings"] = false; + if (NFlag) esv.opts.b["include_verse_numbers"] = false; + if (RFlag) esv.opts.b["include_passage_references"] = false; + if (lFlag != 0) esv.opts.i["line_length"] = lFlag; - string verses = esv.getVerses(args[1].extractBook(), args[2]); - int lines; + verses = esv.getVerses(args[1].parseBook(), args[2]); foreach (string line; verses.splitLines()) ++lines; - // If the passage is very long, pipe it into a pager - if (lines > 32 && !optNoPager) { + /* If the passage is very long, pipe it into a pager */ + if (lines > 32 && !PFlag) { import std.process : pipeProcess, Redirect, wait, ProcessException; - string pager = environment.get(ENV_PAGER, DEFAULT_PAGER); + + pager = environment.get(ENV_PAGER, DEFAULT_PAGER); try { auto pipe = pipeProcess(pager, Redirect.stdin); pipe.stdin.writeln(verses); pipe.stdin.flush(); pipe.stdin.close(); pipe.pid.wait(); - } - catch (ProcessException e) { - if (!e.msg.matchFirst(regex("^Executable file not found")).empty) { - panic(e.msg + } catch (ProcessException e) { + enforce(e.msg.matchFirst(regex("^Executable file not found")).empty, + format!"%s: command not found"(e.msg .matchFirst(": (.+)$")[0] .replaceFirst(regex("^: "), "") - ~ ": command not found" - ); - } - } - } else - writeln(verses); + )); - return 0; + throw new Exception(e.msg); /* catch-all */ + } + + return true; + } + + writeln(verses); + return true; } -string extractOpt(in GetOptException e) @safe +private string extractOpt(in GetOptException e) @safe { return e.msg.matchFirst("-.")[0]; } -string extractBook(in string book) @safe +private string parseBook(in string book) @safe { return book.replaceAll(regex("[-_]"), " "); }