Wednesday, February 25, 2015

Creating a portable bash shell profile - Part 2 - Customize the Shell Command Prompt

Customizing a bash shell profile is a basic exercise for someone familiarizing themselves with the Linux environment.

Customizing the command prompt is a fairly common modification, but doing so often requires arcane knowledge of shell variables and xterm color strings.  My aim here is to list the most common options and show how they might be used.

This discussion assumes one is logging into an account using the bash shell, however korn shell (ksh) and posix shell (sh) should work roughly the same.

The command prompt is defined by the $PS1 system environment variable.  By default, it can simply be assigned a fixed string, typically '$ ' for regular users and '# ' for the root user.

However, if you spend any amount of time navigating around a Unix or Linux from the command line, you can customize your command prompt, turning it into a Command Center of useful information.

Before we start, let's talk about the best place to make our modification to $PS1.  That would be the shell environment file.  For bash, this is $HOME/.bashrc ($HOME/.kshrc for ksh).  You would normally not want to make this modification in your .bash_profile, because this script only runs once per login or subshell invocation.  If you have active variables in your $PS1 string, they must be in your .bashrc file if they need to be updated each time the command prompt is displayed.


First, how about adding the current working directory to the command line prompt?  That way, we can see what directly we are in without having to constantly type 'pwd'.  To do this we use the \w token, which bash interprets as the current working directory.


EXAMPLE 1: Embed the current working directory in the prompt:
PS1='\w $ '

Here's some example output:

~ $ cd /usr/bin
/usr/bin $


You can check 'man bash' for the full list.  It is listed under the 'PROMPTING' section.
Here is the list for bash 4.2.25:

    \a     an ASCII bell character (07)
    \d     the date in "Weekday Month Date" format (e.g., "Tue May 26")
    \D{format}  the format is passed to strftime(3) and the result is inserted into the prompt string; an empty format results  in  a  locale-specific  time  representation.   The braces are required
    \e     an ASCII escape character (033)
    \h     the hostname up to the first `.'
    \H     the hostname
    \j     the number of jobs currently managed by the shell
    \l     the basename of the shell's terminal device name
    \n     newline
    \r     carriage return
    \s     the name of the shell, the basename of $0 (the portion following the final slash)
    \t     the current time in 24-hour HH:MM:SS format
    \T     the current time in 12-hour HH:MM:SS format
    \@     the current time in 12-hour am/pm format
    \A     the current time in 24-hour HH:MM format
    \u     the username of the current user
    \v     the version of bash (e.g., 2.00)
    \V     the release of bash, version + patch level (e.g., 2.00.0)
    \w     the current working directory, with $HOME abbreviated with a tilde (uses the value of the PROMPT_DIRTRIM variable)
    \W     the basename of the current working directory, with $HOME abbreviated with a tilde
    \!     the history number of this command
    \#     the command number of this command
    \$     if the effective UID is 0, a #, otherwise a $
    \nnn   the character corresponding to the octal number nnn
    \\     a backslash
    \[     begin a sequence of non-printing characters, which could be used to embed a terminal control sequence into the prompt
    \]     end a sequence of non-printing characters

It is also possible to embed other environment variables or even the result of shell function calls in the $PS1 prompt.


EXAMPLE 2:  Embed username, hostname, a customized timestamp, and the current path in the $PS1 prompt:

d ()
{
    echo `date +%H%M%S`
}

PS1='\u@\h: $(d) \w $ '

Example output:

pat@mysystem: 124644 /usr/bin $


If you would like a two-line prompt, just use \n to represent the line break.

EXAMPLE 3:

PS1='\u@\h: $(d) \w\n$ '

pat@mysystem: 125139 /usr/bin
$


Adding color to the prompt:

Embedding colors requires adding an escape character and an ANSI color token to the prompt for each color change.  Don't forget to change the color back to neutral once you've completed the prompt.

The escape character is encoded as '\e'.

The color change exit command is '\e[0m'.

There are two sets of ANSI colors.  A simple 8 color list, and a combo map of 16 basic colors, a 24 level greyscale ramp, and a full 6x6x6 color cube.  This is known as "ANSI TrueColor".

Here is the simple 8 color list:

    '\e[0;30m'   Black
    '\e[0;31m'   Red
    '\e[0;32m'   Green
    '\e[0;33m'   Yellow
    '\e[0;34m'   Blue
    '\e[0;35m'   Purple
    '\e[0;36m'   Cyan
    '\e[0;37m'   White

EXAMPLE 4:  Add primary colors to a multi-component bash shell prompt:

PS1='\e[;0;32m\u@\h:\e[;0;34m$(d) \e[0;33m\w\e[0m\n$ '

pat@mysystem:130100 /usr/bin
$


ANSI TrueColor support

To use TrueColor in your command prompt, you need to know the ANSI escape codes for the particular color you would like to use.  There are 16 basic colors, 24 grayscale colors and 216 color cube colors.  An easy way to do this would be to run the following perl script in your terminal shell to see all the colors and their associated ANSI escape sequences.   You can then embed the colors you want in the same way as shown earlier.




$ cat xterm_256c
#!/usr/bin/env perl

# Display ANSI TrueColor escape sequences and color samples:

# colors 16-231 are a 6x6x6 color cube
for ($red = 0; $red < 6; $red++)
{
    for ($green = 0; $green < 6; $green++)
    {
        for ($blue = 0; $blue < 6; $blue++)
        {
            $color = 16 + ($red * 36) + ($green * 6) + $blue;

            printf ("esc[48;5;%dm: \x1b[38;5;%dmcolor\x1b[38;5;0m\x1b[48;5;%dmcolor\x1b[0m rgb: %2.2x/%2.2x/%2.2x\n",
                $color, $color, $color,
                $red * 51, $green * 51, $blue * 51);
        }

        print ("\n");
    }

    print ("\n");
}


# colors 232-255 are a grayscale ramp, intentionally leaving out
# black and white
for ($gray = 0; $gray < 24; $gray++)
{
    $level = ($gray * 10) + 8;
    printf("\x1b]4;%d;rgb:%2.2x/%2.2x/%2.2x\x1b\\",
           232 + $gray, $level, $level, $level);
}


# display the colors

# first the system ones:
print "System colors:\n";
for ($color = 0; $color < 8; $color++)
{
    print "\x1b[48;5;${color}m  ";
}

print "\x1b[0m\n";
for ($color = 8; $color < 16; $color++)
{
    print "\x1b[48;5;${color}m  ";
}
print "\x1b[0m\n\n";

# now the color cube
print "Color cube, 6x6x6:\n";
for ($green = 0; $green < 6; $green++)
{
    for ($red = 0; $red < 6; $red++)
    {
        for ($blue = 0; $blue < 6; $blue++)
        {
            $color = 16 + ($red * 36) + ($green * 6) + $blue;
            print "\x1b[48;5;${color}m  ";
        }
        print "\x1b[0m ";
    }
    print "\n";
}
print "\n";

# now the grayscale ramp
print "Grayscale ramp:\n";
for ($color = 232; $color < 256; $color++)
{
    print "\x1b[48;5;${color}m  ";
}

print "\x1b[0m\n";

Thursday, February 19, 2015

Creating a portable bash shell profile - Part 1 - auto-cleaning your PATH variable

Customizing a bash shell profile is a basic exercise for someone familiarizing themselves with the Linux environment.  Customizing the PATH variable is a fairly typical step in this process.

One quickly realizes, however, that when setting up a new system, possibly on different variants of Linux or UNIX, repeating this exercise becomes a bit bothersome.  We would like to have a flexible and semi-portable .bash_profile -- one that helps us as much as possible with weeding out missing directories, and eliminating duplicate ones as well.  And of course, we want to do this in bash, without needing to resort to a custom perl or python script to do the heavy lifting.

The following bash script snippet will do just that.  Drop it right into your .bash_profile.  The only special requirement here is that bash 4.0 or above is used.  This allows for the use of associative arrays, which makes testing for duplicate directories simple.  If this is not available, there are a number of ways to work around it.

The script is a bit verbose -- feel free to comment out the echo lines if you want less detail.


# Reset PATH to test and include only desired and existing locations:

# Set up a temporary path:
TPATH=$PATH

# This is where you would add your own 
# custom directories to the default path:
for DIR in \                       
    "/bin" \
    "/usr/bin" \
    "/usr/local/bin" \
    "/sbin" \
    "/usr/sbin" \
    "/usr/local/sbin" \
    "$HOME/bin" \
    "$HOME/scr" \
    "$HOME/scr/midpt" \
    "$HOME/scr/endpt" \
    "$HOME/svn/foo/trunk/Src/src" \
    "$HOME/svn/foo/trunk/Src/src/test"
do
    TPATH="$TPATH:$DIR"
done

# Rebuild PATH from TPATH, testing each directory:
PATH=.

# Declare an associative array/map - bash version 4.0+ required.
declare -A PMAP

# Temporarily change IFS to : to make splitting the PATH trivial:
IFS=:
for DIR in $TPATH
do
    if [ -z "${PMAP[$DIR]}" ]
    then
        if [ -d "${DIR}" ]
        then
            echo "  Adding $DIR to PATH"
            PATH="$PATH:$DIR"
            PMAP[$DIR]=1  # add an entry to our map
        else
            echo "! NOT adding $DIR to PATH -- location does not exist"
        fi
    else
        echo "! NOT adding $DIR to PATH -- duplicate path"
    fi
done
IFS=


Sample output:

Last login: Thu Feb 19 09:31:29 2015 from 10.10.1.247
  Adding /usr/local/sbin to PATH
  Adding /usr/local/bin to PATH
  Adding /usr/sbin to PATH
  Adding /usr/bin to PATH
  Adding /sbin to PATH
  Adding /bin to PATH
  Adding /usr/games to PATH
! NOT adding /bin to PATH -- duplicate path
! NOT adding /usr/bin to PATH -- duplicate path
! NOT adding /usr/local/bin to PATH -- duplicate path
! NOT adding /sbin to PATH -- duplicate path
! NOT adding /usr/sbin to PATH -- duplicate path
! NOT adding /usr/local/sbin to PATH -- duplicate path
  Adding /home/pat/bin to PATH
  Adding /home/pat/scr to PATH
  Adding /home/pat/scr/midpt to PATH
  Adding /home/pat/scr/endpt to PATH
! NOT adding /home/pat/svn/foo/trunk/Src/src to PATH -- location does not exist
! NOT adding /home/pat/svn/foo/trunk/Src/src/test to PATH -- location does not exist


Wednesday, February 18, 2015

Clearing the Screen vs Clearing the Scroll Buffer in a Linux or Cygwin shell

There are many ways of getting to a command line shell in the Unix/Linux world.  Built-in terminal emulators, ssh, PuTTY, mintty, and many more.  It would be nice if there was a common way to clear the screen so that you could create portable scripts that need this.  For example, if you needed the ability to create a simple text UI session in a shell.  Not only that, but how about a portable way to clear the screen along with the entire scroll buffer as well?

I decided to write this blog entry because there is a lot of confusing information on the topic out on the web.  Here I try to give you the two simplest, most portable ways to solve this problem:

In the examples below, I define two aliases that should be placed in your .kshrc or .bashrc file.

alias cls='tput clear'
alias clb='tput reset'

    cls -- is short for "clear screen"
    clb -- is short for "clear buffer"

The clear screen command simply scrolls the buffer up one page to give you a clear screen.
The clear buffer command erases the entire scroll buffer, including the current screen.

These two commands should work in any linux-like shell environment (including Cygwin's mintty shell) where the 'ncurses' library has been installed.
 
    [ To check if ncurses is installed, run this command:
         "$ which tput"
       If the command is found, you are good to go. ]

As an alternative, you can use the following two alias definitions, which may be used even without the ncurses libraries.

alias cls='echo -e "\x1b[2J"'
alias clb='echo -e "\x1bc"'

These escape sequences are recognized by any terminal emulating the 'xterm' standard.