I'm losing my sanity here...
Suppose that I want to echo the word "Sans", and then check with grep whether it contains the substring "Sans" or "CrazyStuff":
#!/bin/bash
echo Sans | grep -q Sans && echo 'y ' || echo ' n'
echo Sans | grep -q CrazyStuff && echo 'y ' || echo ' n'
set -o pipefail
echo Sans | grep -q Sans && echo 'y ' || echo ' n'
echo Sans | grep -q CrazyStuff && echo 'y ' || echo ' n'
As expected, "Sans" contains "Sans", and does not contain "CrazyStuff", so regardless of the pipefail settings,
- the 1st
grep Sanssucceeds - the 2nd
grep CrazyStufffails - the 3rd
grep Sanssucceeds - the 4th
grep CrazyStufffails
The output is:
y
n
y
n
So far so good.
Now, let's replace the constant string "Sans" by the output of fc-list (it lists installed fonts; You'll probably also have some Sans-Serif font, so it should contain Sans):
#!/bin/bash
captured="$(fc-list)"
echo "$captured" | grep -q Sans && echo 'y ' || echo ' n'
echo "$captured" | grep -q CrazyStuff && echo 'y ' || echo ' n'
set -o pipefail
echo "$captured" | grep -q Sans && echo 'y ' || echo ' n'
echo "$captured" | grep -q CrazyStuff && echo 'y ' || echo ' n'
Since the $captured output contains the substring Sans, this program behaves exactly as the first one, the output is again:
y
n
y
n
Now, the daredevil stunt: instead of echoing the $captured output of fc-list, we simply invoke fc-list four times:
#!/bin/bash
fc-list | grep -q Sans && echo 'y ' || echo ' n'
fc-list | grep -q CrazyStuff && echo 'y ' || echo ' n'
set -o pipefail
fc-list | grep -q Sans && echo 'y ' || echo ' n'
fc-list | grep -q CrazyStuff && echo 'y ' || echo ' n'
Obviously, it must behave in exactly the same way as the previous two examples, so unsurprisingly, the output is
y
n
n
n
...wait, what?
How can replacing the constant $captured output of fc-list by actual invocations of fc-list change anything at all? It's always just returning exactly the same list of fonts every time.
Can someone please explain what is going on here?
Even more importantly: how do I fix it?
Thanks in advance.
The problem (as others have pointed out) is that
grep -qexits (closing its end of the pipe) beforefc-listhas finished writing to its end of the pipe, sofc-listgets a SIGPIPE signal when it tries to continue writing, and exits with an error.The only reason this doesn't happen with
echois thatechosends its output faster, so it finishes writing beforegrephas time to find its match and exit. Ifechowere slower,grepwere quicker, or the output large enough (especially, large enough to fill the pipe's buffer, soechocouldn't write everything immediately), you'd see this problem withechoas well. This type of timing dependent behavior is known as a "race condition", and rather than trying to fix the timing, the correct solution is to write code that isn't timing-dependent.For pipe failures like this, there are several possible solutions:
Don't use
pipefail. It has an annoying tendency to cause problems like this, especially if you don't fully understand how the programs in a pipe work & interact with each other and the pipe.Avoid using things that don't read their entire input.
In this case, you could replace
grep -q somepatternwithgrep somepattern >/dev/null; this gives the same status indicationgrep -qwould, but it always reads the entire input (and prints matches, but the redirect discards that).Similarly, you could replace
head -n30withsed -n "1,30 p".Force success status for commands in the middle of the pipe, e.g. replacing
... | fc-list | ...with... | (fc-list || true) | ...(but note that this suppresses all errors fromfc-list, not just pipe failures).