tonix
tonix

Reputation: 6959

Is it possible to use alias expansion with shell's builtin commands in Git?

I have the following alias in my .gitconfig:

freq = !history | cut -c 8- | grep ^git | sort | uniq -c | sort -n -r | head -n 5

It should show the 5 git commands I use most. Taken from here: http://alblue.bandlem.com/2011/04/git-tip-of-week-aliases.html (I know the article is old)

When I run the command, it doesn't output anything:

$ git freq
$

But if I run history | cut -c 8- | grep ^git | sort | uniq -c | sort -n -r | head -n 5, output is given:

$ history | cut -c 8- | grep ^git | sort | uniq -c | sort -n -r | head -n
   5 git log
   5 git status
   5 git current
   5 git co master
   4 git co other-branch

I guess that this is because history is a shell builtin command, and if I change the freq alias, e.g. to:

freq = !history

I get:

$ git freq
error: cannot run history: No such file or directory
fatal: While expanding alias 'freq': 'history': No such file or directory
$

Is there a way to make it work?

Upvotes: 3

Views: 230

Answers (1)

Anthony Geoghegan
Anthony Geoghegan

Reputation: 12003

Cause

After reading the question, I also thought the problem was due to the fact that history is a shell builtin.

Debugging git

Git commands can be debugged by setting the GIT_TRACE environment variable to true (for more info, see the relevant section on Git Internals - Environment Variables from Scott Chacon’s Git Book).

$ GIT_TRACE=true git freq

10:14:11.901296 git.c:554               trace: exec: 'git-freq'
10:14:11.901296 run-command.c:341       trace: run_command: 'git-freq'
10:14:11.932496 run-command.c:341       trace: run_command: 'history | tail'
10:14:11.963697 run-command.c:192       trace: exec: '/bin/sh' '-c' 'history | tail' 'history | tail'

Checking the source shows that the external command is processed by the execv_shell_cmd function which calls sane_execvp, a more user friendly front-end to the execvp standard library function which – if the command name doesn’t contain a slash – searches each directory in the PATH for an executable with the same name as the command. Since history is a shell built-in, it won’t ever be found so it returns an error. See


Not being one to let things go, I later carried out further experiments using other shell built-ins and those experiments worked out so I figured it must be something else:

No history for non-interactive shells

Reading the Bash manual explains that Bash History Facilities are only used in interactive shells while shells started by an exec call are non-interactive. That’s why running the history builtin doesn’t print any output.


Solutions

Option 1: Turn on the history features in the non-interactive shell

Thanks to Gilles on Unix and Linux, it turns out that the history features in the non-interactive shell can be turned on by setting the HISTFILE shell variable and then running set -o history as can be seen by running:

bash -c "HISTFILE=~/.bash_history; set -o history; history"

Setting the following alias in .gitconfig will work:

freq = "!HISTFILE=$HOME/.bash_history; set -o history; history | cut -c 8- | grep ^git | sort | uniq -c | sort -n -r | head -n 5"

Note: If you’re using a shell other than Bash, you’ll need to specify the name of the file that your shell saves its command history to. If you are using Bash, it’s possible that the HISTFILE shell variable is set to something other than $HOME/.bash_history.

Also, shell variables such as HISTFILE should not be exported as an environment variable (available to git) which is why I didn’t use it here.

Option 2: Read from the history file

A simpler solution would be to read directly from the shell history file:

freq = !grep ^git $HOME/.bash_history | sort | uniq -c | sort -n -r | head -n 5

This would give the same results as running the history command in a new interactive shell since when a new shell starts, it has no commands in its history other than what it has read from the history file.

Also, there’s no need for the cut command when reading directly from the history file.

Option 3: Bash alias

The other option is to forget about using a Git alias and to simply use a Bash alias:

alias git-freq='history | cut -c 8- | grep ^git | sort | uniq -c | sort -n -r | head -n

Keeping the shell history file up to date

I thought I’d add the following as useful ancillary information. Note that it applies to an interactive Bash shell and it may or may not work for other modern shells.

I often run multiple shells on the same host at the same time and I don’t want saved history commands to be over-written when exiting a shell. Here are my .bashrc settings which ensure that the shell history file is written to after every command:

# Append history entries on logout - instead of over-writing the history file.
shopt -s histappend

# Don't store duplicate lines in the history.
# Ignore any commands that begin with a space.
HISTCONTROL=ignoredups:ignorespace

# Use a very large history file to store lots of commands.
# See http://mywiki.wooledge.org/BashFAQ/088
HISTFILESIZE=5000
# Keeping a large history requires system resources
HISTSIZE=3000

# Append to the history file after every command.
PROMPT_COMMAND="history -a"

Upvotes: 2

Related Questions