Reputation: 365
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
Reputation: 23
Create a Directory for Scripts:
Run the following command to create a directory for your scripts:
mkdir -p $HOME/.zsh_scripts
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.
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"
}
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();
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
}
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.
❯ 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
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.
_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'
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
}
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.
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
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