如何在Linux shell脚本中提示用户输入Yes/No/Cancel?

1861
我想暂停shell脚本中的输入,并提示用户进行选择,例如标准的“是”、“否”或“取消”类型的问题。在典型的bash提示符下,如何实现这一点?

40
请注意:提示符的惯例是如果您提供了 [yn] 选项,则大写字母是默认值。即[Yn] 默认为“yes”,[yN] 默认为“no”。请参见https://ux.stackexchange.com/a/40445/43532 - Tyzoid
4
如果有从ZSH过来的人,请参考这个答案以了解如何使用read命令进行提示。 - smac89
2
你也可以考虑查看我在 U&L.SE 上的相关问答,了解在 bash 中暂停的规范方式。提供的结果可以轻松转移。 - Cadoiz
39个回答

23

使用read命令:

echo Would you like to install? "(Y or N)"

read x

# now check if $x is "y"
if [ "$x" = "y" ]; then
    # do something here!
fi

然后你还需要所有其他必需品。


21
为了获得一个类似ncurses的漂亮的输入框,使用以下命令dialog

#!/bin/bash
if (dialog --title "Message" --yesno "Want to do something risky?" 6 25)
# message box will have the size 25x6 characters
then 
    echo "Let's do something risky"
    # do something risky
else 
    echo "Let's stay boring"
fi

默认情况下,SUSE Linux至少预装了对话框(dialog)软件包。它的外观如下图所示:the "dialog" command in action

还有一个 --defaultno 参数,以确保默认选择“否”选项。 - WiMantis

21

这个解决方案读取单个字符,并在“是”的响应上调用一个函数。

read -p "Are you sure? (y/n) " -n 1
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
    do_something      
fi

2
@Jav echo 在你的响应后打印一个新行。如果没有它,下一个要打印的内容将立即出现在同一行上。尝试删除 echo 以查看效果。 - Dennis

21

使用 LC_MESSAGES 区域设置类别的条目,可以在 POSIX shell 中处理与区域设置相关的“是/否选择”; 该类别提供了现成的正则表达式模式来匹配输入,并提供本地化的“是/否”字符串。

#!/usr/bin/env sh

# Getting LC_MESSAGES values into variables
# shellcheck disable=SC2046 # Intended IFS splitting
IFS='
' set -- $(locale LC_MESSAGES)

yesexpr="$1"
noexpr="$2"
yesstr="$3"
nostr="$4"
messages_codeset="$5" # unused here, but kept as documentation

# Display Yes / No ? prompt into locale
echo "$yesstr / $nostr ?"

# Read answer
read -r yn

