diff --git a/.woodpecker.yml b/.woodpecker.yml index bb36052..c93b017 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yml @@ -19,7 +19,8 @@ steps: gcc g++ make pkgconf autoconf automake libtool autoconf-archive dejagnu texinfo libreadline-dev librime-dev - mkdir build && cd build && autoreconf -i .. - - ../configure --with-readline --with-rime --enable-arif-debug + - ../configure --with-readline --with-rime + --enable-arif-debug --enable-arify --enable-rl-loop CFLAGS='-O0 -g -std=c99 -Wall -Wextra -Wpedantic -Wshadow' CPPFLAGS='-D_POSIX_C_SOURCE=200112L' - make diff --git a/configure.ac b/configure.ac index d7f9906..44ffb1a 100644 --- a/configure.ac +++ b/configure.ac @@ -56,6 +56,16 @@ AM_CONDITIONAL([ENABLE_DEBUG], [test x${enable_debug} = xyes]) ARIF_ARG_ENABLE([arify], [yes], [the arify program]) AM_CONDITIONAL([BUILD_ARIFY], [test x${enable_arify} != xno]) +ARIF_ARG_ENABLE([rl-loop], [no], [the rl-loop program], [], [ + AS_VAR_IF([with_readline], [no], [ + AC_MSG_ERROR(m4_normalize([ + The rl-loop program could not be built, + since mandatory dependency GNU Readline is not configured. + ])) + ]) +]) +AM_CONDITIONAL([BUILD_RL_LOOP], [test x${enable_rl_loop} != xno]) + # -- Checks for compiler builtins and attributes -- AX_GCC_BUILTIN([__builtin_unreachable]) diff --git a/examples/Makefile.am b/examples/Makefile.am index 76131bc..a7e0ba1 100644 --- a/examples/Makefile.am +++ b/examples/Makefile.am @@ -6,9 +6,11 @@ # this notice are preserved. This file is offered as-is, without any warranty. # +bin_PROGRAMS = include_HEADERS = noinst_HEADERS = pkglib_LTLIBRARIES = +dist_man1_MANS = dist_man3_MANS = @@ -25,3 +27,14 @@ if BUILD_ARIF_RIME rime_la_SOURCES = arif_rime.c arif_rime_workaround.cc endif # BUILD_ARIF_RIME + +if BUILD_RL_LOOP + + bin_PROGRAMS += rl-loop + dist_man1_MANS += rl-loop.1 + + rl_loop_CPPFLAGS = $(READLINE_CFLAGS) + rl_loop_LDADD = $(READLINE_LIBS) + rl_loop_SOURCES = rl_loop.c + +endif # BUILD_RL_LOOP diff --git a/examples/rl-loop.1 b/examples/rl-loop.1 new file mode 100644 index 0000000..c905c5e --- /dev/null +++ b/examples/rl-loop.1 @@ -0,0 +1,88 @@ +.TH RL-LOOP 1 "Apr 10, 2024" 0.1.0 "ARIF User Manual" +. +.SH NAME +rl-loop - Readline loop +. +.SH SYNOPSIS +.B rl-loop +.RI [ options ] +.I pathname +.RI [ args ] +. +.SH DESCRIPTION +Interactively reads user input with GNU Readline. +.PP +For each line of user input, executes the program referred to by +.IR pathname , +passing +.I args +as its command-line arguments. +. +.SH OPTIONS +.TP +.B -e +Executes +.I pathname +even if the input line is empty. +.TP +\fB\-i\fR \fIreplace-idx\fR +When executing +.IR pathname , +replaces the value of the nth (starting from 1) argument referred to by +.I replace-idx +with the line of text. +.IP +If this option is not provided, or has a value of 0, the line will be written +to standard input instead. +.TP +\fB\-n\fR \fIname\fR +Readline application name. +Default value is "rl-loop". +.IP +Allows application-specific settings in a Readline init file. +See "Conditional Init Constructs" subsection of GNU Readline's user manual. +.TP +\fB\-p\fR \fIprompt\fR +The prompt text to be printed before reading user input. +Default value is "% ". +. +.SH EXIT STATUS +The program exits with status 0 if there are no errors, or 1 if otherwise. +. +.SH NOTES +The functionalities provided by this program can be achived with a simple +Bash script +.RB ( "read -e" +and friends). +.PP +However, Bash resets Readline state after each +.BR read (1), +necessitating the need to reinstall the completion functions if they are +not handled by Bash, which could be tricky to workaround. +.PP +You may find this program useful when dealing with such cases (e.g. when using +.BR arify (1)). +. +.SH EXAMPLES +Copy each line to the Wayland clipboard: +.PP +.in +4n +.EX +$ rl-loop -i4 -- wl-copy -t text/plain -- % +.EE +.in +. +.SH COPYRIGHT +Copyright (C) 2024 CismonX +.PP +Permission is granted to copy, distribute and/or modify this document +under the terms of the GNU Free Documentation License, Version 1.3 +or any later version published by the Free Software Foundation; +with no Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts. +.PP +You should have received a copy of the license along with this document. +If not, see . +. +.SH SEE ALSO +.BR bash (1), +.BR readline (3) diff --git a/examples/rl_loop.c b/examples/rl_loop.c new file mode 100644 index 0000000..7cdc86a --- /dev/null +++ b/examples/rl_loop.c @@ -0,0 +1,171 @@ +/** + * arif/examples/rl_loop.c + * ---- + * + * Copyright (C) 2024 CismonX + * + * This file is part of ARIF, Another Readline Input Framework. + * + * ARIF is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ARIF 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 ARIF. If not, see . + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#ifdef HAVE_READLINE + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include + +// Forward declaration start +static void print_usage (char const *); +static int send_line (char *, int, char *[]); +// Forward declaration end + +static void +print_usage ( + char const *program +) { + fprintf(stderr, "Usage: %s [options] pathname [args]\n\n", program); + fputs("See the rl-loop(1) man page for details.\n", stderr); +} + +static int +send_line ( + char *line, + int replace_idx, + char *argv[] +) { + int pfds[2]; + if (0 != pipe(pfds)) { + fprintf(stderr, "pipe(): %s\n", strerror(errno)); + return -1; + } + + pid_t child = fork(); + if (child < 0) { + fprintf(stderr, "fork(): %s\n", strerror(errno)); + return -1; + } + + if (child != 0) { + close(pfds[0]); + if (replace_idx == 0) { + for (size_t line_len = rl_end; line_len > 0; ) { + ssize_t nbytes = write(pfds[1], line, line_len); + if (nbytes < 0) { + fprintf(stderr, "write(): %s\n", strerror(errno)); + break; + } + line_len -= nbytes; + } + } + close(pfds[1]); + + if (-1 == waitpid(child, NULL, 0)) { + fprintf(stderr, "waitpid(): %s\n", strerror(errno)); + return -1; + } + } else { + close(pfds[1]); + if (-1 == dup2(pfds[0], STDIN_FILENO)) { + fprintf(stderr, "dup2(): %s\n", strerror(errno)); + return -1; + } + close(pfds[0]); + + if (replace_idx != 0) { + argv[replace_idx] = line; + } + if (0 != execvp(argv[0], argv)) { + fprintf(stderr, "execvp(): %s: %s\n", argv[0], strerror(errno)); + return -1; + } + } + return 0; +} + +int +main ( + int argc, + char *argv[] +) { + char const *name = "rl-loop"; + char const *prompt = "% "; + int replace_idx = 0; + bool send_empty = false; + for (int opt; -1 != (opt = getopt(argc, argv, "ei:n:p:")); ) { + switch (opt) { + case 'e': + send_empty = true; + break; + case 'i': + replace_idx = atoi(optarg); + break; + case 'n': + name = optarg; + break; + case 'p': + prompt = optarg; + break; + case '?': + default: + print_usage(argv[0]); + exit(EXIT_FAILURE); + } + } + argc -= optind; + if (argc == 0) { + print_usage(argv[0]); + exit(EXIT_FAILURE); + } + if (replace_idx < 0 || replace_idx >= argc) { + fprintf(stderr, "%s: bad option -i, expected [0, %d), got %d\n", + argv[0], argc, replace_idx); + exit(EXIT_FAILURE); + } + argv += optind; + rl_readline_name = name; + + for (char *line; ; free(line)) { + line = readline(prompt); + if (line == NULL) { + break; + } + if (rl_end > 0) { + add_history(line); + } else { + if (!send_empty) { + continue; + } + } + if (0 != send_line(line, replace_idx, argv)) { + exit(EXIT_FAILURE); + } + } + exit(EXIT_SUCCESS); +} + +#endif // defined(HAVE_READLINE)