From nobody Fri Nov 07 22:41:09 2025 X-Original-To: dev-commits-src-main@mlmmj.nyi.freebsd.org Received: from mx1.freebsd.org (mx1.freebsd.org [IPv6:2610:1c1:1:606c::19:1]) by mlmmj.nyi.freebsd.org (Postfix) with ESMTP id 4d3DYd4HJ6z6Dkgc; Fri, 07 Nov 2025 22:41:09 +0000 (UTC) (envelope-from git@FreeBSD.org) Received: from mxrelay.nyi.freebsd.org (mxrelay.nyi.freebsd.org [IPv6:2610:1c1:1:606c::19:3]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (4096 bits) server-digest SHA256 client-signature RSA-PSS (4096 bits) client-digest SHA256) (Client CN "mxrelay.nyi.freebsd.org", Issuer "R12" (verified OK)) by mx1.freebsd.org (Postfix) with ESMTPS id 4d3DYd3bJHz3V71; Fri, 07 Nov 2025 22:41:09 +0000 (UTC) (envelope-from git@FreeBSD.org) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=freebsd.org; s=dkim; t=1762555269; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding; bh=fJdnKwDZG0B+dfqxGRuj+WyBidoIWijM0nYMESXJHJg=; b=KQNduSmzO4rXrV9KJHmHnzK2SXNvi0M3Vfu7SkrElYpyBnzTsLCZLa6jO8kTCPFPQtN7Il p5RfJ6qX096MDhZe972IOus7tWkLQ5oOF7JfbZvEN0Go5mmj+EqygleKtZidlSL2TqVFz3 9BALvqFzsl41Ff/1e0lSfVNBdCy0FtJjgLVxSjjS72BLQafJLyzUaj+PsGbXYLKIXb2eGP Zr/3n/eK7EjvcbVcyBWvR5ilTztYIDT0/3tOc/6rlKiJVSjNpuCFubVIdr0gCjadsjJwRv kNqdqTpeuT7HjgV/WIl3UyWVVhy0K8mKYX+tVqgocHA9f+yrRmgN4ifwR8sB6A== ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=freebsd.org; s=dkim; t=1762555269; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding; bh=fJdnKwDZG0B+dfqxGRuj+WyBidoIWijM0nYMESXJHJg=; b=Cs6CUyqEEubPkJ9CAJqpW44s5A7bBWEOKYhsB9Bhr25K54iZgtAy4xwR/wHRb51vHJPOuU w2X5mZD2npFmcBZEWKNDeiP+uKneHTdH1z2PoT7GnhGTFuD6G0Y5u/06iGImP2tzDcR3Il QvVmm3WLY4RrJcMNGOifEdbs7vsML8P9nBiVnGwOUYr4kJxHKLER1OvW0xfRlLujL56BE7 RaHi1yjfmLmsyrGLhpEIjeoBp0Zpp8bysUlQ1nzlKasH5l3QyJTmqOD5j3YpRmJpKnXgqM e9XTkeTWFBCcZ+8fxDltsAQqiIUGyKg9BkYS3HNpuV4WXdd6fBvt7q6w4PiYUQ== ARC-Seal: i=1; s=dkim; d=freebsd.org; t=1762555269; a=rsa-sha256; cv=none; b=VYnnQ0d++vofMYFajy9mEp1IuHAkbtUM7toSxbwdnLwqey0DerLUpPgJ1DYd3+aNLhWbxd sZcA7QDmvNtw3x9xYcMKCgGlmP0Y0se+WOJ4TAxfkVJx9VbcYhpzPrZxY2C2OrXpe6s68H 9+fXhfWorp4SvjuCinIW9+OWzqGbDaR0dkfGmV1vP6symKe9e7YBRU/4HSwk7udOtHUQfC tmqNF6yK29ZL7E6V0fqTT1OS/9OOhxB6v8t3jfihrbmVUrW2pvxUQyNjm+I5QklyU/IeyC It5u8+xyiEY7g34PY1pqGWmIsgmvZFNSD9Bj42dlueA+HQCSog8SgkEBydjSsg== ARC-Authentication-Results: i=1; mx1.freebsd.org; none Received: from gitrepo.freebsd.org (gitrepo.freebsd.org [IPv6:2610:1c1:1:6068::e6a:5]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (4096 bits) server-digest SHA256) (Client did not present a certificate) by mxrelay.nyi.freebsd.org (Postfix) with ESMTPS id 4d3DYd37n2z3kN; Fri, 07 Nov 2025 22:41:09 +0000 (UTC) (envelope-from git@FreeBSD.org) Received: from gitrepo.freebsd.org ([127.0.1.44]) by gitrepo.freebsd.org (8.18.1/8.18.1) with ESMTP id 5A7Mf94b069154; Fri, 7 Nov 2025 22:41:09 GMT (envelope-from git@gitrepo.freebsd.org) Received: (from git@localhost) by gitrepo.freebsd.org (8.18.1/8.18.1/Submit) id 5A7Mf9BX069151; Fri, 7 Nov 2025 22:41:09 GMT (envelope-from git) Date: Fri, 7 Nov 2025 22:41:09 GMT Message-Id: <202511072241.5A7Mf9BX069151@gitrepo.freebsd.org> To: src-committers@FreeBSD.org, dev-commits-src-all@FreeBSD.org, dev-commits-src-main@FreeBSD.org From: Jilles Tjoelker Subject: git: f9e79facf874 - main - sh: Implement simple parameter expansion in PS1 and PS2 List-Id: Commit messages for the main branch of the src repository List-Archive: https://lists.freebsd.org/archives/dev-commits-src-main List-Help: List-Post: List-Subscribe: List-Unsubscribe: X-BeenThere: dev-commits-src-main@freebsd.org Sender: owner-dev-commits-src-main@FreeBSD.org MIME-Version: 1.0 Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: 8bit X-Git-Committer: jilles X-Git-Repository: src X-Git-Refname: refs/heads/main X-Git-Reftype: branch X-Git-Commit: f9e79facf874567f25147b24863e5198164e8d04 Auto-Submitted: auto-generated The branch main has been updated by jilles: URL: https://cgit.FreeBSD.org/src/commit/?id=f9e79facf874567f25147b24863e5198164e8d04 commit f9e79facf874567f25147b24863e5198164e8d04 Author: Matthew Phillips AuthorDate: 2025-10-12 19:27:34 +0000 Commit: Jilles Tjoelker CommitDate: 2025-11-07 22:35:18 +0000 sh: Implement simple parameter expansion in PS1 and PS2 This change follows a localized approach within getprompt() and avoids full parser reentry. While this means we don't support advanced expansions like ${parameter#pattern}, it provides POSIX-compliant basic parameter expansion without the complexity of making the parser reentrant. This is sufficient for the vast majority of use cases. PR: 46441 --- bin/sh/parser.c | 126 +++++++++++++++++++++++++++++++++++++- bin/sh/tests/parser/Makefile | 6 ++ bin/sh/tests/parser/ps1-expand1.0 | 7 +++ bin/sh/tests/parser/ps1-expand2.0 | 7 +++ bin/sh/tests/parser/ps1-expand3.0 | 8 +++ bin/sh/tests/parser/ps1-expand4.0 | 8 +++ bin/sh/tests/parser/ps1-expand5.0 | 8 +++ bin/sh/tests/parser/ps2-expand1.0 | 12 ++++ 8 files changed, 181 insertions(+), 1 deletion(-) diff --git a/bin/sh/parser.c b/bin/sh/parser.c index 0c1b7a91c257..3e42d41caec4 100644 --- a/bin/sh/parser.c +++ b/bin/sh/parser.c @@ -55,6 +55,8 @@ #include "show.h" #include "eval.h" #include "exec.h" /* to check for special builtins */ +#include "main.h" +#include "jobs.h" #ifndef NO_HISTORY #include "myhistedit.h" #endif @@ -2050,7 +2052,129 @@ getprompt(void *unused __unused) * Format prompt string. */ for (i = 0; (i < PROMPTLEN - 1) && (*fmt != '\0'); i++, fmt++) { - if (*fmt != '\\') { + if (*fmt == '$') { + const char *varname_start, *varname_end, *value; + char varname[256]; + int namelen, braced = 0; + + fmt++; /* Skip the '$' */ + + /* Check for ${VAR} syntax */ + if (*fmt == '{') { + braced = 1; + fmt++; + } + + varname_start = fmt; + + /* Extract variable name */ + if (is_digit(*fmt)) { + /* Positional parameter: $0, $1, etc. */ + fmt++; + varname_end = fmt; + } else if (is_special(*fmt)) { + /* Special parameter: $?, $!, $$, etc. */ + fmt++; + varname_end = fmt; + } else if (is_name(*fmt)) { + /* Regular variable name */ + do + fmt++; + while (is_in_name(*fmt)); + varname_end = fmt; + } else { + /* + * Not a valid variable reference. + * Output literal '$'. + */ + ps[i] = '$'; + if (braced && i < PROMPTLEN - 2) + ps[++i] = '{'; + fmt = varname_start - 1; + continue; + } + + namelen = varname_end - varname_start; + if (namelen == 0 || namelen >= (int)sizeof(varname)) { + /* Invalid or too long, output literal */ + ps[i] = '$'; + fmt = varname_start - 1; + continue; + } + + /* Copy variable name */ + memcpy(varname, varname_start, namelen); + varname[namelen] = '\0'; + + /* Handle closing brace for ${VAR} */ + if (braced) { + if (*fmt == '}') { + fmt++; + } else { + /* Missing closing brace, treat as literal */ + ps[i] = '$'; + if (i < PROMPTLEN - 2) + ps[++i] = '{'; + fmt = varname_start - 1; + continue; + } + } + + /* Look up the variable */ + if (namelen == 1 && is_digit(*varname)) { + /* Positional parameters - check digits FIRST */ + int num = *varname - '0'; + if (num == 0) + value = arg0 ? arg0 : ""; + else if (num > 0 && num <= shellparam.nparam) + value = shellparam.p[num - 1]; + else + value = ""; + } else if (namelen == 1 && is_special(*varname)) { + /* Special parameters */ + char valbuf[20]; + int num; + + switch (*varname) { + case '$': + num = rootpid; + break; + case '?': + num = exitstatus; + break; + case '#': + num = shellparam.nparam; + break; + case '!': + num = backgndpidval(); + break; + default: + num = 0; + break; + } + snprintf(valbuf, sizeof(valbuf), "%d", num); + value = valbuf; + } else { + /* Regular variables */ + value = lookupvar(varname); + if (value == NULL) + value = ""; + } + + /* Copy value to output, respecting buffer size */ + while (*value != '\0' && i < PROMPTLEN - 1) { + ps[i++] = *value++; + } + + /* + * Adjust fmt and i for the loop increment. + * fmt will be incremented by the for loop, + * so position it one before where we want. + */ + fmt--; + i--; + continue; + } else if (*fmt != '\\') { ps[i] = *fmt; continue; } diff --git a/bin/sh/tests/parser/Makefile b/bin/sh/tests/parser/Makefile index afeb604710e4..c22af5414526 100644 --- a/bin/sh/tests/parser/Makefile +++ b/bin/sh/tests/parser/Makefile @@ -86,6 +86,12 @@ ${PACKAGE}FILES+= only-redir2.0 ${PACKAGE}FILES+= only-redir3.0 ${PACKAGE}FILES+= only-redir4.0 ${PACKAGE}FILES+= pipe-not1.0 +${PACKAGE}FILES+= ps1-expand1.0 +${PACKAGE}FILES+= ps1-expand2.0 +${PACKAGE}FILES+= ps1-expand3.0 +${PACKAGE}FILES+= ps1-expand4.0 +${PACKAGE}FILES+= ps1-expand5.0 +${PACKAGE}FILES+= ps2-expand1.0 ${PACKAGE}FILES+= set-v1.0 set-v1.0.stderr ${PACKAGE}FILES+= var-assign1.0 diff --git a/bin/sh/tests/parser/ps1-expand1.0 b/bin/sh/tests/parser/ps1-expand1.0 new file mode 100644 index 000000000000..351e6437a023 --- /dev/null +++ b/bin/sh/tests/parser/ps1-expand1.0 @@ -0,0 +1,7 @@ +# Test simple variable expansion in PS1 +testvar=abcdef +output=$(testvar=abcdef PS1='$testvar:' ENV=/dev/null ${SH} +m -i &1) +case $output in +*abcdef*) exit 0 ;; +*) echo "Expected 'abcdef' in prompt output"; exit 1 ;; +esac diff --git a/bin/sh/tests/parser/ps1-expand2.0 b/bin/sh/tests/parser/ps1-expand2.0 new file mode 100644 index 000000000000..ed31a7c17136 --- /dev/null +++ b/bin/sh/tests/parser/ps1-expand2.0 @@ -0,0 +1,7 @@ +# Test braced variable expansion in PS1 +testvar=xyz123 +output=$(testvar=xyz123 PS1='prefix-${testvar}-suffix:' ENV=/dev/null ${SH} +m -i &1) +case $output in +*xyz123*) exit 0 ;; +*) echo "Expected 'xyz123' in prompt output"; exit 1 ;; +esac diff --git a/bin/sh/tests/parser/ps1-expand3.0 b/bin/sh/tests/parser/ps1-expand3.0 new file mode 100644 index 000000000000..0b6270c300ff --- /dev/null +++ b/bin/sh/tests/parser/ps1-expand3.0 @@ -0,0 +1,8 @@ +# Test special parameter $$ (PID) in PS1 +output=$(PS1='pid:$$:' ENV=/dev/null ${SH} +m -i &1) +# Check that output contains "pid:" followed by a number (not literal $$) +case $output in +*pid:\$\$:*) echo "PID not expanded, got literal \$\$"; exit 1 ;; +*pid:[0-9]*) exit 0 ;; +*) echo "Expected PID after 'pid:' in output"; exit 1 ;; +esac diff --git a/bin/sh/tests/parser/ps1-expand4.0 b/bin/sh/tests/parser/ps1-expand4.0 new file mode 100644 index 000000000000..623c52707eec --- /dev/null +++ b/bin/sh/tests/parser/ps1-expand4.0 @@ -0,0 +1,8 @@ +# Test special parameter $? (exit status) in PS1 +output=$(PS1='status:$?:' ENV=/dev/null ${SH} +m -i &1) +# Should start with exit status 0 +case $output in +*status:\$?:*) echo "Exit status not expanded, got literal \$?"; exit 1 ;; +*status:0:*) exit 0 ;; +*) echo "Expected 'status:0:' in initial prompt"; exit 1 ;; +esac diff --git a/bin/sh/tests/parser/ps1-expand5.0 b/bin/sh/tests/parser/ps1-expand5.0 new file mode 100644 index 000000000000..73fe3ba5a3d5 --- /dev/null +++ b/bin/sh/tests/parser/ps1-expand5.0 @@ -0,0 +1,8 @@ +# Test positional parameter $0 in PS1 +output=$(PS1='shell:$0:' ENV=/dev/null ${SH} +m -i &1) +# $0 should contain the shell name/path +case $output in +*shell:\$0:*) echo "Positional parameter not expanded, got literal \$0"; exit 1 ;; +*shell:*sh*:*) exit 0 ;; +*) echo "Expected shell name after 'shell:' in output"; exit 1 ;; +esac diff --git a/bin/sh/tests/parser/ps2-expand1.0 b/bin/sh/tests/parser/ps2-expand1.0 new file mode 100644 index 000000000000..f0a3a77ded1c --- /dev/null +++ b/bin/sh/tests/parser/ps2-expand1.0 @@ -0,0 +1,12 @@ +# Test variable expansion in PS2 (continuation prompt) +testvar=continue +# Send incomplete command (backslash at end) to trigger PS2 +output=$(testvar=continue PS2='$testvar>' ENV=/dev/null ${SH} +m -i <&1 +echo \\ +done +EOF +) +case $output in +*continue\>*) exit 0 ;; +*) echo "Expected 'continue>' in PS2 output"; exit 1 ;; +esac