Shell编程—呈现数据

liushun 2019-12-15

1.标准文件描述符

Linux用文件描述符(file descriptor)来标识每个文件对象。文件描述符是一个非负整数,可以唯一标识会话中打开的文件。每个进程一次 多可以有九个文件描述符。出于特殊目的,bash shell保留了前三个文件描述符(0、1和2):

文件描述符

缩  写

描  述

STDIN

标准输入

1

STDOUT

标准输出

2

STDERR

标准错误

1. STDIN代表shell的标准输入。对终端界面来说,标准输入是键盘。

2. STDOUTSTDOUT文件描述符代表shell的标准输出。在终端界面上,标准输出就是终端显示器。

3. STDERRSTDERR文件描述符处理错误消息。STDERR文件描述符代表shell的标准错误输出。

1.1重定向错误

1. 只重定向错误

$ ls -al test badtest test2 2> test5
-rw-rw-r-- 1 rich rich 158 2014-10-16 11:32 test2
$ cat test5 
ls: cannot access test: No such file or directory 
ls: cannot access badtest: No such file or directory

2. 重定向错误和数据

$ ls -al test test2 test3 badtest 2> test6 1> test7
$ cat test6 
ls: cannot access test: No such file or directory 
ls: cannot access badtest: No such file or directory
$ cat test7
-rw-rw-r-- 1 rich rich 158 2014-10-16 11:32 test2
-rw-rw-r-- 1 rich rich   0 2014-10-16 11:33 test3

另外,也可以将STDERR和STDOUT的输出重定向到同一个输出文件。为此bash shell提供了特殊的重定向符号&>。

$ ls -al test test2 test3 badtest &> test7
$ cat test7
ls: cannot access test: No such file or directory 
ls: cannot access badtest: No such file or directory 
-rw-rw-r-- 1 rich rich 158 2014-10-16 11:32 test2
-rw-rw-r-- 1 rich rich   0 2014-10-16 11:33 test3

当使用&>符时,命令生成的所有输出都会发送到同一位置,包括数据和错误。在badtest文件(列出的后一个文件)的这条错误消息出现在输出文件中的第二行。为了避免错误信息散落在输出文件中,相较于标准输出,bash shell自动赋予了错误消息更高的优先级。

2在脚本中重定向输出

有两种方法来在脚本中重定向输出:

  •  临时重定向行输出
  • 永久重定向脚本中的所有命令

2.1 临时重定向

如果有意在脚本中生成错误消息,可以将单独的一行输出重定向到STDERR。你所需要做的是使用输出重定向符来将输出信息重定向到STDERR文件描述符。在重定向到文件描述符时,你必须在文件描述符数字之前加一个&:

echo "This is an error message" >&2

这行会在脚本的STDERR文件描述符所指向的位置显示文本,而不是通常的STDOUT。

下面这个例子就利用了这项功能。:

$ cat test8 
#!/bin/bash 
# testing STDERR messages 
 echo "This is an error" >&2 
 echo "This is normal output"

如果像平常一样运行这个脚本,你可能看不出什么区别:

$ ./test8 
This is an error This is normal output

记住,默认情况下,Linux会将STDERR导向STDOUT。但是如果你在运行脚本时重定向STDERR,脚本中所有导向STDERR的文本都会被重定向。

$ ./test8 2> test9 This is normal output $ cat test9 This is an error

2.2永久重定向

如果脚本中有大量数据需要重定向,那重定向每个echo语句就会很烦琐。这时可以用exec命令告诉shell在脚本执行期间重定向某个特定文件描述符:

$ cat test10
#!/bin/bash
# redirecting all output to a file 
exec 1>testout
 echo "This is a test of redirecting all output" 
 echo "from a script to another file." 
 echo "without having to redirect every individual line"
 
$ ./test10
$ cat testout
This is a test of redirecting all output 
from a script to another file.
without having to redirect every individual line

exec命令会启动一个新shell并将STDOUT文件描述符重定向到文件。脚本中发给STDOUT的所有输出会被重定向到文件。可以在脚本执行过程中重定向STDOUT。

$ cat test11
#!/bin/bash
# redirecting output to different locations
 exec 2>testerror
 echo "This is the start of the script" 
 echo "now redirecting all output to another location"
 exec 1>testout
 echo "This output should go to the testout file" 
 echo "but this should go to the testerror file" >&2

$ ./test11
This is the start of the script
now redirecting all output to another location
$ cat testout
This output should go to the testout file
$ cat testerror 
but this should go to the testerror file

