Programming C, C++, Java, PHP, Ruby, Turing, VB
Computer Science Canada 
Programming C, C++, Java, PHP, Ruby, Turing, VB  

Username:   Password: 
 RegisterRegister   
 WIP - Bash Whirlwind
Index -> General Programming
View previous topic Printable versionDownload TopicSubscribe to this topicPrivate MessagesRefresh page View next topic
Author Message
wtd




PostPosted: Fri Oct 13, 2006 3:35 pm   Post subject: WIP - Bash Whirlwind

Hello, bash!

Let's take a look at the simplest possible bash script.

code:
#!/bin/sh



The first line (and in the case only) starts with a pound sign, so it's a comment. However, this comment also has a special purpose as the first line in the script. It indicates the program which is to be used to execute the script.

Now we'll look at a script which says hello. Oddly enough, we'll call it hello.sh.

code:
#!/bin/sh

echo Hello, bash\!


The echo program simply spits back the arguments sent to it. The exclamation mark is escaped with a backslash.

To make this executable, we simple need to use:

code:
chmod +x test.sh


Hello... you

Let's greet someone by name, as passed to the script via the command-line parameters.

code:
#!/bin/sh

echo Hello, $1\!


The $1 in the above is referring to a special variable. These numbered variables hold the arguments passed to the script. Perhaps if we create a variable called "name" to refer to this, it would make things clearer.

code:
#!/bin/sh

name=$1

echo Hello, $name\!


A bit of error-checking

What if some sneaky user just called:

code:
$ ./hello.sh


And provided no name?

Well, then we'd get:

code:
Hello, !


That's no good. The "name" variable is basically an empty string, which when used simply results in, well... again nothing.

So let's test for that.

code:
#!/bin/sh

name=$1

if [ $name ]; then
   # name is something other than an empty string
   # so we can greet it.
   echo Hello, $name\!
else
   # name is false, or in other words, an empty
   # string, so greeting it makes no sense
   echo Hello, bash\!
fi


Greet two names

So now that we've greeted one name, it should be simplicity itself to greet two.

code:
#!/bin/sh

name1=$1
name2=$2

if [ $name1 ]; then
   # name is something other than an empty string
   # so we can greet it.
   echo Hello, $name1\!
else
   # name is false, or in other words, an empty
   # string, so greeting it makes no sense
   echo Hello, bash\!
fi

if [ $name2 ]; then
   # name is something other than an empty string
   # so we can greet it.
   echo Hello, $name2\!
else
   # name is false, or in other words, an empty
   # string, so greeting it makes no sense
   echo Hello, bash\!
fi


Perhaps this would be easier, though, if we had that code to greet a name wrapped up somehow in one place. I suppose we could define a function.

code:
#!/bin/sh

greet()
{
   local name=$1
   
   if [ $name ]; then
      echo Hello, $name\!
   else
      echo Hello, bash\!
   fi
}

name1=$1
name2=$2

greet $name1
greet $name2


So, the question is... what's going on here? Well, I define a function named greet. Functions take their arguments in the same way as scripts. So inside I create a local "name" variable to refer to that argument. The variable is made local so that it doesn't interfere with any "name" variables outside of the function.

I then have only to call that function twice.

Greet any number of people!

Now that we've done one name, and two names, why not any number of names?

The arguments to a script get stored in the numbered variables, but the whole collection of them get stored in the $@ special variable. We just have to loop over that to greet everyone.

code:
#!/bin/sh

greet()
{
   local name=$1
   
   if [ $name ]; then
      echo Hello, $name\!
   else
      echo Hello, bash\!
   fi
}

for name in $@; do
   greet $name
done


Is that an array?

In order to answer that, you really have to understand that it's all about strings in shell scripting. The "$@" variable is not an array. Rather it's a string that contains all of the arguments passed to the program. If we called hello.sh as:

code:
./hello.sh foo bar    baz


Then "$@" contains:

code:
foo bar    baz


We can then see the loop in the above code as:

code:
for name in foo bar    baz; do
   greet $name
done


As the shell parses spaces as separators, it sees foo, bar and baz as separate values. Had our loop been written a bit differently, using quotes to group text, these values would be seen as only a single value.

code:
for name in "$@"; do
   greet $name
done


Then the loop becomes:

code:
for name in "foo bar    baz"; do
   greet $name
done


Choosy greet

Let's make our greet function a tad more selective.

code:
greet()
{
   local name=$1
   
   if [ $name = Robert -o $name = robert ]; then
      echo Hey Bob\!
   elsif [ $name = Edward -o $name = edward ]; then
      echo Hey Ed\!
   elsif [ $name ]; then
      echo Hello, $name\!
   else
      echo Hello
   fi
}


