Skip to content

Instantly share code, notes, and snippets.

@mohanpedala
Last active October 31, 2024 16:27
Show Gist options
  • Save mohanpedala/1e2ff5661761d3abd0385e8223e16425 to your computer and use it in GitHub Desktop.
Save mohanpedala/1e2ff5661761d3abd0385e8223e16425 to your computer and use it in GitHub Desktop.
set -e, -u, -o, -x pipefail explanation

Table of Contents

set -e, -u, -x, -o pipefail

  • The set lines
    • These lines deliberately cause your script to fail. Wait, what? Believe me, this is a good thing.

    • With these settings, certain common errors will cause the script to immediately fail, explicitly and loudly. Otherwise, you can get hidden bugs that are discovered only when they blow up in production.

      set -euxo pipefail is short for:

      set -e
      set -u
      set -o pipefail
      set -x
      

set -e

  • The set -e option instructs bash to immediately exit if any command [1] has a non-zero exit status. You wouldn't want to set this for your command-line shell, but in a script it's massively helpful. In all widely used general-purpose programming languages, an unhandled runtime error
  • whether that's a thrown exception in Java, or a segmentation fault in C, or a syntax error in Python - immediately halts execution of the program; subsequent lines are not executed.

    • By default, bash does not do this. This default behavior is exactly what you want if you are using bash on the command line
    • you don't want a typo to log you out! But in a script, you really want the opposite.
    • If one line in a script fails, but the last line succeeds, the whole script has a successful exit code. That makes it very easy to miss the error.
    • Again, what you want when using bash as your command-line shell and using it in scripts are at odds here. Being intolerant of errors is a lot better in scripts, and that's what set -e gives you.

set -x

  • Enables a mode of the shell where all executed commands are printed to the terminal. In your case it's clearly used for debugging, which is a typical use case for set -x : printing every command as it is executed may help you to visualize the control flow of the script if it is not functioning as expected.

set -u

  • Affects variables. When set, a reference to any variable you haven't previously defined - with the exceptions of $* and $@ - is an error, and causes the program to immediately exit. Languages like Python, C, Java and more all behave the same way, for all sorts of good reasons. One is so typos don't create new variables without you realizing it. For example:

    #!/bin/bash
    firstName="Aaron"
    fullName="$firstname Maxwell"
    echo "$fullName"
    
  • Take a moment and look. Do you see the error? The right-hand side of the third line says "firstname", all lowercase, instead of the camel-cased "firstName". Without the -u option, this will be a silent error. But with the -u option, the script exits on that line with an exit code of 1, printing the message "firstname: unbound variable" to stderr.

  • This is what you want: have it fail explicitly and immediately, rather than create subtle bugs that may be discovered too late.

set -o pipefail

  • This setting prevents errors in a pipeline from being masked. If any command in a pipeline fails, that return code will be used as the return code of the whole pipeline. By default, the pipeline's return code is that of the last command even if it succeeds. Imagine finding a sorted list of matching lines in a file:

    $ grep some-string /non/existent/file | sort
    grep: /non/existent/file: No such file or directory
    % echo $?
    0
    
  • Here, grep has an exit code of 2, writes an error message to stderr, and an empty string to stdout.

  • This empty string is then passed through sort, which happily accepts it as valid input, and returns a status code of 0.

  • This is fine for a command line, but bad for a shell script: you almost certainly want the script to exit right then with a nonzero exit code... like this:

    $ set -o pipefail
    $ grep some-string /non/existent/file | sort
    grep: /non/existent/file: No such file or directory
    $ echo $?
    2
    

