diff --git a/bash-abbr b/bash-abbr index e3dc6fa..2590e90 100644 --- a/bash-abbr +++ b/bash-abbr @@ -29,7 +29,7 @@ _control_op () { _control_op+="|$_opening_paren" printf %s "$_control_op" } -_control_op="$(_control_op)" +declare _control_op="$(_control_op)" unset -f _control_op _redir_or_assign_word () { @@ -65,19 +65,32 @@ _redir_or_assign_word () { 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)" +declare -r _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 is_first_word_prefix="($_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: +# declare -r is_first_word_prefix="($_control_op)[[:space:]]*" - declare -r line_until_point=${READLINE_LINE::READLINE_POINT} +unset -v _control_op + +_try_abbrev_internal () { + # Recursively perform abbrevation on the line defined by $1, taking $2 as the point and $3 as the mark. + # Set the vars defined by $4, $5 and $6 to the updated values (line, point and mark). + # $7 is the character/string to self-insert. + declare -r line=$1 point=$2 mark=$3 new_line=$4 new_point=$5 new_mark=$6 \ + self_insertion=$7 \ + level=${#FUNCNAME[@]} + # `level` is needed to keep track of the recursion, because we don't have a return value but rather + # set a variable defined in the calling context. This level is just appended to the identifier to make it unique + # across all the recursive calls. + + declare -r line_until_point=${line::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:]] ]] && + [[ $point -eq ${#line} || "${line:point:1}" = [\<\>\&\;\(\)\|[:space:]] ]] && # Determine word to complete. # Uses the same word separators as bash (| & ; ( ) space tab newline) (aka "shell metacharacters") @@ -87,7 +100,9 @@ _try_abbrev () { # 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. + [[ $replacement ]] && + + # Determine if it's the first word of a simple command, unless we're expanding a global alias (prefix: "*" or "$"). # 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 @@ -99,48 +114,81 @@ _try_abbrev () { # 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 + [[ ${replacement::1} =~ [*$] || ${line_until_point%"$word_at_point"} =~ $is_first_word_prefix$ ]]; then - # Account for global aliases (strip "!" prefix and disable self-insertion) - [[ ${replacement::1} = "!" ]] && replacement=${replacement#!} && set -- '' + _expanded_aliases[$word_at_point]=1 + + # Account for inhibited self-insertion + [[ ${replacement::1} =~ [-$] ]] && set -- '' + + # Strip special flags (or space) from the beginning of the replacement + replacement=${replacement#[*$ -]} + + # Recursively expand replacement if we haven't expanded it already + declare tmp=" $replacement" + [[ $tmp =~ ^([[:space:]]*$is_first_word_prefix) ]] && tmp=${tmp#"${BASH_REMATCH[1]}"} || echo NO + declare -r word_to_expand=${tmp%%[<>&;()|[:space:]]*} + if [[ ! ${_expanded_aliases[$word_to_expand]-} ]]; then + declare -r point_for_replacement=$((${#BASH_REMATCH[1]}+${#word_to_expand})) + declare -g "ret_$level" + declare -n recursively_expanded_replacement="ret_$level" + _try_abbrev_internal " $replacement" "$point_for_replacement" "" "ret_$level" "_" "_" "" + replacement=${recursively_expanded_replacement# } + fi # Calculate point and mark offset. - declare -ir offset=${#replacement}-${#word_at_point}+${#1} + declare -ir offset=${#replacement}-${#word_at_point}+${#self_insertion} # Update line. - READLINE_LINE=${line_until_point%"$word_at_point"}$replacement${1-}${READLINE_LINE:READLINE_POINT} + eval "$new_line"'=${line_until_point%"$word_at_point"}$replacement${self_insertion}${line:point}' else - declare -ir offset=${#1} + declare -ir offset=${#self_insertion} # Self-insert by default - READLINE_LINE=${line_until_point}${1-}${READLINE_LINE:READLINE_POINT} + eval "$new_line"'=${line_until_point}${self_insertion}${line: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)) + [[ $mark && $mark -ge "$point" ]] && eval "$new_mark"'=$((mark+offset))' # Advance point. - READLINE_POINT=$((READLINE_POINT+offset)) - + eval "$new_point"'=$((point+offset))' # NB: READLINE_ARGUMENT is deliberately ignored. } +_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). -# From the bash man page: -# metacharacter -# A character that, when unquoted, separates words. One of the following: -# | & ; ( ) < > space tab newline -# Note: double quote should be escaped, if added to the list of metacharacters -for ch in ' ' '|' '&' ';' '(' ')' '<' '>'; do - bind -x '"\e\C-a'"$ch"'": _try_abbrev "'"$ch"'"' - bind '"'"$ch"'": "\e\C-a'"$ch"'"' - # bind '"'"$ch"'": "\e\C-a'"$ch"'\e^"' # Moves point weirdly -done + # To prevent infinite recursion, we need to keep track + # of which aliases have been expanded. This function re-initializes the associative array (used as a hash set) + declare -gA _expanded_aliases=() -# Newline must be treated specially, because it doesn't self-insert -# and has the extra functionality of accepting a line. -bind -x '"\e\C-an": _try_abbrev ""' -bind '"\C-m": "\e\C-an\e^\C-j"' + # Calls the implementation and pass necessary arguments + _try_abbrev_internal "$READLINE_LINE" "$READLINE_POINT" "$READLINE_MARK" READLINE_LINE READLINE_POINT READLINE_MARK "$@" + + # Don't pollute global namespace + unset -v _expanded_aliases +} + +main () { + # From the bash man page: + # metacharacter + # A character that, when unquoted, separates words. One of the following: + # | & ; ( ) < > space tab newline + # Note: double quote should be escaped, if added to the list of metacharacters + declare ch + for ch in ' ' '|' '&' ';' '(' ')' '<' '>'; do + bind -x '"\e\C-a'"$ch"'": _try_abbrev "'"$ch"'"' + bind '"'"$ch"'": "\e\C-a'"$ch"'"' + # bind '"'"$ch"'": "\e\C-a'"$ch"'\e^"' # Moves point weirdly + done + + # Newline must be treated specially, because it doesn't self-insert + # and has the extra functionality of accepting a line. + bind -x '"\e\C-an": _try_abbrev ""' + bind '"\C-m": "\e\C-an\e^\C-j"' +} + +# Disable plugin by commenting this line +main