1/*-
2 * SPDX-License-Identifier: BSD-2-Clause
3 *
4 * Copyright (c) 2024 Rick Parrish <unitrunker@unitrunker.net>.
5 *
6 * Redistribution and use in source and binary forms, with or without
7 * modification, are permitted provided that the following conditions
8 * are met:
9 * 1. Redistributions of source code must retain the above copyright
10 *	notice, this list of conditions and the following disclaimer.
11 * 2. Redistributions in binary form must reproduce the above copyright
12 *	notice, this list of conditions and the following disclaimer in the
13 *	documentation and/or other materials provided with the distribution.
14 *
15 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
16 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
18 * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
19 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
20 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
21 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
22 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
23 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
24 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
25 * SUCH DAMAGE.
26 */
27
28#include <sys/queue.h>
29#include <sys/stat.h>
30#include <err.h>
31#include <errno.h>
32#include <fcntl.h>
33#include <grp.h>
34#include <limits.h>
35#include <mqueue.h>
36#include <pwd.h>
37#include <stdbool.h>
38#include <stdio.h>
39#include <stdlib.h>
40#include <string.h>
41#include <sysexits.h>
42#include <unistd.h>
43
44struct Creation {
45	/* true if the queue exists. */
46	bool exists;
47	/* true if a mode value was specified. */
48	bool set_mode;
49	/* access mode with rwx permission bits. */
50	mode_t mode;
51	/* maximum queue depth. default to an invalid depth. */
52	long depth;
53	/* maximum message size. default to an invalid size. */
54	long size;
55	/* true for blocking I/O and false for non-blocking I/O. */
56	bool block;
57	/* true if a group ID was specified. */
58	bool set_group;
59	/* group ID. */
60	gid_t group;
61	/* true if a user ID was specified. */
62	bool set_user;
63	/* user ID. */
64	uid_t user;
65};
66
67struct element {
68	STAILQ_ENTRY(element) links;
69	const char *text;
70};
71
72static struct element *
73malloc_element(const char *context)
74{
75	struct element *item = malloc(sizeof(struct element));
76
77	if (item == NULL)
78		/* the only non-EX_* prefixed exit code. */
79		err(1, "malloc(%s)", context);
80	return (item);
81}
82
83static STAILQ_HEAD(tqh, element)
84	queues = STAILQ_HEAD_INITIALIZER(queues),
85	contents = STAILQ_HEAD_INITIALIZER(contents);
86/* send defaults to medium priority. */
87static long priority = MQ_PRIO_MAX / 2;
88static struct Creation creation = {
89	.exists = false,
90	.set_mode = false,
91	.mode = 0755,
92	.depth = -1,
93	.size = -1,
94	.block = true,
95	.set_group = false,
96	.group = 0,
97	.set_user = false,
98	.user = 0
99};
100static const mqd_t fail = (mqd_t)-1;
101static const mode_t accepted_mode_bits =
102    S_IRWXU | S_IRWXG | S_IRWXO | S_ISUID | S_ISGID | S_ISTXT;
103
104/* OPTIONS parsing utilitarian */
105
106static void
107parse_long(const char *text, long *capture, const char *knob, const char *name)
108{
109	char *cursor = NULL;
110	long value = strtol(text, &cursor, 10);
111
112	if (cursor > text && *cursor == 0) {
113		*capture = value;
114	} else {
115		warnx("%s %s invalid format [%s].", knob, name, text);
116	}
117}
118
119static void
120parse_unsigned(const char *text, bool *set,
121   unsigned *capture, const char *knob, const char *name)
122{
123	char *cursor = NULL;
124	unsigned value = strtoul(text, &cursor, 8);
125
126	if (cursor > text && *cursor == 0) {
127		*set = true;
128		*capture = value;
129	} else {
130		warnx("%s %s format [%s] ignored.", knob, name, text);
131	}
132}
133
134static bool
135sane_queue(const char *queue)
136{
137	int size = 0;
138
139	if (queue[size] != '/') {
140		warnx("queue name [%-.*s] must start with '/'.", NAME_MAX, queue);
141		return (false);
142	}
143
144	for (size++; queue[size] != 0 && size < NAME_MAX; size++) {
145		if (queue[size] == '/') {
146			warnx("queue name [%-.*s] - only one '/' permitted.",
147			    NAME_MAX, queue);
148			return (false);
149		}
150	}
151
152	if (size == NAME_MAX && queue[size] != 0) {
153		warnx("queue name [%-.*s...] may not be longer than %d.",
154		    NAME_MAX, queue, NAME_MAX);
155		return (false);
156	}
157	return (true);
158}
159
160/* OPTIONS parsers */
161
162static void
163parse_block(const char *text)
164{
165	if (strcmp(text, "true") == 0 || strcmp(text, "yes") == 0) {
166		creation.block = true;
167	} else if (strcmp(text, "false") == 0 || strcmp(text, "no") == 0) {
168		creation.block = false;
169	} else {
170		char *cursor = NULL;
171		long value = strtol(text, &cursor, 10);
172		if (cursor > text) {
173			creation.block = value != 0;
174		} else {
175			warnx("bad -b block format [%s] ignored.", text);
176		}
177	}
178}
179
180static void
181parse_content(const char *content)
182{
183	struct element *n1 = malloc_element("content");
184
185	n1->text = content;
186	STAILQ_INSERT_TAIL(&contents, n1, links);
187}
188
189static void
190parse_depth(const char *text)
191{
192	parse_long(text, &creation.depth, "-d", "depth");
193}
194
195static void
196parse_group(const char *text)
197{
198	struct group *entry = getgrnam(text);
199
200	if (entry == NULL) {
201		parse_unsigned(text, &creation.set_group,
202		    &creation.group, "-g", "group");
203	} else {
204		creation.set_group = true;
205		creation.group = entry->gr_gid;
206	}
207}
208
209static void
210parse_mode(const char *text)
211{
212	char *cursor = NULL;
213	long value = strtol(text, &cursor, 8);
214
215	// verify only accepted mode bits are set.
216	if (cursor > text && *cursor == 0 && (value & accepted_mode_bits) == value) {
217		creation.set_mode = true;
218		creation.mode = (mode_t)value;
219	} else {
220		warnx("impossible -m mode value [%s] ignored.", text);
221	}
222}
223
224static void
225parse_priority(const char *text)
226{
227	char *cursor = NULL;
228	long value = strtol(text, &cursor, 10);
229
230	if (cursor > text && *cursor == 0) {
231		if (value >= 0 && value < MQ_PRIO_MAX) {
232			priority = value;
233		} else {
234			warnx("bad -p priority range [%s] ignored.", text);
235		}
236	} else {
237		warnx("bad -p priority format [%s] ignored.", text);
238	}
239}
240
241static void
242parse_queue(const char *queue)
243{
244	if (sane_queue(queue)) {
245		struct element *n1 = malloc_element("queue name");
246
247		n1->text = queue;
248		STAILQ_INSERT_TAIL(&queues, n1, links);
249	}
250}
251
252static void
253parse_single_queue(const char *queue)
254{
255	if (sane_queue(queue)) {
256		if (STAILQ_EMPTY(&queues)) {
257			struct element *n1 = malloc_element("queue name");
258
259			n1->text = queue;
260			STAILQ_INSERT_TAIL(&queues, n1, links);
261		} else
262			warnx("ignoring extra -q queue [%s].", queue);
263	}
264}
265
266static void
267parse_size(const char *text)
268{
269	parse_long(text, &creation.size, "-s", "size");
270}
271
272static void
273parse_user(const char *text)
274{
275	struct passwd *entry = getpwnam(text);
276	if (entry == NULL) {
277		parse_unsigned(text, &creation.set_user,
278		    &creation.user, "-u", "user");
279	} else {
280		creation.set_user = true;
281		creation.user = entry->pw_uid;
282	}
283}
284
285/* OPTIONS validators */
286
287static bool
288validate_always_true(void)
289{
290	return (true);
291}
292
293static bool
294validate_content(void)
295{
296	bool valid = !STAILQ_EMPTY(&contents);
297
298	if (!valid)
299		warnx("no content to send.");
300	return (valid);
301}
302
303static bool
304validate_depth(void)
305{
306	bool valid = creation.exists || creation.depth > 0;
307
308	if (!valid)
309		warnx("-d maximum queue depth not provided.");
310	return (valid);
311}
312
313static bool
314validate_queue(void)
315{
316	bool valid = !STAILQ_EMPTY(&queues);
317
318	if (!valid)
319		warnx("missing -q, or no sane queue name given.");
320	return (valid);
321}
322
323static bool
324validate_single_queue(void)
325{
326	bool valid = !STAILQ_EMPTY(&queues) &&
327	    STAILQ_NEXT(STAILQ_FIRST(&queues), links) == NULL;
328
329	if (!valid)
330		warnx("expected one queue.");
331	return (valid);
332}
333
334static bool
335validate_size(void)
336{
337	bool valid = creation.exists || creation.size > 0;
338
339	if (!valid)
340		warnx("-s maximum message size not provided.");
341	return (valid);
342}
343
344/* OPTIONS table handling. */
345
346struct Option {
347	/* points to array of string pointers terminated by a null pointer. */
348	const char **pattern;
349	/* parse argument. */
350	void (*parse)(const char *);
351	/*
352	 * displays an error and returns false if this parameter is not valid.
353	 * returns true otherwise.
354	 */
355	bool (*validate)(void);
356};
357
358/*
359 * parse options by table.
360 * index - current index into argv list.
361 * argc, argv - command line parameters.
362 * options - null terminated list of pointers to options.
363 */
364static void
365parse_options(int index, int argc,
366    const char *argv[], const struct Option **options)
367{
368	while ((index + 1) < argc) {
369		const struct Option **cursor = options;
370		bool match = false;
371		while (*cursor != NULL && !match) {
372			const struct Option *option = cursor[0];
373			const char **pattern = option->pattern;
374
375			while (*pattern != NULL && !match) {
376				const char *knob = *pattern;
377
378				match = strcmp(knob, argv[index]) == 0;
379				if (!match)
380					pattern++;
381			}
382
383			if (match) {
384				option->parse(argv[index + 1]);
385				index += 2;
386				break;
387			}
388			cursor++;
389		}
390
391		if (!match && index < argc) {
392			warnx("skipping [%s].", argv[index]);
393			index++;
394		}
395	}
396
397	if (index < argc) {
398		warnx("skipping [%s].", argv[index]);
399	}
400}
401
402/* options - null terminated list of pointers to options. */
403static bool
404validate_options(const struct Option **options)
405{
406	bool valid = true;
407
408	while (*options != NULL) {
409		const struct Option *option = options[0];
410
411		if (!option->validate())
412			valid = false;
413		options++;
414	}
415	return (valid);
416}
417
418/* SUBCOMMANDS */
419
420/*
421 * queue: name of queue to be created.
422 * q_creation: creation parameters (copied by value).
423 */
424static int
425create(const char *queue, struct Creation q_creation)
426{
427	int flags = O_RDWR;
428	struct mq_attr stuff = {
429		.mq_curmsgs = 0,
430		.mq_maxmsg = q_creation.depth,
431		.mq_msgsize = q_creation.size,
432		.mq_flags = 0
433	};
434
435	if (!q_creation.block) {
436		flags |= O_NONBLOCK;
437		stuff.mq_flags |= O_NONBLOCK;
438	}
439
440	mqd_t handle = mq_open(queue, flags);
441	q_creation.exists = handle != fail;
442	if (!q_creation.exists) {
443		/*
444		 * apply size and depth checks here.
445		 * if queue exists, we can default to existing depth and size.
446		 * but for a new queue, we require that input.
447		 */
448		if (validate_size() && validate_depth()) {
449			/* no need to re-apply mode. */
450			q_creation.set_mode = false;
451			flags |= O_CREAT;
452			handle = mq_open(queue, flags, q_creation.mode, &stuff);
453		}
454	}
455
456	if (handle == fail) {
457		errno_t what = errno;
458
459		warnc(what, "mq_open(create)");
460		return (what);
461	}
462
463#ifdef __FreeBSD__
464	/*
465	 * undocumented.
466	 * See https://bugs.freebsd.org/bugzilla//show_bug.cgi?id=273230
467	 */
468	int fd = mq_getfd_np(handle);
469
470	if (fd < 0) {
471		errno_t what = errno;
472
473		warnc(what, "mq_getfd_np(create)");
474		mq_close(handle);
475		return (what);
476	}
477	struct stat status = {0};
478	int result = fstat(fd, &status);
479	if (result != 0) {
480		errno_t what = errno;
481
482		warnc(what, "fstat(create)");
483		mq_close(handle);
484		return (what);
485	}
486
487	/* do this only if group and / or user given. */
488	if (q_creation.set_group || q_creation.set_user) {
489		q_creation.user =
490		    q_creation.set_user ? q_creation.user : status.st_uid;
491		q_creation.group =
492		    q_creation.set_group ? q_creation.group : status.st_gid;
493		result = fchown(fd, q_creation.user, q_creation.group);
494		if (result != 0) {
495			errno_t what = errno;
496
497			warnc(what, "fchown(create)");
498			mq_close(handle);
499			return (what);
500		}
501	}
502
503	/* do this only if altering mode of an existing queue. */
504	if (q_creation.exists && q_creation.set_mode &&
505	    q_creation.mode != (status.st_mode & accepted_mode_bits)) {
506		result = fchmod(fd, q_creation.mode);
507		if (result != 0) {
508			errno_t what = errno;
509
510			warnc(what, "fchmod(create)");
511			mq_close(handle);
512			return (what);
513		}
514	}
515#endif /* __FreeBSD__ */
516
517	return (mq_close(handle));
518}
519
520/* queue: name of queue to be removed. */
521static int
522rm(const char *queue)
523{
524	int result = mq_unlink(queue);
525
526	if (result != 0) {
527		errno_t what = errno;
528
529		warnc(what, "mq_unlink");
530		return (what);
531	}
532
533	return (result);
534}
535
536/* Return the display character for non-zero mode. */
537static char
538dual(mode_t mode, char display)
539{
540	return (mode != 0 ? display : '-');
541}
542
543/* Select one of four display characters based on mode and modifier. */
544static char
545quad(mode_t mode, mode_t modifier)
546{
547	static const char display[] = "-xSs";
548	unsigned index = 0;
549	if (mode != 0)
550		index += 1;
551	if (modifier)
552		index += 2;
553	return (display[index]);
554}
555
556/* queue: name of queue to be inspected. */
557static int
558info(const char *queue)
559{
560	mqd_t handle = mq_open(queue, O_RDONLY);
561
562	if (handle == fail) {
563		errno_t what = errno;
564
565		warnc(what, "mq_open(info)");
566		return (what);
567	}
568
569	struct mq_attr actual;
570
571	int result = mq_getattr(handle, &actual);
572	if (result != 0) {
573		errno_t what = errno;
574
575		warnc(what, "mq_getattr(info)");
576		return (what);
577	}
578
579	fprintf(stdout,
580	    "queue: '%s'\nQSIZE: %lu\nMSGSIZE: %ld\nMAXMSG: %ld\n"
581	    "CURMSG: %ld\nflags: %03ld\n",
582	    queue, actual.mq_msgsize * actual.mq_curmsgs, actual.mq_msgsize,
583	    actual.mq_maxmsg, actual.mq_curmsgs, actual.mq_flags);
584#ifdef __FreeBSD__
585
586	int fd = mq_getfd_np(handle);
587	struct stat status;
588
589	result = fstat(fd, &status);
590	if (result != 0) {
591		warn("fstat(info)");
592	} else {
593		mode_t mode = status.st_mode;
594
595		fprintf(stdout, "UID: %u\nGID: %u\n", status.st_uid, status.st_gid);
596		fprintf(stdout, "MODE: %c%c%c%c%c%c%c%c%c%c\n",
597		    dual(mode & S_ISVTX, 's'),
598		    dual(mode & S_IRUSR, 'r'),
599		    dual(mode & S_IWUSR, 'w'),
600		    quad(mode & S_IXUSR, mode & S_ISUID),
601		    dual(mode & S_IRGRP, 'r'),
602		    dual(mode & S_IWGRP, 'w'),
603		    quad(mode & S_IXGRP, mode & S_ISGID),
604		    dual(mode & S_IROTH, 'r'),
605		    dual(mode & S_IWOTH, 'w'),
606		    dual(mode & S_IXOTH, 'x'));
607	}
608#endif /* __FreeBSD__ */
609
610	return (mq_close(handle));
611}
612
613/* queue: name of queue to drain one message. */
614static int
615recv(const char *queue)
616{
617	mqd_t handle = mq_open(queue, O_RDONLY);
618
619	if (handle == fail) {
620		errno_t what = errno;
621
622		warnc(what, "mq_open(recv)");
623		return (what);
624	}
625
626	struct mq_attr actual;
627
628	int result = mq_getattr(handle, &actual);
629
630	if (result != 0) {
631		errno_t what = errno;
632
633		warnc(what, "mq_attr(recv)");
634		mq_close(handle);
635		return (what);
636	}
637
638	char *text = malloc(actual.mq_msgsize + 1);
639	unsigned q_priority = 0;
640
641	memset(text, 0, actual.mq_msgsize + 1);
642	result = mq_receive(handle, text, actual.mq_msgsize, &q_priority);
643	if (result < 0) {
644		errno_t what = errno;
645
646		warnc(what, "mq_receive");
647		mq_close(handle);
648		return (what);
649	}
650
651	fprintf(stdout, "[%u]: %-*.*s\n", q_priority, result, result, text);
652	return (mq_close(handle));
653}
654
655/*
656 * queue: name of queue to send one message.
657 * text: message text.
658 * q_priority: message priority in range of 0 to 63.
659 */
660static int
661send(const char *queue, const char *text, unsigned q_priority)
662{
663	mqd_t handle = mq_open(queue, O_WRONLY);
664
665	if (handle == fail) {
666		errno_t what = errno;
667
668		warnc(what, "mq_open(send)");
669		return (what);
670	}
671
672	struct mq_attr actual;
673
674	int result = mq_getattr(handle, &actual);
675
676	if (result != 0) {
677		errno_t what = errno;
678
679		warnc(what, "mq_attr(send)");
680		mq_close(handle);
681		return (what);
682	}
683
684	int size = strlen(text);
685
686	if (size > actual.mq_msgsize) {
687		warnx("truncating message to %ld characters.\n", actual.mq_msgsize);
688		size = actual.mq_msgsize;
689	}
690
691	result = mq_send(handle, text, size, q_priority);
692
693	if (result != 0) {
694		errno_t what = errno;
695
696		warnc(what, "mq_send");
697		mq_close(handle);
698		return (what);
699	}
700
701	return (mq_close(handle));
702}
703
704static void
705usage(FILE *file)
706{
707	fprintf(file,
708	    "usage:\n\tposixmqcontrol [rm|info|recv] -q <queue>\n"
709	    "\tposixmqcontrol create -q <queue> -s <maxsize> -d <maxdepth> "
710	    "[ -m <mode> ] [ -b <block> ] [-u <uid> ] [ -g <gid> ]\n"
711	    "\tposixmqcontrol send -q <queue> -c <content> "
712	    "[-p <priority> ]\n");
713}
714
715/* end of SUBCOMMANDS */
716
717#define _countof(arg) ((sizeof(arg)) / (sizeof((arg)[0])))
718
719/* convert an errno style error code to a sysexits code. */
720static int
721grace(int err_number)
722{
723	static const int xlat[][2] = {
724		/* generally means the mqueuefs driver is not loaded. */
725		{ENOSYS, EX_UNAVAILABLE},
726		/* no such queue name. */
727		{ENOENT, EX_OSFILE},
728		{EIO, EX_IOERR},
729		{ENODEV, EX_IOERR},
730		{ENOTSUP, EX_TEMPFAIL},
731		{EAGAIN, EX_IOERR},
732		{EPERM, EX_NOPERM},
733		{EACCES, EX_NOPERM},
734		{0, EX_OK}
735	};
736
737	for (unsigned i = 0; i < _countof(xlat); i++) {
738		if (xlat[i][0] == err_number)
739			return (xlat[i][1]);
740	}
741
742	return (EX_OSERR);
743}
744
745/* OPTIONS tables */
746
747/* careful: these 'names' arrays must be terminated by a null pointer. */
748static const char *names_queue[] = {"-q", "--queue", "-t", "--topic", NULL};
749static const struct Option option_queue = {
750	.pattern = names_queue,
751	.parse = parse_queue,
752	.validate = validate_queue};
753static const struct Option option_single_queue = {
754	.pattern = names_queue,
755	.parse = parse_single_queue,
756	.validate = validate_single_queue};
757static const char *names_depth[] = {"-d", "--depth", "--maxmsg", NULL};
758static const struct Option option_depth = {
759	.pattern = names_depth,
760	.parse = parse_depth,
761	.validate = validate_always_true};
762static const char *names_size[] = {"-s", "--size", "--msgsize", NULL};
763static const struct Option option_size = {
764	.pattern = names_size,
765	.parse = parse_size,
766	.validate = validate_always_true};
767static const char *names_block[] = {"-b", "--block", NULL};
768static const struct Option option_block = {
769	.pattern = names_block,
770	.parse = parse_block,
771	.validate = validate_always_true};
772static const char *names_content[] = {
773	"-c", "--content", "--data", "--message", NULL};
774static const struct Option option_content = {
775	.pattern = names_content,
776	.parse = parse_content,
777	.validate = validate_content};
778static const char *names_priority[] = {"-p", "--priority", NULL};
779static const struct Option option_priority = {
780	.pattern = names_priority,
781	.parse = parse_priority,
782	.validate = validate_always_true};
783static const char *names_mode[] = {"-m", "--mode", NULL};
784static const struct Option option_mode = {
785	.pattern = names_mode,
786	.parse = parse_mode,
787	.validate = validate_always_true};
788static const char *names_group[] = {"-g", "--gid", NULL};
789static const struct Option option_group = {
790	.pattern = names_group,
791	.parse = parse_group,
792	.validate = validate_always_true};
793static const char *names_user[] = {"-u", "--uid", NULL};
794static const struct Option option_user = {
795	.pattern = names_user,
796	.parse = parse_user,
797	.validate = validate_always_true};
798
799/* careful: these arrays must be terminated by a null pointer. */
800#ifdef __FreeBSD__
801static const struct Option *create_options[] = {
802	&option_queue, &option_depth, &option_size, &option_block,
803	&option_mode, &option_group, &option_user, NULL};
804#else  /* !__FreeBSD__ */
805static const struct Option *create_options[] = {
806	&option_queue, &option_depth, &option_size, &option_block,
807	&option_mode, NULL};
808#endif /* __FreeBSD__ */
809static const struct Option *info_options[] = {&option_queue, NULL};
810static const struct Option *unlink_options[] = {&option_queue, NULL};
811static const struct Option *recv_options[] = {&option_single_queue, NULL};
812static const struct Option *send_options[] = {
813	&option_queue, &option_content, &option_priority, NULL};
814
815int
816main(int argc, const char *argv[])
817{
818	STAILQ_INIT(&queues);
819	STAILQ_INIT(&contents);
820
821	if (argc > 1) {
822		const char *verb = argv[1];
823		int index = 2;
824
825		if (strcmp("create", verb) == 0 || strcmp("attr", verb) == 0) {
826			parse_options(index, argc, argv, create_options);
827			if (validate_options(create_options)) {
828				int worst = 0;
829				struct element *itq;
830
831				STAILQ_FOREACH(itq, &queues, links) {
832					const char *queue = itq->text;
833
834					int result = create(queue, creation);
835					if (result != 0)
836						worst = result;
837				}
838
839				return (grace(worst));
840			}
841
842			return (EX_USAGE);
843		} else if (strcmp("info", verb) == 0 || strcmp("cat", verb) == 0) {
844			parse_options(index, argc, argv, info_options);
845			if (validate_options(info_options)) {
846				int worst = 0;
847				struct element *itq;
848
849				STAILQ_FOREACH(itq, &queues, links) {
850					const char *queue = itq->text;
851					int result = info(queue);
852
853					if (result != 0)
854						worst = result;
855				}
856
857				return (grace(worst));
858			}
859
860			return (EX_USAGE);
861		} else if (strcmp("send", verb) == 0) {
862			parse_options(index, argc, argv, send_options);
863			if (validate_options(send_options)) {
864				int worst = 0;
865				struct element *itq;
866
867				STAILQ_FOREACH(itq, &queues, links) {
868					const char *queue = itq->text;
869					struct element *itc;
870
871					STAILQ_FOREACH(itc, &contents, links) {
872						const char *content = itc->text;
873						int result = send(queue, content, priority);
874
875						if (result != 0)
876							worst = result;
877					}
878				}
879
880				return (grace(worst));
881			}
882			return (EX_USAGE);
883		} else if (strcmp("recv", verb) == 0 ||
884		    strcmp("receive", verb) == 0) {
885			parse_options(index, argc, argv, recv_options);
886			if (validate_options(recv_options)) {
887				const char *queue = STAILQ_FIRST(&queues)->text;
888				int worst = recv(queue);
889
890				return (grace(worst));
891			}
892
893			return (EX_USAGE);
894		} else if (strcmp("unlink", verb) == 0 ||
895		    strcmp("rm", verb) == 0) {
896			parse_options(index, argc, argv, unlink_options);
897			if (validate_options(unlink_options)) {
898				int worst = 0;
899				struct element *itq;
900
901				STAILQ_FOREACH(itq, &queues, links) {
902					const char *queue = itq->text;
903					int result = rm(queue);
904
905					if (result != 0)
906						worst = result;
907				}
908
909				return (grace(worst));
910			}
911
912			return (EX_USAGE);
913		} else if (strcmp("help", verb) == 0) {
914			usage(stdout);
915			return (EX_OK);
916		} else {
917			warnx("Unknown verb [%s]", verb);
918			return (EX_USAGE);
919		}
920	}
921
922	usage(stdout);
923	return (EX_OK);
924}
925