如何在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个回答

2028
一个广泛可用的在shell提示符下获取用户输入的方法是使用read命令。以下是一个演示:
while true; do
    read -p "Do you wish to install this program? " yn
    case $yn in
        [Yy]* ) make install; break;;
        [Nn]* ) exit;;
        * ) echo "Please answer yes or no.";;
    esac
done

另一种方法,由Steven Huwig指出,是Bash的select命令。以下是使用select的相同示例:
echo "Do you wish to install this program?"
select yn in "Yes" "No"; do
    case $yn in
        Yes ) make install; break;;
        No ) exit;;
    esac
done

使用select时,您无需对输入进行清理-它会显示可用选项,并且您只需键入与您的选择相对应的数字。它还会自动循环,因此无需使用while true循环来重试,如果输入无效。
此外,Léa Gris演示了一种使请求与语言无关的方法,可以在她的回答中找到。将我的第一个示例调整为更好地支持多种语言可能如下所示:
set -- $(locale LC_MESSAGES)
yesexpr="$1"; noexpr="$2"; yesword="$3"; noword="$4"

while true; do
    read -p "Install (${yesword} / ${noword})? " yn
    if [[ "$yn" =~ $yesexpr ]]; then make install; exit; fi
    if [[ "$yn" =~ $noexpr ]]; then exit; fi
    echo "Answer ${yesword} / ${noword}."
done

显然,这里还有其他未翻译的通信字符串(安装,回答),这需要在更完整的翻译中解决,但即使是部分翻译在许多情况下也会有所帮助。
最后,请查看优秀回答,作者是F. Hauri

40
在使用OS X Leopard的Bash时,我将exit更改为break,以防在选择“否”时关闭选项卡。 - Trey Piepmeier
1
这个选项超过了是或否,它是如何工作的?在 case 子句中,你是否需要编写类似以下的内容: 安装程序后什么都不做)make install; break; - Shawn
1
@Shawn 使用 read 命令,你可以使用任何由 bash shell 的 switch 语句支持的 glob 或 regex 模式。它会将输入文本与你的模式进行匹配,直到找到匹配项。使用 select 命令,要给出一个选择列表然后显示给用户。你可以将列表中的项目设置为任意长度。我建议查看它们的手册以获取全面的用法信息。 - Myrddin Emrys
4
如果没有循环,为什么在select中要使用break - Jayen
1
顺便说一下,我使用这个例子创建了一个脚本,我打算通过远程SSH会话触发它。我的SSH命令看起来像这样:ssh my-server 'path/to/myscript.sh'。以这种方式执行时,“read -p”命令的提示文本不会显示出来。但是,echo命令的输出确实可见。因此,对我而言,更好的解决方案是使用echo -n“是否执行某项操作?”,然后跟上read yn - Travesty3
显示剩余6条评论

694

一个通用问题至少需要五个答案。

取决于

  • 兼容:可以在具有通用环境的低端系统上工作
  • 特定:使用所谓的bashisms

如果你想的话

  • 简单的“单行”问答(通用解决方案)
  • 漂亮格式化的接口,如或更图形化的使用libgtk或libqt...
  • 使用强大的readline历史记录功能

1. POSIX通用解决方案

您可以使用read命令,后跟if ... then ... else

printf 'Is this a good question (y/n)? '
read answer

if [ "$answer" != "${answer#[Yy]}" ] ;then 
    echo Yes
else
    echo No
fi

(感谢Adam Katz的评论:用更便携且避免一个fork的新测试替换了上面的测试)

POSIX,但单键特性

但是,如果您不希望用户必须按Return键,则可以编写:

编辑:正如@JonathanLeffler所建议的那样,保存 stty 的配置可能比简单地强制它们进入健全状态更好。)

printf 'Is this a good question (y/n)? '
old_stty_cfg=$(stty -g)
stty raw -echo ; answer=$(head -c 1) ; stty $old_stty_cfg # Careful playing with stty
if [ "$answer" != "${answer#[Yy]}" ];then
    echo Yes
else
    echo No
fi

注意:此代码已在下测试通过!

相同的代码,但需要明确等待yn

#/bin/sh
printf 'Is this a good question (y/n)? '
old_stty_cfg=$(stty -g)
stty raw -echo
answer=$( while ! head -c 1 | grep -i '[ny]' ;do true ;done )
stty $old_stty_cfg
if [ "$answer" != "${answer#[Yy]}" ];then
    echo Yes
else
    echo No
fi

使用专门的工具

有许多使用libncurseslibgtklibqt或其他图形库构建的工具。例如,使用whiptail