# Test answer
case "$yn" in
# match only work with the character class from the expression
  ${yesexpr##^}) echo "answer $yesstr" ;;
  ${noexpr##^}) echo "answer $nostr" ;;
esac

编辑:

@Urhixidur他的评论中所提到的:

不幸的是,POSIX仅规定了前两个(yesexprnoexpr)。在Ubuntu 16上,yesstrnostr为空。

参见:https://www.ee.ryerson.ca/~courses/ele709/susv4/xrat/V4_xbd_chap07.html#tag_21_07_03_06

LC_MESSAGES

yesstrnostr区域设置关键字,以及YESSTRNOSTR langinfo项以前用于匹配用户肯定和否定响应。在POSIX.1-2008中,扩展的正则表达式yesexprnoexprYESEXPRNOEXPR已取代它们。应用程序应使用基于区域设置的通用消息传递功能来发出包含所需响应示例的提示消息。

或者使用Bash方式使用区域设置:

#!/usr/bin/env bash

IFS=$'\n' read -r -d '' yesexpr noexpr _ < <(locale LC_MESSAGES)

printf -v yes_or_no_regex "(%s)|(%s)" "$yesexpr" "$noexpr"

printf -v prompt $"Please answer Yes (%s) or No (%s): " "$yesexpr" "$noexpr"

declare -- answer=;

until [[ "$answer" =~ $yes_or_no_regex ]]; do
  read -rp "$prompt" answer
done

if [[ -n "${BASH_REMATCH[1]}" ]]; then
  echo $"You answered: Yes"
else
  echo $"No, was your answer."
fi

使用本地环境提供的正则表达式来匹配答案。

要翻译剩余的信息,请使用 bash --dump-po-strings scriptname 输出用于本地化的po字符串:

#: scriptname:8
msgid "Please answer Yes (%s) or No (%s): "
msgstr ""
#: scriptname:17
msgid "You answered: Yes"
msgstr ""
#: scriptname:19
msgid "No, was your answer."
msgstr ""

我喜欢添加了一种语言不可知选项。干得好。 - Myrddin Emrys
1
不幸的是,POSIX 只指定了前两个(yesexpr 和 noexpr)。在 Ubuntu 16 上,yesstr 和 nostr 是空的。 - Urhixidur
2
但是等等!更糟糕的消息是,bash case语句表达式不是正则表达式,而是文件名表达式。因此Ubuntu 16的yesexpr和noexpr(分别为“ ^ [yY] .*”和“ ^ [nN] .*”)将因嵌入的句点而彻底失败。在正则表达式中,“.*”表示“任何非换行符字符,零次或多次”。但在case语句中,它是一个字面上的“。”后跟任意数量的字符。 - Urhixidur
1
最后,在shell环境中可以使用yesexprnoexpr的最佳方法是在Bash的特定正则表达式匹配中使用它,例如:if [[ "$yn" =~ $yesexpr ]]; then echo $"Answered yes"; else echo $"Answered no"; fi - Léa Gris
我使用的是SUSE Tumbleweed,local LC_MESSAGES显示为 ^[+1yY] ^[-0nN] yes no UTF-8。有趣的是,这些正则表达式接受了+/-和0/1。也许它们期望一个机器人来使用提示。 - foxesque
我正在使用SUSE Tumbleweed,并且local LC_MESSAGES显示^[+1yY] ^[-0nN] yes no UTF-8。有趣的是,这些正则表达式接受+/-和0/1。也许它们期望一个机器人使用提示符。 - undefined

16

在我的情况下,我需要从一个已下载的脚本中读取,即:

curl -Ss https://example.com/installer.sh | sh

在这种情况下,read -r yn </dev/tty 这一行允许它读取输入。

printf "These files will be uploaded. Is this ok? (y/N) "
read -r yn </dev/tty

if [ "$yn" = "y" ]; then
   
   # Yes
else
   
   # No
fi

其中一个重要的部分是输入验证。我认为将我的第一个示例适应接受tty输入,就像你所做的那样,也会对你有所帮助,并且还可以在输入错误时进行循环(想象一下缓冲区中有几个字符;你的方法将强制用户始终选择“否”)。 - Myrddin Emrys

13

只需按一次键

以下是一个更长的、可重复使用和模块化的方法:

  • 返回0=是 和 1=否
  • 无需按Enter键 - 只需按一个字符
  • 可以按Enter键接受默认选择
  • 可以禁用默认选择以强制进行选择
  • 适用于zshbash

在按下Enter时默认为"否"

请注意,N要大写。这里按下了Enter键,接受了默认值:

$ confirm "Show dangerous command" && echo "rm *"
Show dangerous command [y/N]?

还要注意,[y/N]?已被自动添加。 默认情况下,“不”被接受,因此没有任何回显。

重新提示,直到给出有效的响应:

$ confirm "Show dangerous command" && echo "rm *"
Show dangerous command [y/N]? X
Show dangerous command [y/N]? y
rm *

按下回车键默认为“是”

请注意,Y 是大写的:

$ confirm_yes "Show dangerous command" && echo "rm *"
Show dangerous command [Y/n]?
rm *

上面,我只是按了回车键,所以命令运行了。

回车键不是默认选项 - 需要输入 yn

$ get_yes_keypress "Here you cannot press enter. Do you like this [y/n]? "
Here you cannot press enter. Do you like this [y/n]? k
Here you cannot press enter. Do you like this [y/n]?
Here you cannot press enter. Do you like this [y/n]? n
$ echo $?
1

这里返回了1false。请注意,使用这个较低级别的函数,您需要提供自己的[y/n]?提示。

代码

# Read a single char from /dev/tty, prompting with "$*"
# Note: pressing enter will return a null string. Perhaps a version terminated with X and then remove it in caller?
# See https://unix.stackexchange.com/a/367880/143394 for dealing with multi-byte, etc.
function get_keypress {
  local REPLY IFS=
  >/dev/tty printf '%s' "$*"
  [[ $ZSH_VERSION ]] && read -rk1  # Use -u0 to read from STDIN
  # See https://unix.stackexchange.com/q/383197/143394 regarding '\n' -> ''
  [[ $BASH_VERSION ]] && </dev/tty read -rn1
  printf '%s' "$REPLY"
}

# Get a y/n from the user, return yes=0, no=1 enter=$2
# Prompt using $1.
# If set, return $2 on pressing enter, useful for cancel or defualting
function get_yes_keypress {
  local prompt="${1:-Are you sure [y/n]? }"
  local enter_return=$2
  local REPLY
  # [[ ! $prompt ]] && prompt="[y/n]? "
  while REPLY=$(get_keypress "$prompt"); do
    [[ $REPLY ]] && printf '\n' # $REPLY blank if user presses enter
    case "$REPLY" in
      Y|y)  return 0;;
      N|n)  return 1;;
      '')   [[ $enter_return ]] && return "$enter_return"
    esac
  done
}

