parseargs.c

/* This is free software.  See LICENSE for terms.
 * Copyright 2004 - 2015, Patricia Kirk.
 */

#include <stdio.h>
#include <stdlib.h>

#if defined(__linux__)
#define __USE_GNU	/* To get strcasestr() from string.h. */
#endif	/* __linux__ */
#include <string.h>
#undef __USE_GNU

#if defined(__linux__)
#define ARG_MAX 131072
#else	/* __linux__ */
#include <sys/param.h>
#endif	/* __linux__ */

#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <ctype.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <signal.h>
#include <netdb.h>
#include <net/if.h>
#include <getopt.h>

#include "proto.h"

extern struct global g;
extern struct data fresh;
extern struct pixstack *iconpix, *mainpix;

extern char *filename;

extern Display *maindisplay;
extern GC icongc, maingc;

char **cmdargs=NULL;

static struct option options[] = {
    {"iconic",		0,	&g.iconic,		1},	/*  0 Start iconic. */
    {"withdrawn",	0,	&g.withdrawn,		1},	/*  1 Start withdrawn. */
    {"moongraphic",	0,	&g.moongraphic,		1},	/*  2 Graphical moon phase. */
    {"daemonize",	0,	&g.daemonize,		1},	/*  3 Keep running in background. */
    {"logbeep",		0,	&g.logbeep,		1},	/*  4 Make a noise at message logging. */
    {"powerlog",	0,	&g.powerlog,		1},	/*  5 Log powerstate changes to salmon.log. */
    {"timelog",		0,	&g.timelog,		1},	/*  6 Log all time request activity. */
    {"thermlog",	0,	&g.thermlog,		1},	/*  7 Log termal zone state changes. */
    {"execlog",		0,	&g.execlog,		1},	/*  8 Log all execute requests. */
    {"execstop",	0,	&g.execstop,		1},	/*  9 Exit after execute call. */
    {"clickoff",	0,	&g.clickoff,		1},	/* 10 Shutdown at pointer event. */
    {"thermoff",	0,	&g.thermoff,		1},	/* 11 Shutdown at critical temperature level. */
    {"poweroff",	0,	&g.poweroff,		1},	/* 12 Shutdown at critical power level. */
    {"settime",		0,		NULL,		0},	/* 13 Reset system clock at startup, check UID. */
    {"phasecalc",	2,		NULL,		0},	/* 14 Moon phase calculator.  Local time or UTC. */
    {"audiodev",	2,		NULL,		0},	/* 15 Audio device. */
    {"audiocmd",	1,		NULL,		0},	/* 16 Audio command. */
    {"refresh",		1,		NULL,		0},	/* 17 Refresh rate in seconds. */
    {"magnify",		1,		NULL,		0},	/* 18 Size in multiples of 64 pixels. */
    {"pixmap",		1,		NULL,		0},	/* 19 Use a pixmap or pseudo transparent background. */
    {"appname",		1,		NULL,		0},	/* 20 Name to show in X. */
    {"position",	1,		NULL,		0},	/* 21 Location on screen. */
    {"cmdfile",		1,		NULL,		0},	/* 22 Read commands from filename. */
    {"tempdir",		1,		NULL,		0},	/* 23 Directory for manipulating pixmaps. */
    {"datadir",		1,		NULL,		0},	/* 24 Directory for sound and pixmap files. */
    {"display",		1,		NULL,		0},	/* 25 Host name and screen number if not local. */
    {"background",	1,		NULL,		0},	/* 26 Color for background. */
    {"foreground",	1,		NULL,		0},	/* 27 Color for foreground. */
    {"thermtime",	1,		NULL,		0},	/* 28 Shutdown time for thermal critical. */
    {"thermcmd",	1,		NULL,		0},	/* 29 Execute command at thermal shutdown. */
    {"clicktime",	1,		NULL,		0},	/* 30 Shutdown delay in minutes. */
    {"clicksound",	1,		NULL,		0},	/* 31 Make noise at shutdown pointer event. */
    {"stepfile",	1,		NULL,		0},	/* 32 File name of afterstep socket. */
    {"timeport",	1,		NULL,		0},	/* 33 Network time port. */
    {"timehost",	1,		NULL,		0},	/* 34 Network time server. */
    {"timepoll",	1,		NULL,		0},	/* 35 Client machine time poll rate. */
    {"powertime",	1,		NULL,		0},	/* 36 Shutdown time delay at low power. */
    {"powercmd",	1,		NULL,		0},	/* 37 Execute command at power SHUTDOWN. */
    {"execport",	1,		NULL,		0},	/* 38 Respond to execute requests on this port. */
    {"exechost",	1,		NULL,		0},	/* 39 Only used for remote execute. */
    {"execopts",	1,		NULL,		0},	/* 40 Only used for remote execute. */
    {"ctlport",		1,		NULL,		0},	/* 41 Listen for samcon on this port. */
    {"unzip",		1,		NULL,		0},	/* 42 Pixmap unzip command. */
    {"resize",		1,		NULL,		0},	/* 43 Graphics resize command. */
    {"tiler",		1,		NULL,		0},	/* 44 Graphics tile command. */
    {"talker",		1,		NULL,		0},	/* 45 Speech synthesizer command. */
    {"line",		1,		NULL,		0},	/* 46 Define one line. */
    {NULL,		0,		NULL,		0}	/* default */
};

void parseArgs(int argc, char **argv) {
    int i, len, optval, count;
    char *buffer=NULL, *ptr;
#if WITH_REMEXEC
    int port=0, oldbufsize, newbufsize=65535, remote=-1;
    socklen_t buflen=sizeof(int);
    char *host="", *execoptions=NULL;
#endif	/* WITH_REMEXEC */

    setMinums();
    if (argc > 1) {
	CALLOC(buffer, BUFPLUS, sizeof(char), "parseArgs");
	cmdargs = argv;
	count = argc;
	opterr = 0;
	/* Look for help, lines, read, print or execute as an option. */
	for (i = 1; i < count; i++) {
	    ptr = cmdargs[i];
	    while (*ptr == '-')
		ptr++;
	    if (!strcmp(ptr, "help"))
		printOpts();
	    else if (!strcmp(ptr, "lines"))
		printTop();
	    if (!strcmp(ptr, "read") || !strcmp(ptr, "print")) {
		if ((len = snprintf(buffer, BUFSIZE, "%s", argv[2])) >= BUFSIZE) {
		    snprintf(buffer, BUFSIZE, "parseArgs() file name too long \"%s\"",
			    argv[2]);	/* So clip it. */
		    gdayMate(buffer, strerror(E2BIG));
		}
		cmdargs = readArgs(buffer, argv[0], (*ptr == 'p'));
		/* This always returns a valid number. */
		count = (int)strtol(cmdargs[0], NULL, 10);
		/* cmdargs[0] may not be as big as the buffer
		 * but it will be big enough to hold argv[0].
		 */
		snprintf(cmdargs[0], BUFSIZE, "%s", argv[0]);
		break;
	    } else if (!strcmp(ptr, "execute")) {
		if (i > 1)
		    gdayMate("parseArgs() -execute must be the first option when used", NULL);
		else
		    break;
	    }
	}
#if WITH_REMEXEC
	if ((ptr = cmdargs[1])) {
	    while (*ptr == '-')
		ptr++;
	    if (!strcmp(ptr, "execute")) {
		cmdargs++;
		count--;
		buffer[0] = '\0';
		while ((i = GETOPT(count, cmdargs, "", options, &optval)) != -1) {
		    if (i == '?') {
			if (!optarg && (ptr = strchr(cmdargs[optind - 1], ' '))) {
			    *ptr = '=';
			    cmdargs--;
			    count++;
			    continue;
			} else {
			    optval = -1;
			}
		    }
		    switch (optval) {
			case 38:
			    port = parseStrtol(optarg, FALSE, "parseArgs");
			    if (port < LODYNAMIC || port > HIDYNAMIC) {
				snprintf(buffer, BUFPLUS, "parseArgs() -execute port "
					"must be in range %d - %d", LODYNAMIC, HIDYNAMIC);
				gdayMate(buffer, strerror(EDOM));
			    }
			    break;
			case 39:
			    host = optarg;
			    break;
			case 40:
			    execoptions = optarg;
			    break;
			default:
			    snprintf(buffer, BUFPLUS, "parseArgs() -execute option \"%s\" not recognized"
				    " or missing required argument",  cmdargs[optind - 1]);
			    gdayMate(buffer, NULL);
		    }
		}
		if (!port || !strlen(host))
		    gdayMate("parseArgs() -execute args must include -execport and -exechost", NULL);
		if ((remote = openSocket(host, port)) > 0) {
		    if (fcntl(remote, F_SETFL, O_NONBLOCK) < 0)
			gdayMate("parseArgs() failed to set non-blocking on remote execute socket", NULL);
		    if (getsockopt(remote, SOL_SOCKET, SO_RCVBUF, (char *)&oldbufsize, &buflen) < 0)
			oldbufsize = 0;
		    if (oldbufsize < newbufsize) {
			if (setsockopt(remote, SOL_SOCKET, SO_RCVBUF, (char *)&newbufsize, sizeof(&newbufsize)))
			    setsockopt(remote, SOL_SOCKET, SO_RCVBUF, (char *)&oldbufsize, sizeof(&oldbufsize));
		    }
		    snprintf(buffer, BUFPLUS, "salmon ");
		    if (execoptions)
			stringcat(buffer, execoptions, BUFPLUS);
		    send(remote, buffer, strlen(buffer), 256);
		    shutdown(remote, SHUT_RDWR);
		    close(remote);
		}
		gdayMate(NULL, NULL);
	    }
	}
#endif	/* WITH_REMEXEC */
	while ((i = GETOPT(count, cmdargs, "", options, &optval)) != -1) {
	    if (i == '?') {
		if (!optarg && (ptr = strchr(cmdargs[optind - 1], ' '))) {
		    /* Fixes left out equals signs in config files. */
		    *ptr = '=';
		    cmdargs--;
		    count++;
		    continue;
		} else {
		    optval = -1;
		}
	    }
	    switchOpt(optval, optarg, count, TRUE);
	}
	FREE(buffer);
    }
    setRequired();
}

int setClear(char *arg) {

    if (!strcmp(arg, "0"))
	return 0;
    return 1;
}

char *findValue(char *itsachar, int itsanint) {
    static char value[SMALBUF];

    if (itsachar)
	snprintf(value, sizeof(value), "%s", itsachar);
    else
	snprintf(value, sizeof(value), "%d", itsanint);
    return value;
}

char *findName(int index) {

    if (options[index].name)
	return (char *)options[index].name;
    return NULL;
}

int hasArg(int index) {

    return options[index].has_arg;
}

int findIndex(const char *name) {
    int i=0;

    for (;; i++) {
	if (!options[i].name || !strcmp(options[i].name, name))
	    break;
    }
    return i;
}

char *switchOpt(int optval, char *optarg, int count, int set) {
    char *smalbuf;
    int i;

    CALLOC(smalbuf, SMALBUF, sizeof(char), "switchOpt");
    /* Options 0 through 11 are set by getopt_ */
    switch (optval) {
	case 0:
	    if (!set)
		return findValue(NULL, g.iconic);
	    break;
	case 1:
	    if (!set)
		return findValue(NULL, g.withdrawn);
	    break;
	case 2:
	    if (!set)
		return findValue(NULL, g.moongraphic);
	    break;
	case 3:
	    if (!set)
		return findValue(NULL, g.daemonize);
	    break;
	case 4:
	    if (!set)
		return findValue(NULL, g.logbeep);
	    else if (optarg)
		g.logbeep = setClear(optarg);
	    break;
	case 5:
	    if (!set)
		return findValue(NULL, g.powerlog);
	    else if (optarg)
		g.powerlog = setClear(optarg);
	    break;
	case 6:
	    if (!set)
		return findValue(NULL, g.timelog);
	    else if (optarg)
		g.timelog = setClear(optarg);
	    break;
	case 7:
	    if (!set)
		return findValue(NULL, g.thermlog);
	    else if (optarg)
		g.thermlog = setClear(optarg);
	    break;
	case 8:
	    if (!set)
		return findValue(NULL, g.execlog);
	    else if (optarg)
		g.execlog = setClear(optarg);
	    break;
	case 9:
	    if (!set)
		return findValue(NULL, g.execstop);
	    else if (optarg)
		g.execstop = setClear(optarg);
	    break;
	case 10:
	    if (!set)
		return findValue(NULL, g.clickoff);
	    else if (optarg)
		g.clickoff = setClear(optarg);
	    break;
	case 11:
	    if (!set)
		return findValue(NULL, g.thermoff);
	    else if (optarg)
		g.thermoff = setClear(optarg);
	    break;
	case 12:
	    if (!set)
		return findValue(NULL, g.poweroff);
	    else if (optarg)
		g.poweroff = setClear(optarg);
	    break;
	case 13:	/* Reset system wall clock at startup if super user. */
	    if (!set) {
		return findValue(NULL, g.settime);
	    } else if (DAEMON) {
		if (!g.realuid) {
		    if (optarg)
			g.settime = setClear(optarg);
		    else
			g.settime = 1;
		}
	    }
	    break;
	case 14:	/* Moon phase calculator.  Local time or UTC. */
	    if (!set) {
		return findValue(NULL, g.phasecalc);
	    } else if (!g.running) {
		g.phasecalc = 1;
		if (optarg)
		    g.phasecalc++;
		g.linecount += 5;
	    }
	    break;
	case 15:	/* Replace audio device. */
	    if (!set) {
		if (g.audiodev)
		    return findValue(g.audiodev, 0);
		return NULL;
	    } else {
		if (optarg) {
		    STRDUP(g.audiodev, optarg);
		} else {
		    FREE(g.audiodev);
		}
	    }
	    break;
	case 16:	/* Replace audio command. */
	    if (!set) {
		if (g.audiocmd)
		    return findValue(g.audiocmd, 0);
		return NULL;
	    } else {
		STRDUP(g.audiocmd, optarg);
	    }
	    break;
	case 17:	/* Refresh rate in seconds. */
	    if (!set)
		return findValue(NULL, g.refresh);
	    else
		g.refresh = parseStrtol(optarg, TRUE, "switchOpt");
	    break;
	case 18:	/* Size in multiples of 64 pixels. */
	    if (!set)
		return findValue(NULL, g.xmag);
	    else if (DAEMON)
		g.xmag = parseStrtol(optarg, TRUE, "switchOpt");
	    break;
	case 19:	/* Use a pixmap or pseudo transparent background. */
	    if (!set)
		return findValue(NULL, g.pixmap);
	    else if (DAEMON)
		g.pixmap = parseStrtol(optarg, FALSE, "switchOpt");
	    break;
	case 20:	/* Name to show in X. */
	    if (!set) {
		if (g.appname)
		    return findValue(g.appname, 0);
		return NULL;
	    } else if (DAEMON) {
		STRDUP(g.appname, optarg);
	    }
	    break;
	case 21:	/* Location on screen. */
	    if (!set) {
		if (g.position)
		    return findValue(g.position, 0);
		return NULL;
	    } else if (DAEMON) {
		STRDUP(g.position, optarg);
	    }
	    break;
	case 22:	/* Command file to use. */
	    if (!set) {
		if (g.cmdfile)
		    return findValue(g.cmdfile, 0);
		return NULL;
	    } else {
		STRDUP(g.cmdfile, optarg);
	    }
	    break;
	case 23:	/* Directory for manipulating pixmaps. */
	    if (!set) {
		return findValue(g.tempdir, 0);
	    } else {
		STRDUP(g.tempdir, optarg);
	    }
	    break;
	case 24:	/* Directory for sound and pixmap files. */
	    if (!set) {
		return findValue(g.datadir, 0);
	    } else if (DAEMON) {
		STRDUP(g.datadir, optarg);
	    }
	    break;
	case 25:	/* Host name and screen number if not local. */
	    if (!set) {
		if (g.display)
		    return findValue(g.display, 0);
		return NULL;
	    } else if (DAEMON) {
		STRDUP(g.display, optarg);
	    }
	    break;
	case 26:	/* Replace background color. */
	    if (!set) {
		if (iconpix->colorname)
		    return findValue(iconpix->colorname, 0);
		return NULL;
	    } else if (DAEMON) {
		STRDUP(iconpix->colorname, optarg);
		STRDUP(mainpix->colorname, optarg);
	    }
	    break;
	case 27:	/* Replace foreground color. */
	    if (!set) {
		return findValue(iconpix->next->colorname, 0);
	    } else if (DAEMON) {
		STRDUP(iconpix->next->colorname, optarg);
		STRDUP(mainpix->next->colorname, optarg);
	    }
	    break;
	case 28:	/* Thermal SHUTDOWN time. */
	    if (!set)
		return findValue(NULL, g.thermtime);
	    else
		g.thermtime = parseStrtol(optarg, TRUE, "switchOpt");
	    break;
	case 29:	/* Execute command at thermal shutdown. */
	    if (!set) {
		if (g.thermcmd)
		    return findValue(g.thermcmd, 0);
		return NULL;
	    } else {
		STRDUP(g.thermcmd, optarg);
	    }
	    break;
	case 30:	/* Click SHUTDOWN time. */
	    if (!set)
		return findValue(NULL, g.clicktime);
	    else
		g.clicktime = parseStrtol(optarg, TRUE, "switchOpt");
	    break;
	case 31:	/* Make noise at pointer SHUTDOWN. */
	    if (!set)
		return findValue(NULL, g.clicksound);
	    else
		g.clicksound = parseStrtol(optarg, TRUE, "switchOpt");
	    break;
	case 32:	/* Location of afterstep socket. */
	    if (!set) {
		if (g.stepfile)
		    return findValue(g.stepfile, 0);
		return NULL;
	    } else if (!g.running) {
		STRDUP(g.stepfile, optarg);
	    }
	    break;
	case 33:	/* Network time port. */
	    if (!set) {
		return findValue(NULL, g.timeport);
	    } else if (!g.running) {
		g.timeport = parseStrtol(optarg, TRUE, "switchOpt");
		if (g.timeport && (g.timeport < LODYNAMIC || g.timeport > HIDYNAMIC)) {
		    snprintf(smalbuf, SMALBUF, "switchOpt() -timeport must"
			    " be >= %d and <= %d", LODYNAMIC, HIDYNAMIC);
		    logMessage(smalbuf);
		}
		g.timeport -= (!g.timeport);
	    }
	    break;
	case 34:	/* Network time server. */
	    if (!set) {
		if (g.timehost)
		    return findValue(g.timehost, 0);
		return NULL;
	    } else if (!g.running) {
		STRDUP(g.timehost, optarg);
	    }
	    break;
	case 35:	/* Poll rate for time service clients. */
	    if (!set)
		return findValue(NULL, g.timepoll);
	    else
		g.timepoll = parseStrtol(optarg, TRUE, "switchOpt") * 60;
	    break;
	case 36:	/* Shutdown time at low battery. */
	    if (!set)
		return findValue(NULL, g.powertime);
	    else
		g.powertime = parseStrtol(optarg, TRUE, "switchOpt");
	    break;
	case 37:	/* Execute command at low power shutdown. */
	    if (!set) {
		if (g.powercmd)
		    return findValue(g.powercmd, 0);
		return NULL;
	    } else {
		STRDUP(g.powercmd, optarg);
	    }
	    break;
	case 38:	/* Respond to execute requests on this port. */
	    if (!set) {
		return findValue(NULL, g.execport);
	    } else if (!g.running) {
		g.execport = parseStrtol(optarg, TRUE, "switchOpt");
		if (g.execport && (g.execport < LODYNAMIC || g.execport > HIDYNAMIC)) {
		    snprintf(smalbuf, SMALBUF, "switchOpt() -execport must"
			    " be >= %d and <= %d", LODYNAMIC, HIDYNAMIC);
		    logMessage(smalbuf);
		}
		g.execport -= !g.execport;
	    }
	    break;
	case 39: case 40:
	    if (g.running || !set)
		return NULL;
	    cmdargs--;	/* Used by remote execute callers only. */
	    count++;	/* Skip over and continue. */
	    break;
	case 41:	/* Listen for samcon on this port. */
	    if (!set) {
		return findValue(NULL, g.ctlport);
	    } else {
		i = parseStrtol(optarg, TRUE, "switchOpt");
		if (i < LODYNAMIC || i > HIDYNAMIC) {
		    snprintf(smalbuf, SMALBUF, "switchOpt() -ctlport must"
			    " be >= %d and <= %d", LODYNAMIC, HIDYNAMIC);
		    logMessage(smalbuf);
		} else {
		    g.ctlport = i;
		}
	    }
	    break;
	case 42:	/* Execute command to decompress pixmaps. */
	    if (!set) {
		if (g.unzip)
		    return findValue(g.unzip, 0);
		return NULL;
	    } else if (DAEMON) {
		STRDUP(g.unzip, optarg);
	    }
	    break;
	case 43:	/* Execute command to resize pixmaps. */
	    if (!set) {
		if (g.resize)
		    return findValue(g.resize, 0);
		return NULL;
	    } else if (DAEMON) {
		STRDUP(g.resize, optarg);
	    }
	    break;
	case 44:	/* Execute command to make tile backgrounds. */
	    if (!set) {
		if (g.tiler)
		    return findValue(g.tiler, 0);
		return NULL;
	    } else if (DAEMON) {
		STRDUP(g.tiler, optarg);
	    }
	    break;
	case 45:	/* Speech synthesizer command. */
	    if (!set) {
		if (g.talker)
		    return findValue(g.talker, 0);
		return NULL;
	    } else {
		STRDUP(g.talker, optarg);
	    }
	    break;
	case 46:	/* Set options for one line. */
	    if (!set)
		return NULL;
	    else if (!g.running)
		parseLine(optarg);
	    break;
	default:
	    if (g.running)	/* Ignore calls after startup. */
		return NULL;
	    snprintf(smalbuf, SMALBUF, "parseArgs() option \"%s\" not recognized"
		    " or missing/excess argument",  cmdargs[optind - 1]);
	    gdayMate(smalbuf, NULL);
    }
    FREE(smalbuf);
    return NULL;
}

