/* * esv: read the Bible from your terminal * * The GPLv2 License (GPLv2) * Copyright (c) 2023-2024 Jeremy Baxter * * esv is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * esv is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with esv. If not, see . */ module esv; import std.algorithm : startsWith; import std.conv : to, ConvException; import std.file : exists, mkdirRecurse, write, FileException; import std.format : format; import std.getopt : getopt, GetOptException; import std.path : baseName, dirName, expandTilde, isValidPath; import std.process : environment, executeShell; import std.stdio : writef, writeln, writefln, File; import std.string : splitLines; import initial; import config; import esvapi; @safe: enum VERSION = "0.2.0-dev"; bool aFlag; /* audio */ string CFlag; /* config */ bool fFlag, FFlag; /* footnotes */ bool hFlag, HFlag; /* headings */ int lFlag; /* line length */ bool nFlag, NFlag; /* verse numbers */ bool rFlag, RFlag; /* passage references */ string sFlag; /* search passages */ bool VFlag; /* show version */ string[] mainArgs; File stderr; version (OpenBSD) { immutable(char) *promises; } int main(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, cfg.bundling, cfg.caseSensitive, "a", &aFlag, "C", &CFlag, "F", &FFlag, "f", &fFlag, "H", &HFlag, "h", &hFlag, "l", &lFlag, "N", &NFlag, "n", &nFlag, "R", &RFlag, "r", &rFlag, "s", &sFlag, "V", &VFlag, ); } catch (GetOptException e) { enforceDie(!e.msg.startsWith("Unrecognized option"), "unknown option " ~ e.extractOpt()); enforceDie(!e.msg.startsWith("Missing value for argument"), "missing argument for option " ~ e.extractOpt()); die(e.msg); /* catch-all */ } catch (ConvException e) { die("illegal argument to -l option -- must be integer"); } if (VFlag) { writeln("esv " ~ VERSION); return 0; } if (sFlag != "") { /* skip argument validation */ goto config; } if (args.length < 3) { stderr.writefln("usage: %s [-aFfHhNnRrV] [-C config] [-l length] [-s query] book verses", args[0].baseName()); return 1; } 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(); try { if (CFlag != "") { /* if -C was given */ enforceDie(isValidPath(CFlag), CFlag ~ ": invalid path"); configPath = CFlag.expandTilde(); } else { enforceDie(isValidPath(configPath), configPath ~ ": invalid path"); if (!configPath.exists()) { mkdirRecurse(configPath.dirName()); configPath.write(format! "## Configuration file for esv. # An API key is required to access the ESV Bible API. [api] key = %s # Settings that modify how passages are displayed: [passage] #footnotes = false #headings = false #passage-references = false #verse-numbers = false "(DEFAULT_APIKEY)); } } readINIFile(ini, configPath); } catch (FileException e) { /* filesystem syscall errors */ die(e.msg); } enforceDie(!(aFlag && sFlag), "cannot specify both -a and -s flags"); apiKey = ini["api"].key("key"); enforceDie(apiKey != null, "API key not present in configuration file; cannot proceed"); esv = new ESVApi(apiKey); if (aFlag) { string tmpf, mpegPlayer; try tmpf = esv.getAudioPassage(args[1], args[2]); catch (CurlException e) die(e.msg); mpegPlayer = environment.get(ENV_PLAYER, DEFAULT_MPEGPLAYER); /* check for an audio player */ enforceDie( executeShell( format!"command -v %s >/dev/null 2>&1"(mpegPlayer) ).status == 0, mpegPlayer ~ " is required for audio mode; cannot continue"); /* 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 the player */ executeShell(mpegPlayer ~ tmpf); return 0; } if (sFlag) { try writeln(esv.searchFormat(sFlag)); catch (ESVException) die("no results for search"); catch (CurlException e) die(e.msg); return 0; } esv.extraParameters = ini["api"].key("parameters", ""); /* Get [passage] keys */ foreach (string key; [ "footnotes", "headings", "passage-references", "verse-numbers" ]) { try esv.opts.b["include-" ~ key] = ini["passage"].keyAs!bool(key, "true"); catch (INITypeException e) die(configPath ~ ": " ~ e.msg); } /* Get line-length ([passage]) */ try esv.opts.i["line-length"] = ini["passage"].keyAs!int("line-length", "0"); catch (INITypeException e) die(configPath ~ ": " ~ e.msg); 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; try writeln(esv.getPassage(args[1].parseBook(), args[2])); catch (CurlException e) die(e.msg); 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) { import std.regex : matchFirst; return e.msg.matchFirst("-.")[0]; } private string parseBook(in string book) { import std.string : tr; return book.tr("-_", " "); }