shell腳本實現ssh自動登錄遠程服務器示例:
#!/usr/bin/expect
spawn ssh root@192.168.22.194
expect "*password:"
send "123\r"
expect "*#"
interact
Expect是一個用來處理交互的命令。借助Expect,我們可以將交互過程寫在一個腳本上,使之自動化完成。形象的說,ssh登錄,ftp登錄等都符合交互的定義。下文我們首先提出一個問題,然后介紹基礎知四個命令,最后提出解決方法。
問題
如何從機器A上ssh到機器B上,然后執行機器B上的命令?如何使之自動化完成?
四個命令
Expect中最關鍵的四個命令是send,expect,spawn,interact。
send:用於向進程發送字符串
expect:從進程接收字符串
spawn:啟動新的進程
interact:允許用戶交互
1. send命令
send命令接收一個字符串參數,並將該參數發送到進程。
expect1.1> send "hello world\n"
hello world
2. expect命令
(1)基礎知識
expect命令和send命令正好相反,expect通常是用來等待一個進程的反饋。expect可以接收一個字符串參數,也可以接收正則表達式參數。和上文的send命令結合,現在我們可以看一個最簡單的交互式的例子:
expect "hi\n"
send "hello there!\n"
這兩行代碼的意思是:從標准輸入中等到hi和換行鍵后,向標准輸出輸出hello there。
tips: $expect_out(buffer)存儲了所有對expect的輸入,<$expect_out(0,string)>存儲了匹配到expect參數的輸入。
比如如下程序:
expect "hi\n"
send "you typed <$expect_out(buffer)>"
send "but I only expected <$expect_out(0,string)>"
當在標准輸入中輸入
test
hi
是,運行結果如下
you typed: test
hi
I only expect: hi
(2)模式-動作
expect最常用的語法是來自tcl語言的模式-動作。這種語法極其靈活,下面我們就各種語法分別說明。
單一分支模式語法:
expect "hi" {send "You said hi"}
匹配到hi后,會輸出"you said hi"
多分支模式語法:
expect "hi" { send "You said hi\n" } \
"hello" { send "Hello yourself\n" } \
"bye" { send "That was unexpected\n" }
匹配到hi,hello,bye任意一個字符串時,執行相應的輸出。等同於如下寫法:
expect {
"hi" { send "You said hi\n"}
"hello" { send "Hello yourself\n"}
"bye" { send "That was unexpected\n"}
}
3. spawn命令
上文的所有demo都是和標准輸入輸出進行交互,但是我們跟希望他可以和某一個進程進行交互。spawm命令就是用來啟動新的進程的。spawn后的send和expect命令都是和spawn打開的進程進行交互的。結合上文的send和expect命令我們可以看一下更復雜的程序段了。
set timeout -1
spawn ftp ftp.test.com //打開新的進程,該進程用戶連接遠程ftp服務器
expect "Name" //進程返回Name時
send "user\r" //向進程輸入anonymous\r
expect "Password:" //進程返回Password:時
send "123456\r" //向進程輸入don@libes.com\r
expect "ftp> " //進程返回ftp>時
send "binary\r" //向進程輸入binary\r
expect "ftp> " //進程返回ftp>時
send "get test.tar.gz\r" //向進程輸入get test.tar.gz\r
這段代碼的作用是登錄到ftp服務器ftp ftp.uu.net上,並以二進制的方式下載服務器上的文件test.tar.gz。程序中有詳細的注釋。
4.interact
到現在為止,我們已經可以結合spawn、expect、send自動化的完成很多任務了。但是,如何讓人在適當的時候干預這個過程了。比如下載完ftp文件時,仍然可以停留在ftp命令行狀態,以便手動的執行后續命令。interact可以達到這些目的。下面的demo在自動登錄ftp后,允許用戶交互。
spawn ftp ftp.test.com
expect "Name"
send "user\r"
expect "Password:"
send "123456\r"
interact
解決方法
上文中提到:
如何從機器A上ssh到機器B上,然后執行機器B上的命令?如何使之自動化完成?
下面一段腳本實現了從機器A登錄到機器B,然后執行機器B上的pwd命令,並停留在B機器上,等待用戶交互。具體含義請參考上文。
#!/home/tools/bin/64/expect -f
set timeout -1
spawn ssh $BUser@$BHost
expect "*password:" { send "$password\r" }
expect "$*" { send "pwd\r" }
interact
expect學習筆記及實例詳解
1. expect 是基於tcl 演變而來的,所以很多語法和tcl 類似,基本的語法如下
所示:
1.1 首行加上/usr/bin/expect
1.2 spawn: 后面加上需要執行的shell 命令,比如說spawn sudo touch testfile
1.3 expect: 只有spawn 執行的命令結果才會被expect 捕捉到,因為spawn 會啟動一個進程,只有這個進程的相關信息才會被捕捉到,主要包括:標准輸入的提示信息,eof 和timeout。
1.4 send 和send_user:send 會將expect 腳本中需要的信息發送給spawn 啟動的那個進程,而send_user 只是回顯用戶發出的信息,類似於shell 中的echo 而已。
2. 一個小例子,用於linux 下賬戶的建立:
filename: account.sh,可以使用./account.sh newaccout 來執行;
1 #!/usr/bin/expect
2
3 set passwd "mypasswd"
4 set timeout 60
5
6 if {$argc != 1} {
7 send "usage ./account.sh \$newaccount\n"
8 exit
9 }
10
11 set user [lindex $argv [expr $argc-1]]
12
13 spawn sudo useradd -s /bin/bash -g mygroup -m $user
14
15 expect {
16 "assword" {
17 send_user "sudo now\n"
18 send "$passwd\n"
19 exp_continue
20 }
21 eof
22 {
23 send_user "eof\n"
24 }
25 }
26
27 spawn sudo passwd $user
28 expect {
29 "assword" {
30 send "$passwd\n"
31 exp_continue
32 }
33 eof
34 {
35 send_user "eof"
36 }
37 }
38
39 spawn sudo smbpasswd -a $user
40 expect {
41 "assword" {
42 send "$passwd\n"
43 exp_continue
44 }
45 eof
46 {
47 send_user "eof"
48 }
49 }
3. 注意點:
第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 與“{ ”之間直接必須有空格或則TAB間隔,否則會出麻煩,會報錯invalid command name "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 下一個命令,但是不
要依賴這種行為,很有可能今天還可以,明天就不能用了;
4. 其他
下面這個例子比較特殊,在整個過程中就不能expect eof 了:
1 #!/usr/bin/expect
2
3 set timeout 30
4 spawn ssh 10.192.224.224
5 expect "password:"
6 send "mypassword\n"
7 expect "*$"
8 send "mkdir tmpdir\n" #遠程執行命令用send發送,不用spawn
9 expect "*$" #注意這個地方,要與操作系統上環境變量PS1相匹配,尤其是有PS1有空格的情況下,一定在expct "*$ "把空格加上,加不上你就完蛋了。我試過。
這個例子實際上是通過ssh 去登錄遠程機器,並且在遠程機器上創佳一個目錄,
我們看到在我們輸入密碼后並沒有去expect eof,這是因為ssh 這個spawn 並沒
有結束,而且手動操作時ssh 實際上也不會自己結束除非你exit;所以你只能
expect bash 的提示符,當然也可以是機器名等,這樣才可以在遠程創建一個目錄。
注意,請不要用spawn mkdir tmpdir,這樣會使得上一個spawn 即ssh 結束,那
么你的tmpdir 將在本機建立。
當然實際情況下可能會要你確認ssh key,可以通過並行的expect 進行處理,不
多贅述。
5. 覺得bash 很多情況下已經很強大,所以可能用expect 只需要掌握這些就好了,
其他的如果用到可以再去google 了。
源代碼圖片:
6 \實例:下面這個腳本是完成對單個服務器scp任務。
1: #!/usr/bin/expect
2:
3: set timeout 10
4: set host [lindex $argv 0]
5: set username [lindex $argv 1]
6: set password [lindex $argv 2]
7: set src_file [lindex $argv 3]
8: set dest_file [lindex $argv 4]
9:
10: spawn scp $src_file $username@$host:$dest_file
11: expect {
12: "(yes/no)?"
13: {
14: send "yes\n"
15: expect "*assword:" { send "$password\n"}
16: }
17: "*assword:"
18: {
19: send "$password\n"
20: }
21: }
22: expect "100%"
23: expect eof
參考源代碼圖片:
注意代碼剛開始的第一行,指定了expect的路徑,與shell腳本相同,這一句指定了程序在執行時到哪里去尋找相應的啟動程序。代碼剛開始還設定了timeout的時間為10秒,如果在執行scp任務時遇到了代碼中沒有指定的異常,則在等待10秒后該腳本的執行會自動終止。
spawn代表在本地終端執行的語句,在該語句開始執行后,expect開始捕獲終端的輸出信息,然后做出對應的操作。expect代碼中的捕獲的(yes/no)內容用於完成第一次訪問目標主機時保存密鑰的操作。有了這一句,scp的任務減少了中斷的情況。代碼結尾的expect eof與spawn對應,表示捕獲終端輸出信息的終止。
有了這段expect的代碼,還只能完成對單個遠程主機的scp任務。如果需要實現批量scp的任務,則需要再寫一個shell腳本來調用這個expect腳本。
1: #!/bin/sh
2:
3: list_file=$1
4: src_file=$2
5: dest_file=$3
6:
7: cat $list_file | while read line
8: do
9: host_ip=`echo $line | awk '{print $1}'`
10: username=`echo $line | awk '{print $2}'`
11: password=`echo $line | awk '{print $3}'`
12: echo "$host_ip"
13: ./expect_scp $host_ip $username $password $src_file $dest_file
15: done
參考代碼圖片如下:
很簡單的代碼,指定了3個參數:列表文件的位置、本地源文件路徑、遠程主機目標文件路徑。需要說明的是其中的列表文件指定了遠程主機ip、用戶名、密碼,這些信息需要寫成以下的格式:
IP username password
中間用空格或tab鍵來分隔,多台主機的信息需要寫多行內容。
這樣就指定了兩台遠程主機的信息。注意,如果遠程主機密碼中有“$”、“#”這類特殊字符的話,在編寫列表文件時就需要在這些特殊字符前加上轉義字符,否則expect在執行時會輸入錯誤的密碼。
對於這個shell腳本,保存為batch_scp.sh文件,與剛才保存的expect_scp文件和列表文件(就定義為hosts.list文件吧)放到同一目錄下,執行時按照以下方式輸入命令就可以了: