183 lines
12 KiB
Bash
183 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
|
|
_argv_wrap () {
|
|
if >/dev/null 2>&1 command -v bash; then
|
|
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 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#-}}'\''."
|
|
#-# 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.
|