From 269f57e8c05fb39b57e82c4d34eff793b03d74b8 Mon Sep 17 00:00:00 2001 From: tsi Date: Mon, 20 Oct 2025 17:31:32 +0200 Subject: [PATCH] #1: Add initial version of bash-abbr --- bash-abbr | 145 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 bash-abbr diff --git a/bash-abbr b/bash-abbr new file mode 100644 index 0000000..effbf2f --- /dev/null +++ b/bash-abbr @@ -0,0 +1,145 @@ +#!/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. + +# WARNING: This plug-in creates a new readline binding: '\e\C-a' +# and overwrites the binding for '\C-m', ' ', '|', '&', ';', '(', ')', '<', '>' +# Therefore, it might interfere with other plug-ins or custom bindings. +# You may change the '\e\C-a' binding to whatever you want as it's only used +# as a hook inside another readline macro. The bindings '|', '&', ';', '(', ')', '<', '>' +# may also be dropped if you don't need them. + +# Regexes for (heuristically) determining whether we're abbreviating the first word. +# Construction put into functions to prevent global namespace pollution +# and increase readability. +_control_op () { + declare -r _newline=$'\n' \ + _bg_proc_or_and_and='[^&;]&&?' \ + _pipe_or_or_or='[^&|;]\|\|?' \ + _semicolon_or_case_operators='[^;|];;?&?' \ + _opening_paren='[^[:alnum:]]\(' \ + _closing_paren='\)' + 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" + _control_op+="|$_closing_paren" + printf %s "$_control_op" +} +_control_op="$(_control_op)" +unset -f _control_op + +_redir_or_assign_word () { + 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='[^[:space:]<>&;()|'\''"`$]+' \ + _double_quoted_string='\$?"([^"]|\\")*"' \ + _single_quoted_string="\\\$?'[^']*'" \ + _backtick_string='`([^`]|\\`)*`' \ + _simple_variable='\$[[:alpha:]_][[:alnum:]]*' \ + _advanced_variable='\$\{([[:alnum:]]+|[#-_$!*#])[^{}]*\}' \ + _deprecated_arithmetic_eval='\$\[[^][]*\]' \ + _cmd_subst_or_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_or_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))" + 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)" +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 line_until_point=${READLINE_LINE::READLINE_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:]] ]] && + + # 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##*[&;()|[:space:]]} && + + # 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. + # 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"} =~ ($_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 + + # Account for global aliases (strip "!" prefix and disable self-insertion) + [[ ${replacement::1} = "!" ]] && replacement=${replacement#!} && set -- '' + + # Calculate point and mark offset. + declare -ir offset=${#replacement}-${#word_at_point}+${#1} + + # Update line. + READLINE_LINE=${line_until_point%"$word_at_point"}$replacement${1-}${READLINE_LINE:READLINE_POINT} + else + declare -ir offset=${#1} + + # Self-insert by default + READLINE_LINE=${line_until_point}${1-}${READLINE_LINE:READLINE_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)) + + # Advance point. + READLINE_POINT=$((READLINE_POINT+offset)) + + # NB: READLINE_ARGUMENT is deliberately ignored. +} + +# From the bash man page: +# metacharacter +# A character that, when unquoted, separates words. One of the following: +# | & ; ( ) < > space tab newline +for ch in ' ' '|' '&' ';' '(' ')' '<' '>'; do + bind -x '"'"$ch"'": _try_abbrev "'"$ch"'"' # Note: double quote should be escaped, if added to the list of metacharacters +done + +# Newline must be treated specially by creating a new binding which is called +# as a readline macro when pressing enter (C-m). After that is finished, the macro +# continues with a regular C-j, which accepts (=executes) the line as you would expect. +bind -x '"\e\C-a": _try_abbrev ""' +bind '"\C-m": "\e\C-a\e^\C-j"'