(轉)linux exec與重定向


原文:http://xstarcd.github.io/wiki/shell/exec_redirect.html

linux exec與重定向

exec和source都屬於bash內部命令(builtins commands),在bash下輸入man exec或man source可以查看所有的內部命令信息。

bash shell的命令分為兩類:外部命令和內部命令。外部命令是通過系統調用或獨立的程序實現的,如sed、awk等等。內部命令是由特殊的文件格式(.def)所實現,如cd、history、exec等等。

fork概念

在說明exe和source的區別之前,先說明一下fork的概念。

fork是linux的系統調用,用來創建子進程(child process)。子進程是父進程(parent process)的一個副本,從父進程那里獲得一定的資源分配以及繼承父進程的環境。子進程與父進程唯一不同的地方在於pid(process id)。

環境變量(傳給子進程的變量,遺傳性是本地變量和環境變量的根本區別)只能單向從父進程傳給子進程。不管子進程的環境變量如何變化,都不會影響父進程的環境變量。

  • shell script

有兩種方法執行shell scripts,一種是新產生一個shell,然后執行相應的shell scripts;一種是在當前shell下執行,不再啟用其他shell。

新產生一個shell然后再執行scripts的方法是在scripts文件開頭加入以下語句:#!/bin/sh

一般的script文件(.sh)即是這種用法。這種方法先啟用新的sub-shell(新的子進程),然后在其下執行命令。

另外一種方法就是上面說過的source命令,不再產生新的shell,而在當前shell下執行一切命令。

  • source

source命令即點(.)命令。

在bash下輸入man source,找到source命令解釋處,可以看到解釋”Read and execute commands from filename in the current shell environment and …”。從中可以知道,source命令是在當前進程中執行參數文件中的各個命令,而不是另起子進程(或sub-shell)。

  • exec

在bash下輸入man exec,找到exec命令解釋處,可以看到有”No new process is created.”這樣的解釋,這就是說exec命令不產生新的子進程。那么exec與source的區別是什么呢?

exec命令在執行時會把當前的shell process關閉,然后換到后面的命令繼續執行。

系統調用exec是以新的進程去代替原來的進程,但進程的PID保持不變。因此,可以這樣認為,exec系統調用並沒有創建新的進程,只是替換了原來進程上下文的內容。原進程的代碼段,數據段,堆棧段被新的進程所代替。

一個進程主要包括以下幾個方面的內容:

  1. 一個可以執行的程序
  2. 與進程相關聯的全部數據(包括變量,內存,緩沖區)
  3. 程序上下文(程序計數器PC,保存程序執行的位置)

exec是一個函數簇,由6個函數組成,分別是以excl和execv打頭的。

執行exec系統調用,一般都是這樣,用fork()函數新建立一個進程,然后讓進程去執行exec調用。我們知道,在fork()建立新進程之后,父進各與子進程共享代碼段,但數據空間是分開的,但父進程會把自己數據空間的內容copy到子進程中去,還有上下文也會copy到子進程中去。而為了提高效率,采用一種寫時copy的策略,即創建子進程的時候,並不copy父進程的地址空間,父子進程擁有共同的地址空間,只有當子進程需要寫入數據時(如向緩沖區寫入數據),這時候會復制地址空間,復制緩沖區到子進程中去。從而父子進程擁有獨立的地址空間。而對於fork()之后執行exec后,這種策略能夠很好的提高效率,如果一開始就copy,那么exec之后,子進程的數據會被放棄,被新的進程所代替。

  • exec與system的區別
    • exec是直接用新的進程去代替原來的程序運行,運行完畢之后不回到原先的程序中去。
    • system是調用shell執行你的命令,system=fork+exec+waitpid,執行完畢之后,回到原先的程序中去。繼續執行下面的部分。

總之,如果你用exec調用,首先應該fork一個新的進程,然后exec. 而system不需要你fork新進程,已經封裝好了。

詳解及應用實例

基本概念

(這是理解后面的知識的前提,請務必理解)

  • I/O重定向通常與FD有關,shell的FD通常為10個,即0~9;
  • 三個常用FD(默認與keyboard、monitor、monitor有關):
FD 說明
0 stdin,標准輸入
1 stdout,標准輸出
2 stderr,標准錯誤輸出
1
2
3
# 查看文件描述符
lsof -a -p $$ -d0,1,2
ll /proc/ $$ /fd

FD用來改變送出的數據信道(stdout, stderr),使之輸出到指定的檔案;

0 是 與 1> 是一樣的;

在IO重定向中,stdout 與 stderr 的管道會先准備好,才會從 stdin 讀進資料;

管道|(pipe line):上一個命令的 stdout 接到下一個命令的 stdin;

tee 命令是在不影響原本 I/O 的情況下,將 stdout 復制一份到檔案去;

