鍍金池/ 教程/ Linux/ 位置參數(shù)
網(wǎng)絡(luò)系統(tǒng)
打印
重定向
使用命令
位置參數(shù)
權(quán)限
文本處理
疑難排解
layout: book-zh title: 自定制 shell 提示符
查找文件
layout: book-zh title: vi 簡(jiǎn)介
shell 環(huán)境
什么是 shell
編譯程序
鍵盤高級(jí)操作技巧
流程控制:case 分支
流程控制:if 分支結(jié)構(gòu)
layout: book-zh title: 軟件包管理
進(jìn)程
存儲(chǔ)媒介
格式化輸出
編寫第一個(gè) Shell 腳本
啟動(dòng)一個(gè)項(xiàng)目
流程控制:while/until 循環(huán)
文件系統(tǒng)中跳轉(zhuǎn)
字符串和數(shù)字
讀取鍵盤輸入
歸檔和備份
探究操作系統(tǒng)
流程控制:for 循環(huán)
自頂向下設(shè)計(jì)
數(shù)組
操作文件和目錄
奇珍異寶
從 shell 眼中看世界
正則表達(dá)式

位置參數(shù)

現(xiàn)在我們的程序還缺少一種本領(lǐng),就是接收和處理命令行選項(xiàng)和參數(shù)的能力。在這一章中,我們將探究一些能 讓程序訪問命令行內(nèi)容的 shell 性能。

訪問命令行

shell 提供了一個(gè)稱為位置參數(shù)的變量集合,這個(gè)集合包含了命令行中所有獨(dú)立的單詞。這些變量按照從0到9給予命名。 可以以這種方式講明白:

#!/bin/bash
# posit-param: script to view command line parameters
echo "
\$0 = $0
\$1 = $1
\$2 = $2
\$3 = $3
\$4 = $4
\$5 = $5
\$6 = $6
\$7 = $7
\$8 = $8
\$9 = $9
"

一個(gè)非常簡(jiǎn)單的腳本,顯示從 $0 到 $9 所有變量的值。當(dāng)不帶命令行參數(shù)執(zhí)行該腳本時(shí),輸出結(jié)果如下:

[me@linuxbox ~]$ posit-param
$0 = /home/me/bin/posit-param
$1 =
$2 =
$3 =
$4 =
$5 =
$6 =
$7 =
$8 =
$9 =

即使不帶命令行參數(shù),位置參數(shù) $0 總會(huì)包含命令行中出現(xiàn)的第一個(gè)單詞,也就是已執(zhí)行程序的路徑名。 當(dāng)帶參數(shù)執(zhí)行腳本時(shí),我們看看輸出結(jié)果:

[me@linuxbox ~]$ posit-param a b c d
$0 = /home/me/bin/posit-param
$1 = a
$2 = b
$3 = c
$4 = d
$5 =
$6 =
$7 =
$8 =
$9 =

注意: 實(shí)際上通過參數(shù)展開方式你可以訪問的參數(shù)個(gè)數(shù)多于9個(gè)。只要指定一個(gè)大于9的數(shù)字,用花括號(hào)把該數(shù)字括起來就可以。 例如 ${10}, ${55}, ${211},等等。

確定參數(shù)個(gè)數(shù)

另外 shell 還提供了一個(gè)名為 $#,可以得到命令行參數(shù)個(gè)數(shù)的變量:

#!/bin/bash
# posit-param: script to view command line parameters
echo "
Number of arguments: $#
\$0 = $0
\$1 = $1
\$2 = $2
\$3 = $3
\$4 = $4
\$5 = $5
\$6 = $6
\$7 = $7
\$8 = $8
\$9 = $9
"

結(jié)果是:

[me@linuxbox ~]$ posit-param a b c d
Number of arguments: 4
$0 = /home/me/bin/posit-param
$1 = a
$2 = b
$3 = c
$4 = d
$5 =
$6 =
$7 =
$8 =
$9 =

shift - 訪問多個(gè)參數(shù)的利器

但是如果我們給一個(gè)程序添加大量的命令行參數(shù),會(huì)怎么樣呢? 正如下面的例子:

[me@linuxbox ~]$ posit-param *
Number of arguments: 82
$0 = /home/me/bin/posit-param
$1 = addresses.ldif
$2 = bin
$3 = bookmarks.html
$4 = debian-500-i386-netinst.iso
$5 = debian-500-i386-netinst.jigdo
$6 = debian-500-i386-netinst.template
$7 = debian-cd_info.tar.gz
$8 = Desktop
$9 = dirlist-bin.txt

在這個(gè)例子運(yùn)行的環(huán)境下,通配符 * 展開成82個(gè)參數(shù)。我們?nèi)绾翁幚砟敲炊嗟膮?shù)? 為此,shell 提供了一種方法,盡管笨拙,但可以解決這個(gè)問題。執(zhí)行一次 shift 命令, 就會(huì)導(dǎo)致所有的位置參數(shù) “向下移動(dòng)一個(gè)位置”。事實(shí)上,用 shift 命令也可以 處理只有一個(gè)參數(shù)的情況(除了其值永遠(yuǎn)不會(huì)改變的變量 $0):

#!/bin/bash
# posit-param2: script to display all arguments
count=1
while [[ $# -gt 0 ]]; do
    echo "Argument $count = $1"
    count=$((count + 1))
    shift
done

每次 shift 命令執(zhí)行的時(shí)候,變量 $2 的值會(huì)移動(dòng)到變量 $1 中,變量 $3 的值會(huì)移動(dòng)到變量 $2 中,依次類推。 變量 $# 的值也會(huì)相應(yīng)的減1。

在該 posit-param2 程序中,我們編寫了一個(gè)計(jì)算剩余參數(shù)數(shù)量,只要參數(shù)個(gè)數(shù)不為零就會(huì)繼續(xù)執(zhí)行的 while 循環(huán)。 我們顯示當(dāng)前的位置參數(shù),每次循環(huán)迭代變量 count 的值都會(huì)加1,用來計(jì)數(shù)處理的參數(shù)數(shù)量, 最后,執(zhí)行 shift 命令加載 $1,其值為下一個(gè)位置參數(shù)的值。這里是程序運(yùn)行后的輸出結(jié)果:

[me@linuxbox ~]$ posit-param2 a b c d
Argument 1 = a
Argument 2 = b
Argument 3 = c
Argument 4 = d

簡(jiǎn)單應(yīng)用

即使沒有 shift 命令,也可以用位置參數(shù)編寫一個(gè)有用的應(yīng)用。舉例說明,這里是一個(gè)簡(jiǎn)單的輸出文件信息的程序:

#!/bin/bash
# file_info: simple file information program
PROGNAME=$(basename $0)
if [[ -e $1 ]]; then
    echo -e "\nFile Type:"
    file $1
    echo -e "\nFile Status:"
    stat $1
else
    echo "$PROGNAME: usage: $PROGNAME file" >&2
    exit 1
fi

這個(gè)程序顯示一個(gè)具體文件的文件類型(由 file 命令確定)和文件狀態(tài)(來自 stat 命令)。該程序一個(gè)有意思 的特點(diǎn)是 PROGNAME 變量。它的值就是 basename $0 命令的執(zhí)行結(jié)果。這個(gè) basename 命令清除 一個(gè)路徑名的開頭部分,只留下一個(gè)文件的基本名稱。在我們的程序中,basename 命令清除了包含在 $0 位置參數(shù) 中的路徑名的開頭部分,$0 中包含著我們示例程序的完整路徑名。當(dāng)構(gòu)建提示信息正如程序結(jié)尾的使用信息的時(shí)候, basename $0 的執(zhí)行結(jié)果就很有用處。按照這種方式編碼,可以重命名該腳本,且程序信息會(huì)自動(dòng)調(diào)整為 包含相應(yīng)的程序名稱。

Shell 函數(shù)中使用位置參數(shù)

正如位置參數(shù)被用來給 shell 腳本傳遞參數(shù)一樣,它們也能夠被用來給 shell 函數(shù)傳遞參數(shù)。為了說明這一點(diǎn), 我們將把 file_info 腳本轉(zhuǎn)變成一個(gè) shell 函數(shù):

file_info () {
  # file_info: function to display file information
  if [[ -e $1 ]]; then
      echo -e "\nFile Type:"
      file $1
      echo -e "\nFile Status:"
      stat $1
  else
      echo "$FUNCNAME: usage: $FUNCNAME file" >&2
      return 1
  fi
}

現(xiàn)在,如果一個(gè)包含 shell 函數(shù) file_info 的腳本調(diào)用該函數(shù),且?guī)в幸粋€(gè)文件名參數(shù),那這個(gè)參數(shù)會(huì)傳遞給 file_info 函數(shù)。

通過此功能,我們可以寫出許多有用的 shell 函數(shù),這些函數(shù)不僅能在腳本中使用,也可以用在 .bashrc 文件中。

注意那個(gè) PROGNAME 變量已經(jīng)改成 shell 變量 FUNCNAME 了。shell 會(huì)自動(dòng)更新 FUNCNAME 變量,以便 跟蹤當(dāng)前執(zhí)行的 shell 函數(shù)。注意位置參數(shù) $0 總是包含命令行中第一項(xiàng)的完整路徑名(例如,該程序的名字), 但不會(huì)包含這個(gè)我們可能期望的 shell 函數(shù)的名字。

處理集體位置參數(shù)

有時(shí)候把所有的位置參數(shù)作為一個(gè)集體來管理是很有用的。例如,我們可能想為另一個(gè)程序編寫一個(gè) “包裹程序”。 這意味著我們會(huì)創(chuàng)建一個(gè)腳本或 shell 函數(shù),來簡(jiǎn)化另一個(gè)程序的執(zhí)行。包裹程序提供了一個(gè)神秘的命令行選項(xiàng) 列表,然后把這個(gè)參數(shù)列表傳遞給下一級(jí)的程序。

為此 shell 提供了兩種特殊的參數(shù)。他們二者都能擴(kuò)展成完整的位置參數(shù)列表,但以相當(dāng)微妙的方式略有不同。它們是:

表 32-1: * 和 @ 特殊參數(shù)
參數(shù) 描述
$* 展開成一個(gè)從1開始的位置參數(shù)列表。當(dāng)它被用雙引號(hào)引起來的時(shí)候,展開成一個(gè)由雙引號(hào)引起來 的字符串,包含了所有的位置參數(shù),每個(gè)位置參數(shù)由 shell 變量 IFS 的第一個(gè)字符(默認(rèn)為一個(gè)空格)分隔開。
$@ 展開成一個(gè)從1開始的位置參數(shù)列表。當(dāng)它被用雙引號(hào)引起來的時(shí)候, 它把每一個(gè)位置參數(shù)展開成一個(gè)由雙引號(hào)引起來的分開的字符串。

下面這個(gè)腳本用程序中展示了這些特殊參數(shù):

#!/bin/bash
# posit-params3 : script to demonstrate $* and $@
print_params () {
    echo "\$1 = $1"
    echo "\$2 = $2"
    echo "\$3 = $3"
    echo "\$4 = $4"
}
pass_params () {
    echo -e "\n" '$* :';      print_params   $*
    echo -e "\n" '"$*" :';    print_params   "$*"
    echo -e "\n" '$@ :';      print_params   $@
    echo -e "\n" '"$@" :';    print_params   "$@"
}
pass_params "word" "words with spaces"

在這個(gè)相當(dāng)復(fù)雜的程序中,我們創(chuàng)建了兩個(gè)參數(shù): “word” 和 “words with spaces”,然后把它們 傳遞給 pass_params 函數(shù)。這個(gè)函數(shù),依次,再把兩個(gè)參數(shù)傳遞給 print_params 函數(shù), 使用了特殊參數(shù) $* 和 $@ 提供的四種可用方法。腳本運(yùn)行后,揭示了這兩個(gè)特殊參數(shù)存在的差異:

[me@linuxbox ~]$ posit-param3
 $* :
$1 = word
$2 = words
$3 = with
$4 = spaces
 "$*" :
$1 = word words with spaces
$2 =
$3 =
$4 =
 $@ :
$1 = word
$2 = words
$3 = with
$4 = spaces
 "$@" :
$1 = word
$2 = words with spaces
$3 =
$4 =

通過我們的參數(shù),$* 和 $@ 兩個(gè)都產(chǎn)生了一個(gè)有四個(gè)詞的結(jié)果:

word words with spaces
"$*" produces a one word result:
    "word words with spaces"
"$@" produces a two word result:
    "word" "words with spaces"

這個(gè)結(jié)果符合我們實(shí)際的期望。我們從中得到的教訓(xùn)是盡管 shell 提供了四種不同的得到位置參數(shù)列表的方法, 但到目前為止, "$@" 在大多數(shù)情況下是最有用的方法,因?yàn)樗A袅嗣恳粋€(gè)位置參數(shù)的完整性。

