diff --git a/bash-abbr b/bash-abbr index 68dcb90..087eb4a 100644 --- a/bash-abbr +++ b/bash-abbr @@ -4,14 +4,112 @@ # put into the bashrc drop-in directory ~/.bashrc.d/ # and then sourced from ~/.bashrc. -# Disable plugin by uncommenting this line +# Disable plugin by uncommenting the following line # return -# WARNING 1: 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) -# WARNING 2: This plugin needs and enables extended globbing. -shopt -s extglob +###### bash-abbr: fish like abbreviations for bash, and more ###### +# +# After sourcing this plugin and in an interactive bash shell +# where --noediting was not supplied during invocation +# (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. # 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. if [[ -z $self_insertion ]]; then 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. - point=${#line}-${#line_from_point_without_partial_word} + point=$((point+${#trailing_part_of_word})) fi declare -r point line_until_point=${line::point} @@ -117,7 +215,12 @@ _try_abbrev_internal () { # Account for file extension aliases. Normal aliases take precedence 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 ]] && @@ -144,24 +247,29 @@ _try_abbrev_internal () { # Strip special flags (or space) from the beginning of the replacement replacement=${replacement#[*$ -]} + # Account for Post Evaluating Aliases # shellcheck disable=SC2015 [[ ${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 - declare tmp=" $replacement" - [[ $tmp =~ ^([[:space:]]*$is_first_word_prefix) ]] && tmp=${tmp#"${BASH_REMATCH[1]}"} + declare tmp=" $replacement" # Prepend space, makes the logic easier + [[ $tmp =~ ^([[:space:]]*$is_first_word_prefix) ]] && tmp=${tmp#"${BASH_REMATCH[1]}"} # Strip matching prefix declare -r word_to_expand=${tmp%%[<>&;()|[:space:]]*} if [[ -z ${_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# } + replacement=${recursively_expanded_replacement# } # Strip the prepended space again fi - # Eval if it has a trailing exclamation point and strip trailing \n and \r - "$eval_expansion" && replacement=$(eval "${replacement%!}") + # Perform Post Evaluating Alias expansion + "$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. declare -ir offset=${#replacement}-${#word_at_point}+${#self_insertion} @@ -209,7 +317,7 @@ bash_abbr () { 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 + # bind '"'"$ch"'": "\e\C-a'"$ch"'\e^"' # TODO fix. Moves point weirdly atm. done # 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, # 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