From dde18182cdd7f8efa45f5441ec9196b2266a4a17 Mon Sep 17 00:00:00 2001 From: cubernetes Date: Thu, 1 Oct 2026 08:54:28 +0200 Subject: [PATCH] Add working version of bashrc-forward --- bashrc-forward.sh | 175 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 175 insertions(+) create mode 100644 bashrc-forward.sh diff --git a/bashrc-forward.sh b/bashrc-forward.sh new file mode 100644 index 0000000..d4f5148 --- /dev/null +++ b/bashrc-forward.sh @@ -0,0 +1,175 @@ +#!/usr/bin/env bash +# Note: Shebang for syntax highlighting + +# Note: Minimum Bash version on target host: 4.4 + +# Note: Oneliner. TODO: Automate the generation of this. +# Note: XXX: Outdated! Missing `alias sudobash='...'` +# Note: XXX: Outdated! `builtin eval` and so on +# Note: XXX: Outdated! Missing exec bash in the beginning +# Note: ssh -t ar 'if >/dev/null 2>&1 command -v bash; then bash -c '\''if ! ((BASH_VERSINFO[0]>4||(BASH_VERSINFO[0]==4&&BASH_VERSINFO[1]>=4))) 2>/dev/null; then >&2 echo "Failed to SSH with .bashrc-forwarding, the Bash version is too old."; >&2 echo "Please install Bash 4.4 or newer on the target system and make it available in the PATH."; >&2 echo "Falling back to default login shell: '\''\'\'''\''${SHELL:-${0#-}}'\''\'\'''\''."; builtin exec -l -- "${SHELL:-${0#-}}"; fi; ENV=<(s='\''\'\'''\'''"$(2>&1 >/dev/null bash --rcfile <(echo set -v; cat ~/.bashrc; echo set +v) -ic : | sed 's,/nix/store/[^/]\+,,' | sed "s,','\\\\''\\\\'\\\\'''\\\\'',g")"$'\n''alias bash="bash -c '\''\'\'''\''\'\''\'\'''\'''\''\'\'''\''exec -a sh bash \"\$@\"'\''\'\'''\''\'\''\'\'''\'''\''\'\'''\'' _"; BASH_ARGV0=bash; declare +x PROMPT_COMMAND=${PROMPT_COMMAND:+$PROMPT_COMMAND$'\''\'\'''\''\'\''\'\'''\'''\''\'\'''\''\n'\''\'\'''\''\'\''\'\'''\'''\''\'\'''\''}'\''\'\'''\''\'\''\'\'''\'''\''\'\'''\''[[ "${ENV##*/}" =~ ^[0-9]+$ ]] && eval "exec ${ENV##*/}<&-"; builtin exec {ENV}< <(printf %s "${s@A}; $s"); export ENV="/dev/fd/$ENV"'\''\'\'''\''\'\''\'\'''\'''\''\'\'''\'';'\''\'\'''\''; printf %s "${s@A}; $s"; echo '\''\'\'''\''trap '\''\'\'''\''\'\''\'\'''\'''\''\'\'''\''set +o posix; : "$(trap -p DEBUG)"; : "${_#trap -- }"; : "$(eval echo "${_% DEBUG}.")"; : "${_#*[M]ARKER[[:space:]]}"; : "${_%.}"; trap "${_:--}" DEBUG; : MARKER'\''\'\'''\''\'\''\'\'''\'''\''\'\'''\''$'\''\'\'''\''\'\''\'\'''\'''\''\'\'''\''\n'\''\'\'''\''\'\''\'\'''\'''\''\'\'''\''"$(: "$(trap -p DEBUG)"; : "${_#trap -- }"; eval printf %s "${_% DEBUG}")" DEBUG'\''\'\'''\'') builtin exec -a sh bash -l'\''; else >&2 echo "Failed to SSH with .bashrc-forwarding, could not find Bash binary."; >&2 echo "Please install Bash 4.4 or newer on the target system and make it available in the PATH."; >&2 echo "Falling back to default shell (warning, possibly not a login shell anymore!): '\''${SHELL:-${0#-}}'\''."; exec "${SHELL:-${0#-}}" || { _status=$?; >&2 echo "Failed to run shell '\''${SHELL:-${0#-}}'\''"; exit "$_status"; }; fi' + +bashrc_get () { + # Note: Not perfect, but good enough hack for me and quite concise for what it accomplishes + # Note: Has some bugs, which is why we have to introduce this bashrc_get and that bashrc_load function, instead + # Note: of just embedding it inside the command substitution. + echo "set -v" + # Note: Setting BASH_COMPLETION_VERSINFO because nixos likes to hijack your .bashrc to run bash-completion.sh, which + # Note: of course fails on remote systems, so we need a way to stop that from happening. Not perfect, but I'll accept the tradeoff. + echo "[ \"\${BASH_COMPLETION_VERSINFO-}\" ] || BASH_COMPLETION_VERSINFO=" + # Note: There's still a bug in bash at the moment regarding verbose mode not being turned on again after ${ :;} + # Note: https://www.mail-archive.com/bug-bash@gnu.org/msg35966.html + # Note: Therefore, be careful to use this new command substition syntax until it is fixed upstream! + cat ~/.bashrc + echo "set +v" +} + +bashrc_load () { + if [ "$_s" ]; then + # Note: Relay .bashrc if it has already been forwarded + : "${_s#*[B]ASH_COMPLETION_VERSINFO=[[:space:]]}" + printf %s "${_%[[:space:]][s]et +v*}" + else + # Note: Warning: Your `.bashrc` MUST NOT print any error messages at all! + # Note: Additionally, be aware that every time you use bashrc_forward, + # Note: the entire `.bashrc` gets executed, so try to make your `.bashrc` + # Note: as idempotent as possible. You can also add the string + # Note: - ` # bashrc_forward: noreexec` + # Note: (leading space mandatory!) the the end of those lines that should not be re-executed, but be aware that this only + # Note: works for lines directly inside the `~/.bashrc`, not inside files sourced by `~/.bashrc`. + # Note: + # Note: If there's lines you don't want to forward at all (e.g. secrets), add the string + # Note: - ` # bashrc_forward: ignore` + # Note: (leading space mandatory!) to the lines of your `.bashrc` that should not get forwarded! + # Note: You may combine `ignore` and `noreexec` like this: ` bashrc_forward: noreexec ignore`. + # Note: + # Note: The process substition below MUST be a single line. See the command above the call-site of `bashrc_load` to understand why. + 2>&1 >/dev/null bash --rcfile <(bashrc_get | grep -v ' # *bashrc_forward: .*\[ a-z]*$') -ic : + fi | + # Note: Removing comments because final .bashrc must be below 131k bytes because of resource limits (`MAX_ARG_STRLEN` on Linux) + grep -v -e '^[[:space:]]*# *\(Note:\|shellcheck\)' -e ' # *bashrc_forward: .*\[ a-z]*$' | + sed ' + s,/nix/store/[^/]\+,, + s,'\'','\''\\'\'''\''\\'\''\\'\'''\'''\''\\'\'''\'',g + ' +} + +# Note: Warning, forwards the entire effective .bashrc, which may include secrets or confidential data. +# Note: Everything is provided "AS IS" without any warranty. Use at your own risk. +# Note: Without any pipelines and without any external utilies on the remote system (except the shell)*. +# Note: +# Note: *unless you relay the `.bashrc`, in that case the remote system needs sed, grep, cat and bash. +bashrc_forward () { + printf %s ' + _argv_wrap () { + if >/dev/null 2>&1 command -v bash; then + bash -c '\'' + if [ "$#" -gt 0 ]; then + builtin exec -- bash "$@" + # Note: Newest Bash feature used here is parameter transformation (`${_s@A}`), introduced in Bash 4.4 + elif ! ((BASH_VERSINFO[0]>4||(BASH_VERSINFO[0]==4&&BASH_VERSINFO[1]>=4))) 2>/dev/null; then + >&2 echo "Failed to SSH with .bashrc-forwarding, the Bash version is too old." + >&2 echo "Please install Bash 4.4 or newer on the target system and make it available in the PATH." + >&2 echo "Falling back to default login shell: '\''\'\'''\''${SHELL:-${0#-}}'\''\'\'''\''." + builtin exec -l -- "${SHELL:-${0#-}}" + fi + ENV=<( + _s='\''\'\'''\'' + set +o posix + # Note: In case `$0` is checked by other scripts + BASH_ARGV0=bash + # Note: The following host-side command substitution MUST be single line if this file where the bashrc_forward function is defined + # Note: is part of the host-side .bashrc. Otherwise the self-inclusion trick with the `set -v; cat ~/.bashrc; set +v` WILL NOT work, + # Note: either because of a bug in bash or because of a quirk I do not understand. For some reason, if it is multiline, only the line + # Note: where the command substition starts is printed. If there is a trailing backslash, then also the second line is printed. But in + # Note: no circumstance will the last line be printed, no matter what amount of trailing backslashes you add. + '"$(bashrc_load)"' + # Note: Not universal, but good enough to bash is preferrably invoked as sh so our constructed ENV is loaded. + alias bash="bash -c '\''\'\'''\''\'\''\'\'''\'''\''\'\'''\''exec -a sh bash \"\$@\"'\''\'\'''\''\'\''\'\'''\'''\''\'\'''\'' _" + # Note: instead of the sudobash alias, you can also just do `sudo bbrc`. Keeping the `sudobash` alias anyway as an alternative. + # Note: Warning: Be careful! sudobash will run YOUR .bashrc with ROOT on the remote system! You may or may not want this! + alias sudobash="ENV_RAW=\$(<\"\$ENV\") sudo --preserve-env=ENV_RAW bash --norc --noprofile -ic '\''\'\'''\''\'\''\'\'''\'''\''\'\'''\''. <(printf %s \"\$ENV_RAW\"); unset -v ENV_RAW; builtin eval \"\$PROMPT_COMMAND\"; builtin exec -a sh bash \"\$@\"'\''\'\'''\''\'\''\'\'''\'''\''\'\'''\'' _" + # Note: Using PROMPT_COMMAND instead of DEBUG trap because of speed tradeoffs (DEBUG trap runs way too often) + # Note: Also explicitly removing the export flag as to not append needlessly, in case the remote system exported it accidentally. + declare +x PROMPT_COMMAND=${PROMPT_COMMAND:+$PROMPT_COMMAND$'\''\'\'''\''\'\''\'\'''\'''\''\'\'''\''\n'\''\'\'''\''\'\''\'\'''\'''\''\'\'''\''}'\''\'\'''\''\'\''\'\'''\'''\''\'\'''\'' + # Note: Close stale file descriptor + [[ "${ENV##*/}" =~ ^[0-9]+$ ]] && builtin eval "builtin exec ${ENV##*/}<&-" + + # Note: Constructs new ENV after every command, thanks to the quine + builtin exec {ENV}< <( + printf %s "${_s@A} + $_s" + ) + + export ENV="/dev/fd/$ENV" + '\''\'\'''\''\'\''\'\'''\'''\''\'\'''\'' + + '\''\'\'''\'' + + printf %s "${_s@A}; $_s" + + # Note: Suppress the warning `bash: POSIXLY_CORRECT: readonly variable` using a one-shot DEBUG trap + # Note: `PS0` and `PROMPT_COMMAND` will not suffice, `PROMPT_COMMAND` runs AFTER the command, therefore + # Note: our first command would be interpreted in posix mode (bad), and commands in `PS0` always run in a + # Note: subshell that does not affect the parent shell (unless we assume Bash 5.3+, which we do not). + # Note: Therefore, the only sensible options for us to get out of posix mode are: + # Note: - unset `POSIXLY_CORRECT` and make it readonly. When Bash tries to overwrite it, it fails, keeping the shell out of posix mode + # Note: - Run `exec 2> >(stdbuf -o0 tail -n+2)` right here. But this depends on stdbuf and tail, which may or may not exist (and it would be bloated) + # Note: - Print the initial prompt early, disable stderr, and restore stderr in `PROMPT_COMMAND` (runs after the inital command) (robs the first command of its stderr) + # Note: - use a DEBUG trap to unset POSIXLY_CORRECT before the first command runs + # Note: Only the last options seems to satisfy all constraints, albeit less elegant since the DEBUG trap is kinda like a chainsaw for everything. + # Note: But we do not want to always disable posix mode, so we need to reset the DEBUG to its original value + echo '\''\'\'''\'' + trap '\''\'\'''\''\'\''\'\'''\'''\''\'\'''\'' + set +o posix + : "$(trap -p DEBUG)" + : "${_#trap -- }" + : "$(builtin eval echo "${_% DEBUG}.")" + : "${_#*[M]ARKER[[:space:]]}" + : "${_%.}" + trap "${_:--}" DEBUG; : MARKER'\''\'\'''\''\'\''\'\'''\'''\''\'\'''\''$'\''\'\'''\''\'\''\'\'''\'''\''\'\'''\''\n'\''\'\'''\''\'\''\'\'''\'''\''\'\'''\''"$( + : "$(trap -p DEBUG)" + : "${_#trap -- }" + builtin eval printf %s "${_% DEBUG}" + )" DEBUG + '\''\'\'''\'' + ) \ + builtin exec -a sh bash -l + # Note: Note for the exec above: `ENV` is read if and only if bash is invoked as `sh` (or `-sh`), which we enforce using `exec -a sh ...`. + # Note: Additionally, when invoked just as `sh`, it never reads `~/.bashrc` (or `/etc/bash.bashrc`), because of the implicit posix mode. + # Note: When invoked as a login shell however (either by having a leading dash in `argv[0]` or with `--login/-l`), + # Note: then `/etc/profile` and the one user startup file (in this precedence order: `~/.bash_profile`, `~/.bash_login`, `~/.profile`, in posix mode only the last) + # Note: are loaded. `/etc/profile` usually loads `/etc/bash.bashrc` and the user startup file usually loads `~/.bashrc`. + # Note: The system bashrc (`SYS_BASHRC`, usually `/etc/bash.bashrc`) is disabled by default and only some distros enable it, like Debian: + # Note: - https://sources.debian.org/src/bash/5.3-4/debian/patches/deb-bash-config.diff#L32 + # Note: Also, you might think `BASH_ENV` would be more appropriate, but it only works with a non-interactive Bash shell, so it is not an option. + '\'' "$(command -v bash)" "$@" + else + if [ "$#" -gt 0 ]; then + exec "${SHELL:-${0#-}}" "$@" + else + >&2 echo "Failed to SSH with .bashrc-forwarding, could not find Bash binary." + >&2 echo "Please install Bash 4.4 or newer on the target system and make it available in the PATH." + >&2 echo "Falling back to default shell (warning, possibly not a login shell anymore!): '\''${SHELL:-${0#-}}'\''." + # Note: Using generic `exec` call instead of fancy `exec -l --` because we cannot assume that we are inside Bash. + # Note: For example, the exec builtin from the Dash shell does not support any options at all, not even `--`. + exec "${SHELL:-${0#-}}" || { _status=$?; >&2 echo "Failed to run shell '\''${SHELL:-${0#-}}'\''"; exit "$_status"; } + fi + fi + } + _argv_wrap \ + ' +} + +# Note: Special aliases that play nicely with my `bash-abbr` bash plugin. The leading `*` means they expand +# Note: anywhere in the command line (as long as they start a word). Like global aliases in zsh. +# Note: Allows you to do these kinds of things: +# Note: - `docker container run --rm -itu1000 ubuntu bbrc` -> the `bbrc` expands to `bash -c "$(bashrc_forward)"` -> starts ubuntu container as ubuntu user with my bashrc +# Note: -> inside the container: +# Note: - either `sudobash` (escalates to root and keeps my bashrc) +# Note: - or just `sudo bbrc` -> uses the same trick to forward bashrc to root shell +# Note: - `ssh host tbrc` -> `tbrc` expands to `-t "$(bashrc_forward)"` -> ssh to host with my bashrc +# Note: - `su another_user -c brc` -> expands to `su another_user -c "$(bashrc_forward)"` -> switch to another user but keep the bashrc (e.g. in containers) +alias brc='*"$(bashrc_forward)"' +alias bbrc='*sh -c "$(bashrc_forward)"' +alias tbrc='*-t "$(bashrc_forward)"'