Skip to content

Commit

Permalink
Concept: Conditionals (#712)
Browse files Browse the repository at this point in the history
* Concept: Conditionals

* Apply suggestions from code review

Co-authored-by: Isaac Good <[email protected]>

* further review suggestions

* expand the `case` example a bit

* Apply suggestions from code review

Co-authored-by: Isaac Good <[email protected]>

* add about, copy from introduction

---------

Co-authored-by: Isaac Good <[email protected]>
  • Loading branch information
glennj and IsaacG authored Nov 19, 2024
1 parent d81fa84 commit b45c486
Show file tree
Hide file tree
Showing 5 changed files with 372 additions and 0 deletions.
9 changes: 9 additions & 0 deletions concepts/conditionals/.meta/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"authors": [
"glennj"
],
"contributors": [
"IsaacG"
],
"blurb": "Bash control flow commands for branching, and the concept of truthiness"
}
174 changes: 174 additions & 0 deletions concepts/conditionals/about.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
# Introduction

## Truthiness in Bash

Bash does not have the concept of boolean values.
There are strings and numbers, and arrays of strings or numbers.
So how do conditional commands deal with true and false?

## Exit Status

Commands produce an _exit status_ when they end.
An exit status is an integer between 0 and 255 inclusive.
Bash considers a **zero** exit status to represent _success_.
Any other exit status represents _failure_.

This applies to all commands, including bash builtin commands, keywords, and functions.

## The "if" Command

The basic syntax of the `if` command is

```bash
if CONDITIONAL_COMMANDS; then TRUE_COMMANDS; else FALSE_COMMANDS; fi
```

It starts with `if`; the condition commands are separated from the "success" commands by `then`; and it is terminated with `fi`.
The `else` clause is optional.

CONDITIONAL_COMMANDS can be a single command or it can be a [list of commands][command-list].

The CONDITIONAL_COMMANDS are executed, and

* if the exit status is zero (success), then the TRUE_COMMANDS are executed.
* if the exit status is non-zero (failure), then the FALSE_COMMANDS are executed, if they are present.

Cascading branches can be given with `elif`
```bash
if CONDITIONAL_COMMANDS
then TRUE_COMMANDS
elif CONDITIONAL_COMMANDS_2
then TRUE_COMMANDS_2
# more elif branches ...
else FALSE_COMMANDS
fi
```

There must be a semicolon or a newline before the `then`, `elif`, `else` and `fi` words.

To emphasize: it is the **exit status** of the conditional commands that controls the flow.
As an example, `grep` returns 0 if a match is found, and 1 if a match is not found.
The `-q` option suppresses output, only producing the exit status.

```bash
if grep -q "my pattern" my_file; then echo "the pattern is found in the file"; fi
```

## "[" and "test" Commands

There is no special syntax around the CONDITIONAL_COMMANDS.
You may be used to seeing if statements that look like this:

```if
if [ "$password" = "secure" ]; then
echo "Welcome!"
fi
```

`[` is not special syntax.
It is a _command_ that evaluates the conditional expression and exits with a success/failure status.
Like all commands, whitespace is **required** between it and its arguments.

`[` is actually a synonym for the `test` command.
They are exactly the same, except that the last argument to `[` _must_ be `]`.

### Conditional Expressions

Within `[` and `]`, you write a conditional expression.
Some typical conditional expressions include:

```bash
[ -f "$filename" ] # file operations
[ "$string1" = "$string2" ] # string comparisons
[ "$num1" -eq "$num2" ] # arithmetic comparisons
```

There are many more operations available; they are listed in the [Bash Conditional Expressions][cond-expr] section of the manual.

~~~~exercism/note
In the examples above, notice that all the variables are quoted.
The `test` and `[` commands are plain commands, where the arguments are subject to word splitting and filename expansion like any other command.
This is important to point out because conditional expressions are evaluated differently based on _how many arguments_ you provide:
* 0 arguments: the exit status is non-zero (failure)
* 1 argument: the exit status is zero (success) if the argument is not empty, non-zero if it is empty.
* 2 arguments: the first argument must be a unary operator (such as `[ -z "$name" ]`), or a `!` (negating the status of the 1-argument test)
* and [more][test].
You can get unexpected results if you forget to quote:
```bash
str=""
# this prints "empty"
if [ -n "$str" ]; then echo "not empty"; else echo "empty"; fi
# leaving the variable unquoted results in incorrect "not empty" output
if [ -n $str ]; then echo "not empty"; else echo "empty"; fi
```
[test]: https://www.gnu.org/software/bash/manual/bash.html#index-test
~~~~

## "[[" Keyword

The [`[[...]]` conditional construct][cond-construct] is not a _command_, it is a **keyword**.
This means that, although it is handled like any other command, it has special parsing rules.
What's special about `[[` is that the variables expanded within it are **not** subject to word splitting or filename expansion.
That means this command acts as you expect, even without quoting.