bash(ksh)執行命令的過程:分析命令-變量求值-命令替代(``和$( ))-重定向-通配符展開-確定路徑-執行命令;

( ) 將 command group 置於 sub-shell 去執行,也稱 nested sub-shell,它有一點非常重要的特性是:繼承父shell的Standard input, output, and error plus any other open file descriptors。

exec 命令:常用來替代當前 shell 並重新啟動一個 shell,換句話說,並沒有啟動子shell。使用這一命令時任何現有環境都將會被清除。 exec在對文件描述符進行操作的時候,也只有在這時,exec不會覆蓋你當前的 shell 環境。

常用重定向

cmd &n 使用系統調用 dup (2) 復制文件描述符 n 並把結果用作標准輸出

&- 關閉標准輸出

n&- 表示將 n 號輸出關閉

上述所有形式都可以前導一個數字,此時建立的文件描述符由這個數字指定而不是缺省的 0 或 1。如:

... 2>file 運行一個命令並把錯誤輸出(文件描述符 2)定向到 file。

... 2>&1 運行一個命令並把它的標准輸出和輸出合並。(嚴格的說是通過復制文件描述符 1 來建立文件描述符 2 ,但效果通常是合並了兩個流。)

我們對 2>&1詳細說明一下 :

2>&1 也就是 FD2=FD1 ,這里並不是說FD2 的值等於FD1的值,因為 > 是改變送出的數據信道,也就是說把 FD2 的 “數據輸出通道” 改為 FD1 的 “數據輸出通道”。

如果僅僅這樣,這個改變好像沒有什么作用,因為 FD2 的默認輸出和 FD1 的默認輸出本來都是 monitor,一樣的!但是,當 FD1 是其他文件,甚至是其他 FD 時,這個就具有特殊的用途了。請大家務必理解這一點。

恢復

如果 stdin, stdout, stderr 進行了重定向或關閉, 但沒有保存原來的 FD, 可以將其恢復到 default 狀態嗎?

如果關閉了stdin,因為會導致退出,那肯定不能恢復。

如果重定向或關閉 stdout和stderr其中之一,可以恢復,因為他們默認均是送往monitor(但不知會否有其他影響)。

如恢復重定向或關閉的 stdout: exec 1>&2 ,恢復重定向或關閉的stderr:exec 2>&1

如果stdout和stderr全部都關閉了,又沒有保存原來的FD,可以用:exec 1>/dev/tty 恢復。

  • cmd >a 2>a 和 cmd >a 2>&1 為什么不同?

cmd >a 2>a :stdout和stderr都直接送往文件 a ,a文件會被打開兩遍,由此導致stdout和stderr互相覆蓋。

cmd >a 2>&1 :stdout直接送往文件a ,stderr是繼承了FD1的管道之后,再被送往文件a 。a文件只被打開一遍,就是FD1將其打開。

我想:他們的不同點在於:

cmd >a 2>a 相當於使用了兩個互相競爭使用文件a的管道;

而cmd >a 2>&1 只使用了一個管道,但在其源頭已經包括了stdout和stderr。

從IO效率上來講,cmd >a 2>&1的效率應該更高!

  • 常用命令語法及范例
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    exec 0
    exec 1>outfilename # 打開文件outfilename作為stdout
    exec 2>errfilename # 打開文件errfilename作為 stderr
    exec 1&-           # 關閉 FD1
    exec 5>&-          # 關閉 FD5
     
    exec 4<&1          # 備份當前stdout至FD4
    exec 1>1.txt       # stdout重定向至1.txt
    exec 1<&4          # 恢復stdout
    exec 4>&-          # 關閉 FD4
     
    # 重定向操作范例
    cat > 1 <<EOF
    11 22 33 44 55
    66 22 33 11 33
    324 25 63 634 745
    EOF
    cat > 2 <<EOF
    > 1.txt
    EOF
     
    exec 4<&1          # 備份當前stdout
    exec 1>1.txt
     
    while read line; do echo $line; done < 1
     
    exec 1<&4          # 恢復stdout
    exec 4>&-
     
    sh . /2
    cat 1.txt

I/O和I/O重定向的詳細介紹

取至《高級Bash腳本編程指南》中內容。

http://www.tsnc.edu.cn/default/tsnc_wgrj/doc/abs-3.9.1_cn/html/ioredirintro.html

I/O重定向

默認情況下始終有3個"文件"處於打開狀態, stdin(鍵盤), stdout(屏幕), 和stderr(錯誤消息輸出到屏幕上). 這3個文件和其他打開的文件都可以被重定向. 對於重定向簡單的解釋就是捕捉一個文件, 命令, 程序, 腳本, 或者是腳本中的代碼塊(請參考例子 3-1和例子 3-2)的輸出, 然后將這些輸出作為輸入發送到另一個文件, 命令, 程序, 或腳本中.

