From: Chet Ramey Date: Fri, 23 Jan 2026 21:39:00 +0000 (-0500) Subject: new `jobid' loadable builtin like ash-based shells; fix for nofork command substituti... X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=refs%2Fheads%2Fdevel;p=thirdparty%2Fbash.git new `jobid' loadable builtin like ash-based shells; fix for nofork command substitution when followed by an asynchronous subshell using GNU nohup; fix for nofork command substitution to move the file descriptor moved to the anonymous file out of the user-accessible range --- diff --git a/CWRU/CWRU.chlog b/CWRU/CWRU.chlog index 25c19cff..17d982b0 100644 --- a/CWRU/CWRU.chlog +++ b/CWRU/CWRU.chlog @@ -12596,3 +12596,26 @@ lib/readline/complete.c returned multiple matches, but an immediate subsequent completion attempt returned a single match, insert the single match From a report by Adam Purkrt in 5/2025 + + 1/22 + ---- +examples/loadables/jobid.c + - jobid: new builtin, like the NetBSD sh builtin + +subst.c + - function_substitute: unwind-protect stdin_redirected, in case the + nofork comsub causes it to be set (since it can run with pipe input + and output). + From a report by Hany Salem + + 1/23 + ---- +subst.c + - function_substitute: dup the file descriptor returned by anonopen + using move_to_high_fd to get it out of the way of user-accessible + fds. + Report from Stephane Chazelas + +lib/sh/shtty.c,include/shtty.h + - ttseteol, ttfd_seteol, tt_seteol: new functions to set the tty's + EOL character to something other than a newline diff --git a/MANIFEST b/MANIFEST index aebb98c0..8744a844 100644 --- a/MANIFEST +++ b/MANIFEST @@ -785,6 +785,7 @@ examples/loadables/getconf.c f examples/loadables/fdflags.c f examples/loadables/finfo.c f examples/loadables/fltexpr.c f +examples/loadables/jobid.c f examples/loadables/cat.c f examples/loadables/chmod.c f examples/loadables/csv.c f diff --git a/Makefile.in b/Makefile.in index 68557197..f233a4a5 100644 --- a/Makefile.in +++ b/Makefile.in @@ -179,6 +179,10 @@ UBSAN_XLDFLAGS = -fsanitize=undefined -fsanitize=local-bounds -fsanitize=vptr GCOV_XCFLAGS = -fprofile-arcs -ftest-coverage GCOV_XLDFLAGS = -fprofile-arcs -ftest-coverage +COVERAGE_XCFLAGS = -g --coverage -fprofile-arcs -ftest-coverage +COVERAGE_XCLDAGS = -g --coverage -fprofile-arcs -ftest-coverage +COVERAGE_OUT = doc/coverage + # these need CC=clang LSAN_CC = clang LSAN_XCFLAGS = -fsanitize=leak -fno-common -fno-omit-frame-pointer -fno-optimize-sibling-calls @@ -692,6 +696,33 @@ profiling-tests: ${Program} @test "X$$PROFILE_FLAGS" == "X" && { echo "profiling-tests: must be built with profiling enabled" >&2; exit 1; } @${MAKE} $(BASH_MAKEFLAGS) tests TESTSCRIPT=run-gprof +# from gnulib via groff +init-coverage: clean + lcov --directory . --zerocounters + +coverage-tests: build-coverage $(TESTS_SUPPORT) + @-test -d tests || mkdir tests + @-test -d $(COVERAGE_OUT) || mkdir $(COVERAGE_OUT) + @cp $(TESTS_SUPPORT) tests + @( cd $(srcdir)/tests && \ + BUILD_DIR=$(BUILD_DIR) PATH=$(BUILD_DIR)/tests:$$PATH THIS_SH=$(THIS_SH) $(SHELL) ${TESTSCRIPT} ) + +build-coverage: init-coverage + $(MAKE) $(BASH_MAKEFLAGS) ADDON_CFLAGS='$(COVERAGE_XCFLAGS)' \ + ADDON_LDFLAGS='$(COVERAGE_XLDFLAGS)' + +run-coverage: build-coverage coverage-tests + lcov --directory . --output-file $(COVERAGE_OUT)/$(PACKAGE).info --capture + +# external requirement: genhtml +gen-coverage: + genhtml --output-directory $(COVERAGE_OUT) \ + $(COVERAGE_OUT)/$(PACKAGE).info \ + --frames --legend \ + --title "$(PACKAGE_NAME)" + +coverage: init-coverage build-coverage gen-coverage + version.h: $(SOURCES) config.h Makefile patchlevel.h $(SHELL) $(SUPPORT_SRC)mkversion.sh -b -S ${topdir} -s $(RELSTATUS) -d $(Version) -o newversion.h \ && mv newversion.h version.h diff --git a/examples/loadables/Makefile.in b/examples/loadables/Makefile.in index afb5f12b..6e8b635b 100644 --- a/examples/loadables/Makefile.in +++ b/examples/loadables/Makefile.in @@ -103,7 +103,7 @@ INC = -I. -I.. -I$(topdir) -I$(topdir)/lib -I$(topdir)/builtins -I${srcdir} \ ALLPROG = print truefalse sleep finfo logname basename dirname fdflags \ tty pathchk tee head mkdir rmdir mkfifo mktemp printenv id whoami \ uname sync push ln unlink realpath strftime mypid setpgid seq rm \ - accept csv dsv cut stat getconf kv strptime chmod fltexpr + accept csv dsv cut stat getconf kv strptime chmod fltexpr jobid OTHERPROG = necho hello cat pushd asort SUBDIRS = perl @@ -256,6 +256,9 @@ asort: asort.o fltexpr: fltexpr.o $(SHOBJ_LD) $(SHOBJ_LDFLAGS) $(SHOBJ_XLDFLAGS) -o $@ fltexpr.o $(SHOBJ_LIBS) -lm +jobid: jobid.o + $(SHOBJ_LD) $(SHOBJ_LDFLAGS) $(SHOBJ_XLDFLAGS) -o $@ jobid.o $(SHOBJ_LIBS) + # pushd is a special case. We use the same source that the builtin version # uses, with special compilation options. @@ -322,7 +325,7 @@ OBJS = print.o truefalse.o accept.o sleep.o finfo.o getconf.o logname.o \ basename.o dirname.o tty.o pathchk.o tee.o head.o rmdir.o necho.o \ hello.o cat.o csv.o dsv.o kv.o cut.o printenv.o id.o whoami.o uname.o \ sync.o push.o mkdir.o mktemp.o realpath.o strftime.o setpgid.o stat.o \ - fdflags.o seq.o asort.o strptime.o chmod.o + fdflags.o seq.o asort.o strptime.o chmod.o fltexpr.o jobid.o ${OBJS}: ${BUILD_DIR}/config.h @@ -364,3 +367,5 @@ fdflags.o: fdflags.c seq.o: seq.c asort.o: asort.c strptime.o: strptime.c +fltexpr.o: fltexpr.c +jobid.o: jobid.c diff --git a/examples/loadables/jobid.c b/examples/loadables/jobid.c new file mode 100644 index 00000000..90c58588 --- /dev/null +++ b/examples/loadables/jobid.c @@ -0,0 +1,196 @@ +/* See Makefile for compilation details. */ + +/* + Copyright (C) 2026 Free Software Foundation, Inc. + + This file is part of GNU Bash. + Bash 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. + + Bash 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 Bash. If not, see . +*/ + +#include + +#include "bashtypes.h" +#include + +#if defined (HAVE_UNISTD_H) +# include +#endif + +#include + +#include "loadables.h" +#include "jobs.h" +#include "execute_cmd.h" + +static void +printprocs (int job) +{ + PROCESS *p; + + p = jobs[job]->pipe; + do + { + printf ("%ld", (long)p->pid); + p = p->next; + putchar (p == jobs[job]->pipe ? '\n' : ' '); + } + while (p != jobs[job]->pipe); +} + +static int +printinfo (int job, int pgrp, int jobid, int lproc) +{ + if (pgrp) + { + printf ("%ld\n", (long)jobs[job]->pgrp); + return (jobs[job]->pgrp == shell_pgrp); + } + else if (jobid) /* caller validates job */ + { + printf ("%%%d\n", job + 1); + return 0; + } + else if (lproc) + { + PROCESS *p; + + for (p = jobs[job]->pipe; p->next != jobs[job]->pipe; p = p->next) + ; + printf ("%ld\n", (long)p->pid); + return 0; + } + else + printprocs (job); + return 0; +} + +int +jobid_builtin (WORD_LIST *list) +{ + WORD_LIST *l; + int any_failed, opt, job, alljobs; + int pgrp, jobid, lproc; + sigset_t set, oset; + + pgrp = jobid = lproc = alljobs = 0; + reset_internal_getopt (); + while ((opt = internal_getopt (list, "agjp")) != -1) + { + switch (opt) + { + case 'a': alljobs = 1; break; + case 'g': pgrp = 1; break; + case 'j': jobid = 1 ; break; + case 'p': lproc = 1; break; + CASE_HELPOPT; + default: + builtin_usage (); + return (EX_USAGE); + } + } + + list = loptend; + if ((pgrp + jobid + lproc) > 1) + { + builtin_usage (); + return (EX_USAGE); + } + + any_failed = 0; + + /* The -a option means all jobs; JOBSPEC arguments ignored */ + if (alljobs) + { + if (jobs == 0) + return 0; + BLOCK_CHILD (set, oset); + for (job = 0; job < js.j_jobslots; job++) + { + if (INVALID_JOB (job)) + continue; + any_failed += printinfo (job, pgrp, jobid, lproc); + } + UNBLOCK_CHILD (oset); + return (sh_chkwrite (any_failed ? EXECUTION_FAILURE : EXECUTION_SUCCESS)); + } + + /* No JOBSPECs means current job */ + if (list == 0) + { + BLOCK_CHILD (set, oset); + job = get_job_spec (list); /* current job */ + if ((job == NO_JOB) || jobs == 0 || INVALID_JOB (job)) + { + sh_badjob ("%%"); + any_failed++; + } + else + any_failed = printinfo (job, pgrp, jobid, lproc); + UNBLOCK_CHILD (oset); + return (sh_chkwrite (any_failed ? EXECUTION_FAILURE : EXECUTION_SUCCESS)); + } + + /* Otherwise we print info about each JOBSPEC argument */ + BLOCK_CHILD (set, oset); + for (l = list; l; l = l->next) + { + job = get_job_spec (l); + if ((job == NO_JOB) || jobs == 0 || INVALID_JOB (job)) + { + sh_badjob (l->word->word); + any_failed++; + } + else + any_failed += printinfo (job, pgrp, jobid, lproc); + } + UNBLOCK_CHILD (oset); + + return (sh_chkwrite (any_failed ? EXECUTION_FAILURE : EXECUTION_SUCCESS)); +} + +char *jobid_doc[] = { + "Print information about each JOBSPEC.", + "", + "JOBSPEC is any string that can be used to refer to a job. If JOBSPEC", + "is omitted, use the current job.", + "", + "With no options, print the process IDs of the processes in each", + "JOBSPEC on a single line.", + "", + "The '-a' option prints information about each job, and any JOBSPEC", + "arguments are ignored.", + "", + "The '-g' option prints the process group for each JOBSPEC. The 'j' option", + "prints the job identifier for each JOBSPEC using \"%N\" notation, where", + "N is the job number. The 'p' option prints the process ID of the job's", + "process group leader (often the same as 'g'). Only one of these three", + "options may be used at a time.", + "", + "The return value is 2 if an invalid option was supplied, or more than", + "one valid option was supplied; 1 if the 'g' option is supplied and one of", + "the jobs is not in a separate process group; and 0 otherwise.", + (char *)NULL +}; + +/* The standard structure describing a builtin command. bash keeps an array + of these structures. The flags must include BUILTIN_ENABLED so the + builtin can be used. */ +struct builtin jobid_struct = { + "jobid", /* builtin name */ + jobid_builtin, /* function implementing the builtin */ + BUILTIN_ENABLED, /* initial flags for builtin */ + jobid_doc, /* array of long documentation strings. */ + "jobid [-a] [-g|-j|-p] [jobspec...]", /* usage synopsis; becomes short_doc */ + 0 /* reserved for internal use */ +}; diff --git a/examples/scripts/nohup.sh b/examples/scripts/nohup.sh new file mode 100644 index 00000000..299e4a05 --- /dev/null +++ b/examples/scripts/nohup.sh @@ -0,0 +1,12 @@ +( + if [ -t 1 ]; then + exec 1>>nohup.out || exec 1>>~/nohup.out + fi + if [ -t 2 ]; then + exec 2>&1 + fi + + trap '' SIGHUP + + exec "$@" +) diff --git a/include/shtty.h b/include/shtty.h index d1fac33a..e68f4193 100644 --- a/include/shtty.h +++ b/include/shtty.h @@ -85,6 +85,7 @@ extern int tt_setnoecho (TTYSTRUCT *); extern int tt_seteightbit (TTYSTRUCT *); extern int tt_setnocanon (TTYSTRUCT *); extern int tt_setcbreak (TTYSTRUCT *); +extern int tt_seteol (TTYSTRUCT *, int); /* These functions are all generally mutually exclusive. If you call more than one (bracketed with calls to ttsave and ttrestore, of @@ -101,6 +102,8 @@ extern int ttfd_nocanon (int, TTYSTRUCT *); extern int ttfd_cbreak (int, TTYSTRUCT *); +extern int ttfd_seteol (int, TTYSTRUCT *, int); + /* These functions work with fd 0 and the TTYSTRUCT saved with ttsave () */ extern int ttonechar (void); extern int ttnoecho (void); @@ -108,5 +111,6 @@ extern int tteightbit (void); extern int ttnocanon (void); extern int ttcbreak (void); +extern int ttseteol (int); #endif diff --git a/lib/sh/shtty.c b/lib/sh/shtty.c index 027941ca..fb4a98e5 100644 --- a/lib/sh/shtty.c +++ b/lib/sh/shtty.c @@ -308,3 +308,32 @@ ttcbreak (void) tt = ttin; return (ttfd_cbreak (0, &tt)); } + +int +tt_seteol (TTYSTRUCT *ttp, int c) +{ +#if defined (TERMIOS_TTY_DRIVER) || defined (TERMIO_TTY_DRIVER) + ttp->c_cc[VEOL] = c; +#endif + + return 0; +} + +int +ttfd_seteol (int fd, TTYSTRUCT *ttp, int c) +{ + if (tt_seteol (ttp, c) < 0) + return -1; + return (ttsetattr (fd, ttp)); +} + +int +ttseteol (int c) +{ + TTYSTRUCT tt; + + if (ttsaved == 0) + return -1; + tt = ttin; + return (ttfd_seteol (0, &tt, c)); +} diff --git a/subst.c b/subst.c index 320cae17..c960fafc 100644 --- a/subst.c +++ b/subst.c @@ -7060,6 +7060,7 @@ function_substitute (char *string, int quoted, int flags) sys_error ("%s", _("function_substitute: cannot open anonymous file for output")); exp_jump_to_top_level (DISCARD); /* XXX */ } + afd = move_to_high_fd (afd, 1, -1); } gs = sh_getopt_save_istate (); @@ -7082,6 +7083,7 @@ function_substitute (char *string, int quoted, int flags) unwind_protect_pointer (current_builtin); unwind_protect_pointer (currently_executing_command); unwind_protect_int (eof_encountered); + unwind_protect_int (stdin_redirected); add_unwind_protect (uw_pop_var_context, 0); add_unwind_protect (uw_maybe_restore_getopt_state, gs);