shell 腳本之始
POSIX 操作係統本身就像是一個 API。如果你能弄清楚如何在 POSIX 的 shell 中完成一個任務,那麼你可以自動化這個任務。這就是編程,這種日常 POSIX 編程方法的主要方式就是 shell 腳本。
像它的名字那樣,shell 腳本就是一行一行你想讓你的計算機執行的語句,就像你手動的一樣。
因為 shell 腳本包含常見的日常命令,所以熟悉 UNIX 或 Linux(通常稱為 POSIX 係統)對 shell 是有幫助的。你使用 shell 的經驗越多,就越容易編寫新的腳本。這就像學習外語:你心裏的詞匯越多,組織複雜的句子就越容易。
當您打開終端窗口時,就是打開了 shell 。shell 有好幾種,本教程適用於 bash、tcsh、ksh、zsh 和其它幾個。在下麵幾個部分,我提供一些 bash 特定的例子,但最終的腳本不會用那些,所以你可以切換到 bash 中學習設置變量的課程,或做一些簡單的語法調整。
如果你是新手,隻需使用 bash 。它是一個很好的 shell,有許多友好的功能,它是 Linux、Cygwin、WSL、Mac 默認的 shell,並且在 BSD 上也支持。
Hello world
您可以從終端窗口生成您自己的 hello world 腳本 。注意你的引號;單和雙都會有不同的效果(LCTT 譯注:想必你不會在這裏使用中文引號吧)。
$ echo "#\!/bin/sh" > hello.sh
$ echo "echo 'hello world' " >> hello.sh
正如你所看到的,編寫 shell 腳本就是這樣,除了第一行之外,就是把命令“回顯”或粘貼到文本文件中而已。
像應用程序一樣運行腳本:
$ chmod +x hello.sh
$ ./hello.sh
hello world
不管多少,這就是一個 shell 腳本了。
現在讓我們處理一些有用的東西。
去除空格
如果有一件事情會幹擾計算機和人類的交互,那就是文件名中的空格。您在互聯網上看到過:http://example.com/omg%2ccutest%20cat%20photophoto%21%211.jpg 等網址。或者,當你不管不顧地運行一個簡單的命令時,文件名中的空格會讓你掉到坑裏:
$ cp llama pic.jpg ~/photos
cp: cannot stat 'llama': No such file or directory
cp: cannot stat 'pic.jpg': No such file or directory
解決方案是用反斜杠來“轉義”空格,或使用引號:
$ touch foo\ bar.txt
$ ls "foo bar.txt"
foo bar.txt
這些都是要知道的重要的技巧,但是它並不方便,為什麼不寫一個腳本從文件名中刪除這些煩人的空格?
創建一個文件來保存腳本,以釋伴shebang(#!) 開頭,讓係統知道文件應該在 shell 中運行:
$ echo '#!/bin/sh' > despace
好的代碼要從文檔開始。定義好目的讓我們知道要做什麼。這裏有一個很好的 README:
despace is a shell script for removing spaces from file names.
Usage:
$ despace "foo bar.txt"
現在讓我們弄明白如何手動做,並且如何去構建腳本。
假設你有個隻有一個 foo bar.txt 文件的目錄,比如:
$ ls
hello.sh
foo bar.txt
計算機無非就是輸入和輸出而已。在這種情況下,輸入是 ls
特定目錄的請求。輸出是您所期望的結果:該目錄文件的名稱。
在 UNIX 中,可以通過“管道”將輸出作為另一個命令的輸入,無論在管道的另一側是什麼過濾器。 tr
程序恰好設計為專門修改傳輸給它的字符串;對於這個例子,可以使用 --delete
選項刪除引號中定義的字符。
$ ls "foo bar.txt" | tr --delete ' '
foobar.txt
現在你得到了所需的輸出了。
在 Bash shell 中,您可以將輸出存儲為變量 。變量可以視為將信息存儲到其中的空位:
$ NAME=foo
當您需要返回信息時,可以通過在變量名稱前麵綴上美元符號($
)來引用該位置。
$ echo $NAME
foo
要獲得您的這個去除空格後的輸出並將其放在一邊供以後使用,請使用一個變量。將命令的結果放入變量,使用反引號(`
)來完成:
$ NAME=`ls "foo bar.txt" | tr -d ' '`
$ echo $NAME
foobar.txt
我們完成了一半的目標,現在可以從源文件名確定目標文件名了。
到目前為止,腳本看起來像這樣:
#!/bin/sh
NAME=`ls "foo bar.txt" | tr -d ' '`
echo $NAME
第二部分必須執行重命名操作。現在你可能已經知道這個命令:
$ mv "foo bar.txt" foobar.txt
但是,請記住在腳本中,您正在使用一個變量來保存目標名稱。你已經知道如何引用變量:
#!/bin/sh
NAME=`ls "foo bar.txt" | tr -d ' '`
echo $NAME
mv "foo bar.txt" $NAME
您可以將其標記為可執行文件並在測試目錄中運行它。確保您有一個名為 foo bar.txt(或您在腳本中使用的其它名字)的測試文件。
$ touch "foo bar.txt"
$ chmod +x despace
$ ./despace
foobar.txt
$ ls
foobar.txt
去除空格 v2.0
腳本可以正常工作,但不完全如您的文檔所述。它目前非常具體,隻適用於一個名為 foo\ bar.txt
的文件,其它都不適用。
POSIX 命令會將其命令自身稱為 $0
,並將其後鍵入的任何內容依次命名為 $1
,$2
,$3
等。您的 shell 腳本作為 POSIX 命令也可以這樣計數,因此請嚐試用 $1
來替換 foo\ bar.txt
。
#!/bin/sh
NAME=`ls $1 | tr -d ' '`
echo $NAME
mv $1 $NAME
創建幾個新的測試文件,在名稱中包含空格:
$ touch "one two.txt"
$ touch "cat dog.txt"
然後測試你的新腳本:
$ ./despace "one two.txt"
ls: cannot access 'one': No such file or directory
ls: cannot access 'two.txt': No such file or directory
看起來您發現了一個 bug!
這實際上不是一個 bug,一切都按設計工作,但不是你想要的。你的腳本將 $1
變量真真切切地 “擴展” 成了:“one two.txt”,搗亂的就是你試圖消除的那個麻煩的空格。
解決辦法是將變量用以引號封裝文件名的方式封裝變量:
#!/bin/sh
NAME=`ls "$1" | tr -d ' '`
echo $NAME
mv "$1" $NAME
再做個測試:
$ ./despace "one two.txt"
onetwo.txt
$ ./despace c*g.txt
catdog.txt
此腳本的行為與任何其它 POSIX 命令相同。您可以將其與其他命令結合使用,就像您希望的使用的任何 POSIX 程序一樣。您可以將其與命令結合使用:
$ find ~/test0 -type f -exec /path/to/despace {} \;
或者你可以使用它作為循環的一部分:
$ for FILE in ~/test1/* ; do /path/to/despace $FILE ; done
等等。
去除空格 v2.5
這個去除腳本已經可以發揮功用了,但在技術上它可以優化,它可以做一些可用性改進。
首先,變量實際上並不需要。 shell 可以一次計算所需的信息。
POSIX shell 有一個操作順序。在數學中使用同樣的方式來首先處理括號中的語句,shell 在執行命令之前會先解析反引號 `
或 Bash 中的 $()
。因此,下列語句:
$ mv foo\ bar.txt `ls foo\ bar.txt | tr -d ' '`
會變換成:
$ mv foo\ bar.txt foobar.txt
然後實際的 mv
命令執行,就得到了 foobar.txt 文件。
知道這一點,你可以將該 shell 腳本壓縮成:
#!/bin/sh
mv "$1" `ls "$1" | tr -d ' '`
這看起來簡單的令人失望。你可能認為它使腳本減少為一個單行並沒有必要,但沒有幾行的 shell 腳本是有意義的。即使一個用簡單的命令寫的緊縮的腳本仍然可以防止你發生致命的打字錯誤,這在涉及移動文件時尤其重要。
此外,你的腳本仍然可以改進。更多的測試發現了一些弱點。例如,運行沒有參數的 despace
會產生一個沒有意義的錯誤:
$ ./despace
ls: cannot access '': No such file or directory
mv: missing destination file operand after ''
Try 'mv --help' for more information.
這些錯誤是讓人迷惑的,因為它們是針對 ls
和 mv
發出的,但就用戶所知,它運行的不是 ls
或 mv
,而是 despace
。
如果你想一想,如果它沒有得到一個文件作為命令的一部分,這個小腳本甚至不應該嚐試去重命名文件,請嚐試使用你知道的變量以及 test
功能來解決。
if 和 test
if
語句將把你的小 despace 實用程序從腳本蛻變成程序。這裏麵涉及到代碼領域,但不要擔心,它也很容易理解和使用。
if
語句是一種開關;如果某件事情是真的,那麼你會做一件事,如果它是假的,你會做不同的事情。這個if-then
指令的二分決策正好是計算機是擅長的;你需要做的就是為計算機定義什麼是真或假以及並最終執行什麼。
測試真或假的最簡單的方法是 test
實用程序。你不用直接調用它,使用它的語法即可。在終端試試:
$ if [ 1 == 1 ]; then echo "yes, true, affirmative"; fi
yes, true, affirmative
$ if [ 1 == 123 ]; then echo "yes, true, affirmative"; fi
$
這就是 test
的工作方式。你有各種方式的簡寫可供選擇,這裏使用的是 -z
選項,它檢測字符串的長度是否為零(0)。將這個想法翻譯到你的 despace 腳本中就是:
#!/bin/sh
if [ -z "$1" ]; then
echo "Provide a \"file name\", using quotes to nullify the space."
exit 1
fi
mv "$1" `ls "$1" | tr -d ' '`
為了提高可讀性,if
語句被放到單獨的行,但是其概念仍然是:如果 $1
變量中的數據為空(零個字符存在),則打印一個錯誤語句。
嚐試一下:
$ ./despace
Provide a "file name", using quotes to nullify the space.
$
成功!
好吧,其實這是一個失敗,但它是一個漂亮的失敗,更重要的是,一個有意義的失敗。
注意語句 exit 1
。這是 POSIX 應用程序遇到錯誤時向係統發送警報的一種方法。這個功能對於需要在腳本中使用 despace ,並依賴於它成功執行才能順利運行的你或其它人來說很重要。
最後的改進是添加一些東西,以保護用戶不會意外覆蓋文件。理想情況下,您可以將此選項傳遞給腳本,所以它是可選的;但為了簡單起見,這裏對其進行了硬編碼。 -i
選項告訴 mv
在覆蓋已存在的文件之前請求許可:
#!/bin/sh
if [ -z "$1" ]; then
echo "Provide a \"file name\", using quotes to nullify the space."
exit 1
fi
mv -i "$1" `ls "$1" | tr -d ' '`
現在你的 shell 腳本是有意義的、有用的、友好的 - 你是一個程序員了,所以不要停。學習新命令,在終端中使用它們,記下您的操作,然後編寫腳本。最終,你會把自己從工作中解脫出來,當你的機器仆人運行 shell 腳本,接下來的生活將會輕鬆。
原文發布時間為:2017-02-19
本文來自雲棲社區合作夥伴“Linux中國”
最後更新:2017-05-26 08:52:19