Your log
function is the culprit. What it does is: read one line¹, and if the line is not empty, print a timestamp and then the line content. That's all it does: once it's processed one line, it returns.
When cp
emits a first error message, the log
function reads it and processes it. Since the log
function then returns, the process at the right-hand side of the pipe exits, which causes the reading end of the pipe to close. When cp
emits a second error message, it tries to write to a closed pipe, which causes it to die from a SIGPIPE signal. Standard error is line-buffered (by default, and cp
doesn't try to change that), so buffering doesn't come into play.
To process all lines of input, you need read
in a loop.
log () {
while IFS= read -r IN; do
echo "$datetime"$'\t'"$IN"
done | tee -a logfile >&2
}
I also fixed the read
call to IFS= read -r
to actually read one line. I removed the special handling for empty lines which is pointless (there wouldn't be empty lines in the input). I presume you put it in to handle the case when the input is empty (zero lines of input), but the correct way to handle that is to check the return status of the read
command. I also fixed log
to print to its standard error, since it's used to process error messages.
See Prepending a timestamp to each line of output from a command for other ways to do this.
Beware that putting the command on the left-hand side of a pipe has a major downside: it causes the exit status to be ignored. So if cp
fails, your script will continue happily. The errors will be logged somewhere, but subsequent commands will execute normally and nothing will alert you to the fact that you should go and read the logs. In bash, ksh or zsh, you can set the pipefail
option in addition to set -e
so that your script exits with an error status as soon as any command fails, even on the left-hand side of a pipeline.
set -o errexit -o pipefail
copy … |& log
Alternatively, use process substitution instead of a pipe to pipe the error output through another process. Process substitution has slightly different caveats than pipes; errors in log
are effectively ignored, and the command may return before log
has completed (in bash, log
runs in the background).
set -e
copy … 2> >(log)
¹ Almost, you'd need IFS= read -r IN
to never read more and not possibly mangle the line.
SIGPIPE
, when trying to pipe its output to the stdin ofecho IN | ...
, which does not consume it. Better explain what thatlog
function is supposed to do. – Mar 23 '20 at 05:29>>
? – Timothy Wong Mar 23 '20 at 05:31copy ... 2>&1 | tee -a logfile
? – Mar 23 '20 at 05:33log
i.e. a "universal" function to logstdout
intologfile
– Timothy Wong Mar 23 '20 at 05:34log(){ tee -a logfile; }
– Mar 23 '20 at 05:35log
function doesn't make much sense - if the first line is empty after field splitting, you don't write anything, but if it is, you writeIN
to a file, and in either case you ignore the rest of the input. Is that really what it's intended to do? – muru Mar 23 '20 at 05:36IN
before writing it and that's why I'm doing a check. – Timothy Wong Mar 23 '20 at 05:52IN
before writing and that's why I usedIN
in the first place... Any wqay around it while piping totee
? – Timothy Wong Mar 23 '20 at 05:54ts
– muru Mar 23 '20 at 06:22