2010年10月29日 星期五

linux 整合測試

請將受測的Linux IP位址更換為 140.137.215.201–240,例如座號5號同學,其受測Linux IP140.137.215.205。以下將以XXX代表IP的最後一碼。


*測試主機IP:140.137.215.238

1.Linux中依指定要求,建立群組與帳號:
      群組: 10%
群組名稱GID
boss800
webuser801
webclient802

帳號: 20%
帳號名稱群組密碼UID
zoebossahbhc1801
nancywebuserworiow1802
eddiewebuserwienrm1803
hpwebclientxidiwo1804
acerwebclientghjdwd1805


     ans:
           #groupadd -g 800 boss
           #groupadd -g 801 webuser
           #groupadd -g 800 webclient


           #useradd -u 1801 -g boss zoe       //也可以用script做,但人數還ok用手打
           #useradd -u 1802 -g webuser nancy
           #useradd -u 1803 -g webuser eddie
           #useradd -u 1804 -g webclient hp
           #useradd -u 1805 -g webclient acer
           #passwd zoe ...


        測試:
           #su - max
           $su - zoe ...     //使用一般帳號變換身分看帳密是否設定正確


2.請為主機網頁伺服器建立以下虛擬主機,不同網址必須顯示不同的內容,如下表:30%
網址顯示內容
http://140.137.215.XXXHome of 140.137.215.XXX
http://www.aXXX.snpy.orgHome of www.aXXX.snpy.org
http://hp.aXXX.snpy.orgHome of hp
  驗證方法:實際開啟網頁,辨認有無上表顯示內容文字(大小寫須符合)

     ans:
           #cd /var/named/chroot
           #vim etc/named.conf   //在DNS external段加入新的zone
             zone "a238.snpy.org" IN {
                    type master;
                    file "named.snpy238";
             };

           #cp var/named/localhost.zone var/named/named.snpy238  //拿sample來改
           #vim var/named/named.snpy238
$TTL    86400
@               IN SOA  @       root (
                                        50              ; serial (d. adams)
                                        3H              ; refresh
                                        15M             ; retry
                                        1W              ; expiry
                                        1D )            ; minimum

                IN      NS      @
                IN      A       140.137.215.238
www          IN      A       140.137.215.238
hp             IN      A       140.137.215.238

           #/etc/init.d/named restart
           #vim /etc/httpd/conf/httpd.conf   //加入
     
NameVirtualHost *:80

Alias /corp "/opt/corp"      //以下四行為第五題部分,在此一次設定
<Directory "/opt/corp/">
     AllowOverride all
</Directory>

<VirtualHost *:80>
    DocumentRoot /var/www/html
    ServerName 140.137.215.238
</VirtualHost>
<VirtualHost *:80>
    DocumentRoot /opt/www
    ServerName www.a238.snpy.org
</VirtualHost>
<VirtualHost *:80>
    DocumentRoot /opt/hp
    ServerName hp.a238.snpy.org
</VirtualHost>
           #mkdir /opt/www
           #mkdir /opt/hp
           #mkdir /opt/corp
           #echo "
Home of www.a238.snpy.org" > /opt/www/index.html
           #echo "
Home of hp" > /opt/hp/index.html
           #echo "
Home of Home of 140.137.215.238" > /opt/www/index.html
           #chown  hp -R /opt/hp
           #chown :webuser -R /opt/www   //修改群組
           #chmod 2770 -R /opt/www    //讓群組有讀寫權,並利用sgid讓產生出來的檔案皆為webuser群組
           #/etc/init.d/httpd resta

      測試:
         用browser連三個網站要分別正常顯示該顯示的網頁內容
          
3.nancyeddie是本公司網站www.aXXX.snpy.org管理員,請為他們兩位設計上傳網頁的解決方案, 
讓他們可以用FTP上傳網頁到家目錄的www目錄下, 即可共同管理與更新公司首頁內容。15%

驗證方法:以nancyeddie帳號使用FTP連至www.aXXX.snpy.org,切換目錄至www
並上傳一個與index.html同名檔案,試圖覆蓋原檔案。

ans:
     #vim /etc/vsftpd/vsftpd.conf   //修改並設定
     anonymous_enable=NO
     local_umask=002
     chroot_local_user=YES
     chroot_list_enable=YES    //這兩項用來設定除chroot_list中以外的使用者都只能在家目錄中
     #vim /etc/vsftpd/chroot_list   //加入
        nancy
        eddie
     #ln -s /opt/www /home/nancy/www
     #ln -s /opt/www /home/nancy/www

4.hp是本公司的虛擬主機客戶, 擁有 hp.aXXX.snpy.org網址請為hp使用者設計SFTPSCP的上傳方式, 
並信任他的公鑰, 讓他們可以不用輸入密碼即能上傳首頁檔案至家目錄的www目錄。15%

驗證方式:以scp複製一個與首頁檔index.html檔同名的檔案到hp.aXXX.snpy.orgwww目錄下。
(scp index.html hp@hp.aXXX.snpy.org:~/www/)

ans:
      #ssh-keygen   //預設用rsa格式產生公私鑰
      //公鑰放到/home/hp中,私鑰放在要連線過來的主機帳號下
      #mkdir /home/hp/.ssh
      #cat id_rsa.pub > .ssh/authorized_keys
      #chown hp:webclient /home/hp/.ssh
      #chmod 700 /home/hp/.ssh
      #chmod 600 /home/hp/.ssh/authorized_keys
      #ln -s /opt/hp /home/hp/www

5.       acer是本公司的合作夥伴,請在本公司的首頁下新增一目錄corp,網址如下:http://www.aXXX.snpy.org/corp/ 
使用帳號acer與密碼acc989才能夠看到此目錄下的檔案清單。10%

驗證方法:以瀏覽器開啟http://www.aXXX.snpy.org/corp/,以上述規定帳號與密碼登入,檢視是否顯示檔案清單。

ans:
      //由於在設定httpd.conf時以開啟可用htaccess來做設定,alias也已做好
      #vim /opt/corp/.htaccess  //加入
     
AuthUserFile /opt/corp/.htpasswd
AuthName "acer only"
AuthType Basic
require valid-user

Options Indexes

            #htpasswd -c /opt/corp/.passwd acer    //輸入密碼即可




Linux 排程

1.一次性排程: atd
   指令格式: #at 時:分

   [root@localhost ~]# at 09:30   #9:30分時執行
   at> logger 1234567
   at> <EOT>     #用Ctrl+d離開
   job 1 at 2010-10-29 09:30   #此排程序號為1

   [root@localhost ~]# atq    #查看系統現有一次性排程,可用at -l
   3       2010-10-29 09:55 a root
   4       2010-10-29 10:00 a root
   2       2010-10-29 09:50 a root
   [root@localhost ~]# atrm 3   #刪除現有一次性排程,可用at -d
   [root@localhost ~]# atq
   4       2010-10-29 10:00 a root
   2       2010-10-29 09:50 a root

   [root@localhost ~]# at -m 09:54 #執行完排程會將輸出結果mail給設定者
  
   [root@localhost ~]# at now +3 minute(s)  #3分鐘後執行

   atd 相關設定: 在/etc/下
   (1)at.deny: 表示檔案內的使用者不可用cronjob
   (2)at.allow:表示檔案內的使用者可以使用 
       #at.allow用於較嚴謹的管控,兩個檔案同時存在時at.allow生效

2.重複性排程工作: cron job
   指令: crontab -e   #個人,以vi編輯
          分  時  日  月  星期    (執行人)  工作  #僅有root可指定由誰執行

   [root@localhost ~]# crontab -e
   35 10 * * *  logger "root cron job"   #每天早上10:30執行一次
   */5 * * * * /root/test.sh    #每五分鐘執行一次
   30 16 * * 5 mail max@max.com < /root/test.txt  #每周五下午16:30執行一次

   Cron Job 相關設定: 在/etc/下
   (1)cron.deny: 表示檔案內的使用者不可用cronjob
   (2)cron.allow:表示檔案內的使用者可以使用  #優先順序同at.allow,at.deny
   (3)crontab:系統執行的cronjob

2010年10月28日 星期四

Apache安全性設定

前一篇說明apache基本設定,本篇說明安全性的部分
1.一般可使用TCP_wrapper與apache本身的allow,deny來阻擋來源ip
2.另外可使用htaccess方式來限制能讀取該網頁的使用者
   (1)[root@localhost ~]#vim /etc/httpd/conf/httpd.conf
     <Directory "/opt/mis/">
          AllowOverride all
     </Directory>

   (2)[root@localhost ~]#cd /opt/mis
       [root@localhost mis]#vim .htaccess
           Options Indexes
           AuthUserFile /opt/mis/.htpasswd  #指定密碼檔放置位置
           AuthName "mis"
           AuthType Basic
           require valid-user   #表示要正確登入的user才可存取
       [root@localhost mis]#htpasswd -c .htpasswd max
       New passward:
       Re-type new password:

       [root@localhost mis]#htpasswd  .htpasswd marcus

3.HTTPS(SSL)--一般憑證須由公正CA核發,這邊只為測試自己產生
   (1)[root@localhost ~]#rpm -qa |grep ssl   #確定有安裝module
       [root@localhost ~]#cat /etc/httpd/conf/httpd.conf |grep mod_ssl 確定apache 有load module
   (2)[root@localhost ~]#cd /etc/pki/tls/certs
       產生金鑰:max.key
       [root@localhost ~]#openssl genrsa -out max.key 1024  #表示用rsa方式產生1024位元的key

       產生CSR憑證:max.csr--正確流程為csr完成後將key與csr交給CA產生crt
       [root@localhost certs]# openssl req -new -key max.key -out max.key
       You are about to be asked to enter information that will be incorporated into your certificate request.
       What you are about to enter is what is called a Distinguished Name or a DN.
       There are quite a few fields but you can leave some blank For some fields there will be a default value,
       If you enter '.', the field will be left blank.
       -----
       Country Name (2 letter code) [GB]:TW
       State or Province Name (full name) [Berkshire]:none
       Locality Name (eg, city) [Newbury]:Taipei
       Organization Name (eg, company) [My Company Ltd]:Max com.
       Organizational Unit Name (eg, section) []:Sales
       Common Name (eg, your name or your server's hostname) []:www.max.com
       Email Address []:max@max.com

       Please enter the following 'extra' attributes
       to be sent with your certificate request
       A challenge password []:
       An optional company name []:
      
       產生crt憑證
       [root@localhost certs]# openssl x509 -req -days 365 -in max.csr -signkey max.key -out max.crt
       #上列紅字表示憑證有效期限
       處理max.key
       [root@localhost certs]# mv max.key /etc/pki/tls/private/

       修改ssl config
       [root@localhost certs]# vim /etc/httpd/conf.d/ssl.conf
       修改成:
       SSLCertificateFile /etc/pki/tls/certs/max.crt
       SSLCertificateKeyFile /etc/pki/tls/private/max.key

Apache基本設定

環境:
1.CentOS 5.5
2.apache 2.2.3(httpd-2.2.3-43.el5.centos)
3.設定檔:/etc/httpd/conf/httpd.conf
4.apache模組檔:/etc/httpd/conf.d/

實作:
1.[root@localhost ~]#yum install httpd php
2.[root@localhost ~]#vim /etc/httpd/conf/httpd.conf
«Directory /»        #設定目錄預設環境
    Options FollowSymLinks    #啟用軟連結,有Indexes表示啟用檔案列表
    AllowOverride None 
    Order deny,allow   #先處理deny才處理allow,後面設定會覆蓋前面
    Deny from All
«/Directory»

DocumentRoot "/var/www/html"  #表示網站預設目錄為此
«Directory "/var/www/html"»  #設定/var/www/html環境
     AllowOverride all   #表示要使用htaccess來設定權限,None為不啟用
     Order deny,allow 
     Deny from all
     Allow from 140.137.215. #表示全擋,只讓140.137.215.0網段連線
«/Directory»

Alias /mis "/opt/mis"   #別名設定,表示140.137.215.105/mis的內容放在/opt/mis中
Alias /phpbb "/opt/phpbb"

NameVirtualHost *:80   #啟用虛擬主機功能

«VirtualHost *:80»     #如果直接以ip連線顯示的網頁目錄
    DocumentRoot /var/www/html
    ServerName 140.137.215.105
«/VirtualHost»
«VirtualHost *:80»
    DocumentRoot /opt/html
    ServerName www.a105.snpy.org
«/VirtualHost»
«VirtualHost *:80»
    DocumentRoot /opt/hello
    ServerName hello.a105.snpy.org
«/VirtualHost»   

3.[root@localhost ~]#/etc/init.d/httpd restart

2010年10月22日 星期五

DNS練習

以"marcus.org"這個假網域做練習

1.環境:
    OS:  CentOS 5.5
    套件:bind-9.3.6-4.P1.el5_4.2
                bind-utils-9.3.6-4.P1.el5_4.2
                bind-chroot-9.3.6-4.P1.el5_4.2

2.設定:
     #cd /var/named/chroot       //預設啟動chroot
     #ls
         //如果沒有 var與etc或有但沒有應有檔案,需到/usr/share/doc/bind-9.3.6/sample/
           中將這兩個目錄拷貝回來
    # cp /usr/share/doc/bind-9.3.6/sample/* . -rf
    # /usr/sbin/dns-keygen       //記得先做這個動作,將key放到named.conf中
        ZOjwOIHPkDlcEzHNkHUbtjCeKWCgzOeqKIUQgV4ZK9p4an7ELNBSZNKn3g5w
    # vim etc/named.conf           //放入下面這段
        key ddns_key
        {
                algorithm hmac-md5;
                secret "ZOjwOIHPkDlcEzHNkHUbtjCeKWCgzOeqKIUQgV4ZK9p4an7ELNBSZNKn3g5w";
        };

    <1>以localhost練習
         #vim etc/named.conf     //可看到 view "localhost-resolver"段有一個設定
           include "/etc/named.rfc1912.zones";    //代表與localhost相關的zone設定檔
         #vim /etc/named.rfc1912.zones
           加入:
                zone "marcus.org" IN {
                         type master;
                         file "named.marcus";     //資源設定檔位置,預設在var/named/
                         allow-update { none; };
                };

        #cp var/named/localhost.zone /var/named/named.marcus  //用範例檔來做
        #vim /var/named/named.marcus
          修改成:

                $TTL    86400
                @               IN SOA  @       root (
                                                                          42              ; serial (d. adams)
                                                                          3H              ; refresh
                                                                          15M             ; retry
                                                                          1W              ; expiry
                                                                          1D )            ; minimum

                                     IN      NS      @
                                     IN      A       140.137.215.105       
                www             IN      A       140.137.215.105
        #/etc/init.d/named restart

        #dig www.marcus.org @localhost
        結果:
; <<>> DiG 9.3.6-P1-RedHat-9.3.6-4.P1.el5_4.2 <<>> www.marcus.org @localhost
;; global options:  printcmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 7168
;; flags: qr aa rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 1, ADDITIONAL: 1

;; QUESTION SECTION:
;www.marcus.org.                        IN      A

;; ANSWER SECTION:
www.marcus.org.         86400   IN      A       140.137.215.105

;; AUTHORITY SECTION:
marcus.org.             86400   IN      NS      marcus.org.

;; ADDITIONAL SECTION:
marcus.org.             86400   IN      A       140.137.215.105

;; Query time: 1 msec
;; SERVER: 127.0.0.1#53(127.0.0.1)
;; WHEN: Fri Oct 22 22:09:33 2010
;; MSG SIZE  rcvd: 78
代表設定正常

       <2>如果要對外服務,將zone資料放到named.conf的view "external"區段即可

2010年10月21日 星期四

Linux Quota,Apache,FTP基礎練習

Linux情境練習:
1.安裝一台CentOS
磁碟分割:
/boot200M
/10G
/var6G
/home1G
swap2G

2.新增三個使用者,tom,jack,amy,預設群組mis。每位使用者的家目錄最多只能放到20MB資料,超過18MB則警告。
Quota啟用: #vim /etc/fstab
LABEL=/ / ext3 defaults,grpquota 1 1
LABEL=/home /home ext3 defaults,usrquota 1 2
#mount -o remount /
#mount -o remount /home
#cat /etc/mtab
/dev/sda2 / ext3 rw,grpquota 0 0
/dev/sda5 /home ext3 rw,usrquota 0 0
#quotacheck -uvagmc
#quotaon
新增帳號: #useradd -g mis tom
#useradd -g mis jack
#useradd -g mis amy

Quota設定: 1.USER
#setquota tom 18000 20000 0 0 /home
#setquota jack 18000 20000 0 0 /home
#setquota amy 18000 20000 0 0 /home
or
#edquota -p tom -u jack
#edquota -p tom -u amy
2.GROUP
#setquota -g mis 80000 100000 0 0 /

Quota測試: [root@localhost ~]# su - tom
[tom@localhost ~]$ pwd
/home/tom
[tom@localhost ~]$ dd if=/dev/zero of=testfile bs=1M count=19
sda5: warning, user block quota exceeded.
19+0 records in
19+0 records out
19922944 bytes (20 MB) copied,0.495339 秒,40.2 MB/s
[tom@localhost ~]$ dd if=/dev/zero of=testfile bs=1M count=21
sda5: warning, user block quota exceeded.
sda5: write failed, user block limit reached.
dd: 寫入 ‘testfile’: 硬碟 quota 滿了
20+0 records in
19+0 records out
20385792 bytes (20 MB) copied,0.017867 秒,1.1 GB/s

3.建立一個目錄:/opt/mis,這個目錄為三個使用者的共享目錄。最大100MB,超過80MB警告, 並建立家目錄到此目錄的連結。
目錄建置: [root@localhost ~]#mkdir /opt/mis
[root@localhost ~]#chown root:mis /opt/mis
[root@localhost ~]#chmod 775 /opt/mis

group quota測試如上,但最好使用兩個帳號,分別建立75M與30M大小的檔案測試
連結建置: [root@localhost ~]#ln -s /opt/mis /home/tom/mis
[root@localhost ~]#ln -s /opt/mis /home/jack/mis
[root@localhost ~]#ln -s /opt/mis /home/amy/mis

4./opt/mis設定為可用網址方式存取:http://主機ip/mis/。並設定為須輸入帳密才可讀取檔案目錄
httpd設定: [root@localhost ~]#vim /etc/httpd/conf/httpd.conf
修改:

Options FollowSymLinks
AllowOverride All

加入:
Alias /mis/ "/opt/mis/"

AllowOverride all

[root@localhost ~]#vim /opt/mis/.htacess
Options Indexes
AuthUserFile /opt/mis/.htpasswd
AuthName "mis"
AuthType Basic
require valid-user
[root@localhost ~]#htpasswd -c /opt/mis/.htpasswd tom
[root@localhost ~]#htpasswd -c /opt/mis/.htpasswd jack
[root@localhost ~]#htpasswd -c /opt/mis/.htpasswd amy
[root@localhost ~]#service httpd restart

5.主機首頁由tom與amy維護,請設定一個ftp上傳方案。
ftp設定: [root@localhost ~]#vim /etc/vsftp/vsftpd.conf
修改或加入:
anonymous_enable=NO
chroot_local_user=YES
chroot_list_enable=YES
[root@localhost ~]#vim /etc/vsftp/chroot_list
加入:
tom
amy
[root@localhost ~]#service vsftpd restart
Apache首頁目錄設定: [root@localhost ~]#chown root:apache /var/www/html
[root@localhost ~]#chmod 2775 /var/www/html
[root@localhost ~]#usermod -G apache tom
[root@localhost ~]#usermod -G apache amy
[root@localhost ~]#ln -s /var/www/html /home/tom/html
[root@localhost ~]#ln -s /var/www/html /home/amy/html

2010年10月19日 星期二

Host Route + NAT

Linux 基礎最後一堂課練習一個網路環境,蠻有趣的,特別記錄下來
其中U-Client2比較特別,與其他台的ip網段不同,用以練習Host Route

1.U-NAT:

(1)vim /etc/network/interfaces

auto eth0

iface eth0 inet static

address 192.168.213.5

netmask 255.255.255.0

gateway 192.168.213.2

auto eth1

iface eth1 inet static

address 172.29.23.254

netmask 255.255.255.0

(2)vim /etc/rc.local

route add -net 172.29.23.0 netmask 255.255.255.192 gw 172.29.23.200 dev eth1

route add -net 172.29.23.64 netmask 255.255.255.192 gw 172.29.23.201 dev eth1

iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE

(3)vim /etc/sysctl.conf

#net.ipv4.ip_forward=1

將該行註解取消
搞定後記得重開機或下指令讓修改的設定馬上生效

2.U-Router:

(1)vim /etc/network/interfaces

auto eth0

iface eth0 inet static

address 172.29.23.200

netmask 255.255.255.192

gateway 172.29.23.254


auto eth1

iface eth1 inet static

address 172.29.23.62

netmask 255.255.255.192

(2)vim /etc/sysctl.conf

#net.ipv4.ip_forward=1

將該行註解取消
搞定後記得重開機或下指令讓修改的設定馬上生效

3.U-Client1:

(1)vim /etc/network/interfaces

auto eth0

iface eth0 inet static

address 172.29.23.5

netmask 255.255.255.192

gateway 172.29.23.62

搞定後記得重開機或下指令讓修改的設定馬上生效

4.U-Client2:
說明 由於本機器網段10.1.1.0/24與172.29.23.192/26不同,
所以必須使用Host Route方式來使網路可通

(1)vim /etc/network/interfaces

auto eth0

iface eth0 inet static

address 10.1.1.5

netmask 255.255.255.0


(2)vim /etc/rc.local

route add -host 172.29.23.254 dev eth0

route add default gw 172.29.23.254 dev eth0

(3)U-NAT:加入

vim /etc/rc.local

route add -host 10.1.1.5 dev eth1
搞定收工

2010年10月12日 星期二

Expect 教學

Expect 教程中文版
本文出自: 作者: 葫蘆娃 翻譯 (2001-09-12 10:00:00)
[版權聲明] 
 
Copyright(c) 1999 
 
本教程由*葫蘆娃*翻譯,並做了適當的修改,可以自由的用非商業目的。 
但Redistribution時必須拷貝本[版權聲明]。       

[BUG] 

有不少部分,翻譯的時候不能作到“信,達”。當然了,任何時候都沒有做到“雅”,希望各位諒解。 

[原] 
 
Don Libes: National Institute of Standards and Technology 
libes@cme.nist.gov 

[目錄] 
 
1.摘要 
2.關鍵字 
3.簡介 
4.Expect綜述 
5.callback 
6.passwd 和一致性檢查 
7.rogue 和偽終端 
8.ftp 
9.fsck 
10.多進程控制:作業控制 
11.交互式使用Expect 
12.交互式Expect編程 
13.非交互式程序的控制 
14.Expect的速度 
15.安全方面的考慮 
16.Expect資源 
17.參考書籍 

1.[摘要] 

 現代的Shell對程序提供了最小限度的控制(開始,停止,等等),而把交互的特性留給了用戶。 這意味著有些程序,
你不能非交互的運行,比如說passwd。 有一些程序可以非交互的運行,但在很大程度上喪失了靈活性,比如說fsck。
這表明Unix的工具構造邏輯開始出現問題。Expect恰恰填補了其中的一些裂痕,解決了在Unix環境中長期存在著的一些
問題。 

 Expect使用Tcl作為語言核心。不僅如此,不管程序是交互和還是非交互的,Expect都能運用。這是一個小語言和Unix
的其他工具配合起來產生強大功能的經典例子。 
 
 本部分教程並不是有關Expect的實現,而是關Expect語言本身的使用,這主要也是通過不同的腳本描述例子來體現。
其中的幾個例子還例証了Expect的幾個新特征。 
 
2.[關鍵字] 
 
Expect,交互,POSIX,程序化的對話,Shell,Tcl,Unix; 

3.[簡介] 
 
一個叫做fsck的Unix文件系統檢查程序,可以從Shell裡面用-y或者-n選項來執行。 在手冊[1]裡面,-y選項的定
義是象這樣的。 

“對fsck的所有問題都假定一個“yes”響應;在這樣使用的時候,必須特別的小心,因為它實際上允許程序無條
件的繼續運行,即使是遇到了一些非常嚴重的錯誤” 
 
相比之下,-n選項就安全的多,但它實際上幾乎一點用都沒有。這種接口非常的糟糕,但是卻有許多的程序都是這種
風格。 文件傳輸程序ftp有一個選項可以禁止交互式的提問,以便能從一個腳本裡面運行。但一旦發生了錯誤,它沒有
提供的處理措施。 

Expect是一個控制交互式程序的工具。他解決了fsck的問題,用非交互的方式實現了所有交互式的功能。Expect不是
特別為fsck設計的,它也能進行類似ftp的出錯處理。 

fsck和ftp的問題向我們展示了象sh,csh和別的一些shell提供的用戶接口的局限性。 Shell沒有提供從一個程序讀
和象一個程序寫的功能。這意味著shell可以運行fsck但只能以犧牲一部分fsck的靈活性做代價。有一些程序根本就不能
被執行。比如說,如果沒有一個用戶接口交互式的提供輸入,就沒法運行下去。其他還有象Telnet,crypt,su,rlogin等程
序無法在shell腳本裡面自動執行。還有很多其他的應用程序在設計是也是要求用戶輸入的。 

Expect被設計成專門針和交互式程序的交互。一個Expect程序員可以寫一個腳本來描述程序和用戶的對話。接著Expect
程序可以非交互的運行“交互式”的程序。寫交互式程序的腳本和寫非交互式程序的腳本一樣簡單。Expect還可以用對對
話的一部分進行自動化,因為程序的控制可以在鍵盤和腳本之間進行切換。 

bes[2]裡面有詳細的描述。簡單的說,腳本是用一種解釋性語言寫的。(也有C和C++的Expect庫可供使用,但這超出了本文
的范圍).Expect提供了創建交互式進程和讀寫它們的輸入和輸出的命令。 Expect是由它的一個同名的命令而命名的。 

Expect語言是基Tcl的。Tcl實際上是一個子程序庫,這些子程序庫可以嵌入到程序裡從而提供語言服務。 最終的
語言有點象一個典型的Shell語言。裡面有給變量賦值的set命令,控制程序執行的if,for,continue等命令,還能進行普通
的數學和字符串操作。當然了,還可以用exec來調用Unix程序。所有這些功能,Tcl都有。Tcl在參考書籍 Outerhour[3][4]
裡有詳細的描述。 

Expect是在Tcl基礎上創建起來的,它還提供了一些Tcl所沒有的命令。spawn命令激活一個Unix程序來進行交互式的運行。 
send命令向進程發送字符串。expect命令等待進程的某些字符串。 expect支持正規表達式並能同時等待多個字符串,並對
每一個字符串執行不同的操作。expect還能理解一些特殊情況,如超時和遇到文件尾。 

expect命令和Tcl的case命令的風格很相似。都是用一個字符串去匹配多個字符串。(只要有可能,新的命令總是和已
有的Tcl命令相似,以使得該語言保持工具族的繼承性)。下面關expect的定義是從手冊[5]上摘錄下來的。 

expect patlist1 action1 patlist2 action2..... 

該命令一直等到當前進程的輸出和以上的某一個模式相匹配,或者等    到時間超過一個特定的時間長度,
或者等到遇到了文件的結束為止。 
 
如果最一個action是空的,就可以省略它。 

每一個patlist都由一個模式或者模式的表(lists)組成。如果有一個模式匹配成功,相應的action就被執行。執
行的結果從expect返回。 
被精確匹配的字符串(或者當超時發生時,已經讀取但未進行匹配的字符串)被存貯在變量expect_match裡面。如
果patlist是eof或者timeout,則發生文件結束或者超時時才執行相應的action.一般超時的時值是10秒,但可以用類似
"set timeout 30"之類的命令把超時時值設定為30秒。 
 
下面的一個程序段是從一個有關登錄的腳本裡面摘取的。abort是在腳本的別處定義的過程,而其他的action使用
類似與C語言的Tcl原語。 

expect "*welcome*"        break      
 "*busy*"        {print busy;continue} 
 "*failed*"        abort  
 timeout        abort 

模式是通常的C Shell風格的正規表達式。模式必須匹配當前進程的從上一個expect或者interact開始的所有輸
出(所以統配符*使用的非常)的普遍。但是,一旦輸出超過2000個字節,前面的字符就會被忘記,這可以通過設定
match_max的值來改變。 

expect命令確實體現了expect語言的最好和最壞的性質。特別是,expect命令的靈活性是以經常出現令人迷惑的語法
做代價。除了關鍵字模式(比如說eof,timeout)那些模式表可以包括多個模式。這保証提供了一種方法來區分他們。但是分
開這些表需要額外的掃描,如果沒有恰當的用["]括起來,這有可能會把和當成空白字符。由Tcl提供了兩種字符串引用
的方法:單引和雙引,情況變的更糟。(在Tcl裡面,如果不會出現二義性話,沒有必要使用引號)。在expect的手冊裡面,
還有一個獨立的部分來解釋這種復雜性。幸運的是:有一些很好的例子似乎阻止了這種抱怨。但是,這個復雜性很有可能
在將來的版本中再度出現。為了增強可讀性,在本文中,提供的腳本都假定雙引號是足夠的。 

字符可以使用反斜槓來單獨的引用,反斜槓也被用對語句的延續,如果不加反斜槓的話,語句到一行的結尾處就結
束了。這和Tcl也是一致的。Tcl在發現有開的單引號或者開的雙引號時都會繼續掃描。而且,分號可以用在一行中分割
多個語句。這乍聽起來有點讓人困惑,但是,這是解釋性語言的風格,但是,這確實是Tcl的不太漂亮的部分。 

5.[callback] 

令人非常驚訝的是,一些小的腳本如何的產生一些有用的功能。下面是一個撥電話號碼的腳本。他用來把收費反向,
以便使得長途電話對計算機計費。這個腳本用類似“expect callback.exp 12016442332”來激活。其中,腳本的名字便
是callback.exp,而+1(201)644-2332是要撥的電話號碼。 

#first give the user some time to logout 
exec sleep 4 
spawn tip modem 
expect "*connected*" 
send "ATD [index $argv 1] " 
#modem takes a while to connect 
set timeout 60 
expect "*CONNECT*" 

第一行是注釋,第二行展示了如何調用沒有交互的Unix程序。sleep 4會使程序阻塞4秒,以使得用戶有時間來退出,
因為modem總是會回叫用戶已經使用的電話號碼。 

下面一行使用spawn命令來激活tip程序,以便使得tip的輸出能夠被expect所讀取,使得tip能從send讀輸入。一旦
tip說它已經連接上,modem就會要求去撥打大哥電話號碼。(假定modem都是賀氏兼容的,但是本腳本可以很容易的修改
成能適應別的類型的modem)。不論發生了什,expect都會終止。如果呼叫失敗,expect腳本可以設計成進行重試,但
這裡沒有。如果呼叫成功,getty會在expect退出檢測到DTR,並且向用戶提示loging:。(實用的腳本往往提供更多的
錯誤檢測)。 

這個腳本展示了命令行參數的使用,命令行參數存貯在一個叫做argv的表裡面(這和C語言的風格很象)。在這種情況
下,第一個元素就是電話號碼。方括號使得被括起來的部分當作命令來執行,結果就替換被括起來的部分。這也和
C Shell的風格很象。 

這個腳本和一個大約60K的C語言程序實現的功能相似。 
 

6.[passwd和一致性檢查] 

在前面,我們提到passwd程序在缺乏用戶交互的情況下,不能運行,passwd會忽略I/O重定向,也不能嵌入到管道裡
邊以便能從別的程序或者文件裡讀取輸入。這個程序堅持要求真正的與用戶進行交互。因為安全的原因,passwd被設計
成這樣,但結果導致沒有非交互式的方法來檢驗passwd。這樣一個對系統安全至關重要的程序竟然沒有辦法進行可靠的
檢驗,真實具有諷刺意味。 

passwd以一個用戶名作為參數,交互式的提示輸入密碼。下面的expect腳本以用戶名和密碼作為參數而非交互式的運行。 

spawn oasswd [index $argv 1] 
set password [index $argv 2] 
expect "*password:" 
send "$password " 
expect "*password:" 
send "$password " 
expect eof 

第一行以用戶名做參數啟動passwd程序,為方便起見,第二行把密碼存到一個變量裡面。和shell類似,變量的使用也
不需要提前聲明。 

在第三行,expect搜索模式"*password:",其中*允許匹配任意輸入,所以對避免指定所有細節而言是非常有效的。 
上面的程序裡沒有action,所以expect檢測到該模式就繼續運行。 

一旦接收到提示,下一行就就把密碼送給當前進程。表明回車。(實際上,所有的C的關字符的約定都支持)。上面
的程序中有兩個expect-send序列,因為passwd為了對輸入進行確認,要求進行兩次輸入。在非交互式程序裡面,這是毫無
必要的,但由假定passwd是在和用戶進行交互,所以我們的腳本還是這樣做了。 

最,"expect eof"這一行的作用是在passwd的輸出中搜索文件結束符,這一行語句還展示了關鍵字的匹配。另外一
個關鍵字匹配就是timeout了,timeout被用表示所有匹配的失敗而和一段特定長度的時間相匹配。在這裡eof是非常有必
要的,因為passwd被設計成會檢查它的所有I/O是否都成功了,包括第二次輸入密碼時產生的最一個新行。 

這個腳本已經足夠展示passwd命令的基本交互性。另外一個更加完備的例子回檢查別的一些行為。比如說,下面的這
個腳本就能檢查passwd程序的別的幾個方面。所有的提示都進行了檢查。對垃圾輸入的檢查也進行了適當的處理。進程死
亡,超乎尋常的慢響應,或者別的非預期的行為都進行了處理。 

spawn passwd [index $argv 1] 
expect     eof            {exit 1}      
timeout            {exit 2}     
"*No such user.*"    {exit 3}     
"*New password:"     
send "[index $argv 2 " 
expect     eof            {exit 4}     
timeout            {exit 2}     
"*Password too long*"    {exit 5}     
"*Password too short*"    {exit 5}     
"*Retype ew password:" 
send "[index $argv 3] " 
expect     timeout            {exit 2}     
"*Mismatch*"        {exit 6}     
"*Password unchanged*"    {exit 7}     
" "         
expect    timeout            {exit 2}     
"*"            {exit 6}     
eof 

 
這個腳本退出時用一個數字來表示所發生的情況。0表示passwd程序正常運行,1表示非預期的死亡,2表示鎖定,等等。
使用數字是為了簡單起見。expect返回字符串和返回數字是一樣簡單的,即使是派生程序自身產生的消息也是一樣的。實際
上,典型的做法是把整個交互的過程存到一個文件裡面,只有當程序的運行和預期一樣的時候才把這個文件刪除。否則這個
log被留待以進一步的檢查。 

這個passwd檢查腳本被設計成由別的腳本來驅動。這第二個腳本從一個文件裡面讀取參數和預期的結果。對每一個輸
入參數集,它調用第一個腳本並且把結果和預期的結果相比較。(因為這個任務是非交互的,一個普通的老式shell就可以用
來解釋第二個腳本)。比如說,一個passwd的數據文件很有可能就象下面一樣。 

passwd.exp    3    bogus    -        - 
passwd.exp    0    fred    abledabl    abledabl 
passwd.exp    5    fred    abcdefghijklm    - 
passwd.exp    5    fred    abc        - 
passwd.exp    6    fred    foobar        bar     
passwd.exp    4    fred    ^C        - 

第一個域的名字是要被運行的回歸腳本。第二個域是需要和結果相匹配的退出值。第三個域就是用戶名。第四個域和
第五個域就是提示時應該輸入的密碼。減號僅僅表示那裡有一個域,這個域其實絕對不會用到。在第一個行中,bogus表示
用戶名是非法的,因此passwd會響應說:沒有此用戶。expect在退出時會返回3,3恰好就是第二個域。在最一行中,^C就
是被切實的送給程序來驗証程序是否恰當的退出。 

通過這種方法,expect可以用來檢驗和調試交互式軟件,這恰恰是IEEE的POSIX 1003.2(shell和工具)的一致性檢驗所
要求的。進一步的說明請參考Libes[6]。 

7.[rogue 和偽終端] 

Unix用戶肯定對通過管道來和其他進程相聯系的方式非常的熟悉(比如說:一個shell管道)。expect使用偽終端來和派
生的進程相聯系。偽終端提供了終端語義以便程序認為他們正在和真正的終端進行I/O操作。 

比如說,BSD的探險遊戲rogue在生模式下運行,並假定在連接的另一端是一個可尋址的字符終端。可以用expect編程,
使得通過使用用戶界面可以玩這個遊戲。 

rogue這個探險遊戲首先提供給你一個有各種物理屬性,比如說力量值,的角色。在大部分時間裡,力量值都是16,但
在幾乎每20次裡面就會有一個力量值是18。很多的rogue玩家都知道這一點,但沒有人願意啟動程序20次以獲得一個好的配
置。下面的這個腳本就能達到這個目的。 

for {} {1} {} { 
spawn rogue 
expect "*Str:18*"    break     
 "*Str:16*"     
close 
wait 

interact 

第一行是個for循環,和C語言的控制格式很象。rogue啟動,expect就檢查看力量值是18還是16,如果是16,程序
就通過執行close和wait來退出。這兩個命令的作用分別是關閉和偽終端的連接和等待進程退出。rogue讀到一個文件結束
符就推出,從而循環繼續運行,產生一個新的rogue遊戲來檢查。 

當一個值為18的配置找到,控制就推出循環並跳到最一行腳本。interact把控制轉移給用戶以便他們能夠玩這個
特定的遊戲。 

想象一下這個腳本的運行。你所能真正看到的就是20或者30個初始的配置在不到一秒鐘的時間裡掠過屏幕,最留給
你的就是一個有著很好配置的遊戲。唯一比這更好的方法就是使用調試工具來玩遊戲。 

我們很有必要認識到這樣一點:rogue是一個使用光標的圖形遊戲。expect程序員必須了解到:光標的運動並不一定
以一種直觀的方式在屏幕上體現。幸運的是,在我們這個例子裡,這不是一個問題。將來的對expect的改進可能會包括一
個內嵌的能支持字符圖形區域的終端模擬器。 

8.[ftp] 

我們使用expect寫第一個腳本並沒有打印出"Hello,World"。實際上,它實現了一些更有用的功能。它能通過非交互
的方式來運行ftp。ftp是用來在支持TCP/IP的網絡上進行文件傳輸的程序。除了一些簡單的功能,一般的實現都要求用戶
的參與。 

下面這個腳本從一個主機上使用匿名ftp取下一個文件來。其中,主機名是第一個參數。文件名是第二個參數。 

spawn    ftp    [index $argv 1] 
expect "*Name*" 
send     "anonymous " 
expect "*Password:*" 
send [exec whoami] 
expect "*ok*ftp>*" 
send "get [index $argv 2] " 
expect "*ftp>*" 

上面這個程序被設計成在台進行ftp。雖然他們在底層使用和expect類似的機制,但他們的可編程能力留待改進。
因為expect提供了高級語言,你可以對它進行修改來滿足你的特定需求。比如說,你可以加上以下功能: 

:堅持--如果連接或者傳輸失敗,你就可以每分鐘或者每小時,甚 
至可以根據其他因素,比如說用戶的負載,來進行不定期的 
重試。 
:通知--傳輸時可以通過mail,write或者其他程序來通知你,甚至 
可以通知失敗。 
:初始化-每一個用戶都可以有自己的用高級語言編寫的初始化文件 
(比如說,.ftprc)。這和C shell對.cshrc的使用很類似。 

expect還可以執行其他的更復雜的任務。比如說,他可以使用McGill大學的Archie系統。Archie是一個匿名的
Telnet服務,它提供對描述Internet上可通過匿名ftp獲取的文件的數據庫的訪問。通過使用這個服務,腳本可以詢問
Archie某個特定的文件的位置,並把它從ftp服務器上取下來。這個功能的實現只要求在上面那個腳本中加上幾行就可
以。 

現在還沒有什已知的台-ftp能夠實現上面的幾項功能,能不要說所有的功能了。在expect裡面,它的實現卻
是非常的簡單。“堅持”的實現只要求在expect腳本裡面加上一個循環。“通知”的實現只要執行mail和write就可以
了。“初始化文件”的實現可以使用一個命令,source .ftprc,就可以了,在.ftprc裡面可以有任何的expect命令。 

雖然這些特征可以通過在已有的程序裡面加上鉤子函數就可以,但這也不能保証每一個人的要求都能得到滿足。
唯一能夠提供保証的方法就是提供一種通用的語言。一個很好的解決方法就是把Tcl自身融入到ftp和其他的程序中間去。
實際上,這本來就是Tcl的初衷。在還沒有這樣做之前,expect提供了一個能實現大部分功能但又不需要任何重寫的方案。 

9.[fsck] 

fsck是另外一個缺乏足夠的用戶接口的例子。fsck幾乎沒有提供什方法來預先的回答一些問題。你能做的就是給
所有的問題都回答"yes"或者都回答"no"。 

下面的程序段展示了一個腳本如何的使的自動的對某些問題回答"yes",而對某些問題回答"no"。下面的這個腳本一
開始先派生fsck進程,然對其中兩種類型的問題回答"yes",而對其他的問題回答"no"。 

for {} {1} {} { 
expect 
eof        break         
"*UNREF FILE*CLEAR?"    {send "r "}     
"*BAD INODE*FIX?"    {send "y "}     
"*?"            {send "n "}     


在下面這個版本裡面,兩個問題的回答是不同的。而且,如果腳本遇到了什它不能理解的東西,就會執行
interact命令把控制交給用戶。用戶的擊鍵直接交給fsck處理。當執行完,用戶可以通過按"+"鍵來退出或者

把控制交還給expect。如果控制是交還給腳本了,腳本就會自動的控制進程的剩余部分的運行。 

for {} {1} {}{ 
expect              
eof        break         
"*UNREF FILE*CLEAR?"    {send "y "}     
"*BAD INODE*FIX?"    {send "y "}     
"*?"            {interact +}     


如果沒有expect,fsck只有在犧牲一定功能的情況下才可以非交互式的運行。fsck幾乎是不可編程的,但它
卻是系統管理的最重要的工具。許多別的工具的用戶接口也一樣的不足。實際上,正是其中的一些程序的不足導
致了expect的誕生。 

10.[控制多個進程:作業控制] 


 expect的作業控制概念精巧的避免了通常的實現困難。其中包括了兩個問題:一個是expect如何處理經典的
作業控制,即當你在終端上按下^Z鍵時expect如何處理;另外一個就是expect是如何處理多進程的。 

 對第一個問題的處理是:忽略它。expect對經典的作業控制一無所知。比如說,你派生了一個程序並且發送
一個^Z給它,它就會停下來(這是偽終端的完美之處)而expect就會永遠的等下去。 

 但是,實際上,這根本就不成一個問題。對一個expect腳本,沒有必要向進程發送^Z。也就是說,沒有必
要停下一個進程來。expect僅僅是忽略了一個進程,而把自己的注意力轉移到其他的地方。這就是expect的作業
控制思想,這個思想也一直工作的很好。 

從用戶的角度來看是象這樣的:當一個進程通過spawn命令啟動時,變量spawn_id就被設置成某進程的描述
符。由spawn_id描述的進程就被認為是當前進程。(這個描述符恰恰就是偽終端文件的描述符,雖然用戶把它當
作一個不透明的物體)。expect和send命令僅僅和當前進程進行交互。所以,切換一個作業所需要做的僅僅是把
該進程的描述符賦給spawn_id。 

這兒有一個例子向我們展示了如何通過作業控制來使兩個chess進程進行交互。在派生完兩個進程之,一
個進程被通知先動一步。在下面的循環裡面,每一步動作都送給另外一個進程。其中,read_move和write_move
兩個過程留給讀者來實現。(實際上,它們的實現非常的容易,但是,由太長了所以沒有包含在這裡)。 

spawn chess            ;# start player one 
set id1    $spawn_id 
expect "Chess " 
send "first "            ;# force it to go first 
read_move 

spawn chess            ;# start player two 
set id2    $spawn_id 
expect "Chess " 
 
for {} {1} {}{ 
send_move 
read_move 
set spawn_id    $id1 
 
send_move 
read_move 
set spawn_id    $id2 


 有一些應用程序和chess程序不太一樣,在chess程序裡,的兩個玩家輪流動。下面這個腳本實現了一個冒
充程序。它能夠控制一個終端以便用戶能夠登錄和正常的工作。但是,一旦系統提示輸入密碼或者輸入用戶名的
時候,expect就開始把擊鍵記下來,一直到用戶按下回車鍵。這有效的收集了用戶的密碼和用戶名,還避免了普
通的冒充程序的"Incorrect password-tryagain"。而且,如果用戶連接到另外一個主機上,那些額外的登錄也
會被記錄下來。 

spawn tip /dev/tty17        ;# open connection to 
set tty $spawn_id        ;# tty to be spoofed 

spawn login 
set login $spawn_id 

log_user 0 
 
for {} {1} {} { 
set ready [select $tty $login] 
 
case $login in $ready { 
set spawn_id $login 
expect          
{"*password*" "*login*"}{ 
 send_user $expect_match 
 set log 1 
 }     
 "*"        ;# ignore everything else 
set spawn_id    $tty; 
send $expect_match 

case $tty in $ready { 
set spawn_id    $tty 
expect "* *"{ 
if $log { 
 send_user $expect_match 
 set log 0 

 }     
"*" { 
send_user $expect_match 
 } 
set spawn_id     $login; 
send $expect_match 


 

 這個腳本是這樣工作的。首先連接到一個login進程和終端。缺省的,所有的對話都記錄到標準輸出上
(通過send_user)。因為我們對此並不感興趣,所以,我們通過命令"log_user 0"來禁止這個功能。(有很多
的命令來控制可以看見或者可以記錄的東西)。 

 在循環裡面,select等待終端或者login進程上的動作,並且返回一個等待輸入的spawn_id表。如果在
表裡面找到了一個值的話,case就執行一個action。比如說,如果字符串"login"出現在login進程的輸出中,
提示就會被記錄到標準輸出上,並且有一個標志被設置以便通知腳本開始記錄用戶的擊鍵,直至用戶按下了回
車鍵。無論收到什,都會回顯到終端上,一個相應的action會在腳本的終端那一部分執行。 

 這些例子顯示了expect的作業控制方式。通過把自己插入到對話裡面,expect可以在進程之間創建復雜
的I/O流。可以創建多扇出,復用扇入的,動態的數據相關的進程圖。 

 相比之下,shell使得它自己一次一行的讀取一個文件顯的很困難。shell強迫用戶按下控制鍵(比如,
^C,^Z)和關鍵字(比如fg和bg)來實現作業的切換。這些都無法從腳本裡面利用。相似的是:以非交互方式運
行的shell並不處理“歷史記錄”和其他一些僅僅為交互式使用設計的特征。這也出現了和前面哪個passwd程
序的相似問題。相似的,也無法編寫能夠回歸的測試shell的某些動作的shell腳本。結果導致shell的這些方
面無法進行徹底的測試。 

 如果使用expect的話,可以使用它的交互式的作業控制來驅動shell。一個派生的shell認為它是在交互
的運行著,所以會正常的處理作業控制。它不僅能夠解決檢驗處理作業控制的shell和其他一些程序的問題。
還能夠在必要的時候,讓shell代替expect來處理作業。可以支持使用shell風格的作業控制來支持進程的運行。
這意味著:首先派生一個shell,然把命令送給shell來啟動進程。如果進程被掛起,比如說,發送了一個^Z,
進程就會停下來,並把控制返回給shell。對expect而言,它還在處理同一個進程(原來那個shell)。 

expect的解決方法不僅具有很大的靈活性,它還避免了重復已經存在shell中的作業控制軟件。通過使
用shell,由你可以選擇你想派生的shell,所以你可以根據需要獲得作業控制權。而且,一旦你需要(比如
說檢驗的時候),你就可以驅動一個shell來讓這個shell以為它正在交互式的運行。這一點對在檢測到它們
是否在交互式的運行之會改變輸出的緩沖的程序來說也是很重要的。 

為了進一步的控制,在interact執行期間,expect把控制終端(是啟動expect的那個終端,而不是偽終端)
設置成生模式以便字符能夠正確的傳送給派生的進程。當expect在沒有執行interact的時候,終端處熟模式
下,這時候作業控制就可以作用expect本身。 

11.[交互式的使用expect] 

在前面,我們提到可以通過interact命令來交互式的使用腳本。基本上來說,interact命令提供了對對話
的自由訪問,但我們需要一些更精細的控制。這一點,我們也可以使用expect來達到,因為expect從標準輸入
中讀取輸入和從進程中讀取輸入一樣的簡單。 但是,我們要使用expect_user和send_user來進行標準I/O,同
時不改變spawn_id。 

下面的這個腳本在一定的時間內從標準輸入裡面讀取一行。這個腳本叫做timed_read,可以從csh裡面調用,
比如說,set answer="timed_read 30"就能調用它。 

#!/usr/local/bin/expect -f 
set timeout [index $argv 1] 
expect_user "* " 
send_user $expect_match 

 第三行從用戶那裡接收任何以新行符結束的任何一行。最一行把它返回給標準輸出。如果在特定的時間
內沒有得到任何鍵入,則返回也為空。 

 第一行支持"#!"的系統直接的啟動腳本。(如果把腳本的屬性加上可執行屬性則不要在腳本前面加上expect)。
當然了腳本總是可以顯式的用"expect scripot"來啟動。在-c面的選項在任何腳本語句執行前就被執行。比如
說,不要修改腳本本身,僅僅在命令行上加上-c "trace...",該腳本可以加上trace功能了(省略號表示trace的
選項)。 

 在命令行裡實際上可以加上多個命令,只要中間以";"分開就可以了。比如說,下面這個命令行: 

expect -c "set timeout 20;spawn foo;expect" 

一旦你把超時時限設置好而且程序啟動之,expect就開始等待文件結束符或者20秒的超時時限。 如果遇
到了文件結束符(EOF),該程序就會停下來,然expect返回。如果是遇到了超時的情況,expect就返回。在這兩
中情況裡面,都隱式的殺死了當前進程。 

 如果我們不使用expect而來實現以上兩個例子的功能的話,我們還是可以學習到很多的東西的。在這兩中情
況裡面,通常的解決方案都是fork另一個睡眠的子進程並且用signal通知原來的shell。如果這個過程或者讀先發
生的話,shell就會殺司那個睡眠的進程。 傳遞pid和防止台進程產生啟動信息是一個讓除了高手級shell程序員
之外的人頭痛的事情。提供一個通用的方法來象這樣啟動多個進程會使shell腳本非常的復雜。 所以幾乎可以肯定
的是,程序員一般都用一個專門C程序來解決這樣一個問題。 

expect_user,send_user,send_error(向標準錯誤終端輸出)在比較長的,用來把從進程來的復雜交互翻譯成
簡單交互的expect腳本裡面使用的比較頻繁。在參考[7]裡面,Libs描述怎樣用腳本來安全的包裹(wrap)adb,怎樣
把系統管理員從需要掌握adb的細節裡面解脫出來,同時大大的降低了由錯誤的擊鍵而導致的系統崩潰。 

一個簡單的例子能夠讓ftp自動的從一個私人的帳號裡面取文件。在這種情況裡,要求提供密碼。 即使文件
的訪問是受限的,你也應該避免把密碼以明文的方式存儲在文件裡面。把密碼作為腳本運行時的參數也是不合適的,
因為用ps命令能看到它們。有一個解決的方法就是在腳本運行的開始調用expect_user來讓用戶輸入以可能使用的
密碼。這個密碼必須只能讓這個腳本知道,即使你是每個小時都要重試ftp。 

即使信息是立即輸入進去的,這個技巧也是非常有用。比如說,你可以寫一個腳本,把你每一個主機上不
同的帳號上的密碼都改掉,不管他們使用的是不是同一個密碼數據庫。如果你要手工達到這樣一個功能的話,你必
須Telnet到每一個主機上,並且手工輸入新的密碼。而使用expect,你可以只輸入密碼一次而讓腳本來做其它的事
情。 

expect_user和interact也可以在一個腳本裡面混合的使用。考慮一下在調試一個程序的循環時,經過好多
步之才失敗的情況。一個expect腳本可以驅動哪個調試器,設置好斷點,執行該程序循環的若幹步,然將控制
返回給鍵盤。它也可以在返回控制之前,在循環體和條件測試之間來回的切換。 

6.[passwd和一致性檢查] 

在前面,我們提到passwd程序在缺乏用戶交互的情況下,不能運行,passwd 
會忽略I/O重定向,也不能嵌入到管道裡邊以便能從別的程序或者文件裡讀取輸 
入。這個程序堅持要求真正的與用戶進行交互。因為安全的原因,passwd被設計 
成這樣,但結果導致沒有非交互式的方法來檢驗passwd。這樣一個對系統安全 
至關重要的程序竟然沒有辦法進行可靠的檢驗,真實具有諷刺意味。 

passwd以一個用戶名作為參數,交互式的提示輸入密碼。下面的expect腳 
本以用戶名和密碼作為參數而非交互式的運行。 

spawn oasswd [index $argv 1] 
set password [index $argv 2] 
expect "*password:" 
send "$password " 
expect "*password:" 
send "$password " 
expect eof 

第一行以用戶名做參數啟動passwd程序,為方便起見,第二行把密碼存到 
一個變量裡面。和shell類似,變量的使用也不需要提前聲明。 

在第三行,expect搜索模式"*password:",其中*允許匹配任意輸入,所 
以對避免指定所有細節而言是非常有效的。 上面的程序裡沒有action,所以 
expect檢測到該模式就繼續運行。 

一旦接收到提示,下一行就就把密碼送給當前進程。表明回車。(實 
際上,所有的C的關字符的約定都支持)。上面的程序中有兩個expect-send 
序列,因為passwd為了對輸入進行確認,要求進行兩次輸入。在非交互式程序 
裡面,這是毫無必要的,但由假定passwd是在和用戶進行交互,所以我們的 
腳本還是這樣做了。 

最,"expect eof"這一行的作用是在passwd的輸出中搜索文件結束符, 
這一行語句還展示了關鍵字的匹配。另外一個關鍵字匹配就是timeout了, 
timeout被用表示所有匹配的失敗而和一段特定長度的時間相匹配。在這裡 
eof是非常有必要的,因為passwd被設計成會檢查它的所有I/O是否都成功了, 
包括第二次輸入密碼時產生的最一個新行。 

這個腳本已經足夠展示passwd命令的基本交互性。另外一個更加完備的例 
子回檢查別的一些行為。比如說,下面的這個腳本就能檢查passwd程序的別的 
幾個方面。所有的提示都進行了檢查。對垃圾輸入的檢查也進行了適當的處 
理。進程死亡,超乎尋常的慢響應,或者別的非預期的行為都進行了處理。 


spawn passwd [index $argv 1] 
expect     eof            {exit 1}      
timeout            {exit 2}     
"*No such user.*"    {exit 3}     
"*New password:"     
send "[index $argv 2 " 
expect     eof            {exit 4}     
timeout            {exit 2}     
"*Password too long*"    {exit 5}     
"*Password too short*"    {exit 5}     
"*Retype ew password:" 
send "[index $argv 3] " 
expect     timeout            {exit 2}     
"*Mismatch*"        {exit 6}     
"*Password unchanged*"    {exit 7}     
" "         
expect    timeout            {exit 2}     
"*"            {exit 6}     
eof 

 
這個腳本退出時用一個數字來表示所發生的情況。0表示passwd程序正常 
運行,1表示非預期的死亡,2表示鎖定,等等。使用數字是為了簡單起見。 
expect返回字符串和返回數字是一樣簡單的,即使是派生程序自身產生的消息 
也是一樣的。實際上,典型的做法是把整個交互的過程存到一個文件裡面,只 
有當程序的運行和預期一樣的時候才把這個文件刪除。否則這個log被留待以 
進一步的檢查。 

這個passwd檢查腳本被設計成由別的腳本來驅動。這第二個腳本從一個文 
件裡面讀取參數和預期的結果。對每一個輸入參數集,它調用第一個腳本並 
且把結果和預期的結果相比較。(因為這個任務是非交互的,一個普通的老式 
shell就可以用來解釋第二個腳本)。比如說,一個passwd的數據文件很有可能 
就象下面一樣。 

passwd.exp    3    bogus    -        - 
passwd.exp    0    fred    abledabl    abledabl 
passwd.exp    5    fred    abcdefghijklm    - 
passwd.exp    5    fred    abc        - 
passwd.exp    6    fred    foobar        bar     
passwd.exp    4    fred    ^C        - 

第一個域的名字是要被運行的回歸腳本。第二個域是需要和結果相匹配的 
退出值。第三個域就是用戶名。第四個域和第五個域就是提示時應該輸入的密 
碼。減號僅僅表示那裡有一個域,這個域其實絕對不會用到。在第一個行中 
,bogus表示用戶名是非法的,因此passwd會響應說:沒有此用戶。expect在 
退出時會返回3,3恰好就是第二個域。在最一行中,^C就是被切實的送給程 
序來驗証程序是否恰當的退出。 

通過這種方法,expect可以用來檢驗和調試交互式軟件,這恰恰是IEEE的 
POSIX 1003.2(shell和工具)的一致性檢驗所要求的。進一步的說明請參考 
Libes[6]。 

7.[rogue 和偽終端] 

Unix用戶肯定對通過管道來和其他進程相聯系的方式非常的熟悉(比如說: 
一個shell管道)。expect使用偽終端來和派生的進程相聯系。偽終端提供了終 
端語義以便程序認為他們正在和真正的終端進行I/O操作。 

比如說,BSD的探險遊戲rogue在生模式下運行,並假定在連接的另一端是 
一個可尋址的字符終端。可以用expect編程,使得通過使用用戶界面可以玩這 
個遊戲。 

rogue這個探險遊戲首先提供給你一個有各種物理屬性,比如說力量值,的 
角色。在大部分時間裡,力量值都是16,但在幾乎每20次裡面就會有一個力量 
值是18。很多的rogue玩家都知道這一點,但沒有人願意啟動程序20次以獲得一 
個好的配置。下面的這個腳本就能達到這個目的。 

for {} {1} {} { 
spawn rogue 
expect "*Str:18*"    break     
 "*Str:16*"     
close 
wait 

interact 

第一行是個for循環,和C語言的控制格式很象。rogue啟動,expect就 
檢查看力量值是18還是16,如果是16,程序就通過執行close和wait來退出。 
這兩個命令的作用分別是關閉和偽終端的連接和等待進程退出。rogue讀到一 
個文件結束符就推出,從而循環繼續運行,產生一個新的rogue遊戲來檢查。 

當一個值為18的配置找到,控制就推出循環並跳到最一行腳本。 
interact把控制轉移給用戶以便他們能夠玩這個特定的遊戲。 

想象一下這個腳本的運行。你所能真正看到的就是20或者30個初始的配置 
在不到一秒鐘的時間裡掠過屏幕,最留給你的就是一個有著很好配置的遊戲 
。唯一比這更好的方法就是使用調試工具來玩遊戲。 

我們很有必要認識到這樣一點:rogue是一個使用光標的圖形遊戲。 
expect程序員必須了解到:光標的運動並不一定以一種直觀的方式在屏幕上體 
現。幸運的是,在我們這個例子裡,這不是一個問題。將來的對expect的改 
進可能會包括一個內嵌的能支持字符圖形區域的終端模擬器。 

8.[ftp] 

我們使用expect寫第一個腳本並沒有打印出"Hello,World"。實際上,它 
實現了一些更有用的功能。它能通過非交互的方式來運行ftp。ftp是用來在支 
持TCP/IP的網絡上進行文件傳輸的程序。除了一些簡單的功能,一般的實現都 
要求用戶的參與。 

下面這個腳本從一個主機上使用匿名ftp取下一個文件來。其中,主機名 
是第一個參數。文件名是第二個參數。 

spawn    ftp    [index $argv 1] 
expect "*Name*" 
send     "anonymous " 
expect "*Password:*" 
send [exec whoami] 
expect "*ok*ftp>*" 
send "get [index $argv 2] " 
expect "*ftp>*" 

上面這個程序被設計成在台進行ftp。雖然他們在底層使用和expect類 
似的機制,但他們的可編程能力留待改進。因為expect提供了高級語言,你可 
以對它進行修改來滿足你的特定需求。比如說,你可以加上以下功能: 

:堅持--如果連接或者傳輸失敗,你就可以每分鐘或者每小時,甚 
至可以根據其他因素,比如說用戶的負載,來進行不定期的 
重試。 
:通知--傳輸時可以通過mail,write或者其他程序來通知你,甚至 
可以通知失敗。 
:初始化-每一個用戶都可以有自己的用高級語言編寫的初始化文件 
(比如說,.ftprc)。這和C shell對.cshrc的使用很類似。 

expect還可以執行其他的更復雜的任務。比如說,他可以使用McGill大學 
的Archie系統。Archie是一個匿名的Telnet服務,它提供對描述Internet上可 
通過匿名ftp獲取的文件的數據庫的訪問。通過使用這個服務,腳本可以詢問 
Archie某個特定的文件的位置,並把它從ftp服務器上取下來。這個功能的實 
現只要求在上面那個腳本中加上幾行就可以。 

現在還沒有什已知的台-ftp能夠實現上面的幾項功能,能不要說所有 
的功能了。在expect裡面,它的實現卻是非常的簡單。“堅持”的實現只要求 
在expect腳本裡面加上一個循環。“通知”的實現只要執行mail和write就可以 
了。“初始化文件”的實現可以使用一個命令,source .ftprc,就可以了, 
在.ftprc裡面可以有任何的expect命令。 

雖然這些特征可以通過在已有的程序裡面加上鉤子函數就可以,但這也不 
能保証每一個人的要求都能得到滿足。唯一能夠提供保証的方法就是提供一種 
通用的語言。一個很好的解決方法就是把Tcl自身融入到ftp和其他的程序中間 
去。實際上,這本來就是Tcl的初衷。在還沒有這樣做之前,expect提供了一 
個能實現大部分功能但又不需要任何重寫的方案。 

9.[fsck] 

fsck是另外一個缺乏足夠的用戶接口的例子。fsck幾乎沒有提供什方法 
來預先的回答一些問題。你能做的就是給所有的問題都回答"yes"或者都回答 
"no"。 

下面的程序段展示了一個腳本如何的使的自動的對某些問題回答"yes", 
而對某些問題回答"no"。下面的這個腳本一開始先派生fsck進程,然對其 
中兩種類型的問題回答"yes",而對其他的問題回答"no"。 

for {} {1} {} { 
expect 
eof        break         
"*UNREF FILE*CLEAR?"    {send "r "}     
"*BAD INODE*FIX?"    {send "y "}     
"*?"            {send "n "}     


在下面這個版本裡面,兩個問題的回答是不同的。而且,如果腳本遇到 
了什它不能理解的東西,就會執行interact命令把控制交給用戶。用戶的 
擊鍵直接交給fsck處理。當執行完,用戶可以通過按"+"鍵來退出或者把 
控制交還給expect。如果控制是交還給腳本了,腳本就會自動的控制進程的 
剩余部分的運行。 

for {} {1} {}{ 
expect              
eof        break         
"*UNREF FILE*CLEAR?"    {send "y "}     
"*BAD INODE*FIX?"    {send "y "}     
"*?"            {interact +}     


如果沒有expect,fsck只有在犧牲一定功能的情況下才可以非交互式的 
運行。fsck幾乎是不可編程的,但它卻是系統管理的最重要的工具。許多別 
的工具的用戶接口也一樣的不足。實際上,正是其中的一些程序的不足導致 
了expect的誕生。 

10.[控制多個進程:作業控制] 


 expect的作業控制概念精巧的避免了通常的實現困難。其中包括了兩個問 
題:一個是expect如何處理經典的作業控制,即當你在終端上按下^Z鍵時 
expect如何處理;另外一個就是expect是如何處理多進程的。 

 對第一個問題的處理是:忽略它。expect對經典的作業控制一無所知。比 
如說,你派生了一個程序並且發送一個^Z給它,它就會停下來(這是偽終端的 
完美之處)而expect就會永遠的等下去。 

 但是,實際上,這根本就不成一個問題。對一個expect腳本,沒有必要 
向進程發送^Z。也就是說,沒有必要停下一個進程來。expect僅僅是忽略了 
一個進程,而把自己的注意力轉移到其他的地方。這就是expect的作業控制 
思想,這個思想也一直工作的很好。 

從用戶的角度來看是象這樣的:當一個進程通過spawn命令啟動時,變量 
spawn_id就被設置成某進程的描述符。由spawn_id描述的進程就被認為是當 
前進程。(這個描述符恰恰就是偽終端文件的描述符,雖然用戶把它當作一個 
不透明的物體)。expect和send命令僅僅和當前進程進行交互。所以,切換一 
個作業所需要做的僅僅是把該進程的描述符賦給spawn_id。 

這兒有一個例子向我們展示了如何通過作業控制來使兩個chess進程進行 
交互。在派生完兩個進程之,一個進程被通知先動一步。在下面的循環裡 
面,每一步動作都送給另外一個進程。其中,read_move和write_move兩個過 
程留給讀者來實現。(實際上,它們的實現非常的容易,但是,由太長了所 
以沒有包含在這裡)。 

spawn chess            ;# start player one 
set id1    $spawn_id 
expect "Chess " 
send "first "            ;# force it to go first 
read_move 

spawn chess            ;# start player two 
set id2    $spawn_id 
expect "Chess " 
 
for {} {1} {}{ 
send_move 
read_move 
set spawn_id    $id1 
 
send_move 
read_move 
set spawn_id    $id2 


 有一些應用程序和chess程序不太一樣,在chess程序裡,的兩個玩家 
輪流動。下面這個腳本實現了一個冒充程序。它能夠控制一個終端以便用戶 
能夠登錄和正常的工作。但是,一旦系統提示輸入密碼或者輸入用戶名的時 
候,expect就開始把擊鍵記下來,一直到用戶按下回車鍵。這有效的收集了 
用戶的密碼和用戶名,還避免了普通的冒充程序的"Incorrect password-try 
again"。而且,如果用戶連接到另外一個主機上,那些額外的登錄也會被 
記錄下來。 

spawn tip /dev/tty17        ;# open connection to 
set tty $spawn_id        ;# tty to be spoofed 

spawn login 
set login $spawn_id 

log_user 0 
 
for {} {1} {} { 
set ready [select $tty $login] 
 
case $login in $ready { 
set spawn_id $login 
expect          
{"*password*" "*login*"}{ 
 send_user $expect_match 
 set log 1 
 }     
 "*"        ;# ignore everything else 
set spawn_id    $tty; 
send $expect_match 

case $tty in $ready { 
set spawn_id    $tty 
expect "* *"{ 
if $log { 
 send_user $expect_match 
 set log 0 

 }     
"*" { 
send_user $expect_match 
 } 
set spawn_id     $login; 
send $expect_match 


 

 這個腳本是這樣工作的。首先連接到一個login進程和終端。缺省的, 
所有的對話都記錄到標準輸出上(通過send_user)。因為我們對此並不感興趣, 
所以,我們通過命令"log_user 0"來禁止這個功能。(有很多的命令來控制 
可以看見或者可以記錄的東西)。 

 在循環裡面,select等待終端或者login進程上的動作,並且返回一個 
等待輸入的spawn_id表。如果在表裡面找到了一個值的話,case就執行一個 
action。比如說,如果字符串"login"出現在login進程的輸出中,提示就會 
被記錄到標準輸出上,並且有一個標志被設置以便通知腳本開始記錄用戶的 
擊鍵,直至用戶按下了回車鍵。無論收到什,都會回顯到終端上,一個相 
應的action會在腳本的終端那一部分執行。 

 這些例子顯示了expect的作業控制方式。通過把自己插入到對話裡面, 
expect可以在進程之間創建復雜的I/O流。可以創建多扇出,復用扇入的, 
動態的數據相關的進程圖。 

 相比之下,shell使得它自己一次一行的讀取一個文件顯的很困難。 
shell強迫用戶按下控制鍵(比如,^C,^Z)和關鍵字(比如fg和bg)來實現作業的 
切換。這些都無法從腳本裡面利用。相似的是:以非交互方式運行的shell並 
不處理“歷史記錄”和其他一些僅僅為交互式使用設計的特征。這也出現了和 
前面哪個passwd程序的相似問題。相似的,也無法編寫能夠回歸的測試shell 
的某些動作的shell腳本。結果導致shell的這些方面無法進行徹底的測試。 

 如果使用expect的話,可以使用它的交互式的作業控制來驅動shell。一 
個派生的shell認為它是在交互的運行著,所以會正常的處理作業控制。它不 
僅能夠解決檢驗處理作業控制的shell和其他一些程序的問題。還能夠在必要 
的時候,讓shell代替expect來處理作業。可以支持使用shell風格的作業控 
制來支持進程的運行。這意味著:首先派生一個shell,然把命令送給shell 
來啟動進程。如果進程被掛起,比如說,發送了一個^Z,進程就會停下來,並 
把控制返回給shell。對expect而言,它還在處理同一個進程(原來那個 
shell)。 

expect的解決方法不僅具有很大的靈活性,它還避免了重復已經存在 
shell中的作業控制軟件。通過使用shell,由你可以選擇你想派生的shell, 
所以你可以根據需要獲得作業控制權。而且,一旦你需要(比如說檢驗的時 
候),你就可以驅動一個shell來讓這個shell以為它正在交互式的運行。這一 
點對在檢測到它們是否在交互式的運行之會改變輸出的緩沖的程序來說也 
是很重要的。 

為了進一步的控制,在interact執行期間,expect把控制終端(是啟動 
expect的那個終端,而不是偽終端)設置成生模式以便字符能夠正確的傳送給 
派生的進程。當expect在沒有執行interact的時候,終端處熟模式下,這時 
候作業控制就可以作用expect本身。 

11.[交互式的使用expect] 

在前面,我們提到可以通過interact命令來交互式的使用腳本。基本上 
來說,interact命令提供了對對話的自由訪問,但我們需要一些更精細的控 
制。這一點,我們也可以使用expect來達到,因為expect從標準輸入中讀取 
輸入和從進程中讀取輸入一樣的簡單。 但是,我們要使用expect_user和 
send_user來進行標準I/O,同時不改變spawn_id。 

下面的這個腳本在一定的時間內從標準輸入裡面讀取一行。這個腳本叫 
做timed_read,可以從csh裡面調用,比如說,set answer="timed_read 30" 
就能調用它。 

#!/usr/local/bin/expect -f 
set timeout [index $argv 1] 
expect_user "* " 
send_user $expect_match 

 第三行從用戶那裡接收任何以新行符結束的任何一行。最一行把它 
返回給標準輸出。如果在特定的時間內沒有得到任何鍵入,則返回也為空。 

 第一行支持"#!"的系統直接的啟動腳本。(如果把腳本的屬性加上可執 
行屬性則不要在腳本前面加上expect)。當然了腳本總是可以顯式的用 
"expect scripot"來啟動。在-c面的選項在任何腳本語句執行前就被執行。 
比如說,不要修改腳本本身,僅僅在命令行上加上-c "trace...",該腳本可 
以加上trace功能了(省略號表示trace的選項)。 

 在命令行裡實際上可以加上多個命令,只要中間以";"分開就可以了。 
比如說,下面這個命令行: 

expect -c "set timeout 20;spawn foo;expect" 

一旦你把超時時限設置好而且程序啟動之,expect就開始等待文件 
結束符或者20秒的超時時限。 如果遇到了文件結束符(EOF),該程序就會停 
下來,然expect返回。如果是遇到了超時的情況,expect就返回。在這兩 
中情況裡面,都隱式的殺死了當前進程。 

 如果我們不使用expect而來實現以上兩個例子的功能的話,我們還是可 
以學習到很多的東西的。在這兩中情況裡面,通常的解決方案都是fork另一個 
睡眠的子進程並且用signal通知原來的shell。如果這個過程或者讀先發生的 
話,shell就會殺司那個睡眠的進程。 傳遞pid和防止台進程產生啟動信息 
是一個讓除了高手級shell程序員之外的人頭痛的事情。提供一個通用的方法 
來象這樣啟動多個進程會使shell腳本非常的復雜。 所以幾乎可以肯定的是, 
程序員一般都用一個專門C程序來解決這樣一個問題。 

expect_user,send_user,send_error(向標準錯誤終端輸出)在比較長 
的,用來把從進程來的復雜交互翻譯成簡單交互的expect腳本裡面使用的比較 
頻繁。在參考[7]裡面,Libs描述怎樣用腳本來安全的包裹(wrap)adb,怎樣 
把系統管理員從需要掌握adb的細節裡面解脫出來,同時大大的降低了由錯 
誤的擊鍵而導致的系統崩潰。 

一個簡單的例子能夠讓ftp自動的從一個私人的帳號裡面取文件。在這 
種情況裡,要求提供密碼。 即使文件的訪問是受限的,你也應該避免把密碼 
以明文的方式存儲在文件裡面。把密碼作為腳本運行時的參數也是不合適的, 
因為用ps命令能看到它們。有一個解決的方法就是在腳本運行的開始調用 
expect_user來讓用戶輸入以可能使用的密碼。這個密碼必須只能讓這個腳 
本知道,即使你是每個小時都要重試ftp。 

即使信息是立即輸入進去的,這個技巧也是非常有用。比如說,你可 
以寫一個腳本,把你每一個主機上不同的帳號上的密碼都改掉,不管他們使用 
的是不是同一個密碼數據庫。如果你要手工達到這樣一個功能的話,你必須 
Telnet到每一個主機上,並且手工輸入新的密碼。而使用expect,你可以只輸 
入密碼一次而讓腳本來做其它的事情。 

expect_user和interact也可以在一個腳本裡面混合的使用。考慮一下 
在調試一個程序的循環時,經過好多步之才失敗的情況。一個expect腳本 
可以驅動哪個調試器,設置好斷點,執行該程序循環的若幹步,然將控制 
返回給鍵盤。它也可以在返回控制之前,在循環體和條件測試之間來回的切 
換。