1. 程式人生 > >Linux Shell高階技巧

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內完成。