39464
Comment: stuff.
|
37096
The list suggested that binary -a and -o operators were valid for [[ too
|
Deletions are marked like this. | Additions are marked like this. |
Line 1: | Line 1: |
## page was renamed from BashGuide/05.TestsAndConditionals | |
Line 3: | Line 2: |
[[BashGuide/Arrays|<- Arrays]] | [[BashGuide/InputAndOutput|Input and Output ->]] ---- |
[[BashGuide/Patterns|<- Patterns]] | [[BashGuide/Arrays|Arrays ->]] ---- <<TableOfContents>> <<Anchor(StartOfContent)>> |
Line 6: | Line 8: |
<<TableOfContents>> |
|
Line 9: | Line 9: |
-------- |
-------- |
Line 12: | Line 12: |
Line 13: | Line 14: |
Line 25: | Line 25: |
$ ping God ping: unknown host God $ echo $? 2 $ ping -c 1 -W 1 1.1.1.1 PING 1.1.1.1 (1.1.1.1) 56(84) bytes of data. --- 1.1.1.1 ping statistics --- 1 packets transmitted, 0 received, 100% packet loss, time 0ms $ echo $? 1 |
$ ping God ping: unknown host God $ echo $? 2 $ ping -c 1 -W 1 1.1.1.1 PING 1.1.1.1 (1.1.1.1) 56(84) bytes of data. --- 1.1.1.1 ping statistics --- 1 packets transmitted, 0 received, 100% packet loss, time 0ms $ echo $? 1 |
Line 38: | Line 38: |
{{{ rm file || { echo "Could not delete file!"; exit 1; } }}} |
{{{ rm file || { echo 'Could not delete file!' >&2; exit 1; } }}} |
Line 43: | Line 44: |
Line 45: | Line 47: |
-------- |
-------- |
Line 48: | Line 50: |
Line 49: | Line 52: |
Line 55: | Line 57: |
$ mkdir d && cd d }}} This simple example has two commands, `mkdir d` and `cd d`. You could easily just use a semi-colon there to separate both commands and execute them sequentially; but we want something more. In the above example, [[BASH]] will execute `mkdir d`, then `&&` will check the result of the `mkdir` application as it finishes. If the `mkdir` application resulted in a success (exit code 0), then `&&` will execute the next command, `cd d`. If `mkdir d` failed, and returned a non-0 exit code, `&&` will skip the next command, and we will stay in the current directory. |
$ mkdir d && cd d }}} This simple example has two commands, `mkdir d` and `cd d`. You could use a semicolon there to separate the commands and execute them sequentially; but we want something more. In the above example, [[BASH]] will execute `mkdir d`, then `&&` will check the result of the `mkdir` application after it finishes. If the `mkdir` application was successful (exit code 0), then Bash will execute the next command, `cd d`. If `mkdir d` failed, and returned a non-0 exit code, Bash will skip the next command, and we will stay in the current directory. |
Line 62: | Line 64: |
$ rm /etc/some_file.conf || echo "I couldn't remove the file" rm: cannot remove `/etc/some_file.conf': No such file or directory I couldn't remove the file |
$ rm /etc/some_file.conf || echo "I couldn't remove the file" rm: cannot remove `/etc/some_file.conf': No such file or directory I couldn't remove the file |
Line 68: | Line 70: |
You can make a sequence with these operators, but you have to be very careful when you do. Remember what exit code the operator is '''really''' going to be checking against! Here's an example that might cause confusion: {{{ $ false && true || echo "Riddle, riddle?" Riddle, riddle? $ true && false || echo "Riddle, riddle?" Riddle, riddle? }}} `true` is a command that always succeeds. `false` always fails. Can you guess why the `echo` statement is executed in both cases? The key to understanding how to sequence these operators properly is by evaluating exit codes from left to right. In the first example, `false` is unsuccessful, so `&&` does not execute the next command (which is `true`), but the next `||` gets a shot too. `||` still sees that the last exit code was that from `false`, and `||` executes the next command when the previous was unsuccessful. As a result, the `echo` statement is executed. The same for the second statement again. `true` is successful, so the `&&` executes the next statement. That is `false`; the last exit code now becomes unsuccessful. After that, `||` is evaluated, which sees the unsuccessful exit code from `false` and executes the `echo` statement. It's all easy with `true`s and `false`s; but how about real commands? {{{ # There is a problem here.... $ rm file && touch file || echo "File not found!" }}} All seems well with this piece of code, and when you test it, you'll probably see that it does what it's supposed to. It tries to delete a file, and if it succeeds, it creates it again as a new and empty file; if something goes wrong we get the error message. What's the catch? Perhaps you guessed, perhaps not, but here's a hint: imagine the file system fills up (or some other disaster occurs) in between the `rm` and the `touch`; the `rm` will succeed in deleting our file, but `touch` will fail to create it anew. As a result, we get a strange error message saying that the file wasn't found while we were actually trying to '''create''' it. What's up with that? In general, it's ''not'' a good idea to string together multiple control operators in one command. `&&` and `||` are quite useful in simple cases, but not in complex ones. See the next two sections for other ways of dealing with decision-making. -------- . '''Good Practice: <<BR>> It's best not to get overzealous when dealing with conditional operators. They can make your script hard to understand, especially for a person that's assigned to maintain it and didn't write it himself.''' |
In general, it's ''not'' a good idea to string together multiple different control operators in one command (we will explore this in the next section). `&&` and `||` are quite useful in simple cases, but not in complex ones. In the next few sections we'll show some other tools you can use for decision-making. -------- . '''Good Practice: <<BR>> It's best not to get overzealous when dealing with conditional operators. They can make your script hard to understand, especially for a person that's assigned to maintain it and didn't write it themselves.''' |
Line 100: | Line 77: |
Line 102: | Line 80: |
-------- |
-------- |
Line 106: | Line 83: |
Line 107: | Line 85: |
Line 110: | Line 87: |
Suppose you want to delete a file if it contains a certain word but also doesn't contain another word. Using `grep` (a command that checks its input for patterns), we translate these conditions to: {{{ grep -q word "$file" # exit status 0 (success) if "$file" contains 'word' ! grep -q "another word" "$file" # exit status 0 (success) if "$file" does not contain 'another word' }}} We use `-q` (quiet) on grep because we don't want it to output the lines that match, we just want the exit code to be set. |
Suppose you want to delete a file if it contains a certain "good" word but also doesn't contain another "bad" word. Using `grep` (a command that checks its input for patterns), we translate these conditions to: {{{ grep -q goodword "$file" # exit status 0 (success) if "$file" contains 'goodword' ! grep -q "badword" "$file" # exit status 0 (success) if "$file" does not contain 'badword' }}} We use `-q` (quiet) on grep because we don't want it to output the lines that match; we just want the exit code to be set. |
Line 124: | Line 100: |
$ grep -q word "$file" && ! grep -q "another word" "$file" && rm "$file" }}} This works great. Now, imagine we want to show an error message in case the deletion of the file failed: {{{ $ grep -q word "$file" && ! grep -q "another word" "$file" && rm "$file" || echo "Couldn't delete: $file" >&2 }}} |
$ grep -q goodword "$file" && ! grep -q badword "$file" && rm "$file" }}} This works great. (In fact, we can string together as many `&&` as we want, without any problems.) Now, imagine we want to show an error message in case the deletion of the file failed: {{{ $ grep -q goodword "$file" && ! grep -q badword "$file" && rm "$file" || echo "Couldn't delete: $file" >&2 }}} |
Line 135: | Line 109: |
''However'', as pointed out at the end of the previous section on ''Conditional Operators'', should either `grep` command fail, the `&&` operator that follows it will only skip the ''next'' statement, but not the one after that. Imagine the first `grep` failing. The `&&` that follows it will make sure the second `grep` is not tried. [[BASH]] will try the statement after that, though. The next statement is the `rm` statement, but since it is also preceded by a `&&` operator, it will also not be tried (remember, our exit code still indicates failure since the last command that was executed was our failed `grep`). [[BASH]] will move on to the next statement. This time, we see the `echo` statement, and it's preceded by a `||` operator. Since our exit status indicates failure, the `||` operator lets us through, and the `echo` statement is executed. As a result, the failed `grep` causes the error message `Couldn't delete: $file` to be shown on the screen. That's not what we want. Doesn't sound lethal when it's just a wrong error message you receive, but if you're not careful, this '''will''' eventually happen on more dangerous code. You wouldn't want to accidentally delete files or overwrite files as a result of a failure in your logic! The failure in our logic is in the fact that we '''want''' the `rm` and the `echo` statements to belong together. The `echo` is related to the `rm`, not to the `grep`s. So what we need, is to group them. Grouping is done using curly braces: {{{ $ grep -q word "$file" && ! grep -q "another word" "$file" && { rm "$file" || echo "Couldn't delete: $file" >&2; } }}} |
But there's a problem. When we have a sequence of commands separated by ''Conditional Operators'', Bash looks at every one of them, in order from left to right. The exit status is carried through from whichever command was most recently executed, and skipping a command doesn't change it. So, imagine the first `grep` fails (sets the exit status to 1). Bash sees a `&&` next, so it skips the second `grep` altogether. Then it sees another `&&`, so it also skips the `rm` which follows that one. Finally, it sees a `||` operator. Aha! The exit status is "failure", and we have a `||`, so Bash executes the `echo` command, and tells us that it couldn't delete a file -- even though it never actually ''tried'' to! That's not what we want. This doesn't sound too bad when it's just a wrong error message you receive, but if you're not careful, this '''will''' eventually happen on more dangerous code. You wouldn't want to accidentally delete files or overwrite files as a result of a failure in your logic! The failure in our logic is in the fact that we '''want''' the `rm` and the `echo` statements to belong together. The `echo` is related to the `rm`, not to the `grep`s. So what we need is to ''group'' them. Grouping is done using curly braces: {{{ $ grep -q goodword "$file" && ! grep -q badword "$file" && { rm "$file" || echo "Couldn't delete: $file" >&2; } }}} |
Line 147: | Line 122: |
Now we've grouped the `rm` and `echo` command together. That effectively means: before [[BASH]] has gone into this group, it is considered '''one statement''' instead of several. Going back to our situation of the first `grep` failing, instead of [[BASH]] trying the `&& rm "$file"` statement, it will now try the `&& { ... }` statement. Since it is preceded by a `&&` and the last command it ran failed (the failed `grep`), it will skip this group and move on. Command grouping can be used for more things than just grouping statements for ''Conditional Operators''. We may also want to group them so that we can redirect input to a group of statements instead of just one: {{{ { read firstLine read secondLine while read otherLine; do something done } < file }}} Here we're [[BashGuide/InputAndOutput#Redirection|redirecting]] `file` to a group of commands that read input. The file will be opened when the curly brace opens, stay open for the duration of it, and be closed when the curly brace closes. This way, we can keep sequentially reading lines from it with multiple commands. -------- |
Now we've grouped the `rm` and `echo` command together. That effectively means the group is considered '''one statement''' instead of several. Going back to our situation of the first `grep` failing, instead of Bash trying the `&& rm "$file"` statement, it will now try the `&& { ... }` statement. Since it is preceded by a `&&` and the last command it ran failed (the failed `grep`), it will skip this group and move on. Command grouping can be used for more things than just ''Conditional Operators''. We may also want to group them so that we can redirect input to a group of statements instead of just one: {{{ { read firstLine read secondLine while read otherLine; do something done } < file }}} Here we're [[BashGuide/InputAndOutput#Redirection|redirecting]] `file` to a group of commands that read input. The file will be opened when the command group starts, stay open for the duration of it, and be closed when the command group finishes. This way, we can keep sequentially reading lines from it with multiple commands. Another common use of grouping is in simple error handling: {{{ # Check if we can go into appdir. If not, output an error and exit the script. cd "$appdir" || { echo "Please create the appdir and try again" >&2; exit 1; } }}} -------- |
Line 168: | Line 145: |
Line 169: | Line 147: |
`if` is a shell keyword that executes a command, and checks that command's exit code to see whether it was successful. Depending on that exit code, `if` executes a specific block of code. {{{ $ if true > then echo "It was true." > else echo "It was false!" > fi It was true. |
`if` is a shell keyword that executes a command (or a set of commands), and checks that command's exit code to see whether it was successful. Depending on that exit code, `if` executes a specific, different, block of commands. {{{ $ if true > then echo "It was true." > else echo "It was false." > fi It was true. |
Line 184: | Line 161: |
if COMMANDS then OTHER COMMANDS fi if COMMANDS then OTHER COMMANDS fi if COMMANDS; then OTHER COMMANDS fi }}} There are some commands designed specifically to ''test'' things and return an exit status based on what they find. The first such command is `test` (also known as `[`). A more advanced version is called `[[`. `[` is a normal command that reads its arguments and does some checks with them. `[[` is much like `[`, but it's special (a shell keyword), and it offers far more versatility. Let's get practical: {{{ $ if [ a = b ] > then echo "a is the same as b." > else echo "a is not the same as b." > fi a is not the same as b. }}} `if` executes the command `[` (you don't '''need''' `if` to run the `[` command!) with the arguments `a`, `=`, `b` and `]`. `[` uses these arguments to determine what must be checked. In this case, it checks whether the string `a` (the first argument) is equal (the second argument) to the string `b` (the third argument), and if this is the case, it will exit successfully. However, since we know this is not the case, `[` will not exit successfully (its exit code will be 1). `if` sees that `[` terminated unsuccessfully and executes the code in the `else` block. Now, to see why `[[` is so much more interesting and trustworthy than `[`, let us highlight some possible problems with `[`: {{{ $ [ my name = your name ] -bash: [: too many arguments |
if COMMANDS then OTHER COMMANDS fi if COMMANDS then OTHER COMMANDS fi if COMMANDS; then OTHER COMMANDS fi }}} There are some commands designed specifically to ''test'' things and return an exit status based on what they find. The first such command is `test` (also known as `[`). A more advanced version is called `[[`. `[` or `test` is a normal command that reads its arguments and does some checks with them. `[[` is much like `[`, but it's special (a shell keyword), and it offers far more versatility. Let's get practical: {{{ $ if [ a = b ] > then echo "a is the same as b." > else echo "a is not the same as b." > fi a is not the same as b. }}} `if` executes the command `[` (remember, you don't '''need''' an `if` to run the `[` command!) with the arguments `a`, `=`, `b` and `]`. `[` uses these arguments to determine what must be checked. In this case, it checks whether the string `a` (the first argument) is equal (the second argument) to the string `b` (the third argument), and if this is the case, it will exit successfully. However, since the string "a" is not equal to the string "b", `[` will not exit successfully (its exit code will be 1). `if` sees that `[` terminated unsuccessfully and executes the code in the `else` block. The last argument, "]", means nothing to `[`, but it is required. See what happens when you omit it. Here's an example of a common pitfall when `[` is used: {{{ $ myname='Greg Wooledge' yourname='Someone Else' $ [ $myname = $yourname ] -bash: [: too many arguments |
Line 218: | Line 196: |
`[` was executed with the arguments `my`, `name`, `=`, `your`, `name` and `]`. That is 6 arguments, not 4! `[` doesn't understand what test it's supposed to execute, because it expects either the first or second argument to be an operator. In our case, the operator is the third argument. Yet another reason why [[Quotes|quotes]] are so terribly important. Whenever we type whitespace in bash that belongs together with the words before or after it, '''we need to quote the whole string''': {{{ $ [ 'my name' = 'your name' ] }}} This time, `[` sees an operator (`=`) in the second argument and it can continue with its work. Now, this may be easy to see and avoid, but it gets just a little trickier when we put the strings in variables, rather than literally in the statement: {{{ $ me='my name'; you='your name' $ [ $me = $you ] -bash: [: too many arguments }}} How did we mess up this time? Here's a hint: [[BASH]] takes our ''if-statement'' and expands all the parameters in it. The result is `if [ my name = your name ]`, again. Boom, game over. Here's how you would fix this command using `[`: {{{ $ [ "$me" = "$you" ] }}} To help us out a little, the Korn shell introduced (and [[BASH]] adopted) a new style of conditional test. Original as the Korn shell authors are, they called it `[[`. `[[` is loaded with several very interesting features which are missing from `[`. |
`[` was executed with the arguments `Greg`, `Wooledge`, `=`, `Someone`, `Else` and `]`. That is 6 arguments, not 4! `[` doesn't understand what test it's supposed to execute, because it expects either the first or second argument to be an operator. In our case, the operator is the third argument. Yet another reason why [[Quotes|quotes]] are so terribly important. Whenever we type whitespace in Bash that belongs together with the words before or after it, '''we need to quote it''', and the same thing goes for parameter expansions: {{{ $ [ "$myname" = "$yourname" ] }}} This time, `[` sees an operator (`=`) in the second argument and it can continue with its work. To help us out a little, the Korn shell introduced (and Bash adopted) a new style of conditional test. Original as the Korn shell authors are, they called it `[[`. `[[` is loaded with several very interesting features that `[` lacks. |
Line 240: | Line 206: |
{{{ $ [[ $filename = *.png ]] && echo "$filename looks like a PNG file" }}} |
{{{ $ [[ $filename = *.png ]] && echo "$filename looks like a PNG file" }}} |
Line 247: | Line 213: |
$ [[ $me = $you ]] # Fine. $ [[ I am $me = I am $you ]] # Not fine! -bash: conditional binary operator expected -bash: syntax error near `am' }}} This time, `$me` and `$you` did not need quotes. Since `[[` isn't a normal command (while `[` is), but a ''shell keyword'', it has special magical powers. It parses its arguments before they are expanded by Bash and does the expansion itself, taking the result as a single argument, even if that result contains whitespace. (In other words, `[[` does not allow word-splitting of its arguments.) ''However'', be aware that simple strings still have to be quoted properly. `[[` can't know whether your literal whitespace in the statement is intentional or not; so it splits it up just like [[BASH]] normally would. Let's fix our last example: {{{ $ [[ "I am $me" = "I am $you" ]] }}} |
$ [[ $me = $you ]] # Fine. $ [[ I am $me = I am $you ]] # Not fine! -bash: conditional binary operator expected -bash: syntax error near `am' }}} This time, `$me` and `$you` did not need quotes. Since `[[` isn't a normal command (like `[` is), but a ''shell keyword'', it has special magical powers. It parses its arguments before they are expanded by Bash and does the expansion itself, taking the result as a single argument, even if that result contains whitespace. (In other words, `[[` does not allow word-splitting of its arguments.) ''However'', be aware that simple strings still have to be quoted properly. `[[` treats a space outside of quotes as an argument separator, just like Bash normally would. Let's fix our last example: {{{ $ [[ "I am $me" = "I am $you" ]] }}} |
Line 261: | Line 226: |
$ foo=[a-z]* name=lhunath $ [[ $name = $foo ]] && echo "Name $name matches pattern $foo" Name lhunath matches pattern [a-z]* $ [[ $name = "$foo" ]] || echo "Name $name is not equal to the string $foo" Name lhunath is not equal to the string [a-z]* |
$ foo=[a-z]* name=lhunath $ [[ $name = $foo ]] && echo "Name $name matches pattern $foo" Name lhunath matches pattern [a-z]* $ [[ $name = "$foo" ]] || echo "Name $name is not equal to the string $foo" Name lhunath is not equal to the string [a-z]* |
Line 269: | Line 234: |
'''Remember:''' Always quote stuff if you are unsure. If `foo` '''really''' contains a pattern instead of a string (a '''rare''' thing to want! You would normally write the pattern out literally: `[[ $name = [a-z]* ]]`), you will get a safe error here and you can come and fix it. If you neglect to quote, bugs can become very hard to find, since they aren't always easily reproduced! | '''Remember:''' Quoting is usually going to give you the behavior that you want, so make it a habit; omit only when the specific situation requires unquoted behavior. Unfortunately, bugs caused by incorrect quoting are often hard to find, because code is often valid with or without quotes, but may have different meanings. In such cases, bash cannot tell that you did something wrong; it just does what you tell it, even if that's not what you intended. |
Line 274: | Line 239: |
$ name=lhunath $ if [[ $name = "George" ]] > then echo "Bonjour, $name!" > elif [[ $name = "Hans" ]] > then echo "Goeie dag, $name!" > elif [[ $name = "Jack" ]] > then echo "Good day, $name!" > else > echo "You're not George, Hans or Jack. Who the hell are you, $name?" > fi }}} |
$ name=lhunath $ if [[ $name = "George" ]] > then echo "Bonjour, $name" > elif [[ $name = "Hans" ]] > then echo "Goeie dag, $name" > elif [[ $name = "Jack" ]] > then echo "Good day, $name" > else > echo "You're not George, Hans or Jack. Who the hell are you, $name?" > fi }}} Note that "<" and ">" have special significance in bash. Pop quiz: Predict what happens when you do `[ apple < banana ]`. Test your hypothesis (don't cheat by trying without first forming a hypothesis!). Cue Jeopardy music... Answer: bash looks for a file named "banana" in the current directory so that its contents can be sent to `[ apple` (via standard input). Assuming you don't have a file named "banana" in your current directory, this will result in an error. Pop quiz: Assuming the original intention of that command is determine whether "apple" comes before "banana", how would you change the command to get the desired effect? Note that the comparison operators `=`, `!=`, `>`, and `<` treat their arguments as strings. In order for the operands to be treated as numbers, you need to use one of a different set of operators: `-eq`, `-ne` (not equal), `-lt` (less than), `-gt`, `-le` (less than or equal to), or `-ge`. Pop quiz: Come up with an example that shows the difference between `<` and `-lt`. Cue Jeopardy music... Since "314" comes before "9" lexicographically (i.e. the order that the dictionary would put them in), `[` considers the former to be `<` than the later; whereas, `[` considers "314" NOT to be `-lt` "9", because three hundred fourteen is NOT less than nine. |
Line 287: | Line 256: |
* Tests supported by `[` (also known as `test`): | * Tests supported by `[` (also known as `test`) and `[[`: |
Line 292: | Line 261: |
* '''-p PIPE''': True if pipe exists. | |
Line 303: | Line 273: |
* '''STRING = STRING''': True if the first string is identical to the second. * '''STRING != STRING''': True if the first string is not identical to the second. * '''STRING < STRING''': True if the first string sorts before the second. * '''STRING > STRING''': True if the first string sorts after the second. |
* String operators: * '''STRING = STRING''': True if the first string is identical to the second. * '''STRING != STRING''': True if the first string is not identical to the second. * '''STRING < STRING''': True if the first string sorts before the second. * '''STRING > STRING''': True if the first string sorts after the second. * '''! EXPR''': Inverts the result of the expression (logical NOT). * Numeric operators: * '''INT -eq INT''': True if both integers are identical. * '''INT -ne INT''': True if the integers are not identical. * '''INT -lt INT''': True if the first integer is less than the second. * '''INT -gt INT''': True if the first integer is greater than the second. * '''INT -le INT''': True if the first integer is less than or equal to the second. * '''INT -ge INT''': True if the first integer is greater than or equal to the second. * Additional tests supported only by `[[`: * '''STRING = (or ==) PATTERN''': Not string comparison like with `[` (or `test`), but ''pattern matching'' is performed. True if the string matches the glob pattern. * '''STRING != PATTERN''': Not string comparison like with `[` (or `test`), but ''pattern matching'' is performed. True if the string does not match the glob pattern. * '''STRING =~ REGEX''': True if the string matches the regex pattern. * '''( EXPR )''': Parentheses can be used to change the evaluation precedence. * '''EXPR && EXPR''': Much like the '-a' operator of `test`, but does not evaluate the second expression if the first already turns out to be false. * '''EXPR || EXPR''': Much like the '-o' operator of `test`, but does not evaluate the second expression if the first already turns out to be true. * Tests exclusive to `[` (and `test`): |
Line 309: | Line 296: |
* '''! EXPR''': Inverts the result of the expression (logical NOT). * '''INT -eq INT''': True if both integers are identical. * '''INT -ne INT''': True if the integers are not identical. * '''INT -lt INT''': True if the first integer is less than the second. * '''INT -gt INT''': True if the first integer is greater than the second. * '''INT -le INT''': True if the first integer is less than or equal to the second. * '''INT -ge INT''': True if the first integer is greater than or equal to the second. * Additional tests supported only by `[[`: * '''STRING = (or ==) PATTERN''': Not string comparison like with `[` (or `test`), but ''pattern matching'' is performed. True if the string matches the glob pattern. * '''STRING =~ REGEX''': True if the string matches the regex pattern. * '''( EXPR )''': Parantheses can be used to change the evaluation precedence. * '''EXPR && EXPR''': Much like the '-a' operator of `test`, but does not evaluate the second expression if the first already turns out to be false. * '''EXPR || EXPR''': Much like the '-o' operator of `test`, but does not evaluate the second expression if the first already turns out to be true. |
|
Line 324: | Line 298: |
{{{ $ test -e /etc/X11/xorg.conf && echo "Your Xorg is configured!" Your Xorg is configured! $ test -n "$HOME" && echo "Your homedir is set!" Your homedir is set! $ [[ boar != bear ]] && echo "Boars aren't bears!" Boars aren't bears! $ [[ boar != b?ar ]] && echo "Boars don't look like bears!" $ [[ $DISPLAY ]] && echo "Your DISPLAY variable is not empty, you probably have Xorg running." Your DISPLAY variable is not empty, you probably have Xorg running. $ [[ ! $DISPLAY ]] && echo "Your DISPLAY variable is not not empty, you probably don't have Xorg running." }}} -------- . '''Good Practice: <<BR>> Whenever you're making a [[BASH]] script, you should always use `[[` rather than `[`. <<BR>> Whenever you're making a Shell script, which may end up being used in an environment where [[BASH]] is not available, you should use `[`, because it is far more portable. (While being built in to [[BASH]] and some other shells, `[` should be available as an external application as well; meaning it will work as argument to, for example, find's -exec and xargs.) <<BR>> Don't ever use the `-a` or `-o` tests of the `[` command. Use multiple `[` commands instead (or use `[[` if you can). POSIX doesn't define the behavior of `[` with complex sets of tests, so you never know what you'll get.''' |
{{{ $ test -e /etc/X11/xorg.conf && echo 'Your Xorg is configured!' Your Xorg is configured! $ test -n "$HOME" && echo 'Your homedir is set!' Your homedir is set! $ [[ boar != bear ]] && echo "Boars aren't bears." Boars aren't bears! $ [[ boar != b?ar ]] && echo "Boars don't look like bears." $ [[ $DISPLAY ]] && echo "Your DISPLAY variable is not empty, you probably have Xorg running." Your DISPLAY variable is not empty, you probably have Xorg running. $ [[ ! $DISPLAY ]] && echo "Your DISPLAY variable is not not empty, you probably don't have Xorg running." }}} -------- . '''Good Practice: <<BR>> Whenever you're making a Bash script, you should always use `[[` rather than `[`. <<BR>> Whenever you're making a Shell script, which may end up being used in an environment where Bash is not available, you should use `[`, because it is far more portable. (While being built in to Bash and some other shells, `[` should be available as an external application as well; meaning it will work as argument to, for example, find's -exec and xargs.) <<BR>> Don't ever use the `-a` or `-o` tests of the `[` command. Use multiple `[` commands instead (or use `[[` if you can). POSIX doesn't define the behavior of `[` with complex sets of tests, so you never know what you'll get.''' |
Line 341: | Line 314: |
if [ "$food" = apple ] && [ "$drink" = tea ]; then echo "The meal is acceptable." fi }}} |
if [ "$food" = apple ] && [ "$drink" = tea ]; then echo "The meal is acceptable." fi }}} |
Line 347: | Line 321: |
---- . '''In the FAQ: . [[BashFAQ/017|How can I group expressions, e.g. (A AND B) OR C?]] . [[BashFAQ/031|What is the difference between the old and new test commands ([ and [[)?]] . [[BashFAQ/041|How do I determine whether a variable contains a substring?]] . [[BashFAQ/054|How can I tell whether a variable contains a valid number?]]''' ---- . ''If (builtin)'': This command executes a list of commands and then, depending on their exit code, may execute the code in the following `then` (or optionally `else`) block. -------- |
---- . '''In the FAQ: ''' . '''[[BashFAQ/017|How can I group expressions, e.g. (A AND B) OR C?]] ''' . '''[[BashFAQ/031|What is the difference between the old and new test commands ([ and [[)?]] ''' . '''[[BashFAQ/041|How do I determine whether a variable contains a substring?]] ''' . '''[[BashFAQ/054|How can I tell whether a variable contains a valid number?]]''' ---- . ''if (keyword)'': Execute a list of commands and then, depending on their exit code, execute the code in the following `then` (or optionally `else`) block. -------- |
Line 358: | Line 334: |
Line 359: | Line 336: |
Now you've learned how to make some basic decisions in your scripts. However, that's not enough for every kind of task we might want to script. Sometimes we need to repeat things. For that, we need to use a ''loop''. There are two kinds of loops. Using the correct kind of loop will help you keep your scripts readable and maintainable. [[BASH]] supports `while` loops and `for` loops. The `for` loops can appear in three different forms. Here's a summary: * '''`while` ''command''''': Repeat so long as command is executed successfully (exit code: `0`). * '''`until` ''command''''': Repeat so long as command is executed unsuccessfully (exit code: `>0`). |
Now you've learned how to make some basic decisions in your scripts. However, that's not enough for every kind of task we might want to script. Sometimes we need to repeat things. For that, we need to use a ''loop''. There are two basic kinds of loops (plus a couple of variants), and using the correct kind of loop will help you keep your scripts readable and maintainable. The two basic kinds of loops are called `while` and `for`. The `while` loop has a variant called `until` which simply reverses its check; and the `for` loop can appear in two different forms. Here's a summary: * '''`while` ''command''''': Repeat so long as command is executed successfully (exit code is 0). * '''`until` ''command''''': Repeat so long as command is executed unsuccessfully (exit code is not 0). |
Line 367: | Line 343: |
* '''`for ((` ''expression''`;` ''expression''`;` ''expression'' `))`''': Starts by evaluating the first arithmetic expression; repeats the loop so long as the second arithmetic expression is valid; and at the end of each loop evaluates the third arithmetic expression. Each loop form is followed by the key word `do`, then one or more commands in the body, then the key word `done`. The `do` and `done` are similar to the `then` and `fi` (and possible `elif` and/or `else`) from the `if` statement we saw earlier. Let's put that in practice; here are some examples to illustrate the differences but also the similarities between the loops. (Remember: on most operatings systems, you press '''Ctrl-C''' to kill a program that's running on your terminal.) {{{ $ while true > do echo "Infinite loop" > done $ (( i=10 )); while (( i > 0 )) > do echo "$i empty cans of beer." > (( i-- )) > done $ for (( i=10; i > 0; i-- )) > do echo "$i empty cans of beer." > done $ for i in {10..1} > do echo "$i empty cans of beer." > done }}} The last three loops achieve exactly the same result, using different syntax. You'll encounter this many times in your shell scripting experience. There will nearly always be multiple approaches to solving a problem. The test of your skill soon won't be about solving a problem as much as about how best to solve it. You must learn to pick the best angle of approach for the job. Usually, the main factors to take into account will be the simplicity and flexibility of the resulting code. My personal favorite is the last of the examples. In that example I used ''Brace Expansion'' to generate the words; but there are other ways, too. |
* '''`for ((` ''expression''`;` ''expression''`;` ''expression'' `))`''': Starts by evaluating the first arithmetic expression; repeats the loop so long as the second arithmetic expression is successful; and at the end of each loop evaluates the third arithmetic expression. Each loop form is followed by the key word `do`, then one or more commands in the ''body'', then the key word `done`. The `do` and `done` are similar to the `then` and `fi` (and possible `elif` and/or `else`) from the `if` statement we saw earlier. Their job is to tell us where the body of the loop begins and ends. In practice, the loops are used for different kinds of tasks. The `for` loop (first form) is appropriate when we have a list of things, and we want to run through that list sequentially. The `while` loop is appropriate when we don't know exactly how many times we need to repeat something; we simply want it to keep going until we find what we're looking for. Here are some examples to illustrate the differences and also the similarities between the loops. (Remember: on most operating systems, you press '''Ctrl-C''' to kill a program that's running on your terminal.) {{{ $ while true > do echo "Infinite loop" > done }}} {{{ $ while ! ping -c 1 -W 1 1.1.1.1; do > echo "still waiting for 1.1.1.1" > sleep 1 > done }}} {{{ $ (( i=10 )); while (( i > 0 )) > do echo "$i empty cans of beer." > (( i-- )) > done $ for (( i=10; i > 0; i-- )) > do echo "$i empty cans of beer." > done $ for i in {10..1} > do echo "$i empty cans of beer." > done }}} The last three loops achieve exactly the same result, using different syntax. You'll encounter this many times in your shell scripting experience. There will nearly always be multiple approaches to solving a problem. The test of your skill soon won't be about solving a problem as much as about how ''best'' to solve it. You must learn to pick the best angle of approach for the job. Usually, the main factors to take into account will be the simplicity and flexibility of the resulting code. My personal favorite is the last of the examples. In that example I used ''Brace Expansion'' to generate the words; but there are other ways, too. |
Line 391: | Line 378: |
As I mentioned before: `for` runs through a list of words and puts each one in the loop index variable, one at a time, and then loops through the body with it. The tricky part is how [[BASH]] decides what the words are. Let me explain myself by expanding the braces from that previous example: {{{ $ for i in 10 9 8 7 6 5 4 3 2 1 > do echo "$i empty cans of beer." > done }}} [[BASH]] takes the characters between `in` and the end of the line, and splits them up into words. This splitting is done on spaces and tabs, just like argument splitting. However, if there are any unquoted substitutions in there, they will be word-split as well; it is these split-up words that become the iteration elements. |
As I mentioned before: `for` runs through a list of words and puts each one in the loop index variable, one at a time, and then loops through the body with it. The tricky part is how Bash decides what the words are. Let me explain myself by expanding the braces from that previous example: {{{ $ for i in 10 9 8 7 6 5 4 3 2 1 > do echo "$i empty cans of beer." > done }}} Bash takes the characters between `in` and the end of the line, and splits them up into words. This splitting is done on spaces and tabs, just like argument splitting. However, if there are any unquoted substitutions in there, they will be word-split as well (using [[IFS]]). All these split-up words become the iteration elements. |
Line 404: | Line 390: |
$ ls The best song in the world.mp3 $ for file in $(ls *.mp3) > do rm "$file" > done rm: cannot remove `The': No such file or directory rm: cannot remove `best': No such file or directory rm: cannot remove `song': No such file or directory rm: cannot remove `in': No such file or directory rm: cannot remove `the': No such file or directory rm: cannot remove `world.mp3': No such file or directory }}} You should already know to quote the `$file` in the `rm` statement; but what's going wrong here? [[BASH]] expands the command substitution (`$(ls *.mp3)`), replaces it by its output, and as a result executes `for file in The best song in the world.mp3`. [[BASH]] splits that up into words by using ''spaces'' and tries to `rm` each word. ''Boom, you are dead''. |
$ ls The best song in the world.mp3 $ for file in $(ls *.mp3) > do rm "$file" > done rm: cannot remove `The': No such file or directory rm: cannot remove `best': No such file or directory rm: cannot remove `song': No such file or directory rm: cannot remove `in': No such file or directory rm: cannot remove `the': No such file or directory rm: cannot remove `world.mp3': No such file or directory }}} You should already know to quote the `$file` in the `rm` statement; but what's going wrong here? Bash expands the command substitution (`$(ls *.mp3)`), replaces it by its output, and ''then'' performs word splitting on it (because it was unquoted). Essentially, Bash executes `for file in The best song in the world.mp3`. ''Boom, you are dead''. |
Line 421: | Line 407: |
$ ls The best song in the world.mp3 The worst song in the world.mp3 $ for file in "$(ls *.mp3)" > do rm "$file" > done rm: cannot remove `The best song in the world.mp3 The worst song in the world.mp3': No such file or directory }}} Quotes will indeed protect the whitespace in your filenames; but they will do more than that. The quotes will protect '''all the whitespace''' from the output of `ls`. There is no way [[BASH]] can know which parts of the output of `ls` represent filenames; it's not psychic. The output of `ls` is a simple string, and [[BASH]] treats it as such. The `for` puts the whole quoted output in `i` and runs the `rm` command with it. ''Damn, dead again''. |
$ ls The best song in the world.mp3 The worst song in the world.mp3 $ for file in "$(ls *.mp3)" > do rm "$file" > done rm: cannot remove `The best song in the world.mp3 The worst song in the world.mp3': No such file or directory }}} Quotes will indeed protect the whitespace in your filenames; but they will do more than that. The quotes will protect '''all the whitespace''' from the output of `ls`. There is no way Bash can know which parts of the output of `ls` represent filenames; it's not psychic. The output of `ls` is a simple string, and Bash treats it as such. The `for` puts the whole quoted output in `i` and runs the `rm` command with it. ''Damn, dead again''. |
Line 433: | Line 419: |
$ for file in *.mp3 > do rm "$file" > done }}} This time, [[BASH]] '''does''' know that it's dealing with filenames, and it '''does''' know what the filenames are, and as such it can split them up nicely. The result of expanding the glob is this: `for file in "The best song in the world.mp3" "The worst song in the world.mp3"`. Problem resolved. Let's talk about changing that behavior. Say you've got yourself a nice cooking recipe, and you want to write a script that tells you how to use it. Sure, let's get right at it: {{{ $ recipe='2 c. all purpose flour > 6 tsp. baking powder > 2 eggs > 2 c. milk > 1/3 c. oil' $ for ingredient in $recipe > do echo "Take $ingredient; mix well." > done }}} Can you guess what the result will look like? I recommend you run the code if you can't and ponder the output first. It will help you understand things. Yes, as explained earlier, [[BASH]] splits the contents of the `recipe` variable into words using the characters of the Input Field Separator (`IFS`). The `for` loop iterates over those words. To read the recipe correctly, we want to split it up by newlines alone, instead of by spaces and tabs and newlines. Here's how we do that: {{{ $ recipe='2 c. all purpose flour > 6 tsp. baking powder > 2 eggs > 2 c. milk > 1/3 c. oil' $ IFS=$'\n' $ for ingredient in $recipe > do echo "Take $ingredient; mix well." > done Take 2 c. all purpose flour; mix well. Take 6 tsp. baking powder; mix well. Take 2 eggs; mix well. Take 2 c. milk; mix well. Take 1/3 c. oil; mix well. $ unset IFS }}} Excellent. Two special notes: * The syntax `$'\n'` represents a literal newline, and therefore `IFS=$'\n'` puts a literal newline into the `IFS` variable. * We unset the `IFS` variable at the end, so we don't get a nasty shock later, since this is our working shell and `IFS` is used for a lot of other things, too. We should either put it back to its default value or unset it which has the same effect. '''Note: This delimiter is only used when the words consist of an expansion. Not when they're literal. Literal words are always split at spaces:''' {{{ $ oldPATH=$PATH $ PATH=/bin:/usr/bin $ IFS=: $ for i in $PATH > do echo "$i" > done /bin /usr/bin $ for i in $PATH:/usr/local/bin > do echo "$i" > done /bin /usr/bin /usr/local/bin $ for i in /bin:/usr/bin:/usr/local/bin > do echo "$i" > done /bin:/usr/bin:/usr/local/bin $ unset IFS $ PATH=$oldPATH }}} Now let's look at the `while` loop. It promises even more simplicity than this `for` loop, so long as you don't need any `for` specific features. The `while` loop is very interesting for its capacity to execute commands and base the loop's progress on the result of them. Here are a few examples of how `while` loops are very often used: {{{ $ # The sweet machine; hand out sweets for a cute price. $ while read -p $'The sweet machine.\nInsert 20c and enter your name: ' name > do echo "The machine spits out three lollipops at $name." > done $ # Check your email every five minutes. $ while sleep 300 > do kmail --check > done $ # Wait for a host to come back online. $ while ! ping -c 1 -W 1 "$host" > do echo "$host is still unavailable." > done; echo -e "$host is available again!\a" }}} The `until` loop is barely ever used, if only because it is pretty much exactly the same as the `while` loop except for the fact that its command is checked for failure (non-zero exit status) instead of success. As a result, we could rewrite our last example using an until loop as such: {{{ $ # Wait for a host to come back online. $ until ping -c 1 -W 1 "$host" > do echo "$host is still unavailable." > done; echo -e "$host is available again!\a" }}} Lastly, you can use the `continue` builtin to skip ahead to the next iteration of a loop without executing the rest of the body, and the `break` builtin to jump out of the loop and continue with the script after it. |
$ for file in *.mp3 > do rm "$file" > done }}} This time, Bash '''does''' know that it's dealing with filenames, and it '''does''' know what the filenames are, and as such it can split them up nicely. The result of expanding the glob is this: `for file in "The best song in the world.mp3" "The worst song in the world.mp3"`. Problem solved! Now let's look at the `while` loop. The `while` loop is very interesting for its capacity to execute commands until something interesting happens. Here are a few examples of how `while` loops are very often used: {{{ $ # The sweet machine; hand out sweets for a cute price. $ while read -p $'The sweet machine.\nInsert 20c and enter your name: ' name > do echo "The machine spits out three lollipops at $name." > done }}} {{{ $ # Check your email every five minutes. $ while sleep 300 > do kmail --check > done }}} {{{ $ # Wait for a host to come back online. $ while ! ping -c 1 -W 1 "$host" > do echo "$host is still unavailable." > done; echo -e "$host is available again.\a" }}} The `until` loop is barely ever used, if only because it is pretty much exactly the same as `while !`. We could rewrite our last example using an `until` loop: {{{ $ # Wait for a host to come back online. $ until ping -c 1 -W 1 "$host" > do echo "$host is still unavailable." > done; echo -e "$host is available again.\a" }}} In practice, most people simply use `while !` instead. Lastly, you can use the `continue` builtin to skip ahead to the next iteration of a loop without executing the rest of the body, and the `break` builtin to jump out of the loop and continue with the script after it. This works in both `for` and `while` loops. |
Line 532: | Line 459: |
---- . '''In the FAQ: . [[BashFAQ/015|How can I run a command on all files with the extention .gz?]] . [[BashFAQ/018|How can I use numbers with leading zeros in a loop, e.g. 01, 02?]] . [[BashFAQ/020|How can I find and deal with file names containing newlines, spaces or both?]] . [[BashFAQ/030|How can I rename all my *.foo files to *.bar, or convert spaces to underscores, or convert upper-case file names to lower case?]] . [[BashFAQ/034|Can I do a spinner in Bash?]] . [[BashFAQ/046|I want to check to see whether a word is in a list (or an element is a member of a set).]]''' |
---- . '''In the FAQ: ''' . '''[[BashFAQ/015|How can I run a command on all files with the extension .gz?]] ''' . '''[[BashFAQ/018|How can I use numbers with leading zeros in a loop, e.g. 01, 02?]] ''' . '''[[BashFAQ/020|How can I find and deal with file names containing newlines, spaces or both?]] ''' . '''[[BashFAQ/030|How can I rename all my *.foo files to *.bar, or convert spaces to underscores, or convert upper-case file names to lower case?]] ''' . '''[[BashFAQ/034|Can I do a spinner in Bash?]] ''' . '''[[BashFAQ/046|I want to check to see whether a word is in a list (or an element is a member of a set).]]''' |
Line 545: | Line 474: |
-------- |
-------- |
Line 548: | Line 477: |
Line 549: | Line 479: |
Sometimes you want to build application logic depending on the content of a variable, and you already know all of the possible values the variable can contain. You no longer want `if`'s two ''yes or no'' blocks; instead, you want a block of code for each possible value. You could obviously implement this behaviour using several `if`-statements: {{{ $ if [[ $LANG = en* ]] > then echo "Hello!"; fi $ if [[ $LANG = fr* ]] > then echo "Salut!"; fi $ if [[ $LANG = de* ]] > then echo "Guten Tag!"; fi $ if [[ $LANG = nl* ]] > then echo "Hallo!"; fi $ if [[ $LANG = it* ]] > then echo "Ciao!"; fi }}} But let's say we like to actually make clean code, shall we? [[BASH]] provides a keyword called `case` exactly for this kind of situation. A `case` statement basically enumerates several possible ''Glob Patterns'' and checks the content of your parameter against these: {{{ $ case $LANG in > en*) echo 'Hello!' ;; > fr*) echo 'Salut!' ;; > de*) echo 'Guten Tag!' ;; > nl*) echo 'Hallo!' ;; > it*) echo 'Ciao!' ;; > es*) echo 'Hola!' ;; > C|POSIX) echo 'hello world' ;; > *) echo 'I do not speak your language.' ;; > esac }}} Each choice in a `case` statement consists of a pattern (or a list of patterns with `|` between them), a right parenthesis, a block of code that is to be executed if the string matches one of those patterns, and two semi-colons to denote the end of the block of code (since you might need to write it on several lines). `case` stops matching patterns as soon as one is successful. Therefore, we can use the `*` pattern in the end to match any case that has not been caught by the other choices. |
Sometimes you want to build application logic depending on the content of a variable. This could be implemented by taking a different branch of an `if` statement depending on the results of testing against a glob: {{{ shopt -s extglob if [[ $LANG = en* ]]; then echo 'Hello!' elif [[ $LANG = fr* ]]; then echo 'Salut!' elif [[ $LANG = de* ]]; then echo 'Guten Tag!' elif [[ $LANG = nl* ]]; then echo 'Hallo!' elif [[ $LANG = it* ]]; then echo 'Ciao!' elif [[ $LANG = es* ]]; then echo 'Hola!' elif [[ $LANG = @(C|POSIX) ]]; then echo 'hello world' else echo 'I do not speak your language.' fi }}} But all these comparisons are a bit redundant. Bash provides a keyword called `case` exactly for this kind of situation. A `case` statement basically enumerates several possible ''Glob Patterns'' and checks the content of your parameter against these: {{{ case $LANG in en*) echo 'Hello!' ;; fr*) echo 'Salut!' ;; de*) echo 'Guten Tag!' ;; nl*) echo 'Hallo!' ;; it*) echo 'Ciao!' ;; es*) echo 'Hola!' ;; C|POSIX) echo 'hello world' ;; *) echo 'I do not speak your language.' ;; esac }}} Each choice in a `case` statement consists of a pattern (or a list of patterns with `|` between them), a right parenthesis, a block of code that is to be executed if the string matches one of those patterns, and two semi-colons to denote the end of the block of code (since you might need to write it on several lines). A left parenthesis can be added to the left of the pattern. Using `;&` instead of `;;` will grant you the ability to fall-through the `case` matching in bash, zsh and ksh. `case` stops matching patterns as soon as one is successful. Therefore, we can use the `*` pattern in the end to match any case that has not been caught by the other choices. |
Line 585: | Line 523: |
$ echo "Which of these does not belong in the group?"; \ > select choice in Apples Pears Crisps Lemons Kiwis; do > if [[ $choice = Crisps ]] > then echo "Correct! Crisps are not fruit."; break; fi > echo "Errr... no. Try again." > done |
$ echo "Which of these does not belong in the group?"; \ > select choice in Apples Pears Crisps Lemons Kiwis; do > if [[ $choice = Crisps ]] > then echo "Correct! Crisps are not fruit."; break; fi > echo "Errr... no. Try again." > done |
Line 597: | Line 535: |
$ PS3="Which of these does not belong in the group (#)? " \ > select choice in Apples Pears Crisps Lemons Kiwis; do > if [[ $choice = Crisps ]] > then echo "Correct! Crisps are not fruit."; break; fi > echo "Errr... no. Try again." > done }}} |
$ PS3="Which of these does not belong in the group (#)? "; \ > select choice in Apples Pears Crisps Lemons Kiwis; do > if [[ $choice = Crisps ]] > then echo "Correct! Crisps are not fruit."; break; fi > echo "Errr... no. Try again." > done }}} |
Line 607: | Line 544: |
{{{ # A simple menu: while true; do echo "Welcome to the Menu" echo " 1. Say hello" echo " 2. Say good-bye" read -p "-> " response case $response in 1) echo 'Hello there!' ;; 2) echo 'See you later!'; break ;; *) echo 'What was that?' ;; esac done # Alternative: use a variable to terminate the loop instead of an # explicit break command. quit= while test -z "$quit"; do echo "...." read -p "-> " response case $response in ... 2) echo 'See you later!'; quit=y ;; ... esac done }}} |
|
Line 609: | Line 575: |
Line 611: | Line 578: |
---- . '''In the FAQ: . [[BashFAQ/066|I want to check if [[ $var == foo or $var == bar or $var = more ... without repeating $var n times.]] . [[BashFAQ/035|How can I handle command-line arguments (options) to my script easily?]]''' |
---- . '''In the FAQ: ''' . '''[[BashFAQ/066|I want to check if [[ $var == foo or $var == bar or $var = more ... without repeating $var n times.]] ''' . '''[[BashFAQ/035|How can I handle command-line arguments (options) to my script easily?]]''' |
Line 618: | Line 587: |
-------- [[BashGuide/Arrays|<- Arrays]] | [[BashGuide/InputAndOutput|Input and Output ->]] |
<<Anchor(EndOfContent)>> -------- [[BashGuide/Patterns|<- Patterns]] | [[BashGuide/Arrays|Arrays ->]] |
Contents
Tests and Conditionals
Sequential execution of commands is one thing, but to achieve any advanced logic in your scripts or your command line one-liners, you'll need tests and conditionals. Tests determine whether something is true or false. Conditionals are used to make decisions which determine the execution flow of a script.
1. Exit Status
Every command results in an exit code whenever it terminates. This exit code is used by whatever application started it to evaluate whether everything went OK. This exit code is like a return value from functions. It's an integer between 0 and 255 (inclusive). Convention dictates that we use 0 to denote success, and any other number to denote failure of some sort. The specific number is entirely application-specific, and is used to hint as to what exactly went wrong.
For example, the ping command sends ICMP packets over the network to a certain host. That host normally responds to this packet by sending the exact same one right back. This way, we can check whether we can communicate with a remote host. ping has a range of exit codes which can tell us what went wrong, if anything did:
From the Linux ping manual:
If ping does not receive any reply packets at all it will exit with code 1. If a packet count and deadline are both specified, and fewer than count packets are received by the time the deadline has arrived, it will also exit with code 1. On other error it exits with code 2. Otherwise it exits with code 0. This makes it possible to use the exit code to see if a host is alive or not.
The special parameter ? shows us the exit code of the last foreground process that terminated. Let's play around a little with ping to see its exit codes:
$ ping God ping: unknown host God $ echo $? 2 $ ping -c 1 -W 1 1.1.1.1 PING 1.1.1.1 (1.1.1.1) 56(84) bytes of data. --- 1.1.1.1 ping statistics --- 1 packets transmitted, 0 received, 100% packet loss, time 0ms $ echo $? 1
Good Practice:
You should make sure that your scripts always return a non-zero exit code if something unexpected happened in their execution. You can do this with the exit builtin:rm file || { echo 'Could not delete file!' >&2; exit 1; }
In The Manual: Exit Status
Exit Code / Exit Status: Whenever a command ends it notifies its parent (which in our case will always be the shell that started it) of its exit status. This is represented by a number ranging from 0 to 255. This code is a hint as to the success of the command's execution.
2. Control Operators (&& and ||)
Now that we know what exit codes are, and that an exit code of '0' means the command's execution was successful, we'll learn to use this information. The easiest way of performing a certain action depending on the success of a previous command is through the use of control operators. These operators are && and ||, which respectively represent a logical AND and a logical OR. These operators are used between two commands, and they are used to control whether the second command should be executed depending on the success of the first. This concept is called conditional execution.
Let's put that theory in practice:
$ mkdir d && cd d
This simple example has two commands, mkdir d and cd d. You could use a semicolon there to separate the commands and execute them sequentially; but we want something more. In the above example, BASH will execute mkdir d, then && will check the result of the mkdir application after it finishes. If the mkdir application was successful (exit code 0), then Bash will execute the next command, cd d. If mkdir d failed, and returned a non-0 exit code, Bash will skip the next command, and we will stay in the current directory.
Another example:
$ rm /etc/some_file.conf || echo "I couldn't remove the file" rm: cannot remove `/etc/some_file.conf': No such file or directory I couldn't remove the file
|| is much like &&, but it does the exact opposite. It only executes the next command if the first failed. As such, the message is only echoed if the rm command was unsuccessful.
In general, it's not a good idea to string together multiple different control operators in one command (we will explore this in the next section). && and || are quite useful in simple cases, but not in complex ones. In the next few sections we'll show some other tools you can use for decision-making.
Good Practice:
It's best not to get overzealous when dealing with conditional operators. They can make your script hard to understand, especially for a person that's assigned to maintain it and didn't write it themselves.
In The Manual: Lists of Commands
Control Operators: These operators are used to link commands together. They check the exit code of the previous command to determine whether or not to execute the next command in the sequence.
3. Grouping Statements
Using conditional operators is easy and terse if we want to do simple error checking. Things get a bit more dangerous, though, when we want to run multiple statements if a condition holds true, or if we need to evaluate multiple conditions.
Suppose you want to delete a file if it contains a certain "good" word but also doesn't contain another "bad" word. Using grep (a command that checks its input for patterns), we translate these conditions to:
grep -q goodword "$file" # exit status 0 (success) if "$file" contains 'goodword' ! grep -q "badword" "$file" # exit status 0 (success) if "$file" does not contain 'badword'
We use -q (quiet) on grep because we don't want it to output the lines that match; we just want the exit code to be set.
The ! in front of a command causes Bash to negate the command's exit status. If the command returns 0 (success), the ! turns it into a failure. Likewise, if the command returns non-zero (failure), the ! turns it into a success.
Now, to put these conditions together and delete the file as a result of both holding true, we could use Conditional Operators:
$ grep -q goodword "$file" && ! grep -q badword "$file" && rm "$file"
This works great. (In fact, we can string together as many && as we want, without any problems.) Now, imagine we want to show an error message in case the deletion of the file failed:
$ grep -q goodword "$file" && ! grep -q badword "$file" && rm "$file" || echo "Couldn't delete: $file" >&2
This looks OK, at first sight. If rm's exit code is not 0 (success), then the || operator will trigger the next command and echo the error message (>&2: to standard error).
But there's a problem. When we have a sequence of commands separated by Conditional Operators, Bash looks at every one of them, in order from left to right. The exit status is carried through from whichever command was most recently executed, and skipping a command doesn't change it.
So, imagine the first grep fails (sets the exit status to 1). Bash sees a && next, so it skips the second grep altogether. Then it sees another &&, so it also skips the rm which follows that one. Finally, it sees a || operator. Aha! The exit status is "failure", and we have a ||, so Bash executes the echo command, and tells us that it couldn't delete a file -- even though it never actually tried to! That's not what we want.
This doesn't sound too bad when it's just a wrong error message you receive, but if you're not careful, this will eventually happen on more dangerous code. You wouldn't want to accidentally delete files or overwrite files as a result of a failure in your logic!
The failure in our logic is in the fact that we want the rm and the echo statements to belong together. The echo is related to the rm, not to the greps. So what we need is to group them. Grouping is done using curly braces:
$ grep -q goodword "$file" && ! grep -q badword "$file" && { rm "$file" || echo "Couldn't delete: $file" >&2; }
(Note: don't forget that you need a semicolon or newline before the closing curly brace!)
Now we've grouped the rm and echo command together. That effectively means the group is considered one statement instead of several. Going back to our situation of the first grep failing, instead of Bash trying the && rm "$file" statement, it will now try the && { ... } statement. Since it is preceded by a && and the last command it ran failed (the failed grep), it will skip this group and move on.
Command grouping can be used for more things than just Conditional Operators. We may also want to group them so that we can redirect input to a group of statements instead of just one:
{ read firstLine read secondLine while read otherLine; do something done } < file
Here we're redirecting file to a group of commands that read input. The file will be opened when the command group starts, stay open for the duration of it, and be closed when the command group finishes. This way, we can keep sequentially reading lines from it with multiple commands.
Another common use of grouping is in simple error handling:
# Check if we can go into appdir. If not, output an error and exit the script. cd "$appdir" || { echo "Please create the appdir and try again" >&2; exit 1; }
4. Conditional Blocks (if, test and [[)
if is a shell keyword that executes a command (or a set of commands), and checks that command's exit code to see whether it was successful. Depending on that exit code, if executes a specific, different, block of commands.
$ if true > then echo "It was true." > else echo "It was false." > fi It was true.
Here you see the basic outline of an if-statement. We start by calling if with the command true. true is a builtin command that always ends successfully. if runs that command, and once the command is done, if checks the exit code. Since true always exits successfully, if continues to the then-block, and executes that code. Should the true command have failed somehow, and returned an unsuccessful exit code, the if statement would have skipped the then code, and executed the else code block instead.
Different people have different preferred styles for writing if statements. Here are some of the common styles:
if COMMANDS then OTHER COMMANDS fi if COMMANDS then OTHER COMMANDS fi if COMMANDS; then OTHER COMMANDS fi
There are some commands designed specifically to test things and return an exit status based on what they find. The first such command is test (also known as [). A more advanced version is called [[. [ or test is a normal command that reads its arguments and does some checks with them. [[ is much like [, but it's special (a shell keyword), and it offers far more versatility. Let's get practical:
$ if [ a = b ] > then echo "a is the same as b." > else echo "a is not the same as b." > fi a is not the same as b.
if executes the command [ (remember, you don't need an if to run the [ command!) with the arguments a, =, b and ]. [ uses these arguments to determine what must be checked. In this case, it checks whether the string a (the first argument) is equal (the second argument) to the string b (the third argument), and if this is the case, it will exit successfully. However, since the string "a" is not equal to the string "b", [ will not exit successfully (its exit code will be 1). if sees that [ terminated unsuccessfully and executes the code in the else block.
The last argument, "]", means nothing to [, but it is required. See what happens when you omit it.
Here's an example of a common pitfall when [ is used:
$ myname='Greg Wooledge' yourname='Someone Else' $ [ $myname = $yourname ] -bash: [: too many arguments
Can you guess what caused the problem?
[ was executed with the arguments Greg, Wooledge, =, Someone, Else and ]. That is 6 arguments, not 4! [ doesn't understand what test it's supposed to execute, because it expects either the first or second argument to be an operator. In our case, the operator is the third argument. Yet another reason why quotes are so terribly important. Whenever we type whitespace in Bash that belongs together with the words before or after it, we need to quote it, and the same thing goes for parameter expansions:
$ [ "$myname" = "$yourname" ]
This time, [ sees an operator (=) in the second argument and it can continue with its work.
To help us out a little, the Korn shell introduced (and Bash adopted) a new style of conditional test. Original as the Korn shell authors are, they called it [[. [[ is loaded with several very interesting features that [ lacks.
One of the features of [[ is pattern matching:
$ [[ $filename = *.png ]] && echo "$filename looks like a PNG file"
Another feature of [[ helps us in dealing with parameter expansions:
$ [[ $me = $you ]] # Fine. $ [[ I am $me = I am $you ]] # Not fine! -bash: conditional binary operator expected -bash: syntax error near `am'
This time, $me and $you did not need quotes. Since [[ isn't a normal command (like [ is), but a shell keyword, it has special magical powers. It parses its arguments before they are expanded by Bash and does the expansion itself, taking the result as a single argument, even if that result contains whitespace. (In other words, [[ does not allow word-splitting of its arguments.) However, be aware that simple strings still have to be quoted properly. [[ treats a space outside of quotes as an argument separator, just like Bash normally would. Let's fix our last example:
$ [[ "I am $me" = "I am $you" ]]
Also; there is a subtle difference between quoting and not quoting the right-hand side of the comparison in [[. The = operator does pattern matching by default, whenever the right-hand side is not quoted:
$ foo=[a-z]* name=lhunath $ [[ $name = $foo ]] && echo "Name $name matches pattern $foo" Name lhunath matches pattern [a-z]* $ [[ $name = "$foo" ]] || echo "Name $name is not equal to the string $foo" Name lhunath is not equal to the string [a-z]*
The first test checks whether $name matches the pattern in $foo. The second test checks whether $name is equal to the string in $foo. The quotes really do make that much difference -- a subtlety worth noting.
Remember: Quoting is usually going to give you the behavior that you want, so make it a habit; omit only when the specific situation requires unquoted behavior. Unfortunately, bugs caused by incorrect quoting are often hard to find, because code is often valid with or without quotes, but may have different meanings. In such cases, bash cannot tell that you did something wrong; it just does what you tell it, even if that's not what you intended.
You could also combine several if statements into one using elif instead of else, where each test indicates another possibility:
$ name=lhunath $ if [[ $name = "George" ]] > then echo "Bonjour, $name" > elif [[ $name = "Hans" ]] > then echo "Goeie dag, $name" > elif [[ $name = "Jack" ]] > then echo "Good day, $name" > else > echo "You're not George, Hans or Jack. Who the hell are you, $name?" > fi
Note that "<" and ">" have special significance in bash. Pop quiz: Predict what happens when you do [ apple < banana ]. Test your hypothesis (don't cheat by trying without first forming a hypothesis!). Cue Jeopardy music... Answer: bash looks for a file named "banana" in the current directory so that its contents can be sent to [ apple (via standard input). Assuming you don't have a file named "banana" in your current directory, this will result in an error. Pop quiz: Assuming the original intention of that command is determine whether "apple" comes before "banana", how would you change the command to get the desired effect?
Note that the comparison operators =, !=, >, and < treat their arguments as strings. In order for the operands to be treated as numbers, you need to use one of a different set of operators: -eq, -ne (not equal), -lt (less than), -gt, -le (less than or equal to), or -ge. Pop quiz: Come up with an example that shows the difference between < and -lt. Cue Jeopardy music... Since "314" comes before "9" lexicographically (i.e. the order that the dictionary would put them in), [ considers the former to be < than the later; whereas, [ considers "314" NOT to be -lt "9", because three hundred fourteen is NOT less than nine.
Now that you've got a decent understanding of quoting issues that may arise, let's have a look at some of the other features that [ and [[ were blessed with:
Tests supported by [ (also known as test) and [[:
-e FILE: True if file exists.
-f FILE: True if file is a regular file.
-d FILE: True if file is a directory.
-h FILE: True if file is a symbolic link.
-p PIPE: True if pipe exists.
-r FILE: True if file is readable by you.
-s FILE: True if file exists and is not empty.
-t FD : True if FD is opened on a terminal.
-w FILE: True if the file is writable by you.
-x FILE: True if the file is executable by you.
-O FILE: True if the file is effectively owned by you.
-G FILE: True if the file is effectively owned by your group.
FILE -nt FILE: True if the first file is newer than the second.
FILE -ot FILE: True if the first file is older than the second.
-z STRING: True if the string is empty (it's length is zero).
-n STRING: True if the string is not empty (it's length is not zero).
- String operators:
STRING = STRING: True if the first string is identical to the second.
STRING != STRING: True if the first string is not identical to the second.
STRING < STRING: True if the first string sorts before the second.
STRING > STRING: True if the first string sorts after the second.
! EXPR: Inverts the result of the expression (logical NOT).
- Numeric operators:
INT -eq INT: True if both integers are identical.
INT -ne INT: True if the integers are not identical.
INT -lt INT: True if the first integer is less than the second.
INT -gt INT: True if the first integer is greater than the second.
INT -le INT: True if the first integer is less than or equal to the second.
INT -ge INT: True if the first integer is greater than or equal to the second.
Additional tests supported only by [[:
STRING = (or ==) PATTERN: Not string comparison like with [ (or test), but pattern matching is performed. True if the string matches the glob pattern.
STRING != PATTERN: Not string comparison like with [ (or test), but pattern matching is performed. True if the string does not match the glob pattern.
STRING =~ REGEX: True if the string matches the regex pattern.
( EXPR ): Parentheses can be used to change the evaluation precedence.
EXPR && EXPR: Much like the '-a' operator of test, but does not evaluate the second expression if the first already turns out to be false.
EXPR || EXPR: Much like the '-o' operator of test, but does not evaluate the second expression if the first already turns out to be true.
Tests exclusive to [ (and test):
EXPR -a EXPR: True if both expressions are true (logical AND).
EXPR -o EXPR: True if either expression is true (logical OR).
Some examples? Sure:
$ test -e /etc/X11/xorg.conf && echo 'Your Xorg is configured!' Your Xorg is configured! $ test -n "$HOME" && echo 'Your homedir is set!' Your homedir is set! $ [[ boar != bear ]] && echo "Boars aren't bears." Boars aren't bears! $ [[ boar != b?ar ]] && echo "Boars don't look like bears." $ [[ $DISPLAY ]] && echo "Your DISPLAY variable is not empty, you probably have Xorg running." Your DISPLAY variable is not empty, you probably have Xorg running. $ [[ ! $DISPLAY ]] && echo "Your DISPLAY variable is not not empty, you probably don't have Xorg running."
Good Practice:
Whenever you're making a Bash script, you should always use [[ rather than [.
Whenever you're making a Shell script, which may end up being used in an environment where Bash is not available, you should use [, because it is far more portable. (While being built in to Bash and some other shells, [ should be available as an external application as well; meaning it will work as argument to, for example, find's -exec and xargs.)
Don't ever use the -a or -o tests of the [ command. Use multiple [ commands instead (or use [[ if you can). POSIX doesn't define the behavior of [ with complex sets of tests, so you never know what you'll get.if [ "$food" = apple ] && [ "$drink" = tea ]; then echo "The meal is acceptable." fi
In The Manual: Conditional Constructs
if (keyword): Execute a list of commands and then, depending on their exit code, execute the code in the following then (or optionally else) block.
5. Conditional Loops (while, until and for)
Now you've learned how to make some basic decisions in your scripts. However, that's not enough for every kind of task we might want to script. Sometimes we need to repeat things. For that, we need to use a loop. There are two basic kinds of loops (plus a couple of variants), and using the correct kind of loop will help you keep your scripts readable and maintainable.
The two basic kinds of loops are called while and for. The while loop has a variant called until which simply reverses its check; and the for loop can appear in two different forms. Here's a summary:
while command: Repeat so long as command is executed successfully (exit code is 0).
until command: Repeat so long as command is executed unsuccessfully (exit code is not 0).
for variable in words: Repeat the loop for each word, setting variable to each word in turn.
for (( expression; expression; expression )): Starts by evaluating the first arithmetic expression; repeats the loop so long as the second arithmetic expression is successful; and at the end of each loop evaluates the third arithmetic expression.
Each loop form is followed by the key word do, then one or more commands in the body, then the key word done. The do and done are similar to the then and fi (and possible elif and/or else) from the if statement we saw earlier. Their job is to tell us where the body of the loop begins and ends.
In practice, the loops are used for different kinds of tasks. The for loop (first form) is appropriate when we have a list of things, and we want to run through that list sequentially. The while loop is appropriate when we don't know exactly how many times we need to repeat something; we simply want it to keep going until we find what we're looking for.
Here are some examples to illustrate the differences and also the similarities between the loops. (Remember: on most operating systems, you press Ctrl-C to kill a program that's running on your terminal.)
$ while true > do echo "Infinite loop" > done
$ while ! ping -c 1 -W 1 1.1.1.1; do > echo "still waiting for 1.1.1.1" > sleep 1 > done
$ (( i=10 )); while (( i > 0 )) > do echo "$i empty cans of beer." > (( i-- )) > done $ for (( i=10; i > 0; i-- )) > do echo "$i empty cans of beer." > done $ for i in {10..1} > do echo "$i empty cans of beer." > done
The last three loops achieve exactly the same result, using different syntax. You'll encounter this many times in your shell scripting experience. There will nearly always be multiple approaches to solving a problem. The test of your skill soon won't be about solving a problem as much as about how best to solve it. You must learn to pick the best angle of approach for the job. Usually, the main factors to take into account will be the simplicity and flexibility of the resulting code. My personal favorite is the last of the examples. In that example I used Brace Expansion to generate the words; but there are other ways, too.
Let's take a closer look at that last example, because although it looks the easier of the two fors, it can often be the trickier, if you don't know exactly how it works.
As I mentioned before: for runs through a list of words and puts each one in the loop index variable, one at a time, and then loops through the body with it. The tricky part is how Bash decides what the words are. Let me explain myself by expanding the braces from that previous example:
$ for i in 10 9 8 7 6 5 4 3 2 1 > do echo "$i empty cans of beer." > done
Bash takes the characters between in and the end of the line, and splits them up into words. This splitting is done on spaces and tabs, just like argument splitting. However, if there are any unquoted substitutions in there, they will be word-split as well (using IFS). All these split-up words become the iteration elements.
As a result, be VERY careful not to make the following mistake:
$ ls The best song in the world.mp3 $ for file in $(ls *.mp3) > do rm "$file" > done rm: cannot remove `The': No such file or directory rm: cannot remove `best': No such file or directory rm: cannot remove `song': No such file or directory rm: cannot remove `in': No such file or directory rm: cannot remove `the': No such file or directory rm: cannot remove `world.mp3': No such file or directory
You should already know to quote the $file in the rm statement; but what's going wrong here? Bash expands the command substitution ($(ls *.mp3)), replaces it by its output, and then performs word splitting on it (because it was unquoted). Essentially, Bash executes for file in The best song in the world.mp3. Boom, you are dead.
You want to quote it, you say? Let's add another song:
$ ls The best song in the world.mp3 The worst song in the world.mp3 $ for file in "$(ls *.mp3)" > do rm "$file" > done rm: cannot remove `The best song in the world.mp3 The worst song in the world.mp3': No such file or directory
Quotes will indeed protect the whitespace in your filenames; but they will do more than that. The quotes will protect all the whitespace from the output of ls. There is no way Bash can know which parts of the output of ls represent filenames; it's not psychic. The output of ls is a simple string, and Bash treats it as such. The for puts the whole quoted output in i and runs the rm command with it. Damn, dead again.
So what do we do? As suggested earlier, globs are your best friend:
$ for file in *.mp3 > do rm "$file" > done
This time, Bash does know that it's dealing with filenames, and it does know what the filenames are, and as such it can split them up nicely. The result of expanding the glob is this: for file in "The best song in the world.mp3" "The worst song in the world.mp3". Problem solved!
Now let's look at the while loop. The while loop is very interesting for its capacity to execute commands until something interesting happens. Here are a few examples of how while loops are very often used:
$ # The sweet machine; hand out sweets for a cute price. $ while read -p $'The sweet machine.\nInsert 20c and enter your name: ' name > do echo "The machine spits out three lollipops at $name." > done
$ # Check your email every five minutes. $ while sleep 300 > do kmail --check > done
$ # Wait for a host to come back online. $ while ! ping -c 1 -W 1 "$host" > do echo "$host is still unavailable." > done; echo -e "$host is available again.\a"
The until loop is barely ever used, if only because it is pretty much exactly the same as while !. We could rewrite our last example using an until loop:
$ # Wait for a host to come back online. $ until ping -c 1 -W 1 "$host" > do echo "$host is still unavailable." > done; echo -e "$host is available again.\a"
In practice, most people simply use while ! instead.
Lastly, you can use the continue builtin to skip ahead to the next iteration of a loop without executing the rest of the body, and the break builtin to jump out of the loop and continue with the script after it. This works in both for and while loops.
In The Manual: Looping Constructs
In the FAQ:
How can I run a command on all files with the extension .gz?
How can I use numbers with leading zeros in a loop, e.g. 01, 02?
How can I find and deal with file names containing newlines, spaces or both?
I want to check to see whether a word is in a list (or an element is a member of a set).
Loop: A loop is a structure that is designed to repeat the code within until a certain condition has been fulfilled. At that point, the loop stops and the code beyond it is executed.
for (keyword): A for-loop is a type of loop that sets a variable to each of a list of values in turn, and repeats until the list is exhausted.
while (keyword): A while-loop is a type of loop that continues to run its code so long as a certain command (run before each iteration) executes successfully.
until (keyword): An until-loop is a type of loop that continues to run its code so long as a certain command (run before each iteration) executes unsuccessfully.
6. Choices (case and select)
Sometimes you want to build application logic depending on the content of a variable. This could be implemented by taking a different branch of an if statement depending on the results of testing against a glob:
shopt -s extglob if [[ $LANG = en* ]]; then echo 'Hello!' elif [[ $LANG = fr* ]]; then echo 'Salut!' elif [[ $LANG = de* ]]; then echo 'Guten Tag!' elif [[ $LANG = nl* ]]; then echo 'Hallo!' elif [[ $LANG = it* ]]; then echo 'Ciao!' elif [[ $LANG = es* ]]; then echo 'Hola!' elif [[ $LANG = @(C|POSIX) ]]; then echo 'hello world' else echo 'I do not speak your language.' fi
But all these comparisons are a bit redundant. Bash provides a keyword called case exactly for this kind of situation. A case statement basically enumerates several possible Glob Patterns and checks the content of your parameter against these:
case $LANG in en*) echo 'Hello!' ;; fr*) echo 'Salut!' ;; de*) echo 'Guten Tag!' ;; nl*) echo 'Hallo!' ;; it*) echo 'Ciao!' ;; es*) echo 'Hola!' ;; C|POSIX) echo 'hello world' ;; *) echo 'I do not speak your language.' ;; esac
Each choice in a case statement consists of a pattern (or a list of patterns with | between them), a right parenthesis, a block of code that is to be executed if the string matches one of those patterns, and two semi-colons to denote the end of the block of code (since you might need to write it on several lines). A left parenthesis can be added to the left of the pattern. Using ;& instead of ;; will grant you the ability to fall-through the case matching in bash, zsh and ksh. case stops matching patterns as soon as one is successful. Therefore, we can use the * pattern in the end to match any case that has not been caught by the other choices.
Another construct of choice is the select construct. This statement smells like a loop and is a convenience statement for generating a menu of choices that the user can choose from.
The user is presented by choices and asked to enter a number reflecting his choice. The code in the select block is then executed with a variable set to the choice the user made. If the user's choice was invalid, the variable is made empty:
$ echo "Which of these does not belong in the group?"; \ > select choice in Apples Pears Crisps Lemons Kiwis; do > if [[ $choice = Crisps ]] > then echo "Correct! Crisps are not fruit."; break; fi > echo "Errr... no. Try again." > done
The menu reappears so long as the break statement is not executed. In the example the break statement is only executed when the user makes the correct choice.
We can also use the PS3 variable to define the prompt the user replies on. Instead of showing the question before executing the select statement, we could choose to set the question as our prompt:
$ PS3="Which of these does not belong in the group (#)? "; \ > select choice in Apples Pears Crisps Lemons Kiwis; do > if [[ $choice = Crisps ]] > then echo "Correct! Crisps are not fruit."; break; fi > echo "Errr... no. Try again." > done
All of these conditional constructs (if, for, while, and case) can be nested. This means you could have a for loop with a while loop inside it, or any other combination, as deeply as you need to solve your problem.
# A simple menu: while true; do echo "Welcome to the Menu" echo " 1. Say hello" echo " 2. Say good-bye" read -p "-> " response case $response in 1) echo 'Hello there!' ;; 2) echo 'See you later!'; break ;; *) echo 'What was that?' ;; esac done # Alternative: use a variable to terminate the loop instead of an # explicit break command. quit= while test -z "$quit"; do echo "...." read -p "-> " response case $response in ... 2) echo 'See you later!'; quit=y ;; ... esac done
Good Practice:
A select statement makes a simple menu simple, but it doesn't offer much flexibility. If you want something more elaborate, you might prefer to write your own menu using a while loop, some echo or printf commands, and a read command.
In The Manual: Conditional Constructs
In the FAQ:
I want to check if [[ $var == foo or $var == bar or $var = more ... without repeating $var n times.
How can I handle command-line arguments (options) to my script easily?
case (keyword): The case statement evaluates a parameter's value against several given patterns (choices).
select (keyword): The select statement offers the user the choice of several options and executes a block of code with the user's choice in a parameter. The menu repeats until a break command is executed.