int line=1;	/* Config file line counter. */

/* Trap buffer overflow or EOF.  Return a pointer to the
 * ending NULL byte, a backslash line continuation or
 * the # that begins a comment.
 */
char *checkLine(char *buffer, char *file) {
    char *end, *ptr;

    if ((end = strchr(buffer, '\n'))) {
	*end = '\0';
	if ((ptr = strchr(buffer, '#')))
	    return ptr;
	if (strlen(buffer)) {
	    ptr = buffer + strlen(buffer) - 1;
	    if (*ptr == '\\')
		return ptr;
	}
    } else {
	snprintf(buffer, BUFSIZE, "checkLine() line %d too long"
		" or lacks newline in file \"%s\"", line, file);
	gdayMate(buffer, NULL);
    }
    return end;
}

/* Look for a label in a file.  If no label has been
 * specified, return the first one if any are found.
 */
long getLabel(FILE *fp, char *label, char *file, char *quit) {
    char *buffer;
    int j;

    CALLOC(buffer, BUFSIZE, sizeof(char), "getLabel");
    if (!label) {
	STRDUP(label, ":");
    } else {
	*label = ':';
    }
    j = 0;
    while (fgets(buffer, BUFSIZE, fp)) {
	checkLine(buffer, file);
	line++;
	if (*buffer == ':' && (!strcmp(buffer, label) || !strcmp(label, ":"))) {
	    j = 1;
	    break;
	}
    }
    if (!j && !strcmp(label, ":")) {
	line = 1;
	rewind(fp);
    } else if (!j) {
	*label++ = '\0';
	snprintf(buffer, BUFSIZE,
		"getLabel() label \"%s\" not found in file \"%s\"",
		label, file);
	if (!strcmp(quit, "return")) {
	    logMessage(buffer);
	    FREE(buffer);
	    if (!strcmp(label, ":"))
		FREE(label);
	    return 0;
	} else {
	    gdayMate(buffer, NULL);
	}
    }
    FREE(buffer);
    if (!strcmp(label, ":"))
	FREE(label);
    return ftell(fp);
}

