# 第二章 和Sha-Bang(#!)一起出發
> Shell編程聲名顯赫
>
> —— Larry Wall
### 本章目錄
- [2.1 調用一個腳本](02_1_invoking_the_script.md)
- [2.2 牛刀小試](02_2_preliminary_exercises.md)
---
一個最簡單的腳本其實就是將一連串系統命令存儲在一個文件中。最起碼,它能幫你省下重復輸入這一連串命令的功夫。
樣例 2-1. *cleanup*:清理`/var/log`目錄下的日志文件
```bash
# Cleanup
# 請使用root權限執行
cd /var/log
cat /dev/null > messages
cat /dev/null > wtmp
echo "Log files cleaned up."
```
這支腳本僅僅是一些可以很容易從終端或控制臺輸入的命令的集合罷了,沒什么特殊的地方。將命令放在腳本中的好處是,你不用再一遍遍重復輸入這些命令啦。腳本成了一支*程序*、一款*工具*,它可以很容易的被修改或為特殊需求定制。
樣例 2-2. *cleanup*:改進的清理腳本
```bash
#!/bin/bash
# Bash腳本標準起始行。
# Cleanup, version 2
# 請使用root權限執行。
# 這里可以插入代碼來打印錯誤信息,并在未使用root權限時退出。
LOG_DIR=/var/log
# 使用變量比硬編碼(hard-coded)更合適
cd $LOG_DIR
cat /dev/null > messages
cat /dev/null > wtmp
echo "Logs cleaned up."
exit # 正確終止腳本的方式。
# 不帶參數的exit返回上一條指令的運行結果。
```
現在我們看到了一個真正意義上的腳本! 讓我們繼續前進...
樣例 2-3. *cleanup*:改良、通用版
```bash
#!/bin/bash
# Cleanup, version 3
# 注意:
# --------
# 此腳本涉及到許多后邊才會解釋的特性。
# 當你閱讀完整本書的一半以后,理解它們就沒有任何困難了。
LOG_DIR=/var/log
ROOT_UID=0 # UID為0的用戶才擁有root權限。
LINES=50 # 默認保存messages日志文件行數。
E_XCD=86 # 無法切換工作目錄的錯誤碼。
E_NOTROOT=87 # 非root權限用戶執行的錯誤碼。
# 請使用root權限運行。
if [ "$UID" -ne "$ROOT_UID" ]
then
echo "Must be root to run this script."
exit $E_NOTROOT
fi
if [ -n "$1" ]
# 測試命令行參數(保存行數)是否為空
then
lines=$1
else
lines=$LINES # 如果為空則使用默認設置
fi
# Stephane Chazelas 建議使用如下方法檢查命令行參數,
# 但是這已經超出了此階段教程的范圍。
#
# E_WRONGARGS=85 # Non-numerical argument (bad argument format).
# case "$1" in
# "" ) lines=50;;
# *[!0-9]*) echo "Usage: `basename $0` lines-to-cleanup";
# exit $E_WRONGARGS;;
# * ) lines=$1;;
# esac
#
#* 在第十一章“循環與分支”中會對此作詳細的闡述。
cd $LOG_DIR
if [ `pwd` != "$LOG_DIR" ] # 也可以這樣寫 if [ "$PWD" != "$LOG_DIR" ]
# 檢查工作目錄是否為 /var/log ?
then
echo "Can't change to $LOG_DIR"
exit $E_XCD
fi # 在清理日志前,二次確認是否在正確的工作目錄下。
# 更高效的寫法:
#
# cd /var/log || {
# echo "Cannot change to necessary directory." >&2
# exit $E_XCD;
# }
tail -n $lines messages > mesg.temp # 保存messages日志文件最后一部分
mv mesg.temp messages # 替換系統日志文件以達到清理目的
# cat /dev/null > messages
#* 我們不需要使用這個方法了,上面的方法更安全
cat /dev/null > wtmp # ': > wtmp' 與 '> wtmp' 有同樣的效果
echo "Log files cleaned up."
# 注意在/var/log目錄下的其他日志文件不會被這個腳本清除
exit 0
# 返回0表示腳本運行成功
```
也許你并不希望清空全部的系統日志,這個腳本保留了messages日志的最后一部分。隨著學習的深入,你將明白更多提高腳本運行效率的方法。
---
腳本起始行*sha-bang*(#!)[^1]告訴系統這個腳本文件需要使用指定的命令解釋器來執行。#!實際上是一個占兩字節[^2]的*幻數*(magic number),幻數可以用來標識特殊的文件類型,在這里則是標記可執行shell腳本(你可以在終端中輸入`man magic`了解更多信息)。緊隨#!的是一個路徑名。此路徑指向用來解釋此腳本的程序,它可以是shell,可以是程序設計語言,也可以是實用程序。這個解釋器從頭(#!的下一行)開始執行整個腳本的命令,同時忽略注釋。[^3]
```
#!/bin/sh
#!/bin/bash
#!/usr/bin/perl
#!/usr/bin/tcl
#!/bin/sed -f
#!/bin/awk -f
```
上面每一條腳本起始行都調用了不同的解釋器,比如`/bin/sh`調用了系統默認shell(Linux系統中默認是bash)[^4]。大部分UNIX商業發行版中默認的是Bourne shell,即`#!/bin/sh`。你可以以犧牲Bash特性為代價,在非Linux的機器上運行sh腳本。當然,腳本得遵循POSIX[^5] sh標準。
需要注意的是`#!`后的路徑必須正確,否則當你運行腳本時只會得到一條錯誤信息,通常是"Command not found."[^6]
當腳本僅包含一些通用的系統命令而不使用shell內部指令時,可以省略`#!`。第三個例子需要`#!`是因為當對變量賦值時,例如`lines=50`,使用了與shell特性相關的結構[^7]。再重復一次,`#!/bin/sh`調用的是系統默認shell解釋器,在Linux系統中默認為`/bin/bash`。
這個例子鼓勵讀者使用模塊化的方式編寫腳本,并在平時記錄和收集一些在以后可能會用到的代碼模板。最終你將擁有一個相當豐富易用的代碼庫。以下的代碼可以用來測試腳本被調用時的參數數量是否正確。
```bash
E_WRONG_ARGS=85
script_parameters="-a -h -m -z"
# -a = all, -h = help 等等
if [ $# -ne $Number_of_expected_args ]
then
echo "Usage: `basename $0` $script_parameters"
# `basename $0` 是腳本的文件名
exit $E_WRONG_ARGS
fi
```
大多數情況下,你會針對特定的任務編寫腳本。本章的第一個腳本就是這樣。然后你也許會泛化(generalize)腳本使其能夠適應更多相似的任務,比如用變量代替硬編碼,用函數代替重復代碼。
[^1]: 在文獻中更常見的形式是she-bang或者sh-bang。它們都來源于詞匯sharp(#)和bang(!)的連接。
[^2]: 一些UNIX的衍生版(基于4.2 BSD)聲稱他們使用四字節的幻數,在#!后增加一個空格,即`#! /bin/sh`。而[Sven Mascheck](http://www.in-ulm.de/~mascheck/various/shebang/#details)指出這是虛構的。
[^3]: <p>命令解釋器首先將會解釋#!這一行,而因為#!以#打頭,因此解釋器將其視作注釋。起始行作為調用解釋器的作用已經完成了。</p><p>事實上即使腳本中含有不止一個#!,bash也會將除第一個`#!`以外的解釋為注釋。</p><pre>#!/bin/bash<br/><br/>echo "Part 1 of script."<br/>a=1<br/><br/>#!/bin/bash<br/># 這并不會啟動新的腳本<br/><br/>echo "Part 2 of script."<br/>echo $a # $a的值仍舊為1</pre>
[^4]: <p>這里允許使用一些技巧。</p><pre>#!/bin/rm<br/># 自我刪除的腳本<br/><br/># 當你運行這個腳本,除了這個腳本本身消失以外并不會發生什么。<br/><br/>WHATEVER=85<br/><br/>echo "This line will never print (betcha!)."<br/><br/>exit $WHATEVER # 這沒有任何關系。腳本將不會從這里退出。<br/> # 嘗試在腳本終止后打印echo $a。<br/> # 得到的值將會是0而不是85.</pre>當然你也可以建立一個起始行是`#!/bin/more`的README文件,并且使它可以執行。結果就是這個文件成為了一個可以打印本身的文件。(查看樣例 19-3,使用`cat`命令的here document也許是一個更好的選擇)
[^5]: 可移植操作系統接口(POSIX)嘗試標準化類UNIX操作系統。POSIX規范可以在[Open Group site](http://www.opengroup.org/onlinepubs/007904975/toc.htm)中查看。
[^6]: 為了避免這種情況的發生,可以使用`#!/bin/env bash`作為起始行。這在bash不在`/bin`的UNIX系統中會有效果。
[^7]: 如果bash是系統默認shell,那么腳本并不一定需要#!作為起始行。但是當你在其他的shell中運行腳本,例如tcsh,則需要使用#!。
- 第一部分 初見shell
- 1. 為什么使用shell編程
- 2. 和Sha-Bang(#!)一起出發
- 2.1 調用一個腳本
- 2.2 牛刀小試
- 第二部分 shell基礎
- 3. 特殊字符
- 4. 變量與參數
- 4.1 變量替換
- 4.2 變量賦值
- 4.3 Bash弱類型變量
- 4.4 特殊變量類型
- 5. 引用
- 5.1 引用變量
- 5.2 轉義
- 6. 退出與退出狀態
- 7. 測試
- 7.1 測試結構
- 7.2 文件測試操作
- 7.3 其他比較操作
- 7.4 嵌套 if/then 條件測試
- 7.5 牛刀小試
- 8. 運算符相關話題
- 8.1 運算符
- 8.2 數字常量
- 8.3 雙圓括號結構
- 8.4 運算符優先級
- 第三部分 shell進階
- 10. 變量處理
- 10.1 字符串處理
- 10.1.1 使用 awk 處理字符串
- 10.1.2 參考資料
- 10.2 參數替換
- 11. 循環與分支
- 11.1 循環
- 11.2 嵌套循環
- 11.3 循環控制
- 11.4 測試與分支
- 12. 命令替換
- 13. 算術擴展
- 14. 休息時間
- 第五部分 進階話題
- 19. 嵌入文檔
- 20. I/O 重定向
- 20.1 使用 exec
- 20.2 重定向代碼塊
- 20.3 應用程序
- 22. 限制模式的Shell
- 23. 進程替換
- 26. 列表結構
- 25. 別名