Ron Barry/foodini.org
         Latest Entry
prev:20080806           whole blog            in context             next:20080821
%20080816
Just Bash My Head In
If you're actually going to try this, you're going to need to be capable of
doing some customization.  I have done everything I can to make this thing work
for every bash prompt out there, but that dream has pushed me through a dozen
or more (I'm not kidding!) iterations.  You are about to see a serious hack.  If
you find yourself thinking, "why didn't he do it this way?  It would have been
so much simpler."  It's because I did try it that way and one configuration or
another puked on it.

UPDATE 20130214:  As a rule, I don't make major content edits to blog entries
                  after they've been up for a while.  I'm making an exception
                  in this case, because the content is technical, rather than
                  political.  If you want to see the "original" text, I have
                  stashed it here.

I spend a lot of time in bash.  At first blush, it's no more than a
command-line interface, and therefore off the radar of most users who see such
things as an anachronism they'd rather forget, but I do nearly everything in
bash.  Until 2009, I read my email in a terminal, which is why I eschew
marked-up email.  I navigate directories, edit files, engage in my daily source
code checkout and delivery, search for files, search inside files, reboot
my machine, and even occasionally browse web pages from the command line.  bash
is the heart of my digital existence.

The trouble is that I tend to have a plethora ("Do you know what..." never
mind) of bash windows open at a time.  On one machine today, I had one terminal
running a web server, another fiddling with my database, a third, fourth, and
fifth editing different files, while a sixth was grinding away through my
machine trying to record the names of every file on the system.

When I do this, I end up with lots of windows in my start bar (or whatever)
named simply, "bash."  This is fine if I only have one of them, but its agony
when there are many, many more.  I have three computers, each with their own
monitor, under the simultaneous command of one keyboard/mouse pair and I still
feel the need for more.  Each of those windows has several bash terminals open,
so it is not at all unusual to have 15 instances of "bash" going at the same
time.

When a terminal runs in a window, we refer to the title of the window as the
status line.  From within the terminal, within bash, you can control the string
that is written in this space.  Windows and OS X will quickly update the
start bar (or whatever) to associate this new string with the window, making it
easy to choose the correct bash instance, assuming that you've titled them in
ways that make them easy to distinguish.

If you echo "\[\e]0;SOME STRING HERE\a\]" in bash, the status bar takes up the
value, "SOME STRING HERE."  I started by simply setting the hostname into this
space, but when you have 6 local terminals, that's not much help.  My first
change was to have the PS1 environment variable, which is used to generate your
prompt, print the hostname and the current working directory:

  export PS1="\[\e]0;\$HOSTNAME:\$CWD\a\]\$CWD> "

This sets the status bar and prints the current working directory as the
prompt.  However, I wanted the currently-running command in there, as well.

Initially, I went down a rabbit hole of using supporting perl scripts.  It
worked, but had issues of its own.  I have now reduced the whole thing down to
a few lines that go in your .bashrc.  My total list of requirements were:

  * The command prompt, and the basic state of the status line should be:
    $HOSTNAME: $CWD
    $CWD must be stripped down to it's last 37 (feel free to choose your own
    value) characters, and must indicate that it has been truncated by
    prepending an elipsis.
  * When a command is typed, the status line must update to include the
    currently-running command.
  * When a command completes execution, the status line must remove the
    command that was executing and revert back to its basic state.
  * The system must prevent the status line from being updated if the terminal
    doesn't support it.
  * To make it easier for me to find the previous command prompt, and to make
    it easy to visually distinguish hosts, the hostname in the command prompt
    must be colored.  (And I do set a different color on each machine.)

To have the status include the user's command, I took advantage of the bash
debug trap.  If you want to have something executed every time a user enters
a command (or an empty command) you can use the trap like this:

  trap 'echo Please do not press that button again' DEBUG

All I have to do is get the user's command from their history and echo it out
in a way that it will be sent to the status line.

There's a warning at the top of this blog post that you should read now before
you start asking stupid questions like, "Why didn't you just use the HOSTNAME
environment variable via @ENV?"  Simple:  Because that doesn't work for all the
systems I tried it on.


    #Lots of colors for you to use.  You can delete anything you're not using.
    ESC_BLACK='\\[\\033[0;30m\\]'
    ESC_RED='\\[\\033[0;31m\\]'
    ESC_GREEN='\\[\\033[0;32m\\]'
    ESC_BROWN='\\[\\033[0;33m\\]'
    ESC_BLUE='\\[\\033[0;34m\\]'
    ESC_PURPLE='\\[\\033[0;35m\\]'
    ESC_CYAN='\\[\\033[0;36m\\]'
    ESC_LT_GRAY='\\[\\033[0;37m\\]'
    ESC_DK_GRAY='\\[\\033[1;30m\\]'
    ESC_LT_RED='\\[\\033[1;31m\\]'
    ESC_LT_GREEN='\\[\\033[1;32m\\]'
    ESC_YELLOW='\\[\\033[1;33m\\]'
    ESC_LT_BLUE='\\[\\033[1;34m\\]'
    ESC_LT_PURPLE='\\[\\033[1;35m\\]'
    ESC_LT_CYAN='\\[\\033[1;36m\\]'
    ESC_WHITE='\\[\\033[1;37m\\]'
    ESC_UNDERLINE='\\[\\033[4m\\]'
    ESC_NO_UNDERLINE='\\[\\033[24m\\]'
    ESC_END='\\[\\033[0m\\]'

    PC_BLACK='\[\033[0;30m\]'
    PC_RED='\[\033[0;31m\]'
    PC_GREEN='\[\033[0;32m\]'
    PC_BROWN='\[\033[0;33m\]'
    PC_BLUE='\[\033[0;34m\]'
    PC_PURPLE='\[\033[0;35m\]'
    PC_CYAN='\[\033[0;36m\]'
    PC_LT_GRAY='\[\033[0;37m\]'
    PC_DK_GRAY='\[\033[1;30m\]'
    PC_LT_RED='\[\033[1;31m\]'
    PC_LT_GREEN='\[\033[1;32m\]'
    PC_YELLOW='\[\033[1;33m\]'
    PC_LT_BLUE='\[\033[1;34m\]'
    PC_LT_PURPLE='\[\033[1;35m\]'
    PC_LT_CYAN='\[\033[1;36m\]'
    PC_WHITE='\[\033[1;37m\]'
    PC_UNDERLINE='\[\033[4m\]'
    PC_NO_UNDERLINE='\[\033[24m\]'
    PC_END='\[\033[0m\]'

    #If you put slashes in here, make SURE they are escaped with a backslash!!!
    replace_substrings() {
        if [ $BASE == $PWD ]; then
            BACKREF=$1
            CRUFT=$2
            SUB=$3
            SUB_BNW=$4
            BASE=`echo $PWD|sed -e "s/^$CRUFT.*/$SUB/g"`
            BASE_BNW=`echo $PWD|sed -e "s/^$CRUFT.*/$SUB_BNW/g"`
            TRIM=`echo $PWD|sed -e "s/^$CRUFT\(.*\)/$BACKREF/g"`
        fi
    }

    gen_prompt_text() {
        LAST_RETURN_VAL=$?
        #echo $LAST_RETURN_VAL >> $LOGFILE
        #date >> "$LOGFILE"
        #echo >> "$LOGFILE"

        if [ $LAST_RETURN_VAL == 0 ]; then
            PC_BASE_COL=$PC_END
            ESC_BASE_COL=$ESC_END
        else
            PC_BASE_COL=$PC_LT_RED
            ESC_BASE_COL=$ESC_LT_RED
        fi

        HOST_SHORT=`hostname | cut -f1 -d.`

        BASE=$PWD
        TRIM=$PWD

        #replace_substrings takes;
        # * This is the worst bit, and I'll try to figure it out later.  You
        #   have to provide the regular expression backreference ONE BEYOND the
        #   last one you use.  If you don't use any (or you don't know what I'm
        #   talking about, use "\1".  If you use one backreference in the
        #   second argument, use "\2".  Etc.  It's used to extract the last
        #   portion of your current working directory.
        # * a regular expression match for the bit you want to chop off at the
        #   beginning
        # * the string to replace the regex with in colorized prompts. You don't
        #   have to use color.  If you don't this argument and the next will be
        #   the same.
        # * the string to replace the regex with in colorless prompts.
        replace_substrings "\1" ".home.foodini" \
                           "${ESC_LT_GREEN}~${ESC_BASE_COL}" "~"
        replace_substrings "\2" ".home.foodini.blog.([^\/]*)" \
                           "${ESC_LT_GREEN}{\\1}${ESC_BASE_COL} "{\\1}"

        # If none of the above clauses have matched, TRIM is all we're going to
        # display after the hostname, so clear BASE and BASE_BNW
        if [ $BASE == $PWD ]; then
          BASE=""
          BASE_BNW=""
        fi

        if [ ${#TRIM} -gt 37 ]; then
            #TRIM=`echo $TRIM|sed -e "s/^.*\(.\{37\}\)$/\\1/"`
            #TRIM="...$TRIM"
            TRIM=`echo $TRIM|sed -e "s/^.*\(.\{37\}\)$/...\\1/"`
        fi

        PROMPT_COL="$PC_BASE_COL$TRIM>$PC_END "


        export PROMPT_BNW="$HOST_SHORT: $BASE_BNW$TRIM> "
        export PROMPT_COL="$PC_LT_GRAY$HOST_SHORT$PC_BASE_COL: $BASE$PROMPT_COL"
        if [ $SUPPORTS_STATUS_LINE == "true" ]; then
            export PS1="\[\e]0;$PROMPT_BNW\a\]$PROMPT_COL"
        else
            export PS1=$PROMPT_COL
        fi
    }

    export PROMPT_COMMAND='gen_prompt_text'

    case $TERM in
      xterm|screen)
        export SUPPORTS_STATUS_LINE="true"
        ;;
      *)
        export SUPPORTS_STATUS_LINE="false"
        ;;
    esac

    #TODO: The trap fires a number of times in the execution of a command.
    #      How can I trim it down so the trap is turned off until the end of
    #      next prompt generation phase AND use $BASH_COMMAND instead of this
    #      ugly history | sed thing?  ($BASH_COMMAND, during the execution of
    #      gen_prompt_text is "gen_prompt_text"
    if [ $SUPPORTS_STATUS_LINE == "true" ]; then
        TRAPCMD1='if [ "$BASH_COMMAND" == "gen_prompt_text" ]; then CMD=""; '
        TRAPCMD2='else CMD=`history 1|sed -e "s/^[ ]*[0-9]*[ ]*//g"`;fi;echo '
        TRAPCMD3='-en "\e]0;$PROMPT_BNW $CMD\007"'
        trap "$TRAPCMD1$TRAPCMD2$TRAPCMD3" DEBUG
    fi



So now, I have a bazillion windows going and they say things like:

  castro: /home/ronb blog
  Ron-D630: /C/ronb/rails/depot script/server
  Ron-D630: /C/ronb/rails/depot mysql -u ron -p
  Ron-D630: /C/ronb/rails/depot find . > /C/ronb/system.map
  Ron-D630: /C/ronb/rails/depot vi app/views/cart.html.erb
  Ron-D630: /C/perforce/depot/ p4 protect
  Ron-D630: /C/perforce/depot/ p4 sync -f
  Ron-D630: /C/perforce/depot/

From the start bar (or whatever) at the bottom of the screen, I can now tell
which is which at a glance.

Each one has a happily colored prompt:

  Ron-D630: /C/ronb/rails/depot mysql -u ron -p

And I even went a little over-the-top at work:

  10^100: /home/rbarry/google/>


	-rbarry
prev:20080806           whole blog            in context             next:20080821