OSDN Git Service

Regular updates
[twpd/master.git] / bash.md
1 ---
2 title: Bash scripting
3 category: CLI
4 layout: 2017/sheet
5 tags: [Featured]
6 updated: 2020-07-05
7 keywords:
8   - Variables
9   - Functions
10   - Interpolation
11   - Brace expansions
12   - Loops
13   - Conditional execution
14   - Command substitution
15 ---
16
17 ## Getting started
18
19 {: .-three-column}
20
21 ### Introduction
22
23 {: .-intro}
24
25 This is a quick reference to getting started with Bash scripting.
26
27 - [Learn bash in y minutes](https://learnxinyminutes.com/docs/bash/) _(learnxinyminutes.com)_
28 - [Bash Guide](http://mywiki.wooledge.org/BashGuide) _(mywiki.wooledge.org)_
29 - [Bash Hackers Wiki](https://web.archive.org/web/20230406205817/https://wiki.bash-hackers.org/) _(wiki.bash-hackers.org)_
30
31 ### Example
32
33 ```bash
34 #!/usr/bin/env bash
35
36 name="John"
37 echo "Hello $name!"
38 ```
39
40 ### Variables
41
42 ```bash
43 name="John"
44 echo $name  # see below
45 echo "$name"
46 echo "${name}!"
47 ```
48
49 Generally quote your variables unless they contain wildcards to expand or command fragments.
50
51 ```bash
52 wildcard="*.txt"
53 options="iv"
54 cp -$options $wildcard /tmp
55 ```
56
57 ### String quotes
58
59 ```bash
60 name="John"
61 echo "Hi $name"  #=> Hi John
62 echo 'Hi $name'  #=> Hi $name
63 ```
64
65 ### Shell execution
66
67 ```bash
68 echo "I'm in $(pwd)"
69 echo "I'm in `pwd`"  # obsolescent
70 # Same
71 ```
72
73 See [Command substitution](https://web.archive.org/web/20230326081741/https://wiki.bash-hackers.org/syntax/expansion/cmdsubst)
74
75 ### Conditional execution
76
77 ```bash
78 git commit && git push
79 git commit || echo "Commit failed"
80 ```
81
82 ### Functions
83
84 {: id='functions-example'}
85
86 ```bash
87 get_name() {
88   echo "John"
89 }
90
91 echo "You are $(get_name)"
92 ```
93
94 See: [Functions](#functions)
95
96 ### Conditionals
97
98 {: id='conditionals-example'}
99
100 ```bash
101 if [[ -z "$string" ]]; then
102   echo "String is empty"
103 elif [[ -n "$string" ]]; then
104   echo "String is not empty"
105 fi
106 ```
107
108 See: [Conditionals](#conditionals)
109
110 ### Strict mode
111
112 ```bash
113 set -euo pipefail
114 IFS=$'\n\t'
115 ```
116
117 See: [Unofficial bash strict mode](http://redsymbol.net/articles/unofficial-bash-strict-mode/)
118
119 ### Brace expansion
120
121 ```bash
122 echo {A,B}.js
123 ```
124
125 | Expression             | Description           |
126 | ---------------------- | --------------------- |
127 | `{A,B}`                | Same as `A B`         |
128 | `{A,B}.js`             | Same as `A.js B.js`   |
129 | `{1..5}`               | Same as `1 2 3 4 5`   |
130 | <code>&lcub;{1..3},{7..9}}</code> | Same as `1 2 3 7 8 9` |
131
132 See: [Brace expansion](https://web.archive.org/web/20230207192110/https://wiki.bash-hackers.org/syntax/expansion/brace)
133
134 ## Parameter expansions
135
136 {: .-three-column}
137
138 ### Basics
139
140 ```bash
141 name="John"
142 echo "${name}"
143 echo "${name/J/j}"    #=> "john" (substitution)
144 echo "${name:0:2}"    #=> "Jo" (slicing)
145 echo "${name::2}"     #=> "Jo" (slicing)
146 echo "${name::-1}"    #=> "Joh" (slicing)
147 echo "${name:(-1)}"   #=> "n" (slicing from right)
148 echo "${name:(-2):1}" #=> "h" (slicing from right)
149 echo "${food:-Cake}"  #=> $food or "Cake"
150 ```
151
152 ```bash
153 length=2
154 echo "${name:0:length}"  #=> "Jo"
155 ```
156
157 See: [Parameter expansion](https://web.archive.org/web/20230408142504/https://wiki.bash-hackers.org/syntax/pe)
158
159 ```bash
160 str="/path/to/foo.cpp"
161 echo "${str%.cpp}"    # /path/to/foo
162 echo "${str%.cpp}.o"  # /path/to/foo.o
163 echo "${str%/*}"      # /path/to
164
165 echo "${str##*.}"     # cpp (extension)
166 echo "${str##*/}"     # foo.cpp (basepath)
167
168 echo "${str#*/}"      # path/to/foo.cpp
169 echo "${str##*/}"     # foo.cpp
170
171 echo "${str/foo/bar}" # /path/to/bar.cpp
172 ```
173
174 ```bash
175 str="Hello world"
176 echo "${str:6:5}"   # "world"
177 echo "${str: -5:5}"  # "world"
178 ```
179
180 ```bash
181 src="/path/to/foo.cpp"
182 base=${src##*/}   #=> "foo.cpp" (basepath)
183 dir=${src%$base}  #=> "/path/to/" (dirpath)
184 ```
185
186 ### Substitution
187
188 | Code              | Description         |
189 | ----------------- | ------------------- |
190 | `${foo%suffix}`   | Remove suffix       |
191 | `${foo#prefix}`   | Remove prefix       |
192 | ---               | ---                 |
193 | `${foo%%suffix}`  | Remove long suffix  |
194 | `${foo/%suffix}`  | Remove long suffix  |
195 | `${foo##prefix}`  | Remove long prefix  |
196 | `${foo/#prefix}`  | Remove long prefix  |
197 | ---               | ---                 |
198 | `${foo/from/to}`  | Replace first match |
199 | `${foo//from/to}` | Replace all         |
200 | ---               | ---                 |
201 | `${foo/%from/to}` | Replace suffix      |
202 | `${foo/#from/to}` | Replace prefix      |
203
204 ### Comments
205
206 ```bash
207 # Single line comment
208 ```
209
210 ```bash
211 : '
212 This is a
213 multi line
214 comment
215 '
216 ```
217
218 ### Substrings
219
220 | Expression      | Description                    |
221 | --------------- | ------------------------------ |
222 | `${foo:0:3}`    | Substring _(position, length)_ |
223 | `${foo:(-3):3}` | Substring from the right       |
224
225 ### Length
226
227 | Expression | Description      |
228 | ---------- | ---------------- |
229 | `${#foo}`  | Length of `$foo` |
230
231 ### Manipulation
232
233 ```bash
234 str="HELLO WORLD!"
235 echo "${str,}"   #=> "hELLO WORLD!" (lowercase 1st letter)
236 echo "${str,,}"  #=> "hello world!" (all lowercase)
237
238 str="hello world!"
239 echo "${str^}"   #=> "Hello world!" (uppercase 1st letter)
240 echo "${str^^}"  #=> "HELLO WORLD!" (all uppercase)
241 ```
242
243 ### Default values
244
245 | Expression        | Description                                              |
246 | ----------------- | -------------------------------------------------------- |
247 | `${foo:-val}`     | `$foo`, or `val` if unset (or null)                      |
248 | `${foo:=val}`     | Set `$foo` to `val` if unset (or null)                   |
249 | `${foo:+val}`     | `val` if `$foo` is set (and not null)                    |
250 | `${foo:?message}` | Show error message and exit if `$foo` is unset (or null) |
251
252 Omitting the `:` removes the (non)nullity checks, e.g. `${foo-val}` expands to `val` if unset otherwise `$foo`.
253
254 ## Loops
255
256 {: .-three-column}
257
258 ### Basic for loop
259
260 ```bash
261 for i in /etc/rc.*; do
262   echo "$i"
263 done
264 ```
265
266 ### C-like for loop
267
268 ```bash
269 for ((i = 0 ; i < 100 ; i++)); do
270   echo "$i"
271 done
272 ```
273
274 ### Ranges
275
276 ```bash
277 for i in {1..5}; do
278     echo "Welcome $i"
279 done
280 ```
281
282 #### With step size
283
284 ```bash
285 for i in {5..50..5}; do
286     echo "Welcome $i"
287 done
288 ```
289
290 ### Reading lines
291
292 ```bash
293 while read -r line; do
294   echo "$line"
295 done <file.txt
296 ```
297
298 ### Forever
299
300 ```bash
301 while true; do
302   ยทยทยท
303 done
304 ```
305
306 ## Functions
307
308 {: .-three-column}
309
310 ### Defining functions
311
312 ```bash
313 myfunc() {
314     echo "hello $1"
315 }
316 ```
317
318 ```bash
319 # Same as above (alternate syntax)
320 function myfunc() {
321     echo "hello $1"
322 }
323 ```
324
325 ```bash
326 myfunc "John"
327 ```
328
329 ### Returning values
330
331 ```bash
332 myfunc() {
333     local myresult='some value'
334     echo "$myresult"
335 }
336 ```
337
338 ```bash
339 result=$(myfunc)
340 ```
341
342 ### Raising errors
343
344 ```bash
345 myfunc() {
346   return 1
347 }
348 ```
349
350 ```bash
351 if myfunc; then
352   echo "success"
353 else
354   echo "failure"
355 fi
356 ```
357
358 ### Arguments
359
360 | Expression | Description                                    |
361 | ---------- | ---------------------------------------------- |
362 | `$#`       | Number of arguments                            |
363 | `$*`       | All positional arguments (as a single word)    |
364 | `$@`       | All positional arguments (as separate strings) |
365 | `$1`       | First argument                                 |
366 | `$_`       | Last argument of the previous command          |
367
368 **Note**: `$@` and `$*` must be quoted in order to perform as described.
369 Otherwise, they do exactly the same thing (arguments as separate strings).
370
371 See [Special parameters](https://web.archive.org/web/20230318164746/https://wiki.bash-hackers.org/syntax/shellvars#special_parameters_and_shell_variables).
372
373 ## Conditionals
374
375 {: .-three-column}
376
377 ### Conditions
378
379 Note that `[[` is actually a command/program that returns either `0` (true) or `1` (false). Any program that obeys the same logic (like all base utils, such as `grep(1)` or `ping(1)`) can be used as condition, see examples.
380
381 | Condition                | Description           |
382 | ------------------------ | --------------------- |
383 | `[[ -z STRING ]]`        | Empty string          |
384 | `[[ -n STRING ]]`        | Not empty string      |
385 | `[[ STRING == STRING ]]` | Equal                 |
386 | `[[ STRING != STRING ]]` | Not Equal             |
387 | ---                      | ---                   |
388 | `[[ NUM -eq NUM ]]`      | Equal                 |
389 | `[[ NUM -ne NUM ]]`      | Not equal             |
390 | `[[ NUM -lt NUM ]]`      | Less than             |
391 | `[[ NUM -le NUM ]]`      | Less than or equal    |
392 | `[[ NUM -gt NUM ]]`      | Greater than          |
393 | `[[ NUM -ge NUM ]]`      | Greater than or equal |
394 | ---                      | ---                   |
395 | `[[ STRING =~ STRING ]]` | Regexp                |
396 | ---                      | ---                   |
397 | `(( NUM < NUM ))`        | Numeric conditions    |
398
399 #### More conditions
400
401 | Condition            | Description              |
402 | -------------------- | ------------------------ |
403 | `[[ -o noclobber ]]` | If OPTIONNAME is enabled |
404 | ---                  | ---                      |
405 | `[[ ! EXPR ]]`       | Not                      |
406 | `[[ X && Y ]]`       | And                      |
407 | `[[ X || Y ]]`       | Or                       |
408
409 ### File conditions
410
411 | Condition               | Description             |
412 | ----------------------- | ----------------------- |
413 | `[[ -e FILE ]]`         | Exists                  |
414 | `[[ -r FILE ]]`         | Readable                |
415 | `[[ -h FILE ]]`         | Symlink                 |
416 | `[[ -d FILE ]]`         | Directory               |
417 | `[[ -w FILE ]]`         | Writable                |
418 | `[[ -s FILE ]]`         | Size is > 0 bytes       |
419 | `[[ -f FILE ]]`         | File                    |
420 | `[[ -x FILE ]]`         | Executable              |
421 | ---                     | ---                     |
422 | `[[ FILE1 -nt FILE2 ]]` | 1 is more recent than 2 |
423 | `[[ FILE1 -ot FILE2 ]]` | 2 is more recent than 1 |
424 | `[[ FILE1 -ef FILE2 ]]` | Same files              |
425
426 ### Example
427
428 ```bash
429 # String
430 if [[ -z "$string" ]]; then
431   echo "String is empty"
432 elif [[ -n "$string" ]]; then
433   echo "String is not empty"
434 else
435   echo "This never happens"
436 fi
437 ```
438
439 ```bash
440 # Combinations
441 if [[ X && Y ]]; then
442   ...
443 fi
444 ```
445
446 ```bash
447 # Equal
448 if [[ "$A" == "$B" ]]
449 ```
450
451 ```bash
452 # Regex
453 if [[ "A" =~ . ]]
454 ```
455
456 ```bash
457 if (( $a < $b )); then
458    echo "$a is smaller than $b"
459 fi
460 ```
461
462 ```bash
463 if [[ -e "file.txt" ]]; then
464   echo "file exists"
465 fi
466 ```
467
468 ## Arrays
469
470 ### Defining arrays
471
472 ```bash
473 Fruits=('Apple' 'Banana' 'Orange')
474 ```
475
476 ```bash
477 Fruits[0]="Apple"
478 Fruits[1]="Banana"
479 Fruits[2]="Orange"
480 ```
481
482 ### Working with arrays
483
484 ```bash
485 echo "${Fruits[0]}"           # Element #0
486 echo "${Fruits[-1]}"          # Last element
487 echo "${Fruits[@]}"           # All elements, space-separated
488 echo "${#Fruits[@]}"          # Number of elements
489 echo "${#Fruits}"             # String length of the 1st element
490 echo "${#Fruits[3]}"          # String length of the Nth element
491 echo "${Fruits[@]:3:2}"       # Range (from position 3, length 2)
492 echo "${!Fruits[@]}"          # Keys of all elements, space-separated
493 ```
494
495 ### Operations
496
497 ```bash
498 Fruits=("${Fruits[@]}" "Watermelon")    # Push
499 Fruits+=('Watermelon')                  # Also Push
500 Fruits=( "${Fruits[@]/Ap*/}" )          # Remove by regex match
501 unset Fruits[2]                         # Remove one item
502 Fruits=("${Fruits[@]}")                 # Duplicate
503 Fruits=("${Fruits[@]}" "${Veggies[@]}") # Concatenate
504 lines=(`cat "logfile"`)                 # Read from file
505 ```
506
507 ### Iteration
508
509 ```bash
510 for i in "${arrayName[@]}"; do
511   echo "$i"
512 done
513 ```
514
515 ## Dictionaries
516
517 {: .-three-column}
518
519 ### Defining
520
521 ```bash
522 declare -A sounds
523 ```
524
525 ```bash
526 sounds[dog]="bark"
527 sounds[cow]="moo"
528 sounds[bird]="tweet"
529 sounds[wolf]="howl"
530 ```
531
532 Declares `sound` as a Dictionary object (aka associative array).
533
534 ### Working with dictionaries
535
536 ```bash
537 echo "${sounds[dog]}" # Dog's sound
538 echo "${sounds[@]}"   # All values
539 echo "${!sounds[@]}"  # All keys
540 echo "${#sounds[@]}"  # Number of elements
541 unset sounds[dog]     # Delete dog
542 ```
543
544 ### Iteration
545
546 #### Iterate over values
547
548 ```bash
549 for val in "${sounds[@]}"; do
550   echo "$val"
551 done
552 ```
553
554 #### Iterate over keys
555
556 ```bash
557 for key in "${!sounds[@]}"; do
558   echo "$key"
559 done
560 ```
561
562 ## Options
563
564 ### Options
565
566 ```bash
567 set -o noclobber  # Avoid overlay files (echo "hi" > foo)
568 set -o errexit    # Used to exit upon error, avoiding cascading errors
569 set -o pipefail   # Unveils hidden failures
570 set -o nounset    # Exposes unset variables
571 ```
572
573 ### Glob options
574
575 ```bash
576 shopt -s nullglob    # Non-matching globs are removed  ('*.foo' => '')
577 shopt -s failglob    # Non-matching globs throw errors
578 shopt -s nocaseglob  # Case insensitive globs
579 shopt -s dotglob     # Wildcards match dotfiles ("*.sh" => ".foo.sh")
580 shopt -s globstar    # Allow ** for recursive matches ('lib/**/*.rb' => 'lib/a/b/c.rb')
581 ```
582
583 Set `GLOBIGNORE` as a colon-separated list of patterns to be removed from glob
584 matches.
585
586 ## History
587
588 ### Commands
589
590 | Command               | Description                               |
591 | --------------------- | ----------------------------------------- |
592 | `history`             | Show history                              |
593 | `shopt -s histverify` | Don't execute expanded result immediately |
594
595 ### Expansions
596
597 | Expression   | Description                                          |
598 | ------------ | ---------------------------------------------------- |
599 | `!$`         | Expand last parameter of most recent command         |
600 | `!*`         | Expand all parameters of most recent command         |
601 | `!-n`        | Expand `n`th most recent command                     |
602 | `!n`         | Expand `n`th command in history                      |
603 | `!<command>` | Expand most recent invocation of command `<command>` |
604
605 ### Operations
606
607 | Code                 | Description                                                           |
608 | -------------------- | --------------------------------------------------------------------- |
609 | `!!`                 | Execute last command again                                            |
610 | `!!:s/<FROM>/<TO>/`  | Replace first occurrence of `<FROM>` to `<TO>` in most recent command |
611 | `!!:gs/<FROM>/<TO>/` | Replace all occurrences of `<FROM>` to `<TO>` in most recent command  |
612 | `!$:t`               | Expand only basename from last parameter of most recent command       |
613 | `!$:h`               | Expand only directory from last parameter of most recent command      |
614
615 `!!` and `!$` can be replaced with any valid expansion.
616
617 ### Slices
618
619 | Code     | Description                                                                              |
620 | -------- | ---------------------------------------------------------------------------------------- |
621 | `!!:n`   | Expand only `n`th token from most recent command (command is `0`; first argument is `1`) |
622 | `!^`     | Expand first argument from most recent command                                           |
623 | `!$`     | Expand last token from most recent command                                               |
624 | `!!:n-m` | Expand range of tokens from most recent command                                          |
625 | `!!:n-$` | Expand `n`th token to last from most recent command                                      |
626
627 `!!` can be replaced with any valid expansion i.e. `!cat`, `!-2`, `!42`, etc.
628
629 ## Miscellaneous
630
631 ### Numeric calculations
632
633 ```bash
634 $((a + 200))      # Add 200 to $a
635 ```
636
637 ```bash
638 $(($RANDOM%200))  # Random number 0..199
639 ```
640
641 ```bash
642 declare -i count  # Declare as type integer
643 count+=1          # Increment
644 ```
645
646 ### Subshells
647
648 ```bash
649 (cd somedir; echo "I'm now in $PWD")
650 pwd # still in first directory
651 ```
652
653 ### Redirection
654
655 ```bash
656 python hello.py > output.txt            # stdout to (file)
657 python hello.py >> output.txt           # stdout to (file), append
658 python hello.py 2> error.log            # stderr to (file)
659 python hello.py 2>&1                    # stderr to stdout
660 python hello.py 2>/dev/null             # stderr to (null)
661 python hello.py >output.txt 2>&1        # stdout and stderr to (file), equivalent to &>
662 python hello.py &>/dev/null             # stdout and stderr to (null)
663 echo "$0: warning: too many users" >&2  # print diagnostic message to stderr
664 ```
665
666 ```bash
667 python hello.py < foo.txt      # feed foo.txt to stdin for python
668 diff <(ls -r) <(ls)            # Compare two stdout without files
669 ```
670
671 ### Inspecting commands
672
673 ```bash
674 command -V cd
675 #=> "cd is a function/alias/whatever"
676 ```
677
678 ### Trap errors
679
680 ```bash
681 trap 'echo Error at about $LINENO' ERR
682 ```
683
684 or
685
686 ```bash
687 traperr() {
688   echo "ERROR: ${BASH_SOURCE[1]} at about ${BASH_LINENO[0]}"
689 }
690
691 set -o errtrace
692 trap traperr ERR
693 ```
694
695 ### Case/switch
696
697 ```bash
698 case "$1" in
699   start | up)
700     vagrant up
701     ;;
702
703   *)
704     echo "Usage: $0 {start|stop|ssh}"
705     ;;
706 esac
707 ```
708
709 ### Source relative
710
711 ```bash
712 source "${0%/*}/../share/foo.sh"
713 ```
714
715 ### printf
716
717 ```bash
718 printf "Hello %s, I'm %s" Sven Olga
719 #=> "Hello Sven, I'm Olga
720
721 printf "1 + 1 = %d" 2
722 #=> "1 + 1 = 2"
723
724 printf "This is how you print a float: %f" 2
725 #=> "This is how you print a float: 2.000000"
726
727 printf '%s\n' '#!/bin/bash' 'echo hello' >file
728 # format string is applied to each group of arguments
729 printf '%i+%i=%i\n' 1 2 3  4 5 9
730 ```
731
732 ### Transform strings
733
734 | Command option | Description                                         |
735 | -------------- | --------------------------------------------------- |
736 | `-c`           | Operations apply to characters not in the given set |
737 | `-d`           | Delete characters                                   |
738 | `-s`           | Replaces repeated characters with single occurrence |
739 | `-t`           | Truncates                                           |
740 | `[:upper:]`    | All upper case letters                              |
741 | `[:lower:]`    | All lower case letters                              |
742 | `[:digit:]`    | All digits                                          |
743 | `[:space:]`    | All whitespace                                      |
744 | `[:alpha:]`    | All letters                                         |
745 | `[:alnum:]`    | All letters and digits                              |
746
747 #### Example
748
749 ```bash
750 echo "Welcome To Devhints" | tr '[:lower:]' '[:upper:]'
751 WELCOME TO DEVHINTS
752 ```
753
754 ### Directory of script
755
756 ```bash
757 dir=${0%/*}
758 ```
759
760 ### Getting options
761
762 ```bash
763 while [[ "$1" =~ ^- && ! "$1" == "--" ]]; do case $1 in
764   -V | --version )
765     echo "$version"
766     exit
767     ;;
768   -s | --string )
769     shift; string=$1
770     ;;
771   -f | --flag )
772     flag=1
773     ;;
774 esac; shift; done
775 if [[ "$1" == '--' ]]; then shift; fi
776 ```
777
778 ### Heredoc
779
780 ```sh
781 cat <<END
782 hello world
783 END
784 ```
785
786 ### Reading input
787
788 ```bash
789 echo -n "Proceed? [y/n]: "
790 read -r ans
791 echo "$ans"
792 ```
793
794 The `-r` option disables a peculiar legacy behavior with backslashes.
795
796 ```bash
797 read -n 1 ans    # Just one character
798 ```
799
800 ### Special variables
801
802 | Expression         | Description                            |
803 | ------------------ | -------------------------------------- |
804 | `$?`               | Exit status of last task               |
805 | `$!`               | PID of last background task            |
806 | `$$`               | PID of shell                           |
807 | `$0`               | Filename of the shell script           |
808 | `$_`               | Last argument of the previous command  |
809 | `${PIPESTATUS[n]}` | return value of piped commands (array) |
810
811 See [Special parameters](https://web.archive.org/web/20230318164746/https://wiki.bash-hackers.org/syntax/shellvars#special_parameters_and_shell_variables).
812
813 ### Go to previous directory
814
815 ```bash
816 pwd # /home/user/foo
817 cd bar/
818 pwd # /home/user/foo/bar
819 cd -
820 pwd # /home/user/foo
821 ```
822
823 ### Check for command's result
824
825 ```bash
826 if ping -c 1 google.com; then
827   echo "It appears you have a working internet connection"
828 fi
829 ```
830
831 ### Grep check
832
833 ```bash
834 if grep -q 'foo' ~/.bash_history; then
835   echo "You appear to have typed 'foo' in the past"
836 fi
837 ```
838
839 ## Also see
840
841 {: .-one-column}
842
843 - [Bash-hackers wiki](https://web.archive.org/web/20230406205817/https://wiki.bash-hackers.org/) _(bash-hackers.org)_
844 - [Shell vars](https://web.archive.org/web/20230318164746/https://wiki.bash-hackers.org/syntax/shellvars) _(bash-hackers.org)_
845 - [Learn bash in y minutes](https://learnxinyminutes.com/docs/bash/) _(learnxinyminutes.com)_
846 - [Bash Guide](http://mywiki.wooledge.org/BashGuide) _(mywiki.wooledge.org)_
847 - [ShellCheck](https://www.shellcheck.net/) _(shellcheck.net)_