Skip to content

Ongoing: Handle divergences between MacOS vs Ubuntu when writing/appending to redirected file descriptors #283

@balupton

Description

@balupton

for details see:
https://gist.github.com/balupton/cd779f3a39507f75d5956a67e5543ab8

for the failed Dorothy test:
https://github.com/bevry/dorothy/actions/runs/13254558267/job/36998982191#step:2:502

dorothy-config calls echo-lines-before with >"$temp_filepath" as it is a new file, and echo-lines-after with >>"$temp_filepath"
https://github.com/bevry/dorothy/blob/dev/devilbird/commands/dorothy-config#L234
https://github.com/bevry/dorothy/blob/dev/devilbird/commands/dorothy-config#L265
however, because of differences in ubuntu vs macos, on ubuntu this results in nothing being written with the echo-lines-before, and eventually results in file starting with a )

the cause of this is that eval-lines-before and eval-lines-after output each line individually:

__print_lines "$line"

this is handled by stdinargs.bash, which calls bash.bash:eval_capture
which eval_capture --statusvar=status -- "$@" is doing an errexit compatible form of "$@" >/dev/stdout || status=$?

eval_capture --statusvar=status -- "$@"

the >/dev/stdout is the critical piece, and is leftover as a convenience, as --stdoutvar=... --outputvar=... and --stdoutpipe=... --outputpipe=... all allow easy abilities to store in a variable and/or pipe/redirect the stdout/stderr/both content to different locations
local item cmd=() exit_status_local exit_status_variable='exit_status_local' stdout_variable='' stderr_variable='' output_variable='' stdout_pipe='/dev/stdout' stderr_pipe='/dev/stderr'

eval_capture_wrapper >"$stdout_pipe" 2>"$stderr_pipe"

for what can be done:

buffering

Instead of streaming output, that is to say outputting each line as we have them, which causes each line to go through the redirect flow, which is an expensive operation, as each line in this example would have to go through each tee, whereas with buffering, the flow needs to go through each only once.
https://gist.github.com/balupton/9ceaf968d46378e4bed714a3df128676#file-04-multi-experiments-L12-L20

It allows an easy way to opt-out of this new default with say an introduction of a printf '%s\n' one two three | echo-lines --no-buffer.

It reduces the need for echo-wait everywhere, and as such reduces fragility, surface area, and possible sigpipe failures, where the pipe reader has decided it is done on an earlier than all output, causing further writes to fail to write as there is no reader.

put echo-wait everywhere

This keeps the default behaviour as one that is fragile and divergent between systems, and needs to be explicitly opted-in, which means problems only get fixed retroactively.

use numbered file descriptors instead of /dev/stdout, /dev/stderr

This is good, however, on bash version 4.1 it requires fds between 3-9 of which conflicts could arise, or some way to detect availability. Bash v4.1 provides an easy solution for this:
https://gist.github.com/balupton/cd779f3a39507f75d5956a67e5543ab8#file-03-the-core-issue-L298-L311

for general code, this would mean go through everywhere and replace say >/dev/stdout with >&1 and >/dev/stderr with >&2. Using shopt -o noclobber can enforce this, to prevent mistakes.

for eval_capture the simplest solution for convergence, would be to drop the *pipe=<target> handling, and have them always write to >&1 and >&2, and let the caller sort out the redirections, including those to /dev/null.

If however eval_capture was to add convergence to its continued support for *pipe=<target> handling, it could so like so:

# this fixes where it goes, but say if /dev/file is provided, then it doesn't fix whether it appends or overwrites
#   which would then require say a [--append-stdout] flag or something to handle
#   and as ubuntu seems to discard appending-file-descriptors [exec 3>>"$file"] this is a problem
# as such it still seems that ultimately, eval_capture should only write to [>&1] and [>&2]
#   and leave any further redirection to the caller

if [[ -z $stdout_target || $stdout_target === '/dev/stdout' ]]; then
	stdout_target=1
elif [[ $stdout_target === '/dev/stderr' ]]; then
	stdout_target=2
else
	# a file path, or say /dev/tty
	exec 3>"$stdout_target"
	stdout_target=3
fi
eval "$command" >&${stdout_target}
exec >&{stdout_target}-


if [[ -z $stdout_target ||  $stdout_target === '/dev/stdout' ]]; then
	exec 3>1
elif [[ $stdout_target === '/dev/stderr' ]]; then
	exec 3>2
else
	# a file path, or say /dev/tty
	exec 3>"$stdout_target"
fi
eval "$command" >&3
exec >&3-


if [[ -z $stdout_target ||  $stdout_target === '/dev/stdout' ]]; then
	exec {STDOUT_FD}>1
elif [[ $stdout_target === '/dev/stderr' ]]; then
	exec {STDOUT_FD}>2
else
	# a file path, or say /dev/tty
	exec {STDOUT_FD}>"$stdout_target"
fi
eval "$command" >&${STDOUT_FD}
exec >&{STDOUT_FD}-

the problem with any change here, is that it doesn't handle the situation where the target is an actual file path, in which case there needs to be a --append flag and special handling to differentiate between overwrite and append operations.


as it is not clear yet to me whether the ubuntu handling or the macos handling is the correct expected behaviour, the buffering proposal seems the best one at this point, and is what I will do so I can continue with the release cadence of dorothy.

Metadata

Metadata

Assignees

Labels

ongoingOngoing efforts of incremental improvements

Type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions