├── .gitignore ├── 05-compatibility.sh ├── 06-shellcheck.sh ├── 07-variables.sh ├── 08-environment-variables.sh ├── 09-arguments.sh ├── 10-builtins.sh ├── 11-quotes.sh ├── 12-globs.sh ├── 13-redirects.sh ├── 14-brackets.sh ├── 15-bashisms.sh ├── 16-if-statements.sh ├── 17-for-loops.sh ├── 18-reading-input.sh ├── 19-functions.sh ├── 20-pipes.sh ├── 21-parameter-expansion.sh ├── 22-background-processes.sh ├── 23-subshells.sh ├── 24-trap.sh ├── 25.1-errors.sh ├── 25.2-errors-unset.sh ├── 25.3-errors-pipefail.sh ├── 26-debugging.sh ├── README.md ├── files ├── cd.sh ├── error.txt ├── filename with spaces ├── lines.txt ├── owned_by_root.txt ├── star.svg └── test.txt └── zsh.sh /.gitignore: -------------------------------------------------------------------------------- 1 | files/star.png 2 | -------------------------------------------------------------------------------- /05-compatibility.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo ' 4 | ######################################### 5 | ## Example 5.1: # 6 | ## run this script in both sh and bash: # 7 | ## $ sh 05-compatibility.sh # 8 | ## $ bash 05-compatibility.sh # 9 | ######################################### 10 | ' 11 | 12 | echo "this works in both bash and sh:" 13 | 14 | x='hello there' 15 | echo "$x" 16 | 17 | echo "but this only expands to 'x.svg x.png' in bash:" 18 | 19 | echo x{.svg,.png} 20 | 21 | -------------------------------------------------------------------------------- /06-shellcheck.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo ' 4 | ######################################### 5 | ## Example 6.1: # 6 | ## Try running shellcheck on this file! # 7 | ## $ shellcheck 06-shellcheck.sh # 8 | ######################################### 9 | ' 10 | 11 | # shellcheck installation instructions: https://github.com/koalaman/shellcheck#installing 12 | 13 | filename="filename with spaces.txt" 14 | ls $filename # this is an error, shellcheck will catch it! 15 | 16 | # exercise: Try googling "SC2086" for an explanation of that error 17 | 18 | # there's a list of every shellcheck error here: 19 | # https://gist.github.com/nicerobot/53cee11ee0abbdc997661e65b348f375#file-_shellcheck-md 20 | -------------------------------------------------------------------------------- /07-variables.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo " 3 | ########################## 4 | ## Example 7.1: # 5 | ## how to set a variable # 6 | ########################## 7 | " 8 | x='hello there' 9 | echo $x 10 | 11 | echo "here's what happens when you put spaces around the =:\n" 12 | x = 'hello there' # this causes an error 13 | 14 | echo " 15 | ######################################################## 16 | ## Example 7.2: # 17 | ## you don't always need to put quotes around strings: # 18 | ######################################################## 19 | " 20 | 21 | x=banana 22 | 23 | echo "but you do need quotes if there's a space" 24 | 25 | x=hello there 26 | 27 | echo " 28 | ########################################## 29 | ## Example 7.3: # 30 | ## why it's important to quote variables # 31 | ########################################## 32 | " 33 | 34 | echo "here's what happens if you don't put quotes around a filename with spaces:" 35 | 36 | filename="files/filename with spaces" 37 | cat $filename 38 | 39 | echo "it works with quotes:" 40 | cat "$filename" 41 | 42 | echo " 43 | ############################################################### 44 | ## Example 7.4: # 45 | ## how to use ${var} to concatenate a variable with a string: # 46 | ############################################################### 47 | " 48 | 49 | x=panda 50 | echo "${x}bear" 51 | echo "${x}2" 52 | 53 | echo "these doesn't work: there's no variable called 'xbear' or 'x2'" 54 | echo "$xbear" 55 | echo "$x2" 56 | -------------------------------------------------------------------------------- /08-environment-variables.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo ' 4 | ################################################ 5 | ## Example 8.1: # 6 | ## how to print out all environment variables # 7 | ################################################ 8 | ' 9 | 10 | # the first way is `env` 11 | env | tail # I piped this to tail just because there are a LOT 12 | # here's another way to get environment variables that only works on Linux: 13 | cat /proc/self/environ | tr '\0' '\n' | tail 14 | 15 | echo ' 16 | ###################################### 17 | ## Example 8.2: # 18 | ## printing out environment variables # 19 | ## and shell variables # 20 | ####################################### 21 | ' 22 | # we access them both the same way: $varname 23 | x="i'm a variable" 24 | echo "$x" 25 | echo "$HOME" # $HOME is an environment variable 26 | 27 | echo ' 28 | ######################################## 29 | ## Example 8.3: # 30 | ## child processes inherit environment # 31 | ## variables (but not shell variables) # 32 | ######################################## 33 | ' 34 | 35 | # What's going on in this example: 36 | # $ bash -c "some bash code" 37 | # starts a bash child process that runs some bash code 38 | # We're doing that here because it's a simple way to start out a child process 39 | # that prints out its environment variables 40 | 41 | export ENVVAR='panda' 42 | bash -c 'echo ENVVAR is: $ENVVAR' # this prints out 'panda' 43 | # we can also print out the environment variable from a Python child process 44 | # (but we do it from a bash child process in the rest of the examples just 45 | # because it's less code) 46 | python -c "import os; print('from Python: ENVVAR=' + os.environ['ENVVAR'])" 47 | 48 | # but child processes don't inherit regular shell variables 49 | 50 | SHELLVAR='baby seal' 51 | 52 | bash -c 'echo SHELLVAR is: $SHELLVAR' # this doesn't print out anything 53 | 54 | echo ' 55 | ####################################### 56 | ## Example 8.4: # 57 | ## how to set an environment variable # 58 | ## for a child process with env # 59 | ####################################### 60 | ' 61 | 62 | env ANIMAL=porcupine bash -c 'echo in this child process, ANIMAL=$ANIMAL' 63 | # ANIMAL doesn't get set in the main process 64 | echo "but in the main process, ANIMAL=$ANIMAL" 65 | 66 | # it also works without the 'env' 67 | 68 | ANIMAL='banana slug' bash -c 'echo in the second child process, ANIMAL=$ANIMAL' 69 | echo "in the main process, we still have ANIMAL=$ANIMAL" 70 | -------------------------------------------------------------------------------- /09-arguments.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo " 4 | ############################################# 5 | ## Example 9.1: # 6 | ## try running this script with some # 7 | ## command line arguments, like: # 8 | ## $ bash 09-arguments.sh panda swan banana # 9 | ############################################# 10 | " 11 | 12 | echo '$1 is' $1 13 | echo '$2 is' $2 14 | echo '$3 is' $3 15 | 16 | echo 'printing out all the arguments now:' 17 | 18 | for i in "$@" 19 | do 20 | echo $i 21 | done 22 | -------------------------------------------------------------------------------- /10-builtins.sh: -------------------------------------------------------------------------------- 1 | echo " 2 | ############################################ 3 | ## Example 10.1: # 4 | ## let's check the 'type' of some programs # 5 | ############################################ 6 | " 7 | 8 | type alias 9 | type grep 10 | type gibberishasdf # doesn't exist 11 | type ls 12 | alias ls='ls --color' 13 | type ls 14 | type type 15 | type [ 16 | which [ # [ is both a builtin and a program 17 | type if 18 | type [[ 19 | 20 | echo " 21 | ################################# 22 | ## Example 10.2: # 23 | ## running a script with source # 24 | ################################# 25 | " 26 | 27 | echo "cd.sh changes the directory to files/ and sets a variable called PANDA. First, let's run it with cd:" 28 | bash files/cd.sh 29 | pwd 30 | echo $PANDA 31 | echo "Our directory didn't change! and the \$PANDA variable doesn't exist" 32 | echo "Let's use 'source' instead and try it again" 33 | source files/cd.sh 34 | 35 | pwd 36 | echo $PANDA 37 | echo "now we're in a different directory and \$PANDA is set" 38 | -------------------------------------------------------------------------------- /11-quotes.sh: -------------------------------------------------------------------------------- 1 | echo ' 2 | ################################### 3 | ## Example 11.1: # 4 | ## double quotes vs single quotes # 5 | ################################### 6 | ' 7 | 8 | echo 'single quotes: $HOME' 9 | echo "double quotes: $HOME" 10 | 11 | echo ' 12 | ############################## 13 | ## Example 11.2: # 14 | ## quoting multiline strings # 15 | ############################## 16 | ' 17 | 18 | echo "we've been quoting multiline strings this whole time: see the previous line!" 19 | 20 | echo ' 21 | ######################## 22 | ## Example 11.3: # 23 | ## concatenate strings # 24 | ######################## 25 | ' 26 | 27 | echo 'hi'' there' 28 | # + doesn't concatenate, it just puts a literal + in the middle 29 | echo 'hi' + ' there' 30 | -------------------------------------------------------------------------------- /12-globs.sh: -------------------------------------------------------------------------------- 1 | # this creates some files for this example 2 | # and cleans them up when the example is done 3 | ( 4 | cd files 5 | touch bear.txt bearable.txt bugbear.txt 6 | ) 7 | trap 'rm files/*bear*.txt' EXIT 8 | 9 | echo ' 10 | ########################################### 11 | ## Example 12.1: # 12 | ## listing all files starting with "bear" # 13 | ########################################### 14 | ' 15 | 16 | echo 'ls files/bear*:' 17 | ls files/bear* 18 | echo 'ls files/*.txt' 19 | ls files/*.txt 20 | 21 | echo " 22 | ############################################## 23 | ## Example 12.2: # 24 | ## filenames starting with a dot don't match # 25 | ############################################## 26 | " 27 | 28 | # all files with "bash" in your home directory 29 | echo 'ls ~/*bash*' 30 | ls ~/*bash* # no files with a . listed 31 | # this will list .bashrc and .bash_profile, 32 | # if you have them 33 | echo 'ls ~/.bash*' 34 | ls ~/.bash* 35 | 36 | echo " 37 | ################################################# 38 | ## Example 12.3: # 39 | ## quote a * to pass a literal * as an argument # 40 | ################################################# 41 | " 42 | 43 | 44 | echo "let's try it without the quotes" 45 | grep 22* files/lines.txt 46 | echo "no results! this is because it's running this:" 47 | echo grep 22* files/lines.txt 48 | 49 | echo '' 50 | echo "now let's try it with the quotes" 51 | echo "$ grep '22*' files/lines.txt" 52 | grep '22*' files/lines.txt 53 | # the regular expression '22*' is kind of a silly one, this is 54 | # similar to running `grep 2 files/lines.txt` 55 | -------------------------------------------------------------------------------- /13-redirects.sh: -------------------------------------------------------------------------------- 1 | echo " 2 | ########################### 3 | ## Example 13.1: # 4 | ## read input from a file # 5 | ########################### 6 | " 7 | 8 | # these both send files/lines.txt to wc's stdin 9 | wc < files/lines.txt 10 | cat files/lines.txt | wc 11 | 12 | # people say it's bad to use 'cat' when you don't need to 13 | # because it's wasteful / unnecessary but I usually do it 14 | # anyway and it's never harmed me 15 | 16 | echo " 17 | ################################################### 18 | ## Example 13.2: # 19 | ## try making a file owned by root, and then # 20 | ## writing to it in 2 different ways: # 21 | ## $ sudo chown root:root files/owned_by_root.txt # 22 | ## $ sudo echo 'hi' > files/owned_by_root.sh # 23 | ## then # 24 | ## $ echo 'hi' | sudo tee files/owned_by_root.sh # 25 | ################################################### 26 | " 27 | 28 | # no code here, you need to run these commands yourself 29 | # interactively because they need sudo 30 | 31 | # sudo echo 'hi' > files/owned_by_root.sh doesn't work because 32 | # the `>` redirect is done by your shell, which is owned by 33 | # you and doesn't have the permissions needed 34 | 35 | 36 | echo ' 37 | #################################### 38 | ## Example 13.3: # 39 | ## redirecting output to /dev/null # 40 | #################################### 41 | ' 42 | 43 | # run `ls` but send the output to /dev/null 44 | # (not useful and outputs nothing) 45 | echo 'ls > /dev/null' 46 | ls > /dev/null 47 | echo "... it didn't print anything" 48 | 49 | echo 'this prints out an error:' 50 | touch /asdf/file.txt 51 | echo 'this still prints out an error:' 52 | touch /asdf/file.txt > /dev/null 53 | echo "now there's no error:" 54 | # we need to redirect stderr with 2> to get rid of the error 55 | touch /nonexistent-directory/file.txt 2> /dev/null 56 | 57 | echo ' 58 | ####################### 59 | ## Example 13.4: # 60 | ## grepping with 2>&1 # 61 | ####################### 62 | ' 63 | 64 | echo "this doesn't filter for lines containing 'panda'" 65 | # because | only pipes `touch`'s stdout to grep, not the 66 | # stderr. So the error output just goes through unfiltered. 67 | touch /asdf/file.txt | grep panda 68 | 69 | echo "but this does:" 70 | touch /asdf/file.txt 2>&1 | grep panda 71 | 72 | echo ' 73 | ############################################# 74 | ## Example 13.5: # 75 | ## sending both stdout and stderr to a file # 76 | ############################################# 77 | ' 78 | 79 | 80 | echo 'this works' 81 | touch /asdf/file.txt > files/error.txt 2>&1 82 | 83 | echo "this doesn't work" 84 | touch /asdf/file.txt 2>&1 > files/error.txt 85 | # the stderr output doesn't get redirected because we need to 86 | # put the 2>&1 at the end 87 | # I always get confused by this. 88 | -------------------------------------------------------------------------------- /14-brackets.sh: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jvns/shell-examples/b8fe0e7a2704cf03443450b97c25baef5e731d4b/14-brackets.sh -------------------------------------------------------------------------------- /15-bashisms.sh: -------------------------------------------------------------------------------- 1 | # try running this file with both bash & sh 2 | 3 | echo ' 4 | ################## 5 | ## Example 15.1: # 6 | ## [[ # 7 | ################## 8 | ' 9 | 10 | if [[ -e /tmp ]]; then 11 | echo '/tmp exists' 12 | fi 13 | 14 | echo ' 15 | ######################### 16 | ## Example 15.2: # 17 | ## diff <(cmd1) <(cmd2) # 18 | ######################### 19 | ' 20 | 21 | echo 'this one actually causes a syntax error that stops the 22 | rest of the script from running in sh, uncomment it if you 23 | want to see' 24 | 25 | # diff <(ls files/*.txt) <(ls files/) 26 | 27 | echo " 28 | #################### 29 | ## Example 15.3: # 30 | ## brace expansion # 31 | #################### 32 | " 33 | 34 | echo a.{png,svg} 35 | echo {1..5} 36 | 37 | # there are a LOT more bashisms than just these 3 38 | -------------------------------------------------------------------------------- /16-if-statements.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo " 4 | ######################### 5 | ## Example 16.1: # 6 | ## a basic if statement # 7 | ######################### 8 | " 9 | 10 | # `if` can run any command. For example, touch! 11 | if touch files/test.txt 12 | then 13 | echo 'did a thing!' 14 | fi 15 | 16 | # this one will fail because /asdf doesn't exist 17 | if touch /asdf/asdf.txt 18 | then 19 | echo 'it succeeded' 20 | else 21 | echo 'it failed' 22 | fi 23 | 24 | echo " 25 | ########################## 26 | ## Example 16.2: # 27 | ## a 1-line if statement # 28 | ########################## 29 | " 30 | 31 | # if COMMAND; then THING; fi 32 | # the semicolons need to go in exactly those places, otherwise 33 | # it won't work (try putting the semicolons somewhere else!) 34 | if touch files/test.txt; then echo 'success'; fi 35 | 36 | echo " 37 | ######################################## 38 | ## Example 16.3: # 39 | ## run [ by itself to see how it works # 40 | ######################################## 41 | " 42 | 43 | # first, let's run the program [ by itself 44 | /usr/bin/[ -e files/lines.txt ] 45 | echo $? # $? is the exit code of the most recent program run 46 | echo 'the exit code was 0, so it succeeded' 47 | 48 | /usr/bin/[ -e files/doesntexist.txt ] 49 | echo $? # $? is the exit code of the most recent program run 50 | echo 'the exit code was 1, because doesntexist.txt doesn't exist 51 | 52 | echo " 53 | ####################################### 54 | ## Example 16.3: # 55 | ## testing if a file exists with if [ # 56 | ####################################### 57 | " 58 | 59 | 60 | if [ -e files/lines.txt ] 61 | then 62 | echo "files/lines.txt exists!" 63 | fi 64 | 65 | # [ is actually a bash builtin, but it acts like the program 66 | # /usr/bin/[ that we just used above 67 | 68 | echo " 69 | ######################################## 70 | ## Example 16.4: # 71 | ## testing if a file exists with if [[ # 72 | ######################################## 73 | " 74 | 75 | # most things that work with if [ will also work with if [[ 76 | 77 | if [[ -e files/lines.txt ]] 78 | then 79 | echo "files/lines.txt exists!" 80 | fi 81 | 82 | 83 | echo " 84 | ########################### 85 | ## Example 16.5: # 86 | ## negating a test with ! # 87 | ########################### 88 | " 89 | 90 | # ! will check if a command fails 91 | 92 | if ! [ -e files/doesntexist.txt ] 93 | then 94 | echo "files/doesntexist.txt doesn't exist!" 95 | fi 96 | 97 | 98 | echo " 99 | #################################################### 100 | ## Example 16.6: # 101 | ## using && to check if 2 conditions are both true # 102 | #################################################### 103 | " 104 | 105 | if [ -e file1 ] && [ -e file2 ] 106 | then 107 | echo file1 and file2 both exist 108 | else 109 | echo "they don't" 110 | fi 111 | 112 | echo " 113 | ################################################# 114 | ## Example 16.7: # 115 | ## try running # 116 | ## $ man test # 117 | ## to see what else you can put in 'if [ ... ]' # 118 | ################################################# 119 | " 120 | -------------------------------------------------------------------------------- /17-for-loops.sh: -------------------------------------------------------------------------------- 1 | echo " 2 | ########################## 3 | ## Example 17.1: # 4 | ## basic for loop syntax # 5 | ########################## 6 | " 7 | 8 | # you can just list the words you want to loop over! 9 | 10 | for i in panda swan 11 | do 12 | echo "$i" 13 | done 14 | 15 | echo " 16 | ###################### 17 | ## Example 17.2: # 18 | ## a 1-line for loop # 19 | ###################### 20 | " 21 | 22 | # usually when I write for loops on the command line, I just 23 | # press enter and type 24 | # for i in ... 25 | # do 26 | # .... 27 | # done 28 | # But you can also write the for loop on one line if you want! 29 | 30 | for i in banana mango pear; do echo "$i"; done 31 | 32 | echo " 33 | ########################### 34 | ## Example 17.3: # 35 | ## looping over filenames # 36 | ########################### 37 | " 38 | 39 | set -x 40 | # this converts all .svg files in files/ to .pngs 41 | for i in files/*.svg 42 | do 43 | convert "$i" "${i/svg/png}" 44 | done 45 | set +x 46 | 47 | echo " 48 | ######################################### 49 | ## Example 17.4: # 50 | ## for loops loop over words by default # 51 | ######################################### 52 | " 53 | 54 | # notice that "filename with spaces" gets listed in 3 different lines, not 1 line 55 | for i in $(ls files/) 56 | do 57 | echo $i 58 | done 59 | 60 | echo " 61 | ################################# 62 | ## Example 17.5: # 63 | ## loop over a range of numbers # 64 | ################################# 65 | " 66 | 67 | for i in $(seq 1 10) 68 | do 69 | echo $i 70 | done 71 | 72 | echo 'or with {1..5}:' 73 | for i in {1..5} 74 | do 75 | echo $i 76 | done 77 | 78 | 79 | -------------------------------------------------------------------------------- /18-reading-input.sh: -------------------------------------------------------------------------------- 1 | echo " 2 | ############################ 3 | ## Example 18.1: # 4 | ## reading into a variable # 5 | ############################ 6 | " 7 | 8 | echo -n "What's your name? (type your name and press enter) " 9 | read -r name 10 | echo "Hello, $name!" 11 | 12 | echo " 13 | ############################ 14 | ## Example 18.2: # 15 | ## read into multiple variables # 16 | ############################ 17 | " 18 | 19 | echo -n "What's your first & last name? " 20 | read -r first last 21 | echo "first: $first, last: $last" 22 | 23 | 24 | echo " 25 | ################################### 26 | ## Example 18.3: # 27 | ## reading a comma-separated list # 28 | ################################### 29 | " 30 | 31 | # if we pipe to `read` then we can read input from a file 32 | # instead of having to type in the values to read 33 | 34 | echo 'Ramesh,Kumar,22' | ( # This () syntax is a subshell, from page 23 35 | IFS=, # split input on comma 36 | read -r first last age 37 | echo "$first $last is $age years old" 38 | ) 39 | 40 | # I don't usually read from files in bash (to me that's in the 41 | # "complicated" category I'd use another language for) 42 | # but I think it's kind of fun that you could use this to read a CSV 43 | 44 | 45 | 46 | echo " 47 | ###################################### 48 | ## Example 18.4: # 49 | ## read strips whitespace by default # 50 | ###################################### 51 | " 52 | 53 | echo 'type something with a lot of spaces at the beginning' 54 | 55 | read -r something 56 | echo "here's what got read: \"$something\"" 57 | 58 | echo " 59 | ############################################# 60 | ## Example 18.5: # 61 | ## making a for loop read lines from a file # 62 | ############################################# 63 | " 64 | 65 | echo 'by default, a for loop will read one word at a time from a file' 66 | 67 | for i in $(cat files/lines.txt) 68 | do 69 | echo $i 70 | done 71 | 72 | echo "but if you set IFS='', it'll read lines from the file instead" 73 | IFS='' 74 | # the code below is exactly the same as the code above, we 75 | # just changed the value of IFS 76 | for i in $(cat files/lines.txt) 77 | do 78 | echo $i 79 | done 80 | 81 | 82 | -------------------------------------------------------------------------------- /19-functions.sh: -------------------------------------------------------------------------------- 1 | echo " 2 | ################################## 3 | ## Example 19.1: # 4 | ## defining & calling a function # 5 | ################################## 6 | " 7 | 8 | say_hello() { 9 | echo 'hello!' 10 | } 11 | 12 | say_hello 13 | # $? prints the exit code of a function or program 14 | echo $? 15 | 16 | echo " 17 | ########################## 18 | ## Example 19.2: # 19 | ## a function that fails # 20 | ########################## 21 | " 22 | 23 | failing_function() { 24 | echo 'I fail!' 25 | # by default, functions return 0 (success) 26 | return 1 27 | } 28 | 29 | failing_function 30 | # $? prints the exit code of a function or program 31 | echo $? 32 | 33 | 34 | echo " 35 | ######################################## 36 | ## Example 19.3: # 37 | ## how to take arguments in a function # 38 | ######################################## 39 | " 40 | 41 | say_hello() { 42 | name=$1 43 | echo "Hello, $name!" 44 | } 45 | 46 | say_hello "Ahmed" 47 | 48 | echo " 49 | ##################################### 50 | ## Example 19.4: # 51 | ## a function with a local variable # 52 | ##################################### 53 | " 54 | 55 | set_some_variables() { 56 | globalvar=$1 57 | local localvar 58 | localvar=$1 59 | } 60 | 61 | set_some_variables "panda" 62 | # localvar was local to the function, so it's not available 63 | # outside of the function 64 | echo "localvar=$localvar" 65 | echo "globalvar=$globalvar" 66 | 67 | echo " 68 | ###################################################### 69 | ## Example 19.4: # 70 | ## very weird thing: local x=VALUE suppresses errors # 71 | ###################################################### 72 | " 73 | 74 | example_function1() { 75 | local x=$(asdf) 76 | } 77 | 78 | example_function2() { 79 | local x 80 | x=$(asdf) 81 | } 82 | 83 | # this function succeeds, even though it has this "asdf: 84 | # command not found" error 85 | example_function1 86 | echo "example_function1 exit code: $?" 87 | # this function fails, like it should 88 | example_function2 89 | echo "example_function2 exit code: $?" 90 | 91 | echo " 92 | ##################################################### 93 | ## Example 19.5: # 94 | ## functions need to be defined before they're used # 95 | ##################################################### 96 | " 97 | 98 | my_function # error: not found 99 | 100 | my_function() { 101 | echo "my_function got called" 102 | } 103 | 104 | my_function # now it works 105 | -------------------------------------------------------------------------------- /20-pipes.sh: -------------------------------------------------------------------------------- 1 | echo " 2 | ################## 3 | ## Example 20.1: # 4 | ## using a pipe # 5 | ################## 6 | " 7 | 8 | # prints the number of files in the current directory 9 | ls | wc -l 10 | 11 | echo " 12 | ############################################### 13 | ## Example 20.2: # 14 | ## what happens when you fill a pipe's buffer # 15 | ############################################### 16 | " 17 | 18 | # we need to write a slightly unusual program to demonstrate 19 | # this, but let's do it! 20 | 21 | # Program 1 (the first one) writes five 30,000 character lines 22 | # Program 2 reads one line at a time, and after each line pauses for a second 23 | 24 | # Because our pipe's buffer is only about 64KB, Program 1 25 | # can't write all of its lines immediately: it has to wait to 26 | # print lines 3 and 4. So it pauses, even though if there 27 | # weren't a pipe it would be able to print out all of its 28 | # output right away and exit 29 | 30 | # python -c lets you pass in a script for Python to run 31 | python -c ' 32 | import sys 33 | for i in range(5): 34 | print("a" * 30000) 35 | sys.stderr.write("printed line %d\n" % i)' | python -c ' 36 | import fileinput 37 | import time 38 | for line in fileinput.input(): 39 | time.sleep(1) 40 | ' 41 | 42 | 43 | echo " 44 | ################## 45 | ## Example 20.3: # 46 | ## named pipes # 47 | ################## 48 | " 49 | 50 | trap 'rm -f myfifo' EXIT # this trap just cleans up the file at the end 51 | # this does the same thing as running `ls | wc` 52 | mkfifo myfifo 53 | ls > myfifo & 54 | wc < myfifo 55 | 56 | -------------------------------------------------------------------------------- /21-parameter-expansion.sh: -------------------------------------------------------------------------------- 1 | echo " 2 | ###################################### 3 | ## Example 20.1: # 4 | ## how to get the length of a string # 5 | ###################################### 6 | " 7 | 8 | animal=panda 9 | echo "${#animal}" 10 | 11 | echo " 12 | ############################### 13 | ## Example 20.2: # 14 | ## how to do search & replace # 15 | ############################### 16 | " 17 | 18 | filename=swan.svg 19 | echo "convert $filename ${filename/svg/png}" 20 | 21 | echo 'You can also replace all instances of a string with //' 22 | greeting="hello hello hello!" 23 | echo ${greeting//hello/bonjour} 24 | 25 | echo " 26 | ################################################## 27 | ## Example 20.3: # 28 | ## use a default value for an undefined variable # 29 | ################################################## 30 | " 31 | 32 | echo "Hello, ${name:-UNKNOWN NAME}" 33 | name=Julia 34 | echo "Hello, ${name:-UNKNOWN NAME}" 35 | 36 | echo " 37 | ################################## 38 | ## Example 20.4: # 39 | ## remove a suffix from a string # 40 | ################################## 41 | " 42 | 43 | filename="motorcycle.svg" 44 | echo ${filename%.svg} 45 | 46 | echo 'or remove a prefix:' 47 | filename="motorcycle.svg" 48 | echo ${filename#motor} 49 | 50 | 51 | echo " 52 | #################### 53 | ## Example 20.5: # 54 | ## get a substring # 55 | #################### 56 | " 57 | 58 | x="oh, hello there!" 59 | 60 | echo ${x:4} # 4 is the offset 61 | echo ${x:4:5} # 4 is the offset, 5 is the length 62 | 63 | echo " 64 | ################################################## 65 | ## Example 20.6: # 66 | ## exit with an error if a variable is undefined # 67 | ################################################## 68 | " 69 | 70 | echo ${asdf:?"oops, not defined! danger!"} 71 | -------------------------------------------------------------------------------- /22-background-processes.sh: -------------------------------------------------------------------------------- 1 | echo ' 2 | ###################################################### 3 | ## Example 22.1: # 4 | ## `wait` waits for all background processes to finish # 5 | ###################################################### 6 | ' 7 | 8 | sleep 2 & 9 | sleep 1 & 10 | sleep 1 & 11 | sleep 1 & 12 | sleep 2 & 13 | # this waits for all `sleep` processes to finish 14 | # (it takes 2 seconds, not 7 seconds, because they all run at the same time) 15 | wait 16 | 17 | -------------------------------------------------------------------------------- /23-subshells.sh: -------------------------------------------------------------------------------- 1 | echo " 2 | ################################################## 3 | ## Example 23.1: # 4 | ## cd in a subshell doesn't cd in the main shell # 5 | ################################################## 6 | " 7 | 8 | ( 9 | cd files 10 | echo "directory in subshell: $(pwd)" 11 | ) 12 | echo "directory in main shell: $(pwd)" 13 | 14 | echo " 15 | ############################################################# 16 | ## Example 23.2: # 17 | ## variables set in a subshell aren't set in the main shell # 18 | ############################################################# 19 | " 20 | 21 | ( 22 | animal="banana slug" 23 | echo "in subshell: animal=$animal" 24 | ) 25 | echo "in main shell: animal=" 26 | 27 | 28 | echo " 29 | #################################################################################### 30 | ## Example 23.3: # 31 | ## pipes create a subshell # 32 | ## (another example of 'variables set in a subshell aren't set in the main shell') # 33 | #################################################################################### 34 | " 35 | 36 | # here's an example of a place where there's a subshell created in a pretty 37 | # nonobvious way. 38 | 39 | # let's try to read 'hi' into a variable. 40 | echo hi | read -r x 41 | echo "in main shell: x=$x" 42 | # it didn't work! this is because `read -r x` runs in a subshell 43 | 44 | # if we put the `echo` in the same subshell as the `read`, though, it works: 45 | echo hi | ( 46 | read -r x 47 | echo "in subshell: x=$x" 48 | ) 49 | 50 | echo " 51 | ################################################# 52 | ## Example 23.4: # 53 | ## a subshell created with process substitution # 54 | ################################################# 55 | " 56 | 57 | function1() { 58 | echo 'hi there!' 59 | echo "I'm function 1!" 60 | } 61 | 62 | 63 | function2() { 64 | echo 'hi there!' 65 | echo "I'm function 2!" 66 | } 67 | 68 | # this runs both `function1` and `function2` in 2 different subshells, creates 69 | # a named pipe with each one's output, and then passes the filenames of the 2 70 | # named pipes to `diff`. 71 | # I don't know if I've ever used this but it's cool that you can do it! 72 | diff <(function1) <(function2) 73 | 74 | echo " 75 | ######################################################################## 76 | ## Example 23.5: # 77 | ## subshells inherit shell variables (but other child processes don't) # 78 | ######################################################################## 79 | " 80 | 81 | ANIMAL=panda 82 | 83 | ( 84 | echo "in subshell: ANIMAL=$ANIMAL" 85 | ) 86 | 87 | bash -c 'echo "in this other shell: ANIMAL=$ANIMAL "' 88 | 89 | # subshells copy all the variables from their parent shell, but other child 90 | # shell processes, even though both of them are child processes, 91 | -------------------------------------------------------------------------------- /24-trap.sh: -------------------------------------------------------------------------------- 1 | echo " 2 | ############################################# 3 | ## Example 24.1: # 4 | ## trap signals that run on EXIT and Ctrl+C # 5 | ############################################# 6 | " 7 | 8 | trap "echo we\'re exiting the script!" EXIT 9 | 10 | trap "echo you pressed Ctrl+C" INT 11 | 12 | echo 'press Ctrl+C here to see the INT signal handler run.' 13 | # notice that Ctrl+C doesn't actually make the script exit, like it usually 14 | # would! This is because trap replaces the normal signal handler for Ctrl+C 15 | # (which would cause the script to exit). If you put an `; exit` at the end of 16 | # the signal handler command, it'll exit the script 17 | 18 | # this `read` just gives an opportunity to press Ctrl+C 19 | read 20 | 21 | -------------------------------------------------------------------------------- /25.1-errors.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # these 25.x scripts have multiple scripts because they all exit the script so 3 | # we can't put them all in one file 4 | echo " 5 | ################################################################################ 6 | ## Example 25.1: # 7 | ## Run \$ bash 25-1-errors.sh to see how the script exits when there's an error # 8 | ################################################################################ 9 | " 10 | 11 | set -e 12 | 13 | unzip nonexistent-file.zip 14 | echo "does this happen?" # spoiler: it doesn't 15 | -------------------------------------------------------------------------------- /25.2-errors-unset.sh: -------------------------------------------------------------------------------- 1 | echo " 2 | ########################################## 3 | ## Example 25.2: # 4 | ## set -u saves you from unset variables # 5 | ########################################## 6 | " 7 | 8 | set -u 9 | 10 | rm files/whatever/$ASDF 11 | # this isn't THAT unsafe to run without `set -u` (worst case it removes 12 | # files/whatever which doesn't even exist anyway), but other variants of this 13 | # can delete all your files! very scary. 14 | -------------------------------------------------------------------------------- /25.3-errors-pipefail.sh: -------------------------------------------------------------------------------- 1 | echo " 2 | ##################################### 3 | ## Example 25.3: # 4 | ## set -o pipefail makes pipes fail # 5 | ##################################### 6 | " 7 | 8 | set -e 9 | curl 23849234faadsfad.com | tr '\0' '\n' 10 | echo "why didn't the script fail already? curl failed and we asked it to stop on errors?!" 11 | 12 | set -o pipefail 13 | 14 | curl 23849234faadsfad.com | tr '\0' '\n' 15 | 16 | echo "we don't get to this line, set -o pipefail makes the whole pipeline fail because curl failed" 17 | -------------------------------------------------------------------------------- /26-debugging.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo " 4 | ############################################### 5 | ## Example 26.1: # 6 | ## set -x shows every line of code that's run # 7 | ############################################### 8 | " 9 | 10 | set -x 11 | 12 | echo 'hi' 13 | for i in $(seq 1 5) 14 | do 15 | echo $i 16 | done 17 | 18 | set +x # undo the set -x 19 | 20 | echo " 21 | ############################################### 22 | ## Example 26.2: # 23 | ## trap lets us make a fancy step debugger # 24 | ############################################### 25 | " 26 | 27 | trap '(read -p "[$BASH_SOURCE:$LINENO] $BASH_COMMAND ") ' DEBUG 28 | 29 | echo "it makes us press enter to confirm before every line of code" 30 | 31 | for i in $(seq 1 5) 32 | do 33 | echo $i 34 | done 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### bash examples 2 | 3 | This set of examples goes with zine [Bite Size Bash](https://wizardzines.com/zines/bite-size-bash). There's approximately one file per page of the zine. 4 | 5 | ### how to run the examples 6 | 7 | To run the globs example, run: 8 | 9 | ``` 10 | bash 12-globs.sh 11 | ``` 12 | 13 | I'd recommend both opening the example file in a text editor (so you can read 14 | the code) and running the example in a terminal (to see the output). 15 | 16 | ### and play around with them! 17 | 18 | If you're confused about something in one of the examples, experiment! Make 19 | your own examples! See what happens! I've learned a LOT about bash by just 20 | experimenting with different tiny shell scripts and making sure I understand 21 | what they're doing. 22 | -------------------------------------------------------------------------------- /files/cd.sh: -------------------------------------------------------------------------------- 1 | cd files 2 | PANDA="i'm a panda" 3 | -------------------------------------------------------------------------------- /files/error.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jvns/shell-examples/b8fe0e7a2704cf03443450b97c25baef5e731d4b/files/error.txt -------------------------------------------------------------------------------- /files/filename with spaces: -------------------------------------------------------------------------------- 1 | i'm a file, there are spaces in my name! 2 | -------------------------------------------------------------------------------- /files/lines.txt: -------------------------------------------------------------------------------- 1 | line 1 2 | line 2 3 | line 3 4 | -------------------------------------------------------------------------------- /files/owned_by_root.txt: -------------------------------------------------------------------------------- 1 | hi 2 | -------------------------------------------------------------------------------- /files/star.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 22 | image/svg+xml 23 | 25 | 26 | 27 | 28 | 29 | 31 | 51 | 57 | 62 | 67 | 72 | 77 | 78 | -------------------------------------------------------------------------------- /files/test.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jvns/shell-examples/b8fe0e7a2704cf03443450b97c25baef5e731d4b/files/test.txt -------------------------------------------------------------------------------- /zsh.sh: -------------------------------------------------------------------------------- 1 | # Here are a few examples of things that are different between zsh and bash 2 | 3 | echo " 4 | ######################################################### 5 | ## zsh example 1: # 6 | ## you don't need to put quotes around variables in zsh # 7 | ######################################################### 8 | " 9 | 10 | filename='files/filename with spaces' 11 | 12 | # this works in zsh but not in bash 13 | cat $filename 14 | 15 | echo " 16 | ####################################### 17 | ## zsh example 2: # 18 | ## 'which' is a shell builtin in zsh, # 19 | ## in bash it's /usr/bin/which # 20 | ####################################### 21 | " 22 | 23 | # outputs /usr/bin/[ in bash, and 'shell builtin' in zsh 24 | # even though in both bash and zsh [ is a shell builtin 25 | which which 26 | 27 | echo " 28 | ###################################### 29 | ## zsh example 3: # 30 | ## you can't run '/usr/bin/[' in zsh # 31 | ###################################### 32 | " 33 | 34 | # this works in bash, but is a syntax error in zsh 35 | /usr/bin/[ -e files/lines.txt ] 36 | --------------------------------------------------------------------------------