Setting IFS

  • The IFS variable - which stands for Internal Field Separator - controls what Bash calls word splitting. When set to a string, each character in the string is considered by Bash to separate words. This governs how bash will iterate through a sequence. For example, this script:

    #!/bin/bash
    IFS=$' '
    items="a b c"
    for x in $items; do
        echo "$x"
    done
    
    IFS=$'\n'
    for y in $items; do
        echo "$y"
    done
    ... will print out this:
    
    a
    b
    c
    a b c
    
  • In the first for loop, IFS is set to $' '. The $'...' syntax creates a string, with backslash-escaped characters replaced with special characters - like "\t" for tab and "\n" for newline.) Within the for loops, x and y are set to whatever bash considers a "word" in the original sequence.

  • For the first loop, IFS is a space, meaning that words are separated by a space character.

  • For the second loop, "words" are separated by a newline, which means bash considers the whole value of "items" as a single word. If IFS is more than one character, splitting will be done on any of those characters.

  • Got all that? The next question is, why are we setting IFS to a string consisting of a tab character and a newline? Because it gives us better behavior when iterating over a loop. By "better", I mean "much less likely to cause surprising and confusing bugs". This is apparent in working with bash arrays:

    #!/bin/bash
    names=(
    "Aaron Maxwell"
    "Wayne Gretzky"
    "David Beckham"
    )
    
    echo "With default IFS value..."
    for name in ${names[@]}; do
    echo "$name"
    done
    
    echo ""
    echo "With strict-mode IFS value..."
    IFS=$'\n\t'
    for name in ${names[@]}; do
    echo "$name"
    done
    
    
    ## Output
    With default IFS value...
    Aaron
    Maxwell
    Wayne
    Gretzky
    David
    Beckham
    
    With strict-mode IFS value...
    Aaron Maxwell
    Wayne Gretzky
    David Beckham
    

Or consider a script that takes filenames as command line arguments:

```
for arg in $@; do
    echo "doing something with file: $arg"
done
```
  • If you invoke this as myscript.sh notes todo-list 'My Resume.doc', then with the default IFS value, the third argument will be mis-parsed as two separate files - named "My" and "Resume.doc". When actually it's a file that has a space in it, named "My Resume.doc".

  • Which behavior is more generally useful? The second, of course - where we have the ability to not split on spaces. If we have an array of strings that in general contain spaces, we normally want to iterate through them item by item, and not split an individual item into several.

  • Setting IFS to $'\n\t' means that word splitting will happen only on newlines and tab characters. This very often produces useful splitting behavior.

  • By default, bash sets this to $' \n\t' - space, newline, tab - which is too eager.

Original Reference

@mohanpedala
Copy link
Author

Thanks very much! The -u and -o pipefail are new to me.
Just a trivial one: maybe change title/heading from set -e, -u, -o, -x pipefail to set -e, -u, -x, -o pipefail to be more consistent/correct?

Upvote!

I have chnaged it thank you

@thingsiplay
Copy link

thingsiplay commented Sep 15, 2021

Previously I was executing the script using sh ./scripts/deploy.sh Now it's working using bash ./scripts/deploy.sh Thank you so much :)

I am a bit late, but just found this wonderful tutorial. And by accident I just found in the man bash that using bash as sh would put it into strict POSIX mode, which is similar to dash and does not support any of the bash features. So if you have bash set as your sh interpreter and use the sh command, then it goes into this strict mode. That is why it did not work. This is the line from the manual in question:

If bash is invoked with the name sh, it tries to mimic the startup behavior of historical versions of sh as closely as possible, while conforming to the POSIX standard as well.

@bkahlert
Copy link

Nice that this gist receives so much positive feedback. I just ask myself if all that gratitude shouldn't be better directed to Aaron Maxwell, the author of the original article to be found at http://redsymbol.net/articles/unofficial-bash-strict-mode/.
This gist is close to a 1:1 copy.
I recommend to original article for reading as it also describes what to do in cases were the "strict mode" causes problems.

@cvega21
Copy link

cvega21 commented Oct 22, 2021

thank you Aaron Maxwell for this, and thank you mohan for compiling it here! very clear explanations and super useful knowledge.

@kayomarz
Copy link

Very useful gist but as @bkahlert pointed out its almost a copy of the original author @redsymbol who deserves a mention.

Nice that this gist receives so much positive feedback. I just ask myself if all that gratitude shouldn't be better directed to Aaron Maxwell, the author of the original article to be found at http://redsymbol.net/articles/unofficial-bash-strict-mode/. This gist is close to a 1:1 copy. I recommend to original article for reading as it also describes what to do in cases were the "strict mode" causes problems.

@mohanpedala
Copy link
Author

mohanpedala commented Oct 26, 2021

Nice that this gist receives so much positive feedback. I just ask myself if all that gratitude shouldn't be better directed to Aaron Maxwell, the author of the original article to be found at http://redsymbol.net/articles/unofficial-bash-strict-mode/. This gist is close to a 1:1 copy. I recommend to original article for reading as it also describes what to do in cases were the "strict mode" causes problems.