每個打開的文件都會被分配一個文件描述符. [1] stdin, stdout, 和stderr的文件描述符分別是0, 1, 和 2. 除了這3個文件, 對於其他那些需要打開的文件, 保留了文件描述符3到9. 在某些情況下, 將這些額外的文件描述符分配給stdin, stdout, 或stderr作為臨時的副本鏈接是非常有用的. [2] 在經過復雜的重定向和刷新之后需要把它們恢復成正常狀態(請參考例子 16-1).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
# --------------------------------------------------------------------
COMMAND_OUTPUT >
    # 將stdout重定向到一個文件.
    # 如果這個文件不存在, 那就創建, 否則就覆蓋.
 
ls -lR > dir -tree.list
    # 創建一個包含目錄樹列表的文件.
 
: > filename
    # >操作, 將會把文件"filename"變為一個空文件(就是size為0).
    # 如果文件不存在, 那么就創建一個0長度的文件(與'touch'的效果相同).
    # :是一個占位符, 不產生任何輸出.
 
> filename
    # >操作, 將會把文件"filename"變為一個空文件(就是size為0).
    # 如果文件不存在, 那么就創建一個0長度的文件(與'touch'的效果相同).
    # (與上邊的": >"效果相同, 但是某些shell可能不支持這種形式.)
 
COMMAND_OUTPUT >>
    # 將stdout重定向到一個文件.
    # 如果文件不存在, 那么就創建它, 如果存在, 那么就追加到文件后邊.
 
 
    # 單行重定向命令(只會影響它們所在的行):
# --------------------------------------------------------------------
1>filename
    # 重定向stdout到文件"filename".
1>>filename
    # 重定向並追加stdout到文件"filename".
2>filename
    # 重定向stderr到文件"filename".
2>>filename
    # 重定向並追加stderr到文件"filename".
&>filename
    # 將stdout和stderr都重定向到文件"filename".
 
M>N
   # "M"是一個文件描述符, 如果沒有明確指定的話默認為1.
   # "N"是一個文件名.
   # 文件描述符"M"被重定向到文件"N".
M>&N
   # "M"是一個文件描述符, 如果沒有明確指定的話默認為1.
   # "N"是另一個文件描述符.
 
# --------------------------------------------------------------------
# 重定向stdout, 一次一行.
LOGFILE=script.log
 
echo "This statement is sent to the log file, \"$LOGFILE\"." 1>$LOGFILE
echo "This statement is appended to \"$LOGFILE\"." 1>>$LOGFILE
echo "This statement is also appended to \"$LOGFILE\"." 1>>$LOGFILE
echo "This statement is echoed to stdout, and will not appear in \"$LOGFILE\"."
# 每行過后, 這些重定向命令會自動"reset".
 
 
# --------------------------------------------------------------------
# 重定向stderr, 一次一行.
ERRORFILE=script.errors
 
bad_command1 2>$ERRORFILE       #  Error message sent to $ERRORFILE.
bad_command2 2>>$ERRORFILE      #  Error message appended to $ERRORFILE.
bad_command3                    #  Error message echoed to stderr,
                                 #+ and does not appear in $ERRORFILE.
# 每行過后, 這些重定向命令也會自動"reset".
 
# --------------------------------------------------------------------
2>&1
    # 重定向stderr到stdout.
    # 將錯誤消息的輸出, 發送到與標准輸出所指向的地方.
 
i>&j
    # 重定向文件描述符i到j.
    # 指向i文件的所有輸出都發送到j.
 
>&j
    # 默認的, 重定向文件描述符1(stdout)到j.
    # 所有傳遞到stdout的輸出都送到j中去.
 
0< FILENAME
  < FILENAME
    # 從文件中接受輸入.
    # 與">"是成對命令, 並且通常都是結合使用.
    #
    # grep search-word <filename
 
[j]<>filename
    # 為了讀寫"filename", 把文件"filename"打開, 並且將文件描述符"j"分配給它.
    # 如果文件"filename"不存在, 那么就創建它.
    # 如果文件描述符"j"沒指定, 那默認是fd 0, stdin.
    #
    # 這種應用通常是為了寫到一個文件中指定的地方.
    echo 1234567890 > File    # 寫字符串到"File".
    exec 3<> File             # 打開"File"並且將fd 3分配給它.
    read -n 4 <&3             # 只讀取4個字符.
    echo -n . >&3             # 寫一個小數點.
    exec 3>&-                 # 關閉fd 3.
    cat File                  # ==> 1234.67890
    # 隨機訪問.
 
