Transparency
Transparency

Reputation: 365

Print executed alias in zsh or bash

The scenario is currently, I have defined some aliases in .zshrc like

alias gco='git checkout'
alias cdp='cd ..'

and lots like that. My question is How to print out the command each time I typed an alias and press enter?

ex:

$> gco master
> Command: git checkout master
> Git process ...

something like that, if the solution also works in bash would be better! Thanks!

Upvotes: 15

Views: 5145

Answers (2)

gmbiz
gmbiz

Reputation: 23

  1. Create a Directory for Scripts:

    Run the following command to create a directory for your scripts:

    mkdir -p $HOME/.zsh_scripts
    
  2. Ensure Node.js is Installed:

    Make sure Node.js is installed on your system. You can check this by running:

    node -v
    

    If Node.js is not installed, you can install it using a package manager like nvm, brew, or directly from the Node.js website.

  3. Create expand_aliases.sh:

    Inside the .zsh_scripts directory, create a file named expand_aliases.sh with the following content:

    # Fetch all shell aliases and store them in a variable
    _aliases="$(alias)"
    
    # Function to expand the command line by passing it to the Node.js script
    expand_command_line() {
      local cmd="$1"
      shift
      local args="$@"
    
      # Pass aliases and command line to the Node.js script
      node $HOME/.zsh_scripts/expand_aliases.js "$cmd" "$args" "$_aliases"
    }
    
  4. Create expand_aliases.js:

    In the same directory, create a file named expand_aliases.js with the following content:

    #!/usr/bin/env node
    
    // Function to parse aliases from the shell output into a dict object
    function parseAliases(aliasesOutput) {
      const aliasMap = {};
      const aliasLines = aliasesOutput.split("\n");
    
      aliasLines.forEach((line) => {
        const match = line.match(/^([^=:#]+)=(['"]?)(.+)\2$/);
        if (match) {
          let [, aliasName, , aliasValue] = match;
          aliasMap[aliasName.trim()] = aliasValue.trim();
        }
      });
    
      return aliasMap;
    }
    
    // Function to check if a command has an alias and return the expanded alias
    function expandAlias(cmd, aliasMap) {
      return aliasMap[cmd] || cmd;
    }
    
    // Function to expand the first argument (command) of a command line if it has an alias
    function expandCommandLine(commandLine, aliasMap) {
      // RegEx pattern to match potential command separators
      const separatorRegex = /(\s*&&\s*|\s*&\s*|\s*\|\|\s*|\s*\|\s*|\s*;\s*)/;
    
      // Split the command line by separators, keeping the separators in the result
      const parts = commandLine.split(separatorRegex);
    
      const expandedParts = parts.map((part) => {
        // If the part is a separator, return it as is
        if (separatorRegex.test(part)) {
          return part;
        }
    
        // Otherwise, treat it as a command and expand it
        const command = part.trim();
        const cmdParts = command.split(" ");
        const cmd = cmdParts[0];
        const args = cmdParts.slice(1).join(" ");
    
        const expandedCmd = expandAlias(cmd, aliasMap);
    
        if (expandedCmd === cmd) return command; // No alias found,   return original command
    
        let alias = expandedCmd.trim();
    
        if (alias.startsWith("'") && alias.endsWith("'")) {
          // Remove leading and trailing single quotes if they still exist after regex matching
          alias = alias.slice(1, -1);
        }
        return `${alias} ${args}`;
      });
    
      return expandedParts.join("");
    }
    
    // Main function to expand the command passed from zsh
    function main() {
      // The last argument is the aliases from the shell, and the first are the command and arguments
      const args = process.argv.slice(2);
      const aliases = args.pop(); // The last argument is the aliases string
    
      const commandLine = args.join(" ");
      if (!commandLine) {
        console.error("No command provided.");
        process.exit(1);
      }
    
      // Parse the aliases passed from Bash
      const aliasMap = parseAliases(aliases);
    
      // Expand the command line using the aliases
      const expanded = expandCommandLine(commandLine, aliasMap);
    
      // Output the expanded command to be executed in the shell
      if (expanded) console.log(">", expanded);
    }
    
    main();
    
  5. Update .zshrc:

    Add the following lines to your .zshrc file to source the script and set up the preexec function:

    source $HOME/.zsh_scripts/expand_aliases.sh
    preexec() {
      expand_command_line $1
    }
    
  6. Reload Your Shell:

    After making these changes, reload your shell configuration by running:

    source ~/.zshrc
    

This setup will allow you to automatically expand shell aliases using a Node.js script whenever you execute a command in your terminal.

Example outputs

❯ ls
> eza-wrapper.sh 
❯ ls -la
> eza-wrapper.sh -la 

❯ noalias
noalias: command not found

❯ grep
> grep --color=auto --exclude-dir={.bzr,CVS,.git,.hg,.svn,.idea,.tox,.venv,venv} 
Usage: grep [OPTION]... PATTERNS [FILE]...
Try 'grep --help' for more information.

❯ , apt update
> sudo apt update

Thanks to @simont for his inspiring answer.

Upvotes: 0

simont
simont

Reputation: 72547

This is a neat question. We can do it by defining a couple of functions to expand out the aliases, and then use a preexec hook to run the functions before we execute them.

I've taken the answer from here.


1. Evaluate all the aliases

_aliases="$(alias -Lr 2>/dev/null || alias)"

alias_for() {
  [[ $1 =~ '[[:punct:]]' ]] && return
  local found="$( echo "$_aliases" | sed -nE "/^alias ${1}='?(.+)/s//\\1/p" )"
  [[ -n $found ]] && echo "${found%\'}"
}

First, store all aliases in a variable. alias -r prints all the regular aliases (not global or suffix), and alias -L prints them "in a manner suitable for use in startup scripts". The alias_for() function does some cleaning, removing quotes and putting alias in front of the lines. When we do echo ${_aliases}, we get something like this:

alias history='fc -l 1'
alias ls='ls -F -G'
alias lsdf='ls -1l ~/.*(@)'
alias mv='mv -v'

Compare this to the output of alias:

history='fc -l 1'
ls='ls -F -G'
lsdf='ls -1l ~/.*(@)'
mv='mv -v'

2. Function to check if there was an alias entered.

If there was an alias entered, we can now detect it, and thus print it:

expand_command_line() {
  [[ $# -eq 0 ]] && return         # If there's no input, return. Else... 
  local found_alias="$(alias_for $1)"    # Check if there's an alias for the comand.
  if [[ -n $found_alias ]]; then         # If there was
    echo ${found_alias}                  # Print it. 
  fi
}

3. Getting this to run every time a command is entered

The preexec function is perfect for this. It's a function that is:

Executed just after a command has been read and is about to be executed. If the history mechanism is active (and the line was not discarded from the history buffer), the string that the user typed is passed as the first argument, otherwise it is an empty string. The actual command that will be executed (including expanded aliases) is passed in two different forms: the second argument is a single-line, size-limited version of the command (with things like function bodies elided); the third argument contains the full text that is being executed.

from the zsh Manual, chapter 9.

Note, we could probably just use the preeexec function to display what's being run.

To add our function to the preexec, we use a hook using this example:

autoload -U add-zsh-hook        # Load the zsh hook module. 
add-zsh-hook preexec expand_command_line      # Adds the hook 

To remove the hook later, we can use:

# add-zsh-hook -d preexec expand_command_line # Remove it for this hook.

My shell

This is what my shell looks like when I run it:

$ 1
cd -
$ rake
bundle exec rake
^C
$ chmod
usage:  chmod [-fhv] [-R [-H | -L | -P]] [-a | +a | =a  [i][# [ n]]] mode|entry file ...
    chmod [-fhv] [-R [-H | -L | -P]] [-E | -C | -N | -i | -I] file ...
$ git lg1
fatal: Not a git repository (or any of the parent directories): .git

Bugs (or 'features')

As we can see from my shell example, when a command that is not aliased is run (like chmod), the full command is not displayed. When an aliased command (like 1 or rake) is run, the full command is displayed.

When a git alias is run (git lg1, for example), the git alias is not expanded. If you look at my first link, the full example there does use git alias expansion - you should take that and modify if git aliases are vital to you.

Upvotes: 17

Related Questions