361 lines
18 KiB
Bash
361 lines
18 KiB
Bash
#!/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.
|
|
|
|
# Disable plugin by uncommenting the following line
|
|
# return
|
|
|
|
###### 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)
|
|
|
|
# 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
|
|
# and to increase readability.
|
|
_control_op () {
|
|
declare -r _newline=$'\n' \
|
|
_bg_proc_or_and_and='[^\\](\\\\)*&&?' \
|
|
_pipe_or_or_or='[^\\](\\\\)*\|\|?' \
|
|
_semicolon_or_case_operators='[^\\](\\\\)*;;?&?' \
|
|
_opening_paren='(^|[^[:alnum:]\\])(\\\\)*\('
|
|
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"
|
|
printf %s "$_control_op"
|
|
unset -f _control_op
|
|
}
|
|
# shellcheck disable=SC2155
|
|
declare _control_op="$(_control_op)" # TODO use bash 5.3 `${}` command substitution syntax to avoid subshells
|
|
|
|
_redir_or_assign_word () {
|
|
# shellcheck disable=SC2016
|
|
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='[^[:blank:]<>&;()|'\''"`$]+' \
|
|
_double_quoted_string='\$?"([^"]|\\")*"' \
|
|
_single_quoted_string="\\\$?'[^']*'" \
|
|
_backtick_string='`([^`]|\\`)*`' \
|
|
_simple_variable='\$([[:alpha:]_][[:alnum:]]*|[-_$!*#[:digit:]])' \
|
|
_advanced_variable='\$\{([[:alpha:]_][[:alnum:]]*|[-_$!*#[:digit:]])[^}]*\}' \
|
|
_deprecated_arithmetic_eval='\$\[[^][]*\]' \
|
|
_cmd_subst='\$\([^()]*\)' \
|
|
_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"
|
|
_part+="|$_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))"
|
|
# shellcheck disable=SC1087
|
|
declare -r _word_redirection_or_assignment="($_generic_redirection[[:blank:]]*|$_identifier_equals)($_word)"
|
|
printf %s "$_close_redirection|$_word_redirection_or_assignment"
|
|
unset -f _redir_or_assign_word
|
|
}
|
|
# shellcheck disable=SC2155
|
|
declare _redir_or_assign_word="$(_redir_or_assign_word)" # TODO use bash 5.3 `${}` command substitution syntax to avoid subshells
|
|
|
|
declare -gr is_first_word_prefix="($_control_op)[[:blank:]]*((($_redir_or_assign_word)[[:blank:]]+)*)"
|
|
# 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)[[:blank:]]*"
|
|
|
|
unset -v _control_op _redir_or_assign_word
|
|
|
|
_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 mark=$3 new_line=$4 new_point=$5 new_mark=$6 \
|
|
level=${#FUNCNAME[@]}
|
|
declare point=$2
|
|
declare self_insertion=$7
|
|
# `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.
|
|
|
|
# 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),
|
|
# 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}
|
|
trailing_part_of_word=${line_from_point%%["${_bash_meta_chars[*]}"]*}
|
|
# Move point forward to right after the word. Keep mark where it is.
|
|
point=$((point+${#trailing_part_of_word}))
|
|
fi
|
|
declare -r point line_until_point=${line::point}
|
|
|
|
if
|
|
# Check if the character at point is a shell metacharacter or EOL
|
|
# [[ $point -eq ${#line} || "${line:point:1}" = ["${_bash_meta_chars[*]}"] ]] &&
|
|
# NOTE: Above line commented out, it's actually not that useful.
|
|
|
|
# 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##*["${_bash_meta_chars_without_left_right_angle_brackets[*]}"]} &&
|
|
|
|
# Ensure that word does not contain either < or >
|
|
[[ ! $word_at_point =~ [\<\>] ]] &&
|
|
|
|
# Select replacement (if any)
|
|
declare replacement=${BASH_ALIASES["${word_at_point:-__nonexistent}"]-} &&
|
|
|
|
# Account for file extension aliases. Normal aliases take precedence
|
|
declare file_extension_replacement=${BASH_ALIASES[".${word_at_point##*.}"]-} &&
|
|
[[ $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 ]] &&
|
|
|
|
# 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
|
|
# 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"} =~ $is_first_word_prefix$ ]]; then
|
|
|
|
# Prevent infinite recursion
|
|
_expanded_aliases[$word_at_point]=1
|
|
|
|
# Account for inhibited self-insertion
|
|
[[ ${replacement::1} =~ [-$] ]] && self_insertion=
|
|
|
|
# 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
|
|
|
|
# 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" # Prepend space, makes the logic easier
|
|
[[ $tmp =~ ^([[:blank:]]*$is_first_word_prefix) ]] && tmp=${tmp#"${BASH_REMATCH[1]}"} # Strip matching prefix
|
|
declare -r word_to_expand=${tmp%%["${_bash_meta_chars[*]}"]*}
|
|
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# } # Strip the prepended space again
|
|
fi
|
|
# 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}
|
|
|
|
# Update line.
|
|
eval "$new_line"'=${line_until_point%"$word_at_point"}$replacement${self_insertion}${line:point}'
|
|
else
|
|
# shellcheck disable=SC2034
|
|
declare -ir offset=${#self_insertion}
|
|
|
|
# Self-insert by default
|
|
eval "$new_line"'=${line_until_point}${self_insertion}${line:point}'
|
|
fi
|
|
|
|
# Advance mark if it's set and right of (or on) the point.
|
|
[[ $mark && mark -ge point ]] && eval "$new_mark"'=$((mark+offset))'
|
|
|
|
# Advance point.
|
|
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 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).
|
|
|
|
# To prevent infinite recursion, we need to keep track
|
|
# of which aliases have been expanded. This function re-initializes the global associative array (used as a hash set)
|
|
declare -gA _expanded_aliases=()
|
|
|
|
# Calls the implementation and passes necessary arguments
|
|
_try_abbrev_internal "$READLINE_LINE" "$READLINE_POINT" "$READLINE_MARK" READLINE_LINE READLINE_POINT READLINE_MARK "$@"
|
|
|
|
# Don't pollute global namespace
|
|
declare -gA _expanded_aliases=()
|
|
}
|
|
|
|
bash_abbr () {
|
|
# IFS must be null so "${var[*]}" concatenates without space
|
|
declare -r IFS=
|
|
|
|
# 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 "${_bash_meta_chars_without_tab_and_nl[@]}"; do
|
|
bind_if -x '"\e\C-a'"$ch"'": _try_abbrev "'"$ch"'"'
|
|
bind_if '"'"$ch"'": "\e\C-a'"$ch"'"'
|
|
# bind_if '"'"$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),
|
|
# we need to implement recursive alias expansion on the entire line
|
|
# like bash does. Basically something like alias-expand-line but
|
|
# called as many times as possible before recursion would happen.
|
|
# This is pretty hard, and surely even harder to do efficiently,
|
|
# so it will probably never be implemented.
|
|
bind -x '"\e\C-an": _try_abbrev ""'
|
|
bind '"\e\C-j": accept-line'
|
|
bind '"\C-m": "\e\C-an\e^\e\C-j"'
|
|
bind '"\C-j": "\C-m"'
|
|
}
|
|
|
|
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.
|
|
# 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
|