#1: Add initial version of bash-abbr

This commit is contained in:
tsi 2025-10-20 17:31:32 +02:00
parent 816a0a24d7
commit 269f57e8c0
No known key found for this signature in database

145
bash-abbr Normal file
View file

@ -0,0 +1,145 @@
#!/usr/bin/env bash
# Shebang merely for syntax highlighting.
# This file should be pasted directly into ~/.bashrc or
# put into the bashrc drop-in directory ~/.bashrc.d/
# and then sourced from ~/.bashrc.
# WARNING: This plug-in creates a new readline binding: '\e\C-a'
# and overwrites the binding for '\C-m', ' ', '|', '&', ';', '(', ')', '<', '>'
# Therefore, it might interfere with other plug-ins or custom bindings.
# You may change the '\e\C-a' binding to whatever you want as it's only used
# as a hook inside another readline macro. The bindings '|', '&', ';', '(', ')', '<', '>'
# may also be dropped if you don't need them.
# Regexes for (heuristically) determining whether we're abbreviating the first word.
# Construction put into functions to prevent global namespace pollution
# and increase readability.
_control_op () {
declare -r _newline=$'\n' \
_bg_proc_or_and_and='[^&;]&&?' \
_pipe_or_or_or='[^&|;]\|\|?' \
_semicolon_or_case_operators='[^;|];;?&?' \
_opening_paren='[^[:alnum:]]\(' \
_closing_paren='\)'
declare _control_op="^"
_control_op+="|$_newline"
_control_op+="|$_bg_proc_or_and_and"
_control_op+="|$_pipe_or_or_or"
_control_op+="|$_semicolon_or_case_operators"
_control_op+="|$_opening_paren"
_control_op+="|$_closing_paren"
printf %s "$_control_op"
}
_control_op="$(_control_op)"
unset -f _control_op
_redir_or_assign_word () {
declare -r _varredir_or_number='\{[[:alpha:]_][[:alnum:]_]*\}|[[:digit:]]*' \
_redir_in_or_out='>|<' \
_close_or_move_fd='&[[:digit:]]*-?' \
_redir_or_append_out_and_err='(&)(>>?)' \
_heredoc_herestring_in_out_etc='>>?|<(>|<(-?|<))?' \
_identifier_equals='[[:alpha:]_][[:alnum:]]*=' \
_simple_unquoted_word='[^[:space:]<>&;()|'\''"`$]+' \
_double_quoted_string='\$?"([^"]|\\")*"' \
_single_quoted_string="\\\$?'[^']*'" \
_backtick_string='`([^`]|\\`)*`' \
_simple_variable='\$[[:alpha:]_][[:alnum:]]*' \
_advanced_variable='\$\{([[:alnum:]]+|[#-_$!*#])[^{}]*\}' \
_deprecated_arithmetic_eval='\$\[[^][]*\]' \
_cmd_subst_or_arithmetic_eval='\$\([^()]*\)' \
_process_substition='<\([^()]*\)' \
_escaped_char='\\.'
declare _part="$_simple_unquoted_word"
_part+="|$_double_quoted_string"
_part+="|$_single_quoted_string"
_part+="|$_backtick_string"
_part+="|$_simple_variable"
_part+="|$_advanced_variable"
_part+="|$_deprecated_arithmetic_eval"
_part+="|$_cmd_subst_or_arithmetic_eval"
_part+="|$_process_substition"
_part+="|$_escaped_char"
declare -r _word="($_part)+" \
_close_redirection="($_varredir_or_number)($_redir_in_or_out)($_close_or_move_fd)" \
_generic_redirection="($_redir_or_append_out_and_err|($_varredir_or_number)($_heredoc_herestring_in_out_etc))"
declare -r _word_redirection_or_assignment="($_generic_redirection[[:space:]]*|$_identifier_equals)($_word)"
printf %s "$_close_redirection|$_word_redirection_or_assignment"
}
_redir_or_assign_word="$(_redir_or_assign_word)"
unset -f _redir_or_assign_word
_try_abbrev () {
# For use within bind -x.
# Perform abbreviation lookup and interactive expansion on the shell-word left to the cursor (no quote parsing).
# Takes a single argument which is the character to self-insert (usually space or another shell metacharacter).
declare -r line_until_point=${READLINE_LINE::READLINE_POINT}
if
# Check if the character at point is a shell metacharacter or EOL
[[ $READLINE_POINT -eq ${#READLINE_LINE} || "${READLINE_LINE:READLINE_POINT:1}" = [\<\>\&\;\(\)\|[:space:]] ]] &&
# Determine word to complete.
# Uses the same word separators as bash (| & ; ( ) space tab newline) (aka "shell metacharacters")
# But < and > are ignored, since they always expect a file/delimiter/string afterwards and not a command
declare -r word_at_point=${line_until_point##*[&;()|[:space:]]} &&
# Select replacement (if any).
declare replacement=${BASH_ALIASES["${word_at_point:-__nonexistent}"]-} &&
# Determine if it's the first word of a simple command, unless we're expanding a global alias.
# Uses the same command separators as bash (|| & && ; ;; ;& ;;& ( ) | |& newline) (aka "control operators").
# Due to complexity and performance reasons, we cannot reliably know
# whether the cursor position (aka "point" in readline terms) is in a quoted/escaped
# shell context or not (we would have to implement a full-blown bash parser) or
# whether there are any redirections/assignment words before the first word.
# The only heuristic we try is to check whether we're at the beginning of the line or if
# there's a control operator immediately to the left of the word (ignoring whitespace)
# and that there's no character/metacharacter prefixing the control operator
# s.t. there's a syntax error. We also do some rudimentary redirection and assignment word
# checking (including quoting) after the control operator. If the syntax is invalid (as far as
# the regex can tell), then we bail and no alias expansion will happen.
[[ ${replacement::1} = "!" || ${line_until_point%"$word_at_point"} =~ ($_control_op)[[:space:]]*((($_redir_or_assign_word)[[:space:]])*)$ ]] &&
# If the regex leads to problems/performance issues, you may try this simpler regex
# that does no redirection/assignment word checking:
# [[ ${replacement::1} = "!" || ${line_until_point%"$word_at_point"} =~ ($_control_op)[[:space:]]*$ ]] &&
[[ $replacement ]]; then
# Account for global aliases (strip "!" prefix and disable self-insertion)
[[ ${replacement::1} = "!" ]] && replacement=${replacement#!} && set -- ''
# Calculate point and mark offset.
declare -ir offset=${#replacement}-${#word_at_point}+${#1}
# Update line.
READLINE_LINE=${line_until_point%"$word_at_point"}$replacement${1-}${READLINE_LINE:READLINE_POINT}
else
declare -ir offset=${#1}
# Self-insert by default
READLINE_LINE=${line_until_point}${1-}${READLINE_LINE:READLINE_POINT}
fi
# Advance mark if it's right of (or on) the point.
[[ $READLINE_MARK && $READLINE_POINT -le "$READLINE_MARK" ]] && READLINE_MARK=$((READLINE_MARK+offset))
# Advance point.
READLINE_POINT=$((READLINE_POINT+offset))
# NB: READLINE_ARGUMENT is deliberately ignored.
}
# From the bash man page:
# metacharacter
# A character that, when unquoted, separates words. One of the following:
# | & ; ( ) < > space tab newline
for ch in ' ' '|' '&' ';' '(' ')' '<' '>'; do
bind -x '"'"$ch"'": _try_abbrev "'"$ch"'"' # Note: double quote should be escaped, if added to the list of metacharacters
done
# Newline must be treated specially by creating a new binding which is called
# as a readline macro when pressing enter (C-m). After that is finished, the macro
# continues with a regular C-j, which accepts (=executes) the line as you would expect.
bind -x '"\e\C-a": _try_abbrev ""'
bind '"\C-m": "\e\C-a\e^\C-j"'