返回文章列表

Shell指令碼技巧與技術探討

本文探討 Shell 指令碼中浮點數與日期驗證技巧,並示範如何利用 bc 建立任意精確度計算器,以及如何處理 echo 命令的跨平台相容性問題,提供更穩健的指令碼撰寫方法。

Shell指令碼 程式設計

在 Shell 指令碼撰寫過程中,驗證使用者輸入、處理日期和進行精確計算至關重要。本文將探討浮點數和日期的驗證方法,並示範如何利用 bc 建立任意精確度的計算器,以及如何解決 echo 命令的跨平台相容性問題,以提升 Shell 指令碼的可靠性和可維護性。這些技巧能協助開發者更有效率地處理使用者輸入、日期操作和數值計算,進而建構更穩健的 Shell 指令碼應用程式。

驗證浮點數輸入

乍看之下,在 shell 指令碼的範疇和功能內驗證浮點數(或「實數」)似乎是一項艱鉅的任務,但考慮到浮點數只是由小數點分隔的兩個整數,加上能夠在內嵌中參照不同的指令碼(validint),你會發現浮點數驗證測試其實出乎意料地簡短。清單 1-12 中的指令碼假設它與 validint 指令碼在同一目錄下執行。

程式碼

#!/bin/bash
# validfloat--測試某個數字是否為有效的浮點數值。
# 注意這個指令碼無法接受科學記號(1.304e5)。
# 為了測試輸入值是否為有效的浮點數,
# 我們需要將該值分成兩部分:整數部分和小數部分。
# 我們測試第一部分是否為有效的整數,然後測試第二部分是否為 >=0 的整數。
# 因此,-30.5 被評估為有效,但 -30.-8 則不是。
# 要將另一個 shell 指令碼納入此指令碼中,請使用 "." 符號。
# 很簡單。

. validint

validfloat()
{
    fvalue="$1"
    # 檢查輸入數字是否有小數點。
    if [ ! -z $(echo $fvalue | sed 's/[^.]//g') ] ; then
        # 提取小數點前的部分。
        decimalPart="$(echo $fvalue | cut -d. -f1)"
        # 提取小數點後的部分。
        fractionalPart="${fvalue#*\.}"
        
        # 首先測試小數部分,即小數點左邊的所有內容。
        if [ ! -z $decimalPart ] ; then
            # "!" 反轉測試邏輯,因此以下是 "如果不是有效的整數"
            if ! validint "$decimalPart" "" "" ; then
                return 1
            fi
        fi
        
        # 現在讓我們測試小數值。
        # 首先,小數點後面不能有負號,如 33.-11,因此讓我們測試小數中的 '-' 符號。
        if [ "${fractionalPart%${fractionalPart#?}}" = "-" ] ; then
            echo "無效的浮點數:小數點後不允許 '-'。" >&2
            return 1
        fi
        
        if [ "$fractionalPart" != "" ] ; then
            # 如果小數部分不是有效的整數...
            if ! validint "$fractionalPart" "0" "" ; then
                return 1
            fi
        fi
    else
        # 如果整個值只是 "-",那也不好。
        if [ "$fvalue" = "-" ] ; then
            echo "無效的浮點數格式。" >&2
            return 1
        fi
        
        # 最後,檢查剩下的數字是否實際上是有效的整數。
        if ! validint "$fvalue" "" "" ; then
            return 1
        fi
    fi
    
    return 0
}

內容解密:

  1. validfloat 函式:該函式用於驗證輸入值是否為有效的浮點數。它首先檢查輸入值是否有小數點。
  2. 提取小數部分和整數部分:使用 cut 和引數擴充套件來提取小數點前後的數字部分,分別儲存在 decimalPartfractionalPart 中。
  3. 驗證小數部分:檢查 decimalPart 是否為有效的整數,使用 validint 函式。如果不是,則傳回錯誤。
  4. 驗證小數點後的數字:檢查 fractionalPart 是否包含負號,如果包含,則傳回錯誤。同時,檢查 fractionalPart 是否為有效的非負整數。
  5. 處理特殊情況:如果輸入值只是 “-” 或不是有效的整數,則傳回錯誤。