Hi @bkahlert , Original reference has been added at the bottom of the gist when I created it ("Full Reference Click Here"). Please read the full gist before pointing out. and for your reference I keep my daily notes and findings as a gist.
I like to see most of the content in markdown so I have created the content I need in the markdown version. I really appreciate Aaron Maxwell article that is the reason why I did-not change the names or content in the gist.

@mohanpedala
Copy link
Author

mohanpedala commented Oct 26, 2021

Very useful gist but as @bkahlert pointed out its almost a copy of the original author @redsymbol who deserves a mention.

Nice that this gist receives so much positive feedback. I just ask myself if all that gratitude shouldn't be better directed to Aaron Maxwell, the author of the original article to be found at http://redsymbol.net/articles/unofficial-bash-strict-mode/. This gist is close to a 1:1 copy. I recommend to original article for reading as it also describes what to do in cases were the "strict mode" causes problems.

Hi @kayomarz Original reference has been added at the bottom of the gist when I created it ("Full Reference Click Here"). Please read the full gist before pointing out. and for your reference I keep my daily notes and findings as a gist.
I like to see most of the content in markdown so I have created the content I need in the markdown version. I really appreciate Aaron Maxwell article that is the reason why I did-not change the names or content in the gist.

@bkahlert
Copy link

Very useful gist but as @bkahlert pointed out its almost a copy of the original author @redsymbol who deserves a mention.

Nice that this gist receives so much positive feedback. I just ask myself if all that gratitude shouldn't be better directed to Aaron Maxwell, the author of the original article to be found at http://redsymbol.net/articles/unofficial-bash-strict-mode/. This gist is close to a 1:1 copy. I recommend to original article for reading as it also describes what to do in cases were the "strict mode" causes problems.

Hi @kayomarz Original reference has been added at the bottom of the gist when I created it ("Full Reference Click Here"). Please read the full gist before pointing out. and for your reference I keep my daily notes and findings as a gist. I like to see most of the content in markdown so I have created the content I need in the markdown version. I really appreciate Aaron Maxwell article that is the reason why I did-not change the names or content in the gist.

Hi @mohanpedala, I did read your gist completely and well noticed your "Full Reference Click Here" link. That's what made me comment. I consider such a generic reference to a 1:1 copied article inappropriate. Especially in the light of its perceived value. You might want to read Citation styles guide: Choosing a style and citing correctly.

@mohanpedala
Copy link
Author

mohanpedala commented Oct 29, 2021

Very useful gist but as @bkahlert pointed out its almost a copy of the original author @redsymbol who deserves a mention.

Nice that this gist receives so much positive feedback. I just ask myself if all that gratitude shouldn't be better directed to Aaron Maxwell, the author of the original article to be found at http://redsymbol.net/articles/unofficial-bash-strict-mode/. This gist is close to a 1:1 copy. I recommend to original article for reading as it also describes what to do in cases were the "strict mode" causes problems.

Hi @kayomarz Original reference has been added at the bottom of the gist when I created it ("Full Reference Click Here"). Please read the full gist before pointing out. and for your reference I keep my daily notes and findings as a gist. I like to see most of the content in markdown so I have created the content I need in the markdown version. I really appreciate Aaron Maxwell article that is the reason why I did-not change the names or content in the gist.

Hi @mohanpedala, I did read your gist completely and well noticed your "Full Reference Click Here" link. That's what made me comment. I consider such a generic reference to a 1:1 copied article inappropriate. Especially in the light of its perceived value. You might want to read Citation styles guide: Choosing a style and citing correctly.

Thank you @bkahlert I hope this looks good 😊

@bkahlert
Copy link

Wonderful

@Gboom345
Copy link

very good explanation

@bolshakoff
Copy link

Nice.

@fazlearefin
Copy link

@mohanpedala Consider using -E as well as mentioned in https://vaneyckt.io/posts/safer_bash_scripts_with_set_euxo_pipefail/ ?

So it becomes

set -Eeuxo pipefail

@patrikjuvonen
Copy link

patrikjuvonen commented Apr 6, 2022

https://blog.cloudflare.com/pipefail-how-a-missing-shell-option-slowed-cloudflare-down/

Interesting real-life scenario post about how missing the pipefail option caused a major cascading failure in Cloudflare's production.

@RimElbaddazi
Copy link

i didn't get : set -o pipefail

