Linux-expect指令碼
expect是基於tcl演變而來的,所以很多語法和tcl類似,基本的語法如下所示:
- 首行加上/usr/bin/expect
- spawn: 後面加上需要執行的shell命令,比如說spawn sudo touch testfile
- expect: 只有spawn執行的命令結果才會被expect捕捉到,因為spawn會啟動一個程序,只有這個程序的相關資訊才會被捕捉到,主要包括:標準輸入的提示資訊,eof和timeout。
- send和send_user:send會將expect指令碼中需要的資訊傳送給spawn啟動的那個程序,而send_user只是回顯使用者發出的資訊,類似於shell中的echo而已。
一個小例子,用於linux下賬戶的建立:
filename: account.sh,可以使用./account.sh newaccout來執行;
#!/usr/bin/expect set passwd "mypasswd"【這個是你設定的密碼】 set timeout 60 if {$argc != 1} { send "usage ./account.sh \$newaccount\n" exit } set user [lindex $argv [expr $argc-1]] spawn sudo useradd -s /bin/bash -g mygroup -m $user expect { "assword" { send_user "sudo now\n" send "$passwd\n" exp_continue } eof { send_user "eof\n" } } spawn sudo passwd $user expect { "assword" { send "$passwd\n" exp_continue } eof { send_user "eof" } } spawn sudo smbpasswd -a $user expect { "assword" { send "$passwd\n" exp_continue } eof { send_user "eof" } }
注意點:
第3行: 對變數賦值的方法;
第4行: 預設情況下,timeout是10秒;
第6行: 引數的數目可以用$argc得到;
第11行:引數存在$argv當中,比如取第一個引數就是[lindex $argv 0];並且如果需要計算的話必須用expr,如計算2-1,則必須用[expr 2-1];
第13行:用spawn來執行一條shell命令,shell命令根據具體情況可自行調整;有文章說sudo要加-S,經過實際測試,無需加-S亦可;
第15行:一般情況下,如果連續做兩個expect,那麼實際上是序列執行的,用例子中的結構則是並行執行的,主要是看匹配到了哪一個;在這個例子中,如果你寫成序列的話,即
expect "assword" send "$passwd\n" expect eof send_user "eof"
那麼第一次將會正確執行,因為第一次sudo時需要密碼;但是第二次執行時由於密碼已經輸過(預設情況下sudo密碼再次輸入時間為5分鐘),則不會提示使用者去輸入,所以第一個expect將無法匹配到assword,而且必須注意的是如果是spawn命令出現互動式提問的但是expect匹配不上的話,那麼程式會按照timeout的設定進行等待;可是如果spawn直接發出了eof也就是本例的情況,那麼expect "assword"將不會等待,而直接去執行expect eof。
這時就會報expect: spawn id exp6 not open,因為沒有spawn在執行,後面的expect指令碼也將會因為這個原因而不再執行;所以對於類似sudo這種命令分支不定的情況,最好是使用並行的方式進行處理;
第17行:僅僅是一個使用者提示而已,可以刪除;
第18行:向spawn程序傳送password;
第19行:使得spawn程序在匹配到一個後再去匹配接下來的互動提示;
第21行:eof是必須去匹配的,在spawn程序結束後會向expect傳送eof;如果不去匹配,有時也能執行,比如sleep多少秒後再去spawn下一個命令,但是不要依賴這種行為,很有可能今天還可以,明天就不能用了;
其他
下面這個例子比較特殊,在整個過程中就不能expect eof了:
#!/usr/bin/expect set timeout 30 spawn ssh 10.192.224.224 expect "password:" send "mypassword\n" expect "*$" send "mkdir tmpdir\n" expect "*$"
這個例子實際上是通過ssh去登入遠端機器,並且在遠端機器上創佳一個目錄,我們看到在我們輸入密碼後並沒有去expect eof,這是因為ssh這個spawn並沒有結束,而且手動操作時ssh實際上也不會自己結束除非你exit;所以你只能expect bash的提示符,當然也可以是機器名等,這樣才可以在遠端建立一個目錄。
注意,請不要用
spawn mkdir tmpdir
,這樣會使得上一個spawn即ssh結束,那麼你的tmpdir將在本機建立。
當然實際情況下可能會要你確認ssh key,可以通過並行的expect進行處理,不多贅述。覺得bash很多情況下已經很強大,所以可能用expect只需要掌握這些就好了,其他的如果用到可以再去google了。
例項:下面這個指令碼是完成對單個伺服器scp任務。
#!/usr/bin/expect set timeout 10 set host [lindex $argv 0] set username [lindex $argv 1] set password [lindex $argv 2] set src_file [lindex $argv 3] set dest_file [lindex $argv 4] spawn scp $src_file [email protected]$host:$dest_file expect { "(yes/no)?" { send "yes\n" expect "*assword:" { send "$password\n"} } "*assword:" { send "$password\n" } } expect "100%" expect eof
注意程式碼剛開始的第一行,指定了expect的路徑,與shell指令碼相同,這一句指定了程式在執行時到哪裡去尋找相應的啟動程式。程式碼剛開始還設定了timeout的時間為10秒,如果在執行scp任務時遇到了程式碼中沒有指定的異常,則在等待10秒後該指令碼的執行會自動終止。
spawn代表在本地終端執行的語句,在該語句開始執行後,expect開始捕獲終端的輸出資訊,然後做出對應的操作。expect程式碼中的捕獲的(yes/no)內容用於完成第一次訪問目標主機時儲存金鑰的操作。有了這一句,scp的任務減少了中斷的情況。程式碼結尾的expect eof與spawn對應,表示捕獲終端輸出資訊的終止。
有了這段expect的程式碼,還只能完成對單個遠端主機的scp任務。如果需要實現批量scp的任務,則需要再寫一個shell指令碼來呼叫這個expect指令碼。
#!/bin/sh list_file=$1 src_file=$2 dest_file=$3 cat $list_file | while read line do host_ip=`echo $line | awk '{print $1}'` username=`echo $line | awk '{print $2}'` password=`echo $line | awk '{print $3}'` echo "$host_ip" ./expect_scp $host_ip $username $password $src_file $dest_file done
很簡單的程式碼,指定了3個引數:列表檔案的位置、本地原始檔路徑、遠端主機目標檔案路徑。需要說明的是其中的列表檔案指定了遠端主機ip、使用者名稱、密碼,這些資訊需要寫成以下的格式:
IP username password
中間用空格或tab鍵來分隔,多臺主機的資訊需要寫多行內容。
這樣就指定了兩臺遠端主機的資訊。注意,如果遠端主機密碼中有“$”、“#”這類特殊字元的話,在編寫列表檔案時就需要在這些特殊字元前加上轉義字元,否則expect在執行時會輸入錯誤的密碼。
對於這個shell指令碼,儲存為batch_scp.sh檔案,與剛才儲存的expect_scp檔案和列表檔案(就定義為hosts.list檔案吧)放到同一目錄下,執行時按照以下方式輸入命令就可以了:
./batch_scp.sh ./hosts.list /root/src_file /root/destfile
下面我們來看一些expect的一些內部引數:
exp_continue [-continue_timer] The command exp_continue allows expect itself to continue executing rather than returning as it normally would. By default exp_continue resets the timeout timer. The -continue_timer flag prevents timer from being restarted. exp_version [[-exit] version] is useful for assuring that the script is compatible with the current version of Expect. With no arguments, the current version of Expect is returned. This version may then be encoded in your script. If you actually know that you are not using features of recent versions, you can specify an ear- lier version.
具體的用法還可以檢視文件~
#!/bin/sh
# \
exec expect -- "$0" ${1+"[email protected]"}
exp_version -exit 5.0
if {$argc!=2} {
send_user "usage: remote-exec command password\n"
send_user "Eg. remote-exec \"ssh [email protected] ls\; echo done\" password\n"
send_user "or: remote-exec \"scp /local-file [email protected]:/remote-file\" password\n"
send_user "or: remote-exec \"scp [email protected]:/remote-file local-file\" password\n"
send_user "or: remote-exec \"rsync --rsh=ssh /local-file [email protected]:/remote-file\" password\n"
send_user "Caution: command should be quoted.\n"
exit
}
set cmd [lindex $argv 0]
set password [lindex $argv 1]
eval spawn $cmd
set timeout 600
while {1} {
expect -re "Are you sure you want to continue connecting (yes/no)?" {
# First connect, no public key in ~/.ssh/known_hosts
send "yes\r"
} -re "assword:" {
# Already has public key in ~/.ssh/known_hosts
send "$password\r"
} -re "Permission denied, please try again." {
# Password not correct
exit
} -re "kB/s|MB/s" {
# User equivalence already established, no password is necessary
set timeout -1
} -re "file list ..." {
# rsync started
set timeout -1
} -re "bind: Address already in use" {
# For local or remote port forwarding
set timeout -1
} -re "Is a directory|No such file or directory" {
exit
} -re "Connection refused" {
exit
} timeout {
exit
} eof {
exit
}
}
注意用法:
Eg. remote-exec "ssh [email protected] ls; echo done" password
or: remote-exec "scp /local-file [email protected]:/remote-file" password
or: remote-exec "scp [email protected]:/remote-file local-file" password
or: remote-exec "rsync --rsh=ssh /local-file [email protected]:/remote-file" password
Caution: command should be quoted.