Sample Header Ad - 728x90

zsh script prompt reading single keystroke: add newline when appropriate?

4 votes
2 answers
3155 views
I have a script that takes an -i flag (→ $interactive) and if set, asks a yes/no-default-no question for each target, à la rm -i and others. Zsh’s read -q is designed for this exact case—it accepts a single key and sets the variable to y if the key was y or Y, otherwise setting it to n. My issue is that I’m doing the prompt in a loop. So, by itself, it would print my prompt repeatedly on the same line. That is, this code:
# moshPids is an array of pids
for p in $moshPids; do
  if [[ -n $interactive ]]; then
    read -q "answer?Kill $p? (N/y)"
    [[ "${answer}" == 'n' ]] && continue
  fi
  # do_kill set to print “killing $1” in debug
  do_kill $p
done
results in:
$ myCmd
Kill 123? (N/y)nKill 456? (N/y)nKill 789? (N/y)yKilling 789
$
There are many solutions to this—some in answers on this very site—such as including a newline in the prompt, as so:
for p in $moshPids; do
  if [[ -n $interactive ]]; then
    read -q "answer?Kill $p? (N/y)
"
    [[ "${answer}" == 'n' ]] && continue
  fi

  do_kill $p
done
which results in:
$ myCmd
Kill 123? (N/y)
nKill 456? (N/y)
nKill 789? (N/y)
yKilling 789
$
This seems ugly to me. Another solution is adding echo >&2 after the read command, which—at least at first—seems perfect:
$ myCmd
Kill 123? (N/y)n
Kill 456? (N/y)y
Killing 456
Kill 789? (N/y)n
$
However, if you accept the default with ⏎, you get blank lines ( not shown in actual output, but added to show input):
$ myCmd
Kill 123? (N/y)⏎

Kill 456? (N/y)y
Killing 456
Kill 789? (N/y)⏎

$
### Criteria So: I want a single-key response that: - Doesn’t result in multiple prompts/answers printed on a single line - Doesn’t result in blank lines when ⏎ is pressed to accept defaults - Still preserves the behavior of read -q: sets answer to y if the key was y or Y; otherwise sets it to n. ### Solution 1 Here’s my first solution:
for p in $moshPids; do
  if [[ -n $interactive ]]; then
    read -k1 "answer?Kill $p? (N/y)"
    [[ $answer != $'\n' ]] && echo >&2
    [[ "${answer}" =~ '[yY]' ]] || continue
  fi

  do_kill $p
done
Here, -k1 instead of -q is passed to read so that it instead gets the behavior of “read one character, any character”. That way, I can test for the character matching newline. If it doesn’t ($answer != $'\n'), I can just print the missing newline. But now $answer will be set to any pressed key, not just to y or n. So we must check for y or Y ("${answer}" =~ '[yY]'). Is there a better way to handle this? ### Solution 2 I've also thought about this, using stty calls to temporarily disable keyboard echoing:
zsh
for p in $moshPids; do
  if [[ -n $interactive ]]; then
    stty -echo
    read -q "answer?Kill $p? (N/y)"
    stty echo
    echo >&2
    [[ "${answer}" == 'n' ]] && continue
  fi

  do_kill $p
done
which, by disabling the visibility of the keyed response, may or may not be preferable depending on whether you're prioritizing pretty output or being able to see the input in scrollback. (In this case, of a yes/no question where the yes option always prints something, it seems fine to noecho. One could always add an echo -n "${answer}" after the read if desired, with some sort of expansion incantation to turn newline into a blank.) It's a line longer than the first solution, but is perhaps easier to understand since it uses read -q so doesn't have to test against a regex, and unconditionally echos the newline. But on the other hand, it _does_ monkey with the terminal; I’ve put the two commands close enough to each other than there shouldn’t be _too_ large a time window for command interruption to result in terminal insanity, but it is still a concern. (Also, for completeness, I should add a check that -i isn’t being called non-interactively—otherwise, the stty and read operations will fail.) Are there any even better or more idiomatic solutions than the two I've presented? This seems like an obvious behavior that many "-i for interactive" commands (most written in something other than the shell, of course) follow.
Asked by Trey (292 rep)
Feb 3, 2020, 10:49 PM
Last activity: Jun 14, 2023, 02:37 PM