这个脚本用exec命令来将发给STDERR的输出重定向到文件testerror。接下来,脚本用 echo语句向STDOUT显示了几行文本。随后再次使用exec命令来将STDOUT重定向到testout文件。注意,尽管STDOUT被重定向了,但你仍然可以将echo语句的输出发给STDERR,在本例中还是重定向到testerror文件。

3在脚本中重定向输入

exec命令允许你将STDIN重定向到Linux系统上的文件中:

exec 0< testfile

例子:

$ cat test12
#!/bin/bash
# redirecting file input
 exec 0< testfile 
 count=1 
 while read line 
 do 
    echo "Line #$count: $line"    
    count=$[ $count + 1 ] 
 done
 
$ ./test12
Line #1: This is the first line. 
Line #2: This is the second line. 
Line #3: This is the third line.

4创建自己的重定向

在脚本中重定向输入和输出时,并不局限于这3个默认的文件描述符。在shell 中 多可以有9个打开的文件描述符。其他6个从3~8的文件描述符均可用作输入或输出重定向。你可以将这些文件描述符中的任意一个分配给文件,然后在脚本中使用它们。

4.1创建输出文件描述符

可以用exec命令来给输出分配文件描述符。和标准的文件描述符一样,一旦将另一个文件描述符分配给一个文件,这个重定向就会一直有效,直到你重新分配。

$ cat test13
#!/bin/bash
# using an alternative file descriptor
 exec 3>test13out
 echo "This should display on the monitor" 
 echo "and this should be stored in the file" >&3 
 echo "Then this should be back on the monitor"
 
$ ./test13
This should display on the monitor
Then this should be back on the monitor
$ cat test13out 
and this should be stored in the file

这个脚本用exec命令将文件描述符3重定向到另一个文件。当脚本执行echo语句时,输出内容会像预想中那样显示在STDOUT上。但你重定向到文件描述符3的那行echo语句的输出却进入了另一个文件。

4.2重定向文件描述符

你可以分配另外一个文件描述符给标准文件描述符,反之亦然。这意味着你可以将STDOUT的原来位置重定向到另一个文件描述符,然后再利用该文件描述符重定向回STDOUT。

$ cat test14
#!/bin/bash
# storing STDOUT, then coming back to it
 exec 3>&1 
 exec 1>test14out 
 echo "This should store in the output file" 
 echo "along with this line." 
 exec 1>&3  
 echo "Now things should be back to normal"

$ ./test14
Now things should be back to normal
$ cat test14out
This should store in the output file 
along with this line.

首先,脚本将文件描述符3重定向到文件描述符1 的当前位置,也就是STDOUT。这意味着任何发送给文件描述符3的输出都将出现在显示器上。第二个exec命令将STDOUT重定向到文件,shell现在会将发送给STDOUT的输出直接重定向到输出文件中。但是,文件描述符3仍然指向STDOUT原来的位置,也就是显示器。如果此时将输出数据发送给文件描述符3,它仍然会出现在显示器上,尽管STDOUT已经被重定向了。

在向STDOUT(现在指向一个文件)发送一些输出之后,脚本将STDOUT重定向到文件描述符3的当前位置(现在仍然是显示器)。这意味着现在STDOUT又指向了它原来的位置:显示器。

4.3创建输入文件描述符

可以用和重定向输出文件描述符同样的办法重定向输入文件描述符。

$ cat test15 
#!/bin/bash 
# redirecting input file descriptors 
 exec 6<&0 
 exec 0< testfile 
 count=1 
 while read line 
 do 
   echo "Line #$count: $line"    
   count=$[ $count + 1 ] 
 done 
 exec 0<&6 
 read -p "Are you done now? " answer 
 case $answer in 
 Y|y) echo "Goodbye";; 
 N|n) echo "Sorry, this is the end.";; 
 esac 
 
$ ./test15     
Line #1: This is the first line. 
Line #2: This is the second line. 
Line #3: This is the third line. 
Are you done now? y 
Goodbye 
$ cat test15 
#!/bin/bash 
# redirecting input fil

在这个例子中,文件描述符6用来保存STDIN的位置。然后脚本将STDIN重定向到一个文件。 read命令的所有输入都来自重定向后的STDIN(也就是输入文件)。在读取了所有行之后,脚本会将STDIN重定向到文件描述符6,从而将STDIN恢复到原先的位置。该脚本用了另外一个read命令来测试STDIN是否恢复正常了。这次它会等待键盘的输入。