# Credit: http://unix.stackexchange.com/a/14444/143394
# Prompt to confirm, defaulting to NO on <enter>
# Usage: confirm "Dangerous. Are you sure?" && rm *
function confirm {
  local prompt="${*:-Are you sure} [y/N]? "
  get_yes_keypress "$prompt" 1
}    

# Prompt to confirm, defaulting to YES on <enter>
function confirm_yes {
  local prompt="${*:-Are you sure} [Y/n]? "
  get_yes_keypress "$prompt" 0
}

当我测试这个脚本时,我得到的输出不是上面的内容,而是 Show dangerous command [y/N]? [y/n]?Show dangerous command [Y/n]? [y/n]? - Ilias Karim
谢谢@IliasKarim,我刚刚修复了那个问题。 - Tom Hale

13

您可以在read上使用默认的REPLY,转换为小写并与一组变量进行比较。


脚本还支持ja/si/oui

read -rp "Do you want a demo? [y/n/c] "

[[ ${REPLY,,} =~ ^(c|cancel)$ ]] && { echo "Selected Cancel"; exit 1; }

if [[ ${REPLY,,} =~ ^(y|yes|j|ja|s|si|o|oui)$ ]]; then
   echo "Positive"
fi

12
read -e -p "Enter your choice: " choice

-e选项使用户可以使用箭头键编辑输入。

如果您想使用建议作为输入:

read -e -i "yes" -p "Enter your choice: " choice

-i选项会打印一个提示性的输入。


yap,“-e”和“-i”在sh(Bourne shell)中不起作用,但问题特别标记为bash。 - Jahid

11

对于这个问题,有很多好的答案,但从我所看到的来看,没有一个是我理想的,它应该:

  1. 简单,只需要几行shell代码
  2. 可以使用单一的y/n按键(无需按回车键)
  3. 如果您只是按下回车键,则默认为yes
  4. 也可以使用大写的Y/N

以下是符合上述要求的版本:

read -n1 -p "Continue? (Y/n) " confirm

if ! echo $confirm | grep '^[Yy]\?$'; then
  exit 1
fi

您可以修改该条件语句仅在“是”时运行(只需在 if 语句中删除!),或者添加一个 else ,以便在两个分支上运行代码。

1
干得好!捐赠旧问题永远不会太晚。我很感激单键(y、n或enter)回答。我认为你应该包括else版本,因为我的原始问题是一个完整的三分支问题(如果你把“取消”算作“退出”)。 - Myrddin Emrys

10

一句话概括:

read -p "Continue? [Enter] → Yes, [Ctrl]+[C] → No."

假设"No"和"Cancel"的结果相同,所以没有理由对它们进行不同的处理。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接