zsh 脚本提示读取单个击键:在适当的时候添加换行符?

zsh 脚本提示读取单个击键:在适当的时候添加换行符?

我有一个脚本,它带有一个-i标志 (→ $interactive),如果设置了,则会对每个目标询问是/否-默认-否问题,以及rm -i其他目标。

Zshread -q就是针对这种情况而设计的——它接受单个键,y如果键是yY,则将变量设置为 ,否则将其设置为n

我的问题是我正在循环执行提示。因此,它本身会在同一行上重复打印我的提示。也就是说,这段代码:

# 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

结果是:

$ myCmd
Kill 123? (N/y)nKill 456? (N/y)nKill 789? (N/y)yKilling 789
$

对此有很多解决方案(其中一些在本网站的答案中),例如在提示中包含换行符,如下所示:

for p in $moshPids; do
  if [[ -n $interactive ]]; then
    read -q "answer?Kill $p? (N/y)
"
    [[ "${answer}" == 'n' ]] && continue
  fi

  do_kill $p
done

结果是:

$ myCmd
Kill 123? (N/y)
nKill 456? (N/y)
nKill 789? (N/y)
yKilling 789
$

这对我来说似乎很难看。另一个解决方案是在命令echo >&2后面添加read,至少乍一看似乎很完美:

$ myCmd
Kill 123? (N/y)n
Kill 456? (N/y)y
Killing 456
Kill 789? (N/y)n
$

但是,如果您接受默认值,您会得到空行(实际输出中未显示,但添加以显示输入):

$ myCmd
Kill 123? (N/y)⏎

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

$

标准

所以:我想要一个单键响应:

  • 不会导致在一行上打印多个提示/答案
  • 按 接受默认值时不会产生空行
  • 仍然保留 :如果键是or 则read -q设置answer为 的行为;否则将其设置为.yyYn

解决方案1

这是我的第一个解决方案:

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

这里,-k1而不是-q被传递给,read以便它改为获得“读取一个字符,任何字符”的行为。这样,我可以测试字符匹配换行符。如果没有 ( $answer != $'\n'),我可以打印缺少的换行符。

但现在$answer将被设置为任何按下的键,而不仅仅是yn。所以我们必须检查yY( "${answer}" =~ '[yY]')。

有更好的方法来处理这个问题吗?

解决方案2

我也考虑过这个问题,使用stty调用来暂时禁用键盘回显:

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

通过禁用键控响应的可见性,这可能会或可能不会更好,具体取决于您是优先考虑漂亮的输出还是能够在回滚中看到输入。 (在这种情况下,在是/否问题中,yes 选项总是打印一些内容,noecho 似乎很好。如果需要,人们总是可以在 ifecho -n "${answer}"后面添加read,并使用某种扩展咒语将换行符变成空白。)

它比第一个解决方案长一行,但可能更容易理解,因为它使用read -q所以不必针对正则表达式进行测试,并且无条件回显换行符。

但另一方面,它猴子与终端;我已经将两个命令放置得足够接近,但不应该如此命令中断的时间窗口很大,会导致终端疯狂,但这仍然是一个问题。 (另外,为了完整起见,我应该添加一个-i不以非交互方式调用的检查,否则,sttyread操作将会失败。)

还有比我提出的两个更好或更惯用的解决方案吗?这似乎是许多“-i交互式”命令(当然大多数是用 shell 以外的东西编写的)遵循的明显行为。

答案1

更新:

从手册:

read ...

-s Don't echo back characters if reading from the terminal.

它有效:

for p in $pids; do
  read -qs "?Kill $p? (N/y)"
  >&2 echo $REPLY
  case $REPLY in
    (y) do_kill $p ;;
  esac
done
  • read -q还提供返回状态:
for p in $pids; do
  if read -qs "?Kill $p? (N/y)"; then
    >&2 echo $REPLY; do_kill $p  # answer was y or Y
  else
    >&2 echo $REPLY          # answer was not y or Y
  fi
done

如果你喜欢的话。


至于处理-i设置 的选项$interactive,请将testfor放在循环$interactive之外for,例如:

interactive_kill() {
  for p in $@; do
    read -qs "?Kill $p? (N/y)"
    >&2 echo $REPLY
    case $REPLY in
      (y) do_kill $p ;;
    esac
  done
}

if [[ -n $interactive ]]; then
  interactive_kill $pids
else
  do_kill $pids   # if do_kill() needs exactly one arg wrap in a for loop
fi

还:

  • 要测试yY不需要shell 模式,因此=~您可以只使用运算符,或在语句中使用[yY][[=[yY]case
  • 在你的第一个例子中,逻辑似乎有点扭曲:
[[ "${answer}" == 'n' ]] && continue
do_kill $p

什么时候

if [[ $answer = y ]]; then
  do_kill $p
fi

更容易阅读。

答案2

我也想要你所描述的行为,但这并不简单。我写了一个辅助函数:

ask_yes_no() {
 read -qs "?$1 (y/N) "
 out=$?
 print $REPLY >&2
 return $out
}

# example usage
for thing in one two three; do
  if ask_yes_no "Do thing $thing?"; then
    echo "Do thing $thing"
  else
    echo "Nope"
  fi
done

输出示例:

Do thing one? (y/N) n
Nope
Do thing two? (y/N) y
Do thing two
Do thing three? (y/N) y
Do thing three

相关内容