Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Concept looping #713

Merged
merged 1 commit into from
Nov 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions concepts/looping/.meta/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"authors": [
"glennj"
],
"contributors": [
],
"blurb": "Looping constructs."
}
1 change: 1 addition & 0 deletions concepts/looping/about.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# TODO
181 changes: 181 additions & 0 deletions concepts/looping/introduction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
# Looping

Bash has two forms of looping:

* Repeat some commands _for each item in a list of items_,
* Repeat some commands _while some condition is true (or false)_.

## For Loops

To iterate over a list of items, we use the `for` loop.

```bash
for varname in words; do COMMANDS; done
```

`words` is expanded using word splitting and filename expansion (that we learned about in the [Quoting concept][quoting]).
Each word is assigned to the `varname` in turn, and the COMMANDS are executed.

Here are some examples that show when the for loop can be useful

* Perform some commands on a group of files.

```bash
# source all the bash library files in the current directory
for file in ./*.bash; do
source "$file"
done
```

* Perform some command on each of the parameters to a script or function.

```bash
for arg in "$@"; do
echo "argument: ${arg}"
done
```

* Perform some command for each whitespace-separated word in the output of a command.

```bash
for i in $(seq 10); do
echo "${i} squared is $((i * i))"
done
```

We'll see some warnings about this style later on.

### Arithmetic For Loop

Bash does have an arithmetic loop.
This will look somewhat familiar to other programming languages.

```bash
for ((INITIALIZATION; CONDITION; INCREMENT)); do COMMANDS; done
```

The double-parentheses in bash represents an arithmetic context (we'll see more about bash arithmetic in a later concept).
The above example using `seq` can be written like this

```bash
for ((i = 1; i <= 10; i++)); do
echo "${i} squared is $((i * i))"
done
```

### When Not To Use For

[Don't read lines with `for`][bashfaq1].

This is an anti-pattern you'll often see: iterating over the output of `cat`.

```bash
for line in $(cat some.file); do
do_something_with "$line"
done
```

This is wrong on 2 counts.

1. The command substitution is unquoted, so it is subject to word splitting.
Word splitting, by default, splits on _whitespace_ not just newlines.
This for loop is iterating over the **words** in the file, not the lines.
2. The command substitution is unquoted, so it is subject to filename expansion.
Every word in the file will be matched as a glob pattern.

As we learned in the [Quoting concept][quoting], word splitting can be controlled with the `IFS` variable, and filename expansion can be turned off.
But this tends to be a fragile solution.
`for` is the wrong method to iterate over the lines of a file.
The idiomatic way to read a file is with a `while` loop.

## While Loops

Use `while` to repeat a sequence of commands _while_ some condition is true.

```bash
while CONDITION_COMMANDS; do COMMANDS; done
```

As with `if` (as we learned in the [Conditionals concept][conditionals], there is no special syntax for CONDITION_COMMANDS.
The exit status of the command list will determine "true" or "false".

## Controlling Loops

The `break` command jumps out of the loop.
Control resumes with the command following the loop's `done` terminator.

The `continue` command jumps to the next iteration of the loop.

## Reading the Lines of a File

As mentioned, a while loop is the idiomatic way to read a file.

```bash
while IFS= read -r line; do
do_something_with "$line"
done < some.file
```

* The content of the file is provided as input to the loop with the `<` redirection.
* The `read` command returns "true" if it can read a full line from its input.
When the input is consumed, or if the last line ends without a trailing newline, then read returns "false".
* The _truly_ idiomatic way to read the lines of a file, even if the last line does not end with a newline, is

```bash
while IFS= read -r line || [[ -n "$line" ]]; do ...
```

* The `-r` option tells `read` to not substitute backslash sequences: a backslash is just a plain character.
* `IFS=` assigns the empty string to IFS only for the duration of the read command.
This temporarily turns off word splitting so that any leading or trailing whitespace in the incoming line of text is preserved.

## Until

The `until` construct repeats a sequence of commands while some condition is **false**.
The loop repeats _until_ the condition becomes true.

```bash
until CONDITION_COMMANDS; do COMMANDS; done
```

It is rare to use `until`.
It is more common to use a "while not" loop.

```bash
while ! CONDITION_COMANDS; do ...
```

## Infinite Loops

Sometimes you want to loop forever.
Here are two ways to do it.

1. a while loop with a condition that always has a success exit status

```bash
while true; do ...
```

2. an arithmetic for loop with empty expressions

```bash
for ((;;)); do ...
```

## Do-While

There is no explicit do-while construct, but you can achieve the same effect:

```bash
while true; do
COMMANDS

if END_CONDITION; then
break
fi
done
```

[bashfaq1]: https://mywiki.wooledge.org/DontReadLinesWithFor
[quoting]: https://exercism.org/tracks/bash/concepts/quoting
[conditionals]: https://exercism.org/tracks/bash/concepts/conditionals
10 changes: 10 additions & 0 deletions concepts/looping/links.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[
{
"description": "\"Loops\" in the Bash Guide",
"url": "https://mywiki.wooledge.org/BashGuide/TestsAndConditionals#Conditional_Loops_.28while.2C_until_and_for.29"
},
{
"description": "\"Looping constructs\" in the bash manual",
"url": "https://www.gnu.org/software/bash/manual/bash.html#Looping-Constructs"
}
]
5 changes: 5 additions & 0 deletions config.json
Original file line number Diff line number Diff line change
Expand Up @@ -1238,6 +1238,11 @@
"uuid": "fcd13bb3-3557-4f3a-82d8-5ba588a51cf4",
"slug": "conditionals",
"name": "Conditionals"
},
{
"uuid": "ae9f3e82-bcdd-4c09-9788-bc543235fd52",
"slug": "looping",
"name": "Looping"
}
],
"key_features": [
Expand Down