Issue
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 Sans
succeeds - the 2nd
grep CrazyStuff
fails - the 3rd
grep Sans
succeeds - the 4th
grep CrazyStuff
fails
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 echo
ing 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.
Solution
The problem (as others have pointed out) is that grep -q
exits (closing its end of the pipe) before fc-list
has finished writing to its end of the pipe, so fc-list
gets a SIGPIPE signal when it tries to continue writing, and exits with an error.
The only reason this doesn't happen with echo
is that echo
sends its output faster, so it finishes writing before grep
has time to find its match and exit. If echo
were slower, grep
were quicker, or the output large enough (especially, large enough to fill the pipe's buffer, so echo
couldn't write everything immediately), you'd see this problem with echo
as 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 somepattern
withgrep somepattern >/dev/null
; this gives the same status indicationgrep -q
would, but it always reads the entire input (and prints matches, but the redirect discards that).Similarly, you could replace
head -n30
withsed -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).
Answered By - Gordon Davisson Answer Checked By - Katrina (WPSolving Volunteer)