|
    # 管道.
    # 通用目的處理和命令鏈工具.
    # 與">", 很相似, 但是實際上更通用.
    # 對於想將命令, 腳本, 文件和程序串連起來的時候很有用.
    cat *.txt | sort | uniq > result- file
    # 對所有.txt文件的輸出進行排序, 並且刪除重復行.
    # 最后將結果保存到"result-file"中.
  • 可以將輸入輸出重定向和(或)管道的多個實例結合到一起寫在同一行上.
    command < input-file > output-file
    command1 | command2 | command3 > output-file
    

請參考例子 12-28和例子 A-15.

  • 可以將多個輸出流重定向到一個文件上.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    ls -yz >> command .log 2>&1
    #  將錯誤選項"yz"的結果放到文件"command.log"中.
    #  因為stderr被重定向到這個文件中,
    #+ 所有的錯誤消息也就都指向那里了.
     
    #   注意, 下邊這個例子就不會給出相同的結果.
    ls -yz 2>&1 >> command .log
    #  輸出一個錯誤消息, 但是並不寫到文件中.
     
    #  如果將stdout和stderr都重定向,
    #+ 命令的順序會有些不同.
  • 關閉文件描述符
    1
    2
    3
    4
    5
    6
    7
    8
    9
    n<&-
         #關閉輸入文件描述符n.
     
    0<&-, <&-
         #關閉stdin.
    n>&-
         #關閉輸出文件描述符n.
    1>&-, >&-
         #關閉stdout.

子進程繼承了打開的文件描述符. 這就是為什么管道可以工作. 如果想阻止fd被繼承, 那么可以關掉它.

1
2
3
4
5
6
7
8
9
10
11
# 只將stderr重定到一個管道.
 
exec 3>&1                              # 保存當前stdout的"值"(將fd3指向fd0相同目標)
ls -l 2>&1 >&3 3>&- | grep bad 3>&-    # 對'grep'關閉fd 3(但不關閉'ls',正常輸出內容不受grep影響)
#              ^^^^   ^^^^
ls -l 2>&1 >&3 | grep bad              # 這樣輸出內容被轉到了fd3,也不會受grep影響
ls badabc -l 2>&1 >&3 | grep bad        # stderr通過fd1輸出,會受grep影響
 
exec 3>&-                              # 對於剩余的腳本來說, 關閉它.
 
# 感謝, S.C.

如果想了解關於I/O重定向更多的細節, 請參考Appendix E. 注意事項

  • 一個文件描述符說白了就是文件系統為了跟蹤這個打開的文件而分配給它的一個數字. 也可以的將其理解為文件指針的一個簡單版本. 與C語言中文件句柄的概念很相似.
  • 使用文件描述符5可能會引起問題. 當Bash使用exec創建一個子進程的時候, 子進程會繼承fd5(參考Chet Ramey的歸檔e-mail, SUBJECT: RE: File descriptor 5 is held open). 最好還是不要去招惹這個特定的fd.

使用exec

exec <filename命令會將stdin重定向到文件中. 從這句開始, 所有的stdin就都來自於這個文件了, 而不是標准輸入(通常都是鍵盤輸入). 這樣就提供了一種按行讀取文件的方法, 並且可以使用sed和/或awk來對每一行進行分析.

  • 例子 16-1. 使用exec重定向stdin
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    #!/bin/bash
    # 使用'exec'重定向stdin.
     
     
    exec 6<&0          # 將文件描述符#6與stdin鏈接起來.
                        # 保存stdin.
     
    exec < data- file   # stdin被文件"data-file"所代替.
     
    read a1            # 讀取文件"data-file"的第一行.
    read a2            # 讀取文件"data-file"的第二行.
     
    echo
    echo "Following lines read from file."
    echo "-------------------------------"
    echo $a1
    echo $a2
     
    echo ; echo ; echo
     
    exec 0<&6 6<&-
    #  現在將stdin從fd #6中恢復, 因為剛才我們把stdin重定向到#6了,
    #+ 然后關閉fd #6 ( 6<&- ), 好讓這個描述符繼續被其他進程所使用.
    #
    # <&6 6<&-    這么做也可以.
     
    echo -n "Enter data  "
    read b1  # 現在"read"已經恢復正常了, 就是能夠正常的從stdin中讀取.
    echo "Input read from stdin."
    echo "----------------------"
    echo "b1 = $b1"
     
    echo
     
    exit 0

同樣的, exec >filename命令將會把stdout重定向到一個指定的文件中. 這樣所有命令的輸出就都會發送到那個指定的文件, 而不是stdout.

  • Important

exec N > filename會影響整個腳本或當前shell. 對於這個指定PID的腳本或shell來說, 從這句命令執行之后, 就會重定向到這個文件中, 然而 . . .

N > filename只會影響新fork出來的進程, 而不會影響整個腳本或shell. not the entire script or shell.

