-
-
Notifications
You must be signed in to change notification settings - Fork 92
Approaches for reverse-string #657
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
Merged
Merged
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
78abf0a
wip
glennj a9c2c3d
Reverse string approaches, and a Performance article
glennj 30a6f40
Update exercises/practice/reverse-string/.approaches/introduction.md
glennj b04fcb2
Update exercises/practice/reverse-string/.articles/performance/conten…
glennj fb0f9c3
Update exercises/practice/reverse-string/.articles/performance/conten…
glennj a227264
Update exercises/practice/reverse-string/.articles/performance/conten…
glennj cba72bf
Update exercises/practice/reverse-string/.articles/performance/conten…
glennj 597c578
Update exercises/practice/reverse-string/.articles/performance/conten…
glennj d36bfba
Update exercises/practice/reverse-string/.articles/performance/conten…
glennj 2881ac1
review suggestions
glennj File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| { | ||
| "introduction": { | ||
| "authors": [ | ||
| "glennj" | ||
| ], | ||
| "contributors": [] | ||
| }, | ||
| "approaches": [ | ||
| { | ||
| "uuid": "351af57f-efd4-4a8b-967b-e635428937da", | ||
| "slug": "external-tools", | ||
| "title": "External tools", | ||
| "blurb": "Use external tools to reverse a string.", | ||
| "authors": [ | ||
| "glennj" | ||
| ] | ||
| }, | ||
| { | ||
| "uuid": "4363f61c-5caa-4b52-abe9-30ae671817fc", | ||
| "slug": "loops", | ||
| "title": "Looping", | ||
| "blurb": "Loop over the indices of the string.", | ||
| "authors": [ | ||
| "glennj" | ||
| ] | ||
| } | ||
| ] | ||
| } |
27 changes: 27 additions & 0 deletions
27
exercises/practice/reverse-string/.approaches/external-tools/content.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| # External Tools | ||
|
|
||
| The GNU Linux core utilities contain lots of goodies. | ||
| To reverse strings, use `rev`. | ||
|
|
||
| ```bash | ||
| $ echo "Hello, World!" | rev | ||
| !dlroW ,olleH | ||
| ``` | ||
|
|
||
| `rev` also works with files to reverse each line. | ||
|
|
||
| ```bash | ||
| $ printf '%s\n' one two three > myfile | ||
| $ rev myfile | ||
| eno | ||
| owt | ||
| eerht | ||
| ``` | ||
|
|
||
| There are other ways to do this, but none are a simple as `rev`. | ||
|
|
||
| ```bash | ||
| grep -o . <<< "$string" | tac | paste -s -d '' | ||
| perl -lne 'print scalar reverse' <<< "$string" | ||
| # etc | ||
| ``` | ||
1 change: 1 addition & 0 deletions
1
exercises/practice/reverse-string/.approaches/external-tools/snippet.txt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| reversed=$( rev <<< "$string" ) |
50 changes: 50 additions & 0 deletions
50
exercises/practice/reverse-string/.approaches/introduction.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| # Introduction | ||
|
|
||
| To reverse a string in bash, there are two basic approaches. | ||
|
|
||
| ## Approach: External tools | ||
|
|
||
| The most straightforward approach is to call out the `rev` utility. | ||
|
|
||
| ```bash | ||
| reversed=$( echo "$string" | rev ) | ||
| # or | ||
| reversed=$( rev <<< "$string" ) | ||
| ``` | ||
|
|
||
| For more details, see the [External tools approach][app-external]. | ||
|
|
||
| ## Approach: Loop over the string indices | ||
|
|
||
| To reverse a string with just bash, loop over the indices, extract the character at that index, and add it to the accumulating result string. | ||
|
|
||
| ```bash | ||
| # forwards | ||
| reversed='' | ||
| for ((i = 0; i < ${#string}; i++)); do | ||
| reversed="${string:i:1}$reversed" | ||
| done | ||
|
|
||
| # or backwards | ||
| reversed='' | ||
| for ((i = ${#string} - 1; i >= 0; i--)); do | ||
| reversed+="${string:i:1}" | ||
| done | ||
| ``` | ||
|
|
||
| For more details, go to the [Looping approach][app-loop]. | ||
|
|
||
| ## Which approach to use? | ||
|
|
||
| Calling out to `rev` makes this exercise extremely trivial. | ||
| In a production environment, it's exactly the right approach to take. | ||
|
|
||
| If you're interested in learning about bash loops and parameter expansion, experimenting with the loop approach is more interesting. | ||
|
|
||
| Thinking about performance generally isn't something you would care that much about with a shell script. | ||
| However, working with strings can be surprisingly expensive in bash. | ||
| The [Performance article][art-perf] takes a deeper dive. | ||
|
|
||
| [app-external]: /tracks/bash/exercises/reverse-string/approaches/external-tools | ||
| [app-loop]: /tracks/bash/exercises/reverse-string/approaches/loops | ||
| [art-perf]: /tracks/bash/exercises/reverse-string/articles/performance |
43 changes: 43 additions & 0 deletions
43
exercises/practice/reverse-string/.approaches/loops/content.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| # Looping | ||
|
|
||
| In a bash loop, we are looping over the indices of the characters. | ||
| Bash strings and arrays use zero-based indexing. | ||
|
|
||
| ```bash | ||
| reversed='' | ||
| for ((i = 0; i < ${#string}; i++)); do | ||
| reversed="${string:i:1}$reversed" | ||
| done | ||
| ``` | ||
|
|
||
| - We loop from zero up to (but not including) the string length (`${#string}`). | ||
| - Extracting the character at index `i` is done with the `${var:offset:length}` parameter expansion. | ||
| - We _prepend_ the character to the accumulating variable to reverse the string. | ||
|
|
||
| Finding the string length is a surprisingly expensive operation in bash (more details in the [Performance article][art-perf]). | ||
| We don't have to re-calculate it for every loop iteration, just do it once. | ||
|
|
||
| ```bash | ||
| reversed='' | ||
| len=${#string} | ||
| for ((i = 0; i < len; i++)); do | ||
| reversed="${string:i:1}$reversed" | ||
| done | ||
| ``` | ||
|
|
||
| An alternate way to calculate it just once is to loop backwards. | ||
|
|
||
| ```bash | ||
| reversed='' | ||
| for ((i = ${#string} - 1; i >= 0; i--)); do | ||
| reversed+="${string:i:1}" | ||
| done | ||
| ``` | ||
|
|
||
| - Here, we start the loop at one less than the string length, which is the index of the last character, and we loop down to (and including) zero. | ||
| - Since we're accessing the characters in the reverse order, we'll _append_ to the accumulating variable. | ||
|
|
||
| Another performance note: accessing each character with this parameter expansion is still slow: bash has to walk the string until reaching the desired index. | ||
| The most efficient solution is discussed in the [Performance article][art-perf]). | ||
|
|
||
| [art-perf]: /tracks/bash/exercises/reverse-string/articles/performance |
4 changes: 4 additions & 0 deletions
4
exercises/practice/reverse-string/.approaches/loops/snippet.txt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| reversed='' | ||
| for ((i = ${#string} - 1; i >= 0; i--)); do | ||
| reversed+="${string:i:1}" | ||
| done |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| { | ||
| "articles": [ | ||
| { | ||
| "uuid": "b8011164-e0fb-4fc3-ba8d-1b8a67f4ee4a", | ||
| "slug": "performance", | ||
| "title": "Performance considerations", | ||
| "blurb": "Compare the performances of the various reverse-string approaches.", | ||
| "authors": [ | ||
| "glennj" | ||
| ] | ||
| } | ||
| ] | ||
| } |
102 changes: 102 additions & 0 deletions
102
exercises/practice/reverse-string/.articles/performance/content.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,102 @@ | ||
| # Performance | ||
|
|
||
| We usually don't care that much about performance with shell scripts. | ||
| When we know something is slow, we can avoid it. | ||
| This matters much more when the operation is done inside loop. | ||
|
|
||
| ## String length is slow | ||
|
|
||
| Obtaining the length of a string is a surprisingly expensive operation in bash. | ||
| With large strings and/or large loops, performance can be significantly impacted. | ||
| Storing the length in a variable helps significantly. | ||
|
|
||
| Demonstrating an empty loop, iterating over the string indices. | ||
|
|
||
| ```bash | ||
| $ printf -v string "%32767s" foo | ||
| $ time for ((i = 0; i < ${#string}; i++)); do true; done | ||
|
|
||
| real 0m1.448s | ||
| user 0m1.443s | ||
| sys 0m0.005s | ||
|
|
||
| $ len=${#string} | ||
| $ time for ((i = 0; i < len; i++)); do :; done | ||
|
|
||
| real 0m0.159s | ||
| user 0m0.159s | ||
| sys 0m0.000s | ||
| ``` | ||
|
|
||
| If we can loop backwards, we don't even need to save the length to a variable. | ||
| We get a tiny improvement since bash does not need to access the variable contents for each iteration. | ||
|
|
||
| ```bash | ||
| $ time for ((i = ${#string} - 1; i >= 0; i--)); do true; done | ||
|
|
||
| real 0m0.151s | ||
| user 0m0.151s | ||
| sys 0m0.000s | ||
| ``` | ||
|
|
||
| ## Substrings can be slow | ||
|
|
||
| Whether we go backwards or forwards, we still have to extract the character at the given index. | ||
| Re-using our 32,767 character string: | ||
|
|
||
| ```bash | ||
| $ time for ((i = 0; i < len; i++)); do | ||
| char="${string:i:1}" | ||
| done | ||
IsaacG marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| real 0m9.188s | ||
| user 0m9.188s | ||
| sys 0m0.000s | ||
| ``` | ||
|
|
||
| That's 9 seconds just to read each character. | ||
| What if we can iterate over the characters directly? | ||
| We can, with a while-read loop. | ||
|
|
||
| ## While-Read loops | ||
|
|
||
| We'll _redirect_ the string into loop so `read` can extract one character at a time. | ||
|
|
||
| ```bash | ||
| $ time while IFS= read -d "" -r -n 1 char; do | ||
| true | ||
| done < <(printf "%s" "$string") | ||
|
|
||
| real 0m0.336s | ||
| user 0m0.276s | ||
| sys 0m0.060s | ||
| ``` | ||
|
|
||
| There's a 27x improvement. | ||
|
|
||
| What are we doing there? | ||
|
|
||
| - `< <(printf "%s" "%string)` is redirecting (`<`) a [process substitution][process-subst] (`<(...)`). | ||
| We could use a [here-string][here-string] (`<<< "$string"`), but that appends a newline. | ||
| Using `printf` outputs the string without adding a trailing newline. | ||
| - `IFS= read -d "" -r -n 1 char`: There's a lot going on there with this [`read` command][read]. | ||
| - `IFS=`: Normally, `read` will trim leading and trailing whitespace. | ||
| More exactly, characters in `$IFS` are removed from the start and end of the text that `read` captures. | ||
| By default, IFS contains space, tab and newline. | ||
| Our goal is to read each character of the string, including whitespace. | ||
| The empty assignment assigns the empty string to IFS _only for the duration of the `read` command_ so that whitespace characters are not treated specially. | ||
| - `-d ""`: Normally, `read` will read up to a newline and stop there. | ||
| We typically use a while-read loop to read lines from a file. | ||
| But here, we want every character in the string including newlines. | ||
| Bash uses null terminated strings under the hood, so a bash string cannot contain a null character. | ||
| `-d ""` sets the line-ending delimiter to the null character. | ||
| - `-r`: Backslashes are handled specially by `read`. | ||
| We want to handle backslashes just like a normal character. | ||
| - `-n 1`: This limits `read` to take just one character from the input. | ||
|
|
||
| Bash is strongly optimized for reading from and writing to IO streams. | ||
| Although this while-read loop is ugly, it's the fastest way for bash to process text one character at a time. | ||
|
|
||
| [process-subst]: https://www.gnu.org/software/bash/manual/bash.html#Process-Substitution | ||
| [here-string]: https://www.gnu.org/software/bash/manual/bash.html#Here-Strings | ||
| [read]: https://www.gnu.org/software/bash/manual/bash.html#index-read | ||
6 changes: 6 additions & 0 deletions
6
exercises/practice/reverse-string/.articles/performance/snippet.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,6 @@ | ||||||
| ```bash | ||||||
| while IFS= read -d "" -r -n 1 char; do | ||||||
| # do something with $char | ||||||
| echo "$char" | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| done < <(printf "%s" "$string") | ||||||
| ``` | ||||||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.