ITEEDU

8.2. 捕捉用户输入

8.2.1. 使用内建命令read

内建命令 readechoprintf 命令互补的命令。read 命令的语法如下:

read [options] NAME1 NAME2 ... NAMEN

从标准输入或者使用 -u 选项的参数提供的文件描述符读取一行。行的第一个词被赋予第一个名字,NAME1,第二个词被赋予第二个名字,以此类推, with leftover words and their intervening separators assigned to the last name, NAMEN. 如果从输入读取的字符比名字要少,那么接下来的名字将被赋予空值。If there are fewer words read from the input stream than there are names, the remaining names are assigned empty values.

IFS 变量中的字符用来把输入行分割成字或者段;参见 第 3.4.8 节 “字分割”。反斜杠字符可以用来去除读入的下一个字符的特殊含义为了行的延续。

如果没有提供名字,读入的行就赋给变量 REPLY

read 命令的返回代码是零,除非遇到一个文件结束(应该是EOF)字符,(要么)如果 read 超时或者一个非法的文件描述符提供给 -u 选项作为参数。

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

表 8.2. read 内建命令的选项

选项 含义
-a ANAME The words are assigned to sequential indexes of the array variable ANAME, starting at 0. All elements are removed from ANAME before the assignment. Other NAME arguments are ignored.
-d DELIM The first character of DELIM is used to terminate the input line, rather than newline.
-e readline is used to obtain the line.
-n NCHARS read returns after reading NCHARS characters rather than waiting for a complete line of input.
-p PROMPT Display PROMPT, without a trailing newline, before attempting to read any input. The prompt is displayed only if input is coming from a terminal.
-r If this option is given, backslash does not act as an escape character. The backslash is considered to be part of the line. In particular, a backslash-newline pair may not be used as a line continuation.
-s Silent mode. If input is coming from a terminal, characters are not echoed.
-t TIMEOUT Cause read to time out and return failure if a complete line of input is not read within TIMEOUT seconds. This option has no effect if read is not reading input from the terminal or from a pipe.
-u FD Read input from file descriptor 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 and SGID 再Linux手册的介绍中见SUID和SGID。

8.2.3. 重定向和文件描述符

8.2.3.1. 概要

就像你知道的在shell的基本用法中,一个命令的输入和输出可以在执行完毕前被重定向,使用一个特殊的符号-重定向操作符-由shell来解释。重定向也可以用来为当前shell执行环境打开和关闭文件。

重定向也可以出现在一个脚本中,所以它可以从一个文件收到输入,比如,或者发送输出到一个文件。然后,用户可以回顾这个输出文件,或者可以被另外一个脚本当作输入。

文件输入输出由追踪为一个给定的进程所有打开文件的整数句柄来完成。这些数字值就是文件描述符。最为人们所知的文件米描述符是 stdin, stdoutstderr,文件描述符的数字分别是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

你可能想检查 info MAKEDEVinfo proc 来得到更多关于 /proc 子目录和你的系统为每个运行的进程操纵文件描述符的方法的信息。

当你以命令行来运行一个脚本的时候,没有什么太多的改变,因为子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.8 2005/09/05 12:39:22 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 节 “文件输入和输出”),但是一些忘记错误重定向的企图-一些之后可以仰赖的输出。同时,如果你幸运的话,错误会mail给你,可能的错误原因会被被揭示出来。但是不幸的话,错误会导致你的脚本失败而且也不会被捕捉或者发送到任何地方,以至于你载调试的同时什么都做不了。

当重定向错误的时候,注意优先的顺序是有意义的。比如,这个命令,发生在 /var/spool

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

将重定向 ls 命令的输出到在 /var/tmp 中的文件 unaccessible-in-spool, 这个命令

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

将把标准输出和标准错误都定向到文件 spoollist。这个命令

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

仅仅把标准输出定向到目标文件里,因为在标准输出重定向之前标准错误已经拷贝到标准输出。

为了方便,如果确定它们将不使用,错误常常重定向到 /dev/null。可以在你的系统的起始脚本里找到很多例子。

Bash允许你使用如下的结构来重定向标准输出和标准错误到名字是 FILE 扩展的结果的文件:

&> FILE

This is the equivalent of > FILE 2>&1, the construct used in the previous set of examples. It is also often combined with redirection to /dev/null, for instance when you just want a command to execute, no matter what output or errors it gives.

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

/dev/fd 的主要使用价值来自于shell。这种机制允许程序以和其他路径名相同的方式使用路径名参数来操纵标准输入和标准输出。如果 /dev/fd 在系统中不存在,你将不得不找一个办法来迂回解决这个问题。比如可以使用(-)来表明程序需要从管道读取就可以达到目的。一个例子: This mechanism allows for programs that use pathname arguments to handle standard input and standard output in the same way as other pathnames.

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. 读取和exec

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

另外一种着眼文件描述符的方法是把他们认为是分配给文件一个数值。你可以使用文件描述符值,而不是使用文件名。内建命令 exec 是用来给文件分配一个文件描述符。使用

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.
[注意] 文件描述符5

使用这个文件描述符可能导致问题,参见A the Advanced Bash-Scripting Guide第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."

exec 7<&0

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"

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,在每次用户需要读取真实便准输入设备-通常是键盘的时候就关闭。syntax.

以下是一个简单的例子只有在向管道提交一个标准错误时的重定向:

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

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

INPUTDIR="$1"

exec 6>&1

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>&-

exec 6>&-

8.2.4.4. Here 文档

经常性的,你的脚本可能调用其他程序或者脚本来请求输入。 here 文档提供了一种通知shell从当前源读取输入直到找到行仅仅包含搜索字符的方法。(no trailing blanks). 所有读取的行到那个点然后作为一个命令的标准输入。

结果是你不需要访问单独的文件;你可以使用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, it is supposed to be a construct within the same script. This is an example that installs a package automatically, eventhough you should normally confirm:

#!/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