一個(gè)更復(fù)雜的應(yīng)用

經(jīng)過長(zhǎng)時(shí)間的間斷,我們將恢復(fù)程序 sys_info_page 的工作。我們下一步要給程序添加如下幾個(gè)命令行選項(xiàng):

  • 輸出文件。 我們將添加一個(gè)選項(xiàng),以便指定一個(gè)文件名,來包含程序的輸出結(jié)果。 選項(xiàng)格式要么是 -f file,要么是 --file file

  • 交互模式。這個(gè)選項(xiàng)將提示用戶輸入一個(gè)輸出文件名,然后判斷是否指定的文件已經(jīng)存在了。如果文件存在, 在覆蓋這個(gè)存在的文件之前會(huì)提示用戶。這個(gè)選項(xiàng)可以通過 -i 或者 --interactive 來指定。

  • 幫助。指定 -h 選項(xiàng) 或者是 --help 選項(xiàng),可導(dǎo)致程序輸出提示性的使用信息。

這里是處理命令行選項(xiàng)所需的代碼:

usage () {
    echo "$PROGNAME: usage: $PROGNAME [-f file | -i]"
    return
}
# process command line options
interactive=
filename=
while [[ -n $1 ]]; do
    case $1 in
    -f | --file)            shift
                            filename=$1
                            ;;
    -i | --interactive)     interactive=1
                            ;;
    -h | --help)            usage
                            exit
                            ;;
    *)                      usage >&2
                            exit 1
                            ;;
    esac
    shift