if whiptail --yesno "Is this a good question" 20 60 ;then
    echo Yes
else
    echo No
fi

根据您的系统,您可能需要使用其他类似工具来替换whiptail

dialog --yesno "Is this a good question" 20 60 && echo Yes

gdialog --yesno "Is this a good question" 20 60 && echo Yes

kdialog --yesno "Is this a good question" 20 60 && echo Yes

20是对话框高度,以行数表示,而60是对话框宽度。所有这些工具的语法 几乎相同

DIALOG=whiptail
if [ -x /usr/bin/gdialog ] ;then DIALOG=gdialog ; fi
if [ -x /usr/bin/xdialog ] ;then DIALOG=xdialog ; fi
...
$DIALOG --yesno ...

2. Bash特定解决方案

基本的内联方法

read -p "Is this a good question (y/n)? " answer
case ${answer:0:1} in
    y|Y )
        echo Yes
    ;;
    * )
        echo No
    ;;
esac

我更喜欢使用case,这样即使需要检测yes | ja | si | oui也可以实现...

单键特性 同行

在bash下,我们可以为read命令指定预期输入的长度:

read -n 1 -p "Is this a good question (y/n)? " answer

在Bash下,read命令接受一个timeout参数,这可能会很有用。
read -t 3 -n 1 -p "Is this a good question (Y/n)? " answer
[ -z "$answer" ] && answer="Yes"  # if 'yes' have to be default choice

使用倒计时的超时:

i=6 ;while ((i-->1)) &&
! read -sn 1 -t 1 -p $'\rIs this a good question (Y/n)? '$i$'..\e[3D' answer;do
  :;done ;[[ $answer == [nN] ]] && answer=No || answer=Yes ;echo "$answer "

3. 一些专用工具的技巧

更复杂的对话框,超出简单的是 - 否的用途:

dialog --menu "Is this a good question" 20 60 12 y Yes n No m Maybe

进度条:

dialog --gauge "Filling the tank" 20 60 0 < <(
    for i in {1..100};do
        printf "XXX\n%d\n%(%a %b %T)T progress: %d\nXXX\n" $i -1 $i
        sleep .033
    done
) 

小演示:

#!/bin/sh
while true ;do
    [ -x "$(which ${DIALOG%% *})" ] || DIALOG=dialog
    DIALOG=$($DIALOG --menu "Which tool for next run?" 20 60 12 2>&1 \
            whiptail       "dialog boxes from shell scripts" >/dev/tty \
            dialog         "dialog boxes from shell with ncurses" \
            gdialog        "dialog boxes from shell with Gtk" \
            kdialog        "dialog boxes from shell with Kde" ) || break
    clear;echo "Choosed: $DIALOG."
    for i in `seq 1 100`;do
        date +"`printf "XXX\n%d\n%%a %%b %%T progress: %d\nXXX\n" $i $i`"
        sleep .0125
      done | $DIALOG --gauge "Filling the tank" 20 60 0
    $DIALOG --infobox "This is a simple info box\n\nNo action required" 20 60
    sleep 3
    if $DIALOG --yesno  "Do you like this demo?" 20 60 ;then
        AnsYesNo=Yes; else AnsYesNo=No; fi
    AnsInput=$($DIALOG --inputbox "A text:" 20 60 "Text here..." 2>&1 >/dev/tty)
    AnsPass=$($DIALOG --passwordbox "A secret:" 20 60 "First..." 2>&1 >/dev/tty)
    $DIALOG --textbox /etc/motd 20 60
    AnsCkLst=$($DIALOG --checklist "Check some..." 20 60 12 \
        Correct "This demo is useful"        off \
        Fun        "This demo is nice"        off \
        Strong        "This demo is complex"        on 2>&1 >/dev/tty)
    AnsRadio=$($DIALOG --radiolist "I will:" 20 60 12 \
        " -1" "Downgrade this answer"        off \
        "  0" "Not do anything"                on \
        " +1" "Upgrade this anser"        off 2>&1 >/dev/tty)
    out="Your answers:\nLike: $AnsYesNo\nInput: $AnsInput\nSecret: $AnsPass"
    $DIALOG --msgbox "$out\nAttribs: $AnsCkLst\nNote: $AnsRadio" 20 60
  done

需要更多示例吗?请查看使用whiptail选择USB设备USB可移动存储器选择器:USBKeyChooser

5. 使用readline的历史记录

示例:

#!/bin/bash

set -i
HISTFILE=~/.myscript.history
history -c
history -r

myread() {
    read -e -p '> ' $1
    history -s ${!1}
}
trap 'history -a;exit' 0 1 2 3 6