/* Read arguments from .../salmon.cfg or other file.  Start
 * at optional label.  If name starts with a colon, use name
 * as a label in the global config file or alternate cmdfile.
 * If name starts with a tilda, use it as a label in the
 * personal config file.  char *command is argv[0]; salmon,
 * /usr/local/bin/salmon, etc.
 * This is called by parseArgs(), spaceLines(), readAloud()
 * and execComm().  Call with command="return" to return
 * rather than error exit.
 */
char **readArgs(char *name, char *command, int print) {
    extern char **environ;
    int i, len=0, count=0, envsize=0, maxlen;
    long pos=0;
    char *buffer=NULL, *buff=NULL, *file=NULL, *temp=NULL;
    char *label=NULL, *ptr, *cp0, *cp1, *bgn, *end=NULL, **dest=NULL;
    FILE *fp;

    CALLOC(buffer, BUFPLUS, sizeof(char), "readArgs");
    CALLOC(buff, BUFPLUS, sizeof(char), "readArgs");
    CALLOC(file, BUFPLUS, sizeof(char), "readArgs");
    CALLOC(temp, BUFPLUS, sizeof(char), "readArgs");
    if (!strcmp(name, "(null)") || *name == ':') {
	/* Using global salmon.cfg or alternate command file.
	 * Give precedence to the alternate file if defined.
	 */
	if (g.cmdfile)
	    snprintf(file, BUFPLUS, "%s", g.cmdfile);
	else
	    snprintf(file, BUFPLUS, "%s", g.cfgfile);
	if (strcmp(name, "(null)"))
	    label = name;
    } else if (*name == '~') {
	/* Using $HOME/etc/salmon.cfg */
	snprintf(file, BUFPLUS, "%s/etc/salmon.cfg", getenv("HOME"));
	label = name;
    } else if (getenv("HOME") && !strcmp(getenv("HOME"), name)) {
	/* Trap shell expansion of tilda. */
	snprintf(file, BUFPLUS, "%s/etc/salmon.cfg", name);
    } else {
	snprintf(file, BUFPLUS, "%s", name);
    }
    if (!label && (label = strchr(file, ',')))
	/* Separate the label from the file name. */
	*label = '\0';
    if (!(fp = fopen(file, "r"))) {
	snprintf(buffer, BUFPLUS, "readArgs() failed to open file \"%s\"", file);
	if (!strcmp(command, "return")) {
	    logMessage(buffer);
	    goto nullret;
	} else {
	    gdayMate(buffer, strerror(errno));
	}
    }
    if (label || (!label && !print))
	/* If no label was passed, pick the first one if
	 * any are found unless user wants a printout
	 * of the entire file.  pos is at the line with
	 * the label or the first line if there is no
	 * label.  It's only used once to reset fp
	 * after the array has been allocated.
	 */
	pos = getLabel(fp, label, file, command);
    maxlen = strlen(command);
    if (!print) {
	/* Calc and alloc the dest array. */
	while (fgets(buff, BUFPLUS, fp)) {
	    end = checkLine(buff, file);
	    if (strstr(buff, "goto :") == buff) {
		line = 1;
		rewind(fp);
		getLabel(fp, strchr(buff, ':'), file, command);
		continue;
	    }
	    line++;
	    if (*buff == ':')
		break;
	    if (*buff == '#')
		continue;
	    if (*end == '#')
		*end-- = '\0';
	    while (isspace((int)*end))
		*end-- = '\0';
	    bgn = buff;
	    while (isspace((int)*bgn))
		bgn++;
	    len = strlen(bgn);
	    if (!len)
		continue;
	    if (len >= maxlen)
		maxlen = len + 2;
	    count++;
	}
#if defined(__NetBSD__)
	maxlen++;
#endif
	count++;
	for (i = 0;; i++) {
	    if (!environ[i])
		break;
	    envsize += (strlen(environ[i]) + 1);
	}
expand:
	if (len < 0) {
	    /* Start over with larger maxlen. */
	    FREE(dest);
	    maxlen = abs(len);
	}
	maxlen++;
	if (((count * maxlen) + envsize) > ARG_MAX) {
	    snprintf(buffer, BUFPLUS, "readArgs() %ld bytes",
		    (long int)((count * maxlen) + envsize));
	    if (!strcmp(command, "return")) {
		logMessage(buffer);
		goto nullret;
	    } else {
		gdayMate(buffer, strerror(E2BIG));
	    }
	}
	if (!(dest = (char **)calloc(count * maxlen, sizeof(char))))
	    allocFailed("readArgs");
	dest[0] = (char *)calloc(maxlen, sizeof(char));		/* For count at return. */
    }
    if (print && label)
	fprintf(stdout, "\n%s\n", label);
    fseek(fp, pos, SEEK_SET);
    temp[0] = '\0';
    count = 1;
    line = 1;
    while (fgets(buff, BUFPLUS, fp)) {
	end = checkLine(buff, file);
	if ((*buff == '#' || !strlen(buff)) && !print)
	    continue;
	if ((!print || (print && label)) && *buff == ':')
	    break;
	if ((strstr(buff, "goto :") == buff) && !(print && !label)) {
	    line = 1;
	    rewind(fp);
	    getLabel(fp, strchr(buff, ':'), file, command);
	    continue;
	}
	if (print) {
	    fprintf(stdout, "%s\n", buff);
	    continue;
	}
	if (*end == '#')
	    *end-- = '\0';
	while (isspace((int)*end))
	    *end-- = '\0';
	bgn = buff;
	while (isspace((int)*bgn))
	    bgn++;
	if (!strlen(bgn))
	    continue;
	if (*end == '\\') {
	    *end = '\0';
	    if (strlen(temp)) {
		if ((len = stringcat(temp, bgn, maxlen)) < 0)
		    goto expand;
	    } else {
		stringcpy(temp, bgn, BUFPLUS);
	    }
	    continue;
	}
	if (strlen(temp)) {
	    if ((len = stringcat(temp, bgn, maxlen)) < 0)
		goto expand;
	    stringcpy(buff, temp, BUFPLUS);
	    bgn = buff;
	    end = strchr(buff, '\0') - 1;
	    temp[0] = '\0';
	}
	while ((ptr = strcasestr(bgn, "0x"))) {
	    /* Replace 0x or 0X with # to allow hex values to be used. */
	    *ptr++ = '#';
	    memmove(ptr, ptr + 1, strlen(ptr) + 1);
	    end--;
	}
	ptr = cp0 = bgn;	/* First char in buff. */
	while ((ptr = strchr(ptr, '$'))) {
	    /* Attempt to parse environment variables. */
	    if (ptr > cp0) {
		*ptr = '\0';
		if ((len = stringcat(temp, cp0, maxlen)) < 0)
		    goto expand;
		*ptr = '$';
	    }
	    cp0 = ++ptr;
	    while (isalnum((int)*cp0) && (*cp0 == toupper((int)*cp0)))
		cp0++;
	    snprintf(buffer, (cp0 - ptr) + 1, "%s", ptr);
	    if ((cp1 = getenv(buffer))) {
		/* The envar is ptr to cp0 - 1. cp0 is the rest of buff. */
		if ((len = stringcat(temp, cp1, maxlen)) < 0)
		    goto expand;
	    } else {
		/* Not resolved, use it as is. */
		stringcat(temp, "$", BUFPLUS);
		stringcat(temp, buffer, BUFPLUS);
	    }
	    if (!strchr(cp0, '$')) {
		if ((len = stringcat(temp, cp0, maxlen)) < 0)
		    goto expand;
	    }
	}
	if (strlen(temp))
	    stringcpy(buff, temp, maxlen);
	if (strlen(buff)) {
	    snprintf(buffer, BUFPLUS, "-");
#if defined(__NetBSD__)
	    stringcat(buffer, "-", BUFPLUS);
#endif
	    stringcat(buffer, buff, BUFPLUS);
	    dest[count] = (char *)calloc(maxlen, sizeof(char));
	    snprintf(dest[count], maxlen, "%s", buffer);
	}
	temp[0] = '\0';
	count++;
	line++;
    }
    if (print) {
	if (feof(fp))
	    fprintf(stdout, "\n");
	fclose(fp);
	exit(0);
    }
    snprintf(dest[0], maxlen, "%d", count);
nullret:
    FREE(buffer);
    FREE(buff);
    FREE(file);
    FREE(temp);
    return dest;
}

