# Make Bash more like Zsh ## Hooks # https://zsh.sourceforge.io/Doc/Release/Functions.html#Hook-Functions # TODO: support precmd_functions and chpwd_functions arrays ### Defaults: NOOPs function preexec { :; } function precmd { :; } function chpwd { :; } ### Implementations # Alternative cd function that calls chpwd afterwards builtin cd # # zsh: Executed whenever the current working directory is changed. __cd_invoke_chpwd() { builtin cd "$@" chpwd # TODO: chpwd_functions array } # zsh: Executed before each prompt. Note that precommand functions are not # re-executed simply because the command line is redrawn, as happens, for # example, when a notification about an exiting job is displayed. __prompt_command_invoke_precmd() { precmd # TODO: precmd_functions array } # to properly trigger only before command exec this must be the last prompt command __prompt_command_hook_preexec() { trap '__debug_invoke_preexec' DEBUG } __prompt_command_unhook_preexec() { # Unfortunately there's no way to remove just one trap from a signal trap - DEBUG } # zsh: Executed just after a command has been read and is about to be executed. If # the history mechanism is active (regardless of whether the line was discarded # from the history buffer), the string that the user typed is passed as the # first argument, otherwise it is an empty string. The actual command that will # be executed (including expanded aliases) is passed in two different forms: # the second argument is a single-line, size-limited version of the command # (with things like function bodies elided); the third argument contains the # full text that is being executed. __debug_invoke_preexec () { # somehow PROMPT_COMMAND contents seems to be still affected by the # just-removed trap. Ignore ours. [[ "$BASH_COMMAND" == "__prompt_command_unhook_preexec" ]] && return [[ "$BASH_COMMAND" == "__prompt_command_invoke_precmd" ]] && return [[ "$BASH_COMMAND" == "__prompt_command_hook_preexec" ]] && return # TODO: not sure this is still necessary [[ -n "$COMP_LINE" ]] && return # completion local this_command; # TODO: hack: doesn't work when command is not added to history this_command=`history 1 | sed -e "s/^[ ]*[0-9]*[ ]*//g"`; preexec "$this_command" } ### Set up the hooks # Define a function to override the cd builtin # # - does not apply to `bash -c 'cd foo` # - does apply to shubshells: `(cd foo)` will call so side effects beware (env # vars are conveniently scoped to subshell) cd() { __cd_invoke_chpwd "$@"; } # Call on each prompt. This transitively sets preexec to work for each prompt # # PROMPT_COMMAND is evaluated right before PS1 is displayed. This matches zsh # semantics for precmd. # # We forcefully set it instead of taking whatever was set: # - to control order (precmd eval + preexec hooking must be last) # - because of the trap issue PROMPT_COMMAND=( # this also conveniently clears DEBUG traps __prompt_command_unhook_preexec # precmd hook processing __prompt_command_invoke_precmd # must be last to properly have DEBUG trigger only before command exec __prompt_command_hook_preexec ) ## Other bits # clears a line that was not terminated by a LF fixing the dangling prompt # issue by marking it with a reversed %, like zsh clear_incomplete_line() { local row local col # ask for cursor position then read answer # TODO: fix read: mashing keyboard makes syntax errors stty -echo echo -en "\033[6n" IFS=';' read -r -d R -a pos stty echo # extract tput-compatible answer row=$(( ${pos[0]:2} - 1 )) col=$(( ${pos[1]} - 1 )) # not on first column? do clean up: fill with spaces and rag left if [[ $col != 0 ]]; then printf "\e[7m%%\e[m" printf "%*s\r" $(( COLUMNS - $col )) fi } function sub_prompt_colors_unsized() { sed \ -e 's#%F{black}#\\033[30m#g' \ -e 's#%F{red}#\\033[31m#g' \ -e 's#%F{green}#\\033[32m#g' \ -e 's#%F{yellow}#\\033[33m#g' \ -e 's#%F{blue}#\\033[34m#g' \ -e 's#%F{magenta}#\\033[35m#g' \ -e 's#%F{cyan}#\\033[36m#g' \ -e 's#%F{white}#\\033[37m#g' \ -e 's#%f#\\033[00m#g' } function sub_prompt_colors_sized() { sed \ -e 's#%F{black}#\\[\\033[30m\\]#g' \ -e 's#%F{red}#\\[\\033[31m\\]#g' \ -e 's#%F{green}#\\[\\033[32m\\]#g' \ -e 's#%F{yellow}#\\[\\033[33m\\]#g' \ -e 's#%F{blue}#\\[\\033[34m\\]#g' \ -e 's#%F{magenta}#\\[\\033[35m\\]#g' \ -e 's#%F{cyan}#\\[\\033[36m\\]#g' \ -e 's#%F{white}#\\[\\033[37m\\]#g' \ -e 's#%f#\\[\\033[00m\\]#g' } function strip_prompt_colors() { sed \ -e 's#%F{black}##g' \ -e 's#%F{red}##g' \ -e 's#%F{green}##g' \ -e 's#%F{yellow}##g' \ -e 's#%F{blue}##g' \ -e 's#%F{magenta}##g' \ -e 's#%F{cyan}##g' \ -e 's#%F{white}##g' \ -e 's#%f##g' } # right prompt support and PROMPT/RPROMPT vars function apply_prompt_rprompt() { local rprompt=$(echo "${RPROMPT}" | sub_prompt_colors_unsized) local prompt=$(echo "${PROMPT}" | sub_prompt_colors_sized) #local rprompt=$(echo "${RPROMPT}" | strip_prompt_colors) #local prompt=$(echo "${PROMPT}" | strip_prompt_colors) if [[ -n "${RPROMPT}" ]]; then PS1="$(printf "\[%*s\r\]%s" "${COLUMNS}" "${rprompt:-}" "${prompt:-}")" else PS1="${prompt:-}" fi } # vim: ft=bash