fix(terminal): keep legacy bash prompt dispatch out of OSC 133 command start after a DEBUG trap change (#29165)

On bash <= 5.0 the PROMPT_COMMAND is a single scalar string. After the
user changed the DEBUG trap at the prompt (`trap - DEBUG`, `trap ':' DEBUG`),
Orca's preexec never saw the status-capture command, so the legacy wrapper
flag stayed unset and the dispatch tail emitted OSC 133;C before the next
prompt was drawn.

Set __orca_in_legacy_prompt_wrapper directly in the PROMPT_COMMAND string,
and re-arm the Orca DEBUG trap at the top level of that string: bash <= 4.3
(including macOS /bin/bash 3.2) restores the caller's DEBUG trap when a
function returns, so the re-arm inside __orca_finish_legacy_prompt_dispatch
was undone. Tests force the legacy dispatch on any bash and also run with
/bin/bash on macOS.

Fixes #29164

Co-authored-by: Neil <neil@stably.ai>
This commit is contained in:
Mkjung Github
2026-10-26 15:28:41 -07:00
committed by GitHub
co-authored by Neil
parent 1a723194c3
commit 4a8cb6fc71
5 changed files with 64 additions and 12 deletions
@@ -372,8 +372,6 @@ __orca_finish_legacy_prompt_dispatch() {
eval "$__orca_suffix_part"
done
fi
trap '__orca_osc133_preexec' DEBUG
unset __orca_in_legacy_prompt_wrapper
}
__orca_normalize_prompt_command() {
[[ -z "${__orca_prompt_command_normalized:-}" ]] || return 0
@@ -395,7 +393,8 @@ __orca_normalize_prompt_command() {
__orca_prompt_status_variable="__orca_prompt_status_$$"
__orca_prompt_status_capture_command="$__orca_prompt_status_variable=\$?"
__orca_prompt_status_value="\${$__orca_prompt_status_variable}"
PROMPT_COMMAND="$__orca_prompt_status_capture_command; __orca_prompt_status=$__orca_prompt_status_value"'; __orca_prompt_had_functrace=""; if [[ -o functrace ]]; then __orca_prompt_had_functrace=1; set +T; fi; __orca_outer_debug_trap_spec="$(trap -p DEBUG)"; [[ -z "$__orca_prompt_had_functrace" ]] || set -T; unset __orca_prompt_had_functrace; __orca_run_prompt_command_array; __orca_finish_legacy_prompt_dispatch'
# Why: a changed DEBUG trap misses the capture command, and bash <= 4.3 reverts traps set in functions.
PROMPT_COMMAND="$__orca_prompt_status_capture_command; __orca_in_legacy_prompt_wrapper=1; __orca_prompt_status=$__orca_prompt_status_value"'; __orca_prompt_had_functrace=""; if [[ -o functrace ]]; then __orca_prompt_had_functrace=1; set +T; fi; __orca_outer_debug_trap_spec="$(trap -p DEBUG)"; [[ -z "$__orca_prompt_had_functrace" ]] || set -T; unset __orca_prompt_had_functrace; __orca_run_prompt_command_array; __orca_finish_legacy_prompt_dispatch; trap __orca_osc133_preexec DEBUG; unset __orca_in_legacy_prompt_wrapper'
fi
}
__orca_prepend_prompt_command() {
@@ -365,8 +365,6 @@ __orca_finish_legacy_prompt_dispatch() {
eval "$__orca_suffix_part"
done
fi
trap '__orca_osc133_preexec' DEBUG
unset __orca_in_legacy_prompt_wrapper
}
__orca_normalize_prompt_command() {
[[ -z "${__orca_prompt_command_normalized:-}" ]] || return 0
@@ -388,7 +386,8 @@ __orca_normalize_prompt_command() {
__orca_prompt_status_variable="__orca_prompt_status_$$"
__orca_prompt_status_capture_command="$__orca_prompt_status_variable=\$?"
__orca_prompt_status_value="\${$__orca_prompt_status_variable}"
PROMPT_COMMAND="$__orca_prompt_status_capture_command; __orca_prompt_status=$__orca_prompt_status_value"'; __orca_prompt_had_functrace=""; if [[ -o functrace ]]; then __orca_prompt_had_functrace=1; set +T; fi; __orca_outer_debug_trap_spec="$(trap -p DEBUG)"; [[ -z "$__orca_prompt_had_functrace" ]] || set -T; unset __orca_prompt_had_functrace; __orca_run_prompt_command_array; __orca_finish_legacy_prompt_dispatch'
# Why: a changed DEBUG trap misses the capture command, and bash <= 4.3 reverts traps set in functions.
PROMPT_COMMAND="$__orca_prompt_status_capture_command; __orca_in_legacy_prompt_wrapper=1; __orca_prompt_status=$__orca_prompt_status_value"'; __orca_prompt_had_functrace=""; if [[ -o functrace ]]; then __orca_prompt_had_functrace=1; set +T; fi; __orca_outer_debug_trap_spec="$(trap -p DEBUG)"; [[ -z "$__orca_prompt_had_functrace" ]] || set -T; unset __orca_prompt_had_functrace; __orca_run_prompt_command_array; __orca_finish_legacy_prompt_dispatch; trap __orca_osc133_preexec DEBUG; unset __orca_in_legacy_prompt_wrapper'
fi
}
__orca_prepend_prompt_command() {
+2 -3
View File
@@ -133,8 +133,6 @@ __orca_finish_legacy_prompt_dispatch() {
eval "$__orca_suffix_part"
done
fi
trap '__orca_osc133_preexec' DEBUG
unset __orca_in_legacy_prompt_wrapper
}
__orca_normalize_prompt_command() {
[[ -z "\${__orca_prompt_command_normalized:-}" ]] || return 0
@@ -156,7 +154,8 @@ __orca_normalize_prompt_command() {
__orca_prompt_status_variable="__orca_prompt_status_$$"
__orca_prompt_status_capture_command="$__orca_prompt_status_variable=\\$?"
__orca_prompt_status_value="\\\${$__orca_prompt_status_variable}"
PROMPT_COMMAND="$__orca_prompt_status_capture_command; __orca_prompt_status=$__orca_prompt_status_value"'; __orca_prompt_had_functrace=""; if [[ -o functrace ]]; then __orca_prompt_had_functrace=1; set +T; fi; __orca_outer_debug_trap_spec="$(trap -p DEBUG)"; [[ -z "$__orca_prompt_had_functrace" ]] || set -T; unset __orca_prompt_had_functrace; __orca_run_prompt_command_array; __orca_finish_legacy_prompt_dispatch'
# Why: a changed DEBUG trap misses the capture command, and bash <= 4.3 reverts traps set in functions.
PROMPT_COMMAND="$__orca_prompt_status_capture_command; __orca_in_legacy_prompt_wrapper=1; __orca_prompt_status=$__orca_prompt_status_value"'; __orca_prompt_had_functrace=""; if [[ -o functrace ]]; then __orca_prompt_had_functrace=1; set +T; fi; __orca_outer_debug_trap_spec="$(trap -p DEBUG)"; [[ -z "$__orca_prompt_had_functrace" ]] || set -T; unset __orca_prompt_had_functrace; __orca_run_prompt_command_array; __orca_finish_legacy_prompt_dispatch; trap __orca_osc133_preexec DEBUG; unset __orca_in_legacy_prompt_wrapper'
fi
}
__orca_prepend_prompt_command() {
@@ -13,18 +13,21 @@ const bashMajor = hasBash
const bashMinor = hasBash
? Number(spawnSync('bash', ['-lc', 'printf %s "${BASH_VERSINFO[1]}"']).stdout)
: 0
const LEGACY_GATE = 'BASH_VERSINFO[0] > 5 || (BASH_VERSINFO[0] == 5 && BASH_VERSINFO[1] >= 1)'
const bashPreservesOddTerminalBackslash = bashMajor > 4 || (bashMajor === 4 && bashMinor >= 4)
function runInteractiveBash(
profile: string,
tempHome: string,
input = 'true\nfalse\nexit 0\n'
input = 'true\nfalse\nexit 0\n',
rcfileContent = getDaemonBashShellReadyRcfileContent(),
bash = 'bash'
): string {
const rcfile = join(tempHome, 'rcfile')
writeFileSync(join(tempHome, '.bash_profile'), profile)
writeFileSync(rcfile, getDaemonBashShellReadyRcfileContent())
writeFileSync(rcfile, rcfileContent)
const result = spawnSync(
'bash',
bash,
['-lc', '"$BASH" --noprofile --rcfile "$1" -i 2>&1', 'bash', rcfile],
{
input,
@@ -396,6 +399,37 @@ PROMPT_COMMAND='printf "PROMPT_REMATCH:<%s>\\n" "\${BASH_REMATCH[1]-unset}"'
expectLifecycle(output)
})
// Why: macOS /bin/bash 3.2 also reverts DEBUG traps set inside functions.
const legacyBashes = process.platform === 'darwin' ? ['bash', '/bin/bash'] : ['bash']
itWithBash.each(
legacyBashes.flatMap((bash) => [
['trap - DEBUG', bash],
["trap ':' DEBUG", bash]
])
)(
'starts the next command after its prompt when `%s` runs at a legacy prompt (%s)',
(trapChange, bash) => {
// Why: force the bash <= 5.0 scalar PROMPT_COMMAND dispatch on any bash.
const rcfile = getDaemonBashShellReadyRcfileContent()
expect(rcfile).toContain(LEGACY_GATE)
const legacyRcfile = rcfile.replaceAll(LEGACY_GATE, '0')
const output = runInteractiveBash(
"PS1='ORCA_PS1> '",
tempHome,
`${trapChange}\ntrue\nexit 0\n`,
legacyRcfile,
bash
)
const startBeforePrompt = new RegExp(
`${String.fromCharCode(27)}]133;A${String.fromCharCode(7)}(?:(?!ORCA_PS1> )[\\s\\S])*${String.fromCharCode(27)}]133;C`
)
expect(output).toContain('ORCA_PS1> ')
expect(output).not.toMatch(startBeforePrompt)
expectLifecycle(output, 0)
}
)
itWithBash('does not treat user command text as a bash-preexec helper', () => {
const output = runInteractiveBash('', tempHome, 'echo a:__bp_x\nfalse\nexit 0\n')
@@ -398,6 +398,27 @@ describePosix('local PTY shell-ready launch config', () => {
expectBashOsc133Lifecycle(output)
})
itWithBash(
'starts the next command after its prompt when the DEBUG trap is removed at a legacy prompt',
() => {
// Why: force the bash <= 5.0 scalar PROMPT_COMMAND dispatch on any bash.
const gate = 'BASH_VERSINFO[0] > 5 || (BASH_VERSINFO[0] == 5 && BASH_VERSINFO[1] >= 1)'
const rcfile = getBashShellReadyRcfileContent()
expect(rcfile).toContain(gate)
const legacyRcfile = rcfile.replaceAll(gate, '0')
writeFileSync(join(userDataPath, '.bash_profile'), "PS1='ORCA_PS1> '\n")
const input = 'trap - DEBUG\nfalse\nexit 0\n'
const output = runInteractiveBashRcfile(legacyRcfile, userDataPath, input)
const startBeforePrompt = new RegExp(
`${String.fromCharCode(27)}]133;A${String.fromCharCode(7)}(?:(?!ORCA_PS1> )[\\s\\S])*${String.fromCharCode(27)}]133;C`
)
expect(output).toContain('ORCA_PS1> ')
expect(output).not.toMatch(startBeforePrompt)
expectBashOsc133Lifecycle(output)
}
)
itWithBash('composes an inherited PROMPT_COMMAND ending in separators', () => {
writeFileSync(
join(userDataPath, '.bash_profile'),