如何運作

該指令碼首先檢查輸入值是否包含小數點。如果沒有,則它不是浮點數。接下來,將值的整數和小數部分提取出來進行分析。然後,在第一個檢查點,指令碼驗證整數部分(小數點左邊的數字)是否為有效的整數。接下來的檢查更為複雜,因為需要檢查小數部分(小數點右邊的數字)是否有額外的負號(以避免像 17.-30 這樣的奇怪格式),並再次確保小數部分是有效的整數。

執行指令碼

如果在呼叫函式時沒有產生錯誤訊息,則傳回程式碼為 0,並且指定的數字是有效的浮點數值。你可以透過在程式碼末尾附加以下幾行來測試這個指令碼:

if validfloat $1 ; then
    echo "$1 是有效的浮點數值。"
fi
exit 0

結果

validfloat shell 指令碼只需一個引數來嘗試驗證。清單 1-13 使用 validfloat 指令碼來驗證幾個輸入。

改進指令碼

一個很酷的改進方法是擴充這個函式以允許科學記號,如最後一個例子所示。這不會太困難。你需要測試 ’e’ 或 ‘E’ 的存在,然後將結果分成三個部分:整數部分(始終是一位數字)、小數部分和 10 的冪次。然後,你只需要確保每個部分都是有效的整數。

日期格式驗證

確保特定日期實際上在日曆上是可能的,這是 shell 指令碼中一個至關重要但具有挑戰性的驗證任務。如果我們忽略閏年,這個任務並不算太糟糕,因為每年的日曆都是一致的。在這種情況下,我們只需要一個表格,列出每月最大天數,以與指定的日期進行比較。要考慮閏年,你需要在指令碼中新增一些額外的邏輯,這使得事情變得更加複雜。

日期驗證規則

一套用於測試某個年份是否為閏年的規則如下:

  • 不能被 4 整除的年份不是閏年。
  • 能被 4 和 400 整除的年份是閏年。
  • 能被 4 整除,但不能被 400 整除,卻能被 100 整除的年份不是閏年。
  • 所有其他能被 4 整除的年份都是閏年。

程式碼實作(待續)

#!/bin/bash
# valid-date--驗證日期,考慮閏年規則
normdate="您呼叫的 normdate.sh 指令碼名稱"

exceedsDaysInMonth()
{
    # 給定月份名稱和該月的日期編號,此函式將傳回 0,
    # 如果指定的日期值小於或等於該月的最大天數;否則傳回 1。
    case $(echo $1|tr '[:upper:]' '[:lower:]') in
        jan* ) days=31 ;; 
        feb* ) days=28 ;;
        mar* ) days=31 ;; 
        apr* ) days=30 ;;
        may* ) days=31 ;; 
        jun* ) days=30 ;;

內容解密:

  1. exceedsDaysInMonth 函式:該函式根據給定的月份名稱和日期編號,檢查該日期是否超過該月的最大天數。它使用 case 陳述句來根據月份名稱設定最大天數。
  2. 月份大小寫轉換:使用 tr 命令將月份名稱轉換為小寫,以便進行大小寫不敏感的比較。
  3. 不同月份的最大天數:根據月份名稱,將 days 變數設定為相應的最大天數(2 月預設為 28 天,未考慮閏年)。

日期驗證指令碼的設計與實作

概述

本文將探討一個用於驗證日期是否有效的Shell指令碼。該指令碼能夠接受多種日期格式輸入,並檢查輸入的日期是否為有效的日期,包括對閏年的處理。

指令碼結構與功能

該指令碼主要由三個部分組成:

  1. 日期規範化函式 (normdate):將輸入的日期轉換為統一的格式。
  2. 月份天數檢查函式 (exceedsDaysInMonth):檢查給定的日期是否超過該月份的最大天數。
  3. 閏年檢查函式 (isLeapYear):判斷給定的年份是否為閏年。

程式碼實作

#!/bin/bash