const char *helpstrings[] = {
    "start iconic",
    "start withdrawn",
    "moon phase graphic, uses full window",
    "run in the background",
    "sound a beep when a message is logged",
    "log all power state changes",
    "log all time sync requests",
    "log all thermal zone state changes",
    "log all remote execute requests",
    "exit after remote execute	* servers only",
    "shutdown at pointer event",
    "shutdown at critical temperature",
    "shutdown at critical power level",
    "reset wall clock at startup",
    NULL,
    "moon phase calculator",
    "device to send noise to",
    "replace default audio command",
    "refresh rate in seconds",
    "panel/window size in multiples of 64 pixels",
    "use a background tile or pixmap",
    "name of application as seen by X",
    "position on the screen",
    "directory/name of command file",
    "pixmap manipulation and log file directory",
    "location of sound and pixmap files",
    "X host name and screen number",
    "background color",
    "foreground color",
    "time delay at thermal shutdown in minutes",
    "user command to execute at thermal shutdown",
    "time delay at pointer event shutdown",
    "sound to make at pointer event shutdown",
    "location of afterstep socket",
    "number of time sync port",
    "name of master time server",
    "poll rate for time sync clients",
    "time delay at low power shutdown",
    "user command to execute at low power shutdown",
    "remote execute port number	* servers and callers",
    "name of remote execute host	* callers only",
    "remote execute options		* callers only",
    "samcon access port number",
    "pixmap decompress command	* external application",
    "pixmap resize command		* external application",
    "pixmap tiling command		* external application",
    "speech synthesizer command	* external application",
    "define one display line",
    "",
    "When used at the command line FreeBSD, OpenBSD and Linux",
    "require one leading dash, NetBSD requires two.",
    "Unambiguous option abbreviations are recognized.",
    "",
    "Executing salmon without arguments displays:",
    "    current time as %l:%M|%p|%Z",
    "    one minute load average",
    "    free memory",
    "    free swap",
    "    processes",
    "    uptime as ddd hh:mm",
    "in salmon on a black background and -magnify=2.",
    "",
    "* An argument to phasecalc[=?] specifies UTC rather than local time.",
    "* If the audio command needs to be directed to (null), use \"audiodev\".",
    "* The execport range is 49152 to 65535 or zero to select a random port.",
    "* The exechost is the host that will launch the child process.",
    "* See $ man salmon for details on execopts.",
    "* The default decompress command is \"gunzip <\".",
    "* Pixmap manipulation requires a command line image tool (ImageMagick).",
    "* Talking clock requires a speech synthesizer (flite).",
    "",
    "Phasecalc uses the full window or one full panel in four panel mode.",
    "Moongraphic overrides everything except -magnify x and -pixmap 1.",
    "\n\t\t\t\t* * * * *",
    NULL
};

void printOpts() {
    int i=0, j=0;

    fprintf(stdout, "\n\tSalmon version %s\n\n\tsalmon [long options]\n"
	    "\n\tOptions with global effect, no argument:\n", PACKAGE_VERSION);
    for (;; i++, j++) {
	if (!helpstrings[j]) {
	    fprintf(stdout, "\n\tOptions that allow or require an argument:\n");
	    j++;
	    break;
	}
	fprintf(stdout, "\t%13s\t%s\n", options[i].name, helpstrings[j]);
    }
    for (;;) {
	if (!options[i].name)
	    break;
	fprintf(stdout, "\t%12s=\t%-32s", options[i].name, helpstrings[j]);
	if (options[i].has_arg == 2)
	    fprintf(stdout, "%s", "* optional argument");
	fprintf(stdout, "\n");
	i++;
	j++;
	if (!helpstrings[j]) {
	    j++;
	    break;
	}
    }
    for (;; j++) {
	if (!helpstrings[j])
	    break;
	fprintf(stdout, "\t%s\n", helpstrings[j]);
    }
    printLines();
    exit(0);
}
* * o * *