8.2. 捕获用户输入

8.2.1. 使用 read 内建命令

read 内建命令是 echoprintf 命令的对应命令。read 命令的语法如下

read[选项] NAME1 NAME2 ... NAMEN

从标准输入读取一行,或从作为参数提供给以下选项的文件描述符中读取-u选项。行的第一个单词被分配给第一个名称,NAME1,第二个单词分配给第二个名称,依此类推,剩余的单词及其分隔符分配给最后一个名称,NAMEN。如果从输入流中读取的单词少于名称的数量,则其余名称将被分配空值。

变量IFS的值中的字符用于将输入行拆分为单词或标记;请参阅第 3.4.8 节。反斜杠字符可用于删除读取的下一个字符的任何特殊含义以及用于行继续。

如果未提供名称,则读取的行将分配给变量REPLY.

除非遇到文件结束符,否则 read 命令的返回码为零;如果 read 超时,或者如果提供了无效的文件描述符作为以下选项的参数-u选项。

Bash read 内建命令支持以下选项

表 8-2. read 内建命令的选项

选项含义
-aANAME单词被分配给数组变量的顺序索引ANAME,从 0 开始。所有元素都从以下位置删除ANAME在赋值之前。其他NAME参数将被忽略。
-dDELIM的第一个字符DELIM用于终止输入行,而不是换行符。
-e使用 readline 获取行。
-nNCHARSread 在读取后返回NCHARS个字符,而不是等待完整的输入行。
-pPROMPT显示PROMPT,不带尾随换行符,在尝试读取任何输入之前。提示符仅在输入来自终端时显示。
-r如果给出此选项,反斜杠将不充当转义字符。反斜杠被视为行的一部分。特别是,反斜杠-换行符对不能用作行继续符。
-s静默模式。如果输入来自终端,则不回显字符。
-tTIMEOUT如果在以下时间内未读取完整的输入行,则导致 read 超时并返回失败TIMEOUT秒。如果 read 未从终端或管道读取输入,则此选项无效。
-uFD从文件描述符读取输入FD.

这是一个直接的示例,改进了leaptest.sh前一章的脚本

michel ~/test> cat leaptest.sh
#!/bin/bash
# This script will test if you have given a leap year or not.

echo "Type the year that you want to check (4 digits), followed by [ENTER]:"

read year

if (( ("$year" % 400) == "0" )) || (( ("$year" % 4 == "0") && ("$year" % 100 !=
"0") )); then
  echo "$year is a leap year."
else
  echo "This is not a leap year."
fi

michel ~/test> leaptest.sh
Type the year that you want to check (4 digits), followed by [ENTER]:
2000
2000 is a leap year.

8.2.2. 提示用户输入

以下示例显示了如何使用提示来解释用户应输入的内容。

michel ~/test> cat friends.sh
#!/bin/bash

# This is a program that keeps your address book up to date.

friends="/var/tmp/michel/friends"

echo "Hello, "$USER".  This script will register you in Michel's friends database."

echo -n "Enter your name and press [ENTER]: "
read name
echo -n "Enter your gender and press [ENTER]: "
read -n 1 gender
echo

grep -i "$name" "$friends"

if  [ $? == 0 ]; then
  echo "You are already registered, quitting."
  exit 1
elif [ "$gender" == "m" ]; then
  echo "You are added to Michel's friends list."
  exit 1
else
  echo -n "How old are you? "
  read age
  if [ $age -lt 25 ]; then
    echo -n "Which colour of hair do you have? "
    read colour
    echo "$name $age $colour" >> "$friends" 
    echo "You are added to Michel's friends list.  Thank you so much!"
  else
    echo "You are added to Michel's friends list."
    exit 1
  fi
fi

michel ~/test> cp friends.sh /var/tmp; cd /var/tmp

michel ~/test> touch friends; chmod a+w friends

michel ~/test> friends.sh
Hello, michel.  This script will register you in Michel's friends database.
Enter your name and press [ENTER]: michel
Enter your gender and press [ENTER] :m
You are added to Michel's friends list.