4.4创建读写文件描述符

可以用同一个文件描述符对同一个文件进行读写。不过用这种方法时要特别小心。由于你是对同一个文件进行数据读写,shell会维护一个内部指针,指明在文件中的当前位置。任何读或写都会从文件指针上次的位置开始。如果不够小心,它会产生一些令人瞠目的结果。看看下面这个例子:

$ cat test16
#!/bin/bash
# testing input/output file descriptor
 exec 3<> testfile 
 read line <&3 
 echo "Read: $line" 
 echo "This is a test line" >&3
 
$ cat testfile 
This is the first line.
This is the second line.
This is the third line.
$ ./test16
Read: This is the first line.
$ cat testfile
This is the first line. 
This is a test line 
ine.
This is the third line.

输出内容表明脚本读取了testfile文件中的第一行。但如果你在脚本运行完毕后,查看testfile文件内容的话,你会发现写入文件中的数据覆盖了已有的数据。当脚本向文件中写入数据时,它会从文件指针所处的位置开始。read命令读取了第一行数据,所以它使得文件指针指向了第二行数据的第一个字符。在echo语句将数据输出到文件时,它会将数据放在文件指针的当前位置,覆盖了该位置的已有数据。

4.5关闭文件描述符

如果你创建了新的输入或输出文件描述符,shell会在脚本退出时自动关闭它们。然而在有些情况下,你需要在脚本结束前手动关闭文件描述符。要关闭文件描述符,将它重定向到特殊符号&-。

脚本中看起来如下:

exec 3>&-

该语句会关闭文件描述符3,不再在脚本中使用它。这里有个例子来说明当你尝试使用已关闭的文件描述符时会怎样:

$ cat badtest 
#!/bin/bash 
# testing closing file descriptors 
 exec 3> test17file 
 echo "This is a test line of data" >&3 
 exec 3>&- 
 echo "This won‘t work" >&3 
 
$ ./badtest 
./badtest: 3: Bad file descriptor

一旦关闭了文件描述符,就不能在脚本中向它写入任何数据,否则shell会生成错误消息。

在关闭文件描述符时还要注意另一件事。如果随后你在脚本中打开了同一个输出文件,shell 会用一个新文件来替换已有文件。这意味着如果你输出数据,它就会覆盖已有文件。看下面这个问题的例子:

$ cat test17 
#!/bin/bash 
# testing closing file descriptors 
exec 3> test17file 
echo "This is a test line of data" >&3 
exec 3>&- 
cat test17file 
exec 3> test17file 
echo "This‘ll be bad" >&3

$ ./test17
This is a test line of data
$ cat test17file
This‘ll be bad

5列出打开的文件描述符

lsof命令会列出整个Linux系统打开的所有文件描述符。lsof命令位于/usr/sbin目录。

该命令会产生大量的输出。它会显示当前Linux系统上打开的每个文件的有关信息。这包括后台运行的所有进程以及登录到系统的任何用户。

常用的有-p和-d,前者允许指定进程ID(PID),后者允许指定要显示的文件描述符编号。

要想知道进程的当前PID,可以用特殊环境变量$$(shell会将它设为当前PID)。-a选项用来对其他两个选项的结果执行布尔AND运算,这会产生如下输出。

$ /usr/sbin/lsof -a -p $$ -d 0,1,2
COMMAND  PID USER   FD   TYPE DEVICE SIZE NODE NAME
bash    3344 rich    0u   CHR  136,0         2 /dev/pts/0 
bash    3344 rich    1u   CHR  136,0         2 /dev/pts/0 
bash    3344 rich    2u   CHR  136,0         2 /dev/pts/0

lsof的默认输出中有7 列信息:

描  述

COMMAND

正在运行的命令名的前9个字符

PID

进程的PID

USER

进程属主的登录名

FD

文件描述符号以及访问类型(r代表读,w代表写,u代表读写)

TYPE

文件的类型(CHR代表字符型,BLK代表块型,DIR代表目录,REG代表常规文件)

DEVICE

设备的设备号(主设备号和从设备号)

SIZE

如果有的话,表示文件的大小

NODE

本地文件的节点号

NAME

文件名

STDIN STDOUT STDERR   STDIN STDOUT STDERR 件描述符都指向终端,所以输出文件的名称就是终端的设备名。

