input

Prompt for input with readline-like key bindings

git clone https://git.8pit.net/input.git

  1#include <err.h>
  2#include <errno.h>
  3#include <fcntl.h>
  4#include <libgen.h>
  5#include <limits.h>
  6#include <locale.h>
  7#include <signal.h>
  8#include <stdio.h>
  9#include <stdlib.h>
 10#include <string.h>
 11#include <unistd.h>
 12
 13#include <readline/history.h>
 14#include <readline/readline.h>
 15
 16#include <sys/stat.h>
 17#include <sys/types.h>
 18
 19#define GREPCMD "grep -F -f "
 20#define TTYDEVP "/dev/tty"
 21
 22static char *cmdbuf;
 23static char *histfp;
 24
 25static int fdtemp = -1;
 26static char fntemp[] = "/tmp/inputXXXXXX";
 27static int signals[] = {SIGINT, SIGTERM, SIGQUIT, SIGHUP};
 28
 29static void
 30usage(char *prog)
 31{
 32	char *usage = "[-1] [-w] [-c COMMAND] [-p PROMPT] "
 33	              "[-h HISTORY] [-s HISTSIZE]";
 34
 35	fprintf(stderr, "USAGE: %s %s\n", basename(prog), usage);
 36	exit(EXIT_FAILURE);
 37}
 38
 39static void
 40cleanup(void)
 41{
 42	static short clean;
 43
 44	if (clean)
 45		return; /* don't cleanup twice */
 46
 47	if (histfp) {
 48		if (write_history(histfp))
 49			warn("saving history to '%s' failed", histfp);
 50	}
 51
 52	if (fdtemp > 0) {
 53		if (close(fdtemp) == -1)
 54			warn("close failed for '%s'", fntemp);
 55		if (remove(fntemp) == -1)
 56			warn("couldn't remove file '%s'", fntemp);
 57	}
 58
 59	clean = 1;
 60}
 61
 62static void
 63onexit(void)
 64{
 65	sigset_t blockset;
 66
 67	/* cleanup is called atexit(3) and from a signal handler, block
 68	 * all signals here before invoking cleanup to ensure we are not
 69	 * interrupted by the signal handler during cleanup. */
 70
 71	if (sigfillset(&blockset) == -1)
 72		err(EXIT_FAILURE, "sigfillset failed");
 73	if (sigprocmask(SIG_BLOCK, &blockset, NULL))
 74		err(EXIT_FAILURE, "sigprocmask failed");
 75
 76	cleanup();
 77}
 78
 79static void
 80sighandler(int num)
 81{
 82	(void)num;
 83
 84	cleanup();
 85	exit(EXIT_FAILURE);
 86}
 87
 88static void
 89sethandler(void)
 90{
 91	size_t i;
 92	struct sigaction act;
 93
 94	act.sa_flags = 0;
 95	act.sa_handler = sighandler;
 96	if (sigemptyset(&act.sa_mask) == -1)
 97		err(EXIT_FAILURE, "sigemptyset failed");
 98
 99	for (i = 0; i < (sizeof(signals) / sizeof(signals[0])); i++) {
100		if (sigaction(signals[i], &act, NULL))
101			err(EXIT_FAILURE, "sigaction failed");
102	}
103}
104
105static FILE *
106fout(void)
107{
108	FILE *out;
109
110	out = stdout;
111	if (isatty(fileno(out)))
112		return out;
113
114	if (!(out = fopen(TTYDEVP, "w")))
115		err(EXIT_FAILURE, "fopen failed");
116	return out;
117}
118
119static char *
120safegrep(const char *pattern, size_t len)
121{
122	if (ftruncate(fdtemp, 0) == -1)
123		err(EXIT_FAILURE, "ftruncate failed");
124	if (lseek(fdtemp, SEEK_SET, 0) == -1)
125		err(EXIT_FAILURE, "lseek failed");
126
127	if (write(fdtemp, pattern, len) == -1 ||
128	    write(fdtemp, "\n", 1) == -1)
129		err(EXIT_FAILURE, "write failed");
130
131	return cmdbuf;
132}
133
134static char *
135gencomp(const char *input, int state)
136{
137	ssize_t n;
138	size_t inlen;
139	char *r, *cmd;
140	static FILE *pipe;
141	static char *line;
142	static size_t llen;
143
144	inlen = strlen(input);
145
146	if (!state) {
147		cmd = safegrep(input, inlen);
148		if (!(pipe = popen(cmd, "r")))
149			err(EXIT_FAILURE, "popen failed");
150	}
151
152	while ((n = getline(&line, &llen, pipe)) > 0) {
153		if (strncmp(input, line, inlen))
154			continue;
155		if (line[n - 1] == '\n')
156			line[n - 1] = '\0';
157
158		if (!(r = strdup(line)))
159			err(EXIT_FAILURE, "strdup failed");
160		return r;
161	}
162	if (ferror(pipe))
163		errx(EXIT_FAILURE, "ferror failed");
164
165	if (pclose(pipe) == -1)
166		errx(EXIT_FAILURE, "pclose failed");
167
168	return NULL;
169}
170
171static char **
172comp(const char *text, int start, int end)
173{
174	(void)start;
175	(void)end;
176
177	/* Prevent readline from performing file completions */
178	rl_attempted_completion_over = 1;
179
180	/* Don't append any character to completions */
181	rl_completion_append_character = '\0';
182
183#if RL_VERSION_MAJOR >= 6
184	/* Don't sort completions */
185	rl_sort_completion_matches = 0;
186#endif
187
188	return rl_completion_matches(text, gencomp);
189}
190
191static void
192iloop(int single, char *prompt)
193{
194	const char *line;
195
196	while ((line = readline(prompt))) {
197		/* We output empty lines intentionally. */
198		printf("%s\n", line);
199		fflush(stdout);
200
201		if (histfp && *line != '\0')
202			add_history(line);
203		if (single)
204			break;
205	}
206}
207
208static void
209confhist(char *fp, int size)
210{
211	using_history();
212	stifle_history(size);
213
214	if (!access(fp, F_OK) && read_history(fp))
215		err(EXIT_FAILURE, "read_history failed");
216}
217
218static void
219confcomp(char *compcmd, int wflag)
220{
221	int ret;
222	size_t cmdlen;
223
224	if (!(fdtemp = mkstemp(fntemp)))
225		err(EXIT_FAILURE, "mkstemp failed");
226	if (fchmod(fdtemp, 0600) == -1) /* not manadated by POSIX */
227		err(EXIT_FAILURE, "fchmod failed");
228
229	/* + 2 for the null byte and the pipe character. */
230	cmdlen = 2 + strlen(GREPCMD) + strlen(fntemp) + strlen(compcmd);
231	if (!(cmdbuf = malloc(cmdlen)))
232		err(EXIT_FAILURE, "malloc failed");
233
234	ret = snprintf(cmdbuf, cmdlen, "%s|" GREPCMD "%s", compcmd, fntemp);
235	if (ret < 0)
236		err(EXIT_FAILURE, "snprintf failed");
237	else if ((size_t)ret >= cmdlen)
238		errx(EXIT_FAILURE, "buffer for snprintf is too short");
239
240	rl_basic_word_break_characters = (wflag) ? " " : "";
241	rl_attempted_completion_function = comp;
242}
243
244int
245main(int argc, char **argv)
246{
247	int opt, hsiz, wflag, single;
248	char *prompt, *compcmd;
249
250	single = 0;
251	wflag = 0;
252	hsiz = 128;
253	prompt = "> ";
254	compcmd = NULL;
255
256	while ((opt = getopt(argc, argv, "1wc:p:h:s:")) != -1) {
257		switch (opt) {
258		case '1':
259			single = 1;
260			break;
261		case 'w':
262			wflag = 1;
263			break;
264		case 'c':
265			compcmd = optarg;
266			break;
267		case 'p':
268			prompt = optarg;
269			break;
270		case 'h':
271			histfp = optarg;
272			break;
273		case 's':
274			if (!(hsiz = strtol(optarg, (char **)NULL, 10)))
275				err(EXIT_FAILURE, "strtol failed");
276			break;
277		default:
278			usage(*argv);
279		}
280	}
281
282	rl_outstream = fout();
283	if (histfp)
284		confhist(histfp, hsiz);
285	if (compcmd)
286		confcomp(compcmd, wflag);
287	else
288		rl_bind_key('\t', rl_insert); /* disable completion */
289
290	/* setup after initialization to prevent history truncation */
291	sethandler();
292	if (atexit(onexit))
293		err(EXIT_FAILURE, "atexit failed");
294
295	iloop(single, prompt);
296	return EXIT_SUCCESS;
297}