michel ~/test> cat friends

请注意,此处未省略任何输出。该脚本仅存储有关 Michel 感兴趣的人员的信息,但它始终会说您已添加到列表中,除非您已在列表中。

现在其他人可以开始执行脚本

[anny@octarine tmp]$ friends.sh
Hello, anny.  This script will register you in Michel's friends database.
Enter your name and press [ENTER]: anny
Enter your gender and press [ENTER] :f
How old are you? 22
Which colour of hair do you have? black
You are added to Michel's friends list.

一段时间后,friends列表开始看起来像这样

tille 24 black
anny 22 black
katya 22 blonde
maria 21 black
--output omitted--

当然,这种情况并不理想,因为每个人都可以编辑(但不能删除)Michel 的文件。您可以使用脚本文件上的特殊访问模式来解决此问题,请参阅 Linux 入门指南中的 SUID 和 SGID

8.2.3. 重定向和文件描述符

8.2.3.1. 概述

正如您从基本的 shell 用法中了解到的,命令的输入和输出可以在执行之前重定向,使用一种特殊的表示法 - 重定向运算符 - 由 shell 解释。重定向也可以用于为当前 shell 执行环境打开和关闭文件。

重定向也可以在脚本中发生,以便它可以从文件接收输入,例如,或将输出发送到文件。稍后,用户可以查看输出文件,或者它可以被另一个脚本用作输入。

文件输入和输出是通过整数句柄完成的,这些句柄跟踪给定进程的所有打开文件。这些数值称为文件描述符。最著名的文件描述符是 stdinstdoutstderr,文件描述符编号分别为 0、1 和 2。这些编号和各自的设备是保留的。Bash 也可以将网络主机上的 TCP 或 UDP 端口作为文件描述符。

下面的输出显示了保留的文件描述符如何指向实际设备

michel ~> ls -l /dev/std*
lrwxrwxrwx  1 root    root     17 Oct  2 07:46 /dev/stderr -> ../proc/self/fd/2
lrwxrwxrwx  1 root    root     17 Oct  2 07:46 /dev/stdin -> ../proc/self/fd/0
lrwxrwxrwx  1 root    root     17 Oct  2 07:46 /dev/stdout -> ../proc/self/fd/1

michel ~> ls -l /proc/self/fd/[0-2]
lrwx------  1 michel  michel   64 Jan 23 12:11 /proc/self/fd/0 -> /dev/pts/6
lrwx------  1 michel  michel   64 Jan 23 12:11 /proc/self/fd/1 -> /dev/pts/6
lrwx------  1 michel  michel   64 Jan 23 12:11 /proc/self/fd/2 -> /dev/pts/6

请注意,每个进程都有自己对以下文件目录的视图/proc/self,因为它实际上是指向以下位置的符号链接/proc/<进程 ID>.

您可能需要查看 info MAKEDEVinfo proc 以获取有关的更多信息/proc子目录以及您的系统如何处理每个正在运行的进程的标准文件描述符。

当执行给定的命令时,将按顺序执行以下步骤

  • 如果前一个命令的标准输出被管道传输到当前命令的标准输入,则/proc/<当前进程 ID>/fd/0更新为以与以下位置相同的匿名管道为目标/proc/<前一个进程 ID/fd/1.

  • 如果当前命令的标准输出被管道传输到下一个命令的标准输入,则/proc/<当前进程 ID>/fd/1更新为以另一个匿名管道为目标。

  • 当前命令的重定向从左到右处理。

  • 命令之后的重定向 "N>&M""N<&M" 具有创建或更新符号链接的效果/proc/self/fd/N与符号链接的目标相同/proc/self/fd/M.

  • 重定向 "N> file""N< file" 具有创建或更新符号链接的效果/proc/self/fd/N以目标文件为目标。

  • 文件描述符关闭 "N>&-" 具有删除符号链接的效果/proc/self/fd/N.

  • 只有现在才执行当前命令。

