今天分享一個有關shell編程中由通配符引起的問題。
1. 問題代碼
cat test.logs
4567890 *
##*************************************##
rtyuio**tyuio432
##*************************************##
*rtyuiop*2* yuiop
##*************************************##
rtyuiop(3 * 4)iuytr
##*************************************##
8765432
cat script.sh
#!/usr/bin/env bash
# 主要功能是將 非##開頭 的每行記錄寫入到文件中,每個文件保存一行記錄
logsname=test.logs
i=100
while read line
do
if [[ $line =~ '##' ]];then
((i++))
else
echo $line >> $i.txt
fi
done < "${logsname}"
運行script.sh腳本的結果:
從圖片上紅框部分可以看到:
4567890 *
被替換為4567890 script.sh test.logs
rtyuiop(3 * 4)iuytr
被替換為rtyuiop(3 100.txt 101.txt 102.txt script.sh test.logs 4)iuytr
其他行都正常打印結果,為什么這兩行會有問題呢?其他行也有星號,為什么沒有被替換呢?
2. 原因分析
根據輸出結果可以判斷出問題的代碼:echo $line >> $i.txt
首先,介紹一下shell執行腳本的原理:
- shell讀取整個腳本文件,然后從上到下依次執行每一行
- 假設當前line=4567890 *,當shell執行
echo $line >> $i.txt
時- 首先,shell負責替換
$line
的值為:4567890 *,此時代碼是:echo 4567890 * >> $i.txt
- 然后,shell在執行echo命令之前,檢查命令的參數中是否有通配符(PS:此時echo的參數:4567890 *)
- 很明顯,
*
是通配符,shell負責解析通配符,shell會將通配符當作路徑或文件名在磁盤上搜尋可能的匹配:若符合要求的匹配存在,則進行替換(路徑擴展);否則就將該通配符作為一個普通字符參數傳遞給echo,然后再由echo進行處理。 - 解析完通配符后,
*
被替換為script.sh test.logs
,此時echo命令的參數是:4567890 script.sh test.logs
- 最后,shell執行
echo 4567890 script.sh test.logs
,然后將echo命令執行的結果重定向到文件中。
- 首先,shell負責替換
Tips:
通配符看起來有點像正則表達式,但是它與正則表達式不同的,不能相互混淆。可以把通配符理解為shell能夠處理的特殊字符。而且shell的通配符涉及的只有 "*, ?, [], {}" 這幾種。
通配符是shell自身支持的,而正則表達式需要相關工具的支持:grep,awk,vi,perl。在文本過濾工具里,都是用正則表達式,比如像awk,sed等,正則表達式是針對文件的內容。 通配符多用於文件名或者路徑上,比如查找find,ls,cp等。
3. 解決方案
根據上面的分析可以知道,$line
被替換后,通配符*
再次被shell解析。那有什么辦法可以防止shell解析通配符呢?
- 使用引用變量:
"$line"
,在兩端加上引號,這樣"$line"
就變成了一個字符串"4567890 *",而不是兩個單獨的字符串。 - 引用變量可防止分詞和通配符擴展(也就是shell解析通配符),並且可以防止在變量中包含空格、換行符、通配符等時造成腳本中斷。
- 在shell編程中總是使用引用變量的方式,這是一個良好又安全的編碼習慣。