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