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