From f755bd5bdd702f51d277961541dbf60c0c87f02f Mon Sep 17 00:00:00 2001 From: Glenn Jackman Date: Thu, 21 Nov 2024 14:23:32 -0500 Subject: [PATCH] Concept: Looping (#713) --- concepts/looping/.meta/config.json | 8 ++ concepts/looping/about.md | 1 + concepts/looping/introduction.md | 181 +++++++++++++++++++++++++++++ concepts/looping/links.json | 10 ++ config.json | 5 + 5 files changed, 205 insertions(+) create mode 100644 concepts/looping/.meta/config.json create mode 100644 concepts/looping/about.md create mode 100644 concepts/looping/introduction.md create mode 100644 concepts/looping/links.json diff --git a/concepts/looping/.meta/config.json b/concepts/looping/.meta/config.json new file mode 100644 index 00000000..98ece7d3 --- /dev/null +++ b/concepts/looping/.meta/config.json @@ -0,0 +1,8 @@ +{ + "authors": [ + "glennj" + ], + "contributors": [ + ], + "blurb": "Looping constructs." +} diff --git a/concepts/looping/about.md b/concepts/looping/about.md new file mode 100644 index 00000000..46409041 --- /dev/null +++ b/concepts/looping/about.md @@ -0,0 +1 @@ +# TODO diff --git a/concepts/looping/introduction.md b/concepts/looping/introduction.md new file mode 100644 index 00000000..04841312 --- /dev/null +++ b/concepts/looping/introduction.md @@ -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 diff --git a/concepts/looping/links.json b/concepts/looping/links.json new file mode 100644 index 00000000..31bfa6c2 --- /dev/null +++ b/concepts/looping/links.json @@ -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" + } +] diff --git a/config.json b/config.json index edf3b41e..b769bf2e 100644 --- a/config.json +++ b/config.json @@ -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": [