```bash
if [[ -n $str ]]; then echo "not empty"; else echo "empty"; fi
```

`[[` supports all the conditional expressions that `test` and `[` can handle.
In addition, `[[` provides

* the `=~` regular-expression matching operator,
* `==` and `!=` operate as a glob-pattern matching operator,
* `&&` and `||` as logical operators (special parsing rule),
* `<` and `>` as "bare" string comparison operators (special parsing rule: because these are redirection symbols, in `[` they must be escaped).

It is widely held that these special features offer so much benefit that `[[` should be used exclusively.
(For example, the [Google Shell Style Guide][goog-test].)

## The "case" Command

[`case`][case] is another control flow command.
It is like a "switch" statement in other languages.

```bash
case WORD in [PATTERN [| PATTERN]...) COMMANDS ;;]... esac
```
The WORD is matched against each PATTERN.
When one matches, the COMMANDS are executed.
```bash
read -p "Guess the secret word: " word
case "$word" in
secret) echo "Yes, you guessed it!" ;;
??????) echo "That's the right number of letters." ;;
s*) echo "You guessed the first letter." ;;
*) echo "Not even close! Try again." ;;
esac
```

Each COMMANDS clause must end with **two** semicolons, `;;`.

~~~~exercism/note
1. There are alternatives that provide functionality for fall-through command execution, and for pattern matching to continue.
Check [the manual][case] for details.
2. Recall that we talked about patterns in the [Filename Expansion][glob] section of the Quoting concept.
[case]: https://www.gnu.org/software/bash/manual/bash.html#index-case
[glob]: https://exercism.org/tracks/bash/concepts/quoting#h-filename-expansion
~~~~

[command-list]: https://www.gnu.org/software/bash/manual/bash.html#Lists
[if]: https://www.gnu.org/software/bash/manual/bash.html#index-if
[case]: https://www.gnu.org/software/bash/manual/bash.html#index-case
[cond-expr]: https://www.gnu.org/software/bash/manual/bash.html#Bash-Conditional-Expressions
[cond-construct]: https://www.gnu.org/software/bash/manual/bash.html#index-_005b_005b
[goog-test]: https://google.github.io/styleguide/shellguide.html#s6.3-tests
174 changes: 174 additions & 0 deletions concepts/conditionals/introduction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
# Introduction

## Truthiness in Bash

Bash does not have the concept of boolean values.
There are strings and numbers, and arrays of strings or numbers.
So how do conditional commands deal with true and false?

## Exit Status

Commands produce an _exit status_ when they end.
An exit status is an integer between 0 and 255 inclusive.
Bash considers a **zero** exit status to represent _success_.
Any other exit status represents _failure_.

This applies to all commands, including bash builtin commands, keywords, and functions.

## The "if" Command

The basic syntax of the `if` command is

```bash
if CONDITIONAL_COMMANDS; then TRUE_COMMANDS; else FALSE_COMMANDS; fi
```

It starts with `if`; the condition commands are separated from the "success" commands by `then`; and it is terminated with `fi`.
The `else` clause is optional.

CONDITIONAL_COMMANDS can be a single command or it can be a [list of commands][command-list].

The CONDITIONAL_COMMANDS are executed, and

* if the exit status is zero (success), then the TRUE_COMMANDS are executed.
* if the exit status is non-zero (failure), then the FALSE_COMMANDS are executed, if they are present.

Cascading branches can be given with `elif`
```bash
if CONDITIONAL_COMMANDS
then TRUE_COMMANDS
elif CONDITIONAL_COMMANDS_2
then TRUE_COMMANDS_2
# more elif branches ...
else FALSE_COMMANDS
fi
```

There must be a semicolon or a newline before the `then`, `elif`, `else` and `fi` words.

To emphasize: it is the **exit status** of the conditional commands that controls the flow.
As an example, `grep` returns 0 if a match is found, and 1 if a match is not found.
The `-q` option suppresses output, only producing the exit status.

```bash
if grep -q "my pattern" my_file; then echo "the pattern is found in the file"; fi
```

## "[" and "test" Commands

There is no special syntax around the CONDITIONAL_COMMANDS.
You may be used to seeing if statements that look like this:

```if
if [ "$password" = "secure" ]; then
echo "Welcome!"
fi
```

`[` is not special syntax.
It is a _command_ that evaluates the conditional expression and exits with a success/failure status.
Like all commands, whitespace is **required** between it and its arguments.

`[` is actually a synonym for the `test` command.
They are exactly the same, except that the last argument to `[` _must_ be `]`.

### Conditional Expressions

Within `[` and `]`, you write a conditional expression.
Some typical conditional expressions include:

