Linux Shell高级技巧(一)
一、將輸入信息轉換為大寫字符后再進行條件判斷:
?? ? ?我們在讀取用戶的正常輸入后,很有可能會將這些輸入信息用于條件判斷,那么在進行比較時,我們將不得不考慮這些信息的大小寫匹配問題。
?? ?? /> cat > test1.sh
?? ?? #!/bin/sh
?? ?? echo -n "Please let me know your name. "
? ? ? read name
?? ?? #將變量name的值通過管道輸出到tr命令,再由tr命令進行大小寫轉換后重新賦值給name變量。
?? ?? name=`echo $name | tr [a-z] [A-Z]`
?? ?? if [[ $name == "STEPHEN" ]]; then
?? ? ???? echo "Hello, Stephen."
?? ?? else
?? ?????? echo "You are not Stephen."
? ? ? fi
? ? ? CTRL+D
? ? ? /> ./test1.sh
?? ?? Please let me know your name. stephen
? ? ? Hello, Stephen.
二、為調試信息設置輸出級別:
?? ?
?? ?? 我們經常在調試腳本時添加一些必要的調試信息,以便跟蹤到程序中的錯誤。在完成調試后,一般都會選擇刪除這些額外的調試信息,在過了一段時間之后,如果腳本需要添加新的功能,那么我們將不得不重新進行調試,這樣又有可能需要添加這些調試信息,在調試成功之后,這些信息可能會被再次刪除。如果我們能夠為我們的調試信息添加調試級別,使其只在必要的時候輸出,我想這將會是一件非常愜意的事情。
?? ?? /> cat > test2.sh
?? ?? #!/bin/sh
?? ?? if [[ $# == 0 ]]; then
?? ? ? ?? echo "Usage: ./test2.sh -d debug_level"
??? ?? ?? exit 1
? ? ? fi
? ? ? #1. 讀取腳本的命令行選項參數,并將選項賦值給變量argument。
? ? ? while getopts d: argument
? ? ? do
?? ? ? ?? #2. 只有到選項為d(-d)時有效,同時將-d后面的參數($OPTARG)賦值給變量debug,表示當前腳本的調試級別。
?? ?? ??? case $argument in
??? ?? ?? d) debug_level=$OPTARG ;;
??? ?? ?? \?) echo "Usage: ./test2.sh -d debug_level"
??? ?? ?????? exit 1
??? ?? ?????? ;;
??? ?? ?? esac
? ? ? done
? ? ? #3. 如果debug此時的值為空或者不是0-9之間的數字,給debug變量賦缺省值0.
?? ?? if [[ -z $debug_level ||? $debug_level != [0-9] ]]; then
? ? ????? debug_level=0
? ? ? fi
? ? ? echo "The current debug_level level is $debug_level."
?? ?? echo -n "Tell me your name."
? ? ? read name
? ? ? name=`echo $name | tr [a-z] [A-Z]`
? ? ? if [ $name = "STEPHEN" ];then
? ?? ???? #4. 根據當前腳本的調試級別判斷是否輸出其后的調試信息,此時當debug_level > 0時輸出該調試信息。
? ? ????? test $debug_level -gt 0 && echo "This is stephen."
?? ?????? #do something you want here.
?? ?? elif [ $name = "ANN" ]; then
? ?? ???? #5. 當debug_level > 1時輸出該調試信息。
? ? ????? test $debug_level -gt 1 && echo "This is ann."
? ? ????? #do something you want here.
? ? ? else
? ?? ???? #6. 當debug_level > 2時輸出該調試信息。
? ?? ???? test $debug_level -gt 2 && echo "This is others."
? ?? ???? #do any other else.
?? ?? fi
?? ?? CTRL+D
? ? ? /> ./test2.sh
? ? ? Usage: ./test2.sh -d debug_level
? ? ? /> ./test2.sh -d 1
? ? ? The current debug level is 1.
? ? ? Tell me your name. ann
? ? ? /> ./test2.sh -d 2
? ? ? The current debug level is 2.
? ? ? Tell me your name. ann
?? ?? This is ann.
三、判斷參數是否為數字:
?? ?? 有些時候我們需要驗證腳本的參數或某些變量的值是否為數字,如果不是則需要需要給出提示,并退出腳本。
????? /> cat > test3.sh
????? #!/bin/sh
????? #1. $1是腳本的第一個參數,這里作為awk命令的第一個參數傳入給awk命令。
????? #2. 由于沒有輸入文件作為輸入流,因此這里只是在BEGIN塊中完成。
????? #3. 在awk中ARGV數組表示awk命令的參數數組,ARGV[0]表示命令本身,ARGV[1]表示第一個參數。
????? #4. match是awk的內置函數,返回值為匹配的正則表達式在字符串中(ARGV[1])的起始位置,沒有找到返回0。
????? #5. 正則表達式的寫法已經保證了匹配的字符串一定是十進制的正整數,如需要浮點數或負數,僅需修改正則即可。
????? #6. awk執行完成后將結果返回給isdigit變量,并作為其初始化值。
?? ?? #7. isdigit=`echo $1 | awk '{ if (match($1, "^[0-9]+$") != 0) print "true"; else print "false" }' `
?? ?? #8. 上面的寫法也能實現該功能,但是由于有多個進程參與,因此效率低于下面的寫法。
????? isdigit=`awk 'BEGIN { if (match(ARGV[1],"^[0-9]+$") != 0) print "true"; else print "false" }' $1`
????? if [[ $isdigit == "true" ]]; then
????? ? ? echo "This is numeric variable."
????????? number=$1
????? else
????? ? ? echo "This is not numeric variable."
????????? number=0
????? fi
????? CTRL+D
????? /> ./test3.sh 12
????? This is numeric variable.
?? ?? /> ./test3.sh 12r
?? ?? This is not numeric variable.
四、判斷整數變量的奇偶性:
?? ?? 為了簡化問題和突出重點,這里我們假設腳本的輸入參數一定為合法的整數類型,因而在腳本內部將不再進行參數的合法性判斷。
?? ?? /> cat > test4.sh
?? ?? #!/bin/sh
?? ?? #1. 這里的重點主要是sed命令中正則表達式的寫法,它將原有的數字拆分為兩個模式(用圓括號拆分),一個前面的所有高位數字,另一個是最后一位低位數字,之后再用替換符的方式(\2),將原有數字替換為只有最后一位的數字,最后將結果返回為last_digit變量。
?? ?? last_digit=`echo $1 | sed 's/\(.*\)\(.\)$/\2/'`
?? ?? #2. 如果last_digit的值為0,2,4,6,8,就表示其為偶數,否則為奇數。
?? ?? case $last_digit in
?? ?? 0|2|4|6|8)
???? ?? ? echo "This is an even number." ;;
?? ?? *)
???? ?? ? echo "This is not an even number." ;;
?? ?? esac
?? ?? CTRL+D
?? ?? /> ./test4.sh 34
?? ?? This is an even number.
?? ?? /> ./test4.sh 345
?? ?? This is not an even number.
?? ????
五、將Shell命令賦值給指定變量,以保證腳本的移植性:
?? ?? 有的時候當我們在腳本中執行某個命令時,由于操作系統的不同,可能會導致命令所在路徑的不同,甚至是命令名稱或選項的不同,為了保證腳本具有更好的平臺移植性,我們可以將該功能的命令賦值給指定的變量,之后再使用該命令時,直接使用該變量即可。這樣在今后增加更多OS時,我們只需為該變量基于新系統賦予不同的值即可,否則我們將不得不修改更多的地方,這樣很容易導致因誤修改而引發的Bug。
?? ?? /> cat > test5.sh
?? ?? #!/bin/sh
?? ?? #1. 通過uname命令獲取當前的系統名稱,之后再根據OS名稱的不同,給PING變量賦值不同的ping命令的全稱。
?? ?? osname=`uname -s`
?? ?? #2. 可以在case的條件中添加更多的操作系統名稱。
?? ?? case $osname in
?? ?? "Linux")
?? ? ???? PING=/usr/sbin/ping ;;
?? ?? "FreeBSD")
?? ?????? PING=/sbin/ping ;;
?? ?? "SunOS")
?? ?????? PING=/usr/sbin/ping ;;
?? ?? *)
?? ?????? ;;
?? ?? esac
?? ?? CTRL+D
?? ?? /> . ./test5.sh
?? ?? /> echo $PING
?? ?? /usr/sbin/ping
???
六、獲取當前時間距紀元時間(1970年1月1日)所經過的天數:
?? ?? 在獲取兩個時間之間的差值時,需要考慮很多問題,如閏年、月份中不同的天數等。然而如果我們能夠確定兩個時間點之間天數的差值,那么再計算時分秒的差值時就非常簡單了。在系統提供的C語言函數中,獲取的時間值是從1970年1月1日0點到當前時間所流經的秒數,如果我們基于此計算兩個時間之間天數的差值,將會大大簡化我們的計算公式。
?? ?? /> cat > test6.sh
?? ?? #!/bin/sh
?? ?? #1. 將date命令的執行結果(秒 分 小時 日 月 年)賦值給數組變量DATE。
?? ?? declare -a DATE=(`date +"%S %M %k %d %m %Y"`)
?? ?? #2. 為了提高效率,這個直接給出1970年1月1日到新紀元所流經的天數常量。
?? ?? epoch_days=719591
?? ?? #3. 從數組中提取各個時間部分值。
?? ?? year=${DATE[5]}
?? ?? month=${DATE[4]}
?? ?? day=${DATE[3]}
?? ?? hour=${DATE[2]}
?? ?? minute=${DATE[1]}
?? ?? second=${DATE[0]}
?? ?? #4. 當月份值為1或2的時候,將月份變量的值加一,否則將月份值加13,年變量的值減一,這樣做主要是因為后面的公式中取月平均天數時的需要。
?? ?? if [ $month -gt 2 ]; then
?? ? ?? ? month=$((month+1))
?? ?? else
?? ? ?? ? month=$((month+13))
?? ?? ??? year=$((year-1))
?? ?? fi
?? ?? #5. year變量參與的運算是需要考慮閏年問題的,該問題可以自行去google。
?? ?? #6. month變量參與的運算主要是考慮月平均天數。
?? ?? #7. 計算結果為當前日期距新世紀所流經的天數。
?? ?? today_days=$(((year*365)+(year/4)-(year/100)+(year/400)+(month*306001/10000)+day))
?? ?? #8. 總天數減去紀元距離新世紀的天數即可得出我們需要的天數了。
?? ?? days_since_epoch=$((today_days-epoch_days))
?? ?? echo $days_since_epoch
?? ?? seconds_since_epoch=$(((days_since_epoch*86400)+(hour*3600)+(minute*60)+second))
?? ?? echo $seconds_since_epoch
?? ?? CTRL+D
?? ?? /> . ./test6.sh
?? ?? 15310
?? ?? 1322829080
?? ?? 需要說明的是,推薦將該腳本的內容放到一個函數中,以便于我們今后計算類似的時間數據時使用。
七、非直接引用變量:
?? ?? 在Shell中提供了三種為標準(直接)變量賦值的方式:
?? ?? 1. 直接賦值。
?? ?? 2. 存儲一個命令的輸出。
?? ?? 3. 存儲某類型計算的結果。
?? ?? 然而這三種方式都是給已知變量名的變量賦值,如name=Stephen。但是在有些情況下,變量名本身就是動態的,需要依照運行的結果來構造變量名,之后才是為該變量賦值。這種變量被成為動態變量,或非直接變量。
?? ?? /> cat > test7.sh
?? ?? #!/bin/sh
?? ?? work_dir=`pwd`
?? ?? #1. 由于變量名中不能存在反斜杠,因此這里需要將其替換為下劃線。
?? ?? #2. work_dir和file_count兩個變量的變量值用于構建動態變量的變量名。
?? ?? work_dir=`echo $work_dir | sed 's/\//_/g'`
?? ?? file_count=`ls | wc -l`
?? ?? #3. 輸出work_dir和file_count兩個變量的值,以便確認這里的輸出結果和后面構建的命令名一致。
?? ?? echo "work_dir = " $work_dir
?? ?? echo "file_count = " $file_count
? ? ? #4. 通過eval命令進行評估,將變量名展開,如${work_dir}和$file_count,并用其值將其替換,如果不使用eval命令,將不會完成這些展開和替換的操作。最后為動態變量賦值。
? ? ? eval BASE${work_dir}_$file_count=$(ls $(pwd) | wc -l)
? ? ? #5. 先將echo命令后面用雙引號擴住的部分進行展開和替換,由于是在雙引號內,僅完成展開和替換操作即可。
? ? ? #6. echo命令后面的參數部分,先進行展開和替換,使其成為$BASE_root_test_1動態變量,之后在用該變量的值替換該變量本身作為結果輸出。
? ? ? eval echo "BASE${work_dir}_$file_count = " '$BASE'${work_dir}_$file_count
? ? ? CTRL+D
?? ?? /> . ./test7.sh
? ? ? work_dir =? _root_test
? ? ? file_count =? 1
? ? ? BASE_root_test_1 = 1
???
八、在循環中使用管道的技巧:
?? ?? 在Bash Shell中,管道的最后一個命令都是在子Shell中執行的。這意味著在子Shell中賦值的變量對父Shell是無效的。所以當我們將管道輸出傳送到一個循環結構,填入隨后將要使用的變量,那么就會產生很多問題。一旦循環完成,其所依賴的變量就不存在了。
? ? ? /> cat > test8_1.sh
?? ?? #!/bin/sh
?? ?? #1. 先將ls -l命令的結果通過管道傳給grep命令作為管道輸入。
?? ?? #2. grep命令過濾掉包含total的行,之后再通過管道將數據傳給while循環。
?? ?? #3. while read line命令從grep的輸出中讀取數據。注意,while是管道的最后一個命令,將在子Shell中運行。
?? ?? ls -l | grep -v total | while read line
?? ?? do
?? ? ?? ? #4. all變量是在while塊內聲明并賦值的。
??? ?? ?? all="$all $line"
?? ?? ??? echo $line
? ? ? done
? ? ? #5. 由于上面的all變量在while內聲明并初始化,而while內的命令都是在子Shell中運行,包括all變量的賦值,因此該變量的值將不會傳遞到while塊外,因為塊外地命令是它的父Shell中執行。
? ? ? echo "all = " $all
? ? ? CTRL+D
? ? ? /> ./test8_1.sh
? ? ? -rw-r--r--.? 1 root root 193 Nov 24 11:25 outfile
? ? ? -rwxr-xr-x. 1 root root 284 Nov 24 10:01 test7.sh
? ? ? -rwxr-xr-x. 1 root root 108 Nov 24 12:48 test8_1.sh
? ? ? all =
? ? ? 為了解決該問題,我們可以將while之前的命令結果先輸出到一個臨時文件,之后再將該臨時文件作為while的重定向輸入,這樣while內部和外部的命令都將在同一個Shell內完成。
? ? ? /> cat > test8_2.sh
? ? ? #!/bin/sh
? ? ? #1. 這里我們已經將命令的結果重定向到一個臨時文件中。
? ? ? ls -l | grep -v total > outfile
? ? ? while read line
? ? ? do
? ?? ???? #2. all變量是在while塊內聲明并賦值的。
? ?? ???? all="$all $line"
?? ?? ??? echo $line
?? ?? ??? #3. 通過重定向輸入的方式,將臨時文件中的內容傳遞給while循環。
?? ?? done < outfile
? ? ? #4. 刪除該臨時文件。
? ? ? rm -f outfile
? ? ? #5. 在while塊內聲明和賦值的all變量,其值在循環外部仍然有效。
? ? ? echo "all = " $all
? ? ? CTRL+D
? ? ? /> ./test8_2.sh
? ? ? -rw-r--r--.? 1 root root?? 0 Nov 24 12:58 outfile
?? ?? -rwxr-xr-x. 1 root root 284 Nov 24 10:01 test7.sh
?? ?? -rwxr-xr-x. 1 root root 140 Nov 24 12:58 test8_2.sh
? ? ? all =? -rwxr-xr-x. 1 root root 284 Nov 24 10:01 test7.sh -rwxr-xr-x. 1 root root 135 Nov 24 13:16 test8_2.sh
?? ?? 上面的方法只是解決了該問題,然而卻帶來了一些新問題,比如臨時文件的產生容易導致性能問題,以及在腳本異常退出時未能及時刪除當前使用的臨時文件,從而導致生成過多的垃圾文件等。下面將再介紹一種方法,該方法將同時解決以上兩種方法同時存在的問題。該方法是通過HERE-Document的方式來替代之前的臨時文件方法。
?? ?? /> cat > test8_3.sh
? ? ? #!/bin/sh
?? ?? #1. 將命令的結果傳給一個變量?? ?
?? ?? OUTFILE=`ls -l | grep -v total`
?? ?? while read line
? ? ? do
? ?? ???? all="$all $line"
? ?? ???? echo $line
? ? ? done <<EOF
? ? ? #2. 將該變量作為該循環的HERE文檔輸入。
? ? ? $OUTFILE
? ? ? EOF
? ? ? #3. 在循環外部輸出循環內聲明并初始化的變量all的值。
? ? ? echo "all = " $all
? ? ? CTRL+D
? ? ? /> ./test8_3.sh
? ? ? -rwxr-xr-x. 1 root root 284 Nov 24 10:01 test7.sh
? ? ? -rwxr-xr-x. 1 root root 135 Nov 24 13:16 test8_3.sh
? ? ? all =? -rwxr-xr-x. 1 root root 284 Nov 24 10:01 test7.sh -rwxr-xr-x. 1 root root 135 Nov 24 13:16 test8_3.sh
???
九、自鏈接腳本:
?? ?? 通常而言,我們是通過腳本的命令行選項來確定腳本的不同行為,告訴它該如何操作。這里我們將介紹另外一種方式來完成類似的功能,即通過腳本的軟連接名來幫助腳本決定其行為。
?? ?? /> cat > test9.sh
?? ?? #!/bin/sh
? ? ? #1. basename命令將剝離腳本的目錄信息,只保留腳本名,從而確保在相對路徑的模式下執行也沒有任何差異。
? ? ? #2. 通過sed命令過濾掉腳本的擴展名。
? ? ? dowhat=`basename $0 | sed 's/\.sh//'`
? ? ? #3. 這里的case語句只是為了演示方便,因此模擬了應用場景,在實際應用中,可以為不同的分支執行不同的操作,或將某些變量初始化為不同的值和狀態。
? ? ? case $dowhat in
? ? ? test9)
?? ?? ??? echo "I am test9.sh"
?? ?? ??? ;;
?? ?? test9_1)
?? ? ???? echo "I am test9_1.sh."
?? ? ? ?? ;;
?? ?? test9_2)
?? ?????? echo "I am test9_2.sh."
?? ?????? ;;
?? ?? *)
?? ?????? echo "You are illegal link file."
?? ?????? ;;
?? ?? esac
?? ?? CTRL+D
?? ?? /> chmod a+x test9.sh
?? ?? /> ln -s test9.sh test9_1.sh
?? ?? /> ln -s test9.sh test9_2.sh
?? ?? /> ls -l
?? ?? lrwxrwxrwx. 1 root root?? 8 Nov 24 14:32 test9_1.sh -> test9.sh
?? ?? lrwxrwxrwx. 1 root root?? 8 Nov 24 14:32 test9_2.sh -> test9.sh
? ? ? -rwxr-xr-x. 1 root root 235 Nov 24 14:35 test9.sh
? ? ? /> ./test9.sh
? ? ? I am test9.sh.
? ? ? /> ./test9_1.sh
? ? ? I am test9_1.sh.
? ? ? /> ./test9_2.sh
?? ?? I am test9_2.sh.
十、Here文檔的使用技巧:
?? ?? 在命令行交互模式下,我們通常希望能夠直接輸入更多的信息,以便當前的命令能夠完成一定的自動化任務,特別是對于那些支持自定義腳本的命令來說,我們可以將腳本作為輸入的一部分傳遞給該命令,以使其完成該自動化任務。
?? ?? #1. 通過sqlplus以dba的身份登錄Oracle數據庫服務器。
?? ?? #2. 在通過登錄后,立即在sqlplus中執行oracle的腳本CreateMyTables和CreateMyViews。
?? ?? #3. 最后執行sqlplus的退出命令,退出sqlplus。自動化工作完成。
? ? ? /> sqlplus "/as sysdba" <<-SQL
? ? ? > @CreateMyTables
?? ?? > @CreateMyViews
?? ?? > exit
?? ?? > SQL
??? ????
十一、獲取進程的運行時長(單位: 分鐘):
?? ?? 在進程監控腳本中,我們通常需要根據腳本的參數來確定有哪些性能參數將被收集,當這些性能參數大于最高閾值或小于最低閾值時,監控腳本將根據實際的情況,采取預置的措施,如郵件通知、直接殺死進程等,這里我們給出的例子是收集進程運行時長性能參數。
?? ?? ps命令的etime值將給出每個進程的運行時長,其格式主要為以下三種:
?? ?? 1. minutes:seconds,如20:30
?? ?? 2. hours:minutes:seconds,如1:20:30
?? ?? 3. days-hours:minute:seconds,如2-18:20:30
?? ?? 該腳本將會同時處理這三種格式的時間信息,并最終轉換為進程所流經的分鐘數。
?? ?? /> cat > test11.sh
?? ?? #!/bin/sh
?? ?? #1. 通過ps命令獲取所有進程的pid、etime和comm數據。
?? ?? #2. 再通過grep命令過濾,只獲取init進程的數據記錄,這里我們可以根據需要替換為自己想要監控的進程名。
?? ?? #3. 輸出結果通常為:1 09:42:09 init
?? ?? pid_string=`ps -eo pid,etime,comm | grep "init" | grep -v grep`
?? ?? #3. 從這一條記錄信息中抽取出etime數據,即第二列的值09:42:09,并賦值給exec_time變量。
? ? ? exec_time=`echo $pid_string | awk '{print $2}'`
? ? ? #4. 獲取exec_time變量的時間組成部分的數量,這里是3個部分,即時:分:秒,是上述格式中的第二種。
?? ?? time_field_count=`echo $exec_time | awk -F: '{print NF}'`
? ? ? #5. 從exec_time變量中直接提取分鐘數,即倒數第二列的數據(42)。
? ? ? count_of_minutes=`echo $exec_time | awk -F: '{print $(NF-1)}'`
?? ?
?? ?? #6. 判斷當前exec_time變量存儲的時間數據是屬于以上哪種格式。
?? ?? #7. 如果是第一種,那么天數和小時數均為0。
?? ?? #8. 如果是后兩種之一,則需要繼續判斷到底是第一種還是第二種,如果是第二種,其小時部分將不存在橫線(-)分隔符分隔天數和小時數,否則需要將這兩個時間字段繼續拆分,以獲取具體的天數和小時數。對于第二種,天數為0.
?? ?? if [ $time_field_count -lt 3 ]; then
?? ?? ??? count_of_hours=0
?? ?? ??? count_of_days=0
? ? ? else
??? ?? ?? count_of_hours=`echo $exec_time | awk -F: '{print $(NF-2)}'`
??? ?? ?? fields=`echo $count_of_hours | awk -F- '{print NF}'`
??? ?? ?? if [ $fields -ne 1 ]; then
???? ?? ????? count_of_days=`echo $count_of_hours | awk -F- '{print $1}'`
???? ?? ????? count_of_hours=`echo $count_of_hours | awk -F- '{print $2}'`
????? ? ? else
?????? ?? ??? count_of_days=0
????? ? ? fi
? ? ? fi
?? ?? #9. 通過之前代碼獲取的各個字段值,計算出該進程實際所流經的分鐘數。
? ? ? #10. bc命令是計算器命令,可以將echo輸出的數學表達式計算為最終的數字值。
? ? ? elapsed_minutes=`echo "$count_of_days*1440+$count_of_hours*60+$count_of_minutes" | bc`
? ? ? echo "The elapsed minutes of init process is" $elapsed_minutes "minutes."
??? ? CTRL+D
? ? ? /> ./test11.sh
? ? ? The elapsed minutes of init process is 577 minutes.
???
十二、模擬簡單的top命令:
?? ?
?? ?? 這里用腳本實現了一個極為簡單的top命令。為了演示方便,我們在腳本中將很多參數都寫成硬代碼,你可以根據需要更換這些參數,或者用更為靈活的方式替換現有的實現。
?? ?? /> cat > test12.sh
?? ?? #!/bin/sh
?? ?? #1. 將ps命令的title賦值給一個變量,這樣在每次輸出時,直接打印該變量即可。
?? ?? header=`ps aux | head -n 1`
? ? ? #2. 這里是一個無限循環,等價于while true
?? ?? #3. 每次循環先清屏,之后打印uptime命令的輸出。
?? ?? #4. 輸出ps的title。
?? ?? #5. 這里需要用sed命令刪除ps的title行,以避免其參與sort命令的排序。
?? ?? #6. sort先基于CPU%倒排,再基于owner排序,最后基于pid排序,最后再將結果輸出給head命令,僅顯示前20行的數據。
?? ?? #7. 每次等待5秒后刷新一次。
? ?? while :
?? ?? do
????? ? ? clear
????????? uptime
????????? echo "$header"
????????? ps aux | sed -e 1d | sort -k3nr -k1,1 -k2n | head -n 20
????????? sleep 5
?? ?? done
?? ?? CTRL+D?? ?
?? ?? /> ./test12.sh
??? ? 21:55:07 up 13:42,? 2 users,? load average: 0.00, 0.00, 0.00
?? ?? USER?????? PID %CPU %MEM??? VSZ?? RSS?? TTY????? STAT START?? TIME?? COMMAND
?? ?? root????? 6408???? 2.0????? 0.0?? 4740?? 932?? pts/2??? R+??? 21:45???? 0:00?? ps aux
?? ?? root????? 1755???? 0.2????? 2.0? 96976 21260?? ???????? S????? 08:14???? 2:08?? nautilus
?? ?? 68??????? 1195???? 0.0????? 0.4?? 6940?? 4416??? ???????? Ss??? 08:13???? 0:00?? hald
?? ?? postfix?? 1399??? 0.0????? 0.2? 10312? 2120??? ???????? S????? 08:13???? 0:00?? qmgr -l -t fifo -u
?? ?? postfix?? 6021??? 0.0????? 0.2? 10244? 2080??? ? ?????? S????? 21:33???? 0:00?? pickup -l -t fifo -u
?? ?? root???????? 1?????? 0.0????? 0.1?? 2828?? 1364??? ???????? Ss???? 08:12??? 0:02 ? /sbin/init
?? ?? ... ...
十三、格式化輸出指定用戶的當前運行進程:
?? ?? 在這個例子中,我們通過腳本參數的形式,將用戶列表傳遞給該腳本,腳本在讀取參數后,以樹的形式將用戶列表中用戶的所屬進程打印出來。
?? ?? /> cat > test13.sh
?? ?? #!/bin/sh
? ? ? #1. 循環讀取腳本參數,構造egrep可以識別的用戶列表變量(基于grep的擴展正則表達式)。
?? ?? #2. userlist變量尚未賦值,則直接使用第一個參數為它賦值。
? ??? #3. 如果已經賦值,且腳本參數中存在多個用戶,這里需要在每個用戶名之間加一個豎線,在egrep中,豎線是分割的元素之間是或的關系。
? ? ? #4. shift命令向左移動一個腳本的位置參數,這樣可以使循環中始終操作第一個參數。
? ??? while [ $# -gt 0 ]
? ? ? do
? ?? ???? if [ -z "$userlist" ]; then
?? ?? ??????? userlist="$1"
?? ?? ??? else
??? ?? ?????? userlist="$userlist|$1"
???? ?? ? fi
????? ?? ? shift
?? ?? done
?? ?? #5. 如果沒有用戶列表,則搜索所有用戶的進程。
? ? ? #6. "^ *($userlist) ": 下面的調用方式,該正則的展開形式為"^ *(root|avahi|postfix|rpc|dbus) "。其含義為,以0個或多個空格開頭,之后將是root、avahi、postfix、rpc或dbus之中的任何一個字符串,后面再跟隨一個空格。
? ? ? if [ -z "$userlist" ]; then
?? ?? ??? userlist="."
? ? ? else
? ?? ???? userlist="^ *($userlist) "
? ? ? fi
? ? ? #7. ps命令輸出所有進程的user和命令信息,將結果傳遞給sed命令,sed將刪除ps的title部分。
?? ?? #8. egrep過濾所有進程記錄中,包含指定用戶列表的進程記錄,再將過濾后的結果傳遞給sort命令。
?? ?? #9. sort命令中的-b選項將忽略前置空格,并以user,再以進程名排序,將結果傳遞個uniq命令。
?? ?? #10.uniq命令將合并重復記錄,-c選項將會使每條記錄前加重復的行數。
?? ?? #11.第二個sort將再做一次排序,先以user,再以重復計數由大到小,最后以進程名排序。將結果傳給awk命令。
?? ?? #12.awk命令將數據進行格式化,并刪除重復的user。
?? ?? ps -eo user,comm | sed -e 1d | egrep "$userlist" |
???? ?? ? sort -b -k1,1 -k2,2 | uniq -c | sort -b -k2,2 -k1nr,1 -k3,3 |
????? ?? ???? awk ' { user = (lastuser == $2) ? " " : $2;
?????? ? ?? ??????????? lastuser = $2;
?????????? ? ?? ??????? printf("%-15s\t%2d\t%s\n",user,$1,$3)
????????????? }'
?? ?? CTRL+D
?? ?? /> ./test13.sh root avahi postfix rpc dbus
?? ?? avahi???????????? 2????? avahi-daemon
?? ?? dbus???????????? 1????? dbus-daemon
?? ?? postfix????????? 1????? pickup
?? ? ?? ? ??????????????? 1????? qmgr
?? ?? root??? ? ??????? 5????? mingetty
?? ???????????? ? ?? ???? 3????? udevd
?? ???????????????? ? ??? 2????? sort
?? ?????????????????????? 2????? sshd
?? ?? ... ...
?? ?? rpc?? ? ????????? 1????? rpcbind
十四、用腳本完成which命令的基本功能:
????? 我們經常會在腳本中調用其他的應用程序,為了保證腳本具有更好的健壯性,以及錯誤提示的準確性,我們可能需要在執行前驗證該命令是否存在,或者說是否可以被執行。這首先要確認該命令是否位于PATH變量包含的目錄中,再有就是該文件是否為可執行文件。
?? ?? /> cat > test14.sh
?? ?? #!/bin/sh
? ? ? #1. 該函數用于判斷參數1中的命令是否位于參數2所包含的目錄列表中。需要說明的是,函數里面的$1和$2是指函數的參數,而不是腳本的參數,后面也是如此。
? ? ? #2. cmd=$1和path=$2,將參數賦給有意義的變量名,是一個很好的習慣。
? ? ? #3. 由于PATH環境變量中,目錄之間的分隔符是冒號,因此這里需要臨時將IFS設置為冒號,函數結束后再還原。
?? ?? #4. 在for循環中,逐個變量目錄列表中的目錄,以判斷該命令是否存在,且為可執行程序。
? ? ? isInPath() {
?? ?? ??? cmd=$1??????? path=$2????? result=1
???? ?? ? oldIFS=$IFS?? IFS=":"
??? ?? ?? for dir in $path
??? ?? ?? do
????? ?? ???? if [ -x $dir/$cmd ]; then
??????? ?? ?????? result=0
???????? ?? ? fi
????????? done
????? ? ? IFS=oldifs
????? ? ? return $result
?? ?? }
? ? ? #5. 檢查命令是否存在的主功能函數,先判斷是否為絕對路徑,即$var變量的第一個字符是否為/,如果是,再判斷它是否有可執行權限。
? ? ? #6. 如果不是絕對路徑,通過isInPath函數判斷是否該命令在PATH環境變量指定的目錄中。
? ? ? checkCommand() {
??? ?? ?? var=$1
???? ?? ? if [ ! -z "$var" ]; then
????? ?? ???? if [ "${var:0:1}" = "/" ]; then
?????????? ?? ??? if [ ! -x $var ]; then
?????????????? ?? ??? return 1
???????????? ?? ? fi
?????????? ?? elif ! isInPath $var $PATH ; then
?????????? ?? ??? return 2
?????????? ?? fi
????????? fi
?? ?? }
?? ?? #7. 腳本參數的合法性驗證。
? ? ? if [ $# -ne 1 ]; then
? ?? ???? echo "Usage: $0 command" >&2;
? ? ? fi
?? ?? #8. 根據返回值打印不同的信息。我們可以在這里根據我們的需求完成不同的工作。
?? ?? checkCommand $1
? ? ? case $? in
?? ?? 0) echo "$1 found in PATH." ;;
? ? ? 1) echo "$1 not found or not executable." ;;
? ? ? 2) echo "$1 not found in PATH." ;;
? ? ? esac
? ? ? exit 0
? ? ? CTRL+D
? ? ? /> ./test14.sh echo
?? ?? echo found in PATH.
?? ?? /> ./test14.sh MyTest
?? ?? MyTest not found in PATH.
?? ?? /> ./test14.sh /bin/MyTest
? ? ? /bin/MyTest not found or not executable.
十五、驗證輸入信息是否合法:
? ? ? 這里給出的例子是驗證用戶輸入的信息是否都是數字和字母。需要說明的是,之所以將其收集到該系列中,主要是因為它實現的方式比較巧妙。
?? ?? /> cat > test15.sh
?? ?? #!/bin/sh
?? ?? echo -n "Enter your input: "
?? ?? read input
?? ?? #1. 事實上,這里的巧妙之處就是先用sed替換了非法部分,之后再將替換后的結果與原字符串比較。這種寫法也比較容易擴展。?? ?
?? ?? parsed_input=`echo $input | sed 's/[^[:alnum:]]//g'`
?? ?? if [ "$parsed_input" != "$input" ]; then
?? ? ???? echo "Your input must consist of only letters and numbers."
?? ?? else
?? ?????? echo "Input is OK."
?? ?? fi
?? ?? CTRL+D
?? ?? /> ./test15.sh
?? ?? Enter your input: hello123
?? ?? Input is OK.
?? ?? /> ./test15.sh
?? ?? Enter your input: hello world
?? ?? Your input must consist of only letters and numbers.
十六、整數驗證:
? ? ? 整數的重要特征就是只是包含數字0到9和負號(-)。
?? ?? /> cat > test16.sh
?? ?? #!/bin/sh
?? ?? #1. 判斷變量number的第一個字符是否為負號(-),如果只是則刪除該負號,并將刪除后的結果賦值給left_number變量。
?? ?? #2. "${number#-}"的具體含義,可以參考該系列博客中"Linux Shell常用技巧(十一)",搜索關鍵字"變量模式匹配運算符"即可。
?? ?? number=$1
?? ?? if [ "${number:0:1}" = "-" ]; then
?? ? ???? left_number="${number#-}"
?? ?? else
?? ?????? left_number=$number
?? ?? fi
?? ?? #3. 將left_number變量中所有的數字都替換掉,因此如果返回的字符串變量為空,則表示left_number所包含的字符均為數字。
?? ?? nodigits=`echo $left_number | sed 's/[[:digit:]]//g'`
?? ?? if [ "$nodigits" != "" ]; then
?? ?????? echo "Invalid number format!"
?? ?? else
?? ?????? echo "You are valid number."
?? ?? fi
?? ?? CTRL+D
?? ?? /> ./test16.sh -123
?? ?? You are valid number.
?? ?? /> ./test16.sh 123e
?? ?? Invalid number format!
???
十七、判斷指定的年份是否為閏年:
?? ?? 這里我們先列出閏年的規則:
?? ?? 1. 不能被4整除的年一定不是閏年;
?? ?? 2. 可以同時整除4和400的年一定是閏年;
?? ?? 3. 可以整除4和100,但是不能整除400的年,不是閏年;
?? ?? 4. 其他可以整除的年都是閏年。
?? ?? #!/bin/sh???
?? ?? year=$1
? ? ? if [ "$((year % 4))" -ne 0 ]; then
??? ?? ?? echo "This is not a leap year."
????? ? ? exit 1
?? ?? elif [ "$((year % 400))" -eq 0 ]; then
???? ?? ? echo "This is a leap year."
?????? ?? exit 0
?? ?? elif [ "$((year % 100))" -eq 0 ]; then
????? ? ? echo "This is not a leap year."
????????? exit 1
?? ?? else
????? ? ? echo "This is a leap year."
????????? exit 0
?? ?? fi
?? ?? CTRL+D
?? ?? /> ./test17.sh 1933
?? ?? This is not a leap year.
?? ?? /> ./test17.sh 1936
?? ?? This is a leap year.
?? ??? ????
十八、將單列顯示轉換為多列顯示:
????? 我們經常會在顯示時將單行的輸出,格式化為多行的輸出,通常情況下,為了完成該操作,我們將加入更多的代碼,將輸出的結果存入數組或臨時文件,之后再重新遍歷它們,從而實現單行轉多行的目的。在這里我們介紹一個使用xargs命令的技巧,可以用更簡單、更高效的方式來完成該功能。???
? ? ? /> cat > test18.sh
? ? ? #!/bin/sh
?? ?? #1. passwd文件中,有可能在一行內出現一個或者多個空格字符,因此在直接使用cat命令的結果時,for循環會被空格字符切開,從而導致一行的文本被當做多次for循環的輸入,這樣我們不得不在sed命令中,將cat輸出的每行文本進行全局替換,將空格字符替換為%20。事實上,我們當然可以將cat /etc/passwd的輸出以管道的形式傳遞給cut命令,這里之所以這樣寫,主要是為了演示一旦出現類似的問題該如果巧妙的處理。
? ? ? #2. 這里將for循環的輸出以管道的形式傳遞給sort命令,sort命令將基于user排序。
?? ?? #3. -xargs -n 2是這個技巧的重點,它將sort的輸出進行合并,-n選項后面的數字參數將提示xargs命令將多少次輸出合并為一次輸出,并傳遞給其后面的命令。在本例中,xargs會將從sort得到的每兩行數據合并為一行,中間用空格符分離,之后再將合并后的數據傳遞給后面的awk命令。事實上,對于awk而言,你也可以簡單的認為xargs減少了對它(awk)的一半調用。
? ? ? #4. 如果打算在一行內顯示3行或更多的行,可以將-n后面的數字修改為3或其它更高的數字。你還可以修改awk中的print命令,使用更為復雜打印輸出命令,以得到更為可讀的輸出效果。
? ? ? for line in `cat /etc/passwd | sed 's/ /%20/g'`
? ? ? do
?? ?? ??? user=`echo $line | cut -d: -f1`
???? ?? ? echo $user
? ? ? done | \
?? ?? ??? sort -k1,1 | \
?? ?? ??? xargs -n 2 | \
????? ? ? awk '{print $1, $2}'
?? ?? CTRL+D
?? ?? /> ./test18.sh
? ? ? abrt adm
?? ?? apache avahi
?? ?? avahi-autoipd bin
?? ?? daemon daihw
?? ?? dbus ftp
?? ?? games gdm
?? ?? gopher haldaemon
?? ?? halt lp
?? ?? mail nobody
?? ?? ntp operator
?? ?? postfix pulse
?? ?? root rtkit
?? ?? saslauth shutdown
?? ?? sshd sync
?? ?? tcpdump usbmuxd
?? ?? uucp vcsa
十九、將文件的輸出格式化為指定的寬度:
?? ?? 在這個技巧中,不僅包含了如何獲取和文件相關的詳細信息,如行數,字符等,而且還可以讓文件按照指定的寬度輸出。這種應用在輸出幫助信息、License相關信息時還是比較有用的。
?? ?? /> cat > test19.sh
?? ?? #!/bin/sh
?? ?? #1. 這里我們將缺省寬度設置為75,如果超過該寬度,將考慮折行顯示,否則直接在一行中全部打印輸出。這里只是為了演示方便,事實上,你完全可以將該值作為腳本或函數的參數傳入,那樣你將會得到更高的靈活性。?? ?
? ? ? my_width=75
? ? ? #2. for循環的讀取列表來自于腳本的參數。
? ? ? #3. 在獲取lines和chars變量時,sed命令用于過濾掉多余的空格字符。
? ? ? #4. 在if的條件判斷中${#line}用于獲取line變量的字符長度,這是Shell內置的規則。
?? ?? #5. fmt -w 80命令會將echo輸出的整行數據根據其命令選項指定的寬度(80個字符)進行折行顯示,再將折行后的數據以多行的形式傳遞給sed命令。
?? ?? #6. sed在收到fmt命令的格式化輸出后,將會在折行后的第一行頭部添加兩個空格,在其余行的頭部添加一個加號和一個空格以表示差別。
?? ?? for input; do
????? ? ? lines=`wc -l < $input | sed 's/ //g'`
????????? chars=`wc -c < $input | sed 's/ //g'`
????????? owner=`ls -l $input | awk '{print $3}'`
????????? echo "-------------------------------------------------------------------------------"
????????? echo "File $input ($lines lines, $chars characters, owned by $owner):"
????????? echo "-------------------------------------------------------------------------------"
????????? while read line; do
????????????? if [ ${#line} -gt $my_width ]; then
????????????? ? ? echo "$line" | fmt -w 80 | sed -e '1s/^/? /' -e '2,$s/^/+ /'
????????????? else
???????????? ?? ? echo "? $line"
????????????? fi
?????? ?? done < $input
????????? echo "-------------------------------------------------------------------------------"
?? ?? done | more
?? ?? CTRL+D
?? ?? /> ./test19.sh testfile
?? ?? -------------------------------------------------------------------------------
?? ?? File testfile.dat (3 lines, 645 characters, owned by root):
?? ?? -------------------------------------------------------------------------------
?? ? ??? The PostgreSQL Global Development Group today released updates for all
?? ?? + active branches of the PostgreSQL object-relational database system,
?? ?? + including versions 9.1.2, 9.0.6, 8.4.10, 8.3.17 and 8.2.23. Users of any of
?? ?? + the several affected features in this release, including binary replication,
?? ?? + should update their PostgreSQL installations as soon as possible.
?? ? ??? This is also the last update for PostgreSQL 8.2, which is now End-Of-Life
?? ?? + (EOL). Users of version 8.2 should plan to upgrade their PostgreSQL
?? ?? + installations to 8.3 or later within the next couple of months. For more
?? ?? + information, see our Release Support Policy.
?? ? ? ? This is just a test file.
?? ?? -------------------------------------------------------------------------------
?? ????
二十、監控指定目錄下磁盤使用空間過大的用戶:
?? ?? 在將Linux用作文件服務器時,所有的注冊用戶都可以在自己的主目錄下存放各種類型和大小的文件。有的時候,有些用戶的占用空間可能會明顯超過其他人,這時就需要管理員可以及時發現這一異常使用狀況,并根據實際情況作出應對處理。
? ? ? /> cat > test20.sh
? ? ? #!/bin/sh
? ? ? #1. 該腳本僅用于演示一種處理技巧,其中很多閾值都是可以通過腳本參來初始化的,如limited_qutoa和dirs等變量。
? ? ? limited_quota=200
? ? ? dirs="/home /usr /var"
?? ?? #2. 以冒號作為分隔符,截取passwd文件的第一和第三字段,然后將輸出傳遞給awk命令。
?? ?? #3. awk中的$2表示的是uid,其中1-99是系統保留用戶,>=100的uid才是我們自己創建的用戶,awk通過print輸出所有的用戶名給for循環。
?? ?? #4. 注意echo命令的輸出是由八個單詞構成,同時由于-n選項,echo命令并不輸出換行符。
?? ?? #5. 之所以使用find命令,也是為了考慮以點(DOT)開頭的隱藏文件。這里的find將在指定目錄列表內,搜索指定用戶的,類型為普通文件的文件。并通過-ls選項輸出找到文件的詳細信息。其中輸出的詳細信息的第七列為文件大小列。
?? ?? #6. 通過awk命令累加find輸出的第七列,最后再在自己的END塊中將sum的值用MB計算并輸出。該命令的輸出將會與上面echo命令的輸出合并作為for循環的輸出傳遞給后面的awk命令。這里需要指出的是,該awk的輸出就是后面awk命令的$9,因為echo僅僅輸出的8個單詞。
?? ?? #7. 從for循環管道獲取數據的awk命令,由于awk命令執行的動作是用雙引號括起的,所以表示域字段的變量的前綴$符號,需要用\進行轉義。變量$limited_quota變量將會自動完成命令替換,從而構成該awk命令的最終動作參數。
?? ?? for name in `cut -d: -f1,3 /etc/passwd | awk -F: '$2 > 99 {print $1}'`
? ? ? do
?? ?????? echo -n "User $name exceeds disk quota. Disk Usage is: "
?? ?????? find $dirs -user $name -type f -ls |\
?? ???????????? awk '{ sum += $7 } END { print sum / (1024*1024) " MB" }'
?? ?? done | awk "\$9 > $limited_quota { print \$0 }"
?? ?? CTRL+D
?? ?? /> ./test20.sh?? ?
二十一、編寫一個更具可讀性的df命令輸出腳本:
?? ?? 這里我們將以awk腳本的方式來實現df -h的功能。
? ? ? /> cat > test21.sh
?? ?? #!/bin/sh
?? ?? #1. $$表示當前Shell進程的pid。?? ?
?? ?? #2. trap信號捕捉是為了保證在Shell正常或異常退出時,仍然能夠將該腳本創建的臨時awk腳本文件刪除。
?? ?? awk_script_file="/tmp/scf_tmp.$$"
?? ?? trap "rm -f $awk_script_file" EXIT
?? ?? #3. 首先需要說明的是,'EOF'中的單引號非常重要,如果忽略他將無法通過編譯,這是因為awk的命令動作必須要用單引號擴住。
?? ?? #4. awk腳本的show函數中,int(mb * 100) / 100這個技巧是為了保證輸出時保留小數點后兩位。
?? ?? cat << 'EOF' > $awk_script_file
?? ?? function show(size) {
?? ? ???? mb = size / 1024;
?? ?????? int_mb = (int(mb * 100)) / 100;
?? ?????? gb = mb / 1024;
?? ?????? int_gb = (int(gb * 100)) / 100;
?? ?????? if (substr(size,1,1) !~ "[0-9]" || substr(size,2,1) !~ "[0-9]") {
????????????? return size;
?? ?????? } else if (mb < 1) {
?? ?????????? return size "K";
?? ?????? } else if (gb < 1) {
?? ?????????? return int_mb "M";
?? ?????? } else {
?? ?????????? return int_gb "G";
?? ?????? }
?? ?? }
?? ?? #5. 在BEGIN塊中打印重定義的輸出頭信息。
?? ?? BEGIN {
?? ???????? printf "%-20s %7s %7s %7s %8s %s\n","FileSystem","Size","Used","Avail","Use%","Mounted"
?? ?? }
?? ?? #6. !/Filesystem/ 表示過濾掉包含Filesystem的行,即df輸出的第一行。其余行中,有個域字段可以直接使用df的輸出,有的需要通過show函數的計算,以得到更為可讀的顯示結果。
?? ?? !/Filesystem/ {
?? ?????? size = show($2);
?? ?????? used = show($3);
?? ?????? avail = show($4);
?? ?????? printf "%-20s %7s %7s %7s %8s %s\n",$1,size,used,avail,$5,$6
?? ?? }
?? ?? EOF
?? ?? df -k | awk -f $awk_script_file
????? CTRL+D
?? ?? /> ./test12.sh
?? ?? FileSystem????????????? Size?????? Used? ? ? Avail???? Use%?? Mounted
?? ?? /dev/sda2????????????? 3.84G??? 2.28G???? 1.36G????? 63% ? /
?? ?? tmpfs????????????? ?? 503.57M???? 100K 503.47M??????? 1%?? /dev/shm
?? ?? /dev/sda1???????????? 48.41M? 35.27M? 10.63M????? 77%?? /boot
?? ?? /dev/sda3????????????? 14.8G 171.47M? 13.88G????? ? 2%?? /home
???
二十二、編寫一個用于添加新用戶的腳本:
?? ?? 之所以在這里選擇這個腳本,沒有更多的用意,只是感覺這里的有些技巧和常識還是需要了解的,如/etc/passwd、/etc/shadow、/etc/group的文件格式等。
?? ?? /> cat > test22.sh
? ? ? #!/bin/sh
? ? ? #1. 初始化和用戶添加相關的變量。?? ?
?? ?? passwd_file="/etc/passwd"
? ? ? shadow_file="/etc/shadow"
? ? ? group_file="/etc/group"
? ? ? home_root_dir="/home"
?? ?? #2. 只有root用戶可以執行該腳本。?? ?
? ? ? if [ "$(whoami)" != "root" ]; then
?? ?? ??? echo "Error: You must be roor to run this command." >&2
??? ?? ?? exit 1
?? ?? fi
?? ?
?? ?? echo "Add new user account to $(hostname)"
? ? ? echo -n "login: "
?? ?? read login
?? ?? #3. 去唯一uid,即當前最大uid值加一。
? ? ? uid=`awk -F: '{ if (big < $3 && $3 < 5000) big = $3 } END {print big + 1}' $passwd_file`
? ? ? #4. 設定新用戶的主目錄變量
?? ?? home_dir=$home_root_dir/$login
?? ?? gid=$uid
?? ?? #5. 提示輸入和創建新用戶相關的信息,如用戶全名和主Shell。
? ? ? echo -n "full name: "
? ? ? read fullname
? ? ? echo -n "shell: "
? ? ? read shell
? ? ? #6. 將輸入的信息填充到passwd、group和shadow三個關鍵文件中。
? ? ? echo "Setting up account $login for $fullname..."
? ? ? echo ${login}:x:${uid}:${gid}:${fullname}:${home_dir}:$shell >> $passwd_file
? ? ? echo ${login}:*:11647:0:99999:7::: >> $shadow_file
? ? ? echo "${login}:x:${gid}:$login" >> $group_file
? ? ? #7. 創建主目錄,同時將新用戶的profile模板拷貝到新用戶的主目錄內。
? ? ? #8. 設定該主目錄的權限,再將其下所有文件的owner和group設置為新用戶。
? ? ? #9. 為新用戶設定密碼。
? ? ? mkdir $home_dir
? ? ? cp -R /etc/skel/.[a-zA-Z]* $home_dir
? ? ? chmod 755 $home_dir
? ? ? find $home_dir -print | xargs chown ${login}:${login}
?? ?? passwd $login
?? ?? exit 0
?? ?? CTRL+D
? ? ? /> ./test22.sh
? ? ? Add new user account to bogon
? ? ? login: stephen
? ? ? full name: Stephen Liu
?? ?? shell: /bin/shell
?? ?? Setting up account stephen for Stephen Liu...
?? ?? Changing password for user stephen.
? ? ? New password:
? ? ? Retype new password:
? ? ? passwd: all authentication tokens updated successfully.
???
二十三、kill指定用戶或指定終端的用戶進程:
?? ?? 這是一臺運行Oracle數據庫的Linux服務器,現在我們需要對Oracle做一些特殊的優化工作,由于完成此項工作需要消耗更多的系統資源,因此我們不得不殺掉一些其他用戶正在運行的進程,以便節省出更多的系統資源,讓本次優化工作能夠盡快完成。
?? ?? /> cat > test23.sh
? ? ? #!/bin/sh
? ? ? user=""
?? ?? tty=""
?? ?? #1. 通過讀取腳本的命令行選項獲取要kill的用戶或終端。-t后面的參數表示終端,-u后面的參數表示用戶。這兩個選項不能同時使用。
?? ?? #2. case中的代碼對腳本選項進行驗證,一旦失敗則退出腳本。
?? ?? while getopts u:t: opt; do
?? ?? ??? case $opt in
???? ?? ? u) if [ "$tty" != "" ]; then
?? ????????? ? ? echo "-u and -t can not be set at one time."
?? ????????????? exit 1
????????? ? ? fi
????????????? user=$OPTARG
????????????? ;;
?????? ?? t)? if [ "$user" != "" ]; then
?? ?????? ? ???? echo "-u and -t can not be set at one time."
?? ????????????? exit 1
????????????? fi
????????????? tty=$OPTARG
????????????? ;;
????????? ?) echo "Usage: $0 [-u user|-t tty]" >&2
????????????? exit 1
?????? ?? esac
?? ?? done
?? ?? #3. 如果當前選擇的是基于終端kill,就用$tty來過濾ps命令的輸出,否則就用$user來過濾ps命令的輸出。
?? ?? #4. awk命令將僅僅打印出pid字段,之后傳遞給sed命令,sed命令刪除ps命令輸出的頭信息,僅保留后面的進程pids作為輸出,并初始化pids數組。
?? ?? if [ ! -z "$tty" ]; then
????? ? ? pids=$(ps cu -t $tty | awk "{print \$2}" | sed '1d')
?? ?? else
????? ? ? pids=$(ps cu -U $user | awk "{print \$2}" | sed '1d')
?? ?? fi
?? ?? #5. 判斷數組是否為空,空則表示沒有符合要求的進程,直接退出腳本。
?? ?? if [ -z "$pids" ]; then
????? ? ? echo "No processes matches."
?????? ?? exit 1
?? ?? fi
?? ?? #6. 遍歷pids數組,逐個kill指定的進程。
?? ?? for pid in $pids; do
????? ? ? echo "Killing process[pid = $pid]... ..."
?????? ?? kill -9 $pid
?? ?? done
?? ?? exit 0
?? ?? CTRL+D
?? ?? /> ./test23.sh -t pts/1
?? ?? Killing process[pid = 11875]... ...
?? ?? Killing process[pid = 11894]... ...
?? ?? /> ./test23.sh -u stephen
?? ?? Killing process[pid = 11910]... ...
?? ?? Killing process[pid = 11923]... ...
?? ????
二十四、判斷用戶輸入(是/否)的便捷方法:
?? ?? 對于有些交互式程序,經常需要等待用戶的輸入,以便確定下一步的執行流程。通常而言,當用戶輸入"Y/y/Yes/yes"時表示確認當前的行為,而當輸入"N/n/No/no"時則表示否定當前的行為。基于這種規則,我們可以實現一個便捷確認方式,即只判斷輸入的首字母,如果為Y或y,表示確認,如為N或n,則為否定。
? ? ? /> cat > test24.sh
? ? ? #!/bin/sh
? ? ? echo -n "Please type[y/n/yes/no]: "
? ? ? read input
? ? ? #1. 先轉換小寫到大寫,再通過cut截取第一個字符。
? ? ? ret=`echo $input | tr '[a-z]' '[A-Z]' | cut -c1`
?? ?
? ? ? if [ $ret = "Y" ]; then
????????? echo "Your input is Y."
? ? ? elif [ $ret = "N" ]; then
?? ?????? echo "Your input is N."
?? ?? else
?? ?????? echo "Your input is error."
?? ?? fi
?? ?? CTRL+D
?? ?? /> ./test24.sh
?? ?? Please type[y/n/yes/no]: y
?? ?? Your input is Y.
?? ?? /> ./test24.sh
?? ?? Please type[y/n/yes/no]: n
?? ?? Your input is N.??
二十五、通過FTP下載指定的文件:
?? ?? 相比于手工調用FTP命令下載文件,該腳本提供了更為方便的操作方式。
?? ?? /> cat > test25.sh
?? ?? #!/bin/sh
?? ?? #1. 測試腳本參數數量的有效性。?? ?
?? ?? if [ $# -ne 2 ]; then
????? ??? echo "Usage: $0 ftp://... username" >&2
?????? ?? exit 1
?? ?? fi
?? ?? #2. 獲取第一個參數的前六個字符,如果不是"ftp://",則視為非法FTP URL格式。這里cut的-c選項表示按照字符的方式截取第一到第六個字符。
?? ?? header=`echo $1 | cut -c1-6`
?? ?? if [ "$header" != "ftp://" ]; then
????? ? ? echo "$0: Invalid ftp URL." >&2
????????? exit 1
?? ?? fi
?? ?? #3. 合法ftp URL的例子:ftp://ftp.myserver.com/download/test.tar
?? ?? #4. 針對上面的URL示例,cut命令通過/字符作為分隔符,這樣第三個域字段表示server(ftp.myserver.com)。
?? ?? #5. 在截取filename時,cut命令也是通過/字符作為分隔符,但是"-f4-"將獲取從第四個字段開始的后面所有字段(download/test.tar)。
?? ?? #6. 通過basename命令獲取filename的文件名部分。
?? ?? server=`echo $1 | cut -d/ -f3`
?? ?? filename=`echo $1 | cut -d/ -f4-`
?? ?? basefile=`basename $filename`
?? ?? ftpuser=$2
?? ?? #7. 這里需要調用stty -echo,以便后面的密碼輸入不會顯示,在輸入密碼之后,需要再重新打開該選項,以保證后面的輸入可以恢復顯示。
?? ?? #8. echo "",是模擬一次換換。
?? ?? echo -n "Password for $ftpuser: "
?? ?? stty -echo
?? ?? read password
?? ?? stty echo
?? ?? echo ""
?? ?? #9. 通過HERE文檔,批量執行ftp命令。
?? ?? echo ${0}: Downloading $baseile from server $server.
?? ?? ftp -n << EOF
?? ?? open $server
?? ?? user $ftpuser $password
?? ?? get $filename $basefile
?? ?? quit
?? ?? EOF
?? ?? #10.Shell內置變量$?表示上一個Shell進程的退出值,0表示成功執行,其余值均表示不同原因的失敗。
?? ?? if [ $? -eq 0 ]; then
???? ?? ? ls -l $basefile
?? ?? fi
?? ?? exit 0
?? ?? CTRL+D
?? ?? /> ./test25.sh? ftp://ftp.myserver.com/download/test.tar stephen
?? ?? Password for stephen:
?? ?? ./test25.sh: Downloading from server ftp.myserver.com.
?? ?? -rwxr-xr-x. 1 root root 678 Dec? 9 11:46 test.tar
二十六、文件鎖定:
? ? ? 在工業應用中,有些來自于工業設備的文件將會被放到指定的目錄下,由于這些文件需要再被重新格式化后才能被更高層的軟件進行處理。而此時負責處理的腳本程序極有可能是多個實例同時運行,因此這些實例之間就需要一定的同步,以避免多個實例同時操作一個文件而造成的數據不匹配等問題的發生。文件鎖定命令可以幫助我們實現這一同步邏輯。
?? ?? /> cat > test26.sh
?? ?? #!/bin/sh
?? ?? #1. 這里需要先確認flock命令是否存在。
?? ?? if [ -z $(which flock) ]; then
??? ?? ?? echo "flock doesn't exist."
??? ?? ?? exit 1
?? ?? fi
?? ?? #2. flock中的-e選項表示對該文件加排它鎖,-w選項表示如果此時文件正在被加鎖,當前的flock命令將等待20秒,如果能鎖住該文件,就繼續執行,否則退出該命令。
? ? ? #3. 這里鎖定的文件是/var/lock/lockfile1,-c選項表示,如果成功鎖定,則指定其后用雙引號括起的命令,如果是多個命令,可以用分號分隔。
? ? ? #4. 可以在兩個終端同時啟動該腳本,然后觀察腳本的輸出,以及lockfile1文件的內容。
? ? ? flock -e -w 20 /var/lock/lockfile1 -c "sleep 10;echo `date` | cat >> /var/lock/lockfile1"
? ? ? if [ $? -ne 0 ]; then
???? ?? ? echo "Fail."
?? ?? ??? exit 1
? ? ? else
?? ?? ??? echo "Success."
??? ?? ?? exit 0
?? ?? fi
?? ?? CTRL+D
???
二十七、用小文件覆蓋整個磁盤:
?? ?? 假設我們現在遇到這樣一個問題,公司的關鍵資料copy到測試服務器上了,在直接將其刪除后,仍然擔心服務器供應商可以將其恢復,即便是通過fdisk進行重新格式化,也仍然存在被恢復的風險,鑒于此,我們需要編寫一個腳本,創建很多小文件(5MB左右),之后不停在關鍵資料所在的磁盤中復制該文件,以使Linux的inode無法再被重新恢復,為了達到這里效果,我們需要先構造該文件,如:
?? ?? /> find . -name "*" > testfile
?? ?? /> ls -l testfile
?? ?? -rwxr-xr-x. 1 root root 5123678 Dec? 9 11:46 testfile
?? ?? /> cat > test27.sh
?? ?? #!/bin/sh
?? ?? #1. 初始化計數器變量,其中max的值是根據當前需要填充的磁盤空間和testfile的大小計算出來的。
?? ?? counter=0
?? ?? max=2000000
?? ?? remainder=0
?? ?? #2. 每次迭代counter變量都自增一,以保證每次生成不同的文件。當該值大于最大值時退出。
?? ?? #3. 對計數器變量counter按1000取模,這樣可以在每生成1000個文件時打印一次輸出,以便看到覆蓋的進度,輸出時間則便于預估還需要多少時間可以完成。
?? ?? #4. 創建單獨的、用于存放這些覆蓋文件的目錄。
?? ?? #5. 生成臨時文件,如果寫入失敗打印出提示信息。
?? ?? while true
?? ?? do
?? ? ?? ? ((counter=counter+1))
?? ? ?? ? if [ #counter -ge $max ]; then
?? ??? ?? ??? break
?? ??? ?? fi
?? ??? ?? ((remainder=counter%1000))
?? ??? ?? if [ $remainder -eq 0 ]; then
?? ??? ?? ??? echo -e "counter = $counter\t date = " $(date)
?? ??? ?? fi
?? ??? ?? mkdir -p /home/temp2
?? ??? ?? cat < testfile > "/home/temp/myfiles.$counter"
?? ??? ?? if [[ $? -ne 0 ]]; then
?? ??? ? ?? ? echo "Failed to wrtie file."
?? ??? ?? ? ? exit 1
?? ??? ?? fi
?? ?? done
?? ?? echo "Done"
?? ?? CTRL+D
?? ?? /> ./test27.sh
?? ?? counter = 1000?? ??? ?Fri Dec? 9 17:25:04 CST 2011
?? ?? counter = 2000?? ??? ?Fri Dec? 9 17:25:24 CST 2011
?? ?? counter = 3000?? ??? ?Fri Dec? 9 17:25:54 CST 2011
?? ?? ... ...
?? ?? 與此同時,可以通過執行下面的命令監控磁盤空間的使用率。
?? ?? /> watch -n 2 'df -h'
?? ?? Every 2.0s: df -h?????????????????????????????????????? Fri Dec? 9 17:31:56 2011
?? ?
?? ?? Filesystem??????????? Size?? Used Avail Use% Mounted on
?? ?? /dev/sda2???????????? 3.9G? 2.3G? 1.4G? 63% /
?? ?? tmpfs??????????????? ? 504M? 100K? 504M?? 1% /dev/shm
?? ?? /dev/sda1????????????? 49M?? 36M?? 11M? 77% /boot
?? ?? /dev/sda3????????????? 15G? 172M?? 14G?? 2% /home
?? ?? 我們也可以在執行的過程中通過pidstat命令監控腳本進程的每秒讀寫塊數。?? ?
?
二十八、統計當前系統中不同運行狀態的進程數量:
?? ?? 在Linux系統中,進程的運行狀態主要分為四種:運行時、睡眠、停止和僵尸。下面的腳本將統計當前系統中,各種運行狀態的進程數量。
? ? ? /> cat > test28.sh
? ? ? #!/bin/sh
? ? ? #1. 初始化計數器變量,分別對應于運行時、睡眠、停止和僵尸。
? ? ? running=0
? ? ? sleeping=0
? ? ? stopped=0
? ? ? zombie=0
?? ?? #2. 在/proc目錄下,包含很多以數字作為目錄名的子目錄,其含義為,每個數字對應于一個當前正在運行進程的pid,該子目錄下包含一些文件用于描述與該pid進程相關的信息。如1表示init進程的pid。那么其子目錄下的stat文件將包含和該進程運行狀態相關的信息。
? ? ? #3. cat /proc/1/stat,通過該方式可以查看init進程的運行狀態,同時也可以了解該文件的格式,其中第三個字段為進程的運行狀態字段。
?? ?? #4. 通過let表達式累加各個計數器。
? ? ? for pid in /proc/[1-9]*
? ? ? do
??? ?? ?? ((procs=procs+1))
??? ?? ?? stat=`awk '{print $3}' $pid/stat`
???? ?? ? case $stat in
?????? ?? ??? R) ((running=runing+1));;
????? ?? ???? S) ((sleeping=sleeping+1));;
????? ?? ???? T) ((stopped=stopped+1));;
????? ?? ???? Z) ((zombie=zombie+1));
???? ?? ? esac
?? ?? done
? ? ? echo -n "Process Count: "
? ? ? echo -e "Running = $running\tSleeping = $sleeping\tStopped = $stopped\tZombie = $zombie."
?? ?? CTRL+D
?? ?? /> ./test28.sh
?? ?? Process Count: Running = 0????? Sleeping = 136? Stopped = 0???? Zombie = 0.
???
二十九、浮點數驗證:
?? ? 浮點數數的重要特征就是只是包含數字0到9、負號(-)和點(.),其中負號只能出現在最前面,點(.)只能出現一次。
?? ?? /> cat > test29.sh
?? ?? #!/bin/sh
? ? ? #1. 之前的一個條目已經介紹了awk中match函數的功能,如果匹配返回匹配的位置值,否則返回0。
?? ?? #2. 對于Shell中的函數而言,返回0表示成功,其他值表示失敗,該語義等同于Linux中的進程退出值。調用者可以通過內置變量$?獲取返回值,或者作為條件表達式的一部分直接判斷。
? ? ? validint() {
? ?? ???? ret=`echo $1 | awk '{start = match($1,/^-?[0-9]+$/); if (start == 0) print "1"; else print "0"}'`
??? ?? ?? return $ret
?? ?? }
?? ?
? ? ? validfloat() {
???? ?? ? fvalue="$1"
????? ? ? #3. 判斷當前參數中是否包含小數點兒。如果包含則需要將其拆分為整數部分和小數部分,分別進行判斷。
????? ? ? if [ ! -z? $(echo $fvalue | sed 's/[^.]//g') ]; then
????? ?? ???? decimalpart=`echo $fvalue | cut -d. -f1`
??????? ?? ?? fractionalpart=`echo $fvalue | cut -d. -f2`
???????? ?? ? #4. 如果整數部分不為空,但是不是合法的整型,則視為非法格式。
???????? ?? ? if [ ! -z $decimalpart ]; then
????????? ?? ???? if ! validint "$decimalpart" ; then
??????????? ?? ?????? echo "decimalpart is not valid integer."
???????????? ?? ????? return 1
???????????? ?? ? fi
????????? ? ? fi
????????? ? ? #5. 判斷小數部分的第一個字符是否為-,如果是則非法。
????????? ? ? if [ "${fractionalpart:0:1}" = "-" ]; then
???????????? ?? ? echo "Invalid floating-point number: '-' not allowed after decimal point." >&2
???????????? ?? ? return 1
?????????? ?? fi
?????????? ?? #6. 如果小數部分不為空,同時也不是合法的整型,則視為非法格式。
?????????? ?? if [ "$fractionalpart" != "" ]; then
?????????? ?? ??? if ! validint "$fractionalpart" ; then
??????????? ?? ?????? echo "fractionalpart is not valid integer."
???????????? ?? ????? return 1
??????????? ?? ?? fi
????????? ? ? fi
???????? ?? ? #7. 如果整數部分僅為-,或者為空,如果此時小數部分也是空,則為非法格式。
??????? ?? ?? if [ "$decimalpart" = "-" -o -z "$decimalpart" ]; then
????????? ?? ???? if [ -z $fractionalpart ]; then
?????????? ?? ??????? echo "Invalid floating-point format." >&2
???????????? ?? ????? return 1
??????????? ?? ?? fi
???????? ?? ? fi
????? ? ? else
?????? ?? ? ? #8. 如果當前參數僅為-,則視為非法格式。
????????? ? ? if [ "$fvalue" = "-" ]; then
????????? ?? ???? echo "Invalid floating-point format." >&2
????????? ?? ???? return 1
???????? ?? ? fi
????????? ? ? #9. 由于參數中沒有小數點,如果該值不是合法的整數,則為非法格式。
??????? ?? ?? if ! validint "$fvalue" ; then
??????? ?? ?????? echo "Invalid floating-point format." >&2
??????? ?? ?????? return 1
?????? ?? ??? fi
?????? ?? fi
????? ? ? return 0
? ? ? }???
? ? ? if validfloat $1 ; then
? ?? ???? echo "$1 is a valid floating-point value."
?? ?? fi
?? ?? exit 0
?? ?? CTRL+D
? ? ? /> ./test29.sh 47895??? ??
????? 47895 is a valid floating-point value.
?? ?? /> ./test29.sh 47895.33
?? ?? 47895.33 is a valid floating-point value.
?? ?? /> ./test29.sh 47895.3e
?? ?? fractionalpart is not valid integer.
?? ?? /> ./test29.sh 4789t.34
? ? ? decimalpart is not valid integer.???
三十、統計英文文章中每個單詞出現的頻率:
?? ?
?? ?? 這個技巧的主要目的是顯示如何更好的使用awk命令的腳本。
? ? ? /> cat > test30.sh
?? ?? #!/bin/sh
?? ?? #1. 通過當前腳本的pid,生成awk腳本的臨時文件名。
?? ?? #2. 捕捉信號,在腳本退出時刪除該臨時文件,以免造成大量的垃圾臨時文件。
?? ?? awk_script_file="/tmp/scf_tmp.$$"
?? ?? trap "rm -f $awk_script_file" EXIT
?? ?? #3. while循環將以當前目錄下的testfile作為輸入并逐行讀取,在讀取到末尾時退出循環。
? ? ? #4. getline讀取到每一行將作為awk的正常輸入。在內層的for循環中,i要從1開始,因為$0表示整行。NF表示域字段的數量。
? ? ? #5. 使$i作為數組的鍵,如果$i的值匹配正則表達式"^[a-zA-Z]+$",我們將其視為單詞而不是標點。每次遇到單詞時,其鍵值都會遞增。
?? ?? #6. 最后通過awk腳本提供的特殊for循環,遍歷數組的鍵值數據。
? ? ? cat << 'EOF' > $awk_script_file
?? ?? BEGIN {
???? ?? ? while (getline < "./testfile" > 0) {
???? ?? ????? for (i = 1; i <= NF; ++i) {
?????? ?? ?? ???? if (match($i,"^[a-zA-Z]+$") != 0)
???????? ?? ???? ???? arr[$i]++
????????? ? ? }
?????? ?? }
????????? for (word in arr) {
??????? ?? ?? printf "word = %s\t count = %s\n",word,arr[word]
????????? }
?? ?? }
?? ?? EOF
?? ?? awk -f $awk_script_file
? ? ? CTRL+D
? ? ? /> cat testfile
?? ?? hello world liu liu , ,
?? ?? stephen liu , ?
? ? ? /> ./test30.sh
? ? ? word = hello????? count = 1
?? ?? word = world???? count = 1
?? ?? word = stephen count = 1
?? ?? word = liu???????? count = 3
轉載自:http://www.cnblogs.com/stephen-liu74/archive/2012/01/04/2285640.html
與50位技術專家面對面20年技術見證,附贈技術全景圖
總結
以上是生活随笔為你收集整理的Linux Shell高级技巧(一)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Linux Shell常用技巧(十二)
- 下一篇: linux shell 时间运算以及时间