Jon Parise 4e7982fc2b bash: improved 'sudo' command wrapper
The previous approach to wrapping `sudo` had a few shortcomings:

1. We were (re)defining our 'sudo' function wrapper in the "precmd"
   path. It only needs to be defined once in the shell session.
2. If there was an existing 'sudo' alias, the function definition would
   conflict and result in a syntax error.

Fix (1) by hoisting the 'sudo' function into global scope. I also
considered only defining our wrapper if an executable `sudo` binary
could be found (e.g. `-x $(builtin command -v sudo)`, but let's keep the
existing behavior for now. This allows for a `sudo` command to be
installed later in the shell session and still be wrapped.

Address (2) by defining the wrapper function using `function sudo`
(instead of `sudo()`) syntax. An explicit function definition won't
clash with an existing 'sudo' alias, although the alias will continue to
take precedence (i.e. our wrapper won't be called). If the alias is
defined _after_ our 'sudo' function is defined, our function will call
the aliased command.

This ordering is relevant because it can result in different behaviors
depending on when a user defines their aliases relative to sourcing the
shell integration script. Our recommendation remains that users either
use automatic shell injection or manually source the shell integration
script _before_ other things in their `.bashrc`, so that aligns with the
expected behavior of the 'sudo' wrapper with regard to aliases. Given
that, I don't think we need any more explicit user-facing documentation
on this beyond the script-level comments.
2024-12-30 10:15:04 -05:00

180 lines
6.6 KiB
Bash

# This is originally based on the recommended bash integration from
# the semantic prompts proposal as well as some logic from Kitty's
# bash integration.
#
# I'm not a bash expert so this probably has some major issues but for
# my simple bash usage this is working. If a bash expert wants to
# improve this please do!
# We need to be in interactive mode and we need to have the Ghostty
# resources dir set which also tells us we're running in Ghostty.
if [[ "$-" != *i* ]] ; then builtin return; fi
if [ -z "$GHOSTTY_RESOURCES_DIR" ]; then builtin return; fi
# When automatic shell integration is active, we need to manually
# load the normal bash startup files based on the injected state.
if [ -n "$GHOSTTY_BASH_INJECT" ]; then
builtin declare ghostty_bash_inject="$GHOSTTY_BASH_INJECT"
builtin unset GHOSTTY_BASH_INJECT ENV
# At this point, we're in POSIX mode and rely on the injected
# flags to guide is through the rest of the startup sequence.
# POSIX mode was requested by the user so there's nothing
# more to do that optionally source their original $ENV.
# No other startup files are read, per the standard.
if [[ "$ghostty_bash_inject" == *"--posix"* ]]; then
if [ -n "$GHOSTTY_BASH_ENV" ]; then
builtin source "$GHOSTTY_BASH_ENV"
builtin export ENV="$GHOSTTY_BASH_ENV"
fi
else
# Restore bash's default 'posix' behavior. Also reset 'inherit_errexit',
# which doesn't happen as part of the 'posix' reset.
builtin set +o posix
builtin shopt -u inherit_errexit 2>/dev/null
# Unexport HISTFILE if it was set by the shell integration code.
if [[ -n "$GHOSTTY_BASH_UNEXPORT_HISTFILE" ]]; then
builtin export -n HISTFILE
builtin unset GHOSTTY_BASH_UNEXPORT_HISTFILE
fi
# Manually source the startup files, respecting the injected flags like
# --norc and --noprofile that we parsed with the shell integration code.
#
# See also: run_startup_files() in shell.c in the Bash source code
if builtin shopt -q login_shell; then
if [[ $ghostty_bash_inject != *"--noprofile"* ]]; then
[ -r /etc/profile ] && builtin source "/etc/profile"
for rcfile in "$HOME/.bash_profile" "$HOME/.bash_login" "$HOME/.profile"; do
[ -r "$rcfile" ] && { builtin source "$rcfile"; break; }
done
fi
else
if [[ $ghostty_bash_inject != *"--norc"* ]]; then
# The location of the system bashrc is determined at bash build
# time via -DSYS_BASHRC and can therefore vary across distros:
# Arch, Debian, Ubuntu use /etc/bash.bashrc
# Fedora uses /etc/bashrc sourced from ~/.bashrc instead of SYS_BASHRC
# Void Linux uses /etc/bash/bashrc
# Nixos uses /etc/bashrc
for rcfile in /etc/bash.bashrc /etc/bash/bashrc /etc/bashrc; do
[ -r "$rcfile" ] && { builtin source "$rcfile"; break; }
done
if [[ -z "$GHOSTTY_BASH_RCFILE" ]]; then GHOSTTY_BASH_RCFILE="$HOME/.bashrc"; fi
[ -r "$GHOSTTY_BASH_RCFILE" ] && builtin source "$GHOSTTY_BASH_RCFILE"
fi
fi
fi
builtin unset GHOSTTY_BASH_ENV GHOSTTY_BASH_RCFILE
builtin unset ghostty_bash_inject rcfile
fi
# Sudo
if [[ "$GHOSTTY_SHELL_INTEGRATION_NO_SUDO" != "1" && -n "$TERMINFO" ]]; then
# Wrap `sudo` command to ensure Ghostty terminfo is preserved.
#
# This approach supports wrapping a `sudo` alias, but the alias definition
# must come _after_ this function is defined. Otherwise, the alias expansion
# will take precedence over this function, and it won't be wrapped.
function sudo {
builtin local sudo_has_sudoedit_flags="no"
for arg in "$@"; do
# Check if argument is '-e' or '--edit' (sudoedit flags)
if [[ "$arg" == "-e" || $arg == "--edit" ]]; then
sudo_has_sudoedit_flags="yes"
builtin break
fi
# Check if argument is neither an option nor a key-value pair
if [[ "$arg" != -* && "$arg" != *=* ]]; then
builtin break
fi
done
if [[ "$sudo_has_sudoedit_flags" == "yes" ]]; then
builtin command sudo "$@";
else
builtin command sudo TERMINFO="$TERMINFO" "$@";
fi
}
fi
# Import bash-preexec, safe to do multiple times
builtin source "$GHOSTTY_RESOURCES_DIR/shell-integration/bash/bash-preexec.sh"
# This is set to 1 when we're executing a command so that we don't
# send prompt marks multiple times.
_ghostty_executing=""
_ghostty_last_reported_cwd=""
function __ghostty_get_current_command() {
builtin local last_cmd
# shellcheck disable=SC1007
last_cmd=$(HISTTIMEFORMAT= builtin history 1)
last_cmd="${last_cmd#*[[:digit:]]*[[:space:]]}" # remove leading history number
last_cmd="${last_cmd#"${last_cmd%%[![:space:]]*}"}" # remove remaining leading whitespace
builtin printf "\e]2;%s\a" "${last_cmd//[[:cntrl:]]}" # remove any control characters
}
function __ghostty_precmd() {
local ret="$?"
if test "$_ghostty_executing" != "0"; then
_GHOSTTY_SAVE_PS0="$PS0"
_GHOSTTY_SAVE_PS1="$PS1"
_GHOSTTY_SAVE_PS2="$PS2"
# Marks
PS1=$PS1'\[\e]133;B\a\]'
PS2=$PS2'\[\e]133;B\a\]'
# bash doesn't redraw the leading lines in a multiline prompt so
# mark the last line as a secondary prompt (k=s) to prevent the
# preceding lines from being erased by ghostty after a resize.
if [[ "${PS1}" == *"\n"* || "${PS1}" == *$'\n'* ]]; then
PS1=$PS1'\[\e]133;A;k=s\a\]'
fi
# Cursor
if test "$GHOSTTY_SHELL_INTEGRATION_NO_CURSOR" != "1"; then
PS1=$PS1'\[\e[5 q\]'
PS0=$PS0'\[\e[0 q\]'
fi
if [[ "$GHOSTTY_SHELL_INTEGRATION_NO_TITLE" != 1 ]]; then
# Command and working directory
# shellcheck disable=SC2016
PS0=$PS0'$(__ghostty_get_current_command)'
PS1=$PS1'\[\e]2;$PWD\a\]'
fi
fi
if test "$_ghostty_executing" != ""; then
# End of current command. Report its status.
builtin printf "\e]133;D;%s;aid=%s\a" "$ret" "$BASHPID"
fi
# unfortunately bash provides no hooks to detect cwd changes
# in particular this means cwd reporting will not happen for a
# command like cd /test && cat. PS0 is evaluated before cd is run.
if [[ "$_ghostty_last_reported_cwd" != "$PWD" ]]; then
_ghostty_last_reported_cwd="$PWD"
builtin printf "\e]7;kitty-shell-cwd://%s%s\a" "$HOSTNAME" "$PWD"
fi
# Fresh line and start of prompt.
builtin printf "\e]133;A;aid=%s\a" "$BASHPID"
_ghostty_executing=0
}
function __ghostty_preexec() {
PS0="$_GHOSTTY_SAVE_PS0"
PS1="$_GHOSTTY_SAVE_PS1"
PS2="$_GHOSTTY_SAVE_PS2"
builtin printf "\e]133;C;\a"
_ghostty_executing=1
}
preexec_functions+=(__ghostty_preexec)
precmd_functions+=(__ghostty_precmd)