done

首先,我們添加了一個(gè)叫做 usage 的 shell 函數(shù),以便顯示幫助信息,當(dāng)啟用幫助選項(xiàng)或敲寫了一個(gè)未知選項(xiàng)的時(shí)候。

下一步,我們開始處理循環(huán)。當(dāng)位置參數(shù) $1 不為空的時(shí)候,這個(gè)循環(huán)會(huì)持續(xù)運(yùn)行。在循環(huán)的底部,有一個(gè) shift 命令, 用來提升位置參數(shù),以便確保該循環(huán)最終會(huì)終止。在循環(huán)體內(nèi),我們使用了一個(gè) case 語句來檢查當(dāng)前位置參數(shù)的值, 看看它是否匹配某個(gè)支持的選項(xiàng)。若找到了匹配項(xiàng),就會(huì)執(zhí)行與之對(duì)應(yīng)的代碼。若沒有,就會(huì)打印出程序使用信息, 該腳本終止且執(zhí)行錯(cuò)誤。

處理 -f 參數(shù)的方式很有意思。當(dāng)監(jiān)測(cè)到 -f 參數(shù)的時(shí)候,會(huì)執(zhí)行一次 shift 命令,從而提升位置參數(shù) $1 為 伴隨著 -f 選項(xiàng)的 filename 參數(shù)。

我們下一步添加代碼來實(shí)現(xiàn)交互模式:

# interactive mode
if [[ -n $interactive ]]; then
    while true; do
        read -p "Enter name of output file: " filename
        if [[ -e $filename ]]; then
            read -p "'$filename' exists. Overwrite? [y/n/q] > "
            case $REPLY in
            Y|y)    break
                    ;;
            Q|q)    echo "Program terminated."
                    exit
                    ;;
            *)      continue
                    ;;
            esac
        elif [[ -z $filename ]]; then
            continue
        else
            break
        fi
    done
fi

若 interactive 變量不為空,就會(huì)啟動(dòng)一個(gè)無休止的循環(huán),該循環(huán)包含文件名提示和隨后存在的文件處理代碼。 如果所需要的輸出文件已經(jīng)存在,則提示用戶覆蓋,選擇另一個(gè)文件名,或者退出程序。如果用戶選擇覆蓋一個(gè) 已經(jīng)存在的文件,則會(huì)執(zhí)行 break 命令終止循環(huán)。注意 case 語句是怎樣只檢測(cè)用戶選擇了覆蓋還是退出選項(xiàng)。 其它任何選擇都會(huì)導(dǎo)致循環(huán)繼續(xù)并提示用戶再次選擇。

為了實(shí)現(xiàn)這個(gè)輸出文件名的功能,首先我們必須把現(xiàn)有的這個(gè)寫頁面(page-writing)的代碼轉(zhuǎn)變成一個(gè) shell 函數(shù), 一會(huì)兒就會(huì)明白這樣做的原因:

write_html_page () {
    cat <<- _EOF_
        <HTML>
            <HEAD>
                <TITLE>$TITLE</TITLE>
            </HEAD>
            <BODY>
                <H1>$TITLE</H1>
                <P>$TIMESTAMP</P>
                $(report_uptime)
                $(report_disk_space)
                $(report_home_space)
            </BODY>
        </HTML>
    _EOF_
    return
}
# output html page
if [[ -n $filename ]]; then
    if touch $filename && [[ -f $filename ]]; then
        write_html_page > $filename
    else
        echo "$PROGNAME: Cannot write file '$filename'" >&2
        exit 1
    fi
else
    write_html_page
fi

解決 -f 選項(xiàng)邏輯的代碼出現(xiàn)在以上程序片段的末尾。在這段代碼中,我們測(cè)試一個(gè)文件名是否存在,若文件名存在, 則執(zhí)行另一個(gè)測(cè)試看看該文件是不是可寫文件。為此,會(huì)運(yùn)行 touch 命令,緊隨其后執(zhí)行一個(gè)測(cè)試,來決定 touch 命令 創(chuàng)建的文件是否是個(gè)普通文件。這兩個(gè)測(cè)試考慮到了輸入是無效路徑名(touch 命令執(zhí)行失?。鸵粋€(gè)普通文件已經(jīng)存在的情況。

正如我們所看到的,程序調(diào)用 write_html_page 函數(shù)來生成實(shí)際的網(wǎng)頁。函數(shù)輸出要么直接定向到 標(biāo)準(zhǔn)輸出(若 filename 變量為空的話)要么重定向到具體的文件中。

總結(jié)

伴隨著位置參數(shù)的加入,現(xiàn)在我們能編寫相當(dāng)具有功能性的腳本。例如,重復(fù)性的任務(wù),位置參數(shù)使得編寫 非常有用的,可以放置在一個(gè)用戶的 .bashrc 文件中的 shell 函數(shù)成為可能。

我們的 sys_info_page 程序日漸精進(jìn)。這里是一個(gè)完整的程序清單,最新的更改用高亮顯示:

#!/bin/bash
# sys_info_page: program to output a system information page
PROGNAME=$(basename $0)
TITLE="System Information Report For $HOSTNAME"
CURRENT_TIME=$(date +"%x %r %Z")
TIMESTAMP="Generated $CURRENT_TIME, by $USER"
report_uptime () {
    cat <<- _EOF_
        <H2>System Uptime</H2>
        <PRE>$(uptime)</PRE>
    _EOF_
    return
}
report_disk_space () {
    cat <<- _EOF_
        <H2>Disk Space Utilization</H2>
        <PRE>$(df -h)</PRE>
    _EOF_
    return
}
report_home_space () {
    if [[ $(id -u) -eq 0 ]]; then
        cat <<- _EOF_
            <H2>Home Space Utilization (All Users)</H2>
            <PRE>$(du -sh /home/*)</PRE>
        _EOF_
    else
        cat <<- _EOF_
            <H2>Home Space Utilization ($USER)</H2>
            <PRE>$(du -sh $HOME)</PRE>
        _EOF_
    fi
    return
}
usage () {
    echo "$PROGNAME: usage: $PROGNAME [-f file | -i]"
    return
}
write_html_page () {
    cat <<- _EOF_
        <HTML>
            <HEAD>
                <TITLE>$TITLE</TITLE>
            </HEAD>
            <BODY>
                <H1>$TITLE</H1>
                <P>$TIMESTAMP</P>
                $(report_uptime)
                $(report_disk_space)
                $(report_home_space)
            </BODY>
        </HTML>
    _EOF_
    return
}
# process command line options
interactive=
filename=
while [[ -n $1 ]]; do
    case $1 in
        -f | --file)          shift
                              filename=$1
                              ;;
        -i | --interactive)   interactive=1
                              ;;
        -h | --help)          usage
                              exit
                              ;;
        *)                    usage >&2
                              exit 1
                              ;;
    esac
    shift
done
# interactive mode
if [[ -n $interactive ]]; then
    while true; do
        read -p "Enter name of output file: " filename
        if [[ -e $filename ]]; then
            read -p "'$filename' exists. Overwrite? [y/n/q] > "
            case $REPLY in
                Y|y)    break
                        ;;
                Q|q)    echo "Program terminated."
                        exit
                        ;;
                *)      continue
                        ;;
            esac
        fi
    done
fi
# output html page
if [[ -n $filename ]]; then
    if touch $filename && [[ -f $filename ]]; then
        write_html_page > $filename
    else
        echo "$PROGNAME: Cannot write file '$filename'" >&2
        exit 1
    fi
else
    write_html_page
fi

我們還沒有完成。仍然還有許多事情我們可以做,可以改進(jìn)。

拓展閱讀