Format XML with BASH Script

用 BASH 指令將 XML 檔排版

【摘要】一個可以將 XML 檔排版的 BASH 小程式。


【目錄】

    【前言
    【指令檔
    【後語

【前言】

一、有不少現成的工具可以將 XML 檔自動排版。個人自己試著用 BASH 指令寫了一個做為練習,兩年半前曾發表於 Google 的 Blogger,近日稍加修飾,記錄於此。


二、使用方法:

將【指令檔】一節的內容存成 formatXML.sh 檔,在虛擬終端機執行:

bash formatXML.sh filename.xml

  filename.xml 是要被處理的檔名;一次只處理一檔。

處理好的檔案會以相同的檔名放入 NewDir 目錄(若無此目錄,會自動新增)。


三、預設的縮排單位是兩個空格,以 Unit 變數設定,可自行更改。


四、因為無法預知 XML 的註解內容,所以一律改成跨行註解,頭尾標記從行首開始,內容則縮排一個單位。


【指令檔】

 #!/bin/bash
 # 將 XML 檔排版
 
 # 檢查是否有給 XML 檔,若無,即中止
if [ -z "${1}" ]; then
  echo
  echo "  Usage:  ./${0##*/}  XML-file"
  echo
  echo "  You have to specify an XML file."
  echo
  exit
fi
 
 # 縮排一階的單位;需用單引號,下同
Unit='  '
 # 縮排總數
Indent=''
 # 本行標籤種類
Case=''
 # 前一行標籤種類;是否要縮排,大多決定於前一行
CasePrev=''
 # 註解的標記;0 表本行非註解,1 表是註解
Comment=0
 # 中間檔檔名;程式會先產生一個所有標籤自成一行且完全未縮排的中間檔,最後再將之刪除
TempFile='tempfile'
 
 # XML 檔的路徑
echo ${1} | grep '/' &> /dev/null
if [ ${?} -eq 0 ]; then
  OldPath="${1%/*}"
  # 如果給的是全路徑,即取出其路徑
else
  OldPath="${PWD}"
  # 如果只給檔名,則用目前工作路徑
fi
 
 # 取出檔名
FileName="${1##*/}"
 # 輸出檔的路徑
NewPath="${OldPath}/NewDir"
 
 # 新增存放輸出檔的目錄;若已有,會有錯誤訊息,所以將之導開
mkdir "${NewPath}" &> /dev/null
 # 若無輸出檔,即新增之;若已有,將之清空
: > "${NewPath}/${FileName}"
 
 # 建立中間檔
cat ${1} |
sed -r 's/\r//g; s/\\/\\\\/g; s/</\n</g; s/>/>\n/g; s/(<!--)/\1\n/g; s/(-->)/\n\1/g' |
 # 改為 Unix 換行格式;反斜線要先 escaped;所有標籤自成一行;註解之頭尾分開
sed -r 's/^[ \t]*//g; s/[ \t]*$//g' |
 # 去除頭尾空白;這不能與前指令合併,因為有些空白是在分行時才產生的
sed -r '/^$/d' > "${NewPath}/${TempFile}"
 # 去除空白行;這不能與前指令合併,因為有些空白行是在去除頭尾空白時才產生的
 
 # 主程式段
 # 先判斷標籤種類,再行縮排