当您从命令行运行脚本时,不会发生太大变化,因为子 shell 进程将使用与父进程相同的文件描述符。当没有这样的父进程可用时,例如当您使用 cron 工具运行脚本时,除非使用某种形式的重定向,否则标准文件描述符是管道或其他(临时)文件。以下示例对此进行了演示,该示例显示了来自简单 at 脚本的输出

michel ~> date
Fri Jan 24 11:05:50 CET 2003

michel ~> at 1107
warning: commands will be executed using (in order) 
a) $SHELL b) login shell c)/bin/sh
at> ls -l /proc/self/fd/ > /var/tmp/fdtest.at
at> <EOT>
job 10 at 2003-01-24 11:07

michel ~> cat /var/tmp/fdtest.at
total 0
lr-x------    1 michel michel  64 Jan 24 11:07 0 -> /var/spool/at/!0000c010959eb (deleted)
l-wx------    1 michel michel  64 Jan 24 11:07 1 -> /var/tmp/fdtest.at
l-wx------    1 michel michel  64 Jan 24 11:07 2 -> /var/spool/at/spool/a0000c010959eb
lr-x------    1 michel michel  64 Jan 24 11:07 3 -> /proc/21949/fd

以及一个带有 cron

michel ~> crontab -l
# DO NOT EDIT THIS FILE - edit the master and reinstall.
# (/tmp/crontab.21968 installed on Fri Jan 24 11:30:41 2003)
# (Cron version -- $Id: chap8.xml,v 1.9 2006/09/28 09:42:45 tille Exp $)
32 11 * * * ls -l /proc/self/fd/ > /var/tmp/fdtest.cron

michel ~> cat /var/tmp/fdtest.cron
total 0
lr-x------    1 michel michel  64 Jan 24 11:32 0 -> pipe:[124440]
l-wx------    1 michel michel  64 Jan 24 11:32 1 -> /var/tmp/fdtest.cron
l-wx------    1 michel michel  64 Jan 24 11:32 2 -> pipe:[124441]
lr-x------    1 michel michel  64 Jan 24 11:32 3 -> /proc/21974/fd

8.2.3.2. 错误重定向

从前面的示例中,很明显您可以为脚本提供输入和输出文件(更多信息请参见第 8.2.4 节),但有些人往往忘记重定向错误 - 稍后可能会依赖的输出。此外,如果您幸运的话,错误将通过邮件发送给您,并且最终的失败原因可能会被揭示。如果您不那么幸运,错误将导致您的脚本失败,并且不会被捕获或发送到任何地方,因此您无法开始进行任何有价值的调试。

重定向错误时,请注意优先级顺序很重要。例如,此命令在以下位置发出:/var/spool

ls -l * 2> /var/tmp/unaccessible-in-spool

ls 命令的标准输出重定向到文件unaccessible-in-spool/var/tmp。命令

ls -l * > /var/tmp/spoollist 2>&1

将标准输入和标准错误都定向到文件spoollist。命令

ls -l * 2 >& 1 > /var/tmp/spoollist

仅将标准输出定向到目标文件,因为在重定向标准输出之前,标准错误已复制到标准输出。

为了方便起见,错误通常会重定向到/dev/null,如果确定它们不需要。在系统的启动脚本中可以找到数百个示例。

Bash 允许将标准输出和标准错误都重定向到文件,该文件的名称是扩展的结果FILE使用此构造

&> FILE

这等效于 > FILE 2>&1,即上一组示例中使用的构造。它也经常与重定向结合使用/dev/null,例如,当您只想执行命令时,无论它给出什么输出或错误。

8.2.4. 文件输入和输出

8.2.4.1. 使用 /dev/fd

目录/dev/fd包含名为 的条目0, 1, 2,等等。打开文件/dev/fd/N等效于复制文件描述符 N。如果您的系统提供/dev/stdin, /dev/stdout/dev/stderr,您将看到这些等效于/dev/fd/0, /dev/fd/1/dev/fd/2,分别。