# 檢查日期是否超過月份的最大天數
exceedsDaysInMonth() {
  case $(echo "$1" | tr '[:upper:]' '[:lower:]') in
    jan* ) days=31 ;;
    feb* ) days=28 ;; # 假設不是閏年
    mar* ) days=31 ;;
    apr* ) days=30 ;;
    may* ) days=31 ;;
    jun* ) days=30 ;;
    jul* ) days=31 ;;
    aug* ) days=31 ;;
    sep* ) days=30 ;;
    oct* ) days=31 ;;
    nov* ) days=30 ;;
    dec* ) days=31 ;;
    * ) echo "$0: 未知的月份名稱 $1" >&2; exit 1 ;;
  esac
  
  if [ $2 -lt 1 -o $2 -gt $days ] ; then
    return 1
  else
    return 0
  fi
}

# 檢查是否為閏年
isLeapYear() {
  year=$1
  
  if [ "$((year % 4))" -ne 0 ] ; then
    return 1
  elif [ "$((year % 400))" -eq 0 ] ; then
    return 0
  elif [ "$((year % 100))" -eq 0 ] ; then
    return 1
  else
    return 0
  fi
}

# 主程式
if [ $# -ne 3 ] ; then
  echo "用法: $0 月 日 年" >&2
  echo "典型的輸入格式為 August 3 1962 或 8 3 1962" >&2
  exit 1
fi

newdate=$(normdate "$@")
if [ $? -eq 1 ] ; then
  exit 1
fi

month=$(echo $newdate | cut -d' ' -f1)
day=$(echo $newdate | cut -d' ' -f2)
year=$(echo $newdate | cut -d' ' -f3)

if ! exceedsDaysInMonth $month "$day" ; then
  if [ "$month" = "Feb" -a "$day" -eq "29" ] ; then
    if ! isLeapYear $year ; then
      echo "$0: $year 不是閏年,因此二月沒有29天。" >&2
      exit 1
    fi
  else
    echo "$0: 日期值錯誤:$month沒有$day天。" >&2
    exit 1
  fi
fi

echo "有效的日期:$newdate"
exit 0

程式碼解析:

  1. exceedsDaysInMonth函式

    • 使用case陳述式檢查月份並確定該月份的天數。
    • 對二月預設為28天,後續再根據isLeapYear函式的結果進行調整。
    • 邏輯分析:

      • 該函式首先將輸入的月份名稱轉換為小寫,以便於比較。
      • 使用case陳述式匹配月份名稱的前三個字母,以確定月份。
      • 若輸入的日期超出該月份的合理範圍,則傳回1表示無效日期。
  2. isLeapYear函式

    • 使用數學運算檢查年份是否符合閏年的條件。
    • 邏輯分析:

      • 首先檢查年份是否能被4整除,不能則直接傳回1表示不是閏年。
      • 若能被4整除,再檢查是否能被400整除,能則傳回0表示是閏年。
      • 若能被4整除但不能被400整除,則檢查是否能被100整除,能則傳回1表示不是閏年。
      • 其他情況傳回0表示是閏年。
  3. 主程式邏輯

    • 首先檢查輸入引數的數量和格式,若不符合要求則輸出錯誤資訊並離開。
    • 使用normdate函式規範化輸入的日期格式。
    • 將規範化後的日期拆分為月份、日期和年份三個部分。
    • 使用exceedsDaysInMonth函式檢查日期是否有效,特別處理二月29日的情況,結合isLeapYear函式判斷是否為閏年。
    • 邏輯分析:

      • 主程式首先呼叫normdate函式統一日期格式。
      • 然後透過exceedsDaysInMonth函式檢查日期的有效性。
      • 特別地,對二月29日進行額外檢查,利用isLeapYear函式確認年份是否為閏年。

使用與測試

  • 該指令碼支援多種日期輸入格式,例如完整的月份名稱、縮寫或數字表示。
  • 可透過命令列輸入日期進行測試,如valid-date august 3 1960valid-date 9 31 2001

改進與擴充套件方向

  • 可增加對時間格式的驗證功能,支援24小時制或AM/PM字尾的時間輸入。
  • 利用GNU date命令簡化閏年的判斷邏輯。
  • 對月份名稱的匹配規則進行最佳化,增加對常見縮寫和拼寫錯誤的支援。

解決echo命令實作不一致的問題

在Unix和GNU/Linux系統中,大多數現代實作都支援echo命令的-n選項,用於抑制輸出中的尾隨換行符。然而,並非所有實作都以相同的方式運作。有些實作使用\c作為特殊的嵌入字元來避免預設行為,而其他的則堅持包含尾隨的換行符,無論如何。

測試你的echo命令

要確定你的echo命令是否正確實作了-n選項,可以簡單地輸入以下命令並觀察輸出:

$ echo -n "The rain in Spain"; echo " falls mainly on the Plain"

如果你的echo命令正確支援-n選項,你將看到如下輸出:

The rain in Spain falls mainly on the Plain

如果不支援,你將看到如下輸出:

-n The rain in Spain
falls mainly on the Plain

建立一個可靠的echon函式

為了確保指令碼輸出按照預期呈現給使用者,我們將建立一個名為echon的替代echo版本,它總是會抑制尾隨的換行符。這樣,我們就有了一個可靠的命令,可以在需要echo -n功能時呼叫。

使用awk printf命令實作echon

echon()
{
  echo "$*" | awk '{ printf "%s", $0 }'
}

內容解密:

  1. echo "$*":將所有輸入引數作為單一字串輸出。
  2. awk '{ printf "%s", $0 }':使用awkprintf函式輸出輸入字串,但不新增尾隨換行符。
  3. $0代表awk讀取的整行輸入。

使用printf命令實作echon(如果可用)

echon()
{
  printf "%s" "$*"
}

內容解密:

  1. printf "%s":輸出字串但不新增尾隨換行符。
  2. "$*":將所有輸入引數作為單一字串傳遞給printf

使用tr命令實作echon(作為替代方案)

echon()
{
  echo "$*" | tr -d '\n'
}

內容解密:

  1. echo "$*":輸出所有輸入引數。
  2. tr -d '\n':刪除輸出中的換行符\n

使用echon函式

只需將包含echon函式的指令碼檔案新增到你的PATH中,你就可以用echon取代任何echo -n的呼叫,以可靠地在列印後將使用者的遊標留在行尾。

測試echon命令

$ echon "Enter coordinates for satellite acquisition: "
Enter coordinates for satellite acquisition: 12,34

改進指令碼

考慮建立一個函式來自動測試echo命令的輸出,以確定其行為並相應地修改呼叫方式。例如,可以寫入類別似於 echo -n hi | wc -c 的命令,然後測試結果是兩個字元(hi)、三個字元(hi加上換行符)等。

建構一個任意精確度的浮點數計算器

Shell指令碼中的 $(( )) 序列允許你執行基本的數學運算。然而,它不支援分數或小數運算。為瞭解決這個問題,我們可以使用 bc 命令,一個任意精確度的計算器程式,並為它寫一個包裝指令碼,使其更易於使用。

scriptbc指令碼

#!/bin/bash
# scriptbc--Wrapper for 'bc' that returns the result of a calculation

if [ "$1" = "-p" ]; then
  precision=$2
  shift 2
else
  precision=2 # 預設精確度
fi

bc -q -l << EOF
scale=$precision
$*
quit
EOF

exit 0

內容解密:

  1. if [ "$1" = "-p" ]; then:檢查是否指定了精確度引數。
  2. precision=$2:設定精確度。
  3. bc -q -l << EOF:呼叫bc命令,並使用here檔案提供輸入。
  4. scale=$precision:設定bc中的精確度。
  5. $*:將傳遞給指令碼的引數作為計算式傳遞給bc
  6. quit:離開bc
  7. EOF:標誌here檔案的結束。

使用scriptbc指令碼

透過這個包裝指令碼,你可以輕鬆地以指定的精確度執行計算。預設情況下,精確度為2位小數,但你可以使用 -p 選項來指定不同的精確度。