wtd
|
Posted: 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.
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:
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:
And provided no name?
Well, then we'd get:
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:
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 |
|
|
|