bashrc-forward/bashrc-forward.sh

185 lines
12 KiB
Bash

#!/usr/bin/env bash
#-# Shebang for syntax highlighting
#-# Minimum Bash version on target host: 4.4
bashrc_get () {
#-# Not perfect, but good enough hack for me and quite concise for what it accomplishes
#-# Has some bugs, which is why we have to introduce this bashrc_get and that bashrc_load function, instead
#-# of just embedding it inside the command substitution.
echo "set -v"
#-# Marker and variable so we can slice out the original .bashrc for relaying and have custom login only for forwarded .bashrc
echo "BASHRC_FORWARDED=1"
#-# There's still a bug in bash at the moment regarding verbose mode not being turned on again after ${ :;}
#-# https://www.mail-archive.com/bug-bash@gnu.org/msg35966.html
#-# Therefore, be careful to use this new command substition syntax until it is fixed upstream!
cat ~/.bashrc
echo "set +v"
}
_strip_comments () {
grep -v '^[[:space:]]*#\+-#'
}
bashrc_load () {
if [ "$_s" ]; then
#-# Relay .bashrc if it has already been forwarded
: "${_s#*[B]ASHRC_FORWARDED=1[[:space:]]}"
printf '%s\n%s\n%s\n' "BASHRC_FORWARDED=1" "${_%[[:space:]][s]et +v*}" "set +v"
else
#-# Warning: Your `.bashrc` MUST NOT print any error messages at all!
#-# Additionally, be aware that every time you use bashrc_forward,
#-# the entire `.bashrc` gets executed, so try to make your `.bashrc`
#-# as idempotent as possible. You can also add the string
#-# - ` # bashrc_forward: noreexec`
#-# (leading space mandatory!) the the end of those lines that should not be re-executed, but be aware that this only
#-# works for lines directly inside the `~/.bashrc`, not inside files sourced by `~/.bashrc`.
#-#
#-# If there's lines you don't want to forward at all (e.g. secrets), add the string
#-# - ` # bashrc_forward: ignore`
#-# (leading space mandatory!) to the lines of your `.bashrc` that should not get forwarded!
#-# You may combine `ignore` and `noreexec` like this: ` bashrc_forward: noreexec ignore`.
#-#
#-# 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: .*\<noreexec\>[ a-z]*$') -ic :
fi |
#-# Removing comments (if marked as such by using #-#) because final `.bashrc` must be
#-# below 131k bytes because of resource limits (`MAX_ARG_STRLEN` on Linux)
_strip_comments |
grep -v -e ' # *bashrc_forward: .*\<ignore\>[ a-z]*$' |
sed '
s,/[n]ix/store/[^/]\+,,
s,'\'','\''\\'\'''\''\\'\''\\'\'''\'''\''\\'\'''\'',g
'
}
#-# Warning, forwards the entire effective .bashrc, which may include secrets or confidential data.
#-# Everything is provided "AS IS" without any warranty. Use at your own risk.
#-# Without any pipelines and without any external utilies on the remote system (except the shell)*.
#-#
#-# *unless you relay the `.bashrc`, in that case the remote system needs sed, grep, cat and bash.
bashrc_forward () {
printf %s '
#-# If bashrc was forwarded using environment variable, unset it, since we do not need it anymore and to keep the env clean. Relaying will set it anyways to something fresh.
unset FORWARDED_BASHRC
#-# Set default IFS, since our staging command line may not contain double quotes since they behave weirdly in powershell.
IFS=$(printf '\'' \t\n.'\''); IFS=${IFS%.}
_argv_wrap () {
if >/dev/null 2>&1 command -v bash; then
exec bash -c '\''
#-# Newest Bash feature used here is parameter transformation (`${_s@A}`), introduced in Bash 4.4
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='\''\'\'''\''
set +o posix
#-# In case `$0` is checked by other scripts
BASH_ARGV0=bash
#-# The following host-side command substitution MUST be single line if this file where the bashrc_forward function is defined
#-# is part of the host-side .bashrc. Otherwise the self-inclusion trick with the `set -v; cat ~/.bashrc; set +v` WILL NOT work,
#-# 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
#-# where the command substition starts is printed. If there is a trailing backslash, then also the second line is printed. But in
#-# no circumstance will the last line be printed, no matter what amount of trailing backslashes you add.
'"$(bashrc_load)"'
#-# Using PROMPT_COMMAND instead of DEBUG trap because of speed tradeoffs (DEBUG trap runs way too often)
#-# 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'\''\'\'''\''\'\''\'\'''\'''\''\'\'''\''
#-# Close stale file descriptor
[[ "${ENV##*/}" =~ ^[0-9]+$ ]] && builtin eval "builtin exec ${ENV##*/}<&-"
#-# 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"
#-# Suppress the warning `bash: POSIXLY_CORRECT: readonly variable` using a one-shot DEBUG trap
#-# `PS0` and `PROMPT_COMMAND` will not suffice, `PROMPT_COMMAND` runs AFTER the command, therefore
#-# our first command would be interpreted in posix mode (bad), and commands in `PS0` always run in a
#-# subshell that does not affect the parent shell (unless we assume Bash 5.3+, which we do not).
#-# Therefore, the only sensible options for us to get out of posix mode are:
#-# - unset `POSIXLY_CORRECT` and make it readonly. When Bash tries to overwrite it, it fails, keeping the shell out of posix mode
#-# - 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)
#-# - 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)
#-# - use a DEBUG trap to unset POSIXLY_CORRECT before the first command runs
#-# Only the last options seems to satisfy all constraints, albeit less elegant since the DEBUG trap is kinda like a chainsaw for everything.
#-# 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 "$@" # TODO: Login shell is only the default with SSH. With docker run, for example, it is not!
#-# 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 ...`.
#-# Additionally, when invoked just as `sh`, it never reads `~/.bashrc` (or `/etc/bash.bashrc`), because of the implicit posix mode.
#-# When invoked as a login shell however (either by having a leading dash in `argv[0]` or with `--login/-l`),
#-# then `/etc/profile` and the one user startup file (in this precedence order: `~/.bash_profile`, `~/.bash_login`, `~/.profile`, in posix mode only the last)
#-# are loaded. `/etc/profile` usually loads `/etc/bash.bashrc` and the user startup file usually loads `~/.bashrc`.
#-# The system bashrc (`SYS_BASHRC`, usually `/etc/bash.bashrc`) is disabled by default and only some distros enable it, like Debian:
#-# - https://sources.debian.org/src/bash/5.3-4/debian/patches/deb-bash-config.diff#L32
#-# 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 forward .bashrc, 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#-}}'\''."
#-# Using generic `exec` call instead of fancy `exec -l --` because we cannot assume that we are inside Bash.
#-# 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
}
#-# Allow accepting shell arguments
_argv_wrap \
' | _strip_comments
}
#-# HARD TO FIX BUG: shopt -u expand_aliases must be run in ENV so bash-abbr works, but ENV is run before the shell switches to interactive
#-# use, and --rcfile is run after. We need to disable expand_aliases AFTER the shell switches to interactive mode, because on that switch
#-# alias expansion is always turned on. You'd think you could use a trap, PROMPT_COMMAND, or bind -x trick for that, but no, there's a bug
#-# currently in bash that prevents exactly that. The only workaround at the moment is to create a custom binding that injects the
#-# shopt -u expand_aliases command ONCE before the very first command.
#-# When bash finally fixes that trap bug, then we don't need to do that anymore. Reproducer for the bug (run from another bash shell):
#-# env -i bash --noprofile --rcfile <(echo 'alias foo="echo EXPANDED"; trap "shopt -u expand_aliases; trap - DEBUG" DEBUG') -ic $'shopt expand_aliases\nfoo\nshopt -u expand_aliases\nfoo'
#-# Special aliases that play nicely with my `bash-abbr` bash plugin. The leading `*` means they expand
#-# anywhere in the command line (as long as they start a word). Like global aliases in zsh.
#-# Allows you to do these kinds of things:
#-# - `docker container run --rm -itu1000 ubuntu bbrc` -> the `bbrc` expands to `bash -c "$(bashrc_forward)"` -> starts ubuntu container as ubuntu user with my bashrc
#-# -> inside the container:
#-# - either `sudobash` (escalates to root and keeps my bashrc)
#-# - or just `sudo bbrc` -> uses the same trick to forward bashrc to root shell
#-# - `ssh host tbrc` -> `tbrc` expands to `-t "$(bashrc_forward)"` -> ssh to host with my bashrc
#-# - `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)"'
#-# TODO consider bash --rcfile <() approach, maybe it works better than ENV approach. Would also solve the "HARD TO FIX BUG" from above.