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

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

system函數能夠直接讓perl調用操做系統中的命令並執行。html

system入門示例

例如:shell

#!/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會將它們用空格的方式鏈接起來。bash

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

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

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

在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有兩種語法:spa

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系統函數(man execvp)來執行,它的效率比unix的system()更高
  2. 當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%;
}
相關文章
相關標籤/搜索