shell編程中星號(asterisk "*")的坑

  • 2020 年 3 月 11 日
  • 筆記

今天分享一個有關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執行腳本的原理:

  1. shell讀取整個腳本文件,然後從上到下依次執行每一行
  2. 假設當前line=4567890 *,當shell執行echo $line >> $i.txt時
    1. 首先,shell負責替換$line的值為:4567890 *,此時程式碼是:echo 4567890 * >> $i.txt
    2. 然後,shell在執行echo命令之前,檢查命令的參數中是否有通配符(PS:此時echo的參數:4567890 *)
    3. 很明顯,*是通配符,shell負責解析通配符,shell會將通配符當作路徑或文件名在磁碟上搜尋可能的匹配:若符合要求的匹配存在,則進行替換(路徑擴展);否則就將該通配符作為一個普通字元參數傳遞給echo,然後再由echo進行處理。
    4. 解析完通配符後,*被替換為script.sh test.logs,此時echo命令的參數是:4567890 script.sh test.logs
    5. 最後,shell執行echo 4567890 script.sh test.logs,然後將echo命令執行的結果重定向到文件中。

Tips:

通配符看起來有點像正則表達式,但是它與正則表達式不同的,不能相互混淆。可以把通配符理解為shell能夠處理的特殊字元。而且shell的通配符涉及的只有 "*, ?, [], {}" 這幾種。

3. 解決方案

根據上面的分析可以知道,$line被替換後,通配符*再次被shell解析。哪有什麼辦法可以防止shell解析通配符呢?

  1. 使用引用變數:"$line",在兩端加上引號,這樣"$line"就變成了一個字元串"4567890 *",而不是兩個單獨的字元串。
  2. 引用變數可防止分詞和通配符擴展(也就是shell解析通配符),並且可以防止在變數中包含空格、換行符、通配符等時造成腳本中斷。
  3. 在shell編程中總是使用引用變數的方式,這是一個良好又安全的編碼習慣。

4. 參考資料

  1. Reading asterisk character (*) from a file in bash
  2. When to wrap quotes around a shell variable?
  3. Security implications of forgetting to quote a variable in bash/POSIX shells
  4. shellcheck.net-finds bugs in your shell scripts.
  5. Linux Shell 通配符、元字元、轉義符使用實例介紹

Tags: