Saving current directory to bash history
Asked Answered
W

9

69

I'd like to save the current directory where the each command was issued alongside the command in the history. In order not to mess things up, I was thinking about adding the current directory as a comment at the end of the line. An example might help:

$ cd /usr/local/wherever
$ grep timmy accounts.txt

I'd like bash to save the last command as:

grep timmy accounts.txt # /usr/local/wherever

The idea is that this way I could immediately see where I issued the command.

Whimsy answered 3/6, 2009 at 15:1 Comment(0)
K
54

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
Kasten answered 4/6, 2009 at 3:49 Comment(15)
Nasty... I thought about that but decided it's too complex. +1 for thatMighty
In the pattern where you remove the history number, why do you need to escape the second two spaces, but not the first?Whimsy
Sorry, those escapes aren't necessary - they're leftovers from some debugging. I'm going to edit the answer later today with that and some other minor refinements. Thanks for pointing it out - I had forgotten that one.Kasten
I removed the unnecessary escapes from the spaces and changed two of the strip instructions so the "cd" one makes a long match (%%) and the comment one (###) makes a short match (%). I will have a much better version to post in the next day or so.Kasten
Hi Dennis, I wrote a bash script that uses this code and thought you'd be interested: github.com/pconerly/lc-listcommands-bash I'd love to hear your feedback.Friend
I guess there is some critical reason that you strip off the history line number? I am kind of fond of that thing... Any way I can keep it?Naturopathy
@NickRoosevelt: history -s puts the line back in the history and the history line number gets added back. If it's not stripped, it shows up twice.Kasten
I get two lines in the result of history, one without the comment, one with the comment.Mooring
@Gauthier, I had the same problem and solved it including a history -d $(hpwd% *) before the history -s. Something like this PROMPT_COMMAND='hpwd=$(history 1); history -d ${hcmnt% *}; hpwd="${hpwd# *[0-9]* }"; if [[ ${hpwd%% *} == "cd" ]]; then cwd=$OLDPWD; else cwd=$PWD; fi; hpwd="${hpwd% ### *} ### $cwd"; history -s "$hpwd"'Careaga
@DennisWilliamson this is a true gem! Thank you very much for this.Stillhunt
@DennisWilliamson love this very much. Is it possible to add the directory output as its own 'pseudo' command in the history db prior to the actual command? The current solution is great except when using up, down, ! and other ways to navigate through the history - it forces me to backspace over the ### cwd to remove it prior to running a command from the history. Yes, it has no effect to run the command with the ### (that's why you suggest it!), but when needing to modify a previous command it requires navigation to prior to the ### or backspace over it to remove it.Chloris
@DennisWilliamson.. this works, but when I press the up arrow for editing the previous command and re-run.. those comments also show up.....Moonshine
@ePhrygian.. yes.. exactly, I also have the same issue.Moonshine
@ePhrygian: You should be able to change hpwd="${hpwd% ### *} ### $cwd"; to hpwd="cd \"$cwd\"; ${hpwd% ### *} ### $cwd"; which will add a cd command to each history entry so that the directory is changed before the rest of the command is executed when you choose a previous entry to re-execute - is that what you mean?Kasten
@stack0114106: See my comment to ePhrygian regarding cwd. Regarding the comments, that's what this command is designed to do. If you don't want the comments in active history but do want detailed logging in a separate file see my long or short function versions. Otherwise I'm unsure what you're asking about.Kasten
K
22

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
# https://mcmap.net/q/279797/-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

Kasten answered 6/6, 2009 at 22:17 Comment(8)
"HISTTIMEFORMAT still works in a subshell, however, since it gets unset automatically" - when the subshell exitsKasten
Thanks for this! One little thing is that nulling HISTTIMEFORMAT instead of unsetting it means that .bash_history gets some annoying timestamp lines. I changed the nulling line to if [[ ${!HISTTIMEFORMAT[@]} ]]; then unset HISTTIMEFORMAT; fi, using the funky test described at #874889Pamphlet
@Janus: I don't think the test is necessary. You can unset a variable that is not set without error.Kasten
On my machine, I had to replace' "${hcmnt# [0-9] }"' by '"${hcmnt#*[0-9]* }"' (without the leading space). The ip-address switch gives an error with my coreutils version 8.27. Still +1Fiume
This only works with history -a (which I really like) if you redirect the log to a different file: PROMPT_COMMAND=' history -a;hcmnt -eyl ~/.history_log;'Fiume
Since some version --ips seems becoming default. Sorry for the vague expression. I cannot find this option anywhere in coreutil source code revisions. If someone knows please tell me.Fructify
--ips exists in a debian patchFructify
I would recommend using trap 'hcmnt' DEBUG rather than PROMPT_COMMAND='hcmnt' to avoid history missing when i.e. the shell is killed.Fructify
L
16

You could install Advanced Shell History, an open source tool that writes your bash or zsh history to a sqlite database. This records things like the current working directory, the command exit code, command start and stop times, session start and stop times, tty, etc.

If you want to query the history database, you can write your own SQL queries, save them and make them available within the bundled ash_query tool. There are a few useful prepackaged queries, but since I know SQL pretty well, I usually just open the database and query interactively when I need to look for something.

One query I find very useful, though, is looking at the history of the current working directory. It helps me remember where I left off when I was working on something.

vagrant@precise32:~$ ash_query -q CWD
session
    when                   what
1
    2014-08-27 17:13:07    ls -la
    2014-08-27 17:13:09    cd .ash
    2014-08-27 17:16:27    ls
    2014-08-27 17:16:33    rm -rf advanced-shell-history/
    2014-08-27 17:16:35    ls
    2014-08-27 17:16:37    less postinstall.sh
    2014-08-27 17:16:57    sudo reboot -n

And the same history using the current working directory (and anything below it):

vagrant@precise32:~$ ash_query -q RCWD
session
    where
        when                   what
1
    /home/vagrant/advanced-shell-history
        2014-08-27 17:11:34    nano ~/.bashrc
        2014-08-27 17:12:54    source /usr/lib/advanced_shell_history/bash
        2014-08-27 17:12:57    source /usr/lib/advanced_shell_history/bash
        2014-08-27 17:13:05    cd
    /home/vagrant
        2014-08-27 17:13:07    ls -la
        2014-08-27 17:13:09    cd .ash
    /home/vagrant/.ash
        2014-08-27 17:13:10    ls
        2014-08-27 17:13:11    ls -l
        2014-08-27 17:13:16    sqlite3 history.db
        2014-08-27 17:13:43    ash_query
        2014-08-27 17:13:50    ash_query -Q
        2014-08-27 17:13:56    ash_query -q DEMO
        2014-08-27 17:14:39    ash_query -q ME
        2014-08-27 17:16:26    cd
    /home/vagrant
        2014-08-27 17:16:27    ls
        2014-08-27 17:16:33    rm -rf advanced-shell-history/
        2014-08-27 17:16:35    ls
        2014-08-27 17:16:37    less postinstall.sh
        2014-08-27 17:16:57    sudo reboot -n

FWIW - I'm the author and maintainer of the project.

Lynden answered 30/8, 2014 at 20:3 Comment(0)
K
7

hcmnts - short function version

Here is a short version in the form of a function. I've also posted a one-liner (the original) and a longer function 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. Read the entry for the one-liner for additional information on how this 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 hcmnts 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/hcmnts

Comment out the line that sets hcmntextra if you don't want the date and time (or you can change its format or use some other command besides date).

That's all there is to it.

#!/bin/bash
hcmnts() {
    # adds comments to bash history entries

    # the *S*hort version of hcmnt (which has many more features)

    # by Dennis Williamson
    # https://mcmap.net/q/279797/-saving-current-directory-to-bash-history
    # (thanks to Lajos Nagy for the idea)

    # 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 hcmnt
    local cwd
    local extra

    hcmnt=$(history 1)
    hcmnt="${hcmnt# *[0-9]*  }"

    if [[ ${hcmnt%% *} == "cd" ]]
    then
        cwd=$OLDPWD
    else
        cwd=$PWD
    fi

    extra=$(eval "$hcmntextra")

    hcmnt="${hcmnt% ### *}"
    hcmnt="$hcmnt ### ${extra:+$extra }$cwd"

    history -s "$hcmnt"
}
export hcmntextra='date +"%Y%m%d %R"'
export PROMPT_COMMAND='hcmnts'
Kasten answered 6/6, 2009 at 22:12 Comment(0)
H
2

For those who want this in zsh I've modified Jeet Sukumaran's implementation and percol to allow interactive keyword searching and extraction of either the command or path it was executed in. It's also possible to filter out duplicate commands and hide fields (date, command, path)

Hypermetropia answered 3/10, 2014 at 8:31 Comment(0)
L
2

Here's a one liner of what I use. Sticking it here because it's vastly simpler, and I have no problem with per-session history, I just also want to have a history with the working directory.

Also the one-liner above mucks with your user interface too much.

export PROMPT_COMMAND='if [ "$(id -u)" -ne 0 ]; then echo "$(date "+%Y-%m-%d.%H:%M:%S") $(pwd) $(history 1)" >> ~/.bash.log; fi'

Since my home dir is typically a cross-mounted gluster thingy, this has the side effect of being a history of everything I've ever done. Optionally add $(hostname) to the echo command above... depending on your working environment.

Even with 100k entries, grep is more than good enough. No need to sqlite log it. Just don't type passwords on the command line and you're good. Passwords are 90's tech anyway!

Also, for searching I tend to do this:

function hh() {
    grep "$1" ~/.bash.log
}
Lief answered 18/10, 2017 at 13:9 Comment(1)
good answer.. you need to add history -a to append it to the HISTFILEMoonshine
O
1

Gentleman this works better.. The only thing I can not figure out is how to make the script NOT log to syslog on login and log the last command in history. But works like a charm so far.

#!/bin/bash

trackerbash() {
    # adds comments to bash history entries

    # by Dennis Williamson
    # https://mcmap.net/q/279797/-saving-current-directory-to-bash-history
    # (thanks to Lajos Nagy for the idea)

    #Supper Enhanced by QXT


    # INSTALLATION: source this file in your .bashrc

    export HISTTIMEFORMAT=
#    export HISTTIMEFORMAT='%F   %T    '

    local hcmnt
    local cwd
    local extra
    local thistty
    local whoiam
    local sudouser
    local shelldate
    local TRACKIP
    local TRACKHOST


            thistty=`/usr/bin/tty|/bin/cut -f3-4 -d/`
            whoiam=`/usr/bin/whoami`
            sudouser=`last |grep $thistty |head -1 | awk '{ print $1 }' |cut -c 1-10`
            hcmnt=$(history 1)
            hcmnt="${hcmnt# *[0-9]*  }"
            cwd=`pwd`



            hcmnt="${hcmnt% ### *}"
            hcmnt=" $hcmnt ${extra:+$extra }"

            shelldate=`date +"%Y %b %d %R:%S"`
            TRACKHOST=`whoami | sed -r "s/.*\((.*)\).*/\\1/"`
            TRACKIP=`last |grep $thistty |head -1 | awk '{ print $3 }'`


            logger -p local1.notice -t bashtracker -i -- "$sudouser ${USER}: $thistty: $TRACKIP: $shelldate: $cwd : $hcmnt"
            history -w 

}
export PROMPT_COMMAND='trackerbash'

Olmstead answered 28/6, 2011 at 19:51 Comment(1)
Hi, the one before works for me but this one doesnt seem to work. Is it working for everybody?Prudi
M
1

Full disclosure: I'm the author of the FOSS-tool
shournal - A (file-) journal for your shell:
Using it's bash integration, the working directory of a command is also stored within shournal's sqlite-database and can be retrieved via

shournal --query -cmdcwd "$PWD"

Querying for sub-working-directories can be done with

shournal --query -cmdcwd -like "$PWD/%"
Marvel answered 23/7, 2019 at 23:0 Comment(0)
E
0

You could consider an independent project (I wrote it) that supports saving the path for each command: https://github.com/chrissound/MoscoviumOrange

Where you can add a hook into Bash to save each entry with:

$(jq -n --arg command "$1" --arg path "$PWD" '{"command":$command, "path":$path}' | "$(echo 'readlink -f $(which nc)' | nix run nixpkgs.netcat)" -N -U ~/.config/moscoviumOrange/monitor.soc &)
Evars answered 8/12, 2019 at 11:20 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.