感謝你, Ahmed Darwish, 指出這個問題.

  • 例子 16-2. 使用exec來重定向stdout
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    #!/bin/bash
    # reassign-stdout.sh
     
    LOGFILE=logfile.txt
     
    exec 6>&1           # 將fd #6與stdout鏈接起來.
                         # 保存stdout.
     
    exec > $LOGFILE     # stdout就被文件"logfile.txt"所代替了.
     
    # ----------------------------------------------------------- #
    # 在這塊中所有命令的輸出都會發送到文件$LOGFILE中.
     
    echo -n "Logfile: "
    date
    echo "-------------------------------------"
    echo
     
    echo "Output of \"ls -al\" command"
    echo
    ls -al
    echo ; echo
    echo "Output of \"df\" command"
    echo
    df
     
    # ----------------------------------------------------------- #
     
    exec 1>&6 6>&-      # 恢復stdout, 然后關閉文件描述符#6.
     
    echo
    echo "== stdout now restored to default == "
    echo
    ls -al
    echo
     
    exit 0
  • 例子 16-3. 使用exec在同一個腳本中重定向stdin和stdout
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    #!/bin/bash
    # upperconv.sh
    # 將一個指定的輸入文件轉換為大寫.
     
    E_FILE_ACCESS=70
    E_WRONG_ARGS=71
     
    if [ ! -r "$1" ]     # 判斷指定的輸入文件是否可讀?
    then
       echo "Can't read from input file!"
       echo "Usage: $0 input-file output-file"
       exit $E_FILE_ACCESS
    fi                   #  即使輸入文件($1)沒被指定
                          #+ 也還是會以相同的錯誤退出(為什么?).
     
    if [ -z "$2" ]
    then
       echo "Need to specify output file."
       echo "Usage: $0 input-file output-file"
       exit $E_WRONG_ARGS
    fi
     
     
    exec 4<&0
    exec < $1            # 將會從輸入文件中讀取.
     
    exec 7>&1
    exec > $2            # 將寫到輸出文件中.
                          # 假設輸出文件是可寫的(添加檢查?).
     
    # -----------------------------------------------
         cat - | tr a-z A-Z   # 轉換為大寫.
    #   ^^^^^                # 從stdin中讀取.
    #           ^^^^^^^^^^   # 寫到stdout上.
    # 然而, stdin和stdout都被重定向了.
    # -----------------------------------------------
     
    exec 1>&7 7>&-       # 恢復stout.
    exec 0<&4 4<&-       # 恢復stdin.
     
    # 恢復之后, 下邊這行代碼將會如預期的一樣打印到stdout上.
    echo "File \"$1\" written to \"$2\" as uppercase conversion."
     
    exit 0

I/O重定向是一種避免可怕的子shell中不可訪問變量問題的方法.

  • 例子 16-4. 避免子shell
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    #!/bin/bash
    # avoid-subshell.sh
    # 由Matthew Walker所提出的建議.
     
    Lines=0
     
    echo
     
    cat myfile.txt | while read line;  #  (譯者注: 管道會產生子shell)
                      do {
                        echo $line
                        (( Lines++ ));  #  增加這個變量的值
                                        #+ 但是外部循環卻不能訪問.
                                        #  子shell問題.
                      }
                      done
     
    echo "Number of lines read = $Lines"     # 0
                                              # 錯誤!
     
    echo "------------------------"
     
    exec 3<> myfile.txt
    while read line <&3
    do {
       echo "$line"
       (( Lines++ ));                   #  增加這個變量的值
                                        #+ 現在外部循環就可以訪問了.
                                        #  沒有子shell, 現在就沒問題了.
    }
    done
    exec 3>&-
     
    echo "Number of lines read = $Lines"     # 8
     
    echo
     
    exit 0
     
    # 下邊這些行是這個腳本的結果, 腳本是不會走到這里的.
     
    $ cat myfile.txt
     
    Line 1.
    Line 2.
    Line 3.
    Line 4.
    Line 5.
    Line 6.
    Line 7.
    Line 8.

代碼塊重定向

象while, until, 和for循環代碼塊, 甚至if/then測試結構的代碼塊, 都可以對stdin進行重定向. 即使函數也可以使用這種重定向方式(請參考例子 23-11). 要想做到這些, 都要依靠代碼塊結尾的<操作符.

  • 例子 16-5. while循環的重定向
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    #!/bin/bash
    # redir2.sh
     
    if [ -z "$1" ]
    then
       Filename=names.data       # 如果沒有指定文件名, 則使用這個默認值.
    else
       Filename=$1
    fi
    #+ Filename=${1:-names.data}
    #  這句可代替上面的測試(參數替換).
     
    count=0
     
    echo
     
    while [ "$name" != Smith ]  # 為什么變量$name要用引號?
    do
       read name                 # 從$Filename文件中讀取輸入, 而不是在stdin中讀取輸入.
       echo $name
       let "count += 1"
    done < "$Filename"           # 重定向stdin到文件$Filename.
    #    ^^^^^^^^^^^^
     
    echo ; echo "$count names read" ; echo
     
    exit 0
     
    #  注意在一些比較老的shell腳本編程語言中,
    #+ 重定向的循環是放在子shell里運行的.
    #  因此, $count 值返回后會是 0, 此值是在循環開始前的初始值.
    #  *如果可能的話*, 盡量避免在Bash或ksh中使用子shell,
    #+ 所以這個腳本能夠正確的運行.
    #  (多謝Heiner Steven指出這個問題.)
     
    #  然而 . . .
    #  Bash有時還是*會*在一個使用管道的"while-read"循環中啟動一個子shell,
    #+ 與重定向的"while"循環還是有區別的.
     
    abc=hi
    echo -e "1\n2\n3" | while read l
          do abc= "$l"
             echo $abc
          done
    echo $abc
     
    #  感謝, Bruno de Oliveira Schneider
    #+ 給出上面的代碼片段來演示此問題.
    #  同時, 感謝, Brian Onn, 修正了一個注釋錯誤.
  • 例子 16-6. 重定向while循環的另一種形式
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    #!/bin/bash
     
    # 這是上個腳本的另一個版本.
     
    #  Heiner Steven建議,
    #+ 為了避免重定向循環運行在子shell中(老版本的shell會這么做), 最好讓重定向循環運行在當前工作區內,
    #+ 這樣的話, 需要提前進行文件描述符重定向,
    #+ 因為變量如果在(子shell上運行的)循環中被修改的話, 循環結束后並不會保存修改后的值.
     
     
    if [ -z "$1" ]
    then
       Filename=names.data     # 如果沒有指定文件名則使用默認值.
    else
       Filename=$1
    fi
     
    exec 3<&0                 # 將stdin保存到文件描述符3.
    exec 0< "$Filename"        # 重定向標准輸入.
     
    count=0
    echo
     
    while [ "$name" != Smith ]
    do
       read name               # 從stdin(現在已經是$Filename了)中讀取.
       echo $name
       let "count += 1"
    done                      #  從文件$Filename中循環讀取
                               #+ 因為文件(譯者注:指默認文件, 在本節最后)有20行.
     
    #  這個腳本原先在"while"循環的結尾還有一句:
    #+      done <"$Filename"
    #  練習:
    #  為什么不需要這句了?
     
    exec 0<&3                 # 恢復保存的stdin.
    exec 3<&-                 # 關閉臨時文件描述符3.
     
    echo ; echo "$count names read" ; echo
     
    exit 0
  • 例子 16-7. 重定向until循環
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    #!/bin/bash
    # 和前面的例子相同, 但使用的是"until"循環.
     
    if [ -z "$1" ]
    then
       Filename=names.data         # 如果沒有指定文件名那就使用默認值.
    else
       Filename=$1
    fi
     
    # while [ "$name" != Smith ]
    until [ "$name" = Smith ]     # 把!=改為=.
    do
       read name                   # 從$Filename中讀取, 而不是從stdin中讀取.
       echo $name
    done < "$Filename"             # 重定向stdin到文件$Filename.
    #    ^^^^^^^^^^^^
     
    # 結果和前面例子的"while"循環相同.
     
    exit 0
  • 例子 16-8. 重定向for循環
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    #!/bin/bash
     
    if [ -z "$1" ]
    then
       Filename=names.data          # 如果沒有指定文件名就使用默認值.
    else
       Filename=$1
    fi
     
    line_count=` wc $Filename | awk '{ print $1 }' `
    #           目標文件的行數.
    #
    #  此處的代碼太過做作, 並且寫得很難看,
    #+ 但至少展示了"for"循環的stdin可以重定向...
    #+ 當然, 你得足夠聰明, 才能看得出來.
    #
    # 更簡潔的寫法是     line_count=$(wc -l < "$Filename")
     
     
    for name in ` seq $line_count`  # "seq"打印出數字序列.
    # while [ "$name" != Smith ]   --   比"while"循環更復雜   --
    do
       read name                    # 從$Filename中, 而非從stdin中讀取.
       echo $name
       if [ "$name" = Smith ]       # 因為用for循環, 所以需要這個多余測試.
       then
         break
       fi
    done < "$Filename"              # 重定向stdin到文件$Filename.
    #    ^^^^^^^^^^^^
     
    exit 0

我們也可以修改前面的例子使其能重定向循環的標准輸出.

  • 例子 16-9. 重定向for循環(stdin和stdout都進行重定向)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    #!/bin/bash
     
    if [ -z "$1" ]
    then
       Filename=names.data          # 如果沒有指定文件名, 則使用默認值.
    else
       Filename=$1
    fi
     
    Savefile=$Filename.new         # 保存最終結果的文件名.
    FinalName=Jonah                # 終止"read"時的名稱.
     
    line_count=` wc $Filename | awk '{ print $1 }' # 目標文件的行數.
     
     
    for name in ` seq $line_count`
    do
       read name
       echo "$name"
       if [ "$name" = "$FinalName" ]
       then
         break
       fi
    done < "$Filename" > "$Savefile"     # 重定向stdin到文件$Filename,
    #    ^^^^^^^^^^^^^^^^^^^^^^^^^^^       並且將它保存到備份文件中.
     
    exit 0
  • 例子 16-10. 重定向if/then測試結構
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    #!/bin/bash
     
    if [ -z "$1" ]
    then
       Filename=names.data   # 如果文件名沒有指定, 使用默認值.
    else
       Filename=$1
    fi
     
    TRUE=1
     
    if [ "$TRUE" ]          # if true    和   if :   都可以.
    then
      read name
      echo $name
    fi < "$Filename"
    #  ^^^^^^^^^^^^
     
    # 只讀取了文件的第一行.
    # An "if/then"測試結構不能自動地反復地執行, 除非把它們嵌到循環里.
     
    exit 0
  • 例子 16-11. 用於上面例子的"names.data"數據文件
    Aristotle
    Belisarius
    Capablanca
    Euler
    Goethe
    Hamurabi
    Jonah
    Laplace
    Maroczy
    Purcell
    Schmidt
    Semmelweiss
    Smith
    Turing
    Venn
    Wilson
    Znosko-Borowski
    
    #  此數據文件用於:
    #+ "redir2.sh", "redir3.sh", "redir4.sh", "redir4a.sh", "redir5.sh".
    

重定向代碼塊的stdout, 與"將代碼塊的輸出保存到文件中"具有相同的效果. 請參考例子 3-2.

重定向的應用

巧妙地運用I/O重定向, 能夠解析和粘合命令輸出的各個片斷(請參考例子 11-7). 這樣就可以產生報告與日志文件.

  • 例子 16-12. 事件紀錄
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    #!/bin/bash
    # logevents.sh, 由Stephane Chazelas所編寫.
     
    # 把事件記錄在一個文件中.
    # 必須以root身份運行 (這樣才有權限訪問/var/log).
     
    ROOT_UID=0     # 只有$UID值為0的用戶才具有root權限.
    E_NOTROOT=67   # 非root用戶的退出錯誤.
     
     
    if [ "$UID" - ne "$ROOT_UID" ]
    then
       echo "Must be root to run this script."
       exit $E_NOTROOT
    fi
     
    FD_DEBUG1=3
    FD_DEBUG2=4
    FD_DEBUG3=5
     
    # 去掉下邊兩行注釋中的一行, 來激活腳本.
    # LOG_EVENTS=1
    # LOG_VARS=1
     
     
    log()  # 把時間和日期寫入日志文件.
    {
    echo "$(date)  $*" >&7     # 這會把日期*附加*到文件中.
                                   # 參考下邊的代碼.
    }
     
    case $LOG_LEVEL in
      1) exec 3>&2         4> /dev/null 5> /dev/null ;;
      2) exec 3>&2         4>&2         5> /dev/null ;;
      3) exec 3>&2         4>&2         5>&2;;
      *) exec 3> /dev/null 4> /dev/null 5> /dev/null ;;
    esac
     
    FD_LOGVARS=6
    if [[ $LOG_VARS ]]
    then exec 6>> /var/log/vars .log
    else exec 6> /dev/null               # 丟棄輸出.
    fi
     
    FD_LOGEVENTS=7
    if [[ $LOG_EVENTS ]]
    then
       # then exec 7 >(exec gawk '{print strftime(), $0}' >> /var/log/event.log)
       # 上面這行不能在2.04版本的Bash上運行.
       exec 7>> /var/log/event .log        # 附加到"event.log".
       log                                      # 記錄日期與時間.
    else exec 7> /dev/null                  # 丟棄輸出.
    fi
     
    echo "DEBUG3: beginning" >&${FD_DEBUG3}
     
    ls -l >&5 2>&4                       # command1 >&5 2>&4
     
    echo "Done"                                # command2
     
    echo "sending mail" >&${FD_LOGEVENTS}   # 將字符串"sending mail"寫到文件描述符#7.
     
    exit 0

Appendix E. I/O和I/O重定向的詳細介紹

由Stephane Chazelas編寫, 本書作者修訂

一個命令期望前3個文件描述符是可用的. 第一個, fd 0(標准輸入, stdin), 用作讀取. 另外兩個, (fd 1, stdout和fd 2, stderr), 用來寫入.

每個命令都會關聯到stdin, stdout, 和stderr. ls 2>&1意味着臨時的將ls命令的stderr連接到shell的stdout.

按慣例, 命令一般都是從fd 0(stdin)上讀取輸入, 打印輸出到fd 1(stdout)上, 錯誤輸出一般都輸出到fd 2(stderr)上. 如果這3個文件描述中的某一個沒打開, 你可能就會遇到麻煩了:

bash$ cat /etc/passwd >&-
cat: standard output: Bad file descriptor

比如說, 當xterm運行的時候, 它首先會初始化自身. 在運行用戶shell之前, xterm會打開終端設備(/dev/pts/<n> 或者類似的東西)三次.

這里, Bash繼承了這三個文件描述符, 而且每個運行在Bash上的命令(子進程)也都依次繼承了它們, 除非你重定向了這些命令. 重定向意味着將這些文件描述符中的某一個, 重新分配到其他文件中(或者分配到一個管道中, 或者是其他任何可能的東西). 文件描述符既可以被局部重分配(對於一個命令, 命令組, 一個子shell, 一個while循環, if或case結構...), 也可以全局重分配, 對於余下的shell(使用exec).

ls > /dev/null 表示將運行的ls命令的fd 1連接到/dev/null上.

bash$ lsof -a -p $$ -d0,1,2
COMMAND PID     USER   FD   TYPE DEVICE SIZE NODE NAME
 bash    363 bozo        0u   CHR  136,1         3 /dev/pts/1
 bash    363 bozo        1u   CHR  136,1         3 /dev/pts/1
 bash    363 bozo        2u   CHR  136,1         3 /dev/pts/1

bash$ exec 2> /dev/null
bash$ lsof -a -p $$ -d0,1,2
COMMAND PID     USER   FD   TYPE DEVICE SIZE NODE NAME
 bash    371 bozo        0u   CHR  136,1         3 /dev/pts/1
 bash    371 bozo        1u   CHR  136,1         3 /dev/pts/1
 bash    371 bozo        2w   CHR    1,3       120 /dev/null

bash$ bash -c 'lsof -a -p $$ -d0,1,2' | cat
COMMAND PID USER   FD   TYPE DEVICE SIZE NODE NAME
 lsof    379 root    0u   CHR  136,1         3 /dev/pts/1
 lsof    379 root    1w  FIFO    0,0      7118 pipe
 lsof    379 root    2u   CHR  136,1         3 /dev/pts/1

bash$ echo "$(bash -c 'lsof -a -p $$ -d0,1,2' 2>&1)"
COMMAND PID USER   FD   TYPE DEVICE SIZE NODE NAME
 lsof    426 root    0u   CHR  136,1         3 /dev/pts/1
 lsof    426 root    1w  FIFO    0,0      7520 pipe
 lsof    426 root    2w  FIFO    0,0      7520 pipe

這是用來展示不同類型的重定向.

練習: 分析下面的腳本.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#!/usr/bin/env bash
 
mkfifo /tmp/fifo1 /tmp/fifo2
 
while read a; do echo "FIFO1: $a" ; done < /tmp/fifo1 &
exec 7> /tmp/fifo1
exec 8> >( while read a; do echo "FD8: $a, to fd7" ; done >&7)
 
exec 3>&1
(
  (
   (
    while read a; do echo "FIFO2: $a" ; done < /tmp/fifo2 | tee /dev/stderr | tee /dev/fd/4 | tee /dev/fd/5 | tee /dev/fd/6 >&7 &
    exec 3> /tmp/fifo2
 
    echo 1st, to stdout
    sleep 1
    echo 2nd, to stderr >&2
    sleep 1
    echo 3rd, to fd 3 >&3
    sleep 1
    echo 4th, to fd 4 >&4
    sleep 1
    echo 5th, to fd 5 >&5
    sleep 1
    echo 6th, through a pipe | sed 's/.*/PIPE: &, to fd 5/' >&5
    sleep 1
    echo 7th, to fd 6 >&6
    sleep 1
    echo 8th, to fd 7 >&7
    sleep 1
    echo 9th, to fd 8 >&8
 
   ) 4>&1 >&3 3>&- | while read a; do echo "FD4: $a" ; done 1>&3 5>&- 6>&-
  ) 5>&1 >&3 | while read a; do echo "FD5: $a" ; done 1>&3 6>&-
) 6>&1 >&3 | while read a; do echo "FD6: $a" ; done 3>&-
 
rm -f /tmp/fifo1 /tmp/fifo2
 
# 對於每個命令和子shell, 分別指出每個fd的指向.
 
exit 0


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM