The differences between Linux shells (namely BASH and TCSH)

Overview

The two most common shells in Linux are CSH and BASH. Shells are programs used to allow a user to enter and execute commands as well as to interpret entire files of commands called scripts. You can work with Graphical User Interfaces (GUIs) in Linux but the real power of Linux is that it was designed around Command Line Interfaces (CLIs) and thus you realize significant benefits when working with CLIs from a shell.

I would recommend you pick one of these shells and learn it, and that you learn it well.

The Korn shell (KSH) is another fairly popular shell which extends from the Bourne shell (sh), just as BASH does, but I won’t spend a lot of time talking about KSH here.  It is worth learning a little about KSH, you may like the fact that it has backward compatibility with sh and it has an interactive style similar to CSH.

C shell – CSH/TCSH

The C shell shares a lot of the flow control constructs as the C programming language and thus this shell is dubbed the “C shell”. The Tee-shell, Tee-cee-sheel, or Tee-cee-ess-aitch (TCSH) is really just CSH with command-line completion and other features, most people today who are using CSH are really using TCSH, yet only refer to their shell as CSH. For the remained of this post CSH will be used but it will be referring to TCSH as a whole.

Bourne again shell – BASH

The Bourne shell (SH) was the standard shell in older versions of UNIX (starting with the Seventh Edition). As part of the GNU project, the Bourne shell was re-implemented to provide interactive features and this newer version of the Bourne shell is called the Bourne again shell (BASH). Almost all UNIX based systems today are still delivered with SH or what is really BASH emulating SH. Of the popular shells, BASH/SH is the oldest and most widely used.

Because every UNIX based system is delivered with BASH you will undoubtedly have to use it if you are working on Linux for an extended period of time.

Regardless of your preferred shell, I would also recommend you learn the basics of BASH.

Interactive features of CSH and BASH

CSH

One benefit of CSH is that it has a truly interactive feature not found in BASH. For instance, the following lines are evaluated interactively (i.e. line-by-line) in CSH:

> if ( 1 ) then
>  echo true
true
> endif

BASH

While SH (Bourne Shell) does not support interactive mode supposedly BASH does. However, this might behave differently than you would expect, as BASH commands are evaluated in logical blocks.

There are some subtle differences between CSH and BASH interactive modes. For example, BASH will evaluate blocks at the end whereas CSH will evaluate blocks as they are entered. Here is the same example as provided for CSH, but now provided for BASH:

$ if [ 1 ]; then
>  echo true
> fi
true

Both shells behave very similar, with one difference and that is: BASH evaluates in blocks of logic in the interactive mode while CSH does the evaluation line-by-line.

Variables

Shell Variables

BASH

In BASH you can set a shell variable using:

$ abc=123

BASH does not handle spaces very well:

$ abc = 123
BASH: abc: command not found

When we say a shell variable we mean that child processes will not inherit the variable.  This enables you to set variables and work with them locally, and to be sure that what you did won’t have some perverse effect on child processes that you start.

To demonstrate what a shell variable means, let’s create a run.sh script:

#!/bin/bash
echo switch:$switch

To execute this script, we would use (make sure it has executable permissions [chmod +x ./run.sh]):

$ ./run.sh
switch:

From here you will see that nothing is printed for the switch variable.

CSH

In CSH you can set a shell variable using:

> set abc=123

CSH trims spaces:

> set abc =   123
> echo "'${abc}'"
'123'

Let’s create a script to demonstrate some of the use cases.  Let’s call it run.csh:

#!/bin/csh
echo switch:$switch

To invoke this script, we would use (make sure it has executable permissions [chmod +x ./run.csh]):

> ./run.csh
switch: Undefined variable.

As you can see, for CSH an unset variable is an error condition.

Referencing Undefined Variables

BASH

Unset variables in BASH are allowed by default.  Just as you saw above with the run.csh script this is an error condition for CSH. Earlier we ran the run.sh script and that didn’t error even though it had very similar code. Here is that example again:

$ ./run.sh 
switch:

We can actually force BASH to report an error for undefined variables.  This is done using BASH set variables, and to make our scripts behave the same way we would do this:

#!/bin/bash
set -u
echo switch:$switch

Running this script again will produce an error now:

$ ./run.sh 
./run.sh: line 3: switch: unbound variable

CSH

As we already pointed out CSH will error, by default, if an undefined variable is referenced. Here is that example provided again:

> ./run.csh
switch: Undefined variable.

Environment Variables

BASH

From here you can run export to make this variable accessible to the child processes:

$ switch=on
$ export switch
$ ./run.sh
switch:on

$ echo test:$switch
test:on

You can simplify this by declaring, defining, and exporting the variable all in the same line:

$ export switch=off
$ ./run.sh 
switch:off

CSH

In CSH we use the setenv command to make environment variables visible to child processes. Here is an example of that syntax:

> setenv switch=on
> ./run.csh
switch:on

> echo test:$switch
test:on

Per Command Variables

BASH

With BASH we can actually set this variable on a per command line basis:

$ switch=on ./run.sh
switch:on

$ echo test:$switch
test:

Setting a variable on the same line as a command, in BASH, will affect the child process. However, the local shell or script will not be impacted (as shown above with ‘test:’).

Furthermore, we can also run this on multiple lines and see different results than when set on the same line as a command:

$ switch=on
$ ./run.sh
switch:

$ echo test:$switch
test:on

In the script you can see that the variable shows as unset, even though in the parent shell we have set it. This is what is meant by a shell variable.

CSH

CSH doesn’t have as convenient of a way as BASH does for setting a variable on a per command line basis, but you can still do this using the concept of subshells:

> (setenv switch on; ./run.csh)
switch:on

> echo test:$switch
test:

Setting a variable in a subshell will not impact the parent shell or script.

However, we can also run this on multiple lines with different results:

> switch=on
> ./run.sh
switch:

> echo test:$switch
test:on

In the script you can see that the variable shows as unset, even though in the parent shell we have set it. This is what is meant by a ‘local’ variable.

Unset a Variable

BASH

Once a variable is set you can unset and unexport it with the unset command:

$ export switch=off
$ unset switch
$ ./run.sh 
switch:

Even if you set the variable again, it is no longer exported after an unset:

$ export switch=off
$ unset switch
$ switch=on
$ ./run.sh 
switch:

However, it is important to note that once a variable is exported changes to it’s value will impact new child processes:

$ export switch=off
$ ./run.sh 
switch:off
$ switch=on
$ ./run.sh 
switch:on

CSH

Before we talk about how to unset a variable in CSH we should talk about how variables are managed in CSH. The shell variables and the environment variables are handled in completely different ways and these different mechanisms can be said to be independent of each-other. Furthermore, it is only the referencing of these variables that is the same.

What is meant by independent mechanisms? The shell variables can be set and unset independent of environment variables. Once a variable is set you can unset it with the unset or unsetenv command.  Here’s an example:

> setenv switch on
> echo envvar:$switch
envvar:on
> set switch=off
> echo shell:$switch
shell:off
> unset switch
> echo envvar:$switch
envvar:on
> unsetenv switch
> echo $switch
switch: Undefined variable.

appending $(path 123)

direct access

Variable interpretation

Both shells will evaluate non-quoted variable expressions

Here is an example in CSH:

> set test = "value"
> echo $test
value

Here is an equivalent example is BASH:

$ test="value"
$ echo $test
value

Both shells will not evaluate single-quoted variable expressions

Here is an example in CSH:

> set test = "value"
> echo '$test'
$test

Here is an equivalent example is BASH:

$ test="value"
$ echo '$test'
$test

Both shells will evaluate double-quoted variable expressions

Here is an example in CSH:

> set test = "value"
> echo "$test"
value

Here is an equivalent example is BASH:

$ test="value"
$ echo "$test"
value

Things start to differ when escaping the dollar sign ($) in a double quotes use case. Here is an example script, test.sh, script showing how to escape variable evaluation in BASH:

#!/bin/bash
#test
check="test"
grep "${check}$" $0

This script should search (using grep) this file ($0 is this file) for lines that end with “test” (in regex $ signifies the end-of-line), and this does work as expected. Running this script finds the ‘#test’ line as expected:

$ ./test.sh
#test

However, there are some nuances with CSH. For example, this script (test.csh), looks like it should work:

#!/bin/csh
#test
set check = "test"
grep "${check}$" $0

This actually produces an error:

> ./test.csh
Variable name must contain alphanumeric characters.

In CSH this should actually be executed as (quotes are removed from grep):

#!/bin/csh
#test
set check = "test"
grep ${check}$ $0

This will work as expected:

> ./test.csh
#test

It is nearly impossible to escape a dollar sign ($) inside of double quotes when using CSH; However, single quotes will prevent expansion, or just using variables without quotes enables escape sequences to solve the problem.

Syntax checking in BASH

Syntax can be checked in BASH with:

bash -n script.sh

This syntax checking process is nice as it does not execute any of the commands, and just checks the entire script for syntax.  This is a quick and easy way to validate all logical branches in a script, for syntax. If you forget to add a semicolon after a square bracket in an if statement, such as this test.sh file:

#/bin/bash
one=1
if [ $one ] then
  echo test
fi

Then the following syntax error will be reported:

./test.sh: line 5: syntax error near unexpected token `fi'
./test.sh: line 5: `fi'

However, a simple syntax error such as adding spaces around the equal sign in a variable assignment will not be caught:

#/bin/bash
one = 1
if [ $one ]; then
  echo test
fi

This is because the syntax checker doesn’t considered this a syntax error. The BASH syntax checker will think that one is a command.  This script will error at run-time with the following error:

