第十六章 命令行腳本
===================
如果能把我們想做的東西寫到一個文件或腳本中,并且像執行其他操作系統命令一樣執行的話通常會非常方便。一些重量級的程序通常以腳本的形式提供接口,用戶可以經常編寫他們自己的腳本或修改已有的腳本來滿足特定的需求。毫無疑問大部分的編程任務都以腳本的形式來執行。對于很多用戶而言,這是他們唯一會做的編程了。
Unix或DOS等操作系統(以及Windows系統提供的命令行接口)都提供了腳本的機制。但是這些腳本語言都相當的不成熟。通常一個腳本就是一串可以在命令行上輸入的命令。這樣用戶可以免于每次用這些命令(或相似的命令)都重新輸入。某些腳本語言包含一些簡單的編程功能如條件語句和循環,但這就是所有的了。這對于簡單的程序是足夠了。但是當腳本越來越大,要求越來越高——大部分情況都是如此——人們通常會覺得需要一些功能全面的編程語言。包含足夠操作系統接口的Scheme語言讓腳本編寫變得簡單而可維護。
這一節會描述如何在Scheme中編寫腳本。由于Scheme有太多方言一節太多實現不同的實現方法,我們專注于使用MzScheme方言,附錄A中講解了如果用其他方言需要有哪些修改。我們現在也專門講解Unix操作系統。附錄B里討論了DOS系統需要注意的問題。
## 16.1 再來一次Hello,World!
我們現在來創建一個Scheme腳本來對世界說hello。這對于通常的腳本語言也算不上什么難事。然而,為了后期上道編寫更復雜的腳本,我們必須理解如何用Scheme來編寫這個HelloWorld。首先,一個通常的Unix版的HelloWorld是一個文件,里面的內容如下:
```shell
echo Hello, World!
```
這里使用了命令`echo`,這個腳本可以被命名為`hello`,使用下面的命令使之可執行:
```shell
chmod +x hello
```
然后把它放在`PATH`環境變量中的任意一個目錄下。然后任何時候從命令行輸入
```shell
hello
```
就會輸出上面的問候。
Scheme的hello腳本也會用Scheme產生相同的輸出(見下面的腳本),但是我們得做點什么,讓操作系統知道它應該用Scheme來分析文件中的命令,而不是用它默認的腳本語言。Scheme的腳本文件,有命名為hello,內容如下:
```scheme
":"; exec mzscheme -r $0 "$@"
(display "Hello, World!")
(newline))
```
除了第一行以外都是Scheme代碼。然而第一行就是把這些代碼指定為“腳本”的神奇之處。當用戶在Unix命令行上輸入`hello`的時候,Unix會像讀取一般的腳本一樣來讀取這個文件。首先讀到一個`":"`,這是一個shell的空語句。后面的;是分隔符。下一個命令是`exec`。`exec`告訴Unix放棄當前腳本的執行并轉而執行`mzscheme -r $0 "$@"`,這里參數`$0`會被替換為當前文件的名字,參數`$@`會被替換為用戶運行該腳本時附加的參數。(在本例中沒有參數)
我們現在事實上以及把`hello`命令變換為另一個不同的命令,即:
```shell
mzscheme -r /whereveritis/hello
```
其中`/whereveritis/hello`是`hello`文件的路徑名。
mzscheme命令調用了MzScheme的可執行文件。`-r`選項告訴它把緊跟在該選項后面的參數作為一個Scheme文件來加載,在這之前還要把所有其他參數(如果有的話)放進一個叫`argv`的向量中(在本例中,`argv`是一個空向量)。
因此,Scheme腳本會作為一個Scheme文件來執行,而且該文件中的Scheme代碼還可以通過`argv`訪問到所有該腳本原先的參數。
現在,Scheme不得不來處理這個腳本中的第一行了。正如我們所看到的,這一行可是一個精心構造的Shell腳本。`":"`是一個Scheme中自求值的字符串所以沒有關系。`;`則開啟了Scheme的注釋,因此后面的exec等代碼都被安全的忽略掉了。文件剩下的部分都是Scheme代碼,被按順序求值,所有的求值完成后,Scheme就退出了。
總之,在命令提示符后面輸入`hello`會產生:
```
Hello, World!
```
并把命令提示符返回給你。
## 16.2 帶參數的腳本
Scheme腳本使用argv變量來引用它的參數。例如,下面的腳本輸出其所有參數,每個一行:
```scheme
":"; exec mzscheme -r $0 "$@"
;Put in argv-count the number of arguments supplied
(define argv-count (vector-length argv))
(let loop ((i 0))
(unless (>= i argv-count)
(display (vector-ref argv i))
(newline)
(loop (+ i 1))))
```
我們把這個腳本命名為`echoall`。調用`echoall 1 2 3`會顯示:
```
1
2
3
```
注意腳本名稱不包括在參數向量中。
## 16.3 例子
我們現在來解決一些更大的問題。我們需要在兩臺電腦之間傳輸文件,而唯一的方式是使用一張3.6英寸的軟盤作為媒介。我們需要一個`split4floppy`的腳本來把大于1.44MB的文件分割為軟盤能裝下的小塊。腳本`split4floppy`如下:
```scheme
":";exec mzscheme -r $0 "$@"
;floppy-size = number of bytes that will comfortably fit on a
; 3.5" floppy
(define floppy-size 1440000)
;split splits the bigfile f into the smaller, floppy-sized
;subfiles, viz, subfile-prefix.1, subfile-prefix.2, etc.
(define split
(lambda (f subfile-prefix)
(call-with-input-file f
(lambda (i)
(let loop ((n 1))
(if (copy-to-floppy-sized-subfile i subfile-prefix n)
(loop (+ n 1))))))))
;copy-to-floppy-sized-subfile copies the next 1.44 million
;bytes (if there are less than that many bytes left, it
;copies all of them) from the big file to the nth
;subfile. Returns true if there are bytes left over,
;otherwise returns false.
(define copy-to-floppy-sized-subfile
(lambda (i subfile-prefix n)
(let ((nth-subfile (string-append subfile-prefix "."
(number->string n))))
(if (file-exists? nth-subfile) (delete-file nth-subfile))
(call-with-output-file nth-subfile
(lambda (o)
(let loop ((k 1))
(let ((c (read-char i)))
(cond ((eof-object? c) #f)
(else
(write-char c o)
(if (< k floppy-size)
(loop (+ k 1))
#t))))))))))
;bigfile = script's first arg
; = the file that needs splitting
(define bigfile (vector-ref argv 0))
;subfile-prefix = script's second arg
; = the basename of the subfiles
(define subfile-prefix (vector-ref argv 1))
;Call split, making subfile-prefix.{1,2,3,...} from
;bigfile
(split bigfile subfile-prefix)
```
腳本`split4floppy`用如下方法調用:
```shell
split4floppy largefile chunk
```
這會把`largefile`分割成`chunk.1`、`chunk.2`等等,每個小塊文件都能裝進軟盤中。
所有`chunk.i`都移動到目標電腦上以后可以通過把`chunk.i`按順序拼起來還原`largefile`原文件,在Unix上這樣做:
```shell
cat chunk.1 chunk.2 ... > largefile
```
在DOS下這樣做:
```DOS
copy /b chunk.1+chunk.2+... largefile
```