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

This commit is contained in:
Chet Ramey
2026-01-23 16:39:00 -05:00
parent c4b56ed9ac
commit b805bbec80
9 changed files with 305 additions and 2 deletions
+23
View File
@@ -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 <adampurkrt78@gmail.com> 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 <hanysalemtx@gmail.com>
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 <stephane@chazelas.org>
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
+1
View File
@@ -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
+31
View File
@@ -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
+7 -2
View File
@@ -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
+196
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#include <config.h>
#include "bashtypes.h"
#include <signal.h>
#if defined (HAVE_UNISTD_H)
# include <unistd.h>
#endif
#include <stdio.h>
#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 */
};
+12
View File
@@ -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 "$@"
)
+4
View File
@@ -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
+29
View File
@@ -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));
}
+2
View File
@@ -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);