$ cat test18 
#!/bin/bash 
# testing lsof with file descriptors 
 exec 3> test18file1 
 exec 6> test18file2 
 exec 7< testfile 
/usr/sbin/lsof -a -p $$ -d0,1,2,3,6,7 

$ ./test18 
COMMAND  PID USER   FD   TYPE DEVICE SIZE   NODE NAME 
test18  3594 rich    0u   CHR  136,0           2 /dev/pts/0 
test18  3594 rich    1u   CHR  136,0           2 /dev/pts/0 
test18  3594 rich    2u   CHR  136,0           2 /dev/pts/0 
18      3594 rich    3w   REG  253,0    0 360712 /home/rich/test18file1 
18      3594 rich    6w   REG  253,0    0 360715 /home/rich/test18file2 
18      3594 rich    7r   REG  253,0   73 360717 /home/rich/testfile

6阻止命令输出

null文件跟它的名字很像,文件里什么都没有。可以将STDERR重定向到一个叫作null文件的特殊文件。shell输出到null文件的任何数据都不会保存,全部都被丢掉了。比如:

$ ls -al > /dev/null $ cat /dev/null

下面是避免出现错误消息,也无需保存它们的一个常用方法:

$ ls -al badfile test16 2> /dev/null 
-rwxr--r--    1 rich     rich          135 Oct 29 19:57 test16*

可以在输入重定向中将/dev/null作为输入文件。由于/dev/null文件不含有任何内容,通常用它来快速清除现有文件中的数据,而不用先删除文件再重新创建。

$ cat testfile
This is the first line.
This is the second line.
This is the third line.
$ cat /dev/null > testfile
$ cat testfile

文件testfile仍然存在系统上,但现在它是空文件。

7创建临时文件

mktemp命令可以在/tmp目录中创建一个唯一的临时文件。shell会创建这个文件,但不用默认的umask值。

7.1创建本地临时文件

默认情况下,mktemp会在本地目录中创建一个文件。要用mktemp命令在本地目录中创建一个临时文件,你只要指定一个文件名模板就行了。模板可以包含任意文本文件名,在文件名末尾加上6个X就行了:

$ mktemp testing.XXXXXX
$ ls -al testing* 
-rw-------   1 rich     rich      0 Oct 17 21:30 testing.UfIi13

mktemp命令会用6个字符码替换这6个X,从而保证文件名在目录中是唯一的。

mktemp命令的输出正是它所创建的文件的名字。在脚本中使用mktemp命令时,可能要将文件名保存到变量中,这样就能在后面的脚本中引用了。

$ cat test19
#!/bin/bash
# creating and using a temp file
 tempfile=$(mktemp test19.XXXXXX)
 exec 3>$tempfile
 echo "This script writes to temp file $tempfile"
 echo "This is the first line" >&3 
 echo "This is the second line." >&3 
 echo "This is the last line." >&3 
 exec 3>&-
 echo "Done creating temp file. The contents are:" 
 cat $tempfile 
 rm -f $tempfile 2> /dev/null
 
$ ./test19
This script writes to temp file test19.vCHoya 
Done creating temp file. The contents are:
This is the first line
This is the second line.
This is the last line.
$ ls -al test19*
-rwxr--r--    1 rich     rich          356 Oct 29 22:03 test19*

7.2/tmp 目录创建临时文件

-t选项会强制mktemp命令来在系统的临时目录来创建该文件。mktemp命令会返回用来创建临时文件的全路径,而不是只有文件名。

$ cat test20
#!/bin/bash
# creating a temp file in /tmp
 tempfile=$(mktemp -t tmp.XXXXXX)
 echo "This is a test file." > $tempfile 
 echo "This is the second line of the test." >> $tempfile
 echo "The temp file is located at: $tempfile" 
 cat $tempfile 
 rm -f $tempfile
 
$ ./test20
The temp file is located at: /tmp/tmp.Ma3390 
This is a test file.
This is the second line of the test.

7.3创建临时目录

-d选项告诉mktemp命令来创建一个临时目录而不是临时文件。

$ cat test21
#!/bin/bash
# using a temporary directory
 tempdir=$(mktemp -d dir.XXXXXX) 
 cd $tempdir 
 tempfile1=$(mktemp temp.XXXXXX) 
 tempfile2=$(mktemp temp.XXXXXX) 
 exec 7> $tempfile1 
 exec 8> $tempfile2
echo "Sending data to directory $tempdir" 
echo "This is a test line of data for $tempfile1" >&7 
echo "This is a test line of data for $tempfile2" >&8

$ ./test21
Sending data to directory dir.ouT8S8 
$ cd dir.ouT8S8
$ cat temp.N5F3O6
This is a test line of data for temp.N5F3O6
$ cat temp.SQslb7
This is a test line of data for temp.SQslb7

8记录消息

tee命令相当于管道的一个T型接头。它将从STDIN过来的数据同时发往两处。一处是STDOUT,另一处是tee命令行所指定的文件名:

tee filename

由于tee会重定向来自STDIN的数据,你可以用它配合管道命令来重定向命令输出。如果你想将数据追加到文件中,必须用-a选项。

$ cat test22
#!/bin/bash
# using the tee command for logging
 tempfile=test22file
 echo "This is the start of the test" | tee $tempfile 
 echo "This is the second line of the test" | tee -a $tempfile 
 echo "This is the end of the test" | tee -a $tempfile
 
$ ./test22
This is the start of the test
This is the second line of the test
This is the end of the test
$ cat test22file
This is the start of the test
This is the second line of the test
This is the end of the test

9实例

 shell脚本使用命令行参数指定待读取的.csv文件。.csv格式用于从电子表格中导出数据,所以你可以把数据库数据放入电子表格中,把电子表格保存成.csv格式,读取文件,然后创建INSERT 语句将数据插入MySQL数据库。脚本内容如下:

$cat test23
#!/bin/bash
# read file and create INSERT statements for MySQL
outfile=‘members.sql‘ 
IFS=‘,‘ 
while read lname fname address city state zip 
do
   cat >> $outfile << EOF
   INSERT INTO members (lname,fname,address,city,state,zip) VALUES 
(‘$lname‘, ‘$fname‘, ‘$address‘, ‘$city‘, ‘$state‘, ‘$zip‘); 
EOF 
done < ${1}

注意在done语句中出现的重定向符号:

done < ${1}

当运行程序test23时,$1代表第一个命令行参数。它指明了待读取数据的文件。read语句会使用IFS字符解析读入的文本,我们在这里将IFS指定为逗号。 脚本中另外两处重定向操作出现在同一条语句中:

cat >> $outfile << EOF

这条语句中包含一个输出追加重定向(双大于号)和一个输入追加重定向(双小于号)。输出重定向将cat命令的输出追加到由$outfile变量指定的文件中。cat命令的输入不再取自标准输入,而是被重定向到脚本中存储的数据。EOF符号标记了追加到文件中的数据的起止。

INSERT INTO members (lname,fname,address,city,state,zip) VALUES (‘$lname‘, ‘$fname‘, ‘$address‘, ‘$city‘, ‘$state‘, ‘$zip‘);

上面的文本生成了一个标准的SQL INSERT语句。注意,其中的数据会由变量来替换,变量中内容则是由read语句存入的。 所以基本上while循环一次读取一行数据,将这些值放入INSERT语句模板中,然后将结果输出到输出文件中。在这个例子中,使用以下输入数据文件。

$ cat members.csv 
Blum,Richard,123 Main St.,Chicago,IL,60601 
Blum,Barbara,123 Main St.,Chicago,IL,60601 
Bresnahan,Christine,456 Oak Ave.,Columbus,OH,43201 
Bresnahan,Timothy,456 Oak Ave.,Columbus,OH,43201

运行脚本时,显示器上不会出现任何输出:

$ ./test23 < members.csv

但是在members.sql输出文件中,你会看到如下输出内容。

$ cat members.sql 
   INSERT INTO members (lname,fname,address,city,state,zip) VALUES (‘Blum‘,  ‘Richard‘, ‘123 Main St.‘, ‘Chicago‘, ‘IL‘, ‘60601‘); 
   INSERT INTO members (lname,fname,address,city,state,zip) VALUES (‘Blum‘, ‘Barbara‘, ‘123 Main St.‘, ‘Chicago‘, ‘IL‘, ‘60601‘); 
   INSERT INTO members (lname,fname,address,city,state,zip) VALUES (‘Bresnahan‘, ‘Christine‘, ‘456 Oak Ave.‘, ‘Columbus‘, ‘OH‘, ‘43201‘); 
   INSERT INTO members (lname,fname,address,city,state,zip) VALUES (‘Bresnahan‘, ‘Timothy‘, ‘456 Oak Ave.‘, ‘Columbus‘, ‘OH‘, ‘43201‘)

相关推荐