The branch main has been updated by jilles:

URL: 
https://cgit.FreeBSD.org/src/commit/?id=f9e79facf874567f25147b24863e5198164e8d04

commit f9e79facf874567f25147b24863e5198164e8d04
Author:     Matthew Phillips <[email protected]>
AuthorDate: 2025-10-12 19:27:34 +0000
Commit:     Jilles Tjoelker <[email protected]>
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 </dev/null 
2>&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 </dev/null 2>&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 </dev/null 2>&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 </dev/null 2>&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 </dev/null 2>&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 <<EOF 2>&1
+echo \\
+done
+EOF
+)
+case $output in
+*continue\>*) exit 0 ;;
+*) echo "Expected 'continue>' in PS2 output"; exit 1 ;;
+esac

Reply via email to