1. 程式人生 > >Perl和操作系統交互(一):system、exec和反引號

Perl和操作系統交互(一):system、exec和反引號

拷貝 擔心 關於 調試 分析 除了 特殊功能 多余 以及

調用操作系統命令:system函數

system函數可以直接讓perl調用操作系統中的命令並執行。

system入門示例

例如:

#!/usr/bin/perl

system 'date +"%F %T"';
system 'echo hello world';
system 'echo',"hello","world";

執行結果:

2018-06-21 18:32:50
hello world
hello world

註意system的參數可以被單個引號包圍,也可以用多個引號分隔成多個參數,如果分隔開,system會將它們用空格的方式連接起來。

另外,上面使用了單引號、雙引號,都能正確執行,但註意,雙引號會解析perl中的特殊符號。例如:

$myname="Malongshuai";
system "echo $myname";   # 輸出:Malongshuai
system 'echo $USER';     # 輸出當前登錄的用戶:root

可見,雙引號中的變量$myname被perl解析了,而單引號中的變量$USER不被perl解析,perl將其交給bash,由shell負責解析,所以會輸出當前用戶名。

在system中,還可以使用shell的重定向、管道等功能。

$myname="Malongshuai";
system "echo $myname >/tmp/a.txt";
print "==============================\n";
system "cat <1.plx";
print "==============================\n";
system 'find . -type f -name "*.pl" -print0 | xargs -0 -i ls -l {}';
system 'sleep 30 &';

深入system

system有兩種語法:

system LIST
system PROGRAM LIST

這裏忽略第二種,因為它是一種以欺騙的防止執行命令的:LIST中的第一個參數作為命令,但欺騙自己說自己執行的是PROGRAM命令。

下面將詳細討論第一種語法。

基礎知識

在討論之前,先解釋一下bash命令行執行命令時的引號解析問題。例如:

awk -F ":" 'NR<=3{username=$1;print "username:",username}' /etc/passwd
find /root -type f -name "*.log"

shell命令行中執行命令時,包含兩部分:一個是程序名,一個是程序的參數部分。在真正執行之前,shell的詞法分析行為會解析程序名稱、參數部分。但有些時候命令行中會使用一些shell的特殊符號來實現shell的特殊功能。例如shell的星號通配符*、管道功能|、重定向功能> < >> << <<<、命令替換功能$()等。但有些程序自身,其用法規則中可能也會使用一些特殊符號(如find -name "*.log"的星號),這會和shell的特殊符號沖突。由於shell的解析行為在命令執行之前,為了保留特殊符號給程序自身來解釋,需要使用引號來保護這些特殊符號以避免被shell解析。

正如上面awk中的":"‘{}‘以及find中的"*.log",它們都使用引號包圍特殊符號,使得這些符號"逃過"shell的解析過程,從而讓程序自身解析。

更通俗一點,如果不是執行命令要依賴於shell環境的存在,如果能直接在最純粹的環境中執行命令,那麽特殊符號是無需加引號保護的。例如,awk如果能脫離shell單獨執行,下面的第一條命令才是正確的,第二條命令卻是錯誤的。

awk -F : NR<=3{username=$1;print "username:",username} /etc/passwd
awk -F ":" 'NR<=3{username=$1;print "username:",username}' /etc/passwd

system參數細節

system LIST中的system要求的是列表上下文參數LIST,就像print函數一樣。所以,當LIST是一個標量字符串,它其實也是一個列表,只不過是只包含一個元素的列表。

例如:

system 'find /perlapp -type f -name "*.pl"';   # 是一個標量字符串構成的LIST

system "ls","-lh","/root";    # 包含多元素的列表參數

@cmd_arg=qw(-lh /root);
system "ls",@cmd_arg;       # 包含多元素的列表參數

對於system LIST語法,perl在執行LIST中的命令之前,會先檢查LIST:

  1. 當system的參數是一個只有單元素的列表(即上面第一個例子),它將檢查這個參數整體中是否有需要shell解析的特殊元字符(如shell中的通配符* ? [],shell中的重定向< > >> <<< <<,shell中的管道|,shell的後臺任務符號&,命令替換$()等等):
    • 如果有這些需要shell解析的特殊元字符,則調用/bin/sh -c STRING的方式來執行LIST,其中LIST就是STRING部分
    • 如果沒有需要shell解析的特殊元字符,則perl將其分割成一個一個單詞,並傳遞給execvp系統函數來執行,它的效率更高
  2. 當system的參數是一個包含多元素的列表:
    • 它將認為列表中的第一個元素是待執行的命令,並直接執行它(按照spawn的方式),而不會先調用bash,再通過bash shell來解析並執行它。
    • 所以,使用多元素的列表參數時,將失去shell中重定向、管道、命令替換等等功能
    • 但如果第一個元素作為命令spawn失敗(和語法、參數等無關,而是權限或其它系統層面的失敗),將降級回使用shell來執行

註:bash -c STRING的c選項會從STRING中讀取命令並執行。

幾個示例:

@arg1=qw(-lh /root);
system "ls",@arg1;          # 1.可正確執行

system "ls -lh /root/*.log"; # 2.可正確執行

@arg2=qw(-lh /root/*.log);
system "ls",@arg2;           # 3.將執行失敗

system "ls -lh","/root";     # 4.執行失敗,更準確的是spawn過程就失敗
system "ls","-lh /root";     # 5.執行失敗
system "ls","-l -h","/root"; # 6.執行失敗

上面第二個system能執行成功,而第三個system會執行失敗,是因為:

  • 第二個system的參數是一個單元素的列表,而且有需要解析的通配星號字符,所以它等價於/bin/sh -c ls -lh /root/*.log命令
  • 第三個system的參數是多個元素構成的列表,所以它會直接spawn一個ls進程,由於不在shell環境中執行,ls程序又不認識星號字符,所以執行失敗

第四個system也執行失敗,因為不止一個參數,於是取第一個參數作為命令來spawn新的進程,但這第一個參數是ls -lh整體,而不是ls,這等價於"ls -lh" /root,所以spawn失敗,找不到這個命令。

第5個system執行失敗,因為"-lh /root"作為列表的第二個元素,它是一個整體。所以它等價於ls "-lh /root",這顯然是錯誤的。

第6個system執行失敗,原因同上。

所以可以稍微總結下,如果使用多個參數的system,每個原本在unix shell命令行中需要空格分開的選項和參數,都需要單獨作為列表的獨立元素

正如:

system "ls","-lh","/root";

@args=qw(-lh /root);
system "ls",@args;

更復雜一點的示例:

@cmd_arg1=qw(/perlapp -type f -name *.pl);
system "/usr/bin/find",@cmd_arg1;        # 1.正確

@cmd_arg2=qw(/perlapp -type f -name "*.pl");   # 加上了雙引號
system "/usr/bin/find",@cmd_arg2;        # 2.錯誤

$prog="/usr/bin/awk";
@arg3=("-F",":",'NR<=3{username=$1;print "username: ",username}','/etc/passwd');
system $prog,@arg3;      # 3.正確

上面第二個system中,是多參數的system,不會調用shell來解析,而*.pl使用了引號包圍,但對於find來說,引號不可識別的字符,它會將其當作要查找文件名的一部分,所以執行失敗。之所以在shell命令中的find要加上引號,是為了防止*被shell解析。

第三個system中,沒有使用qw()的方式生成列表,因為awk的表達式部分存在空格,使用qw生成列表的方式無法保留空格,所以這裏采用最原始的生成列表的形式。當然,也可以實現split來生成:

@arg3=split /%/,q(-F%:%NR<=3{username=$1;print "username: ",username}%/etc/passwd);

使用單個參數還是多參數?

關於使用單個參數的system還是使用多參數的system。

如果對shell解析熟悉,使用單個參數比較好,能比較直接地使用shell相關的功能(重定向、管道等)。但使用單個參數,引號引用和轉義引用方面畢竟比較復雜,容易出錯,可能需要多次調試。

多個參數也有好處,不用擔心太多引號問題,但卻失去了使用shell功能的能力。如果想要在多參數的system中使用管道、重定向等特殊符號帶來的shell功能,可以將‘/bin/sh‘,‘-c‘作為system的前兩個參數,使得system強制調用shell來執行命令。

/bin/sh -c STRING執行命令的方式是shell從STRING中讀取命令來執行。所以,為了保證完整性,STRING部分建議全都包含在一個引號中。例如:

shell> bash -c 'find . -type f -name "*.pl" | xargs ls -l'

回到system的調用/bin/sh -c的用法,例如:

$arg1=q(find . -type f -name "*.pl" -print0);    # 1
$arg2=q( | xargs -0 -i ls -l {});                # 2
system '/bin/sh','-c',"$arg1 $arg2";             # 3

上面3行,每行都有關鍵點:

  • 第一行:
    • 不能使用數組、列表,而是標量的字符串
    • 因為要給shell解析,所以*.pl還是要加上引號包圍
  • 第二行:
    • 同樣,不能使用數組、列表,而是標量字符串
    • 即使是特殊的管道符號(或其它符號),也可以直接放在標量字符串中
  • 第三行:
    • 前兩個參數是/bin/sh-c
    • 第三個參數必須是字符串STRING,強烈建議使用引號包圍,保證參數的完整性
    • 如果不加引號包圍STRING,而是將arg1和arg2作為參數列表的兩個元素,將割裂兩者,導致只執行到$arg1中的命令,甚至有時候會因為$arg1不完整或有多余字符而報錯

看上去規則很多,而且書寫必須十分規範,失之毫厘,結果將差之千裏。如非必須,還不如直接寫成單個參數的system。例如,上面的3行等價於:

system '/bin/sh','-c','find . -type f -name "*.pl -print0 | xargs -0 -i ls -l {}"';
system 'find . -type f -name "*.pl -print0 | xargs -0 -i ls -l {}"';

捕獲system的錯誤狀態

system執行命令時的返回值為$?,它和bash的$?不太一致。當最後一個管道關閉時、反引號執行命令、wait()或waitpid()成功執行時或system(),都會返回$?。在Perl中,$?包含兩部分共16字節,低8位是信號信息,高8位才是所執行的命令的狀態碼。也就是說,perl中的$?的高8位才對應bash中的$?

因此,要獲取退出狀態碼,需要使用$?>>8

#!/usr/bin/perl

system '(exit 4)';
print $?>>8,"\n";    # 輸出4

如果,想要直接在執行的命令上判斷命令是否正確執行,然後決定是否die。可以在system的前面加上一個!取反。這是因為在shell中,非0的狀態碼表示命令錯誤執行,0狀態碼才表示執行正確。這和perl的布爾值正好相反,所以加上感嘆號取反:

!system '(exit 4)' or die "command return error num: ",$?>>8;

需要註意,這裏不能使用$!,在perl中有多種不同的錯誤捕獲變量,$!捕獲的是perl在發起系統調用層面的錯誤,而system執行的命令的錯誤發生在命令執行時。對於system函數來說,perl只要成功執行system,不管裏面的命令是否執行成功,perl發起的系統調用都已經結束了。

關於如何獲取信號信息,參見官方手冊。或者:

The “low” octet combines several things. The highest bit notes if a core dump happened.The hexadecimal and binary representations (recall them from Chapter 2) can help mask out the parts you don’t want:

my $low_octet = $return_value & 0xFF; # mask out high octet
my $dumped_core = $low_octet & 0b1_0000000; # 128
my $signal_number = $low_octet & 0b0111_1111; # 0x7f, or 127

system的內部細節

在Perl中,除了system,還有exec、fork、pipe、IPC等進程操作方式,在後文會一一解釋。此處先解釋system執行的細節。

在執行到system時,system會直接拷貝一份當前perl進程(稱為子進程),然後自己進入睡眠態,並使用wait()等待子進程執行完畢。

因為是直接拷貝的,所以子進程初始時和perl父進程是完全一致的。所以,標準輸入(STDIN)、標準輸出(STDOUT)、標準錯誤輸出(STDERR)都是和父進程共享的。

system 'read -p "enter your name: " name;echo "your name is: " $name';

在system中的命令執行之前,perl首先會解析system的參數列表,關於解析的方式,在前文已經詳細解釋過了。如果命令是直接執行的,則命令所在進程就是perl進程的子進程。如果命令需要通過通過調用/bin/sh -c來執行,則shell進程是子進程,真正執行的命令則是孫進程(grandchild)或者是下一代。
例如,在參數中放入shell的for循環,因為這是bash內置屬性,它會直接在當前bash進程中完成。

system 'for i in {1..10};do echo $i;done';

這些內容比較復雜,可參見:bash內置命令的特殊性,後臺任務的"本質"

當命令執行完畢後,將回到perl進程,perl進程會執行wait(),然後結束system。

調用操作系統命令:exec

exec和system除了一種行為之外,其它用法和system完全一致。exec和system的區別之處在於:

  • system會創建子進程,然後自己進入睡眠,去等待子進程執行完畢,最後執行wait()
  • exec不會創建子進程,而是在當前Perl進程自身去執行命令,相當於用命令去覆蓋當前進程,所以沒有睡眠
  • 當exec執行的命令結束後,將直接結束當前perl進程,沒有wait()行為

由於exec執行完命令後,立即退出當前perl進程,所以命令執行的正確與否,無法被捕獲。但如果exec啟動待執行命令過程就出錯了,這屬於perl的系統調用過程出錯,可以使用$!捕獲。

exec 'date';
die "date couldn't run: $!";

一般來說,很少直接使用exec,而是fork+exec同時使用。關於fork,見後文。

調用操作系統命令:反引號和qx()

Perl和操作系統交互(一):system、exec和反引號