├── LICENSE ├── README.md ├── appa ├── bash_idioms_style_guide.html └── bash_idioms_style_guide.md ├── bcb2-appd.pdf ├── ch03 ├── list.sh └── wrapper.sh ├── ch04 ├── parameter-expansion.out └── parameter-expansion.sh ├── ch07 ├── hashes.out ├── hashes.sh ├── lists.out ├── lists.sh ├── word-count-example.sh └── word-count-example.txt ├── ch08 ├── parseit.sh ├── parselong.sh └── parselonghelp.sh ├── ch09 ├── fancy_mapfile.out ├── fancy_mapfile.sh ├── fiddle-ifs.out ├── fiddle-ifs.sh ├── shebang-bash.sh ├── shebang-env.sh ├── trivial_trap.out └── trivial_trap.sh ├── ch10 ├── embedded-docs.sh ├── run-embedded-docs.out ├── run-embedded-docs.sh ├── select-ssh.sh └── ssh_config └── template.sh /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 JP Vossen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # _bash Idioms_ examples 2 | 3 | 4 | 5 | 6 | Welcome to the examples from O'Reilly's _bash Idioms_, by Carl Albing and JP Vossen. 7 | 8 | * 9 | * 10 | * By the same authors, the _bash Cookbook_ (2nd edition) 11 | * 12 | * 13 | * 14 | * Other bash resources 15 | * 16 | 17 | 18 | ## About the Book 19 | 20 | Shell scripts are everywhere, especially those written in bash-compatible syntax. But these scripts can be complex and obscure. Complexity is the enemy of security, but it’s also the enemy of readability and understanding. With this practical book, you’ll learn how to decipher old bash code and write new code that’s as clear and readable as possible. 21 | 22 | Authors Carl Albing and JP Vossen show you how to use the power and flexibility of the shell to your advantage. You may know enough bash to get by, but this book will take your skills from manageable to magnificent. Whether you use Linux, Unix, Windows, or a Mac, you’ll learn how to read and write scripts like an expert. Your future you will thank you. You’ll explore the clear idioms to use and obscure ones to avoid, so that you can: 23 | 24 | * Write useful, flexible, and readable bash code with style 25 | * Decode bash code such as `${MAKEMELC,,}` and `${PATHNAME##*/}` 26 | * Save time and ensure consistency when automating tasks 27 | * Discover how bash idioms can make your code clean and concise 28 | 29 | 30 | ## Example Files 31 | 32 | Each sub-directory contains the important, long, or difficult-to-type examples from the relevant chapter. 33 | 34 | * `bcb2-appd.pdf` is appendix D of _bash Cookbook_ Second Edition (ISBN 978-1-491-97533-6), extracted here as a stand-alone document for ease of reference and to encourage using revisions control for your bash coding. 35 | * `appa\` is the "Bash Idioms Style Guide" from chapter 11, but without the commentary and discussion: 36 | * HTML version: `bash_idioms_style_guide.html`. 37 | * Markdown version: `bash_idioms_style_guide.md`. 38 | * 39 | * `template.sh` is a sample boilerplate or template script to copy when creating a new script. 40 | * `ch*\` examples from chapters: 41 | * `*.sh` is the actual bash code. 42 | * `*.out` in some cases, is the output as included in the book. 43 | * `ch10/ssh_config` is the sample SSH config file to create a menu used in `ch10/select-ssh.sh` and dome debugging 44 | -------------------------------------------------------------------------------- /appa/bash_idioms_style_guide.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | bash Idioms Style Guide 8 | 14 | 77 | 78 | 79 | 95 |

The bash Idioms Style Guide

96 |

This is a copy of the points in chapter 11 of bash Idioms but without the commentary and examples. There's also a Markdown file in the examples directory so you can download and tweak it as desired, then render or include it as needed using pandoc or some other tool. Get the code from the https://github.com/vossenjp/bashidioms-examples/tree/master/appa[book's GitHub page].

97 |

The bash Idioms Style Guide Is Not Portable

98 |

This bash Idioms style guide is specifically for bash, so it is not portable to POSIX, Bourne, Dash, or other shells. If you need to write for those shells, you will need to test and tweak this guide to account for the supported syntax and feature of those shells.

99 |

Be especially careful in Docker or other containers where /bin/sh is not bash and /bin/bash may not even exist! This applies to Internet of Things and other constrained environments such as industrial controllers. See "bash in Containers" in the preface and "Shebang" in chapter 9 of bash Idioms.

100 |

Readability

101 |

Readability of your code is important! Or as Python says, readability counts. You only write it once, but you (and others) will probably read it many times. Spend the extra few seconds or minutes thinking about the poor clueless person trying to read the code next year...it's very likely to be you. There's a balance and a tension between abstraction (Don't Repeat Yourself) and readability:

102 |
    103 |
  • KISS (Keep It Simple, Stupid!).
  • 104 |
  • Readability: don't be "clever," be clear.
  • 105 |
  • Good names are critical!
  • 106 |
  • Always use a header.
  • 107 |
  • If at all possible, emit something useful for -h, --help, and incorrect arguments! 108 |
      109 |
    • Prefer using a "here" document (with leading tabs) rather than a bunch of echo lines because there's less friction when you need to update and probably rewrap it later.
    • 110 |
  • 111 |
  • Use source (instead of ., which is easy to miss seeing and harder to search for) to include config files, which should end in .cfg (or .conf or whatever your standard is).
  • 112 |
  • If at all possible, use https://oreil.ly/6QyeH[ISO-8601] dates for everything.
  • 113 |
  • If at all possible, keep lists of things in alphabetical order; this prevents duplication and makes it easier to add or remove items. Examples include IP addresses (use GNU sort -V), hostnames, packages to install, case statements, and contents of variables or arrays/lists.
  • 114 |
  • If possible, use long arguments to commands for readability, e.g., use diff --quiet instead of diff -q, though watch out for portability to non-GNU/Linux systems. 115 |
      116 |
    • If any options are short or obscure, add comments.
    • 117 |
    • Strongly consider documenting why you chose or needed the options you chose, and even options you considered but didn't use for some reason.
    • 118 |
    • Definitely document any options that might seem like a good idea but that actually can cause problems, especially if you commonly use them elsewhere.
    • 119 |
  • 120 |
121 |

Comments

122 |
    123 |
  • Always use a header.
  • 124 |
  • Write your comments for the new person on the team a year from now.
  • 125 |
  • Comment your functions.
  • 126 |
  • Do not comment on what you did. Comment on why you did, or did not do, something. 127 |
      128 |
    • Exception: comment on what you did when bash itself is obscure.
    • 129 |
  • 130 |
  • Consider adding comments about external program options, especially if they are short or obscure.
  • 131 |
  • Use an initial capital on the first word of the comment, but omit ending punctuation unless the comment is more than one sentence.
  • 132 |
133 |

Names

134 |
    135 |
  • Good names are critical!
  • 136 |
  • Global variables and constants are in UPPER case. 137 |
      138 |
    • Prefer not to make changes to global variables, but sometimes that's just much simpler (KISS).
    • 139 |
    • Use readonly or declare -r for constants.
    • 140 |
  • 141 |
  • Other variables are in lowercase.
  • 142 |
  • Functions are in Mixed_Case.
  • 143 |
  • Use "_", not CamelCase, in place of space (remember, "-" is not allowed in variable names).
  • 144 |
  • Use bash arrays carefully; they can be hard to read (see chapter 7 of bash Idioms). for var in $regular_var often works as well.
  • 145 |
  • Replace $1, $2, .. $N with readable names ASAP. 146 |
      147 |
    • That makes everything much more debuggable and readable, but it also makes it easy to have defaults and add or rearrange arguments.
    • 148 |
  • 149 |
  • Distinguish between types of referents, like $input_file versus $input_dir.
  • 150 |
  • Use consistent "FIXME" and "TODO" labels, with names and ticket numbers if appropriate.
  • 151 |
152 |

Functions

153 |
    154 |
  • Always use a header.
  • 155 |
  • Good names are critical!
  • 156 |
  • Functions must be defined before they are used. 157 |
      158 |
    • Group them at the top, and use two blank lines and a function separator between each function.
    • 159 |
    • Do not intersperse code between functions!
    • 160 |
  • 161 |
  • Use Camel_Case and "_" to make function names stand out from variable names.
  • 162 |
  • Use function My_Func_Name { instead of My_Func_Name() { because it's clearer and easier to grep -P '^\s*function '.
  • 163 |
  • Each function should have comments defining what it does, inputs (including GLOBALS), and outputs.
  • 164 |
  • When you have useful, standalone pieces of code, or any time you use the same (or substantially similar) block of code more than once, make them into functions. If they are very common, like logging or emailing, consider creating a "library," that is, a single common file you can source as needed. 165 |
      166 |
    • Prefix "library" functions with "_", like _Log or some other prefix.
    • 167 |
  • 168 |
  • Consider using "filler" words for readability in arguments if it makes sense, then define them as local junk1="$2" # Unused filler, e.g.: 169 |
      170 |
    • _Create_File_If_Needed "/path/to/$file" containing 'important value'
    • 171 |
  • 172 |
  • Do use the local builtin when setting variables in functions. 173 |
      174 |
    • But be aware that successfully being "local," it will mask a failed return code, so declare and assign it on separate lines if using command substitution, like local my_input and then my_input="$(some-command)".
    • 175 |
  • 176 |
  • For any function longer than about 25 lines, close it with } # End of function MyFuncName to make it easier to track where you are in the code on your screen. For functions shorter than 25 lines, this is optional but encouraged unless it gets too cluttered.
  • 177 |
  • Don't use a main function; it's almost always just an unnecessary layer. 178 |
      179 |
    • That said, using "main" makes sense to Python and C programmers, or if the code is also used as a library, and it may be required if you do a lot of unit testing.
    • 180 |
  • 181 |
  • Consider using two blank lines and a main separator above the main section, especially when you have a lot of functions and definitions at the top.
  • 182 |
183 |

Quoting

184 |
    185 |
  • Do put quotes around variables and strings because it makes them stand out a little and clarifies your intent. 186 |
      187 |
    • Unless it gets too cluttered.
    • 188 |
    • Or they need to be unquoted for expansion.
    • 189 |
  • 190 |
  • Don't quote integers.
  • 191 |
  • Use single quotes unless interpolation is required.
  • 192 |
  • Don't use ${var} unless needed; it's too cluttered. 193 |
      194 |
    • But that is needed sometimes, like ${variable}_suffix or ${being_lower_cased,,}.
    • 195 |
  • 196 |
  • Do quote command substitutions, like var="$(command)".
  • 197 |
  • Always quote both sides of any test statement, like [[ "$foo" == 'bar' ]]. 198 |
      199 |
    • Unless one side is an integer.
    • 200 |
    • And unless you are using =~, in which case you can't quote the regular expression!
    • 201 |
  • 202 |
  • Consider single-quoting variables inside echo statements, like echo "cd to '$DIR' failed." because it's visible when a variable is unexpectedly undefined or empty. 203 |
      204 |
    • Or echo "cd to [$DIR] failed." as you like.
    • 205 |
    • If using set -u, you will get an error if the variable is not defined—but not if it is defined but is just unexpectedly empty.
    • 206 |
  • 207 |
  • Prefer single quotes around printf formats (see "POSIX output" in chapter 6 of bash Idioms and the rest of chapter 6 in general).
  • 208 |
209 |

Layout

210 |
    211 |
  • Line things up! Multiple spaces almost never matter in bash (except around =), and lining up similar commands makes it easier to read and to see both the similarities and differences.
  • 212 |
  • Do not allow trailing white space! This will later cause noise in the VCS (version control system) when removed.
  • 213 |
  • Indent using four spaces, but use TAB with here-documents as needed.
  • 214 |
  • Break long lines at around 78 columns, indent line continuations two spaces, and break just before | or > so those parts jump out as you scan down the code.
  • 215 |
  • The code to open a block goes on one line, like: 216 |
      217 |
    • if expression; then
    • 218 |
    • for expression; do
    • 219 |
  • 220 |
  • List elements in case..esac are indented four spaces, and closing ;; are at that same indent level. Blocks for each item are also indented four spaces. 221 |
      222 |
    • One-line elements should be closed with ;; on the same line.
    • 223 |
    • Prefer lining up the ) in each element, unless it gets cluttered or out of hand.
    • 224 |
    • See the example code in parselonghelp.sh.
    • 225 |
  • 226 |
227 |

Syntax

228 |
    229 |
  • Use #!/bin/bash - or #!/usr/bin/env bash when writing bash code, not #!/bin/sh.
  • 230 |
  • Use $@ unless you are really sure you need $*.
  • 231 |
  • Use == instead of = for equality, to reduce confusion with assignment.
  • 232 |
  • Use $(...) instead of `...` backticks/backquotes.
  • 233 |
  • Use [[ instead of [ (unless you need [ for portability, e.g., dash).
  • 234 |
  • Use ((...)) and $((...)) as needed for integer arithmetic; avoid let and expr.
  • 235 |
  • Use [[ expression ]] && block or [[ expression ]] || block when it is simple and readable to do so. Do not use [[ expression ]] && block || block because that doesn't do what you think it does; use if .. then .. (elif ..) else .. fi for that.
  • 236 |
  • Consider using "Unofficial bash Strict Mode" (see "Unofficial bash Strict Mode" in chapter 9 of bash Idioms). 237 |
      238 |
    • set -euo pipefail will prevent or unmask many simple errors.
    • 239 |
    • Watch out for this one, and use it carefully (if you use it at all): IFS=$'\n\t'.
    • 240 |
  • 241 |
242 |

Other

243 |
    244 |
  • For "system" scripts, log to syslog and let the OS worry about final destination(s), log rotation, etc.
  • 245 |
  • Error messages should go to STDERR, like echo 'A Bad Thing happened' 1>&2.
  • 246 |
  • Sanity-check that external tools are available using [ -x '/path/to/tool' ] || { ...error code block... }.
  • 247 |
  • Provide useful messages when things fail.
  • 248 |
  • Set exit codes, especially when you fail.
  • 249 |
250 |

Script Template

251 |
#!/bin/bash -
252 | # Or possibly: #!/usr/bin/env bash
253 | # <Name>: <description>
254 | # Original Author & date:
255 | # Current maintainer?
256 | # Copyright/License?
257 | # Where this code belongs?  (Hosts, paths, etc.)
258 | # Project/repo?
259 | # Caveats/gotchas?
260 | # Usage?  (Better to have `-h` and/or `--help` options!)
261 | # $URL$  # If using SVN
262 | ID=''    # If using SVN
263 | #_________________________________________________________________________
264 | PROGRAM=${0##*/}  # bash version of `basename`
265 | 
266 | # Unofficial bash Strict Mode?
267 | #set -euo pipefail
268 | ### CAREFUL: IFS=$'\n\t'
269 | 
270 | # GLOBAL and constant variables are in UPPER case
271 | LOG_DIR='/path/to/log/dir'
272 | 
273 | ### Consider adding argument handling to YOUR template; see:
274 | # examples/ch08/parseit.sh
275 | # examples/ch08/parselong.sh
276 | # examples/ch08/parselonghelp.sh
277 | 
278 | # Functions are in Mixed Case
279 | ###########################################################################
280 | # Define functions
281 | 
282 | #--------------------------------------------------------------------------
283 | # Example function
284 | # Globals: none
285 | # Input:   nothing
286 | # Output:  nothing
287 | function Foo {
288 |     local var1="$1"
289 |     ...
290 | } # End of function foo
291 | 
292 | 
293 | #--------------------------------------------------------------------------
294 | # Another example function
295 | # Globals: none
296 | # Input:   nothing
297 | # Output:  nothing
298 | function Bar {
299 |     local var1="$1"
300 |     ...
301 | } # End of function bar
302 | 
303 | 
304 | ###########################################################################
305 | # Main
306 | 
307 | # Code...
308 | 309 | 310 | -------------------------------------------------------------------------------- /appa/bash_idioms_style_guide.md: -------------------------------------------------------------------------------- 1 | ## The bash Idioms Style Guide 2 | 3 | This is a copy of the points in chapter 11 of _bash Idioms_ but without the commentary and examples. There's also a Markdown file in the examples directory so you can download and tweak it as desired, then render or include it as needed using `pandoc` or some other tool. Get the code from the https://github.com/vossenjp/bashidioms-examples/tree/master/appa[book's GitHub page]. 4 | 5 | 6 | ### The _bash Idioms_ Style Guide Is Not Portable 7 | 8 | This _bash Idioms_ style guide is specifically for bash, so it is not portable to POSIX, Bourne, Dash, or other shells. If you need to write for those shells, you will need to test and tweak this guide to account for the supported syntax and feature of those shells. 9 | 10 | Be especially careful in Docker or other containers where `/bin/sh` is not bash and `/bin/bash` may not even exist! This applies to Internet of Things and other constrained environments such as industrial controllers. See "bash in Containers" in the preface and "Shebang" in chapter 9 of _bash Idioms_. 11 | 12 | 13 | ### Readability 14 | 15 | Readability of your code is important! Or as Python says, _readability counts._ You only write it once, but you (and others) will probably read it many times. Spend the extra few seconds or minutes thinking about the poor clueless person trying to read the code next year...it's very likely to be you. There's a balance and a tension between abstraction (Don't Repeat Yourself) and readability: 16 | 17 | * KISS (Keep It Simple, Stupid!). 18 | * _Readability_: don't be "clever," be clear. 19 | * Good names are critical! 20 | * _Always use a header._ 21 | * If at all possible, emit something useful for `-h`, `--help`, and incorrect arguments! 22 | * Prefer using a "here" document (with leading tabs) rather than a bunch of echo lines because there's less friction when you need to update and probably rewrap it later. 23 | * Use `source` (instead of `.`, which is easy to miss seeing and harder to search for) to include config files, which should end in `.cfg` (or `.conf` or whatever your standard is). 24 | * If at all possible, use https://oreil.ly/6QyeH[ISO-8601] dates for everything. 25 | * If at all possible, keep lists of things in alphabetical order; this prevents duplication and makes it easier to add or remove items. Examples include IP addresses (use GNU `sort -V`), hostnames, packages to install, `case` statements, and contents of variables or arrays/lists. 26 | * If possible, use long arguments to commands for readability, e.g., use `diff --quiet` instead of `diff -q`, though watch out for portability to non-GNU/Linux systems. 27 | * If any options are short or obscure, add comments. 28 | * Strongly consider documenting why you chose or needed the options you chose, and even options you considered but didn't use for some reason. 29 | * Definitely document any options that might seem like a good idea but that actually can cause problems, especially if you commonly use them elsewhere. 30 | 31 | 32 | ### Comments 33 | 34 | * _Always use a header._ 35 | * Write your comments for the new person on the team a year from now. 36 | * Comment your functions. 37 | * Do not comment on what you did. Comment on why you did, or did not do, something. 38 | * Exception: comment on what you did when bash itself is obscure. 39 | * Consider adding comments about external program options, especially if they are short or obscure. 40 | * Use an initial capital on the first word of the comment, but omit ending punctuation unless the comment is more than one sentence. 41 | 42 | 43 | ### Names 44 | 45 | * Good names are critical! 46 | * Global variables and constants are in UPPER case. 47 | * Prefer not to make changes to global variables, but sometimes that's just much simpler (KISS). 48 | * Use `readonly` or `declare -r` for constants. 49 | * Other variables are in lowercase. 50 | * Functions are in Mixed_Case. 51 | * Use "_", not CamelCase, in place of space (remember, "-" is not allowed in variable names). 52 | * Use bash arrays carefully; they can be hard to read (see chapter 7 of _bash Idioms_). `for var in $regular_var` often works as well. 53 | * Replace `$1`, `$2`, .. `$N` with readable names ASAP. 54 | * That makes everything much more debuggable and readable, but it also makes it easy to have defaults and add or rearrange arguments. 55 | * Distinguish between types of referents, like `$input_file` versus `$input_dir`. 56 | * Use consistent "FIXME" and "TODO" labels, with names and ticket numbers if appropriate. 57 | 58 | 59 | ### Functions 60 | 61 | * _Always use a header._ 62 | * Good names are critical! 63 | * Functions must be defined before they are used. 64 | * Group them at the top, and use two blank lines and a function separator between each function. 65 | * Do _not_ intersperse code between functions! 66 | * Use Camel_Case and "_" to make function names stand out from variable names. 67 | * Use `function My_Func_Name {` instead of `My_Func_Name() {` because it's clearer and easier to `grep -P '^\s*function '`. 68 | * Each function should have comments defining what it does, inputs (including GLOBALS), and outputs. 69 | * When you have useful, standalone pieces of code, or any time you use the same (or substantially similar) block of code more than once, make them into functions. If they are very common, like logging or emailing, consider creating a "library," that is, a single common file you can source as needed. 70 | * Prefix "library" functions with "_", like `_Log` or some other prefix. 71 | * Consider using "filler" words for readability in arguments if it makes sense, then define them as `local junk1="$2" # Unused filler`, e.g.: 72 | * `_Create_File_If_Needed "/path/to/$file" containing 'important value'` 73 | * Do use the `local` builtin when setting variables in functions. 74 | * But be aware that successfully being "local," it will mask a failed return code, so declare and assign it on separate lines if using command substitution, like `local my_input` and then `my_input="$(some-command)"`. 75 | * For any function longer than about 25 lines, close it with `} # End of function MyFuncName` to make it easier to track where you are in the code on your screen. For functions shorter than 25 lines, this is optional but encouraged unless it gets too cluttered. 76 | * Don't use a `main` function; it's almost always just an unnecessary layer. 77 | * That said, using "main" makes sense to Python and C programmers, or if the code is also used as a library, and it may be required if you do a lot of unit testing. 78 | * Consider using two blank lines and a main separator above the main section, especially when you have a lot of functions and definitions at the top. 79 | 80 | 81 | ### Quoting 82 | 83 | * Do put quotes around variables and strings because it makes them stand out a little and clarifies your intent. 84 | * Unless it gets too cluttered. 85 | * Or they need to be unquoted for expansion. 86 | * Don't quote integers. 87 | * Use single quotes unless interpolation is required. 88 | * Don't use `${var}` unless needed; it's too cluttered. 89 | * But that _is_ needed sometimes, like `${variable}_suffix` or `${being_lower_cased,,}`. 90 | * Do quote command substitutions, like `var="$(command)"`. 91 | * _Always_ quote both sides of any test statement, like `[[ "$foo" == 'bar' ]]`. 92 | * Unless one side is an integer. 93 | * And unless you are using `=~`, in which case you can't quote the regular expression! 94 | * Consider single-quoting variables inside `echo` statements, like `` echo "cd to '$DIR' failed." `` because it's visible when a variable is unexpectedly undefined or empty. 95 | * Or `echo "cd to [$DIR] failed."` as you like. 96 | * If using `set -u`, you will get an error if the variable is not defined—but not if it is defined but is just unexpectedly empty. 97 | * Prefer single quotes around `printf` formats (see "POSIX output" in chapter 6 of _bash Idioms_ and the rest of chapter 6 in general). 98 | 99 | 100 | ### Layout 101 | 102 | * Line things up! Multiple spaces almost never matter in bash (except around `=`), and lining up similar commands makes it easier to read and to see both the similarities and differences. 103 | * _Do not allow trailing white space!_ This will later cause noise in the VCS (version control system) when removed. 104 | * Indent using four spaces, but use TAB with here-documents as needed. 105 | * Break long lines at around 78 columns, indent line continuations two spaces, and break just before `|` or `>` so those parts jump out as you scan down the code. 106 | * The code to open a block goes on one line, like: 107 | * `if expression; then` 108 | * `for expression; do` 109 | * List elements in `case..esac` are indented four spaces, and closing `;;` are at that same indent level. Blocks for each item are also indented four spaces. 110 | * One-line elements should be closed with `;;` on the same line. 111 | * Prefer lining up the `)` in each element, unless it gets cluttered or out of hand. 112 | * See the example code in [parselonghelp.sh](https://github.com/vossenjp/bashidioms-examples/tree/master/ch08/parselonghelp.sh). 113 | 114 | 115 | ### Syntax 116 | 117 | * Use `#!/bin/bash -` or `#!/usr/bin/env bash` when writing bash code, not `#!/bin/sh`. 118 | * Use `$@` unless you are _really_ sure you need `$*`. 119 | * Use `==` instead of `=` for equality, to reduce confusion with assignment. 120 | * Use `$(...)` instead of `` `...` `` backticks/backquotes. 121 | * Use `[[` instead of `[` (unless you need `[` for portability, e.g., `dash`). 122 | * Use `((...))` and `$((...))` as needed for integer arithmetic; avoid `let` and `expr`. 123 | * Use `[[ expression ]] && block` or `[[ expression ]] || block` when it is simple and readable to do so. Do not use `[[ expression ]] && block || block` because that doesn't do what you think it does; use `if .. then .. (elif ..) else .. fi` for that. 124 | * Consider using "Unofficial bash Strict Mode" (see "Unofficial bash Strict Mode" in chapter 9 of _bash Idioms_). 125 | * `set -euo pipefail` will prevent or unmask many simple errors. 126 | * Watch out for this one, and use it carefully (if you use it at all): `IFS=$'\n\t'`. 127 | 128 | 129 | ### Other 130 | 131 | * For "system" scripts, log to syslog and let the OS worry about final destination(s), log rotation, etc. 132 | * Error messages should go to STDERR, like `echo 'A Bad Thing happened' 1>&2`. 133 | * Sanity-check that external tools are available using `[ -x '/path/to/tool' ] || { ...error code block... }`. 134 | * Provide useful messages when things fail. 135 | * Set `exit` codes, especially when you fail. 136 | 137 | 138 | ### Script Template 139 | 140 | ~~~~ {.bash} 141 | #!/bin/bash - 142 | # Or possibly: #!/usr/bin/env bash 143 | # : 144 | # Original Author & date: 145 | # Current maintainer? 146 | # Copyright/License? 147 | # Where this code belongs? (Hosts, paths, etc.) 148 | # Project/repo? 149 | # Caveats/gotchas? 150 | # Usage? (Better to have `-h` and/or `--help` options!) 151 | # $URL$ # If using SVN 152 | ID='' # If using SVN 153 | #_________________________________________________________________________ 154 | PROGRAM=${0##*/} # bash version of `basename` 155 | 156 | # Unofficial bash Strict Mode? 157 | #set -euo pipefail 158 | ### CAREFUL: IFS=$'\n\t' 159 | 160 | # GLOBAL and constant variables are in UPPER case 161 | LOG_DIR='/path/to/log/dir' 162 | 163 | ### Consider adding argument handling to YOUR template; see: 164 | # examples/ch08/parseit.sh 165 | # examples/ch08/parselong.sh 166 | # examples/ch08/parselonghelp.sh 167 | 168 | # Functions are in Mixed Case 169 | ########################################################################### 170 | # Define functions 171 | 172 | #-------------------------------------------------------------------------- 173 | # Example function 174 | # Globals: none 175 | # Input: nothing 176 | # Output: nothing 177 | function Foo { 178 | local var1="$1" 179 | ... 180 | } # End of function foo 181 | 182 | 183 | #-------------------------------------------------------------------------- 184 | # Another example function 185 | # Globals: none 186 | # Input: nothing 187 | # Output: nothing 188 | function Bar { 189 | local var1="$1" 190 | ... 191 | } # End of function bar 192 | 193 | 194 | ########################################################################### 195 | # Main 196 | 197 | # Code... 198 | ~~~~ 199 | -------------------------------------------------------------------------------- /bcb2-appd.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vossenjp/bashidioms-examples/d1e940fa39b06af8d91b9abd65bda8fd60e1940f/bcb2-appd.pdf -------------------------------------------------------------------------------- /ch03/list.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # list.sh: A wrapper script for ls-related tools & simple `case..esac` demo 3 | # Original Author & date: _bash Idioms_ 2022 4 | # bash Idioms filename: examples/ch03/list.sh 5 | #_________________________________________________________________________ 6 | VERSION='v1.2b' 7 | 8 | function Usage_Exit { 9 | echo "$0 [color|last|len|long]" 10 | exit 11 | } 12 | 13 | # Show each filename preceded by the length of its name, sorted by filename 14 | # length. Note '-' is valid but uncommon in function names, but it is not 15 | # valid in variable names. We don't usually use it, but you can. 16 | function Ls-Length { 17 | ls -1 "$@" | while read fn; do 18 | printf '%3d %s\n' ${#fn} ${fn} 19 | done | sort -n 20 | } 21 | 22 | (( $# < 1 )) && Usage_Exit # <1> 23 | sub=$1 24 | shift 25 | 26 | case $sub in 27 | color) # Colorized ls 28 | ls -N --color=tty -T 0 "$@" 29 | ;; 30 | 31 | last | latest) # Latest files # <2> 32 | ls -lrt | tail "-n${1:-5}" # <3> 33 | ;; 34 | 35 | len*) # Files with name lengths # <4> 36 | Ls-Length "$@" 37 | ;; 38 | 39 | long) # File with longest name 40 | Ls-Length "$@" | tail -1 41 | ;; 42 | 43 | *) # Default 44 | echo "unknown command: $sub" 45 | Usage_Exit 46 | ;; 47 | esac 48 | -------------------------------------------------------------------------------- /ch03/wrapper.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # wrapper.sh: Simple "wrapper" script demo 3 | # Original Author & date: _bash Idioms_ 2022 4 | # bash Idioms filename: examples/ch03/wrapper.sh 5 | #_________________________________________________________________________ 6 | 7 | # Trivial Sanity Checks # <1> 8 | [ -n "$BOOK_ASC" ] || { 9 | echo "FATAL: export \$BOOK_ASC to the location of the Asciidoc files!" 10 | exit 1 11 | } 12 | \cd "$BOOK_ASC" || { 13 | echo "FATAL: can't cd to '$BOOK_ASC'!" 14 | exit 2 15 | } 16 | 17 | SELF="$0" # <2> 18 | 19 | action="$1" # <3> 20 | shift # <4> 21 | [ -x /usr/bin/xsel -a $# -lt 1 ] && { # <5> 22 | # Read/write the clipboard on Linux 23 | text=$(xsel -b) 24 | function Output { 25 | echo -en "$*" | xsel -bi 26 | } 27 | } || { 28 | # Read/write STDIN/STDOUT 29 | text=$* 30 | function Output { 31 | echo -en "$*" 32 | } 33 | } 34 | 35 | case "$action" in # <6> 36 | 37 | ####################################################################### 38 | # Content/Markup # <7> 39 | 40 | ### Headers # <8> 41 | h1 ) # Inside chapter heading 1 (really AsciiDoc h3) <9> 42 | Output "[[$($SELF id $text)]]\n=== $text" # <10> 43 | ;; 44 | h2 ) # Inside chapter heading 2 (really AsciiDoc h4) 45 | Output "[[$($SELF id $text)]]\n==== $text" 46 | ;; 47 | h3 ) # Inside chapter heading 3 (really AsciiDoc h5) 48 | Output "[[$($SELF id $text)]]\n===== $text" 49 | ;; 50 | 51 | ### Lists 52 | bul|bullet ) # Bullet list (** = level 2, + = multiline element) 53 | Output "* $text" 54 | ;; 55 | nul|number|order* ) # Numbered/ordered list (.. = level 2, + = multiline) 56 | Output ". $text" 57 | ;; 58 | term ) # Terms 59 | Output "term_here::\n $text" 60 | ;; 61 | 62 | ### Inline 63 | bold ) # Inline bold (O'Reilly prefers italics to bold) 64 | Output "*$text*" 65 | ;; 66 | i|italic*|itl ) # Inline italics (O'Reilly prefers italics to bold) 67 | Output "_${text}_" 68 | ;; 69 | c|constant|cons ) # Inline constant width (command, code, keywords, more) 70 | Output "+$text+" 71 | ;; 72 | type|constantbold ) # Inline bold constant width (user types literally) 73 | Output "*+$text+*" 74 | ;; 75 | var|constantitalic ) # Inline italic constant width (user-supplied values) 76 | Output "_++$text++_" 77 | ;; 78 | sub|subscript ) # Inline subscript 79 | Output "~$text~" 80 | ;; 81 | sup|superscript ) # Inline superscript 82 | Output "^$text^" 83 | ;; 84 | foot ) # Create a footnote 85 | Output "footnote:[$text]" 86 | ;; 87 | url|link ) # Create a URL with alternate text 88 | Output "link:\$\$$text\$\$[]" # URL[link shows as] 89 | ;; 90 | esc|escape ) # Escape a character (esp. *) 91 | Output "\$\$$text\$\$" # $$*$$ 92 | ;; 93 | 94 | 95 | ####################################################################### 96 | # Tools # <11> 97 | 98 | id ) ## Convert a hack/recipe name to an ID 99 | #us_text=${text// /_} # Space to '_' 100 | #lc_text=${us_text,,} # Lowercase; bash 4+ only! 101 | # Initial `tr -s '_' ' '` to preserve _ in case we process an ID 102 | # twice (like from "xref") 103 | # Also note you can break long lines with a trailing \ # <12> 104 | Output $(echo $text | tr -s '_' ' ' | tr '[:upper:]' '[:lower:]' \ 105 | | tr -d '[:punct:]' | tr -s ' ' '_') 106 | ;; 107 | 108 | index ) ## Creates 'index.txt' in AsciiDoc dir 109 | # Like: 110 | # ch02.asciidoc:== The Text Utils 111 | # ch02.asciidoc:=== Common Text Utils and similar tools 112 | # ch02.asciidoc:=== Finding data 113 | egrep '^=== ' ch*.asciidoc | egrep -v '^ch00.asciidoc' \ 114 | > $BOOK_ASC/index.txt && { 115 | echo "Updated: $BOOK_ASC/index.txt" 116 | exit 0 117 | } || { 118 | echo "FAILED to update: $BOOK_ASC/index.txt" 119 | exit 1 120 | } 121 | ;; 122 | 123 | rerun ) ## Run examples to re-create (existing!) output files 124 | # Only re-run for code that ALREADY HAS a *.out file...not ALL *.sh code 125 | for output in examples/*/*.out; do 126 | code=${output/out/sh} 127 | echo "Re-running code for: $code > $output" 128 | $code > $output 129 | done 130 | ;; 131 | 132 | cleanup ) ## Clean up all the xHTML/XML/PDF cruft 133 | rm -fv {ch??,app?}.{pdf,xml,html} book.{xml,html} docbook-xsl.css 134 | ;; 135 | 136 | 137 | * ) # <13> 138 | \cd - # UGLY cheat to revert the 'cd' above... 139 | ( echo "Usage:" # <14> 140 | egrep '\)[[:space:]]+# ' $0 # <15> 141 | echo '' 142 | egrep '\)[[:space:]]+## ' $0 # <16> 143 | echo '' 144 | egrep '\)[[:space:]]+### ' $0 ) | grep "${1:-.}" | more # <17> 145 | ;; 146 | 147 | esac 148 | -------------------------------------------------------------------------------- /ch04/parameter-expansion.out: -------------------------------------------------------------------------------- 1 | 2 | Say we have this string: Acme Inc subnet 10.11.12.13/24 3 | 4 | When the code runs we get: 5 | 6 | Customer name: Acme Inc 7 | Subnet: 10.11.12.13/24 8 | IPA 10.11.12.13 9 | CIDR mask: 24 10 | FW Object: acme_inc_subnet_10.11.12.13-24 11 | -------------------------------------------------------------------------------- /ch04/parameter-expansion.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # parameter-expansion.sh: parameter expansion for parsing, and a big list 3 | # Original Author & date: _bash Idioms_ 2022 4 | # bash Idioms filename: examples/ch04/parameter-expansion.sh 5 | #_________________________________________________________________________ 6 | # Does not work on Zsh 5.4.2! 7 | 8 | customer_subnet_name='Acme Inc subnet 10.11.12.13/24' 9 | 10 | echo '' 11 | echo "Say we have this string: $customer_subnet_name" 12 | 13 | customer_name=${customer_subnet_name%subnet*} # Trim from 'subnet' to end 14 | subnet=${customer_subnet_name##* } # Remove leading 'space*' 15 | ipa=${subnet%/*} # Remove trailing '/*' 16 | cidr=${subnet#*/} # Remove up to '/*' 17 | fw_object_name=${customer_subnet_name// /_} # Replace space with '_- 18 | fw_object_name=${fw_object_name////-} # Replace '/' with '-' 19 | fw_object_name=${fw_object_name,,} # Lowercase 20 | 21 | echo '' 22 | echo 'When the code runs we get:' 23 | echo '' 24 | echo "Customer name: $customer_name" 25 | echo "Subnet: $subnet" 26 | echo "IPA $ipa" 27 | echo "CIDR mask: $cidr" 28 | echo "FW Object: $fw_object_name" 29 | 30 | # bash Shell Parameter Expansion: https://oreil.ly/Af8lw 31 | 32 | # ${var#pattern} Remove shortest (nongreedy) leading pattern 33 | # ${var##pattern} Remove longest (greedy) leading pattern 34 | # ${var%pattern} Remove shortest (nongreedy) trailing pattern 35 | # ${var%%pattern} Remove longest (greedy) trailing pattern 36 | 37 | # ${var/pattern/replacement} Replace first +pattern+ with +replacement+ 38 | # ${var//pattern/replacement} Replace all +pattern+ with +replacement+ 39 | 40 | # ${var^pattern} Uppercase first matching optional pattern 41 | # ${var^^pattern} Uppercase all matching optional pattern 42 | # ${var,pattern} Lowercase first matching optional pattern 43 | # ${var,,pattern} Lowercase all matching optional pattern 44 | 45 | # ${var:offset} Substring starting at +offset+ 46 | # ${var:offset:length} Substring starting at +offset+ for +length+ 47 | 48 | # ${var:-default} Var if set, otherwise +default+ 49 | # ${var:=default} Assign +default+ to +var+ if +var+ not already set 50 | # ${var:?error_message} Barf with +error_message+ if +var+ not set 51 | # ${var:+replaced} Expand to +replaced+ if +var+ _is_ set 52 | 53 | # ${#var} Length of var 54 | # ${!var[*]} Expand to indexes or keys 55 | # ${!var[@]} Expand to indexes or keys, quoted 56 | 57 | # ${!prefix*} Expand to variable names starting with +prefix+ 58 | # ${!prefix@} Expand to variable names starting with +prefix+, quoted 59 | 60 | # ${var@Q} Quoted 61 | # ${var@E} Expanded (better than `eval`!) 62 | # ${var@P} Expanded as prompt 63 | # ${var@A} Assign or declare 64 | # ${var@a} Return attributes 65 | -------------------------------------------------------------------------------- /ch07/hashes.out: -------------------------------------------------------------------------------- 1 | 2 | The key count is: 7 or 7 3 | 4 | The length of the value of key [e] is: 4 5 | 6 | Dump or list: 7 | declare -A myhash=([a]="foo" [b]="bar" [c]="baz" [d]="three" 8 | [e]="four" [f]="five by five" [g]="six" ) 9 | ${myhash[@]} = foo|bar|baz|three|four|five|by|five|six| 10 | ${myhash[*]} = foo|bar|baz|three|four|five|by|five|six| 11 | "${myhash[@]}" = foo|bar|baz|three|four|five\ by\ five|six| 12 | "${myhash[*]}" = foo\ bar\ baz\ three\ four\ five\ by\ five\ six| # Broken! 13 | 14 | Join ',' ${myhash[@]} = foo,bar,baz,three,four,five by five,six 15 | String_Join '<>' ${myhash[@]} = foo<>bar<>baz<>three<>four<>five by five<>six 16 | foreach "${!myhash[@]}": 17 | Key: a; value: foo 18 | Key: b; value: bar 19 | Key: c; value: baz 20 | Key: d; value: three 21 | Key: e; value: four 22 | Key: f; value: five by five 23 | Key: g; value: six 24 | 25 | But don't do this: ${myhash[*]} 26 | Key: foo; value: 27 | Key: bar; value: 28 | Key: baz; value: 29 | Key: three; value: 30 | Key: four; value: 31 | Key: five; value: 32 | Key: by; value: 33 | Key: five; value: 34 | Key: six; value: 35 | 36 | Start from hash insertion element 5 and show a slice of 2 elements: 37 | four|five\ by\ five| 38 | 39 | Start from hash insertion element 0 (huh?) and show a slice of 3 elements: 40 | foo|bar|baz| 41 | 42 | Start from hash insertion element 1 and show a slice of 3 elements: 43 | foo|bar|baz| 44 | 45 | Delete key c using unset (dumped before and after): 46 | declare -A myhash=([a]="foo" [b]="bar" [c]="baz" [d]="three" 47 | [e]="four" [f]="five by five" [g]="six" ) 48 | declare -A myhash=([a]="foo" [b]="bar" [d]="three" [e]="four" 49 | [f]="five by five" [g]="six" ) 50 | -------------------------------------------------------------------------------- /ch07/hashes.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # hashes.sh: bash Hash example code 3 | # Original Author & date: _bash Idioms_ 2022 4 | # bash Idioms filename: examples/ch07/hashes.sh 5 | #_________________________________________________________________________ 6 | # Does not work on Zsh 5.4.2! 7 | 8 | # Books are not as wide as some screens! 9 | FORMAT='fmt --width 70 --split-only' 10 | 11 | # Declare a hash <1> 12 | declare -A myhash # MUST do this, or `local -A` or `readonly -A` 13 | 14 | # Assign to it, note no "+" <2> 15 | ###myhash=(bar) # Error: needs subscript when assigning associative array 16 | myhash[a]='foo' # Insertion 1, not 0...sort of 17 | myhash[b]='bar' # Insertion 2 18 | myhash[c]='baz' # Insertion 3 19 | myhash[d]='three' # 4 Different than our list example 20 | myhash[e]='four' # Insertion 5, note, not 4 21 | myhash[f]='five by five' # 6 Note spaces 22 | myhash[g]='six' # Insertion 7 23 | 24 | # OR 25 | #myhash=([a]=foo [b]=bar [c]=baz [d]="three" [e]="four" [f]="five by five" [g]="six") 26 | 27 | # Display or dump the details and values <3> 28 | echo -e "\nThe key count is: ${#myhash[@]} or ${#myhash[*]}" 29 | 30 | echo -e "\nThe length of the value of key [e] is: ${#myhash[e]}" 31 | 32 | echo -e "\nDump or list:" 33 | declare -p myhash | $FORMAT 34 | echo -n "\${myhash[@]} = " ; printf "%q|" ${myhash[@]} 35 | echo -en "\n\${myhash[*]} = " ; printf "%q|" ${myhash[*]} 36 | echo -en "\n\"\${myhash[@]}\" = " ; printf "%q|" "${myhash[@]}" 37 | echo -en "\n\"\${myhash[*]}\" = " ; printf "%q|" "${myhash[*]}" 38 | echo -e " # Broken!" # Previous line is bad and no newline 39 | # See `help printf` or chapter 6 "printf for reuse or debugging", we need 40 | # this to show the correct words: 41 | # %q quote the argument in a way that can be reused as shell input 42 | 43 | # "Join" the values <4> 44 | function Join { local IFS="$1"; shift; echo "$*"; } # One character delimiter! 45 | # Note the Join above requires "$*" and not "$@"! 46 | echo -en "\nJoin ',' \${myhash[@]} = " ; Join ',' "${myhash[@]}" 47 | function String_Join { 48 | local delimiter="$1" 49 | local first_element="$2" 50 | shift 2 51 | printf '%s' "$first_element" "${@/#/$delimiter}" 52 | # Print first element, then reuse the '%s' format to display the rest of 53 | # the elements (from the function args $@), but add a prefix of $delimiter 54 | # by "replacing" the leading empty pattern (/#) with $delimiter. 55 | } 56 | echo -n "String_Join '<>' \${myhash[@]} = " ; String_Join '<>' "${myhash[@]}" 57 | 58 | # Iterate over the keys and values <5> 59 | echo -e "\nforeach \"\${!myhash[@]}\":" 60 | for key in "${!myhash[@]}"; do 61 | echo -e "\tKey: $key; value: ${myhash[$key]}" 62 | done 63 | 64 | echo -e "\nBut don't do this: \${myhash[*]}" 65 | for key in ${myhash[*]}; do 66 | echo -e "\tKey: $key; value: ${myhash[$key]}" 67 | done 68 | 69 | # Handle slices (subsets) of the hash <6> 70 | echo -e "\nStart from hash insertion element 5 and show a slice of 2 elements:" 71 | printf "%q|" "${myhash[@]:5:2}" 72 | echo '' # No newline in above 73 | echo -e "\nStart from hash insertion element 0 (huh?) and show a slice of 3 elements:" 74 | printf "%q|" "${myhash[@]:0:3}" 75 | echo '' # No newline in above 76 | echo -e "\nStart from hash insertion element 1 and show a slice of 3 elements:" 77 | printf "%q|" "${myhash[@]:1:3}" 78 | echo '' # No newline in above 79 | 80 | #echo -e "\nShift FIRST key [0]:" = makes no sense in a hash! 81 | #echo -e "\nPop LAST key:" = makes no sense in a hash! 82 | 83 | # Delete keys <7> 84 | echo -e "\nDelete key c using unset (dumped before and after):" 85 | declare -p myhash | $FORMAT 86 | unset -v 'myhash[c]' 87 | declare -p myhash | $FORMAT 88 | 89 | # Delete the entire hash <8> 90 | unset -v myhash 91 | -------------------------------------------------------------------------------- /ch07/lists.out: -------------------------------------------------------------------------------- 1 | 2 | The element count is: 7 or 7 3 | 4 | The length of element [4] is: 4 5 | 6 | Dump or list: 7 | declare -a mylist=([0]="foo" [1]="bar" [2]="baz" [3]="three" 8 | [4]="four" [5]="five by five" [6]="six") 9 | ${mylist[@]} = foo|bar|baz|three|four|five|by|five|six| 10 | ${mylist[*]} = foo|bar|baz|three|four|five|by|five|six| 11 | "${mylist[@]}" = foo|bar|baz|three|four|five\ by\ five|six| 12 | "${mylist[*]}" = foo\ bar\ baz\ three\ four\ five\ by\ five\ six| # Broken! 13 | 14 | Join ',' ${mylist[@]} = foo,bar,baz,three,four,five by five,six 15 | String_Join '<>' ${mylist[@]} = foo<>bar<>baz<>three<>four<>five by five<>six 16 | foreach "${!mylist[@]}": 17 | Element: 0; value: foo 18 | Element: 1; value: bar 19 | Element: 2; value: baz 20 | Element: 3; value: three 21 | Element: 4; value: four 22 | Element: 5; value: five by five 23 | Element: 6; value: six 24 | 25 | But don't do this: ${mylist[*]} 26 | Element: foo; value: foo 27 | Element: bar; value: foo 28 | Element: baz; value: foo 29 | Element: three; value: foo 30 | Element: four; value: foo 31 | Element: five; value: foo 32 | Element: by; value: foo 33 | Element: five; value: foo 34 | Element: six; value: foo 35 | 36 | Start from element 5 and show a slice of 2 elements: 37 | five\ by\ five|six| 38 | 39 | Shift FIRST element [0] (dumped before and after): 40 | declare -a mylist=([0]="foo" [1]="bar" [2]="baz" [3]="three" 41 | [4]="four" [5]="five by five" [6]="six") 42 | declare -a mylist=([0]="bar" [1]="baz" [2]="three" [3]="four" 43 | [4]="five by five" [5]="six") 44 | 45 | Pop LAST element (dumped before and after): 46 | declare -a mylist=([0]="bar" [1]="baz" [2]="three" [3]="four" 47 | [4]="five by five" [5]="six") 48 | declare -a mylist=([0]="bar" [1]="baz" [2]="three" [3]="four" [4]="five by five") 49 | 50 | Delete element 2 using unset (dumped before and after): 51 | declare -a mylist=([0]="bar" [1]="baz" [2]="three" [3]="four" [4]="five by five") 52 | declare -a mylist=([0]="bar" [1]="baz" [3]="four" [4]="five by five") 53 | -------------------------------------------------------------------------------- /ch07/lists.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # lists.sh: bash list example code 3 | # Original Author & date: _bash Idioms_ 2022 4 | # bash Idioms filename: examples/ch07/lists.sh 5 | #_________________________________________________________________________ 6 | # Does not work on Zsh 5.4.2! 7 | 8 | # Books are not as wide as some screens! 9 | FORMAT='fmt --width 70 --split-only' 10 | 11 | # Declare a list <1> 12 | # declare -a mylist # Can do this, or `local -a` or `readonly -a` or: 13 | mylist[0]='foo' # This both declares and assigns to mylist[0] 14 | 15 | # OR Both declares & assigns: 16 | #mylist=(foo bar baz three four "five by five" six) 17 | 18 | # Push or assign, note the += and () <2> 19 | ###mylist=(bar) # Would overwrite mylist[0] 20 | mylist+=(bar) # mylist[1] 21 | mylist+=(baz) # mylist[2] 22 | mylist+=(three four) # mylist[3] AND mylist[4] 23 | mylist+=("five by five") # mylist[5] Note spaces and quotes 24 | mylist+=("six") # mylist[6] 25 | 26 | # OR APPEND, note the "+" and we're assuming foo was already assigned 27 | #mylist+=(bar baz three four "five by five" six) 28 | 29 | # Display or dump the values <3> 30 | echo -e "\nThe element count is: ${#mylist[@]} or ${#mylist[*]}" 31 | 32 | echo -e "\nThe length of element [4] is: ${#mylist[4]}" 33 | 34 | echo -e "\nDump or list:" 35 | declare -p mylist | $FORMAT 36 | echo -n "\${mylist[@]} = " ; printf "%q|" ${mylist[@]} 37 | echo -en "\n\${mylist[*]} = " ; printf "%q|" ${mylist[*]} 38 | echo -en "\n\"\${mylist[@]}\" = " ; printf "%q|" "${mylist[@]}" 39 | echo -en "\n\"\${mylist[*]}\" = " ; printf "%q|" "${mylist[*]}" 40 | echo -e " # Broken!" # Previous line is bad and no newline 41 | # See `help printf` or chapter 6 "printf for reuse or debugging", we need 42 | # this to show the correct words: 43 | # %q quote the argument in a way that can be reused as shell input 44 | 45 | # "Join" the values <4> 46 | function Join { local IFS="$1"; shift; echo "$*"; } # One character delimiter! 47 | # Note that the Join above requires "$*" and not "$@"! 48 | echo -en "\nJoin ',' \${mylist[@]} = "; Join ',' "${mylist[@]}" 49 | function String_Join { 50 | local delimiter="$1" 51 | local first_element="$2" 52 | shift 2 53 | printf '%s' "$first_element" "${@/#/$delimiter}" 54 | # Print first element, then reuse the '%s' format to display the rest of 55 | # the elements (from the function args $@), but add a prefix of $delimiter 56 | # by "replacing" the leading empty pattern (/#) with $delimiter. 57 | } 58 | echo -n "String_Join '<>' \${mylist[@]} = " ; String_Join '<>' "${mylist[@]}" 59 | 60 | # Iterate over the values <5> 61 | echo -e "\nforeach \"\${!mylist[@]}\":" 62 | for element in "${!mylist[@]}"; do 63 | echo -e "\tElement: $element; value: ${mylist[$element]}" 64 | done 65 | 66 | echo -e "\nBut don't do this: \${mylist[*]}" 67 | for element in ${mylist[*]}; do 68 | echo -e "\tElement: $element; value: ${mylist[$element]}" 69 | done 70 | 71 | # Handle slices (subsets) of the list, shift and pop <6> 72 | echo -e "\nStart from element 5 and show a slice of 2 elements:" 73 | printf "%q|" "${mylist[@]:5:2}" 74 | echo '' # No newline in above 75 | 76 | echo -e "\nShift FIRST element [0] (dumped before and after):" 77 | declare -p mylist | $FORMAT # Display before 78 | mylist=("${mylist[@]:1}") # First element, needs quotes 79 | #mylist=("${mylist[@]:$count}") # First #count elements 80 | declare -p mylist | $FORMAT # Display after 81 | 82 | echo -e "\nPop LAST element (dumped before and after):" 83 | declare -p mylist | $FORMAT 84 | unset -v 'mylist[-1]' # bash v4.3+ 85 | #unset -v "mylist[${#mylist[*]}-1]" # Older 86 | declare -p mylist 87 | 88 | # Delete slices <7> 89 | echo -e "\nDelete element 2 using unset (dumped before and after):" 90 | declare -p mylist 91 | unset -v 'mylist[2]' 92 | declare -p mylist 93 | 94 | # Delete the entire list <8> 95 | unset -v mylist 96 | -------------------------------------------------------------------------------- /ch07/word-count-example.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # word-count-example.sh: More examples for bash lists and hashes, and $RANDOM 3 | # Original Author & date: _bash Idioms_ 2022 4 | # bash Idioms filename: examples/ch07/word-count-example.sh 5 | #_________________________________________________________________________ 6 | # Does not work on Zsh 5.4.2! 7 | # See also: `man uniq` 8 | 9 | WORD_FILE='/tmp/words.txt' 10 | > $WORD_FILE # <1> 11 | trap "rm -f $WORD_FILE" ABRT EXIT HUP INT QUIT TERM 12 | 13 | declare -A myhash # <2> 14 | 15 | echo "Creating & reading random word list in: $WORD_FILE" 16 | 17 | # Create a list of words to use for the hash example 18 | mylist=(foo bar baz one two three four) 19 | 20 | # Loop, and randomly pick elements out of the list 21 | range="${#mylist[@]}" # <3> 22 | for ((i=0; i<35; i++)); do 23 | random_element="$(( $RANDOM % $range ))" # <4> 24 | echo "${mylist[$random_element]}" >> $WORD_FILE # <5> 25 | done 26 | 27 | # Read the word list into a hash 28 | while read line; do # <6> 29 | (( myhash[$line]++ )) # <7> 30 | done < $WORD_FILE # <8> 31 | 32 | 33 | echo -e "\nUnique words from: $WORD_FILE" # <9> 34 | for key in "${!myhash[@]}"; do 35 | echo "$key" 36 | done | sort 37 | 38 | echo -e "\nWord counts, ordered by word, from: $WORD_FILE" # <10> 39 | for key in "${!myhash[@]}"; do 40 | printf "%s\t%d\n" $key ${myhash[$key]} 41 | done | sort 42 | 43 | echo -e "\nWord counts, ordered by count, from: $WORD_FILE" # <11> 44 | for key in "${!myhash[@]}"; do 45 | printf "%s\t%d\n" $key ${myhash[$key]} 46 | done | sort -k2,2n 47 | -------------------------------------------------------------------------------- /ch07/word-count-example.txt: -------------------------------------------------------------------------------- 1 | Creating & reading random word list in: /tmp/words.txt 2 | 3 | Unique words from: /tmp/words.txt 4 | bar 5 | baz 6 | foo 7 | four 8 | one 9 | three 10 | two 11 | 12 | Word counts, ordered by word, from: /tmp/words.txt 13 | bar 7 14 | baz 6 15 | foo 4 16 | four 3 17 | one 5 18 | three 4 19 | two 6 20 | 21 | Word counts, ordered by count, from: /tmp/words.txt 22 | four 3 23 | foo 4 24 | three 4 25 | one 5 26 | baz 6 27 | two 6 28 | bar 7 29 | -------------------------------------------------------------------------------- /ch08/parseit.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # parseit.sh: Use getopts to parse these arguments 3 | # Original Author & date: _bash Idioms_ 2022 4 | # bash Idioms filename: examples/ch08/parseit.sh 5 | #_________________________________________________________________________ 6 | 7 | while getopts ':ao:v' VAL ; do # <1> 8 | case $VAL in # <2> 9 | a ) AMODE=1 ;; 10 | o ) OFILE="$OPTARG" ;; 11 | v ) VERBOSE=1 ;; 12 | : ) echo "error: no arg supplied to $OPTARG option" ;; # <3> 13 | * ) # <4> 14 | echo "error: unknown option $OPTARG" 15 | echo " valid options are: aov" 16 | ;; 17 | esac 18 | done 19 | shift $((OPTIND -1)) # <5> 20 | -------------------------------------------------------------------------------- /ch08/parselong.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # parselong.sh: Use getopts to parse these arguments, including long ones 3 | # Original Author & date: _bash Idioms_ 2022 4 | # bash Idioms filename: examples/ch08/parselong.sh 5 | #_________________________________________________________________________ 6 | # Long arguments: --amode 7 | # and --outfile filename or --outfile=filename 8 | 9 | VERBOSE=':' # Default is off (no-op) 10 | 11 | while getopts ':-:ao:v' VAL ; do # <1> 12 | case $VAL in 13 | a ) AMODE=1 ;; 14 | o ) OFILE="$OPTARG" ;; 15 | v ) VERBOSE='echo' ;; 16 | #-------------------------------------------------------- 17 | - ) # This section added to support long arguments # <2> 18 | case $OPTARG in 19 | amode ) AMODE=1 ;; 20 | outfile=* ) OFILE="${OPTARG#*=}" ;; # <3> 21 | outfile ) # <4> 22 | OFILE="${!OPTIND}" # <5> 23 | let OPTIND++ # <6> 24 | ;; 25 | verbose ) VERBOSE='echo' ;; 26 | * ) 27 | echo "unknown long argument: $OPTARG" 28 | exit 29 | ;; 30 | esac 31 | ;; 32 | #-------------------------------------------------------- 33 | : ) echo "error: no argument supplied" ;; 34 | * ) 35 | echo "error: unknown option $OPTARG" 36 | echo " valid options are: aov" 37 | ;; 38 | esac 39 | done 40 | shift $((OPTIND -1)) 41 | -------------------------------------------------------------------------------- /ch08/parselonghelp.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # parselonghelp.sh: Use getopts to parse these arguments, including long & help 3 | # Original Author & date: _bash Idioms_ 2022 4 | # bash Idioms filename: examples/ch08/parselonghelp.sh 5 | #_________________________________________________________________________ 6 | # Long arguments: --amode and --help 7 | # and --outfile filename or --outfile=filename 8 | 9 | PROGRAM=${0##*/} # bash version of `basename` 10 | VERSION="$PROGRAM v1.2.3" 11 | 12 | VERBOSE=':' # Default is off (no-op) 13 | DEBUG=':' # Default is off (no-op) 14 | 15 | function Display_Help { 16 | # Tab indents below, starting after the EoN (End-of-Note) line! 17 | cat <<-EoN 18 | This script does nothing but show help; a real script should be 19 | more exciting. 20 | usage: $PROGRAM (options) 21 | 22 | Options: 23 | -a | --amode = Enable "amode", default is off 24 | -d | --debug = Include debug output, default is off 25 | -h | --help = Show this help message and exit 26 | -o | --outfile = Send output to file instead of STDOUT 27 | -v | --verbose = Include verbose output, default is off 28 | -V | --version = Show the version and exit 29 | 30 | You can put more detail here if you need to. 31 | EoN 32 | # Tab indents above! 33 | # If we have this next line, the script will always exit after calling 34 | # Display_Help. You may or may not want that...you decide. 35 | # exit 1 # If you use this, remove the other exits after the call! 36 | } # end of function Display_Help 37 | 38 | while getopts ':-:adho:vV' VAL ; do 39 | case $VAL in 40 | # If you keep options in lexical order, they are easier to find and 41 | # you reduce the chances of a collision 42 | a ) AMODE=1 ;; 43 | d ) DEBUG='echo' ;; 44 | h ) Display_Help ; exit 1 ;; # We violated our style here 45 | o ) OFILE="$OPTARG" ;; 46 | v ) VERBOSE='echo' ;; 47 | V ) echo "$VERSION" && exit 0 ;; # We violated our style here too 48 | #-------------------------------------------------------- 49 | -) # This section added to support long arguments 50 | case $OPTARG in 51 | amode ) AMODE=1 ;; 52 | debug ) DEBUG='echo' ;; 53 | help ) 54 | Display_Help 55 | exit 1 56 | ;; 57 | outfile=* ) OFILE="${OPTARG#*=}" ;; 58 | outfile ) 59 | OFILE="${!OPTIND}" 60 | let OPTIND++ 61 | ;; 62 | verbose ) VERBOSE='echo' ;; 63 | version ) 64 | echo "$VERSION" 65 | exit 0 66 | ;; 67 | * ) 68 | echo "unknown long argument: $OPTARG" 69 | exit 70 | ;; 71 | esac 72 | ;; 73 | #-------------------------------------------------------- 74 | : ) echo "error: no argument supplied" ;; 75 | * ) 76 | echo "error: unknown option $OPTARG" 77 | echo " valid options are: aov" 78 | ;; 79 | esac 80 | done 81 | shift $((OPTIND -1)) 82 | 83 | echo "Code for $0 goes here." 84 | 85 | $VERBOSE 'Example verbose message...' 86 | $DEBUG 'Example DEBUG message...' 87 | 88 | echo "End of $PROGRAM run." 89 | -------------------------------------------------------------------------------- /ch09/fancy_mapfile.out: -------------------------------------------------------------------------------- 1 | 2 | Nodes to process: 10 3 | Batch size and count: 4 / 2 4 | Sleep seconds per node: 1 5 | Sleep seconds per batch: 1 6 | 7 | node 0: node0 8 | node 1: node1 9 | node 2: node2 10 | node 3: node3 11 | Completed 4 of 10 nodes; batch 1 of 2; sleeping for 1 seconds... 12 | node 4: node4 13 | node 5: node5 14 | node 6: node6 15 | node 7: node7 16 | Completed 8 of 10 nodes; batch 2 of 2; sleeping for 1 seconds... 17 | node 8: node8 18 | node 9: node9 19 | -------------------------------------------------------------------------------- /ch09/fancy_mapfile.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # fancy_mapfile.sh: Fancy `mapfile` example 3 | # Original Author & date: _bash Idioms_ 2022 4 | # bash Idioms filename: examples/ch09/fancy_mapfile.sh 5 | #_________________________________________________________________________ 6 | # Does not work on Zsh 5.4.2! 7 | 8 | HOSTS_FILE='/tmp/nodes.txt' 9 | 10 | # Create test file # <1> 11 | > $HOSTS_FILE 12 | for n in node{0..9}; do echo "$n" >> $HOSTS_FILE; done 13 | 14 | ### ADJUSTABLE VARIABLES 15 | #BATCH_SIZE=0 # Do the entire file at once (default); watch out for memory 16 | BATCH_SIZE=4 17 | SLEEP_SECS_PER_NODE=1 # Can set to 0 18 | SLEEP_SECS_PER_BATCH=1 # Set to zero if `BATCH_SIZE=0`! 19 | 20 | # Display runtime feedback to STDERR (so STDOUT can go into `tee` or a file) 21 | node_count="$(wc -l < $HOSTS_FILE)" # <2> 22 | batch_count="$(( node_count / BATCH_SIZE ))" # <3> 23 | echo '' 1>&2 24 | echo "Nodes to process: $node_count" 1>&2 25 | echo "Batch size and count: $BATCH_SIZE / $batch_count" 1>&2 # <4> 26 | echo "Sleep seconds per node: $SLEEP_SECS_PER_NODE" 1>&2 27 | echo "Sleep seconds per batch: $SLEEP_SECS_PER_BATCH" 1>&2 28 | echo '' 1>&2 29 | 30 | node_counter=0 31 | batch_counter=0 32 | # While we're reading data... && there is still data in $HOSTS_FILE 33 | while mapfile -t -n $BATCH_SIZE nodes && ((${#nodes[@]})); do 34 | for node in ${nodes[@]}; do # <5> 35 | echo "node $(( node_counter++ )): $node" 36 | sleep $SLEEP_SECS_PER_NODE 37 | done 38 | (( batch_counter++ )) 39 | # Don't get stuck here AFTER the last (partial) batch... 40 | [ "$node_counter" -lt "$node_count" ] && { 41 | # You can also use `mapfile -C Call_Back -c $BATCH_SIZE` for feedback but 42 | # it runs the callback up front too, so if you have a delay you'll 43 | # have to wait for that 44 | echo "Completed $node_counter of $node_count nodes;" \ 45 | "batch $batch_counter of $batch_count;" \ 46 | "sleeping for $SLEEP_SECS_PER_BATCH seconds..." 1>&2 47 | sleep $SLEEP_SECS_PER_BATCH 48 | } 49 | done < $HOSTS_FILE 50 | -------------------------------------------------------------------------------- /ch09/fiddle-ifs.out: -------------------------------------------------------------------------------- 1 | Normal $IFS and `read` operation; split into words: 2 | $IFS before: $' \t\n' 3 | IFS during: $' \t\n' line = line1, w1 = wd1, w2 = wd2, w3 = wd3 4 | IFS during: $' \t\n' line = line2, w1 = wd1, w2 = wd2, w3 = wd3 5 | IFS during: $' \t\n' line = line3, w1 = wd1, w2 = wd2, w3 = wd3 6 | IFS after: $' \t\n' 7 | 8 | Temporary $IFS change for `read` inline: 9 | Words are NOT split, yet $IFS appears unchanged, because only the read 10 | line has the changed $IFS. We also shortened "line" to "ln" to make 11 | it fit a book page. 12 | IFS before: $' \t\n' 13 | IFS during: $' \t\n' ln = line1\ wd1\ wd2\ wd3, w1 = '', w2 = '', w3 = '' 14 | IFS during: $' \t\n' ln = line2\ wd1\ wd2\ wd3, w1 = '', w2 = '', w3 = '' 15 | IFS during: $' \t\n' ln = line3\ wd1\ wd2\ wd3, w1 = '', w2 = '', w3 = '' 16 | IFS after: $' \t\n' 17 | 18 | Temporary $IFS change for `read` in a function; NOT split, $IFS changed: 19 | IFS before: $' \t\n' 20 | IFS during: '' line = line1\ wd1\ wd2\ wd3, w1 = '', w2 = '', w3 = '' 21 | IFS during: '' line = line2\ wd1\ wd2\ wd3, w1 = '', w2 = '', w3 = '' 22 | IFS during: '' line = line3\ wd1\ wd2\ wd3, w1 = '', w2 = '', w3 = '' 23 | IFS after: $' \t\n' 24 | 25 | But you may not need to change $IFS at all... See `help read` and 26 | note the parts about: 27 | ...leftover words assigned to the last NAME 28 | ...[read line until] DELIM is read, rather than newline 29 | Normal $IFS and `read` operation using only 1 variable: 30 | IFS before: $' \t\n' 31 | IFS during: $' \t\n' line = line1\ wd1\ wd2\ wd3 32 | IFS during: $' \t\n' line = line2\ wd1\ wd2\ wd3 33 | IFS during: $' \t\n' line = line3\ wd1\ wd2\ wd3 34 | IFS after: $' \t\n' 35 | -------------------------------------------------------------------------------- /ch09/fiddle-ifs.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # fiddle-ifs.sh: Fiddling with $IFS for fun and profit, to read files 3 | # Original Author & date: _bash Idioms_ 2022 4 | # bash Idioms filename: examples/ch09/fiddle-ifs.sh 5 | #_________________________________________________________________________ 6 | 7 | # Create test file (not spelling out "word" to keep output < 80 columns) 8 | IFS_TEST_FILE='/tmp/ifs-test.txt' 9 | cat <<'EoF' > $IFS_TEST_FILE 10 | line1 wd1 wd2 wd3 11 | line2 wd1 wd2 wd3 12 | line3 wd1 wd2 wd3 13 | EoF 14 | 15 | 16 | #-------------------------------------------------------------------------- 17 | echo 'Normal $IFS and `read` operation; split into words:' 18 | printf '$IFS before: %q\n' "$IFS" 19 | while read line w1 w2 w3; do 20 | printf 'IFS during: %q\tline = %q, w1 = %q, w2 = %q, w3 = %q\n' \ 21 | "$IFS" "$line" "$w1" "$w2" "$w3" 22 | done < $IFS_TEST_FILE 23 | printf 'IFS after: %q\n' "$IFS" 24 | 25 | 26 | #-------------------------------------------------------------------------- 27 | echo '' 28 | echo 'Temporary $IFS change for `read` inline:' 29 | echo 'Words are NOT split, yet $IFS appears unchanged, because only the read' 30 | echo 'line has the changed $IFS. We also shortened "line" to "ln" to make' 31 | echo 'it fit a book page.' 32 | printf 'IFS before: %q\n' "$IFS" 33 | while IFS='' read line w1 w2 w3; do 34 | printf 'IFS during: %q\tln = %q, w1 = %q, w2 = %q, w3 = %q\n' \ 35 | "$IFS" "$line" "$w1" "$w2" "$w3" 36 | done < $IFS_TEST_FILE 37 | printf 'IFS after: %q\n' "$IFS" 38 | 39 | 40 | #-------------------------------------------------------------------------- 41 | function Read_A_File { 42 | local file="$1" 43 | local IFS='' 44 | 45 | while read line w1 w2 w3; do 46 | printf 'IFS during: %q\tline = %q, w1 = %q, w2 = %q, w3 = %q\n' \ 47 | "$IFS" "$line" "$w1" "$w2" "$w3" 48 | done < "$file" 49 | } 50 | 51 | echo '' 52 | echo 'Temporary $IFS change for `read` in a function; NOT split, $IFS changed:' 53 | printf 'IFS before: %q\n' "$IFS" 54 | Read_A_File $IFS_TEST_FILE 55 | printf 'IFS after: %q\n' "$IFS" 56 | 57 | 58 | #-------------------------------------------------------------------------- 59 | echo '' 60 | echo 'But you may not need to change $IFS at all... See `help read` and' 61 | echo 'note the parts about:' 62 | echo ' ...leftover words assigned to the last NAME' 63 | echo ' ...[read line until] DELIM is read, rather than newline' 64 | echo 'Normal $IFS and `read` operation using only 1 variable:' 65 | printf 'IFS before: %q\n' "$IFS" 66 | while read line; do 67 | printf 'IFS during: %q\tline = %q\n' "$IFS" "$line" 68 | done < $IFS_TEST_FILE 69 | printf 'IFS after: %q\n' "$IFS" 70 | -------------------------------------------------------------------------------- /ch09/shebang-bash.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash - 2 | : 3 | -------------------------------------------------------------------------------- /ch09/shebang-env.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | : 3 | -------------------------------------------------------------------------------- /ch09/trivial_trap.out: -------------------------------------------------------------------------------- 1 | Starting script examples/ch09/trivial_trap.sh... 2 | Setting the trap... 3 | About to exit... 4 | Cleanup was triggered! Cleaning up... 5 | -------------------------------------------------------------------------------- /ch09/trivial_trap.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # trivial_trap.sh: Trivial bash trap example 3 | # Original Author & date: _bash Idioms_ 2022 4 | # bash Idioms filename: examples/ch09/trivial_trap.sh 5 | #_________________________________________________________________________ 6 | # Does not work on Zsh 5.4.2! 7 | 8 | function Cleanup { 9 | echo "$FUNCNAME was triggered! Cleaning up..." 10 | } 11 | 12 | echo "Starting script $0..." 13 | 14 | echo 'Setting the trap...' 15 | # Will call Cleanup on any of these 6 signals 16 | trap Cleanup ABRT EXIT HUP INT QUIT TERM 17 | 18 | echo 'About to exit...' 19 | -------------------------------------------------------------------------------- /ch10/embedded-docs.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # embedded-docs.sh: Example of bash code with various kinds of embedded docs 3 | # Original Author & date: _bash Idioms_ 2022 4 | # bash Idioms filename: examples/ch10/embedded-docs.sh 5 | #_________________________________________________________________________ 6 | # Does not work on Zsh 5.4.2! 7 | 8 | [[ "$1" == '--emit-docs' ]] && { 9 | # Use the Perl "range" operator to print only the lines BETWEEN the bash 10 | # here-document "DOCS" markers, excluding when we talk about this code 11 | # below in the docs themselves. See the output for more. 12 | perl -ne "s/^\t+//; print if m/DOCS'?\$/ .. m/^\s*'?DOCS'?\$/ \ 13 | and not m/DOCS'?$/;" $0 14 | exit 0 15 | } 16 | 17 | echo 'Code, code, code... <2>' 18 | echo 'Code, code, code...' 19 | : << 'DOCS' 20 | === Docs for My Script <7> 21 | 22 | Ignore the callout in this title; it only makes sense in the output later. 23 | 24 | Docs can be Markdown, AsciiDoc, Textile, whatever. This block is generic markup. 25 | 26 | We've wrapped them using the no-op ':' operator and a here-doc, but you 27 | have to remember, it's not bash that's processing the here-docs, so using 28 | <<-DOC for indenting will not work. Not quoting your marker will not allow 29 | variable interpolation either, or rather, it will, but that won't affect your 30 | documentation output. So always quote your here-doc marker so your docs do 31 | not interfere with your script (e.g., via the backticks we'll use below). 32 | 33 | All of your docs could be grouped near the top of the file, like this, 34 | for discoverability. Or they could all be at the bottom, to stay grouped 35 | but out of the way of the code. Or they could be interspersed to stay near 36 | the relevant code. Do whatever makes sense to you and your team. 37 | 38 | DOCS 39 | echo 'More code, code, code... <3>' 40 | echo 'More code, code, code...' 41 | : << 'DOCS' 42 | =head1 POD Example <8> 43 | 44 | Ignore the callout in this title; it only makes sense in the output later. 45 | 46 | This block is Perl POD (Plain Old Documentation). 47 | 48 | If you use POD, you can then use `perldoc` and the various `pod2*` tools, 49 | like `pod2html`, that handle that. But you can't indent if using POD, or 50 | Perl won't see the markup, unless you preprocess the indent away before 51 | feeding the POD tools. 52 | 53 | And don't forget the `=cut` line! 54 | 55 | =cut 56 | 57 | DOCS 58 | echo 'Still more code, code, code... <4>' 59 | echo 'Still more code, code, code...' 60 | : << 'DOCS' 61 | Emitting Documentation <9> 62 | ---------------------- 63 | 64 | Ignore the callout in this title; it only makes sense in the output later. 65 | 66 | This could be POD or markup, whatever. 67 | 68 | This section uses a TAB indented here-doc, just because we can, but we 69 | handle that in the Perl post processor, not via bash. :-/ 70 | 71 | You should add a "handler" to your argument processing/help options to emit 72 | your docs. If you use POD, use those tools, but make sure they are installed! 73 | If you use some other markup, you have to extract it yourself somehow. 74 | 75 | We know this is a bash book, but this Perl one-liner using regular 76 | expressions and the range operator is really handy: 77 | perl -ne "s/^\t+//; print if m/DOCS'?\$/ .. m/^\s*'?DOCS'?\$/ \ 78 | and not m/DOCS'?$/;" $0 79 | 80 | That will start printing lines when it matches the regular expression 81 | m/DOCS'?$/, and stop printing when it matches m/^\s*'?DOCS'?\$/, except that 82 | it won't print the actual line containing m/DOCS/ at all. 83 | 84 | Add that to your argument processing as "emit documentation." 85 | 86 | DOCS 87 | 88 | echo 'End of code... <5>' 89 | 90 | exit 0 # Unless we already exited >0 above 91 | 92 | : << 'DOCS' 93 | h2. More Docs AFTER the code <10> 94 | 95 | Ignore the callout in this title; it only makes sense in the output later. 96 | 97 | This block is back to generic markup. We do *not* recommend mixing and 98 | matching like we've done here! Pick a markup and some tools and stick to 99 | them. If in doubt, GitHub has made Markdown _very_ popular. 100 | 101 | Docs can just go *after* the end of the code. There's an argument for putting 102 | all the docs together in one place at the top or bottom of the script. This 103 | makes the bottom easy. On the other hand, there's an argument for keeping 104 | the docs _close_ to the relevant code, especially for functions. So...your call. 105 | 106 | But if this section only has an `exit 0` above it and is not wrapped in a 107 | bogo-here-doc, this might cause some syntax highlighters to be unhappy, and our 108 | Perl doc emitter will miss it, so you have to find a different way to 109 | display the docs. 110 | DOCS 111 | -------------------------------------------------------------------------------- /ch10/run-embedded-docs.out: -------------------------------------------------------------------------------- 1 | ### Running looks like this (`./embedded-docs.sh`): <1> 2 | Code, code, code... <2> 3 | Code, code, code... 4 | More code, code, code... <3> 5 | More code, code, code... 6 | Still more code, code, code... <4> 7 | Still more code, code, code... 8 | End of code... <5> 9 | 10 | 11 | ### Emitting (un-rendered) docs looks like this (`./embedded-docs.sh --emit-docs`): <6> 12 | === Docs for My Script <7> 13 | 14 | Ignore the callout in this title; it only makes sense in the output later. 15 | 16 | Docs can be Markdown, AsciiDoc, Textile, whatever. This block is generic markup. 17 | 18 | We've wrapped them using the no-op ':' operator and a here-doc, but you 19 | have to remember, it's not bash that's processing the here-docs, so using 20 | <<-DOC for indenting will not work. Not quoting your marker will not allow 21 | variable interpolation either, or rather, it will, but that won't affect your 22 | documentation output. So always quote your here-doc marker so your docs do 23 | not interfere with your script (e.g., via the backticks we'll use below). 24 | 25 | All of your docs could be grouped near the top of the file, like this, 26 | for discoverability. Or they could all be at the bottom, to stay grouped 27 | but out of the way of the code. Or they could be interspersed to stay near 28 | the relevant code. Do whatever makes sense to you and your team. 29 | 30 | =head1 POD Example <8> 31 | 32 | Ignore the callout in this title; it only makes sense in the output later. 33 | 34 | This block is Perl POD (Plain Old Documentation). 35 | 36 | If you use POD, you can then use `perldoc` and the various `pod2*` tools, 37 | like `pod2html`, that handle that. But you can't indent if using POD, or 38 | Perl won't see the markup, unless you preprocess the indent away before 39 | feeding the POD tools. 40 | 41 | And don't forget the `=cut` line! 42 | 43 | =cut 44 | 45 | Emitting Documentation <9> 46 | ---------------------- 47 | 48 | Ignore the callout in this title; it only makes sense in the output later. 49 | 50 | This could be POD or markup, whatever. 51 | 52 | This section uses a TAB indented here-doc, just because we can, but we 53 | handle that in the Perl post processor, not via bash. :-/ 54 | 55 | You should add a "handler" to your argument processing/help options to emit 56 | your docs. If you use POD, use those tools, but make sure they are installed! 57 | If you use some other markup, you have to extract it yourself somehow. 58 | 59 | We know this is a bash book, but this Perl one-liner using regular 60 | expressions and the range operator is really handy: 61 | perl -ne "s/^\t+//; print if m/DOCS'?\$/ .. m/^\s*'?DOCS'?\$/ \ 62 | and not m/DOCS'?$/;" $0 63 | 64 | That will start printing lines when it matches the regular expression 65 | m/DOCS'?$/, and stop printing when it matches m/^\s*'?DOCS'?\$/, except that 66 | it won't print the actual line containing m/DOCS/ at all. 67 | 68 | Add that to your argument processing as "emit documentation." 69 | 70 | h2. More Docs AFTER the code <10> 71 | 72 | Ignore the callout in this title; it only makes sense in the output later. 73 | 74 | This block is back to generic markup. We do *not* recommend mixing and 75 | matching like we've done here! Pick a markup and some tools and stick to 76 | them. If in doubt, GitHub has made Markdown _very_ popular. 77 | 78 | Docs can just go *after* the end of the code. There's an argument for putting 79 | all the docs together in one place at the top or bottom of the script. This 80 | makes the bottom easy. On the other hand, there's an argument for keeping 81 | the docs _close_ to the relevant code, especially for functions. So...your call. 82 | 83 | But if this section only has an `exit 0` above it and is not wrapped in a 84 | bogo-here-doc, this might cause some syntax highlighters to be unhappy, and our 85 | Perl doc emitter will miss it, so you have to find a different way to 86 | display the docs. 87 | -------------------------------------------------------------------------------- /ch10/run-embedded-docs.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # run-embedded-docs.sh: Wrapper for automatic BI example code output updater 3 | # Original Author & date: _bash Idioms_ 2022 4 | # bash Idioms filename: examples/ch10/run-embedded-docs.sh 5 | #_________________________________________________________________________ 6 | 7 | OUTPUT_INCLUDE_FILE='examples/ch10/embedded-docs.out' 8 | 9 | # We *could* send all STDOUT from this script to the output file like this: 10 | #exec > $OUTPUT_INCLUDE_FILE 11 | # We do not do that here because our "example code output updater" already 12 | # does itself, the way we want it to. 13 | 14 | echo '### Running looks like this (`./embedded-docs.sh`): <1>' # <1> 15 | examples/ch10/embedded-docs.sh 16 | 17 | echo -e "\n" 18 | echo '### Emitting (un-rendered) docs looks like this (`./embedded-docs.sh --emit-docs`): <6>' # <6> 19 | examples/ch10/embedded-docs.sh --emit-docs 20 | -------------------------------------------------------------------------------- /ch10/select-ssh.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # select-ssh.sh: Create a menu from ~/.ssh/config to set ssh_target, 3 | # then SSH to it 4 | # Original Author & date: _bash Idioms_ 2022 5 | # bash Idioms filename: examples/ch10/select-ssh.sh 6 | #_________________________________________________________________________ 7 | 8 | #ssh_config="$HOME/.ssh/config" # Real one 9 | # Replace the trailing 'select-ssh.sh' with 'ssh_config' 10 | ssh_config="${0%/*}/ssh_config" # Idioms test file 11 | 12 | PS3='SSH to> ' 13 | select ssh_target in Exit \ 14 | $(egrep -i '^Host \w+' "$ssh_config" | cut -d' ' -f2-); do 15 | case $REPLY in 16 | 1|q|x|quit|exit) exit 0 17 | ;; 18 | *) break 19 | ;; 20 | esac 21 | done 22 | 23 | # This is only an example, so echo what we would have done 24 | echo ssh $ssh_target 25 | -------------------------------------------------------------------------------- /ch10/ssh_config: -------------------------------------------------------------------------------- 1 | # ssh_config: SAMPLE SSH config file to create a menu from ~/.ssh/config 2 | # Original Author & date: _bash Idioms_ 2022 3 | # bash Idioms filename: examples/ch10/ssh_config 4 | #_________________________________________________________________________ 5 | 6 | Host git.atlas.oreilly.com 7 | User foo 8 | 9 | Host gitlab.com 10 | User bar 11 | IdentityFile ~/.ssh/id_gitlab 12 | IdentitiesOnly yes 13 | 14 | Host github.com 15 | User baz 16 | IdentityFile ~/.ssh/id_github 17 | IdentitiesOnly yes 18 | 19 | host mythtv-be01 20 | User myth 21 | 22 | host kitchen 23 | User shared 24 | -------------------------------------------------------------------------------- /template.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash - 2 | # Or possibly: #!/usr/bin/env bash 3 | # : 4 | # Original Author & date: 5 | # Current maintainer? 6 | # Copyright/License? 7 | # Where this code belongs? (Hosts, paths, etc.) 8 | # Project/repo? 9 | # Caveats/gotchas? 10 | # Usage? (Better to have `-h` and/or `--help` options!) 11 | # $URL$ # If using SVN 12 | ID='' # If using SVN 13 | #_________________________________________________________________________ 14 | PROGRAM=${0##*/} # bash version of `basename` 15 | 16 | # Unofficial bash Strict Mode? 17 | #set -euo pipefail 18 | ### CAREFUL: IFS=$'\n\t' 19 | 20 | # GLOBAL and constant variables are in UPPER case 21 | LOG_DIR='/path/to/log/dir' 22 | 23 | ### Consider adding argument handling to YOUR template; see: 24 | # examples/ch08/parseit.sh 25 | # examples/ch08/parselong.sh 26 | # examples/ch08/parselonghelp.sh 27 | 28 | # Functions are in Mixed Case 29 | ########################################################################### 30 | # Define functions 31 | 32 | #-------------------------------------------------------------------------- 33 | # Example function 34 | # Globals: none 35 | # Input: nothing 36 | # Output: nothing 37 | function Foo { 38 | local var1="$1" 39 | ... 40 | } # End of function foo 41 | 42 | 43 | #-------------------------------------------------------------------------- 44 | # Another example function 45 | # Globals: none 46 | # Input: nothing 47 | # Output: nothing 48 | function Bar { 49 | local var1="$1" 50 | ... 51 | } # End of function bar 52 | 53 | 54 | ########################################################################### 55 | # Main 56 | 57 | # Code... 58 | --------------------------------------------------------------------------------