Thursday, January 10, 2013

Easy Mac Bash Enhancements

Today I'm writing about two different things pertinent to enhancing Bash (on my mac):

  • Adding information to history
  • Making the prompt a little more likeable

As we all know, sometimes you enter a command in bash, but then find that you entered it a lot and need to look at the different ways you entered it. So there's the history; type "history" into the prompt, and you'll see all your commands (for some time, its limited in size by the HISTSIZE environment variable). 

But frequently, we might want more information than merely the command itself. Such as when it was entered, who entered it, on what terminal in what process. 

After searching around for a while, I found this script, and decided to enhance it:
log_history() {
# Detailed history log of shell activities, including time stamps, working directory etc.
#
# Based on 'hcmnt' by Dennis Williamson - 2009-06-05 - updated 2009-06-19
# (http://stackoverflow.com/questions/945288/saving-current-directory-to-bash-history)
#
# Add this function to your '~/.bashrc':
#
# 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 histentrycmdextra variable
#     h - add the hostname
#     y - add the terminal device (tty)
#     n - don't add the directory
#     t - add the from and to directories for cd commands
#     l - path to the log file (default = $HOME/.bash_log)
#     ext or a variable
#
# See bottom of this function for examples.
#
    # make sure this is not changed elsewhere in '.bashrc';
    # if it is, you have to update the reg-ex's below
    export HISTTIMEFORMAT="[%F %T] ~~~ "
    local script=$FUNCNAME
    local histentrycmd=
    local cwd=
    local extra=
    local text=
    local logfile="$HOME/.bash_log"
    local hostname=
    local histentry=
    local histleader=
    local datetimestamp=
    local histlinenum=
    local options=":hyntel:"
    local option=
    OPTIND=1
    local usage="Usage: $script [-h] [-y] [-n|-t] [-e] [text] [-l logfile]"
    local ExtraOpt=
    local NoneOpt=
    local ToOpt=
    local tty=
    local ip=
    # *** process options to set flags ***
    while getopts $options option
    do
        case $option in
            h ) hostname=$HOSTNAME;;
            y ) tty=$(tty);;
            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;;
            e ) ExtraOpt=1;;        # include histentrycmdextra
            l ) logfile=$OPTARG;;
            : ) 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[*]}}"
    # add the previous command(s) to the history file immediately
    # so that the history file is in sync across multiple shell sessions
    history -a
    # grab the most recent command from the command history
    histentry=$(history 1)
    # parse it out
    histleader=`expr "$histentry" : ' *\([0-9]*  \[[0-9]*-[0-9]*-[0-9]* [0-9]*:[0-9]*:[0-9]*\]\)'`
    histlinenum=`expr "$histleader" : ' *\([0-9]*  \)'`
    datetimestamp=`expr "$histleader" : '.*\(\[[0-9]*-[0-9]*-[0-9]* [0-9]*:[0-9]*:[0-9]*\]\)'`
    histentrycmd=${histentry#*~~~ }
    # protect against relogging previous command
    # if all that was actually entered by the user
    # was a (no-op) blank line
    #if [[ -z $__PREV_HISTLINE || -z $__PREV_HISTCMD ]]
    #then
        # new shell; initialize variables for next command
    #    export __PREV_HISTLINE=$histlinenum
    #    export __PREV_HISTCMD=$histentrycmd
    #    return
    #elif [[ $histlinenum == $__PREV_HISTLINE  && $histentrycmd == $__PREV_HISTCMD ]]
    #then
        # no new command was actually entered
    #    return
    #else
        # new command entered; store for next comparison
    #    export __PREV_HISTLINE=$histlinenum
    #    export __PREV_HISTCMD=$histentrycmd
    #fi
    if [[ -z $NoneOpt ]]            # are we adding the directory?
    then
        if [[ ${histentrycmd%% *} == "cd" || ${histentrycmd%% *} == "jd" ]]    # 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 [[ -z $OLDPWD ]]
            then
                OLDPWD="$HOME"
            fi
            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 && $histentrycmdextra ]]    # do we want a little something extra?
    then
        extra=$(eval "$histentrycmdextra")
    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 with some decoration)
    histentrycmd="${datetimestamp} ${text:+[$text] }${tty:+[$tty] } [PPID:$PPID] ${ip:+[$ip]} ${extra:+[$extra] }~~~ ${hostname:+$hostname:}$cwd ~~~ ${histentrycmd# * ~~~ }"
    # save the entry in a logfile
    echo "$histentrycmd" >> $logfile || echo "$script: file error." ; return 1
} # END FUNCTION _loghistory


As long as this is somewhere that will be available to every terminal, you'll be in business.

Now, one thing I don't like is silent failure. If a command fails, it should output something. Most of the time, this occurs, but sometimes it does not. If you check the result, you can see this explicitly, but who wants to type that all the time? So I came up with this:

PROMPT_COMMAND='PS1="\033[0;33m\d \@ || \u\n\`if [[ \$? = "0" ]]; then echo "\\[\\033[32m\\]"; else echo "\\[\\033[31m\\]"; fi\`[\w]\$ \[\033[0m\] "; echo -ne "\033]0;`hostname -s`:`pwd`; `log_history -h -y -t -e -l ~/.bash_log`\007 "'

Now your prompt will be shorter for your commands, show you the time of each previous command (if you should ever need that) and turn red when things fail.

No comments:

Post a Comment