boxes/src/cmdline.c
2023-12-09 14:53:13 +01:00

827 lines
26 KiB
C

/*
* boxes - Command line filter to draw/remove ASCII boxes around text
* Copyright (c) 1999-2023 Thomas Jensen and the boxes contributors
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License, version 3, as published by the Free Software Foundation.
* This program 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 this program.
* If not, see <https://www.gnu.org/licenses/>.
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*/
/*
* Processing of command line options.
*/
#include "config.h"
#include <errno.h>
#include <getopt.h>
#include <stdio.h>
#include <string.h>
#include <strings.h>
#include <uniconv.h>
#include <unistd.h>
#ifdef __MINGW32__
#include <fcntl.h> /* _O_BINARY */
#include <io.h> /* _setmode() */
#endif
#include "boxes.h"
#include "discovery.h"
#include "query.h"
#include "tools.h"
#include "cmdline.h"
extern char *optarg; /* for getopt() */
extern int optind; /* for getopt() */
/* default tab stop distance (part of -t) */
#define DEF_TABSTOP 8
/* max. allowed tab stop distance */
#define MAX_TABSTOP 16
/* System default line terminator.
* Used only for display in usage info. The real default is always "\n", with stdout in text mode. */
#ifdef __MINGW32__
#define EOL_DEFAULT "\r\n"
#else
#define EOL_DEFAULT "\n"
#endif
/**
* Print abbreviated usage information on stream `st`.
* @param st the stream to print to
*/
static void usage_short(FILE *st)
{
bx_fprintf(st, "Usage: %s [options] [infile [outfile]]\n", PROJECT);
bx_fprintf(st, "Try `%s -h' for more information.\n", PROJECT);
}
/**
* Print usage information on stream `st`, including a header text.
* @param st the stream to print to
*/
void usage_long(FILE *st)
{
bxstr_t *config_file = discover_config_file(0);
fprintf(st, "%s - draws any kind of box around your text (or removes it)\n", PROJECT);
fprintf(st, " Website: https://boxes.thomasjensen.com/\n");
fprintf(st, "Usage: %s [options] [infile [outfile]]\n", PROJECT);
fprintf(st, " -a, --align <fmt> Alignment/positioning of text inside box [default: hlvt]\n");
fprintf(st, " -c, --create <str> Use single shape box design where str is the W shape\n");
fprintf(st, " --color Force output of ANSI sequences if present\n");
fprintf(st, " --no-color Force monochrome output (no ANSI sequences)\n");
fprintf(st, " -d, --design <name> Box design [default: first one in file]\n");
fprintf(st, " -e, --eol <eol> Override line break type (experimental) [default: %s]\n",
strcmp(EOL_DEFAULT, "\r\n") == 0 ? "CRLF" : "LF");
fprintf(st, " -f, --config <file> Configuration file [default: %s]\n",
config_file != NULL ? bxs_to_output(config_file) : "none");
fprintf(st, " -h, --help Print usage information\n");
fprintf(st, " -i, --indent <mode> Indentation mode [default: box]\n");
fprintf(st, " -k <bool> Leading/trailing blank line retention on removal\n");
fprintf(st, " --kill-blank Kill leading/trailing blank lines on removal (like -k true)\n");
fprintf(st, " --no-kill-blank Retain leading/trailing blank lines on removal (like -k false)\n");
fprintf(st, " -l, --list List available box designs w/ samples\n");
fprintf(st, " -m, --mend Mend (repair) box\n");
fprintf(st, " -n, --encoding <enc> Character encoding of input and output [default: %s]\n", locale_charset());
fprintf(st, " -p, --padding <fmt> Padding [default: none]\n");
fprintf(st, " -q, --tag-query <qry> Query the list of designs by tag\n"); /* with "(undoc)" as query, trigger undocumented webui stuff instead */
fprintf(st, " -r, --remove Remove box\n");
fprintf(st, " -s, --size <wxh> Box size (width w and/or height h)\n");
fprintf(st, " -t, --tabs <str> Tab stop distance and expansion [default: %de]\n", DEF_TABSTOP);
fprintf(st, " -v, --version Print version information\n");
bxs_free(config_file);
}
static opt_t *create_new_opt()
{
opt_t *result = (opt_t *) calloc(1, sizeof(opt_t));
if (result != NULL) {
/* all values initialized with 0 or NULL */
result->color = color_from_terminal;
result->tabstop = DEF_TABSTOP;
result->eol = "\n"; /* we must default to "\n" instead of EOL_DEFAULT as long as stdout is in text mode */
result->tabexp = 'e';
result->killblank = -1;
for (int i = 0; i < NUM_SIDES; ++i) {
result->padding[i] = -1;
}
}
else {
perror(PROJECT);
}
return result;
}
/**
* Alignment/positioning of text inside box.
* @param result the options struct we are building
* @param optarg the argument to `-a` on the command line
* @returns 0 on success, anything else on error
*/
static int alignment(opt_t *result, char *optarg)
{
int errfl = 1;
char *p = optarg;
while (*p) {
errfl = 0;
if (p[1] == '\0' && !strchr("lLcCrR", *p)) {
errfl = 1;
break;
}
switch (*p) {
case 'h': case 'H':
switch (p[1]) {
case 'c': case 'C': result->halign = 'c'; break;
case 'l': case 'L': result->halign = 'l'; break;
case 'r': case 'R': result->halign = 'r'; break;
default: errfl = 1; break;
}
++p;
break;
case 'v': case 'V':
switch (p[1]) {
case 'c': case 'C': result->valign = 'c'; break;
case 't': case 'T': result->valign = 't'; break;
case 'b': case 'B': result->valign = 'b'; break;
default: errfl = 1; break;
}
++p;
break;
case 'j': case 'J':
switch (p[1]) {
case 'l': case 'L': result->justify = 'l'; break;
case 'c': case 'C': result->justify = 'c'; break;
case 'r': case 'R': result->justify = 'r'; break;
default: errfl = 1; break;
}
++p;
break;
case 'l': case 'L':
result->justify = 'l';
result->halign = 'l';
result->valign = 'c';
break;
case 'r': case 'R':
result->justify = 'r';
result->halign = 'r';
result->valign = 'c';
break;
case 'c': case 'C':
result->justify = 'c';
result->halign = 'c';
result->valign = 'c';
break;
default:
errfl = 1;
break;
}
if (errfl) {
break;
} else {
++p;
}
}
if (errfl) {
bx_fprintf(stderr, "%s: Illegal text format -- %s\n", PROJECT, optarg);
return 1;
}
return 0;
}
/**
* Command line design definition.
* @param result the options struct we are building
* @param optarg the argument to `-c` on the command line
* @returns 0 on success, anything else on error
*/
static int command_line_design(opt_t *result, char *optarg)
{
if (strlen(optarg) == 0) {
bx_fprintf(stderr, "%s: empty command line design definition\n", PROJECT);
return 2;
}
result->cld = (char *) strdup(optarg);
if (result->cld == NULL) {
perror(PROJECT);
return 1;
}
result->design_choice_by_user = 1;
return 0;
}
/**
* Box design selection.
* @param result the options struct we are building
* @param optarg the argument to `-d` on the command line
* @returns 0 on success, anything else on error
*/
static int design_choice(opt_t *result, char *optarg)
{
BFREE (result->design);
result->design = (design_t *) ((char *) strdup(optarg));
if (result->design == NULL) {
perror(PROJECT);
return 1;
}
result->design_choice_by_user = 1;
return 0;
}
/**
* EOL Override.
* @param result the options struct we are building
* @param optarg the argument to `-e` on the command line
* @returns 0 on success, anything else on error
*/
static int eol_override(opt_t *result, char *optarg)
{
result->eol_overridden = 1;
if (strcasecmp(optarg, "CRLF") == 0) {
result->eol = "\r\n";
}
else if (strcasecmp(optarg, "LF") == 0) {
result->eol = "\n";
}
else if (strcasecmp(optarg, "CR") == 0) {
result->eol = "\r";
}
else {
bx_fprintf(stderr, "%s: invalid eol spec -- %s\n", PROJECT, optarg);
return 1;
}
return 0;
}
/**
* Indentation mode.
* @param result the options struct we are building
* @param optarg the argument to `-i` on the command line
* @returns 0 on success, anything else on error
*/
static int indentation_mode(opt_t *result, char *optarg)
{
size_t optlen = strlen(optarg);
if (optlen <= 3 && !strncasecmp("box", optarg, optlen)) {
result->indentmode = 'b';
}
else if (optlen <= 4 && !strncasecmp("text", optarg, optlen)) {
result->indentmode = 't';
}
else if (optlen <= 4 && !strncasecmp("none", optarg, optlen)) {
result->indentmode = 'n';
}
else {
bx_fprintf(stderr, "%s: invalid indentation mode\n", PROJECT);
return 1;
}
return 0;
}
/**
* Kill blank lines or not [default: design-dependent].
* @param result the options struct we are building
* @param optarg the argument to `-k` on the command line
* @returns 0 on success, anything else on error
*/
static int killblank(opt_t *result, char *optarg)
{
if (result->killblank == -1) {
if (strisyes(optarg)) {
result->killblank = 1;
}
else if (strisno(optarg)) {
result->killblank = 0;
}
else {
bx_fprintf(stderr, "%s: -k: invalid parameter\n", PROJECT);
return 1;
}
}
return 0;
}
/**
* Padding. Format is `([ahvtrbl]n)+`.
* @param result the options struct we are building
* @param optarg the argument to `-p` on the command line
* @returns 0 on success, anything else on error
*/
static int padding(opt_t *result, char *optarg)
{
int errfl = 1;
char *p = optarg;
while (*p) {
errfl = 0;
if (p[1] == '\0') {
errfl = 1;
break;
}
char c = *p;
errno = 0;
int size = (int) strtol(p + 1, &p, 10);
if (errno || size < 0) {
errfl = 1;
break;
}
switch (c) {
case 'a': case 'A':
result->padding[BTOP] = size;
result->padding[BBOT] = size;
result->padding[BLEF] = size;
result->padding[BRIG] = size;
break;
case 'h': case 'H':
result->padding[BLEF] = size;
result->padding[BRIG] = size;
break;
case 'v': case 'V':
result->padding[BTOP] = size;
result->padding[BBOT] = size;
break;
case 't': case 'T':
result->padding[BTOP] = size;
break;
case 'l': case 'L':
result->padding[BLEF] = size;
break;
case 'b': case 'B':
result->padding[BBOT] = size;
break;
case 'r': case 'R':
result->padding[BRIG] = size;
break;
default:
errfl = 1;
break;
}
if (errfl) {
break;
}
}
if (errfl) {
bx_fprintf(stderr, "%s: invalid padding specification - %s\n", PROJECT, optarg);
return 1;
}
return 0;
}
/**
* Parse the tag query specified with `-q`.
* @param result the options struct we are building
* @param optarg the argument to `-q` on the command line
* @returns 0 on success, anything else on error
*/
static int query(opt_t *result, char *optarg)
{
char **query = parse_query(optarg);
result->query = query;
return query != NULL ? 0 : 1;
}
/**
* Specify desired box target size.
* @param result the options struct we are building
* @param optarg the argument to `-s` on the command line
* @returns 0 on success, anything else on error
*/
static int size_of_box(opt_t *result, char *optarg)
{
char *p = strchr(optarg, 'x');
if (!p) {
p = strchr(optarg, 'X');
}
if (p) {
*p = '\0';
}
errno = 0;
if (optarg != p) {
result->reqwidth = strtol(optarg, NULL, 10);
}
if (p) {
result->reqheight = strtol(p + 1, NULL, 10);
*p = 'x';
}
if (errno || (result->reqwidth == 0 && result->reqheight == 0) || result->reqwidth < 0 || result->reqheight < 0) {
bx_fprintf(stderr, "%s: invalid box size specification -- %s\n", PROJECT, optarg);
return 1;
}
return 0;
}
/**
* Tab handling. Format is `n[eku]`.
* @param result the options struct we are building
* @param optarg the argument to `-t` on the command line
* @returns 0 on success, anything else on error
*/
static int tab_handling(opt_t *result, char *optarg)
{
char *p;
int width = (int) strtol(optarg, &p, 10);
if (width < 1 || width > MAX_TABSTOP) {
bx_fprintf(stderr, "%s: invalid tab stop distance -- %d\n", PROJECT, width);
return 1;
}
result->tabstop = width;
int errfl = 0;
if (*p != '\0') {
if (p[1] != '\0') {
errfl = 1;
}
else {
switch (*p) {
case 'e': case 'E':
result->tabexp = 'e';
break;
case 'k': case 'K':
result->tabexp = 'k';
break;
case 'u': case 'U':
result->tabexp = 'u';
break;
default:
errfl = 1;
break;
}
}
}
if (errfl) {
bx_fprintf(stderr, "%s: invalid tab handling specification - %s\n", PROJECT, optarg);
return 1;
}
return 0;
}
/**
* Set *stdout* to binary mode, so that we can control the line terminator. This function only ever does anything on
* Windows, because on Linux, we already do have control over line terminators.
* @param result the options struct we are building
* @return the *stdout* stream, reconfigured to binary if necessary
*/
static FILE *get_stdout_configured(opt_t *result)
{
if (result->eol_overridden) {
#ifdef __MINGW32__
int rc = _setmode(fileno(stdout), _O_BINARY);
if (rc == -1) {
perror(PROJECT);
}
#endif
}
return stdout;
}
/**
* Input and Output Files. After any command line options, an input file and an output file may be specified (in that
* order). "-" may be substituted for standard input or output. A third file name would be invalid.
* @param result the options struct we are building
* @param argv the original command line options as specified
* @param optind the index of the next element to be processed in `argv`
* @returns 0 on success, anything else on error
*/
static int input_output_files(opt_t *result, char *argv[], int optind)
{
if (argv[optind] == NULL) { /* neither infile nor outfile given */
result->infile = stdin;
result->outfile = get_stdout_configured(result);
}
else if (argv[optind + 1] && argv[optind + 2]) { /* illegal third file */
bx_fprintf(stderr, "%s: illegal parameter -- %s\n", PROJECT, argv[optind + 2]);
usage_short(stderr);
return 1;
}
else {
if (strcmp(argv[optind], "-") == 0) {
result->infile = stdin; /* use stdin for input */
}
else {
result->infile = fopen(argv[optind], "r");
if (result->infile == NULL) {
bx_fprintf(stderr, "%s: Can\'t open input file -- %s\n", PROJECT, argv[optind]);
return 9; /* can't read infile */
}
}
if (argv[optind + 1] == NULL) {
result->outfile = get_stdout_configured(result); /* no outfile given */
}
else if (strcmp(argv[optind + 1], "-") == 0) {
result->outfile = get_stdout_configured(result); /* use stdout for output */
}
else {
result->outfile = fopen(argv[optind + 1], "wb");
if (result->outfile == NULL) {
perror(PROJECT);
if (result->infile != stdin) {
fclose(result->infile);
}
return 10;
}
}
}
return 0;
}
static void print_debug_info(opt_t *result)
{
if (result != NULL) {
#if defined(DEBUG)
fprintf (stderr, "Command line option settings (excerpt):\n");
fprintf (stderr, "- Alignment (-a): horiz %c, vert %c\n",
result->halign ? result->halign : '?', result->valign ? result->valign : '?');
fprintf (stderr, "- Line justification (-a): \'%c\'\n", result->justify ? result->justify : '?');
fprintf (stderr, "- Design Definition W shape (-c): %s\n", result->cld ? result->cld : "n/a");
fprintf (stderr, "- Color mode: %d\n", result->color);
fprintf (stderr, "- Line terminator used (-e): %s\n",
strcmp(result->eol, "\r\n") == 0 ? "CRLF" : (strcmp(result->eol, "\r") == 0 ? "CR" : "LF"));
fprintf (stderr, "- Explicit config file (-f): %s\n", result->f ? result->f : "no");
fprintf (stderr, "- Indentmode (-i): \'%c\'\n", result->indentmode ? result->indentmode : '?');
fprintf (stderr, "- Kill blank lines (-k): %d\n", result->killblank);
fprintf (stderr, "- Mend box (-m): %d\n", result->mend);
fprintf (stderr, "- Padding (-p): l:%d t:%d r:%d b:%d\n",
result->padding[BLEF], result->padding[BTOP], result->padding[BRIG], result->padding[BBOT]);
fprintf (stderr, "- Tag Query / Special handling for Web UI (-q): ");
if (result->query != NULL) {
for (size_t qidx = 0; result->query[qidx] != NULL; ++qidx) {
fprintf(stderr, "%s%s", qidx > 0 ? ", " : "", result->query[qidx]);
}
} else {
fprintf (stderr, "(none)");
}
fprintf (stderr, "\n");
fprintf (stderr, "- Remove box (-r): %d\n", result->r);
fprintf (stderr, "- Requested box size (-s): %ldx%ld\n", result->reqwidth, result->reqheight);
fprintf (stderr, "- Tabstop distance (-t): %d\n", result->tabstop);
fprintf (stderr, "- Tab handling (-t): \'%c\'\n", result->tabexp);
#endif
}
}
opt_t *process_commandline(int argc, char *argv[])
{
#ifdef DEBUG
fprintf(stderr, "argc = %d\n", argc);
fprintf(stderr, "argv = [");
for(int i=0; i<=argc; i++) {
fprintf(stderr, "%s%s", argv[i], i < argc ? ", " : "");
}
fprintf(stderr, "]\n");
#endif
opt_t *result = create_new_opt();
/* Intercept '-?' case first, as it is not supported by getopt_long() */
if (argc >= 2 && argv[1] != NULL && strcmp(argv[1], "-?") == 0) {
result->help = 1;
return result;
}
optind = 1; /* ensure that getopt() will process all arguments, even in unit test situations */
int option_index = 0;
const struct option long_options[] = {
{ "align", required_argument, NULL, 'a' },
{ "create", required_argument, NULL, 'c' },
{ "color", no_argument, NULL, OPT_COLOR },
{ "no-color", no_argument, NULL, OPT_NO_COLOR },
{ "design", required_argument, NULL, 'd' },
{ "eol", required_argument, NULL, 'e' },
{ "config", required_argument, NULL, 'f' },
{ "help", no_argument, NULL, 'h' },
{ "indent", required_argument, NULL, 'i' },
{ "kill-blank", no_argument, NULL, OPT_KILLBLANK },
{ "no-kill-blank", no_argument, NULL, OPT_NO_KILLBLANK },
{ "list", no_argument, NULL, 'l' },
{ "mend", no_argument, NULL, 'm' },
{ "encoding", required_argument, NULL, 'n' },
{ "padding", required_argument, NULL, 'p' },
{ "tag-query", required_argument, NULL, 'q' },
{ "remove", no_argument, NULL, 'r' },
{ "size", required_argument, NULL, 's' },
{ "tabs", required_argument, NULL, 't' },
{ "version", no_argument, NULL, 'v' },
{ NULL, 0, NULL, 0 }
};
const char *short_options = "a:c:d:e:f:hi:k:lmn:p:q:rs:t:v";
int oc; /* option character */
do {
oc = getopt_long(argc, argv, short_options, long_options, &option_index);
switch (oc) {
case 'a':
if (alignment(result, optarg) != 0) {
BFREE(result);
return NULL;
}
break;
case 'c':
if (command_line_design(result, optarg) != 0) {
BFREE(result);
return NULL;
}
break;
case OPT_COLOR:
result->color = force_ansi_color;
break;
case OPT_NO_COLOR:
result->color = force_monochrome;
break;
case 'd':
if (design_choice(result, optarg) != 0) {
BFREE(result);
return NULL;
}
break;
case 'e':
if (eol_override(result, optarg) != 0) {
BFREE(result);
return NULL;
}
break;
case 'f':
result->f = strdup(optarg); /* input file */
break;
case 'h':
result->help = 1;
return result;
case 'i':
if (indentation_mode(result, optarg) != 0) {
BFREE(result);
return NULL;
}
break;
case 'k':
if (killblank(result, optarg) != 0) {
BFREE(result);
return NULL;
}
break;
case OPT_KILLBLANK:
if (result->killblank == -1) {
result->killblank = 1;
}
break;
case OPT_NO_KILLBLANK:
if (result->killblank == -1) {
result->killblank = 0;
}
break;
case 'l':
result->l = 1; /* list available box styles */
break;
case 'm':
result->mend = 2; /* Mend box: remove, then redraw */
result->r = 1;
result->killblank = 0;
break;
case 'n':
result->encoding = strdup(optarg); /* character encoding */
if (result->encoding == NULL) {
perror(PROJECT);
BFREE(result);
return NULL;
}
break;
case 'p':
if (padding(result, optarg) != 0) {
BFREE(result);
return NULL;
}
break;
case 'q':
if (query(result, optarg) != 0) {
BFREE(result);
return NULL;
}
break;
case 'r':
result->r = 1; /* remove box */
break;
case 's':
if (size_of_box(result, optarg) != 0) {
BFREE(result);
return NULL;
}
break;
case 't':
if (tab_handling(result, optarg) != 0) {
BFREE(result);
return NULL;
}
break;
case 'v':
result->version_requested = 1; /* print version number */
return result;
case ':':
case '?':
/* Missing argument or illegal option - do nothing else */
usage_short(stderr);
BFREE(result);
return NULL;
case EOF:
/* End of list, do nothing more */
break;
default:
bx_fprintf(stderr, "%s: internal error\n", PROJECT);
BFREE(result);
return NULL;
}
} while (oc != EOF);
if (input_output_files(result, argv, optind) != 0) {
BFREE(result);
return NULL;
}
print_debug_info(result);
return result;
}
/*EOF*/ /* vim: set sw=4: */