4 ms·
First of all, that's a hack zsh really shouldn't have to do. It's not like it can see the output of the commands that it runs. That's the job of the terminal. T
by jolmg 8y ago
First of all, that's a hack zsh really shouldn't have to do. It's not like it can see the output of the commands that it runs. That's the job of the terminal. To make that '%' effect work, when a command it's waiting for dies, it outputs a '%' and then as many spaces as it knows there are columns in the terminal and then a carriage return '\r'. So it uses line-wrapping to move to the next line if there's content in the current one. That's really out of scope of what a shell should be doing. It also will not work if you're not working with the shell through a terminal, but something else like a tcp connection or a serial device. What's going to happen then is that that last "line" is going to disappear in zsh. The prompt will be rendered on top of it.
Anyway, it's not just about the shell prompt. Things are just simpler and more composable when using it as a terminator instead of a separator.
Consider programs that might every now and then have to warn you about errors, like this:
cmd() {
>&2 printf "%s-error-1" "$1"
sleep 1
>&2 printf "\n%s-error-2" "$1"
}
and them being used in pipe:
cmd foo | cmd bar
using newlines as separators causes them to merge lines like so:
foo-error-1bar-error-1
foo-error-2
bar-error-2%
if one used line terminators:
cmd() {
>&2 printf "%s-error-1\n" "$1"
sleep 1
>&2 printf "%s-error-2\n" "$1"
}
then the output would be like so:
foo-error-1
bar-error-1
foo-error-2
bar-error-2
Now consider what would happen if you need to conditionalize the first line:
cmd() {
if (( $RANDOM % 2 == 0 )); then
>&2 printf "%s-error-1" "$1"
fi
sleep 1
>&2 printf "\n%s-error-2" "$1"
}
Now, if that first line doesn't output anything, you'll get a extra, blank line. So, you need to include the newline in the conditional body:
cmd() {
if (( $RANDOM % 2 == 0 )); then
>&2 printf "%s-error-1" "$1"
>&2 printf "\n"
fi
sleep 1
>&2 printf "%s-error-2" "$1"
}
Now what if you need to conditionalize the last line?
cmd() {
if (( $RANDOM % 2 == 0 )); then
>&2 printf "%s-error-1" "$1"
>&2 printf "\n"
fi
sleep 1
if (( $RANDOM % 2 == 0 )); then
>&2 printf "%s-error-2" "$1"
end
}
should the newline go in the first or second conditional body? If it's on the first, then if the first condition is true and the second is false you'll get a trailing newline (something you don't want if you're trying to have separators).
What's the solution here? The newline will have to be added only when BOTH conditional bodies run:
cmd() {
if (( $RANDOM % 2 == 0 )); then
first_was_true=1
>&2 printf "%s-error-1" "$1"
fi
sleep 1
if (( $RANDOM % 2 == 0 )); then
if [[ $first_was_true ]]; then
>&2 printf "\n"
fi
>&2 printf "%s-error-2" "$1"
end
}
What if you have a 3rd conditional error? Well, that's a new newline that will have to be output iff the second and third ran or the first and third ran:
cmd() {
if (( $RANDOM % 2 == 0 )); then
first_was_true=1
>&2 printf "%s-error-1" "$1"
fi
sleep 1
if (( $RANDOM % 2 == 0 )); then
second_was_true=1
if [[ $first_was_true ]]; then
>&2 printf "\n"
fi
>&2 printf "%s-error-2" "$1"
end
sleep 1
if (( $RANDOM % 2 == 0 )); then
if [[ $first_was_true || $second_was_true ]]; then
>&2 printf "\n"
fi
>&2 printf "%s-error-2" "$1"
end
}
Can you see how this quickly becomes unwieldy? Each new body becomes coupled to the ones before it. The third conditional needs to know about the first and second when semantically it shouldn't need to care at all! If you use line terminators, things are much simpler:
cmd() {
if (( $RANDOM % 2 == 0 )); then
>&2 printf "%s-error-1\n" "$1"
fi
sleep 1
if (( $RANDOM % 2 == 0 )); then
>&2 printf "%s-error-2\n" "$1"
fi
sleep 1
if (( $RANDOM % 2 == 0 )); then
>&2 printf "%s-error-3\n" "$1"
fi
}
This isn't just an arbitrary Unix decision. It's objectively good design.
- eridius 8y agoWhen a job completes, it should end with a newline (if it printed anything). That's good UX. But you cannot mandate this for two big reasons: * You can't enforce this at the process level, because you may want to use multiple process invocations to build up a single line of text, or want to accept input after printing a prompt. So this logic has to live at the job level. * Programs that print data that exist somewhere else should not be adding unexpected newlines. `cat foo > bar` should produce an identical copy of foo, not a copy with a newline added. Since this has to happen at the job level, the only place this can be done is in the shell. Ideally there'd be some way to coordinate with the terminal emulator to say "if I'm not at the beginning of the line already, please add some indication of this and move the cursor to a new line", which not only means the terminal is responsible for the indicator instead of the shell, but it also enables smart behavior like being able to copy the output without including the line terminator indicator Unfortunately, terminal emulators don't have a way to do this, so the shell has to hack it together.
- deleted 8y ago[deleted]
- jolmg 8y agoI'm not sure what your stance is. If it's "individual programs should use line terminators as convention and if the shell can help improve UX in the face of programs that break convention then it should", then I agree with you. What I meant by how zsh shouldn't be doing this is that if programs and people working on the command line followed the convention of terminating newlines, then the shell wouldn't need to do that. If it's "individual programs should use line separators and the shell should add the terminating newline", then I disagree. I don't think shells and terminals are so fundamental that this behavior should depend on them. It also does nothing to address the example of `cmd foo | cmd bar` that I gave because that's 1 job.
- eridius 8y agoIt's, as I said, that programs should use line terminators, but we cannot mandate that all programs do use line terminators, so it's still the responsibility of the shell to deal with this. Specifically, it's a reaction to > First of all, that's a hack zsh really shouldn't have to do. In an ideal world, zsh wouldn't need a hack, but it would still need to send a signal to the terminal emulator to say "please ensure the cursor is at the beginning of a line; if it's not, mark this somehow so the user understands". Slightly less ideally, a simple way to say "am I at the beginning of a line?" (this is less ideal only in that now the terminal can't be smart about copying program output). But either way, it's still the responsibility of the shell to ensure its prompt ends up starting at column 0 without overwriting the previous program's output. Because no matter what conventions you establish, there will be programs that exit without printing a line terminator (even the best-behaved of programs can still do this if they use unbuffered output and you send a signal to kill the process in the middle of a line).