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
This commit is contained in:
committed by
Jilles Tjoelker
parent
9b7d2cee03
commit
f9e79facf8
+125
-1
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user