Add extensive documentation and fix 2 bugs

Bug 1: Allow trailing space for inhibiting recognition of evaluating aliases

Bug 2: Preserve pre-evaluation aliases in file extension aliases
This commit is contained in:
tsi 2025-12-25 20:25:38 +01:00
parent fb37d2b89e
commit d241bfda6a
No known key found for this signature in database

147
bash-abbr
View file

@ -4,14 +4,112 @@
# put into the bashrc drop-in directory ~/.bashrc.d/ # put into the bashrc drop-in directory ~/.bashrc.d/
# and then sourced from ~/.bashrc. # and then sourced from ~/.bashrc.
# Disable plugin by uncommenting this line # Disable plugin by uncommenting the following line
# return # return
# WARNING 1: This plugin creates and overwrites various readline bindings. ###### bash-abbr: fish like abbreviations for bash, and more ######
# 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) # After sourcing this plugin and in an interactive bash shell
# WARNING 2: This plugin needs and enables extended globbing. # where --noediting was not supplied during invocation
shopt -s extglob # (readline must be enabled), regular bash aliases will be
# expanded as you type. This is different from normal alias
# behavior, where they are only expanded internally, invisible
# to the user. This is useful when sharing the screen, for
# making edits to the expanded version of an alias, or for
# personal reassurance that you're executing the correct command.
#
# Regular, internal alias expansion will be disabled for this in order
# to not exhibit unexpected behavior. See the end of this file for an explanation.
#
# Besides the expected behavior, there are a few extra features
# that are usually useful.
#
## Global Aliases (can be combined with other alias types):
# These aliases expand anywhere on the command line.
# Syntax:
# - alias [alias-name]=*[alias-value]
# - alias [alias-name]=$[alias-value] (identical but self-insertion is inhibited, see below)
# Examples:
# - alias G='*| grep'
# Typing `G` and pressing space will expand the string after the asterisk
# and self-insert the space.
#
## Post Evaluating Aliases:
# These aliases expand to the evaluation of the *recursively expanded* alias-value.
# Syntax:
# - alias [alias-name]=[alias-value]!
# Examples:
# - alias g=git gph='printf "g push %q HEAD" "$(git remote | head -1)"!'
# Typing `gph` and pressing enter might evaluate to something like
# `git push origin HEAD`. The `origin` part is dynamic, it will
# expand to whatever the first configured git remote is. Because this
# is the post-evaluating form of the evaluating aliases, the `g` was first
# expanded to `git`, only after which it was evaluated.
#
## Pre Evaluating Aliases:
# These aliases expand to the evaluation of the initial alias-value, after
# which recursive alias expansion continues.
# Syntax:
# - alias [alias-name]=[alias-value]$
# Examples:
# - alias c='*wl-paste$'
# Typing `c` anywhere on the command line will immediately expand to the
# clipboard contents on wayland based systems. The expansion is subject
# to further recursive alias expansion. Note that
# - alias c='*wl-paste!'
# makes less sense, because copying a command that contains an alias would
# not expand in this case, leading to a "command not found"
#
## File Extension Aliases:
# These aliases expand words on the command line that end with a file extension
# (dot + suffix) to a custom command invocation that opens files of that file type.
# Similar to xdg-open and Freedesktop MIME types, but for the command line.
# Syntax:
# - alias .[file-extension-without-dot]=[program-handler]
# Examples:
# - alias .txt=v .js=v .bashrc=v
# Typing `test.txt`, `~/.bashrc` or `index.js` and pressing enter will expand to
# `v test.txt` (or the others respectively), which might in turn expand to `vim test.txt`
# if there is an alias like `alias v=vim`.
#
## Aliases with inhibited self-insertion:
# Upon expanding these aliases, there won't be any self-insertion at all.
# Syntax:
# - alias [alias-name]=-[alias-value]
# - alias [alias-name]=$[alias-value] (global alias)
# Examples:
# - alias gssh='$git@github.com:'
# Typing `gssh` and pressing space will expand to `git@github.com:` without any trailing space.
# This is useful, because you usually enter a GitHub username directly after the colon without
# any characters in between. The `$` form was chosen because this alias can usually appear
# anywhere on the command line.
# - alias cuhs='-curl https://'
# Typing `cuhs` and pressing space will expand to `curl https://` without any trailing space.
# This is similar to the example above, only that the `-` form was chose because this alias
# usually doesn't make sense anywhere on the command line.
#
## Preventing alias types from being recognized:
# If the alias-value is supposed to start with a $, * or !, then you can prefix it with a single space.
# If the alias-value is supposed to start with a space, prefix it with another space.
# If the alias-value is supposed to end with $ or !, suffix it with a space.
# If the alias-value is supposed to end with a space, suffix it with another space.
#
## Combing alias types:
# Every alias type can be combined with any other alias type, as far as the syntax
# allows it (only one trailing character, only one leading character).
# Therefore, you can create an alias like this:
# - alias c=cat .txt='$c!'
# And then use it on the command line like this:
# - echo a.txt|
# `|` represents the cursor. Assuming the file a.txt contains the text `hey`,
# pressing space now would lead to the following result:
# - echo hey|
# Therefore, a global alias without self-insertion ($) was used on a file extension alias (.txt)
# and the result recursively expanded (c -> cat) and finally evaluated (post-evaluation, !).
# WARNING: This plugin creates and overwrites various readline 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)
# Regexes for heuristically determining whether we're abbreviating the first word. # Regexes for heuristically determining whether we're abbreviating the first word.
# Construction put into functions to prevent global namespace pollution # Construction put into functions to prevent global namespace pollution
@ -96,9 +194,9 @@ _try_abbrev_internal () {
# 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}
line_from_point_without_partial_word=${line_from_point##*([^<>&;()|[:space:]])} # extglob must be enabled! trailing_part_of_word=${line_from_point%%[<>&;()|[:space:]]*}
# 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=${#line}-${#line_from_point_without_partial_word} point=$((point+${#trailing_part_of_word}))
fi fi
declare -r point line_until_point=${line::point} declare -r point line_until_point=${line::point}
@ -117,7 +215,12 @@ _try_abbrev_internal () {
# Account for file extension aliases. Normal aliases take precedence # Account for file extension aliases. Normal aliases take precedence
declare file_extension_replacement=${BASH_ALIASES[".${word_at_point##*.}"]-} && declare file_extension_replacement=${BASH_ALIASES[".${word_at_point##*.}"]-} &&
[[ $replacement ]] || { replacement=${file_extension_replacement:+${file_extension_replacement%!} $word_at_point${file_extension_replacement: -1}} && replacement=${replacement%[^!]}; } && [[ $replacement ]] ||
{ # The logic below preserved trailing exclamation points (`!`) or dollar signs (`$`) at the end,
# which are special characters used for evaluating aliases.
replacement=${file_extension_replacement:+${file_extension_replacement%[$\!]} $word_at_point${file_extension_replacement: -1}} &&
replacement=${replacement%[^$\!]}
} &&
[[ $replacement ]] && [[ $replacement ]] &&
@ -144,24 +247,29 @@ _try_abbrev_internal () {
# Strip special flags (or space) from the beginning of the replacement # Strip special flags (or space) from the beginning of the replacement
replacement=${replacement#[*$ -]} replacement=${replacement#[*$ -]}
# Account for Post Evaluating Aliases
# shellcheck disable=SC2015 # shellcheck disable=SC2015
[[ ${replacement: -1} = "!" ]] && declare -r eval_expansion=true || declare -r eval_expansion=false [[ ${replacement: -1} = "!" ]] && declare -r eval_expansion=true || declare -r eval_expansion=false
[[ ${replacement: -1} = "$" ]] && replacement=$(eval "${replacement%$}") # Account for and potentially perform Pre Evaluating Alias expansion
[[ ${replacement: -1} = "$" ]] && replacement=$(eval "${replacement%$}") # TODO use bash 5.3 `${}` command substitution syntax to avoid subshells
# Recursively expand replacement if we haven't expanded it already # Recursively expand replacement if we haven't expanded it already
declare tmp=" $replacement" declare tmp=" $replacement" # Prepend space, makes the logic easier
[[ $tmp =~ ^([[:space:]]*$is_first_word_prefix) ]] && tmp=${tmp#"${BASH_REMATCH[1]}"} [[ $tmp =~ ^([[:space:]]*$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%%[<>&;()|[:space:]]*}
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"
declare -n recursively_expanded_replacement="ret_$level" declare -n recursively_expanded_replacement="ret_$level"
_try_abbrev_internal " $replacement" "$point_for_replacement" "" "ret_$level" "_" "_" "" _try_abbrev_internal " $replacement" "$point_for_replacement" "" "ret_$level" "_" "_" ""
replacement=${recursively_expanded_replacement# } replacement=${recursively_expanded_replacement# } # Strip the prepended space again
fi fi
# Eval if it has a trailing exclamation point and strip trailing \n and \r # Perform Post Evaluating Alias expansion
"$eval_expansion" && replacement=$(eval "${replacement%!}") "$eval_expansion" && replacement=$(eval "${replacement%!}") # TODO use bash 5.3 `${}` command substitution syntax to avoid subshells
# Strip trailing space that might've been added to prevent evaluating aliases from being recognized
replacement=${replacement% }
# Calculate point and mark offset. # Calculate point and mark offset.
declare -ir offset=${#replacement}-${#word_at_point}+${#self_insertion} declare -ir offset=${#replacement}-${#word_at_point}+${#self_insertion}
@ -209,7 +317,7 @@ bash_abbr () {
for ch in ' ' '|' '&' ';' '(' ')' '<' '>'; do for ch in ' ' '|' '&' ';' '(' ')' '<' '>'; do
bind -x '"\e\C-a'"$ch"'": _try_abbrev "'"$ch"'"' bind -x '"\e\C-a'"$ch"'": _try_abbrev "'"$ch"'"'
bind '"'"$ch"'": "\e\C-a'"$ch"'"' bind '"'"$ch"'": "\e\C-a'"$ch"'"'
# bind '"'"$ch"'": "\e\C-a'"$ch"'\e^"' # Moves point weirdly # bind '"'"$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),
@ -228,4 +336,9 @@ bash_abbr
# If the plugin is enabled, we usually don't want regular alias expansion, # If the plugin is enabled, we usually don't want regular alias expansion,
# since it can lead to undesirable "double expansions" if an alias is defined recursively. # since it can lead to undesirable "double expansions" if an alias is defined recursively.
type bash_abbr &>/dev/null && shopt -u expand_aliases # fish solves this by treating aliases differently from abbreviations.
# This, however, leads to the invonvenience of defining aliases/abbrevations twice,
# or give up one of the two. This plugin prefers the disabling of normal aliases, since
# it's more transparent to the user. The final command line is exactly what will be
# executed, no hidden aliases that might add hidden flags to some of your commands.
[[ $(type -t bash_abbr) = function ]] && shopt -u expand_aliases # TODO use bash 5.3 `${}` command substitution syntax to avoid subshells