文件的主要用途是来自 shell。/dev/fd此机制允许使用路径名参数的程序以与其他路径名相同的方式处理标准输入和标准输出。如果/dev/fd在系统上不可用,您必须找到一种绕过问题的方法。例如,可以使用连字符 (-) 来指示程序应从管道读取。一个例子

michel ~> filter body.txt.gz | cat header.txt - footer.txt
This text is printed at the beginning of each print job and thanks the sysadmin
for setting us up such a great printing infrastructure.

Text to be filtered.

This text is printed at the end of each print job.

cat 命令首先读取文件header.txt,接下来是它的标准输入,它是 filter 命令的输出,最后是footer.txt文件。连字符作为命令行参数引用标准输入或标准输出的特殊含义是一种误解,这种误解已蔓延到许多程序中。当将连字符指定为第一个参数时,也可能存在问题,因为它可能被解释为前面命令的选项。使用/dev/fd允许统一并防止混淆

michel ~> filter body.txt | cat header.txt /dev/fd/0 footer.txt | lp

在这个干净的示例中,所有输出都通过 lp 管道传输,以将其发送到默认打印机。

8.2.4.2. Read 和 exec

8.2.4.2.1. 将文件描述符分配给文件

查看文件描述符的另一种方法是将它们视为将数值分配给文件的一种方式。您可以不使用文件名,而是使用文件描述符编号。exec 内建命令可用于替换当前进程的 shell 或更改当前 shell 的文件描述符。例如,它可用于将文件描述符分配给文件。使用

exec fdN>file

用于将文件描述符 N 分配给file用于输出,以及

exec fdN<file

用于将文件描述符 N 分配给file用于输入。将文件描述符分配给文件后,它可以与 shell 重定向运算符一起使用,如下例所示

michel ~> exec 4> result.txt

michel ~> filter body.txt | cat header.txt /dev/fd/0 footer.txt >& 4

michel ~> cat result.txt
This text is printed at the beginning of each print job and thanks the sysadmin
for setting us up such a great printing infrastructure.

Text to be filtered.

This text is printed at the end of each print job.

Note文件描述符 5
 

使用此文件描述符可能会导致问题,请参阅 高级 Bash 脚本指南,第 16 章。强烈建议您不要使用它。

8.2.4.2.2. 在脚本中读取

以下示例显示了如何在文件输入和命令行输入之间切换

michel ~/testdir> cat sysnotes.sh
#!/bin/bash

# This script makes an index of important config files, puts them together in
# a backup file and allows for adding comment for each file.

CONFIG=/var/tmp/sysconfig.out
rm "$CONFIG" 2>/dev/null

echo "Output will be saved in $CONFIG."

# create fd 7 with same target as fd 0 (save stdin "value")
exec 7<&0

# update fd 0 to target file /etc/passwd
exec < /etc/passwd

# Read the first line of /etc/passwd
read rootpasswd

echo "Saving root account info..."
echo "Your root account info:" >> "$CONFIG"
echo $rootpasswd >> "$CONFIG"

# update fd 0 to target fd 7 target (old fd 0 target); delete fd 7
exec 0<&7 7<&-

echo -n "Enter comment or [ENTER] for no comment: "
read comment; echo $comment >> "$CONFIG"

echo "Saving hosts information..."

# first prepare a hosts file not containing any comments
TEMP="/var/tmp/hosts.tmp"
cat /etc/hosts | grep -v "^#" > "$TEMP"

exec 7<&0
exec < "$TEMP"

read ip1 name1 alias1
read ip2 name2 alias2

echo "Your local host configuration:" >> "$CONFIG"

echo "$ip1 $name1 $alias1" >> "$CONFIG"
echo "$ip2 $name2 $alias2" >> "$CONFIG"

exec 0<&7 7<&-

echo -n "Enter comment or [ENTER] for no comment: "
read comment; echo $comment >> "$CONFIG"
rm "$TEMP"