while myread line;do
    case ${line%% *} in
        exit )  break ;;
        *    )  echo "Doing something with '$line'" ;;
      esac
  done

这将在您的$HOME目录中创建一个名为.myscript.history的文件,然后您可以使用readline的历史命令,例如UpDownCtrl+r等。

6
注意:stty 提供 -g 选项以供使用:old_stty=$(stty -g); stty raw -echo; …; stty "$old_stty"。这会恢复设置为它们原来的状态,可能与 stty -sane 的设置相同或不同。 - Jonathan Leffler
7
read answer会解释空格和换行符之前的反斜杠,并将它们删除,这通常不是预期的。根据SC2162,请改用read -r answer - vlfig
7
您可以在POSIX和Bash中都使用case(使用通配符条件而不是Bash子字符串:case $answer in; [Yy]* ) echo Yes ;;),但我更喜欢使用条件语句,更喜欢 [ "$answer" != "${answer#[Yy]}" ] 而非您的 echo "$answer" | grep -iq ^y。 这种方式更具可移植性(一些非GNU grep未正确实现-q)并且没有系统调用。${answer#[Yy]}使用参数扩展从$answer开头删除Yy,当其中任何一个存在时会导致不等式。这适用于任何POSIX shell(dash,ksh,bash,zsh,busybox等)。 - Adam Katz
3
@CarterPape 是的,这只是一个玩笑!但在这个详细的答案中,您可能会发现很多提示(第二点至少提供了3种不同的方法)!而且...从大约5年前开始,你是第一个提到我的计数方法的人!;-)) - F. Hauri - Give Up GitHub
1
在 POSIX 中,应该使用 printf %s 而不是 echo -n。根据 POSIX.1-2017 的规定,echo 不识别任何标志。 - snath03
显示剩余3条评论

359
echo "Please enter some input: "
read input_variable
echo "You entered: $input_variable"

35
我不同意,因为它只实现了DOS中“是,否,取消”对话框功能的一部分。它没有实现的部分是输入检查......循环直到收到有效答案。 - Myrddin Emrys
4
在Linux shell脚本中如何提示输入? - Pistos
8
但是原问题描述并没有改变,始终要求回答一个是/否/取消的提示。标题已更新为比我原来的更清晰,但问题描述一直很清楚(在我看来)。 - Myrddin Emrys

178
你可以使用内置的read命令;使用-p选项提示用户提问。
自BASH4以来,现在可以使用-i来建议答案:
read -e -p "Enter the path to the file: " -i "/usr/local/etc/" FILEPATH
echo $FILEPATH

(但是要记得使用“readline”选项-e,以允许使用箭头键进行行编辑)

如果你想要"是/否"逻辑,可以这样做:

read -e -p "
List the content of your home dir ? [Y/n] " YN

[[ $YN == "y" || $YN == "Y" || $YN == "" ]] && ls -la ~/

6
需要注意的是,FILEPATH 是您选择的变量名称,并使用命令提示符的答案进行设置。例如,如果您随后运行 vlc "$FILEPATH",那么 vlc 将打开该文件。 - Ken Sharp
第二个例子中使用-e有什么好处(简单的是/否)? - JBallin
有什么理由使用“-e -p”而不是“-ep”吗? - JBallin
1
如果没有使用-e标志/选项,您可能(根据实现)无法输入“y”,然后改变主意并将其替换为“n”(或其他任何内容); 在记录命令时,单独列出选项有助于提高可读性/清晰度,以及其他原因。 - yPhil
只是一个提示:请注意确实使用 bash,而不是像 这个 U&L.SE 上的答案 中所描述的 sh - Cadoiz

127

Bash有select命令可用于此目的。以下是如何在脚本中使用:

select result in Yes No Cancel
do
    echo $result
done

这是使用它的样子:

$ bash examplescript.sh
1) Yes
2) No
3) Cancel
#? 1
Yes
#? 2
No
#? 3
Cancel
#?

24
巧妙而简单的解决方案。唯一的问题是:这将不断地提示,直到你添加一个exit :) - kaiser
7
要退出程序,请输入 EOT:Ctrl-D。但是,实际上在使用它的真正代码中,需要在函数体内添加break或exit语句。 - Zorawar
19
这并不允许你输入 y 或 n,你需要通过输入 1、2 或 3 来做出选择。 - djjeck
8
exit 将完全退出脚本,break 只会退出当前循环(如果你在 whilecase 循环中)。 - wranvaud

66
read -p "Are you alright? (y/n) " RESP
if [ "$RESP" = "y" ]; then
  echo "Glad to hear it"
