在 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
}
內容解密:
validfloat函式:該函式用於驗證輸入值是否為有效的浮點數。它首先檢查輸入值是否有小數點。- 提取小數部分和整數部分:使用
cut和引數擴充套件來提取小數點前後的數字部分,分別儲存在decimalPart和fractionalPart中。 - 驗證小數部分:檢查
decimalPart是否為有效的整數,使用validint函式。如果不是,則傳回錯誤。 - 驗證小數點後的數字:檢查
fractionalPart是否包含負號,如果包含,則傳回錯誤。同時,檢查fractionalPart是否為有效的非負整數。 - 處理特殊情況:如果輸入值只是 “-” 或不是有效的整數,則傳回錯誤。
如何運作
該指令碼首先檢查輸入值是否包含小數點。如果沒有,則它不是浮點數。接下來,將值的整數和小數部分提取出來進行分析。然後,在第一個檢查點,指令碼驗證整數部分(小數點左邊的數字)是否為有效的整數。接下來的檢查更為複雜,因為需要檢查小數部分(小數點右邊的數字)是否有額外的負號(以避免像 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 ;;
內容解密:
exceedsDaysInMonth函式:該函式根據給定的月份名稱和日期編號,檢查該日期是否超過該月的最大天數。它使用case陳述句來根據月份名稱設定最大天數。- 月份大小寫轉換:使用
tr命令將月份名稱轉換為小寫,以便進行大小寫不敏感的比較。 - 不同月份的最大天數:根據月份名稱,將
days變數設定為相應的最大天數(2 月預設為 28 天,未考慮閏年)。
日期驗證指令碼的設計與實作
概述
本文將探討一個用於驗證日期是否有效的Shell指令碼。該指令碼能夠接受多種日期格式輸入,並檢查輸入的日期是否為有效的日期,包括對閏年的處理。
指令碼結構與功能
該指令碼主要由三個部分組成:
- 日期規範化函式 (
normdate):將輸入的日期轉換為統一的格式。 - 月份天數檢查函式 (
exceedsDaysInMonth):檢查給定的日期是否超過該月份的最大天數。 - 閏年檢查函式 (
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
程式碼解析:
exceedsDaysInMonth函式:- 使用
case陳述式檢查月份並確定該月份的天數。 - 對二月預設為28天,後續再根據
isLeapYear函式的結果進行調整。 邏輯分析:
- 該函式首先將輸入的月份名稱轉換為小寫,以便於比較。
- 使用
case陳述式匹配月份名稱的前三個字母,以確定月份。 - 若輸入的日期超出該月份的合理範圍,則傳回1表示無效日期。
- 使用
isLeapYear函式:- 使用數學運算檢查年份是否符合閏年的條件。
邏輯分析:
- 首先檢查年份是否能被4整除,不能則直接傳回1表示不是閏年。
- 若能被4整除,再檢查是否能被400整除,能則傳回0表示是閏年。
- 若能被4整除但不能被400整除,則檢查是否能被100整除,能則傳回1表示不是閏年。
- 其他情況傳回0表示是閏年。
主程式邏輯:
- 首先檢查輸入引數的數量和格式,若不符合要求則輸出錯誤資訊並離開。
- 使用
normdate函式規範化輸入的日期格式。 - 將規範化後的日期拆分為月份、日期和年份三個部分。
- 使用
exceedsDaysInMonth函式檢查日期是否有效,特別處理二月29日的情況,結合isLeapYear函式判斷是否為閏年。 邏輯分析:
- 主程式首先呼叫
normdate函式統一日期格式。 - 然後透過
exceedsDaysInMonth函式檢查日期的有效性。 - 特別地,對二月29日進行額外檢查,利用
isLeapYear函式確認年份是否為閏年。
- 主程式首先呼叫
使用與測試
- 該指令碼支援多種日期輸入格式,例如完整的月份名稱、縮寫或數字表示。
- 可透過命令列輸入日期進行測試,如
valid-date august 3 1960或valid-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 }'
}
內容解密:
echo "$*":將所有輸入引數作為單一字串輸出。awk '{ printf "%s", $0 }':使用awk的printf函式輸出輸入字串,但不新增尾隨換行符。$0代表awk讀取的整行輸入。
使用printf命令實作echon(如果可用)
echon()
{
printf "%s" "$*"
}
內容解密:
printf "%s":輸出字串但不新增尾隨換行符。"$*":將所有輸入引數作為單一字串傳遞給printf。
使用tr命令實作echon(作為替代方案)
echon()
{
echo "$*" | tr -d '\n'
}
內容解密:
echo "$*":輸出所有輸入引數。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
內容解密:
if [ "$1" = "-p" ]; then:檢查是否指定了精確度引數。precision=$2:設定精確度。bc -q -l << EOF:呼叫bc命令,並使用here檔案提供輸入。scale=$precision:設定bc中的精確度。$*:將傳遞給指令碼的引數作為計算式傳遞給bc。quit:離開bc。EOF:標誌here檔案的結束。
使用scriptbc指令碼
透過這個包裝指令碼,你可以輕鬆地以指定的精確度執行計算。預設情況下,精確度為2位小數,但你可以使用 -p 選項來指定不同的精確度。