Allow an even number of backslashes before control operators and use variables for shell metacharacters

This commit is contained in:
tsi 2026-01-03 01:21:57 +01:00
parent 31ad7d7c28
commit b2639827d7
No known key found for this signature in database

View file

@ -111,15 +111,21 @@
# Therefore, it might interfere with other plugins/custom bindings. # Therefore, it might interfere with other plugins/custom bindings.
# Some bindings may be changed (e.g. \e\C-a...) while others should not (e.g. \C-m) # Some bindings may be changed (e.g. \e\C-a...) while others should not (e.g. \C-m)
# Regexes for heuristically determining whether we're abbreviating the first word. # Setting IFS to null so we can use "${var[*]}" syntax to make a string out of the meta char array
_bash_base_meta_chars=('|' '&' ';' '(' ')')
_bash_meta_chars_without_left_right_angle_brackets=("${_bash_base_meta_chars[@]}" ' ' $'\t' $'\n')
_bash_meta_chars_without_tab_and_nl=("${_bash_base_meta_chars[@]}" '<' '>' ' ')
_bash_meta_chars=("${_bash_base_meta_chars[@]}" '<' '>' ' ' $'\t' $'\n')
# Regexes for heuristically determining whether we're doing expansion on the first word.
# Construction put into functions to prevent global namespace pollution # Construction put into functions to prevent global namespace pollution
# and to increase readability. # and to increase readability.
_control_op () { _control_op () {
declare -r _newline=$'\n' \ declare -r _newline=$'\n' \
_bg_proc_or_and_and='[^&;]&&?' \ _bg_proc_or_and_and='[^\\](\\\\)*&&?' \
_pipe_or_or_or='[^&|;]\|\|?' \ _pipe_or_or_or='[^\\](\\\\)*\|\|?' \
_semicolon_or_case_operators='[^;|];;?&?' \ _semicolon_or_case_operators='[^\\](\\\\)*;;?&?' \
_opening_paren='(^|[^[:alnum:]])\(' _opening_paren='(^|[^[:alnum:]\\])(\\\\)*\('
declare _control_op="^" declare _control_op="^"
_control_op+="|$_newline" _control_op+="|$_newline"
_control_op+="|$_bg_proc_or_and_and" _control_op+="|$_bg_proc_or_and_and"
@ -190,11 +196,14 @@ _try_abbrev_internal () {
# set a variable defined in the calling context. This level is just appended to the identifier to make it unique # 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. # across all the recursive calls.
# IFS must be null so "${var[*]}" concatenates without space
declare -r IFS=
# If the self-insertion character is null (i.e. when pressing enter/accepting a line), # If the self-insertion character is null (i.e. when pressing enter/accepting a line),
# then we also want to expand aliases if the cursor/point is *on* the word instead of just *after* the word. # then we also want to expand aliases if the cursor/point is *on* the word instead of just *after* the word.
if [[ -z $self_insertion ]]; then if [[ -z $self_insertion ]]; then
line_from_point=${line:point} line_from_point=${line:point}
trailing_part_of_word=${line_from_point%%[<>&;()|[:space:]]*} trailing_part_of_word=${line_from_point%%["${_bash_meta_chars[*]}"]*}
# Move point forward to right after the word. Keep mark where it is. # Move point forward to right after the word. Keep mark where it is.
point=$((point+${#trailing_part_of_word})) point=$((point+${#trailing_part_of_word}))
fi fi
@ -202,13 +211,13 @@ _try_abbrev_internal () {
if if
# Check if the character at point is a shell metacharacter or EOL # Check if the character at point is a shell metacharacter or EOL
# [[ $point -eq ${#line} || "${line:point:1}" = [\<\>\&\;\(\)\|[:space:]] ]] && # [[ $point -eq ${#line} || "${line:point:1}" = ["${_bash_meta_chars[*]}"] ]] &&
# NOTE: Above line commented out, it's actually not that useful. # NOTE: Above line commented out, it's actually not that useful.
# Determine word to complete # Determine word to complete
# Uses the same word separators as bash (| & ; ( ) space tab newline) (aka "shell metacharacters") # 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 # 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:]]} && declare -r word_at_point=${line_until_point##*["${_bash_meta_chars_without_left_right_angle_brackets[*]}"]} &&
# Ensure that word does not contain either < or > # Ensure that word does not contain either < or >
[[ ! $word_at_point =~ [\<\>] ]] && [[ ! $word_at_point =~ [\<\>] ]] &&
@ -260,7 +269,7 @@ _try_abbrev_internal () {
# Recursively expand replacement if we haven't expanded it already # Recursively expand replacement if we haven't expanded it already
declare tmp=" $replacement" # Prepend space, makes the logic easier declare tmp=" $replacement" # Prepend space, makes the logic easier
[[ $tmp =~ ^([[:blank:]]*$is_first_word_prefix) ]] && tmp=${tmp#"${BASH_REMATCH[1]}"} # Strip matching prefix [[ $tmp =~ ^([[:blank:]]*$is_first_word_prefix) ]] && tmp=${tmp#"${BASH_REMATCH[1]}"} # Strip matching prefix
declare -r word_to_expand=${tmp%%[<>&;()|[:space:]]*} declare -r word_to_expand=${tmp%%["${_bash_meta_chars[*]}"]*}
if [[ -z ${_expanded_aliases[$word_to_expand]-} ]]; then if [[ -z ${_expanded_aliases[$word_to_expand]-} ]]; then
declare -r point_for_replacement=$((${#BASH_REMATCH[1]}+${#word_to_expand})) declare -r point_for_replacement=$((${#BASH_REMATCH[1]}+${#word_to_expand}))
declare -g "ret_$level" declare -g "ret_$level"
@ -307,20 +316,23 @@ _try_abbrev () {
_try_abbrev_internal "$READLINE_LINE" "$READLINE_POINT" "$READLINE_MARK" READLINE_LINE READLINE_POINT READLINE_MARK "$@" _try_abbrev_internal "$READLINE_LINE" "$READLINE_POINT" "$READLINE_MARK" READLINE_LINE READLINE_POINT READLINE_MARK "$@"
# Don't pollute global namespace # Don't pollute global namespace
unset -v _expanded_aliases declare -gA _expanded_aliases=()
} }
bash_abbr () { bash_abbr () {
# IFS must be null so "${var[*]}" concatenates without space
declare -r IFS=
# From the bash man page: # From the bash man page:
# metacharacter # metacharacter
# A character that, when unquoted, separates words. One of the following: # A character that, when unquoted, separates words. One of the following:
# | & ; ( ) < > space tab newline # | & ; ( ) < > space tab newline
# Note: double quote should be escaped, if added to the list of metacharacters # Note: double quote should be escaped, if added to the list of metacharacters
declare ch declare ch
for ch in ' ' '|' '&' ';' '(' ')' '<' '>'; do for ch in "${_bash_meta_chars_without_tab_and_nl[@]}"; do
bind -x '"\e\C-a'"$ch"'": _try_abbrev "'"$ch"'"' bind_if -x '"\e\C-a'"$ch"'": _try_abbrev "'"$ch"'"'
bind '"'"$ch"'": "\e\C-a'"$ch"'"' bind_if '"'"$ch"'": "\e\C-a'"$ch"'"'
# bind '"'"$ch"'": "\e\C-a'"$ch"'\e^"' # TODO fix. Moves point weirdly atm. # bind_if '"'"$ch"'": "\e\C-a'"$ch"'\e^"' # TODO fix. Moves point weirdly atm.
done done
# TODO To get it fully working in all edge cases (as far as we can parse), # TODO To get it fully working in all edge cases (as far as we can parse),