else
  echo "You need more bash programming"
fi

这是最易读的一个。+1 - Yuji 'Tomita' Tomita

47
inquire ()  {
  echo  -n "$1 [y/n]? "
  read answer
  finish="-1"
  while [ "$finish" = '-1' ]
  do
    finish="1"
    if [ "$answer" = '' ];
    then
      answer=""
    else
      case $answer in
        y | Y | yes | YES ) answer="y";;
        n | N | no | NO ) answer="n";;
        *) finish="-1";
           echo -n 'Invalid response -- please reenter:';
           read answer;;
       esac
    fi
  done
}

... other stuff

inquire "Install now?"

...

2
在每行开头加上四个空格以保留代码的格式。 - Jouni K. Seppänen
10
如果情况分支语句是硬编码的,为什么我们要在inquire()函数中提供'y'和'n'作为参数?这样做很容易被误用。它们是固定的参数,不可更改,因此第2行的echo应该是:echo -n "$1 [Y/N]? "既然它们无法更改,就不应该提供它们作为参数。 - Myrddin Emrys
1
@MyrddinEmrys,您能否详细说明您的评论?或者发布一篇文章链接或几个关键词,这样我就可以自己进行研究。 - Mateusz Piotrowski
4
自从我发表评论以来,这个答案已经被编辑和改进。你可以点击上面的“编辑于12月23日”的链接查看所有之前的版本。回到2008年,代码有很大不同。 - Myrddin Emrys

45

这是我整理的一些东西:

#!/bin/sh

promptyn () {
    while true; do
        read -p "$1 " yn
        case $yn in
            [Yy]* ) return 0;;
            [Nn]* ) return 1;;
            * ) echo "Please answer yes or no.";;
        esac
    done
}

if promptyn "is the sky blue?"; then
    echo "yes"
else
    echo "no"
fi

我是初学者,所以这只是我的一些看法,但它似乎有效。


9
如果你将 case $yn in 改为 case ${yn:-$2} in,那么你可以使用第二个参数作为默认值,即 Y 或 N。 - jchook
1
或将case $yn更改为case "${yn:-Y}",以使是作为默认选项。 - rubo77
很好,你的代码可以直接使用 if 语句!但是在使用 set -e 的脚本中似乎无法正常工作。你有什么解决方法吗? - winklerrr
@winklerrr 您需要详细说明。使用 set -e 看起来还不错。您确定脚本中没有其他错误吗? - mpen
在我的解决方案中,我直接在函数中回显了答案是yes还是no,我试图用子shell来捕获它,就像这样:answer="$(promptyn "is the sky blue?")"。我认为这导致了“错误”。 - winklerrr

34

你需要的是:

  • bash内置命令(即可移植的)
  • 检查tty
  • 默认答案
  • 超时
  • 有颜色的问题

片段

do_xxxx=y                      # In batch mode => Default is Yes
[[ -t 0 ]] &&                  # If TTY => Prompt the question
read -n 1 -p $'\e[1;32m
Do xxxx? (Y/n)\e[0m ' do_xxxx  # Store the answer in $do_xxxx
if [[ $do_xxxx =~ ^(y|Y|)$ ]]  # Do if 'y' or 'Y' or empty
then
    xxxx
fi

解释

  • [[ -t 0 ]] && read ... => 如果是TTY终端,则调用命令read
  • read -n 1 => 等待一个字符
  • $'\e[1;32m ... \e[0m ' => 以绿色打印
    (绿色是可以在白色/黑色背景上阅读的)
  • [[ $do_xxxx =~ ^(y|Y|)$ ]] => bash正则表达式

超时 => 默认答案为No

do_xxxx=y
[[ -t 0 ]] && {                   # Timeout 5 seconds (read -t 5)
read -t 5 -n 1 -p $'\e[1;32m
Do xxxx? (Y/n)\e[0m ' do_xxxx ||  # read 'fails' on timeout
do_xxxx=n ; }                     # Timeout => answer No
if [[ $do_xxxx =~ ^(y|Y|)$ ]]
then
    xxxx
fi

似乎有一个打字错误:
if [[ $do_xxxx =~ ^(y|Y|)$ ]] 应该是: if [[ $do_xxxx =~ ^(y|Y)$ ]]
- Vladimir Yakovenko

28

最简单的方法,且所需代码最少的方式如下:

read -p "<Your Friendly Message here> : y/n/cancel" CONDITION;

if [ "$CONDITION" == "y" ]; then
   # do something here!
fi

if 只是一个例子:如何处理这个变量取决于你。


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