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
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
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
@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
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
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.
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
seq.o: seq.c
asort.o: asort.c
strptime.o: strptime.c
+fltexpr.o: fltexpr.c
+jobid.o: jobid.c
--- /dev/null
+/* 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 */
+};
--- /dev/null
+(
+ if [ -t 1 ]; then
+ exec 1>>nohup.out || exec 1>>~/nohup.out
+ fi
+ if [ -t 2 ]; then
+ exec 2>&1
+ fi
+
+ trap '' SIGHUP
+
+ exec "$@"
+)
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
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);
extern int ttnocanon (void);
extern int ttcbreak (void);
+extern int ttseteol (int);
#endif
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));
+}
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 ();
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);