Linux 操作不求人系列 - 貳章之貳 - Shell Script 程式設計(II) - BASH 與 TCSH / CSH

     在上章,我們介紹很多bash shell指令的應用方式,並讓它們變成 script,在這章此節,我們要承襲上節,繼續討論 bash shell script 的其它程式設計概念,與讓程式可重複使用的方法,就是利用函式(function)。
     首先,我們來創作一個判斷是否為閏年的函數,script的名稱就取為 check_year.sh ,請自行將其設為可執行。程式碼如圖2-6,為了解說方便,筆者利用指令 nl check_year.sh 將程式碼包含行數印出(圖2-7),其他除顏色外,都與圖2-6同。




(圖2-6)



(圖2-7)

       圖2-7第2-6行,與之前的範例相似,皆有防堵參數個數輸入錯誤的判斷。第7-11行為接著判斷輸入的年分,是否為真的正整數,也就是大於零的數字。其中第7行可解釋為,利用正規表示式搜尋 $2 字串值得頭至尾部的字元,皆由 0-9 組成,若有,則為真(True)會進入 if 內的陳述執行,但我們想要的,應該是僅要字串其中一字元為非正整數,便進入if 內的警告並跳出。故,筆者在判斷式前多加一個 ! ,代表著當字元完全是正整數時,就不要執行 if 內陳述,而直接往第12行執行,但若其中有一個字元為非正整數,則會進入if 內印出錯誤訊息並跳出 Script 。在此例使用者輸入非正整數等字串(如:12ab、cde、1a1b),便會出現錯誤訊息"Error Value",並跳出 Script 。而第8行的判斷式,效果跟第7行相同,但只能在BASH 3.0才能支援,故筆者故意保留,讓讀者可以學到另一種表示方式。
        第12-28行,為宣告一個函式 leapyr (),在 BASH Script內若要使用含式,必須在使用之前先建立函式的功能,如函式建立在第12-28行,則若要呼叫使用(Call)函式,則必須在第29行之後才能呼叫,並且可重複呼叫。第13行為定義函式呼叫時,一定會有一個外部參數,此外部參數非該 Script 的外部參數,而是由程式撰寫者給入,故在此無設定防呆判斷。第14-16則是利用echo 印數學運算式,再把結果傳到指令 bc ,讓其運算出來。bc 為 Linux 下一個簡易型計算器語言,可以接受很多數學運算式,如 % (mod,取餘數),a(x)(arctangent),有興趣的讀者,可以參考GNU網站說明,https://www.gnu.org/software/bc/manual/html_mono/bc.html。因閏年的規則為,可以被4(年)整除,且不可被100(年)整除,故取除4的餘數要為零,取除100的餘數不能為零。除此之外,若剛好遇取100的餘數為零且亦可以被400(年)整除的年分,亦可以算為閏年。排除這兩個條件,其它年分便為非閏年。是故,第17、20、24行,便是利用 if elif else 的敘述,來處理這些判斷,在 tcsh/csh 下,可能便改為 if elseif else。第26行便是印出判斷完的結果,而第27行通常為代表函式完成的傳回值,為保留字,請讀者勿遺忘。
        第29-43為利用指令 while 讀入 Script 外部參數,並利用 shift 的能力,可以讓使用者選擇是否要輸出到檔案,或僅列印到螢幕。第29行 while [] 指令,[]內必須給判斷式,判斷是為真(True),才進入while的內部步驟執行,若為假(False),則不會執行while內的步驟。在此例,判斷式為當 Script 輸入的參數數目大於 0 時(上節已說明,不再贅述),因我們之前已經利用 if 來限定 Script一定要給兩個外部參數,故此處 while 的判斷一定為真。第30行的 do,聰明的讀者一定可以連結到,在 BASH Script 內,只要有迴圈式的指令,如 for,請記得一定要搭配 do。(所以您看看,程式宅男是不是常把 do loop,do loop 掛嘴上)
       承上,進入 do 迴圈後,可以看到變數宣告 arga、argb、yr、output 的部分,筆者已經不在使用保留字 export (主要用在宣告環境變數,或 declare 等宣告自訂變數的方式),當然這在 BASH 是適用,但在 tcsh/csh 並不適用,通常使用 set 指令。arga接受第一序號的外部參數後,便使用 shift 指令,因 shift 指令的作用,就會讓 $2 的位置變成  $1 的位置,也就是假設本來指令是 check_year -y 2016,遇到 shift 後,就變成 check_year 2016,的相對位置。故 argb的值,就是 2016。緊接著第33行就進入 case 指令的選擇,若有第一次 arga 值為 -y,argb值就指定給 變數 yr,若第二次 arga 為 -o,argb 值就指定為變數 output,若非 -y 或 -o 就是印出 Arguments are undefined,然後跳出 Script。
        第45行,是為了將數字的字串值轉成數字型態,如有人輸入0990,就會轉成 990,較符合運算與人看得懂的型態。第46行,為將轉好型態的變數 yr,當成 leapyr 函式的外部參數,並將結果用指令 tee 來呈現,若 $output有值(如值為 myfile.out),則除了螢幕有印出結果,亦會將結果印在檔名myfile.out的內容裡,若$output無值,則僅會在螢幕畫面上印出結果。
       到目前為止,我們已經介紹 bash script 程式設計常用到的指令與方法,相信對於讀者們應該是多加練習後,可以善用這些方法,來解決大部分的反覆操作問題。
      緊接著筆者就來介紹一些 tcsh/csh 的應用。因為很多學術界的人士,大多會使用 tcsh/csh(為方便,以下就簡稱 tcsh) 或 Perl (https://en.wikipedia.org/wiki/Perl,筆者往後會另開新篇介紹) 來當它們的資料處理語言,且 tcsh 與 bash僅僅差異的程式語法(syntax)不同,但概念與結構卻是一模一樣,所以想一想,是否可以前章教過的 alias 來取代語法的不同?但先敘明,筆者在本節,是要討論標準的 TCSH Script 程式設計喔。一開始,我們就拿之前用 vim 編輯產生 路徑為 ~/Desktop的 myfile 來處理資料吧!
  我們先在放程式的 ~/Desktop/myfolder 內,先創立一個 cal_dist.tcsh 的檔案,並用 chmod 讓它可以執行,如圖2-8



















(圖2-8)

     圖2-8第1行為宣告以下的指令皆為使用於 TCSH Shell 環境,這點在所有 Shell Script 的宣告都一樣,就算是如 PHP Script 也要宣告 #!/usr/bin/php,否則執行此 Script 時,還要加上直譯器位置在執行擋前面才能執行。第2到6行為防止使用者給的外部參數數目不對,故不再贅述。第7行為將第一個(此例也是唯一壹個)外部參數值,設定給變數 file 的值,故第8至12行便是判斷 file 的值是否為一個檔案,若非檔案,則跳出 Script。
接著第13至15行為利用指令 set 設定一個觀測站座標值,要用來觀測我們前一章所產生的 myfile 檔案內的資料點,其觀測站的座標為變數值 $sx,$sy,$sz。第16至17行為計算 myfile 每一個被觀測物體,在三維空間內至觀測站的距離。第16行最後的 \ ,為串接下一行的指令,僅為了當指令太長需拆解才較易排版時,可利用此符號,令直譯器來組合數行併為為同一指令來執行。第17行為使用到空間上兩點距離的公式,代入 awk 程式語言來運算,並格化輸出數值後,再利用轉向子將標準輸出轉存至檔案 mytmp.out。此時,mytmp.out 為單欄的各被觀測點與觀測站的距離數值(圖2-9下)。
     第18行,筆者為了展現指令 paste 與 tee 的特性,特將 myfile 內的數值與計算所得結果 mytmpout 合併,利用指令 tee 另存成 myresult.data,並列出帶行數的各被觀測點之數值結果(圖2-9上)。















(圖2-9)

     緊接著,我們再利用前一節的 BASH Script 產生的每天的隨機資料,再撰寫一個TCSH script,檔案名稱為 cal_daily.tcsh(圖2-10),再利用指令 chmod 讓其可以執行。這類對階層式資料夾與資料夾內檔案處理的 Script,請讀者們,要特別注意位置與路徑的正確性
再提醒讀者,此例為在 ~/Desktop/myfolder 內的資料夾執行,且處理的資料夾為 ~/Desktop/myfolder/2016 內,含有各天的資料夾,在各天的資料夾下一層,便為要處理的資料檔案。






































(圖2-10)

     圖2-10第1行宣告,很簡單但是相當重要,前一節與這一節皆已經解釋過,故筆者在此不再說明。圖2-10第1區塊,為防止使用者輸入非2個外部參數,而造成錯誤。第2區塊,亦為檢查第1個外部參數是否確為資料夾,防止路徑或名稱上的錯誤。第3區塊,為將第一個參數設定為變數 yr 之值,且將兩個檔案檔名,分別當成變數 avglog 與 stdevlog 之值。緊接著利用指令 cd ,進入到下一層資料夾。
     第4至6的區塊,主要為利用 TCSH 的指令 switch 的結構,而 switch() 內的參數,則為外部第2個參數來決定,本立給定僅接受 avg 字串(Block 4)與 stdev 字串(Block 5),若非此二字串,則回進入 default: ,用來印出錯誤訊息並跳出Script。以下將第4區塊與第5區塊,分開說明:
因第二個參數為字串 avg ,而指令 case 'avg' 啟動進入第4區塊,接著我們設計一個要儲存資料的檔案變數 avglog ,先將資料的抬頭說明印到檔案內當第一行,緊接著利用 TCSH 特殊的 foreach 指令來逐一進入各天資料夾工作,直到遇到 end 結束每一天,foreach 指令說明如下
foreach day (`ls -d $yr".*"`)   /*  利用 ls 指令,將有 2016.* 此型態的資料夾,僅資料夾名稱列出,如 2016.001,...,2016.366 等,皆屬於此型態,* 代表 .  符號後,緊接的字串數數目不固定皆可。列出來的字串,會逐一來當成變數 day 的變數值。*/
cd $day    # 進入該天資料夾
echo "Calculating Average of $day"      # 提醒使用者要開始算該天數值的平均
set dfile = `ls $day.TS.data`      # 找出該天要用的數值檔案變成 $dfile 值

     接著 avga,avgb,avgc,就是利用 awk 來算 x,y,z 資料的的平均了,算出後,再利用 echo 覆寫入檔案 $avglog,請讀者記得,因我們剛剛把檔案產生在上一層,故覆寫請注意要用 ../$avglog,覆寫完後,記得利用指令 cd ..,來回到上層,這樣 foreach 的下一天,才能產生作用。如此一來,當 foreach 所有天的資料夾皆進入出來各一次,便算出每天的所有資料點,其 x y z 方向的平均數數值,因為 2016 資料夾有 366 天,故最後含抬頭說明應該為 367 行的資料值。因 TCSH 無函式(function)的功能,所以相似的 awk 運算,便要打三次,著實不符合程式碼需可重複使用的趨勢。
因第二個參數為字串 stdev ,而指令 case 'stdev' 啟動進入第5區塊,同上述,會先產生一個欲儲存資料的檔案變數 stdevlog,一樣先寫入資料抬頭,再利用 foreach 來逐一訪尋各天資料夾,此區塊與第4區塊最大的差異在於,awk 程式為使用於計算標準差的公式,並一樣的將結果逐一覆寫至檔案 $stdevlog。
圖2-10的倒數第2行,請記得要用 cd ..,回到執行 Script 那層資料夾。最後一行則是提醒使用者,Script 已經跑完,再請讀者自行變化。最後我們將產生的兩個檔案內的數值,利用LibreOffice Calc,匯出圖2-11,來看看每天的資料變化,當然,這是人為虛擬的數值點。













(圖2-11)

     最後一個 TCSH Script,筆者要介紹的是,在 TCSH 下,亦可以利用 shift 指令,把 script 變成如命令樣式。我們新增一個 script 檔案,cal_gcd.csh,令其可以執行,主要用來計算之前所建立的 myfile 檔案內的資料點,現假設其為在地球空間上的經緯度(第一、二欄位)資料,與使用者外部輸入的觀測點經緯度,再輸出到自訂的檔案內,如圖2-12。




























(圖2-12)

     圖2-12第1區塊為防例外錯誤措施,不再贅述。第二個區塊,如 BASH (圖2-7)一般,先判斷是否還有參數,若有,就用 shift 指令,來每次讀入兩個,再將值放入 switch 指令,內取值,而 TCSH 的 switch 指令,與 BASH 的 case 指令,功能相同但語法不同,圖2-10有說明,亦不再贅述。第2區塊為取得使用者輸入的( -lon )經度值、( -lat )緯度值、跟要( -o )轉存的檔案名稱值後,就進入第三區塊,說明如下:
set R=6371.008    /* 設定地球半徑,我使用 NASA 的值, http://nssdc.gsfc.nasa.gov/planetary/factsheet/earthfact.html   */

set PI= `echo "scale=10;" 4*a(1)" | bc -l `  /*  為利用指令 echo ,將運算式傳入簡易計算程式 bc ,來算出圓周率。  scale 代表十進位表示法,一般圓周率( π ) PI 算法為 4 * arctan(1)(反正切,讀做 arc tangent, 值為 inverse tangent),在 bc下表示為 a(),在其他程式語言函數名可能為 atan() 或 atan2()。 */

set file="myfile"  # 設定要讀入內含資料的檔案名稱,如 myfile。

     第4個區塊便為利用 awk ,數學關係式在本篇不討論,為引自wiki,https://en.wikipedia.org/wiki/Great-circle_distance,算出地球兩座標點間大圓路徑(Great-circle Path)的距離,並將其格式化列印至螢幕與利用指令 tee,轉到我們要存檔的檔名,如圖 2-13
。slon 與 slat 欄位為假設資料點在地球座標的經度與緯度,rlon 與 rlat 為假設觀測站在地球座標的經緯度,GCD 欄位便為此兩者座標的大圓路徑距離了。因程式碼較長,為了好說明,故使用 \ 來串接為同一行,以下簡單說明 awk 內的數學運算式:
 ^ 與 ** 共通,皆代表次方數。atan2 為反正切 arc/inverse tangent 的函數。sin()為正弦 (sine) 函數,cos()為餘弦(cosine)函數。
( lon - $1 ) < 0? lamda=(($1-lon)*rad) : lamda=((lon-$1)*rad)   /* 代表判斷 (lon-$1)是否小於0,若是就是做 : 左邊,算式交換,保持相減後為正值,若否就是做 : 右邊。為了取經度相減的絕對值,緯度亦用如此運算來取決對值。 sqrt() 為取開平方根 。*/























(圖2-13)

     關於本章的資料輸入給指令處理,皆是由檔案已經打好的欄位式資料輸入。若是讀者要使用手動輸入資料,如在命令列模式與 Script 內給幾欄要處理的資料,命列列如以下範例說明:
$ awk  '{print sqrt($1^2+$2^2)}' << EOF
> 121.21  22.11
> 121.41  22.33
> 121.61  22.55
> 122.21  23.11
> 122.41  23.33
> 122.61  23.55
>EOF
/*  重複讀取轉向子 << ,為將右方的資料逐一讀入指令 awk,EOF 為設定要讀入資料的尾部標籤,僅不要使用到該 Shell 的保留字字元皆可。接著開始輸入您使用的資料,值至資料結束,再下一行給上EOF,此時指令就知道該結束讀取,然後做您指定的行為,要在 Script 內用,僅去掉上例的提示字元$,>。  */

若是在 Script 內,要等待使用者輸入字元當變數值,則可如以下範例:
#!/bin/tcsh
echo "How old are you ?"
set yr = <       # 設定變數值,為使用者一次的輸入
echo "You are  $yr  years old. "
/* 以上 bash 要讀入使用者輸入的互動關係,亦改為 read -p 'How old are you ? ' yr,詳細可man read 查詢參數 */

      我們在這章產生相當多的程式檔案,這些都是很珍貴的經驗,若我們要將這些資料打包並且另存一份備份,則可以利用 tar 指令(圖2-14),如第2個指令:
$ tar -jcvf myshell.tar.bz  *.sh  *.tcsh   /* 參數 -j 為除打包成一個檔案,還要對該檔案做 bzip2 (http://www.bzip.org/)的壓縮,若要使用 gzip (http://www.gzip.org/)的壓縮,請把 -j 改為 -z-c為新建一包裹檔案,壓縮方式僅能選一種。參數 v (verbose),為將打包的檔案列出,參數 f ,為指定打包為壓縮檔案,此例檔名則為 myshell.tar.gz (請自行取名),後面接著一大串要打包起來的檔案。  */

     若要解開包裹亦使用 tar,如第4個指令:
$ tar -jxvf myshell.tar.bz  -C ./tmp_tar   /* 先前用什麼方法包裝(bzip2,gzip),就用什麼方法解,僅差別在解開包裹為使用參數 x (extract) ,若要指定解到哪個路徑下,請使用參數 -C ,後面接上目錄路徑,未指定路徑,則為直接解到現在的資料夾下。tar 的所有參數詳細用法,有興趣的讀者,亦可藉由 man tar,來得知。 */






































(圖2-14)
     最後,若我們寫了太多 Script ,且忘記放置的位置,則可以利用指令 find 來搜尋,如下
$ find  ~/  -type  f  -name  '*.*sh'  -print   /* 搜尋使用者家目錄下( ~/ )(含子目錄)的所有檔案,參數 f ,若是要搜尋資料夾,則改為 d 。檔案名稱( -name )為類似 *.*sh  ( * 為萬用字元,字數不需固定 ),皆列印至螢幕畫面。其他 find 指令的活用方法,詳細可參考GNU網站https://www.gnu.org/software/findutils/manual/html_mono/find.html  */
 若讀者的script需要花很多時間來執行完成,或是因為遠端連線無法一直連線的原因,會使得script的工作一再停止,此時可使用以下指令
$ nohup myshell.sh
/* 若myshell.csh 這個 script需很多時間才能完成程序,可以利用nohup來放置到系統背景,而非僅有終端機背景。有興趣的讀者可以自行man nohup。*/
 倘若讀者要把輸出在螢幕上的訊息 與錯誤輸出資訊存於 output.log ,則可用以下指令:
$ myshell.sh >& output.log

$ myshell.sh > test.log 2>&1
/* 利用標準輸出轉向子 > 或 1> 將檔案覆蓋,重新寫入 output.log檔案,並且利用標準錯誤轉向子 >& 或 2> 將錯誤訊息亦寫入相同檔案。若不想在螢幕顯示錯誤訊息,可寫入 /dev/null 。若要接續寫則改為 1>> 或 >>,標準錯誤資訊接續寫入則為 >>& 或 2>>。反之 myshell.sh > /dev/null 1 >&2 則將標準錯誤輸出與標準輸出皆輸出於螢幕上 */
若要將標準錯誤訊息存到檔案err.log,且標準輸出的訊息存到檔案output.log,可以利用以下指令
$ myshell.sh 2> err.log | tee output.log

$ myshell.sh  2> err.log  1> output.log
--------------------------------

     綜合本章兩節的說明,經由一些簡單的Shell Script 程式設計,相信讀者應該可以體會到 BASH 與 TCSH,在某些使用程度上的差異,故請讀者自行依個人的學經歷程與喜好,來選擇其中一樣,您可以習慣的 Shell Script,再加以專精學習後,定能使工作效率加倍。接下來的篇章,即將介紹 Linux 管理者操作部分,並進而可以帶入利用Linux的穩定性,架設各種實用的伺服器軟體。一般使用者操作與 BASH/TCSH Shell Script 程式設計的部分,就介紹到這裡,緊接著,就讓筆者帶領各位讀者,慢慢進入至 Linux 極客(Geek)的領域吧!



If you have any feedback or question, please go to my forum to discuss.

這個網誌中的熱門文章

Linux操作不求人 - 伍章之伍 - make 巨集式編譯器

Linux 操作不求人系列 - 貳章之壹 - Shell Script 程式設計(I) - BASH

Linux操作不求人 - 肆章之貳 - 伺服器架設(II) - 郵件伺服器 - postfix 與 dovecot