The "-o" is the "or" operator.

However, we can maybe do this a bit more nicely.

code:
greet()
{
   local name=$1
   
   if [ $name ]; then
      case $name in
         Robert|robert)
            echo Hey Bob\!
            ;;
         Edward|edward)
            echo Hey Ed\!
            ;;
         *)
            echo Hello, $name\!
      esac
   else
      echo Hello
   fi
}


But then, since the patterns in "case" structures are just regular expressions, we can make this a bit nicer.

code:
greet()
{
   local name=$1
   
   if [ $name ]; then
      case $name in
         [Rr]obert)
            echo Hey Bob\!
            ;;
         [Ee]dward)
            echo Hey Ed\!
            ;;
         *)
            echo Hello, $name\!
      esac
   else
      echo Hello
   fi
}


More error checking

As our script stands, if no names are entered, then nothing happens. What if we change that a bit so it tells us that isn't proper use of the script?

Let's use the special variable $# (which holds the argument count) and the "less than" operator to do just that.

code:
#!/bin/sh

greet()
{
   local name=$1
   
   if [ $name ]; then
      case $name in
         [Rr]obert)
            echo Hey Bob\!
            ;;
         [Ee]dward)
            echo Hey Ed\!
            ;;
         *)
            echo Hello, $name\!
      esac
   else
      echo Hello
   fi
}

if [ $# -lt 1 ]; then
   echo Please input at least one name.
   exit
fi

for name in $@; do
   greet $name
done


Greeting a complicated name

This should be a short section. Let's say I want to greet "Bob Smith."

code:
$ ./hello.sh Bob Smith
Hello, Bob!
Hello, Smith!


This happens because the shell parser sees Bob and Smith as two entirely separate arguments. We can use quotes, though, to rectify this situation.

code:
$ ./hello.sh "Bob Smith"
Hello, Bob Smith!


It should also be known that this applies to the "echo" program that we have used extensively. It takes multiple arguments, and echoes them out, separated by a single space.

code:
$ echo Bob Smith
Bob Smith


That does what we expect, but if we introduce multiple spaces...

code:
$ echo Bob       Smith
Bob Smith


We can only retain that formatting by using quotes and making this a single argument.

code:
$ echo "Bob       Smith"
Bob       Smith


Redirection

Let's say I want to print the output of the script to a file, instead of the console.

code:
$ ./hello.sh Bob Smith > greetings.txt
$ cat greetings.txt
Hello, Bob!
Hello, Smith!
$


Let's append another run of the file, not overwriting the old results.

code:
$ ./hello.sh foo bar >> greetings.txt
$ cat greetings.txt
Hello, Bob!
Hello, Smith!
Hello, foo!
Hello, bar!
$


Now, there's a problem.

code:
$ ./hello.sh > greetings.txt
$ cat greetings.txt
Please input at least one name.
$


Did we really want the error message getting redirected out to the file? Probably not. If there was an error, we wanted to see it, and leave the file empty.

code:
$ ./hello.sh > greetings.txt
Please input at least one name.
$ cat greetings.txt
$


To get this outcome, we need to redirect our error message to standard error. By default, it gets echoed to standard output. We can redirect that standard output to standard error, though.

code:
#!/bin/sh

greet()
{
   local name=$1
   
   if [ $name ]; then
      case $name in
         [Rr]obert)
            echo Hey Bob\!
            ;;
         [Ee]dward)
            echo Hey Ed\!
            ;;
         *)
            echo Hello, $name\!
      esac
   else
      echo Hello
   fi
}

if [ $# -lt 1 ]; then
   echo Please input at least one name. out>&err
   exit
fi

for name in $@; do
   greet $name
done


Or alternatively...

code:
if [ $# -lt 1 ]; then
   echo Please input at least one name. 1>&2
   exit
fi


A small note: semi-colons

You may have noticed me using semi-colons and wondered if they were just part of the syntax for loops and conditionals. They are not. They simply allow us to separate lines without using a newline.

code:
if [ foo ]; then
   echo bar
fi


Could be written as:

code:
if [ foo ]
then
   echo bar
fi


Or even:

code:
if [ foo ]; then echo bar; fi
Sponsor
Sponsor
Sponsor
sponsor
Display posts from previous:   
   Index -> General Programming
View previous topic Tell A FriendPrintable versionDownload TopicSubscribe to this topicPrivate MessagesRefresh page View next topic

Page 1 of 1  [ 1 Posts ]
Jump to:   


Style:  
Search: