Claude pointed out that while rather unlikely, it is possible for the USER and
HOSTNAME environment variables to contain problematic characters that require
escaping. Now that that escaping no longer involves the fork+exec overhead of
calling sed, let's ensure those fields do get put through the escape routine.
Refactor __systemd_osc_context_escape so that it not only takes the value and
prints that, but also the format specifier used for emitting the field. That
way, we avoid an extra subshell (i.e. fork+pipe and all that overhead), and
combined with the previous commit, we now only spawn one subshell instead of
two per start= sequence.
That could be reduced to zero, if the "callers" were rewritten to something
like
printf "\033]3008;start=%.64s;type=shell" "$systemd_osc_context_shell_id"
__systemd_osc_context_common
printf "\033\\"
but that will mean that the sequence is not emitted with a single write()
system call [which isn't really guaranteed currently either, just very likely],
so some background process could end up writing to the middle of the sequence,
thus losing that output and mangling the OSC3008 info. This in turn could be
overcome by building the whole sequence in a shell variable using 'printf -v'
and only printing at the end, but that would be a somewhat invasive change.
Now that the %s specifier used to embed the common fields in the output appears
immediately before the cwd= field, we can just emit that cwd= field as part of
the common fields.
Not because it is required by the specification, but it will make the following
patches simpler. Also, the fact that the spec explicitly does call out that
type= can appear at the end or in the middle suggests that people would
normally expect it to appear at the beginning.
There's really no point in having bash do arithmetic expansion on the
systemd_exitstatus variable, i.e. have it convert the string to a number, do no
actual arithmetic, then convert it back to a string to be used as the argument to
the printf, which will again convert it to a number for the %d specifier, and
finally emit it as a decimal. Also, it deviates for no obvious reason from how
it is passed to printf in the "probably died by signal" case just above.
The %.20d specifier was introduced in 2d738a0aee ("profile/systemd-osc-context:
Enforce length limits"). But, for numeric conversions, the precision is not an
upper bound, but rather a lower bound on the output width, padding as necessary
with 0 on the left. So as-is, this does not in fact limit the output to at most
20 characters.
Of course, in practice, pids on linux are never greater than 2^22, and
certainly never larger than what would fit in a 20-digit decimal. On the other
hand, that more or less guarantees that the pid= field is always emitted with
12+ leadings zeroes, which is a bit silly. Moreover, a leading 0 can cause a
parser to treat it as octal.
Since $$ does expand to the PID in decimal, just print that as a string, with the
enforced 20 character limit.
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 ===
This was requested in https://github.com/systemd/systemd/issues/42333.
Indeed, this integration is using three levels of systemd magick, so
doing this correctly is not obvious. Let's include specific instructions
to help people for whom this integration is causing problems.
UAPI.15 requires:
* No C0 Control Characters (`\x00-\x1f`)
* No DEL character (`\x7f`)
The following would be nice, but requires a `sed` implementation that is
aware of UTF-8: `-e $'y/\x00-\x1f\x7f/␀-␟␡/'`.
References:
[0] https://uapi-group.org/specifications/specs/osc_context/#general-syntax
Bash does not provide an easy way to discern if an exit status came from
a signal, or was a legitimate non-zero exit (i.e. a failure). It can be
done, by using job control or by invoking another program; however, such
approaches require modifying the command entered by a user and are, thus,
invasive and risky.
Since an exit status of 129 on a command could either indicate it exited
cleanly with `exit(129)` or was interrupted by `SIGHUP`, the osc context
should report both possibilities, to acknowledge our uncertainty. Given
we have no idea what happened, besides an unsuccessful exit, the exit is
described as `exit=failure`.
Moreover, discerning between an `interrupt` and a `crash` with a command
likely involves categorizing every signal. It is fairly obvious `SIGINT`
is an interrupt and also obvious, IMO, that `SIGSEGV` is a crash. Avoid
the complication (and potential disagreements) by using the encompassing,
generic value — one that remains true if no signal occurred.
References:
[0] https://stackoverflow.com/a/66431355
Symlinks to files inside sysconfdir are now only installed if
ìnstall_sysconfdir=true (which is the default).
If sshconfdir,sshdconfdir,shellprofiledir are not inside sysconfdir and
install_sysconfidr=false, these symlinks are still installed to the
configured directory.
Let's do the check early and skip most of the file if appropriate. Also, treat
missing $TERM same as "dumb". We're almost certainly at a dump terminal in that
case.
In https://github.com/systemd/systemd/issues/39114 users are reporting
that our script overrides PROMPT_COMMAND that they had. After looking
at /etc/bashrc in Fedora, I see that it only sets PROMPT_COMMAND if
[ -z "$PROMPT_COMMAND" ]. Let's adjust the script so this continues to
work.
Fixes https://github.com/systemd/systemd/issues/39114.
(This is a bit of a stretch. 39114 was originally about SecureCRT,
but that was resolved in SecureCRT. But there was a lot of dicussion
about the prompt being overriden, which this commit should fix.)
While we can safely assume that `/proc/sys/kernel/random/boot_id`
exists, the same can't be said for `/etc/machine-id` in environments
where systemd is installed, but not running. An example would be OCI
containers like with the official Arch Linux image, see [0].
Without this check the prompt would constantly output `/etc/machine-id:
no such file or directory` with the OSC events introduced in dadbb34
(v258).
[0] https://gitlab.archlinux.org/archlinux/archlinux-docker/-/issues/107
Now our baseline of meson is 0.62, hence install_symlink() can be used.
Note, install_symlink() implies install_emptydir() for specified
install_dir. Hence, this also drops several unnecessary
install_emptydir() calls.
Note, the function currently does not support 'relative' and 'force' flags,
so several 'ln -frsT' inline calls cannot be replaced.
If - for whatever reason - a script uses set -u (nounset) and includes
/etc/profile.d/70-systemd-shell-extra.sh (e.g. transitively via
/etc/profile) the script would fail with:
/etc/profile.d/70-systemd-shell-extra.sh: line 15: SHELL_PROMPT_PREFIX: unbound variable
For example:
$ cat > foo.sh <<EOF
#!/bin/sh
set -u
source /etc/profile
EOF
$ chmod 700 foo.sh
$ ./foo.sh
/etc/profile.d/70-systemd-shell-extra.sh: line 15: SHELL_PROMPT_PREFIX: unbound variable
Fix this by using shell parameter substitution[^1] (which is a POSIX
shell concept) to set the $SHELL_* variables to the empty string if
undefined.
[^1]: https://pubs.opengroup.org/onlinepubs/9699919799.2018edition/utilities/V3_chap02.html