玩轉 Bash 變數

spacewander發表於2016-09-23

PS : 注意本文討論的是Bash,而不一定是/bin/sh所連結的那個shell。這裡出現的所有程式碼片段,預設在頂上都新增了#!/bin/bash

一門自帶混淆的語言

while (( $# )); do
    case $1 in
        -a*)
            # Error checking
            [[ ${1#-a} ]] || { echo "bash: ${FUNCNAME[0]}: `$1': missing"
                "number specifier" 1>&2; return 1; }
            printf %d "${1#-a}" &> /dev/null || { echo "bash:"
                "${FUNCNAME[0]}: `$1': invalid number specifier" 1>&2
            return 1; }
            # Assign array of -aN elements
            [[ "$2" ]] && unset -v "$2" && eval $2=("${@:3:${1#-a}}") &&
                shift $((${1#-a} + 2)) || { echo "bash: ${FUNCNAME[0]}:"
                "`$1${2+ }$2': missing argument(s)" 1>&2; return 1; }
            ;;
        -v)
            # Assign single value
            [[ "$2" ]] && unset -v "$2" && eval $2="$3" &&
                shift 3 || { echo "bash: ${FUNCNAME[0]}: $1: missing"
                "argument(s)" 1>&2; return 1; }
            ;;
        *)
            echo "bash: ${FUNCNAME[0]}: $1: invalid option" 1>&2
            return 1 ;;
    esac
done

如果你覺得閱讀上面的Bash程式碼,就像閱讀段子一樣順暢,那麼是時候關掉這個頁面,去做別的更有意義的行為,比如去喝個水什麼的。

如果你覺得上面的Bash程式碼猶如鬼畫符,並且實際生活中不得不面對它,那麼就看下去吧。

Bash變數操作

正式開始正文內容。

想要在一篇文章裡,講述要看懂開篇程式碼所需的所有知識點,這是不自量力的行為。因此,本文將講且僅講Bash中操作變數的方法。所以,即使你看完了這篇文章,你多半還是看不懂開篇的程式碼。

不過看完這篇文章之後,你對Bash的變數操作會有更為深入的認識。而且更重要的是,Bash之於你,不再是怎麼也看不清摸不透。下一次要寫指令碼的時候,你也將更加堅定地下定決心 —— 人生苦短,我用Python/Ruby。

嚴格意義上的Bash變數型別

Bash變數只有兩種型別,字串和陣列。不過從嚴格意義上,Bash沒有變數型別。Bash中的變數,在執行的時候會被展開成其對應的值(字串)。你可以把它看做C/C++中的巨集定義,或者一些模板語言中的佔位符。

一般情況下,變數通過=賦值,注意=兩邊不要留空格。有些好孩子,已經養成了符號兩端留空格的習慣,結果當開始寫Bash的時候,他們抓狂了。

要想訪問變數,只需在變數名前面新增$,直譯器就會對它進行展開。如果該變數並不存在,直譯器會把它展開成“”。

me=spacewander
echo $me
echo $who

來自命令列的你

作為指令碼語言,第一要義當然是要隨時隨地獲取到命令列輸入啦。

在Bash中,使用$1可以獲取命令列輸入的第一個引數,$2可以獲取命令列輸入的第2個引數,$3可以獲取命令列輸入的第……

你看,$1到$10000的用法就這麼交代完了。Bash還是挺有邏輯的嘛。

順便一提,$0獲取的指令碼的名字(其實就是其他語言中的第0個引數),$@獲取所有的引數,$#獲取引數的數目。記住@#這兩個符號,在Bash這一神祕的符文體系中,前者表示全部引數,後者表示引數的數目。

展開,然後Bomb!

假如Bash變數中含有空白字元,或者含有特殊字元,比如*,展開後會汙染到外面的字串,結果就是Bomb

比如

Oops='*'
# '*'解釋成所有匹配的檔名
echo $Oops
# 所以需要加雙引號括起來
echo "$Oops"
# 加單引號會怎樣呢?
echo '$Oops'

上面的程式碼值得一試。

另外一種Bomb的可能是,變數後面需要接其它字串,比如$FRUITs。如果想讓直譯器識別成$FRUIT變數,而不是$FRUITs,需要用花括號括起來,像${FRUIT}s

陣列和關聯陣列

Bash中可以使用兩種容器。

一種是陣列,另一種是關聯陣列,類似於其他語言中的Map/Hash/Dict。

宣告陣列的常用語法: declare -a ARY或者ARY=(1 2 3)

宣告關聯陣列的唯一語法: declare -A MAP

賦值的語法:

直接ARY[N]=VALUE,N可以是數字索引也可以是鍵。關聯陣列可以使用MAP=([x]=a [y]=b)進行多項賦值,注意這是賦值的語句而不是宣告。

親測陣列中的索引不一定要按順序來,你可以先給2和3上的元素賦值。(同樣算是弱型別的Javascript也支援這種無厘頭賦值,這算通病麼?)

往現有陣列批量新增元素:

ARY+=(a b c)
MAP+=([a]=1 [b]=2)

取值:

${ARY[INDEX]}
${MAP[KEY]}

注意花括號的使用

${A[@]} 展開成所有的變數,而獲取陣列長度使用 ${#A[@]}

切片:

${ARY[@]:N:M} N是offset而M是length

返回索引,相當於keys():

${!MAP[@]}

試試下面的程式碼:

declare -a ARY
declare -A MAP
MAP+=([a]=1 [b]=2)
ARY+=(a b c)

echo ${ARY[1]}
echo ${MAP[a]}
echo "${ARY[@]}"
echo "${MAP[@]}"
echo "${ARY[@]:0:1}"
echo ${#ARY[@]}
echo "${!MAP[@]}"

ARY[4]=a
echo ${ARY[@]}
echo ${ARY[3]}

變數(字串)變換

Bash中的變數變換,大體是${變數[操作符]}的形式

大小寫變換

HI=HellO

echo "$HI" # HellO
echo ${HI^} # HellO
echo ${HI^^} # HELLO
echo ${HI,} # hellO
echo ${HI,,} # hello
echo ${HI~} # hellO
echo ${HI~~} #hELLo

^大寫,,小寫, ~大小寫切換

重複一次只修改首字母,重複兩次則應用於所有字母。

混著用會怎樣?

echo ${HI^,^} # HellO

看來是不行的×_×

移除匹配的字串

%xx 從後往前,開始匹配,移除匹配的內容
%%xx 跟上面的差不多,不過這是貪婪匹配
#xx 從前往後,開始匹配,移除匹配的內容
##xx 跟上面的差不多,不過這是貪婪匹配

這個比較難理解,不過看下面幾個例子應該能明白了。

FILENAME=/home/spacewander/param.sh
echo ${FILENAME%/*} # /home/spacewander
echo ${FILENAME%%/*} #
echo ${FILENAME#*/} # home/spacewander/param.sh
echo ${FILENAME##*/} # param.sh

查詢並替換

/MATCH/VALUE 替換第一個匹配的內容。
//MATCH/VALUE 替換匹配的內容

echo ${FILENAME/home/office} # /office/spacewander/param.sh
echo ${FILENAME//s/S} # /home/Spacewander/param.Sh

其它字串操作

獲取變數(字串)長度:${#FILENAME}

字串切片:跟陣列切片是同樣的語法,${STR:offset:len}

TEXT=這個程式充滿了BUG!
echo ${TEXT:0:8}
echo ${TEXT:4}

# 你還可以使用負數作為offset,這時候就是從後往前算起。注意負數要用括號括起來,避免衝突。
echo ${TEXT:(-4)}

關於變數,其它的內容

Bash中有一項特性,你可以方便地檢查某個變數是否設定了,如果沒有設定,就賦予一個預設值。尤其在處理環境變數的時候,這項特性會讓你感到欣慰。

語法是${VAR:=VALUE}或者${VAR:=VALUE}。此外,還有一個相似的語法,${VAR:=VALUE}${VAR:=VALUE}

下面展示下兩者的區別

# expand to default variable
echo ${NULL-"Not null"} # Not null
echo ${NULL} #

# set default variable
echo ${NIL="Not nil"} # Not nil
echo ${NIL} # Not nil

可以看出,前者只是當變數不存在時,展開成指定的值。而後者在變數不存在時,將變數的值設定為指定值。

最後介紹一個,當目標變數不存在時,指定報錯資訊的語法。

echo ${TARGET?Not Found} # 當$TARGET不存在時,顯示TARGET: Not Found,並結束程式。

相關文章