michel ~/testdir> sysnotes.sh
Output will be saved in /var/tmp/sysconfig.out.
Saving root account info...
Enter comment or [ENTER] for no comment: hint for password: blue lagoon
Saving hosts information...
Enter comment or [ENTER] for no comment: in central DNS

michel ~/testdir> cat /var/tmp/sysconfig.out
Your root account info:
root:x:0:0:root:/root:/bin/bash
hint for password: blue lagoon
Your local host configuration:
127.0.0.1 localhost.localdomain localhost
192.168.42.1 tintagel.kingarthur.com tintagel
in central DNS

8.2.4.3. 关闭文件描述符

由于子进程继承打开的文件描述符,因此在不再需要文件描述符时关闭它是很好的做法。这是使用以下方法完成的

exec fd<&-

语法。在上面的示例中,每次用户需要访问实际的标准输入设备(通常是键盘)时,都会关闭已分配给标准输入的文件描述符 7。

以下是一个仅将标准错误重定向到管道的简单示例

michel ~> cat listdirs.sh
#!/bin/bash

# This script prints standard output unchanged, while standard error is 
# redirected for processing by awk.

INPUTDIR="$1"

# fd 6 targets fd 1 target (console out) in current shell
exec 6>&1

# fd 1 targets pipe, fd 2 targets fd 1 target (pipe),
# fd 1 targets fd 6 target (console out), fd 6 closed, execute ls
ls "$INPUTDIR"/* 2>&1 >&6 6>&- \
				# Closes fd 6 for awk, but not for ls.

| awk 'BEGIN { FS=":" } { print "YOU HAVE NO ACCESS TO" $2 }' 6>&-

# fd 6 closed for current shell
exec 6>&-

8.2.4.4. Here 文档

通常,您的脚本可能会调用另一个需要输入的程序或脚本。Here 文档提供了一种指示 shell 从当前源读取输入的方法,直到找到仅包含搜索字符串的行(没有尾随空格)。到那时为止读取的所有行都用作命令的标准输入。

结果是您不需要调用单独的文件;您可以使用 shell 特殊字符,并且它看起来比一堆 echo 命令更好

michel ~> cat startsurf.sh
#!/bin/bash

# This script provides an easy way for users to choose between browsers.

echo "These are the web browsers on this system:"
 
# Start here document
cat << BROWSERS
mozilla
links
lynx
konqueror
opera
netscape
BROWSERS
# End here document

echo -n "Which is your favorite? "
read browser

echo "Starting $browser, please wait..."
$browser &

michel ~> startsurf.sh
These are the web browsers on this system:
mozilla
links
lynx
konqueror
opera
netscape
Which is your favorite? opera
Starting opera, please wait...

虽然我们谈论的是 here document,但它应该是在同一脚本中的构造。这是一个自动安装软件包的示例,即使您通常应该确认

#!/bin/bash
 
# This script installs packages automatically, using yum.
 
if [ $# -lt 1 ]; then
        echo "Usage: $0 package."
        exit 1
fi
 
yum install $1 << CONFIRM
y
CONFIRM

这就是脚本的运行方式。当提示 "Is this ok [y/N]" 字符串时,脚本会自动回答 "y"

[root@picon bin]# ./install.sh tuxracer
Gathering header information file(s) from server(s)
Server: Fedora Linux 2 - i386 - core
Server: Fedora Linux 2 - i386 - freshrpms
Server: JPackage 1.5 for Fedora Core 2
Server: JPackage 1.5, generic
Server: Fedora Linux 2 - i386 - updates
Finding updated packages
Downloading needed headers
Resolving dependencies
Dependencies resolved
I will do the following:
[install: tuxracer 0.61-26.i386]
Is this ok [y/N]: EnterDownloading Packages
Running test transaction:
Test transaction complete, Success!
tuxracer 100 % done 1/1
Installed:  tuxracer 0.61-26.i386
Transaction(s) Complete