while read
do
  if [ "${REPLY}" = '<!--' -o "${REPLY}" = '-->' ]; then
  # 若內容是 '<!--' 或 '-->',是註解
  # 註解與眾不同,需先濾除,故不能將此檢查後移
    Case="comment"
    Comment=1
    # 註解標記 on
  elif [ "${REPLY:0:2}" = '<?' -a "${REPLY:(-2)}" = '?>' ]; then
  # 若頭二字元是 '<?' 且末二字元是 '?>',是宣告序文
    Case="prolog"
  elif [ "${REPLY:0:1}" = '<' -a "${REPLY:(-2)}" = '/>' ]; then
  # 單獨成行的標籤,其倒數第二字元是 '/'
    Case="self"
  elif [ "${REPLY:0:2}" = '</' -a "${REPLY:(-1)}" = '>' ]; then
  # 結束標籤的第二字元是 '/'
    Case="end"
  elif [ "${REPLY:0:1}" = '<' -a "${REPLY:(-1)}" = '>' -a "${REPLY:1:1}" != '/' -a "${REPLY:(-2):1}" != '/' ]; then
  # 若第二字元和倒數第二字元不是 '/',是開始標籤
    Case="start"
  elif [ "${REPLY:0:1}" != '<' -a "${REPLY:(-1)}" != '>' ]; then
  # 若頭尾不是 '<' 和 '>',應該是文字內容
    Case="text"
  else
  # 以上皆非,做備用
    Case="exception"
  fi
 
  if [ "${Comment}" -eq 1 ]; then
  # 註解先另外處理
    if [ "${REPLY:0:4}" = '<!--' ]; then
    # 註解開始標記,行首開始
      echo "${REPLY}" >> "${NewPath}/${FileName}"
    elif [ "${REPLY:(-3)}" = '-->' ]; then
    # 註解結束標記,行首開始
      echo "${REPLY}" >> "${NewPath}/${FileName}"
      Comment=0
      # 註解已結束,故標為 off
    else
    # 剩下的是內容,縮排一階
      echo "${Unit}${REPLY}" >> "${NewPath}/${FileName}"
    fi
    CasePrev=comment
    # 是否要縮排,大多決定於前一行,故需記錄下來
    # 讀下一行時,此行變前一行
  else
  # 非註解行
    case "${Case}" in
      prolog)
      # 本行是宣告;一律從行首開始
        echo "${REPLY}" >> "${NewPath}/${FileName}"
        CasePrev=prolog
        ;;
      text)
      # 一般文字
        case "${CasePrev}" in
          start)
          # 前一行是開始標籤;縮排加一階
            Indent="${Indent}${Unit}"
            echo "${Indent}${REPLY}" >> "${NewPath}/${FileName}"
            ;;
          text | end | self | comment)
          # 其餘狀況;同前一行
            echo "${Indent}${REPLY}" >> "${NewPath}/${FileName}"
            ;;
          *)
          # 預防有意外狀況
            echo "!!!EXCEPTION > ${REPLY}" >> "${NewPath}/${FileName}"
            ;;
        esac
        CasePrev=text
        ;;
      start)
      # 本行是開始標籤
        case "${CasePrev}" in
          prolog)
          # 前一行是宣告;行首開始
            echo "${REPLY}" >> "${NewPath}/${FileName}"
            ;;
          start)
          # 前一行是開始標籤;縮排加一階
            Indent="${Indent}${Unit}"
            echo "${Indent}${REPLY}" >> "${NewPath}/${FileName}"
            ;;
          text | end | self | comment)
          # 其餘狀況;同前一行
            echo "${Indent}${REPLY}" >> "${NewPath}/${FileName}"
            ;;
          *)
            echo "!!!EXCEPTION > ${REPLY}" >> "${NewPath}/${FileName}"
            ;;
        esac
        CasePrev=start
        ;;
      end)
      # 本行是結束標籤
        case "${CasePrev}" in
          text | end | self)
          # 前一行是一般文字、結束標籤、單行標籤;縮排減一階
            Indent="${Indent:${#Unit}}"
            echo "${Indent}${REPLY}" >> "${NewPath}/${FileName}"
            ;;
          start | comment)
          # 前一行是開始標籤、註解;同前一行
            echo "${Indent}${REPLY}" >> "${NewPath}/${FileName}"
            ;;
          *)
            echo "!!!EXCEPTION > ${REPLY}" >> "${NewPath}/${FileName}"
            ;;
        esac
        CasePrev=end
        ;;
      self)
      # 本行是單行標籤
        case "${CasePrev}" in
          start)
          # 前一行是開始標籤;縮排加一階
            Indent="${Indent}${Unit}"
            echo "${Indent}${REPLY}" >> "${NewPath}/${FileName}"
            ;;
          prolog | text | end | self | comment)
          # 其餘狀況;同前一行
            echo "${Indent}${REPLY}" >> "${NewPath}/${FileName}"
            ;;
          *)
            echo "!!!EXCEPTION > ${REPLY}" >> "${NewPath}/${FileName}"
            ;;
        esac
        CasePrev=self
        ;;
      *)
      # 預防有意外狀況
        echo "!!!EXCEPTION > ${REPLY}" >> "${NewPath}/${FileName}"
        CasePrev=exception
        ;;
    esac
  fi
done < "${NewPath}/${TempFile}"
 
 # 刪除中間檔
rm "${NewPath}/${TempFile}"
 
exit

【後語】

本文只是提供參考,或許還有不少 bugs,請自行修正。


發表留言