1. 程式人生 > 程式設計 >第5期:Shell特殊引數$@和$*舉例解析

第5期:Shell特殊引數$@和$*舉例解析

Shell 中若當前指令碼只是作為一個包裝器(wrapper),需要把所有位置引數繼續傳遞給實際的指令碼或函式。這時候,可以選擇特殊引數完成:艾特符號$@ 和星號 $*,它們的基本含義是擴充套件為所有位置引數。多數情況下,使用起來都沒什麼區別,但不追究明白很容易出現bug,下面舉例說明。

man檔案

關於這兩個引數的說明,首先可以在man bash裡的PARAMETERS章節下的Special Parameter檢視說明,下面是節選:

The shell treats several parameters specially.  These parameters may only be referenced; assignment to them is not allowed.
*      Expands to the positional parameters,starting from one.  When the expansion occurs within double quotes,it expands to a single word  with the  value  of  each  parameter  separated by the first character of the IFS special variable.  That is,"$*"
is equivalent to "$1c$2c...",where c is the first character of the value of the IFS variable. If IFS is unset,the parameters are separated by spaces. If IFS is null,the parameters are joined without intervening separators. @ Expands to the positional parameters,each parameter expands to a sepa- rate word. That is,"$@
"
is equivalent to "$1" "$2" ... If the double-quoted expansion occurs within a word,the expansion of the first parameter is joined with the beginning part of the original word,and the expansion of the last parameter is joined with the last part of the original word. When there are no positional parameters,"$@
"
and $@ expand to nothing (i.e.,they are removed). 複製程式碼

man檔案太抽象,我們先放下。

四種引用形式

使用@或*符號,加上是否使用雙引號兩種情況,我們一共可以得到四種引用形式,到底應該怎樣選擇呢?

  • $*
  • $@
  • "$*"
  • "$@"

舉例說明

man檔案的說明太抽象,我們編寫一個測試指令碼(假如叫test.sh),加深理解。指令碼中bar()函式用來接受引數,我們依次使用四種形式將位置引數傳遞過去,看一下實際的解析情況。程式碼如下:

#!/bin/bash

bar(){
  echo "num of args: $#"
  echo "arg1: $1"
  echo "arg2: $2"
  echo "arg3: $3"
  echo "arg4: $4"
  echo ""
}

echo 'using $*'
bar $*

echo 'using $@'
bar $@

echo 'using "$*"'
bar "$*"

echo 'using "$@"'
bar "$@"
複製程式碼

在測試之前,我們還要在當前目錄touch一個名為javac的檔案,如下:

maoshuai@maoshuai-ubuntu-desktop-18:~/test$ touch javac
maoshuai@maoshuai-ubuntu-desktop-18:~/test$ ls -l
total 4
-rw-rw-r-- 1 maoshuai maoshuai   0 Mar 24 15:18 javac
-rwxr-xr-x 1 maoshuai maoshuai 233 Mar 24 15:13 test.sh
複製程式碼

最後,執行命令/test.sh "I like" coding "java*"觀察輸出:

maoshuai@maoshuai-ubuntu-desktop-18:~/test$ ./test.sh "I like" coding "java*"
using $*
num of args: 4
arg1: I
arg2: like
arg3: coding
arg4: javac

using $@
num of args: 4
arg1: I
arg2: like
arg3: coding
arg4: javac

using "$*"
num of args: 1
arg1: I like coding java*
arg2:
arg3:
arg4:

using "$@"
num of args: 3
arg1: I like
arg2: coding
arg3: java*
arg4:

複製程式碼

可以看出,輸出都不一樣,按照我們的本意,第4個是符合預期要求的,其他情況都有所改變。

  1. 使用$*

    相當於將所有位置引數不帶引號的形式再次傳遞出去。由於不帶引號,最大的問題是引數個數會被重新解析。比如第一個引數"I like"作為一個整體被拆成了2個位置引數。 另外,由於每個位置引數都沒有雙引號,會再次做變數擴充套件,因此java*變成了"javac"。 這種使用方式,相當於bar $1 $2 $3 $4。所以最終輸出結果是4個位置引數,並且java*被擴充套件為"javac"

  2. 使用$@

    $*沒有區別。

  3. 使用""$*"

    根據man檔案解釋,相當於將所有位置引數連線成一個word。連線符就是IFS(一般就是空格),所以,此時輸出的結果只有一個位置引數,位置引數格式變成了1個。由於有雙引號的作用,java*沒有被擴充套件為"javac"。 這種方式,相當於bar "$1 $2 $3 $4"

  4. 使用"$@" 根據man檔案解釋,相當於拆分成多個word,並且每個word都有雙引號。即相當於bar "$1" "$2" "$3" "$4"。因此,位置引數格式保持不變,同時java*沒有被擴充套件為"javac"

總結下來,下面四種情況的等價寫法如註釋:

#!/bin/bash

bar(){
  echo "num of args: $#"
  echo "arg1: $1"
  echo "arg2: $2"
  echo "arg3: $3"
  echo "arg4: $4"
  echo ""
}

echo 'using $*'
# bar $1 $2 $3 $4
bar $*

echo 'using $@'
# bar $1 $2 $3 $4
bar $@

echo 'using "$*"'
# bar "$1 $2 $3 $4"
bar "$*"

echo 'using "$@"'
# bar "$1" "$2" "$3" "$4"
bar "$@"
複製程式碼

優秀程式碼印證

大多數成熟的軟體提供的指令碼wrapper裡,都是使用"$@"的,比如: maven的bin/mvn命令:

exec "$JAVACMD" \
  $MAVEN_OPTS \
  $MAVEN_DEBUG_OPTS \
  -classpath "${CLASSWORLDS_JAR}" \
  "-Dclassworlds.conf=${MAVEN_HOME}/bin/m2.conf" \
  "-Dmaven.home=${MAVEN_HOME}" \
  "-Dlibrary.jansi.path=${MAVEN_HOME}/lib/jansi-native" \
  "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
  ${CLASSWORLDS_LAUNCHER} "$@"
複製程式碼

flink的bin/flink命令

exec $JAVA_RUN $JVM_ARGS "${log_setting[@]}" -classpath "`manglePathList "$CC_CLASSPATH:$INTERNAL_HADOOP_CLASSPATHS"`" org.apache.flink.client.cli.CliFrontend "$@"
複製程式碼

tomcat的bin/startup

exec "$PRGDIR"/"$EXECUTABLE" start "$@"
複製程式碼

但也有寫存在問題的,比如maven的deploySite.sh內容是:

mvn -Preporting site site:stage $@
mvn scm-publish:publish-scm $@
複製程式碼

修改為帶雙引號則更為安全:

mvn -Preporting site site:stage "$@"
mvn scm-publish:publish-scm "$@""
複製程式碼

總結

通過測試,可以得出下面的經驗:

  1. 如果希望位置引數原封不動的將引數傳遞出去,需使用帶雙引號的@符號,即"$@"
  2. 腳本里對變數的引用最好總是加上雙引號,否則可能引起不可預知的變數擴充套件(variable expansion),比如上面例子,當前目錄有個javac檔案,導致"java*"被擴充套件成了"javac"

See also


《Java與Linux學習週刊》每週五發布,同步更新於:Github知乎掘金