@icntrs
Copy link

icntrs commented Jun 16, 2022

Little tinny thing :
$ grep some-string /non/existent/file | sort
grep: /non/existent/file: No such file or directory
% echo $? --> $ echo $?
0

very useful info, thanks :)

@LukeSavefrogs
Copy link

LukeSavefrogs commented Oct 6, 2022

This has already been said by others (like @jhult), but i'll leave my 2 cents here...

I suggest you not to use -e without knowing its side effects.

The problem

I had a script which used the arithmetic expansion to increase a counter through the ((counter++)) syntax. It kept exiting without ever reaching the end, and it seemed it failed always at the same point. So i put set -x inside the function that was the cause and discovered that the culprit was this line: ((variable++)).

Well... Turns out that arithmetic expansion returns the number, so when it returns 1 (error return code) it caused the entire script to stop:

$ i=-1;

$ ((i++)) && echo Succeed || echo Fail; echo $i;
Succeed
0

$ ((i++)) && echo Succeed || echo Fail; echo $i;
Fail
1

$ ((i++)) && echo Succeed || echo Fail; echo $i;
Succeed
2

If i use the ((++i)) construct, instead:

$ i=-1;

$ ((++i)) && echo Succeed || echo Fail; echo $i;
Fail
0

$ ((++i)) && echo Succeed || echo Fail; echo $i;
Succeed
1

$ ((++i)) && echo Succeed || echo Fail; echo $i;
Succeed
2

The same goes for ((i+=1)):

$ i=-1;

$ ((i+=1)) && echo Succeed || echo Fail; echo $i;
Fail
0

$ ((i+=1)) && echo Succeed || echo Fail; echo $i;
Succeed
1

Sure, there are other ways to do arithmetic operations that don't trigger the error, but this is something to keep in mind..

Considerations

You need to be extra careful when using set -e, since it can lead to all kind of unexpected results if you use constructs you don't fully know.
It is good to use while testing, since it is a more "destructive" way to test your scripts, but i wouldn't use it in production.
Instead of relying on set -e to exit on errors, learn to catch errors by checking the return code and exiting from the script where and when you want it to

@frankmalin
Copy link

@LukeSavefrogs thanks, I had encountered the failure on my loop, and got past the issue w/ || true and never understood the root cause (which when explained makes logical sense).

@LukeSavefrogs
Copy link

LukeSavefrogs commented Dec 1, 2022

@LukeSavefrogs thanks, I had encountered the failure on my loop, and got past the issue w/ || true and never understood the root cause (which when explained makes logical sense).

Happy it was useful to someone else too 😃

You're right, i forgot to add that you can "bypass" this behaviour when using set -e by adding || true after the arithmetial operation:

((i+=1)) || true

Thanks for pointing that out 👍

@ernstki
Copy link

ernstki commented Aug 15, 2023

There are valid use cases for mucking with IFS. But I have mixed feelings about the argument made for setting IFS=$'\n\t', in the original blog post1 that served as raw material for this Gist.

For one thing, it breaks the normal (and very useful) behavior of "${array[*]}", which yields all elements of array as a single string, separated by the first character of IFS; by default, a space. You could be forgiven for messing with IFS if you weren't aware of this behavior, and maybe the blog post's author wasn't at that time.

As for preserving whitespace within array elements, that use case is better accommodated by simply double-quoting your variables when they're expanded:

array=("the rain" "in Spain" lies mainly "on the plain")

# "${array[@]}" yields each array element individually double-quoted
for var in "${array[@]}"; do
    touch "$var.txt"  # or "${var}.txt"
done

Result:

$ ls -1 *.txt
in Spain.txt
lies.txt
mainly.txt
on the plain.txt
the rain.txt

Double-quoting variable expansions is a Bash scripting "best practice" anyway—at least until you're proficient enough to understand the few times when it's not required.

The downside is it requires a bit more discipline on the part of you, the programmer. The upside is you will write more robust code, and won't be scratching your head when you happen upon a filename that contains a literal tab or newline, which would otherwise crash your script.

See also

Footnotes

  1. "Use the Unofficial Bash Strict Mode (Unless You Looove Debugging)" by Aaron Maxwell (Wayback Machine link)

@doolio
Copy link

doolio commented Sep 17, 2023

Is it just me or is the markdown bullet formatting in the set -e section off? As it stands it doesn't read very well.

@ernstki
Copy link

ernstki commented Sep 17, 2023

Is it just me or is the markdown bullet formatting in the set -e section off? As it stands it doesn't read very well.

@doolio Try the original blog post, which reads as prose, and may be clearer.

Do not be alarmed if your browser throws an alert about the blog being http-only. It's a blog. Not a bank. ;)

@doolio
Copy link

doolio commented Sep 18, 2023

Thanks @ernstki.

@snpefk
Copy link

snpefk commented Feb 21, 2024

Some parts of the gist produce strange text due to ambiguity in Markdown parsing, displaying in some places what appears to be LaTeX code instead of regular text. Could you please wrap the code segments with the ` symbol?

Here is the updated text in "set -u" section

  • Affects variables. When set, a reference to any variable you haven't previously defined - with the exceptions of $* and $@ - is an error, and causes the program to immediately exit. Languages like Python, C, Java and more all behave the same way, for all sorts of good reasons. One is so typos don't create new variables without you realizing it. For example:

And another in IFS section

  • In the first for loop, IFS is set to $' '. The $'...' syntax creates a string, with backslash-escaped characters replaced with special characters - like "\t" for tab and "\n" for newline.) Within the for loops, x and y are set to whatever bash considers a "word" in the original sequence.

@mohanpedala
Copy link
Author

Some parts of the gist produce strange text due to ambiguity in Markdown parsing, displaying in some places what appears to be LaTeX code instead of regular text. Could you please wrap the code segments with the ` symbol?

Here is the updated text in "set -u" section

  • Affects variables. When set, a reference to any variable you haven't previously defined - with the exceptions of $* and $@ - is an error, and causes the program to immediately exit. Languages like Python, C, Java and more all behave the same way, for all sorts of good reasons. One is so typos don't create new variables without you realizing it. For example:

And another in IFS section

  • In the first for loop, IFS is set to $' '. The $'...' syntax creates a string, with backslash-escaped characters replaced with special characters - like "\t" for tab and "\n" for newline.) Within the for loops, x and y are set to whatever bash considers a "word" in the original sequence.

Thank you, I have updated the gist.

@maksymsan
Copy link

If you set pipefail, you may want to handle processes terminated by SIGPIPE. In the following example, grep closes the pipe after finding a first match, causing cat to terminate with SIGPIPE. The status of the pipeline is returned as an error, while the pipeline works as expected:

$ set -o pipefail
$ cat pg1112.txt | grep -m1 Romeo || echo pipe status: "${PIPESTATUS[@]}"
The Project Gutenberg EBook of Romeo and Juliet, by William Shakespeare
pipe status: 141 0
$ 

@ko1nksm
Copy link

ko1nksm commented Aug 17, 2024

Example of how to ignore SIGPIPE with any command

igpipe() {
  case $- in
    *e*) set +e; (set -e; "$@"); set -e -- $? ;;
    *) ("$@"); set -- $? ;;
  esac
  [ "$1" -ge 128 ] || return "$1"
  [ "$(kill -l "$1")" = PIPE ] || return "$1"
}
$ set -o pipefail

$ seq 1000000 | grep -q 1
$ echo "$? : ${PIPESTATUS[@]}"
141 : 141 0

$ igpipe seq 1000000 | grep -q 1
$ echo "$? : ${PIPESTATUS[@]}"
0 : 0 0

@LukeSavefrogs
Copy link

LukeSavefrogs commented Aug 17, 2024

Example of how to ignore SIGPIPE with any command

igpipe() {
  case $- in
    *e*) set +e; (set -e; "$@"); set -e -- $? ;;
    *) ("$@"); set -- $? ;;
  esac
  [ "$1" -ge 128 ] || return "$1"
  [ "$(kill -l "$1")" = PIPE ] || return "$1"
}
$ set -o pipefail
$ seq 1000000 | grep -q 1
$ echo "$? : ${PIPESTATUS[@]}"
141 : 141 0

$ igpipe seq 1000000 | grep -q 1
0 : 0 0

Only you could come up with such a solution 😄👏 @ko1nksm

@rhoog-ine
Copy link

export SHELLOPTS=pipefail should appear in EVERY Makefile! Or rather make should export it by default.

@snukone
Copy link

snukone commented Oct 24, 2024

Thx a lot for this gist

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment