調用操作系統命令: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系統函數(man execvp)來執行,它的效率比unix的system()更高
- 如果有這些需要shell解析的特殊元字符,則調用
- 當system的參數是一個包含多元素的列表:
- 它將認為列表中的第一個元素是待執行的命令,並直接執行它,而不會先調用shell,再通過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等進程操作方式,它們的細節,都可man system、man exec、man fork等等來獲取。在后文會一一解釋,此處先解釋system執行的細節。
在執行到system時,system會直接拷貝一份當前perl進程(稱為子進程),然后自己進入睡眠態,並使用waitpid()等待子進程執行完畢。
unix系統中的system()用來調用一個shell解釋器來執行命令,用來啟動一個新的程序,是fork+execl("/bin/sh -c COMMAND")+waitpid()的結合,因為多一層shell的調用,效率相比於fork+exec來說較低,且需要waitpid()的等待,無法控制子進程也無法並發。
perl的system()和unix的system()不太一樣,多了一層判斷來決定是使用fork+execl("/bin/sh -c COMMAND")+waitpid()還是直接使用fork+execvp(COMMAND)+waitpid()。
因為是直接拷貝的,所以子進程初始時和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,見perl和操作系統交互:fork。
調用操作系統命令:反引號和qx()
perl中的system()和exec()執行命令時,都是直接執行命令,並將執行結果輸出到某個地方(比如屏幕)。但是反引號(`COMMAND`)可以將執行的結果插入到某個地方或者進行賦值,而不是直接輸出。就像shell中的反引號一樣。
例如,將操作系統中date命令的執行結果賦值給一個變量。
#!/usr/bin/perl
my $date=`date +"%F %T"`;
print $date;
由於反引號是將命令的輸出結果捕獲起來並插入到某個地方或賦值,如果反引號單獨成一個語句,也即是在空上下文(void)中,它的結果會丟棄。一般來說,這是多此一舉或者是浪費的行為,除非是要通過執行命令臨時做出某些設置:
`date +"%F %T"`; # 命令的結果將直接丟棄
qx()和反引號執行命令是一樣的,只不過寫法不同,使得某些特殊符號的處理變得更容易,就像shell中也有一個$()的方式替換` `。特別地,由於perl反引號是以雙引號的方式解釋反引號內部的內容,如果反引號中間有perl可以解釋的特殊符號,就會被perl先解釋,再傳遞給shell去執行。如果使用qx並使用單引號作為定界符(即qx'COMMAND'),perl將使用單引號的方式去解釋COMMAND,使得perl不再解釋一些特殊符號。
例如下面的例子中,在shell環境中導出了一個環境變量name,值為"Gaoxiaofang",而perl程序內部正好也定義了一個變量$name,這時使用反引號`COMMAND`和qx'COMMAND'就不再一樣。
以下是shell中執行的命令:
export name="Gaoxiaofang"
以下是perl程序中的內容
#!/usr/bin/perl
$name="Malongshuai";
my $new_name1=`echo $name`;
print $new_name1; # 輸出Malongshuai
my $new_name2=qx'echo $name';
print $new_name2; # 輸出Gaoxiaofang
但需要注意的是,shell反引號做的命令替換,由於常用來插入到某個表達式中間,所以shell在反引號執行完畢后會自動移除換行符,除非使用雙引號包圍反引號。而perl則有所不同,perl總會保證所執行即所得,perl的反引號會保留每一個換行符。
一般來說,在perl中使用反引號的時候,都會使用chomp去除最后一個換行符。
chomp(my $date=`date +"%F %T"`);
print $date;
如果在列表上下文中使用反引號,則反引號中命令的每一行輸出都會保存為列表的元素。
my @new_name=qx'who';
print "$new_name[0]";
同樣地,可以將反引號放進foreach,因為foreach的迭代目標正是一個列表:
foreach (`cat /etc/passwd`){
print $_ if m%bin/bash%;
}
