Tuesday, April 23, 2013

Bash/Korn Shell Diary - Part 2

Understanding the Command Line

Bash (I will refer to the entire family of “Ksh/Bash/Sh” as “Bash” from now on to make things simpler for me) provides two environments – an interactive command-line and a scripting engine which can parse commands from a user provided shell script file.

When running in command-line mode, Bash provides the user with a running environment.  Everything that takes place is within a single process with additional (non-built-in) commands running as child processes.  The shell provides the user with special variables that can customize the environment, and like all unix piped commands it also provides three streams (0 – standard input which defaults to the keyboard, 1 – standard output and 2 – standard error which both default to the terminal screen). 

To redirect these streams we can use the following formats:
    Commands  > output.txt    # redirect standard-output to a file
    Commands >> output.txt    # redirect to a file, appending to the end
    Commands  < inputs.txt    # take standard-input from a file
    Commands 2> /dev/null     # redirect standard-error to nothing (dump it)
    Commands  > out.txt 2>&1  # send standard-out and error to a file
    Commands | tee –a out.txt # split the output stream, append to a file
    Commands |& tee out.txt   # split the output and error stream


Bash also allows you to temporarily redirect a sequence of commands by using the following notation:
    {
        Commands
        …
    } > output.txt


So: “>” indicates an overwrite, “>>” indicates an append, and “|” indicates a pipe – which passes the standard-output from one command to the standard-input of the next command.  Please note that using a Pipe (“|”) will fork a new process, this takes significantly longer than using shell built-in commands and is the major cause of slow-downs in scripts.

Each command returns a value.  This value can be tested against either directly in an “if” check or indirectly by looking at the return value.
    $ if `echo “DATE” | grep “DAY” | tr ‘A-Z’ ‘a-z’`; then
    >    echo “Success”
    > else
    >    echo “Failure”
    > fi
    Success

    $ echo “DATE” | grep “DAY” | tr ‘A-Z’ ‘a-z’
    $ echo “$?”
    0


A return value of 0 indicates success, while a different value indicates failure.  This is because there is only one way for a command to succeed, but there can be multiple ways for a command to fail.  Looking at the above example we see that we received a Successful exit status from our piped command, even though we failed to produce any output, this is because piped commands (or functions) return the value of the last command executed.  By looking a little deeper we can see that our second command is the one that actually failed:

    $ echo “DATE” | grep “DAY” | tr ‘A-Z’ ‘a-z’
    $ echo “${PIPESTATUS[*]}”
    0 1 0


In these last couple of examples we’ve seen a few of the environment variables of importance to our shell:
    $? – exit status of the last executed command
    ${PIPESTATUS[*]} – exit status of the last executed series of commands executed


Some other useful variables for setting up your shell:
    $ PS1=” \[\e[33m\]\w\[\e[0m\]\n\$”
    $ PS2=”>   “


The PS1 variable sets up your command line prompt, in this case we are using special codes to change the color to brown “[e[33m]” and again to white “[e[0m]” an escape sequence to display the path “\w”, and a final newline “\n” followed by the traditional Bash-type prompt “$”.  PS2 is a special prompt that is displayed when  you are entering a multi-line subroutine (such as our compound “if” statement above).  To assign new values to our variables we use this format, to display a variable we can either dump all values (“env”) or display them using the special $variable syntax:
    $PWD      - current directory (as determined by the shell)
    $OLDPWD   - previous directory
    $PATH     - the search path for any command that is kicked out to the external shell

   
Some characters need to be escaped out (“\$”) because those characters can have special meaning in our shell.  When we are inside of double quotes (“”) the shell is allowed to pass over the input and do variable expansion ($MYVAR shows the value held in the variable “MYVAR”).  Other quotes have other special behaviors: Single Quotes (‘ – on the same key as a double quote) do not expand variabes, and Backticks (` - on the same key as the tilde ~) will execute the contents.

    TMP=”a”

    #     |--- evaluate/execute between these ticks ----|
    WORD4=`echo “this is $TMP test” | awk ‘{ print $4 }’`
    #           |-- expand vars --|       |-- don’t ---|
    #                 in here               expand here
    echo “$WORD4”   # <-- “test”


I’m going to assume you know the basics of moving around and manipulating your environment: cd, pwd, ls, cp, mv, rm.  As a fun side-note if you ever wanted to write your own shell the “cd” and “pwd” commands are the only things you really have to implement since the shell needs to know where you are, all other commands could be forked to the outer shell (a bare minimum shell can be written in about a page of C++ code and makes for a fun topic when people ask you if you’ve ever done any shell programming).  Fortunately, Bash provides significantly more than that.

From the command line you have the ability to remember previous commands:

    $ cd ~/SCRIPTS/TEST01/RUN01
$ history                     ß list previously executed commands
1 cd ~/SCRIPTS/TEST01/RUN01
2 history

$ !cd
    $ ^01^02                      ß substitute the 1st “02” for “01” in the last command I’ve run
    cd ~/SCRIPTS/TEST02/RUN01
    $ ^!!:gs/0/3                  ß substitute “3” for “2” globally in the previous command
    cd ~/SCRIPTS/TEST32/RUN31
    $ !h                          ß repeat the last command starting with this “h”
    history
    1 cd ~/SCRIPTS/TEST01/RUN01
    2 history
       :
    6 history

    $ !!                          ß execute the last command again
    1 cd ~/SCRIPTS/TEST01/RUN01
       :
    6 history
    7 history


Bash also gives us some level of Job control, the ability to place jobs into the background (“command&”), examine what jobs are in the background (“jobs”), the ability to manipulate jobs by job ID (“%1”, “%2”…) as opposed to the unique process ID for that job ($$ - gives us our own PID, $! – the PID of the most recent backgrounded child) bring them to the foreground (“fg %1” - only one job can be running in the foreground at once, and this will pause your current shell environment until it has completed), and the ability to kill jobs (“kill %1”).

No comments:

Post a Comment