From 9ca54880462db7d0c2f91d6e8f114294458fe08d Mon Sep 17 00:00:00 2001 From: Rasmus Villemoes Date: Mon, 20 Jul 2026 14:49:06 +0200 Subject: [PATCH] profile/systemd-osc-context: do not use sed for escaping MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bash is perfectly capable of performing the simple substitutions needed for escaping according to the OSC 3008 spec, so there is no need for the fork+exec and other overhead of calling sed. In fact, when writing a test for ensuring that this is a drop-in replacement, I found out that the current sed method is flawed: If $PWD contains newline characters, they are passed through unchanged, because sed obviously is line-oriented. I do not know which bash version started supporting ${foo//pattern/replacement}, but I ran the below on all Debian images from docker hub going back to Debian 6 (EOL 2016), carrying BASH_VERSION = 4.1.5(1)-release, and they all succeeded. Since the logic otherwise relies on PROMPT_COMMAND being an array variable, which happened in 5.1, this should be all good. === test.sh === #!/bin/bash echo "BASH_VERSION = $BASH_VERSION" ret=0 using_sed() { echo "$1" | sed -e 's/\\/\\x5c/g' -e 's/;/\\x3b/g' -e 's/[[:cntrl:]]/⍰/g' } pure_bash() { local str="$1" str="${str//\\/\\x5c}" str="${str//;/\\x3b}" str="${str//[[:cntrl:]]/⍰}" printf "%s" "${str}" } do_test() { local r0="$1" local r1="$(using_sed "$r0")" local r2="$(pure_bash "$r0")" local s0="$(printf '%s' "$r0" | od -A x -t x1z -w40 | head -n1)" local s1="$(printf '%s' "$r1" | od -A x -t x1z -w40 | head -n1)" local s2="$(printf '%s' "$r2" | od -A x -t x1z -w40 | head -n1)" if [ "$r1" != "$r2" ] || [ "$s1" != "$s2" ] ; then echo "Input: $s0" echo "sed: $s1" echo "bash: $s2" ret=1 fi } do_test '' do_test "one ; semicolon" do_test "two ; semicolons ;" do_test 'bs \ sc ; bs \ sc ;' do_test $'\t' do_test $'sc ; cntrl \x01 \x02 \x03 \x1f \t bs \\ bs \\ sc ; ' do_test $'sc ; tab \x09 del \x7f bs \\ ;' # The last cases show that the existing function doesn't actually work in # the case of $PWD containing a newline character, because sed is # line-oriented, so a newline character will never be replaced. if [ "$1" = "all" ] ; then do_test $'embedded \n newline' do_test $'ending in newline\n' fi exit "$ret" === test.sh === --- profile.d/80-systemd-osc-context.sh | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/profile.d/80-systemd-osc-context.sh b/profile.d/80-systemd-osc-context.sh index ea8f456caf..9bbb71285f 100644 --- a/profile.d/80-systemd-osc-context.sh +++ b/profile.d/80-systemd-osc-context.sh @@ -35,13 +35,17 @@ shopt -q promptvars || return 0 __systemd_osc_context_escape() { - # Escape according to the OSC 3008 spec. Since this requires shelling out - # to 'sed' we'll only do it where it's strictly necessary, and skip it when - # processing strings we are pretty sure we won't need it for, such as - # uuids, id128, hostnames, usernames, since they all come with syntax - # requirements that exclude \ and ; anyway. This hence primarily is about - # escaping the current working directory. - echo "$1" | sed -e 's/\\/\\x5c/g' -e 's/;/\\x3b/g' -e 's/[[:cntrl:]]/⍰/g' + # Escape according to the OSC 3008 spec. We'll only do it where it's + # strictly necessary, and skip it when processing strings we are pretty + # sure we won't need it for, such as uuids, id128, hostnames, usernames, + # since they all come with syntax requirements that exclude \ and ; + # anyway. This hence primarily is about escaping the current working + # directory. + local systemd_value="$1" + systemd_value="${systemd_value//\\/\\x5c}" + systemd_value="${systemd_value//;/\\x3b}" + systemd_value="${systemd_value//[[:cntrl:]]/⍰}" + printf "%s" "$systemd_value" } __systemd_osc_context_common() {