./test.sh: line 2: one: command not found

Bottom-line: The syntax checker in BASH is nice and should be used to check syntax of BASH scripts, but be aware that the syntax checker will not catch everything.  Unfortunately, CSH doesn’t have an equivalent.

Startup scripts

BASH

The following scripts are used in BASH…

Login

  • /etc/profile (system)
  • /etc/bash.bashrc (system)
  • ~/.bash_profile
  • ~/.bash_login
  • ~/.profile
  • ~/.bashrc

Opening a shell

  • ~/.bashrc

Running a script

  • /etc/bash.bashrc (system)

CSH

The following scripts are used in CSH…

Login

  • /etc/CSH.cshrc (system)
  • /etc/CSH.login (system)
  • ~/.tcshrc
  • ~/.cshrc
  • ~/.history
  • ~/.login
  • ~/.cshdirs

Opening a shell

  • /etc/CSH.cshrc (system)
  • ~/.tcshrc
  • ~/.cshrc

Running a script

  • ~/.cshrc

Prevent loading the .cshrc file

The .cshrc file is a file where you can specify CSH commands to be executed every-time you load CSH. However, sometimes you want default CSH (i.e. that does not have your custom commands run) and this can be done with the -f switch.  This can, and should, be set in all CSH scripts as it helps to ensure that you are not depending on settings in your current environment:

#!/bin/csh -f
env

This script will show you what your environment variables looks like without your .cshrc file being sourced. You can also use the -f switch when opening a new shell:

> csh -f

Prevent loading the .bashrc and .bash_profile files

The .bashrc and .bash_profile files are places where you can specify custom commands to be executed every-time you start a BASH shell or login, respectively. However, sometimes you want default BASH (i.e. that does not have your custom commands run) and this can be done with the –norc and –noprofile switches.  These arguments can only be specified on the command line:

$ bash --norc --noprofile

Running with a clean environment

Have you ever been stuck seeing strange behavior that nobody else on your project is seeing? Have a sneaking suspicion that your environment is messed up? In swoops the ‘env’ command to save the day!

Running with ‘env -i <command>’ will prevent your environment variables from being inherited from child processes. When used in conjunction with switches to prevent shell scripts from running, you can be pretty confident that you have a clean environment.

BASH

To run clean:

$ env -i bash --norc --noprofile

CSH

To run clean:

> env -i csh -f

PATH and path

Both BASH and CSH shells have and use the $PATH environment variable. However, CSH has a variable called $path which can be manipulated in a slightly different way. Generally the use case of the PATH environment variable is so that applications can find executables and this is done by adding executable directories to the PATH environment variable. CSH gives us a direct and convenient way to do this:

set path = ($path /usr/local/bin)

The lower case path variable enables direct access indexes:

> echo $path[1]
/home/pi/bin

Redirects catching just STDERR, STDOUT, or both

BASH

Let’s start with a script that prints a message to both STDERR and STDOUT:

#!/bin/bash
echo "message intended for stdout"
(&amp;amp;amp;amp;amp;amp;gt;&amp;amp;amp;amp;amp;amp;amp;2 echo "message intended for stderr")

Now if we call this script this is what we’ll see:

$ ./std.sh 
message intended for stdout
message intended for stderr

In BASH to redirect STDOUT only:

$ ./std.sh > out.log
message intended for stderr
$ cat out.log
message intended for stdout

In BASH to redirect STDERR only:

$ ./std.sh 2> err.log
message intended for stdout
$ cat err.log
message intended for stderr

In BASH to redirect both:

$ ./std.sh >& std.log
$ cat std.log
message intended for stdout
message intended for stderr

In BASH to redirect STDOUT to one file and STDERR to another:

$ ( ./std.sh 2> err.log ) > out.log
$ cat err.log
message intended for stderr
$ cat out.log
message intended for stdout

CSH

We will use the same script provided above (BASH) that prints a message to both STDERR and STDOUT, but we will invoke this script from CSH and manipulate the STDERR and STDOUT streams:

#!/bin/csh
echo "message intended for stdout"
(>&2 echo "message intended for stderr")

Now if we call this script this is what we’ll see (remember we are in CSH):

> ./std.sh
message intended for stdout
message intended for stderr

In CSH to redirect STDOUT only:

> ./std.sh > out.log
message intended for stderr
> cat out.log
message intended for stdout

In CSH to redirect STDERR only:

$ ( ./std.sh > /dev/tty ) > & err.log
message intended for stdout
$ cat err.log
message intended for stderr

In CSH to redirect both:

$ ./std.sh > & std.log
$ cat std.log
message intended for stdout
message intended for stderr

In CSH to redirect STDOUT to one file and STDERR to another:

$ ( ./std.sh > out.log ) > & err.log
$ cat err.log
message intended for stderr
$ cat out.log
message intended for stdout
Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s