Saving current directory to bash history
One-liner version
Here is a one-liner version. It's the original. I've also posted a short function version and a long function version with several added features. I like the function versions because they won't clobber other variables in your environment and they're much more readable than the one-liner. This post has some information on how they all work which may not be duplicated in the others.
Add the following to your ~/.bashrc
file:
export PROMPT_COMMAND='hpwd=$(history 1); hpwd="${hpwd# *[0-9]* }"; if [[ ${hpwd%% *} == "cd" ]]; then cwd=$OLDPWD; else cwd=$PWD; fi; hpwd="${hpwd% ### *} ### $cwd"; history -s "$hpwd"'
This makes a history entry that looks like:
rm subdir/file ### /some/dir
I use ###
as a comment delimiter to set it apart from comments that the user might type and to reduce the chance of collisions when stripping old path comments that would otherwise accumulate if you press enter on a blank command line. Unfortunately, the side affect is that a command like echo " ### "
gets mangled, although that should be fairly rare.
Some people will find the fact that I reuse the same variable name to be unpleasant. Ordinarily I wouldn't, but here I'm trying to minimize the footprint. It's easily changed in any case.
It blindly assumes that you aren't using HISTTIMEFORMAT
or modifying the history in some other way. It would be easy to add a date
command to the comment in lieu of the HISTTIMEFORMAT
feature. However, if you need to use it for some reason, it still works in a subshell since it gets unset automatically:
$ htf="%Y-%m-%d %R " # save it for re-use
$ (HISTTIMEFORMAT=$htf; history 20)|grep 11:25
There are a couple of very small problems with it. One is if you use the history
command like this, for example:
$ history 3
echo "hello world" ### /home/dennis
ls -l /tmp/file ### /home/dennis
history 3
The result will not show the comment on the history
command itself, even though you'll see it if you press up-arrow or issue another history
command.
The other is that commands with embedded newlines leave an uncommented copy in the history in addition to the commented copy.
There may be other problems that show up. Let me know if you find any.
How it works
Bash executes a command contained in the PROMPT_COMMAND
variable each time the PS1
primary prompt is issued. This little script takes advantage of that to grab the last command in the history, add a comment to it and save it back.
Here it is split apart with comments:
hpwd=$(history 1) # grab the most recent command
hpwd="${hpwd# *[0-9]* }" # strip off the history line number
if [[ ${hpwd%% *} == "cd" ]] # if it's a cd command, we want the old directory
then # so the comment matches other commands "where *were* you when this was done?"
cwd=$OLDPWD
else
cwd=$PWD
fi
hpwd="${hpwd% ### *} ### $cwd" # strip off the old ### comment if there was one so they
# don't accumulate, then build the comment
history -s "$hpwd" # replace the most recent command with itself plus the comment
hcmnt - long function version
Here is a long version in the form of a function. It's a monster, but it adds several useful features. I've also posted a one-liner (the original) and a shorter function. I like the function versions because they won't clobber other variables in your environment and they're much more readable than the one-liner. Read the entry for the one-liner and the commments in the function below for additional information on how it works and some limitations. I've posted each version in its own answer in order to keep things more organized.
To use this one, save it in a file called hcmnt
in a location like /usr/local/bin
(you can chmod +x
it if you want) then source it in your ~/.bashrc
like this:
source /usr/local/bin/hcmnt
export hcmntextra='date "+%Y%m%d %R"'
export PROMPT_COMMAND='hcmnt'
Don't edit the function's file where PROMPT_COMMAND
or hcmntextra
are set. Leave them as is so they remain as defaults. Include them in your .bashrc
as shown above and edit them there to set options for hcmnt
or to change or unset hcmntextra
. Unlike the short function, with this one you must both have the hcmntextra
variable set and use the -e
option to make that feature work.
You can add several options which are documented (with a couple of examples) in the comments in the function. One notable feature is to have the history entry with appended comment logged to a file and leave the actual history untouched. In order to use this function, just
add the -l
filename option like so:
export PROMPT_COMMAND="hcmnt -l ~/histlog"
You can use any combination of options, except that -n
and -t
are mutually exclusive.
#!/bin/bash
hcmnt() {
# adds comments to bash history entries (or logs them)
# by Dennis Williamson - 2009-06-05 - updated 2009-06-19
# http://stackoverflow.com/questions/945288/saving-current-directory-to-bash-history
# (thanks to Lajos Nagy for the idea)
# the comments can include the directory
# that was current when the command was issued
# plus optionally, the date or other information
# set the bash variable PROMPT_COMMAND to the name
# of this function and include these options:
# -e - add the output of an extra command contained in the hcmntextra variable
# -i - add ip address of terminal that you are logged in *from*
# if you're using screen, the screen number is shown
# if you're directly logged in, the tty number or X display number is shown
# -l - log the entry rather than replacing it in the history
# -n - don't add the directory
# -t - add the from and to directories for cd commands
# -y - add the terminal device (tty)
# text or a variable
# Example result for PROMPT_COMMAND='hcmnt -et $LOGNAME'
# when hcmntextra='date "+%Y%m%d %R"'
# cd /usr/bin ### mike 20090605 14:34 /home/mike -> /usr/bin
# Example for PROMPT_COMMAND='hcmnt'
# cd /usr/bin ### /home/mike
# Example for detailed logging:
# when hcmntextra='date "+%Y%m%d %R"'
# and PROMPT_COMMAND='hcmnt -eityl ~/.hcmnt.log $LOGNAME@$HOSTNAME'
# $ tail -1 ~/.hcmnt.log
# cd /var/log ### dave@hammerhead /dev/pts/3 192.168.1.1 20090617 16:12 /etc -> /var/log
# INSTALLATION: source this file in your .bashrc
# will not work if HISTTIMEFORMAT is used - use hcmntextra instead
export HISTTIMEFORMAT=
# HISTTIMEFORMAT still works in a subshell, however, since it gets unset automatically:
# $ htf="%Y-%m-%d %R " # save it for re-use
# $ (HISTTIMEFORMAT=$htf; history 20)|grep 11:25
local script=$FUNCNAME
local hcmnt=
local cwd=
local extra=
local text=
local logfile=
local options=":eil:nty"
local option=
OPTIND=1
local usage="Usage: $script [-e] [-i] [-l logfile] [-n|-t] [-y] [text]"
local newline=$'\n' # used in workaround for bash history newline bug
local histline= # used in workaround for bash history newline bug
local ExtraOpt=
local LogOpt=
local NoneOpt=
local ToOpt=
local tty=
local ip=
# *** process options to set flags ***
while getopts $options option
do
case $option in
e ) ExtraOpt=1;; # include hcmntextra
i ) ip="$(who --ips -m)" # include the terminal's ip address
ip=($ip)
ip="${ip[4]}"
if [[ -z $ip ]]
then
ip=$(tty)
fi;;
l ) LogOpt=1 # log the entry
logfile=$OPTARG;;
n ) if [[ $ToOpt ]]
then
echo "$script: can't include both -n and -t."
echo $usage
return 1
else
NoneOpt=1 # don't include path
fi;;
t ) if [[ $NoneOpt ]]
then
echo "$script: can't include both -n and -t."
echo $usage
return 1
else
ToOpt=1 # cd shows "from -> to"
fi;;
y ) tty=$(tty);;
: ) echo "$script: missing filename: -$OPTARG."
echo $usage
return 1;;
* ) echo "$script: invalid option: -$OPTARG."
echo $usage
return 1;;
esac
done
text=($@) # arguments after the options are saved to add to the comment
text="${text[*]:$OPTIND - 1:${#text[*]}}"
# *** process the history entry ***
hcmnt=$(history 1) # grab the most recent command
# save history line number for workaround for bash history newline bug
histline="${hcmnt% *}"
hcmnt="${hcmnt# *[0-9]* }" # strip off the history line number
if [[ -z $NoneOpt ]] # are we adding the directory?
then
if [[ ${hcmnt%% *} == "cd" ]] # if it's a cd command, we want the old directory
then # so the comment matches other commands "where *were* you when this was done?"
if [[ $ToOpt ]]
then
cwd="$OLDPWD -> $PWD" # show "from -> to" for cd
else
cwd=$OLDPWD # just show "from"
fi
else
cwd=$PWD # it's not a cd, so just show where we are
fi
fi
if [[ $ExtraOpt && $hcmntextra ]] # do we want a little something extra?
then
extra=$(eval "$hcmntextra")
fi
# strip off the old ### comment if there was one so they don't accumulate
# then build the string (if text or extra aren't empty, add them plus a space)
hcmnt="${hcmnt% ### *} ### ${text:+$text }${tty:+$tty }${ip:+$ip }${extra:+$extra }$cwd"
if [[ $LogOpt ]]
then
# save the entry in a logfile
echo "$hcmnt" >> $logfile || echo "$script: file error." ; return 1
else
# workaround for bash history newline bug
if [[ $hcmnt != ${hcmnt/$newline/} ]] # if there a newline in the command
then
history -d $histline # then delete the current command so it's not duplicated
fi
# replace the history entry
history -s "$hcmnt"
fi
} # END FUNCTION hcmnt
# set a default (must use -e option to include it)
export hcmntextra='date "+%Y%m%d %R"' # you must be really careful to get the quoting right
# start using it
export PROMPT_COMMAND='hcmnt'
update 2009-06-19: Added options useful for logging (ip and tty), a workaround for the duplicate entry problem, removed extraneous null assignments