#1: Add initial version of bash-abbr
This commit is contained in:
parent
816a0a24d7
commit
269f57e8c0
1 changed files with 145 additions and 0 deletions
145
bash-abbr
Normal file
145
bash-abbr
Normal 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"'
|
||||
Loading…
Add table
Add a link
Reference in a new issue