$> set -o pipefail
$> fortune | head -1 > /dev/null && echo "ΠΠΎΠ²Π΅Π·Π»ΠΎ!" || echo "ΠΡ ΠΏΡΠΎΠΈΠ³ΡΠ°Π»ΠΈ"
ΠΠΎΠ²Π΅Π·Π»ΠΎ!
$> fortune | head -1 > /dev/null && echo "ΠΠΎΠ²Π΅Π·Π»ΠΎ!" || echo "ΠΡ ΠΏΡΠΎΠΈΠ³ΡΠ°Π»ΠΈ"
ΠΡ ΠΏΡΠΎΠΈΠ³ΡΠ°Π»ΠΈ
Here fortune
conditional program without exit(rand())
.
Can you explain what is wrong here?
Lyrical-historical digression
The first time I met this Heisenbug was a quarter of a century ago. Then for the gateway in FaxNET it was required to make several utilities through
Diligence in "careful error handling" then added to me the previous experience of dealing with bugs in sendmail and uucp / uupc. It makes no sense to dive into the details of that story, but I fought this Heisenbug for two weeks at 10-14 hours. Therefore, it was remembered, and yesterday this old acquaintance again looked for a visit.
TL;DR Reply
Utility head
can close the channel fortune
at once as soon as it reads the first line. If fortune
outputs more than one line, then the appropriate call write()
will return an error, or report fewer bytes than requested. In turn, written with careful error handling fortune
may reflect this situation in its exit status. Then due to setting set -o pipefail
will work || echo "ΠΡ ΠΏΡΠΎΠΈΠ³ΡΠ°Π»ΠΈ"
.
But, head
may not have time close before fortune
finish outputting data. Then it will work && echo "ΠΠΎΠ²Π΅Π·Π»ΠΎ!"
.
In one of my today GNUMakefile
echo '#define MDBX_BUILD_COMPILER "$(shell set -o pipefail; $(CC) --version | head -1 || echo 'Please use GCC or CLANG compatible compiler')"'
Translated into human
Here the usual --version
it is asked who he is, and if the option is not supported, then a stub is substituted "Please use GCC or CLANG compatible compiler".
Like
#define MDBX_BUILD_COMPILER "lcc:1.23.20:Sep--4-2019:e2k-v3-linux Please use GCC or CLANG compatible compiler"
Frankly, I did not immediately see the old "acquaintance". Moreover, the project has already been repeatedly tested on Elbrus and under a lot of different distributions, including Alt. With different compilers, versions of GNU Make and bash. Therefore, I did not want to see my oversight here.
When trying to reproduce the problem and / or understand what the matter is, there are more oddities.
Command line spell:
echo "#define MDBX_BUILD_COMPILER '$(set -o pipefail; LC_ALL=C cc --version | head -1 || echo "Please use GCC or CLANG compatible compiler")'"
Through-time it gave out an extra text, then no ... Often one of the options stuck for a long time, but if you poke a little longer, you always get both!
Of course, strace
By the way, like any self-respecting strace
prefers not to reproduce.
So what's going on?
- Utility
head
has the right (or rather even forced) to close the readable channel as soon as it reads the requested number of lines. - The data-generating program-writer (in this case
cc
) can output multiple lines and free do it with multiple callswrite()
. - If the reader will have time to close the channel on his side before the end of the recording on the writer's side, then the writer will receive an error.
- Program Writer right how to ignore the write error to the pipe, and reflect it in your exit code.
- Due to the installation
set -o pipefail
the pipeline termination code will be non-zero (erroneous) if the result of at least one element is non-zero, and then it will work|| echo "Please use GCC or CLANG compatible compiler"
.
There may be variations, depending on how the writer program works with signals. For example, the program may crash (with automatic generation of a non-zero/erroneous exit status), or write()
will return the result of writing fewer bytes than requested and set errno = EPIPE
.
Who is to blame?
In the described case little bit of everything. Error handling in cc
(lcc:1.23.20:Sepβ4-2019:e2k-v3-linux) is not redundant. In many cases, it's better to be safe, although this reveals sudden flaws in the boilerplate designed for traditional behavior.
What to do?
Wrong:
fortune | head -1 && echo "ΠΠΎΠ²Π΅Π·Π»ΠΎ, Π½ΠΎ Π²Ρ ΡΠΈΡΠΊΡΠ΅ΡΠ΅!" || echo "WTF?"
Correctly:
-
(fortune && echo "Π£ΡΠΏΠ΅ΡΠ½ΠΎ" || echo "ΠΡΠΈΠ±ΠΊΠ°") | head -1
Here, early closing of the pipe will be handled by the command interpreter when servicing the nested pipeline ("within" the parentheses). Accordingly, if
fortune
will report in the status about the error of writing to a closed channel, then the output|| echo "ΠΡΠΈΠ±ΠΊΠ°"
will not get anywhere, since the channel is already closed. -
fortune | cat - | head -1 && echo "Π£ΡΠΏΠ΅ΡΠ½ΠΎ" || echo "ΠΡΠΈΠ±ΠΊΠ°"
Here is the utility
cat
acts as a damper, as it ignores the errorEPIPE
when withdrawing. This is enough for nowfortune
small (several lines) and fits in the channel buffer (from 512 bytes to β64K, in most OSes β₯4K). Otherwise, the problem may return.
How to handle correctly EPIPE
and other recording errors?
There is no single correct solution, but there are simple recommendations:
EPIPE
required be sure to process (and reflected in the exit status) when outputting data requiring integrity. For example, during the work of archivers or backup utilities.EPIPE
better to ignore when displaying informational and auxiliary messages. For example, when displaying information on options--help
or--version
.- If the code being developed is allowed to be used in a pipeline before
| head
thenEPIPE
it is better to ignore, otherwise it is better to process and reflect in the exit status.
I would like to take this opportunity to thank the team
Keep it up Camarades, up
Thank you
KDPV from
Source: habr.com