Search code examples
bashechopipelinealias

Can I echo the alias text every time I run them without running into pipeline issues?


For learning purposes, I would like to automatically print out the aliases I use each time I run them. My hope is that it will help me learn what some of the useful flags involved in the commands do, and also keep my memory fresh for pieces in aliases involving pipelines.

I know I can just run type alias-name each time I'd like to remember, but I want this to happen automatically.

So far I have this script which I source from ~/.bashrc

declare -a ALIASES
while read -r; do
   ALIASES+=("$REPLY")
done < <(alias)

for i in "${ALIASES[@]}"
do
    ALIAS_AND_NAME=$(awk -F '=' '{print $1}' <<< "$i")  # puts the keyword alias and the alias itself into the variable
    NAME_ONLY=$(awk '{print $2}' <<< "$ALIAS_AND_NAME") # removes the keyword alias
    
    COMMAND_AFTER_EQUALS=$(sed -e 's/^[^=]*=//g' <<< "$i")
    # regex: matches up until the '=', then also matches '=', and replaces the match with nothing  (removes the alias piece)
    ALIAS_COMMAND=$(sed -e "s/^'//" -e "s/'$//" <<< "$COMMAND_AFTER_EQUALS")
    # remove single quotes

    # now remake the alias, first echoing what the alias is, then running the command
    alias $NAME_ONLY="echo ~~~alias $NAME_ONLY=\'$ALIAS_COMMAND\'~~~ 1>&2; $ALIAS_COMMAND"
done

As far as I can tell, this works just as I'd like whenever an alias is used standalone. The issue I'm running into now is when aliased commands like

alias grep='grep --color=auto'

are used in pipelines. I believe now the input is going from the previous pipeline command into the echo statement in the new alias. Is there a way to avoid this?


Solution

  • You can't use an alias on its own for what you're trying to do -- but it can be pulled off if you pair that alias with a function:

    write_alias_warning_and_execute() {
      local orig_alias_name orig_alias_str orig_alias_q args_q
      orig_alias_name=$1; shift
      orig_alias_str=$1; shift
      printf -v args_q '%q ' "$@"
      printf -v orig_alias_q '%q' "$orig_alias_str"
      echo "alias $orig_alias_name=$orig_alias_q" >&2
      eval "command $orig_alias_str $args_q"
    }
    
    for alias_name in "${!BASH_ALIASES[@]}"; do
      alias_content=${BASH_ALIASES[$alias_name]}
      if [[ $alias_content = *write_alias_warning_and_execute* ]]; then
        echo "alias $alias_name is already wrapped; skipping" >&2
      fi
    
      printf -v new_alias 'write_alias_warning_and_execute %q %q' "$alias_name" "$alias_content"
      BASH_ALIASES[$alias_name]=$new_alias
    done
    

    Some notes, stylistic and otherwise:

    • Note that we're using lowercase names for variables the script above defines itself. That's because all-caps names are in a namespace used for variables meaningful to the shell and operating system; names outside that space are reserved for "application use"; your script, in this context, is an application.
    • There's no need to parse the output of the alias command, because (in the reserved meaningful-to-the-shell-itself all-caps namespace) bash exposes an associative array, BASH_ALIASES, containing the names and content of all defined aliases.
    • Using printf %q "$var" (for compatibility: bash 5.0+ allows the newer and better ${var@Q}) ensures that we're escaping our strings in a way that evaluates back to their original text, thereby avoiding injection attacks and data-centric bugs as are otherwise associated with eval.
    • The command builtin is used to prevent recursion, ensuring that our eval doesn't re-invoke the alias itself.
    • Consider making a habit of using functions instead of aliases in the first place. That is to say, instead of alias grep='grep --color', one can run grep() { command grep --color "$@"; }. In that context, adding additional commands to the function can look like grep() { echo "adding --color to grep" >&2; command grep --color "$@"; }