```bash
[ -f "$filename" ] # file operations
[ "$string1" = "$string2" ] # string comparisons
[ "$num1" -eq "$num2" ] # arithmetic comparisons
```

There are many more operations available; they are listed in the [Bash Conditional Expressions][cond-expr] section of the manual.

~~~~exercism/note
In the examples above, notice that all the variables are quoted.
The `test` and `[` commands are plain commands, where the arguments are subject to word splitting and filename expansion like any other command.
This is important to point out because conditional expressions are evaluated differently based on _how many arguments_ you provide:
* 0 arguments: the exit status is non-zero (failure)
* 1 argument: the exit status is zero (success) if the argument is not empty, non-zero if it is empty.
* 2 arguments: the first argument must be a unary operator (such as `[ -z "$name" ]`), or a `!` (negating the status of the 1-argument test)
* and [more][test].
You can get unexpected results if you forget to quote:
```bash
str=""
# this prints "empty"
if [ -n "$str" ]; then echo "not empty"; else echo "empty"; fi
# leaving the variable unquoted results in incorrect "not empty" output
if [ -n $str ]; then echo "not empty"; else echo "empty"; fi
```
[test]: https://www.gnu.org/software/bash/manual/bash.html#index-test
~~~~

## "[[" Keyword

The [`[[...]]` conditional construct][cond-construct] is not a _command_, it is a **keyword**.
This means that, although it is handled like any other command, it has special parsing rules.
What's special about `[[` is that the variables expanded within it are **not** subject to word splitting or filename expansion.
That means this command acts as you expect, even without quoting.

```bash
if [[ -n $str ]]; then echo "not empty"; else echo "empty"; fi
```

`[[` supports all the conditional expressions that `test` and `[` can handle.
In addition, `[[` provides

* the `=~` regular-expression matching operator,
* `==` and `!=` operate as a glob-pattern matching operator,
* `&&` and `||` as logical operators (special parsing rule),
* `<` and `>` as "bare" string comparison operators (special parsing rule: because these are redirection symbols, in `[` they must be escaped).

It is widely held that these special features offer so much benefit that `[[` should be used exclusively.
(For example, the [Google Shell Style Guide][goog-test].)

## The "case" Command

[`case`][case] is another control flow command.
It is like a "switch" statement in other languages.

```bash
case WORD in [PATTERN [| PATTERN]...) COMMANDS ;;]... esac
```
The WORD is matched against each PATTERN.
When one matches, the COMMANDS are executed.
```bash
read -p "Guess the secret word: " word
case "$word" in
secret) echo "Yes, you guessed it!" ;;
??????) echo "That's the right number of letters." ;;
s*) echo "You guessed the first letter." ;;
*) echo "Not even close! Try again." ;;
esac
```

Each COMMANDS clause must end with **two** semicolons, `;;`.

~~~~exercism/note
1. There are alternatives that provide functionality for fall-through command execution, and for pattern matching to continue.
Check [the manual][case] for details.
2. Recall that we talked about patterns in the [Filename Expansion][glob] section of the Quoting concept.
[case]: https://www.gnu.org/software/bash/manual/bash.html#index-case
[glob]: https://exercism.org/tracks/bash/concepts/quoting#h-filename-expansion
~~~~

[command-list]: https://www.gnu.org/software/bash/manual/bash.html#Lists
[if]: https://www.gnu.org/software/bash/manual/bash.html#index-if
[case]: https://www.gnu.org/software/bash/manual/bash.html#index-case
[cond-expr]: https://www.gnu.org/software/bash/manual/bash.html#Bash-Conditional-Expressions
[cond-construct]: https://www.gnu.org/software/bash/manual/bash.html#index-_005b_005b
[goog-test]: https://google.github.io/styleguide/shellguide.html#s6.3-tests
10 changes: 10 additions & 0 deletions concepts/conditionals/links.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[
{
"url": "https://www.gnu.org/software/bash/manual/bash.html#index-if",
"description": "The bash `if` command"
},
{
"url": "https://mywiki.wooledge.org/BashGuide/TestsAndConditionals#Conditional_Blocks_.28if.2C_test_and_.5B.5B.29",
"description": "Conditional Blocks in the Bash Guide"
}
]
5 changes: 5 additions & 0 deletions config.json
Original file line number Diff line number Diff line change
Expand Up @@ -1233,6 +1233,11 @@
"uuid": "548085e3-720b-425e-a6c7-85b96afef657",
"slug": "quoting",
"name": "The Importance of Quoting"
},
{
"uuid": "fcd13bb3-3557-4f3a-82d8-5ba588a51cf4",
"slug": "conditionals",
"name": "Conditionals"
}
],
"key_features": [
Expand Down

0 comments on commit b45c486

Please sign in to comment.