RM新时代网站-首页

0
  • 聊天消息
  • 系統(tǒng)消息
  • 評論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會(huì)員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識(shí)你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

Bash編程常見錯(cuò)誤范例及原因分析

馬哥Linux運(yùn)維 ? 來源:團(tuán)子的小窩 ? 作者:團(tuán)子的小窩 ? 2022-06-12 16:48 ? 次閱讀

Bash Pitfalls[1]文章介紹了 40 多條日常 Bash 編程中,老手和新手都容易忽略的錯(cuò)誤編程習(xí)慣。每條作者在給出錯(cuò)誤的范例上,詳細(xì)分析與解釋錯(cuò)誤的原因,同時(shí)給出正確的改寫建議。文中有不少引用的文章,也值得大家仔細(xì)閱讀。仔細(xì)閱讀了這篇文章后,收獲很多,不感獨(dú)享,把這篇文章以半翻譯半筆記的形式分享給大家。

1. for i in $(ls *.mp3)

Bash 寫循環(huán)代碼的時(shí)候,確實(shí)比較容易犯下面的錯(cuò)誤:

foriin$(ls*.mp3);do#錯(cuò)誤!
somecommand$i#錯(cuò)誤!
done

foriin$(ls)#錯(cuò)誤!
foriin`ls`#錯(cuò)誤!

foriin$(find.-typef)#錯(cuò)誤!
foriin`find.-typef`#錯(cuò)誤!

files=($(find.-typef))#錯(cuò)誤!
foriin${files[@]}#錯(cuò)誤!

這里主要兩個(gè)問題:

  • 使用命令展開時(shí)不帶引號(hào),其執(zhí)行結(jié)果會(huì)使用 IFS 作為分隔符,拆分成參數(shù)傳遞給 for 循環(huán)處理;
  • 不應(yīng)該讓腳本去解析 ls 命令的結(jié)果[2];

我們不能避免某些文件名中包含空格,Shell 會(huì)對$(ls *.mp3)展開的結(jié)果會(huì)被做單詞拆分 (WordSplitting[3]) 的處理。假設(shè)有一個(gè)文件,名字為 01 - Don't Eat the Yellow Snow.mp3,for 循環(huán)處理的時(shí)候,會(huì)今次遍歷文件名中的每個(gè)單詞:01, -, Don't, Eat 等等:

$foriin$(ls*.mp3);doecho$i;done
01
-
Don't
Eat
the
Yellow
Snow.mp3

比這更差的情況是,上面命令展開的結(jié)果可能被 Shell 進(jìn)一步處理,比如文件名展開[4]。比如,ls 執(zhí)行的結(jié)果中包含*號(hào),按照通配符的規(guī)則 , * 號(hào)會(huì)被展開成當(dāng)前目錄下的所有文件 :

$touch"1*.mp3""1.mp3""11.mp3""12.mp3"
$foriin$(ls*.mp3);doecho$i;done
1*.mp31.mp311.mp312.mp3
1.mp3
11.mp3
12.mp3
1.mp3
11.mp3
12.mp3

不過,在這種場景下,你即使加上引號(hào),也是無濟(jì)于事的:

$foriin"$(ls*.mp3)";doecho--$i--;done
--1*.mp31.mp311.mp312.mp3--

加上引號(hào)后,ls 執(zhí)行的結(jié)果會(huì)被當(dāng)成一個(gè)整體,所以 for 循環(huán)只會(huì)執(zhí)行一次,達(dá)不到預(yù)期的效果。

事實(shí)上,這種情況下,根本不需要使用 ls 命令。ls 命令的結(jié)果本身就設(shè)計(jì)成給人讀的,而不是給腳本解析的。正確的處理方法是,直接使用文件名展開(通配符)的功能:

$foriin*.mp3;do
>echo"$i"
>done
1*.mp3
1.mp3
11.mp3
12.mp3

文件名展開是位于各種展開(花括號(hào)展開、變量替換、命令展開等)功能中的最后一個(gè)環(huán)節(jié),所以不會(huì)有之前不帶引號(hào)的命令展開的副作用。如果你需要遞歸地處理文件,可以考慮使用 Find 命令[5]

到這一步,之間的問題看樣子已經(jīng)修復(fù)了。但是,如果你進(jìn)一步思考,假設(shè)當(dāng)前目錄上沒有文件時(shí)會(huì)怎么樣?沒有文件的時(shí)候,*.mp3 不會(huì)被展開直接傳遞給 for 循環(huán)處理,所以這個(gè)時(shí)候循環(huán)還是會(huì)執(zhí)行一次。這種情況不是我們預(yù)期的行為。保險(xiǎn)起見,可以在循環(huán)處理的時(shí)候,檢查下文件是否存在:

#POSIX
foriin*.mp3;do
[-e"$i"]||continue
somecommand"$i"
done

如果你有使用引號(hào)[6]和避免單詞拆分[7]的習(xí)慣,你完全可以避免很多錯(cuò)誤。

注意下循環(huán)體內(nèi)部的 "$i",這里會(huì)導(dǎo)致下面我們要說的另外一個(gè)比較容易犯的錯(cuò)誤。

2. cp $file $target

上面的命令有什么問題呢?如果你提前知道,$file 和 $target 文件名中不會(huì)包含空格或者*號(hào)。否則,這行命令執(zhí)行前在經(jīng)過單詞拆分和文件名展開的時(shí)候會(huì)出現(xiàn)問題。所以,兩次強(qiáng)調(diào),在使用展開的地方切勿忘記使用引號(hào):

$cp--"$file""$target"

如果不帶引號(hào),當(dāng)你執(zhí)行如下命令時(shí)就會(huì)出錯(cuò):

$file="01-Don'tEattheYellowSnow.mp3"
$target="/tmp"
$cp$file$target
cp:cannotstat‘01’:Nosuchfileordirectory
..

如果帶上引號(hào),就不會(huì)有上面的問題,除非文件名以 '-' 開頭,在這種情況下,cp 會(huì)認(rèn)為你提供的是一個(gè)命令行選項(xiàng),這個(gè)錯(cuò)誤下面會(huì)介紹。

3. 文件名中包含短橫 '-'

文件名以 '-' 開頭會(huì)導(dǎo)致許多問題,*.mp3 這種通配符會(huì)根據(jù)當(dāng)前的locale[8]展開成一個(gè)列表,但在絕大多數(shù)環(huán)境下,'-' 排序的時(shí)候會(huì)排在大多數(shù)字母前。這個(gè)展開的列表傳遞給有些命令的時(shí)候,會(huì)錯(cuò)誤的將-filename 解析成命令行選項(xiàng)。這里有兩種方法來解決這個(gè)問題。

第一種方法是在命令和參數(shù)之間加上--,這種語法告訴命令不要繼續(xù)對--之后的內(nèi)容進(jìn)行命令行參數(shù) / 選項(xiàng)解析:

$cp--"$file""$target"

這種方法可以解這個(gè)問題,但是你需要在每個(gè)命令后面都要加上--,而且依賴具體的命令解析的方式,如果一些命令不兼容這種約定俗成的規(guī)范,這種做法是無效的。

另外一種方法是,確保文件名都使用相對或者絕對的路徑,以目錄開頭:

foriin./*.mp3;do
cp"$i"/target
...
done

這種情況下,即使某個(gè)文件以-開頭,展開后文件名依然是 ./-foo.mp3 這種形式,完全不會(huì)有問題。

4. [ $foo = "bar" ]

這是一個(gè)與第 2 個(gè)問題類似的問題,雖然用到了引號(hào),但是放錯(cuò)了位置,對于字符串字面值,除非有特殊符號(hào),否則不大需要用引號(hào)括起來。但是,你應(yīng)該把變量的值用括號(hào)括起來,從而避免它們包含空格或能通配符,這一點(diǎn)我們在前面的問題中都解釋過。

這個(gè)例子在以下情況下會(huì)出錯(cuò):

  • 如果 [中的變量不存在,或者為空,這個(gè)時(shí)候上面的例子最終解析結(jié)果是:

    [="bar"]#錯(cuò)誤!
    

    并且執(zhí)行會(huì)出錯(cuò):unary operator expected,因?yàn)?= 是二元操作符,它需要左右各一個(gè)操作數(shù)。

  • 如果變量值包含空格,它首先在執(zhí)行之前進(jìn)行單詞拆分,因此 [命令看到的樣子可能是這樣的:

    [multiplewordshere="bar"];
    

正確的做法應(yīng)該是:

#POSIX
["$foo"=bar]

這種寫法,在 POSIX 兼容的實(shí)現(xiàn)中都不會(huì)有問題,即使 $foo 以短橫 "-" 開頭,因?yàn)?POSIX 實(shí)現(xiàn)的 test 命令通過傳遞的參數(shù)來確定執(zhí)行的行為。

只有一些非常古老的 shell 可能會(huì)遇到問題,這個(gè)時(shí)候你可以使用下面的寫法來解決(相信你肯定看到過這種寫法):

#POSIX/Bourne
[x"$foo"=xbar]

在 Bash 中,還有另外一種選擇是使用[[關(guān)鍵字[9]

#Bash/Ksh
[[$foo==bar]]

這里你不需要使用引號(hào),因?yàn)樵?[[里面參數(shù)不會(huì)進(jìn)行展開,當(dāng)然帶上引號(hào)也不會(huì)有錯(cuò)。

不過有一點(diǎn)要注意的是,[[里的 == 不僅僅是文本比較,它會(huì)檢查左邊的值是否匹配右側(cè)的表達(dá)式,== 右側(cè)的值加上引號(hào),會(huì)讓它成為一個(gè)普通的字面量,*? 等通配符會(huì)失去特殊含義。

5. cd $(dirname "$f")

這又是一個(gè)引號(hào)的問題,命令展開的結(jié)果會(huì)進(jìn)一步地進(jìn)行單詞拆分或者文件名展開。因此下面的寫法才是正確的:

cd"$(dirname"$f")"

但是,上面引號(hào)的寫法可能比較怪異,你可能會(huì)認(rèn)為第一、二個(gè)引號(hào),第三、四個(gè)引號(hào)是一組的。

但是事實(shí)上,Bash 將命令替換里面的引號(hào)當(dāng)成一組,外面的當(dāng)成另外一組。如果你是用反引號(hào)的寫法,引號(hào)的行為就不是這樣的了,所以[$() 寫法更加推薦](http://mywiki.wooledge.org/BashFAQ/082 "$() 寫法更加推薦")。

6. [ "$foo" = bar && "$bar" = foo ]

不要在test 命令[10]內(nèi)部使用 &&,Bash 解析器會(huì)把你的命令分隔成兩個(gè)命令,在 && 之前和之后。你應(yīng)該使用下面的寫法:

[bar="$foo"]&&[foo="$bar"]#POSIX
[[$foo=bar&&$bar=foo]]#Bash/Ksh

盡量避免使用下面的寫法,雖然它是正確的,但是這種寫法可移植性不好,并且已經(jīng)在 POSIX-2008 中被廢棄:

[bar="$foo"-afoo="$bar"]

7. [[ $foo > 7 ]]

原文作者認(rèn)為算術(shù)比較不應(yīng)該用 [[,而是用 ((,我沒弄明白是為什么。

如果有理解的同學(xué),歡迎以評論回復(fù),謝謝。

8. grep foo bar | while read -r; do ((count++)); done

這種寫法初看沒有問題,但是你會(huì)發(fā)現(xiàn)當(dāng)執(zhí)行完后,count 變量并沒有變化。原因是管道后面的命令是在一個(gè)子 Shell[11]中執(zhí)行的。

POSIX 規(guī)范并沒有說明管道的最后一個(gè)命令是不是在子 Shell 中執(zhí)行的。一些 shell,例如 ksh93 或者 Bash>=4.2 可以通過shopt -s lastpipe命令,指明管道中的最后一個(gè)命令在當(dāng)前 shell 中執(zhí)行。由于篇幅限制,在此就不展開,有興趣的可以看Bash FAQ #24[12]

9. if [grep foo myfile]

初學(xué)者會(huì)錯(cuò)誤地認(rèn)為,[是 if 語法的一部分,正如 C 語言中的 if ()。但是事實(shí)并非如此,if 后面跟著的是一個(gè)命令,[是一個(gè)命令,它是內(nèi)置命令 test 的簡寫形式,只不過它要求最后一個(gè)參數(shù)必須是]。下面兩種寫法是一樣的:

#POSIX
if[false];thenecho"HELP";fi
iftestfalse;thenecho"HELP";fi

兩個(gè)都是檢查參數(shù) "false" 是不是非空的,所以上面兩個(gè)語句都會(huì)輸出 HELP。

if 語句的語法是:

if COMMANDS
then 
elif  # optional
then 
else  # optional
fi # required

再次強(qiáng)調(diào),[是一個(gè)命令,它同其它常規(guī)的命令一樣接受參數(shù)。if 是一個(gè)復(fù)合命令,它包含其它命令,[并不是 if 語法中的一部分。

如果你想根據(jù) grep 命令的結(jié)果來做事情,你不需要把 grep 放到 [里面,只需要在 if 后面緊跟 grep 即可:

ifgrep-qfooregexmyfile;then
...
fi

如果 grep 在 myfile 中找到匹配的行,它的執(zhí)行結(jié)果為 0(true),then 后面的部分就會(huì)執(zhí)行。

10. if [bar="$foo"]; then ...

正如上一個(gè)問題中提到的,[是一個(gè)命令,它的參數(shù)之間必須用空格分隔。

11. if [ [ a = b ] && [ c = d ] ]; then ...

不要用把 [命令看成 C 語言中 if 語句的條件一樣,它是一個(gè)命令。

如果你想表達(dá)一個(gè)復(fù)合的條件表達(dá)式,可以這樣寫:

if[a=b]&&[c=d];then...

注意,if 后面有兩個(gè)命令,它們用 && 分開。等價(jià)于下面的寫法:

iftesta=b&&testc=d;then...

如果第一個(gè) test(或者 [) 命令返回 false,then 后面的語句不會(huì)執(zhí)行;如果第一個(gè)返回 true,第二個(gè) test 命令會(huì)執(zhí)行;只有第二個(gè)命令同樣返回 true 的情況下,then 后面的語句才會(huì)執(zhí)行。

除此之外,還可以使用 [[關(guān)鍵字,因?yàn)樗С?&& 的用法:

if[[a=b&&c=d]];then...

12. read $foo

read 命令中你不需要在變量名之前使用 $。如果你想把讀入的數(shù)據(jù)存放到名為 foo 的變量中,下面的寫法就夠了:

readfoo

或者,更加安全地方法:

IFS=read-rfoo

read $foo會(huì)把一行的內(nèi)容讀入到變量中,該變量的名稱存儲(chǔ)在 $foo 中。所以兩者的含義是完全不一樣的。

13. cat file | sed s/foo/bar/ > file

你不應(yīng)該在一個(gè)管道中,從一個(gè)文件讀的同時(shí),再往相同的文件里面寫,這樣的后果是未知的。

你可以為此創(chuàng)建一個(gè)臨時(shí)文件,這種做法比較安全可靠:

#sed's/foo/bar/g'file>tmpfile&&mvtmpfilefile

或者,如果你用得是 GNU Sed 4.x 以上的版本,可以使用-i 選項(xiàng)即時(shí)修改文件的內(nèi)容:

#sed-i's/foo/bar/g'file

14. echo $foo

這種看似無害的命令往往會(huì)給初學(xué)者千萬極大的困擾,他們會(huì)懷疑是不是因?yàn)?$foo 變量的值是錯(cuò)誤的。事實(shí)卻是因?yàn)椋?foo 變量在這里沒有使用雙引號(hào),所以在解析的時(shí)候會(huì)進(jìn)行單詞拆分[13]文件名展開[14],最終導(dǎo)致執(zhí)行結(jié)果與預(yù)期大相徑庭:

msg="Pleaseenterafilenameoftheform*.zip"
echo$msg

這里整句話會(huì)被拆分成單詞,然后其中的通配符會(huì)被展開,例如 *.zip。當(dāng)你的用戶看到如下的結(jié)果時(shí),他們會(huì)怎樣想:

Pleaseenterafilenameoftheformfreenfss.ziplw35nfss.zip

再舉一個(gè)例子(假設(shè)當(dāng)前目錄下有以 .zip 結(jié)尾的文件):

var="*.zip"#var包括一個(gè)星號(hào),一個(gè)點(diǎn)號(hào)和zip
echo"$var"#輸出*.zip
echo$var#輸出所有以.zip結(jié)尾的文件

實(shí)際上,這里使用 echo 命令并不是絕對的安全。例如,當(dāng)變量的值包含-n 時(shí),echo 會(huì)認(rèn)為它是一個(gè)合法的選項(xiàng)而不是要輸出的內(nèi)容(當(dāng)然如果你能夠保證不會(huì)有-n 這種值,可以放心地使用 echo 命令)。

完全可靠的打印變量值的方法是使用 printf:

printf"%s
""$foo"

15. $foo=bar

略過

16. foo = bar

當(dāng)賦值時(shí),等號(hào)兩邊是不允許出現(xiàn)空格的,這同 C 語言不一樣。當(dāng)你寫下 foo = bar 時(shí),shell 會(huì)將該命令解析成三個(gè)單詞,然后第一個(gè)單詞 foo 會(huì)被認(rèn)為是一個(gè)命令,后面的內(nèi)容會(huì)被當(dāng)作命令參數(shù)。

同樣地,下面的寫法也是錯(cuò)誤的:

foo=bar#WRONG!
foo=bar#WRONG!
$foo=bar;#COMPLETELYWRONG!

正確的寫法應(yīng)該是這樣的:
"prettyprintlang-sh">
foo=bar#Right.
foo="bar"#MoreRight.

17. echo <

當(dāng)腳本需要嵌入大段的文本內(nèi)容時(shí),here document[15]往往是一個(gè)非常有用的工具,它將其中的文本作為命令的標(biāo)準(zhǔn)輸入。不過,echo 命令并不支持從標(biāo)準(zhǔn)輸入讀取內(nèi)容,所以下面的寫法是錯(cuò)誤的:

#Thisiswrong:
echo<'sitgoing?
EOF

正確的方法是,使用 cat 命令來完成:

#Thisiswhatyouweretryingtodo:
cat<'sitgoing?
EOF

或者可以使用雙引號(hào),它也可以跨越多行,而且因?yàn)?echo 命令是內(nèi)置命令,相同情況下它會(huì)更加高效:

echo"Helloworld
How'sitgoing?"

18. su -c 'some command'

這種寫法“幾乎”是正確的。問題是,在許多平臺(tái)上,su 支持 -c 參數(shù),但是它不一定是你認(rèn)為的。比如,在 OpenBSD 平臺(tái)上你這樣執(zhí)行會(huì)出錯(cuò):

$su-c'echohello'
su:onlythesuperusermayspecifyaloginclass

在這里,-c 是用于指定 login-class[16]。如果你想要傳遞 -c 'some command' 給 shell,最好在之前顯示地指定 username:

$suroot-c'somecommand'#Nowit'sright.

19. cd /foo; bar

如果你不檢查 cd 命令執(zhí)行是否成功,你可以會(huì)在錯(cuò)誤的目錄下執(zhí)行 bar 命令,這有可能會(huì)帶來災(zāi)難,比如 bar 命令是 rm -rf *。

你必須經(jīng)常檢查 cd 命令執(zhí)行是否有錯(cuò)誤,簡單的做法是:

cd/foo&&bar

如果在 cd 命令后有多個(gè)命令,你可以選擇這樣寫:

cd/foo||exit1
bar
baz
bat...#Lotsofcommands.

出錯(cuò)時(shí),cd 命令會(huì)報(bào)告無法改變當(dāng)前目錄,同時(shí)將錯(cuò)誤消息輸出到標(biāo)準(zhǔn)錯(cuò)誤,例如 "bash: cd: /foo: No such file or directory"。如果你想要在標(biāo)準(zhǔn)輸出同時(shí)輸出自定義的錯(cuò)誤提示,可以使用復(fù)合命令(command grouping[17]):

cd/net||{echo"Can'tread/net.Makesureyou'veloggedintotheSambanetwork,andtryagain.";exit1;}
do_stuff
more_stuff

注意,在{號(hào)和 echo 之間需要有一個(gè)空格,同時(shí)}之前要加上分號(hào)。

順便提一下,如果你要在腳本里頻繁改變當(dāng)前目錄,可以看看 pushd/popd/dirs 等命令,可能你在代碼里面寫的 cd/pwd 命令都是沒有必要的。

說到這,比較下下面兩種寫法:

find...-typed-print0|whileIFS=read-r-d''subdir;do
here=$PWD
cd"$subdir"&&whatever&&...
cd"$here"
done
find...-typed-print0|whileIFS=read-r-d''subdir;do
(cd"$subdir"||exit;whatever;...)
done

下面的寫法,在循環(huán)中 fork 了一個(gè)子 shell 進(jìn)程,子 shell 進(jìn)程中的 cd 命令僅會(huì)影響當(dāng)前 shell 的環(huán)境變量,所以父進(jìn)程中的環(huán)境命令不會(huì)被改變;當(dāng)執(zhí)行到下一次循環(huán)時(shí),無論之前的 cd 命令有沒有執(zhí)行成功,我們會(huì)回到相同的當(dāng)前目錄。這種寫法相較前面的用法,代碼更加干凈。

20. [ bar == "$foo" ]

正確的用法 :

[bar="$foo"]&&echoyes
[[bar==$foo]]&&echoyes

21. for i in {1..10}; do ./something &; done

你不應(yīng)該在 & 后面添加分號(hào),刪除它:

foriin{1..10};do./something&done

或者改成多行的形式:

foriin{1..10};do
./something&
done

& 和分號(hào)一樣也可以用作命令終止符,所以你不要將兩個(gè)混用到一起。一般情況下,分號(hào)可以被換行符替換,但是不是所有的換行符都可以用分號(hào)替換。

22. cmd1 && cmd2 || cmd3

有些人喜歡把 && 和 || 作為 if...then...else...fi 的簡寫語法,在多數(shù)情況下,這種寫法沒有問題。例如:

[[-s$errorlog]]&&echo"Uhoh,thereweresomeerrors."||echo"Successful."

但是,這種結(jié)構(gòu)并不是在所有情況下都完全等價(jià)于 if...fi 語法。這是因?yàn)樵?&& 后面的命令執(zhí)行結(jié)束時(shí)也會(huì)生成一個(gè)返回碼,如果該返回碼不是真值(0 代表 true),|| 后面的命令也會(huì)執(zhí)行,例如:

i=0
true&&((i++))||((i--))
echo$i#輸出0

看起來上面的結(jié)果應(yīng)該是返回 1,但是結(jié)果卻是輸出 0,為什么呢?原因是這里 i++ 和 i-- 都執(zhí)行了一遍。

其中,((i++)) 命令執(zhí)行算術(shù)運(yùn)算,表達(dá)式計(jì)算的結(jié)果為 0。這里和 C 語言一樣,表達(dá)式的結(jié)果為 0 被認(rèn)為是 false。所以當(dāng) i=0 的時(shí)候,((i++)) 命令執(zhí)行的返回碼為 1(false),從而會(huì)執(zhí)行接下來的 ((i--)) 命令。

如果我們在這里使用前綴自增運(yùn)算符的話,返回的結(jié)果恰恰為 1,因?yàn)?((++i)) 執(zhí)行的返回碼是 0(true):

i=0
true&&((++i))||((--i))
echo$i#Prints1

不過在你無法保證 y 的執(zhí)行結(jié)果是,絕對不要依靠 x && y || z 這種寫法。上面這種巧合,在 i 初始化為-1 時(shí)也會(huì)有問題。

如果你喜歡代碼更加安全健壯,建議使用 if...fi 語法:

i=0
iftrue;then
((i++))
else
((i--))
fi

echo$i#輸出1

23. echo "Hello World!"

在交互式的 Shell 環(huán)境下,你執(zhí)行以上命令會(huì)遇到下面的錯(cuò)誤:

bash:!":eventnotfound

這是因?yàn)?,在默認(rèn)的交互式 Shell 環(huán)境下,Bash 發(fā)現(xiàn)感嘆號(hào)時(shí)會(huì)執(zhí)行歷史命令展開。在 Shell 腳本中,這種行為是被禁止的,所以不會(huì)發(fā)生錯(cuò)誤。

不幸地是,你認(rèn)為明顯正確地修復(fù)方法,也不能工作,你會(huì)發(fā)現(xiàn)反斜杠并沒有轉(zhuǎn)義感嘆號(hào)[18]

#echo"hi!"
hi!

最簡單地方法是禁用 histexpand 選項(xiàng),你可以通過 set +H 或者 set +o histexpand 命令來完成。

下面四種寫法都可以解決:

#1.使用單引號(hào)
echo'HelloWorld!'

#2.禁用histexpand選項(xiàng)
set+H
echo"HelloWorld!"

#3.重置histchars
histchars=

#4.控制shell展開的順序,命令行歷史展開是在單詞拆分之前執(zhí)行的
#參見:Bash man 手冊的History Expansion一節(jié)
exmark='!'
echo"Hello,world$exmark"

24. for arg in $*

和大多數(shù) Shell 一樣,Bash 支持依次讀取單個(gè)命令行參數(shù)的語法。不過這并是 $*或者 $@,這兩種寫法都不正確,它們只能得到完整的參數(shù)列表,并非單獨(dú)的一個(gè)個(gè)參數(shù)。

正確的語法是(沒錯(cuò)要加上引號(hào)):

forargin"$@"

#或者更簡單的寫法
forarg

在腳本中遍歷所有參數(shù)是一個(gè)再普遍不過的需求,所以 for arg 默認(rèn)等價(jià)于 for arg in "$@"。$@ 使用雙引號(hào)后就有特殊的魔力,每個(gè)參數(shù)展開后成為一個(gè)獨(dú)立的單詞。("$@" 等價(jià)于 "$1" "$2" "$3" ...)

下面是一個(gè)錯(cuò)誤的例子 :

forxin$*;do
echo"parameter:'$x'"
done

執(zhí)行的結(jié)果為:

$./myscript'arg1'arg2arg3
parameter:'arg'
parameter:'1'
parameter:'arg2'
parameter:'arg3'

正確的寫法:

forxin"$@";do
echo"parameter:'$x'"
done

執(zhí)行的結(jié)果為:

$./myscript'arg1'arg2arg3
parameter:'arg1'
parameter:'arg2'
parameter:'arg3'

上面正確的例子中,第一個(gè)參數(shù) 'arg 1' 在展開后依然是一個(gè)獨(dú)立的單詞,而不會(huì)被拆分成兩個(gè)。

25. function foo()

這種寫法不一定能夠兼容所有 shell,兼容的寫法是:

foo(){
...
}

26. echo "~"

波浪號(hào)展開(Tilde expansion)[19]僅當(dāng)~沒有引號(hào)的時(shí)候發(fā)生,在上面的例子中,只會(huì)向標(biāo)準(zhǔn)輸出打印~符號(hào),而不是當(dāng)前用戶的家目錄路徑。

當(dāng)用引號(hào)將路徑參數(shù)引起來時(shí), 如果要用引號(hào)將相對于家目錄的路徑引起來時(shí),推薦使用 $HOME 而不是 ~, 假如 $HOME 目錄是 "/home/my photos",路徑中包含空格。

下面是幾組例子:

"~/dirwithspaces"#expandsto"~/dirwithspaces"
~"/dirwithspaces"#expandsto"~/dirwithspaces"
~/"dirwithspaces"#expandsto"/home/myphotos/dirwithspaces"
"$HOME/dirwithspaces"#expandsto"/home/myphotos/dirwithspaces"

27. local varname=$(command)

當(dāng)在函數(shù)中聲明局部變量時(shí),local[20]作為一個(gè)獨(dú)立的命令,這種奇特的行為有時(shí)候可能會(huì)導(dǎo)致困擾。比如,當(dāng)你想要捕獲命令替換[21]的返回碼時(shí),你就不能這樣做。local 命令的返回碼會(huì)覆蓋它。

這種情況下,你只能分成兩行寫:

localvarname
varname=$(command)
rc=$?

28. export foo=~/bar

export 與 local 命令一樣,并不是賦值語句的一部分。因此,在有些 Shell 下(比如 Bash),export foo=~/bar 會(huì)展開,但是有些(比如 Dash)卻不行。

下面是兩種比較健壯的寫法:

foo=~/bar;exportfoo#Right!
exportfoo="$HOME/bar"#Right!

29. sed 's/$foo/good bye/'

單引號(hào)內(nèi)部不會(huì)展開 $foo 變量,在這里可以換成雙引號(hào):

foo="hello";sed"s/$foo/goodbye/"

但是要注意,如果你使用了雙引號(hào),就需要考慮更多轉(zhuǎn)義的事情,具體可以看Quotes[22]這一頁。.

30. tr [A-Z] [a-z]

這里至少有三個(gè)問題。第一個(gè)問題是, [A-Z] 和 [a-z] 會(huì)被 shell 認(rèn)為是通配符。如果在當(dāng)前目錄下沒用文件名為單個(gè)字母的文件,這個(gè)命令似乎能正確執(zhí)行,否則會(huì)錯(cuò)誤地執(zhí)行,也許你會(huì)在周末耗費(fèi)許多小時(shí)來修復(fù)這個(gè)問題。

第二個(gè)問題是,這不是 tr 命令正確的寫法,實(shí)際上,上面的命令會(huì)把 [轉(zhuǎn)換成 [,將任意大寫字符轉(zhuǎn)換成對應(yīng)的小寫字符,將] 轉(zhuǎn)換成],所以你根本不需要加上括號(hào),這樣第一個(gè)問題就可以解決了。

第三個(gè)問題是,上面的命令執(zhí)行結(jié)果依賴于當(dāng)前的locale[23],A-Z 或者 a-z 不一定會(huì)代表 26 個(gè) ASCII 字母。實(shí)際上,在一些語言環(huán)境下,z 位于字母表的中間位置。這個(gè)問題的解法,取決于你希望發(fā)生的行為是哪一種。

如果你僅希望改變 26 個(gè)英文字母的大小寫(強(qiáng)制 locale 為 C):

LC_COLLATE=CtrA-Za-z

如果你希望根據(jù)實(shí)際的語言環(huán)境來轉(zhuǎn)換:

tr'[]''[]'

31. ps ax | grep gedit

這里的根本問題是正在運(yùn)行的進(jìn)程名稱,本質(zhì)上是不可靠的??赡軙?huì)有多個(gè)合法的 gedit 進(jìn)程,也有可能是別的東西偽裝成 gedit 進(jìn)程(改變執(zhí)行命令名稱是一件簡單的事情 ), 更多細(xì)節(jié)可以看ProcessManagement[24]這一篇文章。

執(zhí)行以上命令,往往會(huì)在結(jié)果中包含 grep 進(jìn)程:

#psax|grepgedit
10530?S6:23gedit
32118pts/0R+0:00grepgedit

這個(gè)時(shí)候,需要過濾多余的結(jié)果:

#psax|grep-vgrep|grepgedit

上面的寫法比較丑陋,另外一種方法是:

#psax|grep[g]edit

32. printf "$foo"

如果 $foo 變量的值中包括 或者 % 符號(hào),上面命令的執(zhí)行結(jié)果可能會(huì)出乎你的意料之外。

下面是正確的寫法:

printf%s"$foo"
printf'%s
'"$foo"

33. for i in {1..$n}

Bash 的命令解釋器[25]會(huì)優(yōu)先展開大括號(hào)[26],所以這時(shí)大括號(hào){}表達(dá)式里面看到的是文字上的 $n(沒有展開)。$n 不是一個(gè)數(shù)值,所以這里的大括號(hào){}并不會(huì)展開成數(shù)字列表??梢?,這導(dǎo)致很難使用大括號(hào)來展開大小只能在運(yùn)行時(shí)才知道的列表。

可以用下面的方法:

for((i=1;i<=n;?i++));?do
...
done

注:之前我也有寫過一篇文章來介紹這個(gè)問題:Shell 生成數(shù)字序列[27]。

34. if [[ $foo = $bar ]]

在 [[內(nèi)部,當(dāng) = 號(hào)右邊的值沒有用引號(hào)引起來,bash 會(huì)將它當(dāng)作模式來匹配,而不是一個(gè)簡單的字符串。所以,在上面的例子中 ,如果 bar 的值是一個(gè)*號(hào),執(zhí)行的結(jié)果永遠(yuǎn)是 true。

所以,如果你想檢查兩側(cè)的字符串是否相同,等號(hào)右側(cè)的值一定要用引號(hào)引起來。

if[[$foo="$bar"]]

如果你確實(shí)要執(zhí)行模式匹配,聰明的做法是取一個(gè)更加有意義的變量名(例如 $patt),或者加上注釋說明。

35. if [[ $foo =~ 'some RE' ]]

同上,如果 =~號(hào)右側(cè)的值加上引號(hào),它會(huì)散失特殊的正則表達(dá)式含義,而變成一個(gè)普通的字符串。

如果你想使用一個(gè)長的或者復(fù)雜的正則表達(dá)式,避免大量的反斜杠轉(zhuǎn)義,建議把它放在一個(gè)變量中:

re='someRE'
if[[$foo=~$re]]

36. [ -n $foo ] or [ -z $foo ]

這個(gè)例子中,$foo 沒有用引號(hào)引起來,當(dāng) $foo 包含空格或者 $foo 為空時(shí)都會(huì)出問題:

$foo="someword"&&[-n$foo]&&echoyes
-bash:[:some:binaryoperatorexpected

$foo=""&&[-n$foo]&&echoyes
yes

正確的寫法是:

[-n"$foo"]
[-z"$foo"]
[-n"$(somecommandwitha"$file"init)"]

[[-n$foo]]
[[-z$foo]]

37. [[ -e "$broken_symlink" ]] returns 1 even though $broken_symlink exists

這里-e 選項(xiàng)是看文件是否存在,當(dāng)緊跟的文件是一個(gè)軟鏈接時(shí),它不看軟鏈接是否存在,而是看實(shí)際指向的文件是否存在。所以當(dāng)軟鏈接損壞時(shí),即實(shí)際指向的文件被刪除后,-e 的結(jié)果返回 1。

所以如果你確實(shí)要判斷后面的文件是否存在,正確的寫法是:

[[-e"$broken_symlink"||-L"$broken_symlink"]]

38. ed file <<<"g/d{0,3}/s//e/g" fails

ed 命令使用的正則語法,不支持 0 次出現(xiàn)次數(shù),下面的就可以正常工作:

edfile<<<"g/d{1,3}/s//e/g"

略過,現(xiàn)在很少會(huì)有人用 ed 命令吧。

39. expr sub-string fails for "match"

下面的例子多數(shù)情況下運(yùn)行不會(huì)有問題:

word=abcde
expr"$word":".(.*)"
bcde

但是當(dāng) $work 不巧剛好是 match 時(shí),就有可能出錯(cuò)了(MAC OSX 下的 expr 命令不支持 match,所以依然能正常工作):

word=match
expr"$word":".(.*)"

原因是 match 是 expr 命令里面的一個(gè)特殊關(guān)鍵字,針對 GNU 系統(tǒng),解決方法是在前面加一個(gè) '+':

word=match
expr+"$word":".(.*)"
atch

'+' 號(hào)可以讓 expr 命令忽略后續(xù) token 的特殊含義。

另外一個(gè)建議是,不要再使用 expr 命令了,expr 能做的事情都可以用 Bash 原生支持的參數(shù)展開(Parameter Expansion[28])或者字符串展開(Substring Expansion)來完成。并且相同情況下,內(nèi)置的功能肯定比外部命令的效率要高。

上面的例子,目的是為了刪除單詞中的首字符,可以這樣做:

$word=match
$echo"${word#?}"#PE
atch
$echo"${word:1}"#SE
atch

40. On UTF-8 and Byte-Order Marks (BOM)

多數(shù)情況下,UNIX 下 UTF-8 類型的文本不需要使用 BOM,文本的編碼是根據(jù)當(dāng)前語言環(huán)境,MIME 類型或者其它文件元數(shù)據(jù)信息確定的。人為閱讀時(shí),不會(huì)因?yàn)樵谖募_始處加 BOM 標(biāo)記而腚影響,但是當(dāng)文件要被腳本解釋執(zhí)行時(shí),BOM 標(biāo)記會(huì)像 MS-DOS 下的換行符(^M)一樣奇怪。

41. content=$(

這里沒有什么錯(cuò)誤,不過你要知道命令替換會(huì)刪除結(jié)尾多余的換行符。

略過,原文給的優(yōu)化方法需要 Bash 4.2+ 以上的版本,手頭沒有這樣的環(huán)境。

42. somecmd 2>&1 >>logfile

這是一個(gè)很常見的錯(cuò)誤,顯然你本來是想將標(biāo)準(zhǔn)輸出與標(biāo)準(zhǔn)錯(cuò)誤輸出都重定向到文件 logfile 中,但是你會(huì)驚訝地發(fā)現(xiàn),標(biāo)準(zhǔn)錯(cuò)誤依然輸出到屏幕中。

這種行為的原因是,重定向[29]在命令執(zhí)行之前解析,并且是從左往右解析。上面的命令可以翻譯成,將標(biāo)準(zhǔn)錯(cuò)誤輸出重定向到標(biāo)準(zhǔn)輸出(此刻是終端),然后將標(biāo)準(zhǔn)輸出重定向到文件 logfile 中。所以,到最后,標(biāo)準(zhǔn)錯(cuò)誤并沒有重定向到文件中,而是依然輸出到終端:

somecmd>>logfile2>&1

更加詳細(xì)的說明見BashFAQ[30]。

43. cmd; (( ! $? )) || die

只有需要捕獲上一個(gè)命令的執(zhí)行結(jié)果進(jìn),才需要記錄 $? 的值,否則如果你只需要檢查上一個(gè)命令是否執(zhí)行成功,直接檢測命令:

ifcmd;then
...
fi

或者使用 case 語句來檢測多個(gè)或能的返回碼:

cmd
status=$?
case$statusin
0)
echosuccess>&2
;;
1)
echo'Mustsupplyaparameter,exiting.'>&2
exit1
;;
*)
echo'Unknownerror,exiting.'>&2
exit$status
esac

原文標(biāo)題:Bash 編程易錯(cuò)總結(jié)大全

文章出處:【微信公眾號(hào):馬哥Linux運(yùn)維】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。

審核編輯:湯梓紅
聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點(diǎn)僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問題,請聯(lián)系本站處理。 舉報(bào)投訴
  • 編程
    +關(guān)注

    關(guān)注

    88

    文章

    3614

    瀏覽量

    93686
  • Bash
    +關(guān)注

    關(guān)注

    0

    文章

    57

    瀏覽量

    10179

原文標(biāo)題:Bash 編程易錯(cuò)總結(jié)大全

文章出處:【微信號(hào):magedu-Linux,微信公眾號(hào):馬哥Linux運(yùn)維】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。

收藏 人收藏

    評論

    相關(guān)推薦

    keil常見錯(cuò)誤與警告分析

    keil常見錯(cuò)誤與警告分析
    發(fā)表于 08-19 09:38

    Keil_c編譯時(shí)常見錯(cuò)誤分析。

    本帖最后由 eehome 于 2013-1-5 09:46 編輯 用KEIL的時(shí)候,是不是有許多錯(cuò)誤不知道什么原因造成的?那就看看吧。里邊有各種常見錯(cuò)誤
    發(fā)表于 12-14 14:38

    labview的常見問題查找相關(guān)范例卻顯示腳本錯(cuò)誤

    我的labview安裝文件中有關(guān)于labview的相關(guān)范例,我在安裝的文件中能找到相關(guān)范例而且能打開,可是我用labview來查找相關(guān)范例卻顯示腳本錯(cuò)誤,沒辦法打開???怎么辦呀???
    發(fā)表于 11-07 11:35

    常見的充電問題分析手段以及該分析措施的原因

    前言:充電問題在手機(jī)研發(fā)和其它的移動(dòng)設(shè)備中是非常常見的一種類型,其中最主要的有兩種:充電電流小和類型識(shí)別錯(cuò)誤,那么在遇到這種充電問題時(shí),我們究竟應(yīng)該如何分析,才能一步步探究到問題的根源呢?本文即總結(jié)
    發(fā)表于 09-15 08:47

    bash高級編程相關(guān)資料下載

    電子發(fā)燒友網(wǎng)站提供《bash高級編程相關(guān)資料下載.pdf》資料免費(fèi)下載
    發(fā)表于 04-25 10:17 ?3次下載

    Matlab編程常見錯(cuò)誤與解決辦法

    Matlab編程常見錯(cuò)誤與解決辦法求人不如求己
    發(fā)表于 03-16 15:58 ?0次下載

    VHDL常見錯(cuò)誤及其原因分析

    初學(xué)VHDL,往往會(huì)碰到不少問題和錯(cuò)誤。例如:綜合時(shí)出現(xiàn)警告和錯(cuò)誤、編譯無法通過等問題,使得設(shè)計(jì)無法實(shí)現(xiàn);或者程序,綜合等均通過,但不能得到正確的仿真結(jié)果,即所設(shè)計(jì)的硬件與原意要求不符等等。通過資料的收集和歸納,總結(jié)出以下一些注意事項(xiàng),
    發(fā)表于 05-04 11:31 ?0次下載

    硬件設(shè)計(jì)中的30個(gè)錯(cuò)誤想法和原因分析

    硬件設(shè)計(jì)中的30個(gè)錯(cuò)誤想法和原因分析
    發(fā)表于 12-15 18:25 ?97次下載

    DDE編程范例

    DDE編程范例
    發(fā)表于 10-24 08:57 ?1次下載

    高級Bash 腳本編程指南

    高級Bash 腳本編程指南
    發(fā)表于 10-26 08:33 ?7次下載
    高級<b class='flag-5'>Bash</b> 腳本<b class='flag-5'>編程</b>指南

    西門子SETP7常見錯(cuò)誤分析

    西門子STEP7編程常見錯(cuò)誤分析解答。
    發(fā)表于 04-30 11:15 ?56次下載

    使用Bash處理變量的常見錯(cuò)誤

    Linux 中的 Bash 腳本語言支持對變量的操作。但是,如果您從事過其他流行的編程語言開發(fā),那么使用 Bash 處理變量會(huì)很容易出錯(cuò)。因?yàn)樗恼Z法與其他語法不同(甚至對某些開發(fā)人員來說有點(diǎn)奇怪)。
    的頭像 發(fā)表于 05-13 15:52 ?1361次閱讀

    常見的PLC編程邏輯錯(cuò)誤

    在編制PLC程序時(shí),不管是新手還是老手,都會(huì)犯下面的這種低級錯(cuò)誤。因?yàn)檫@種錯(cuò)誤是非語法上的,所以用編程軟件也不能檢查出錯(cuò)誤之處。此錯(cuò)誤一旦發(fā)
    的頭像 發(fā)表于 06-16 12:53 ?1223次閱讀
    最<b class='flag-5'>常見</b>的PLC<b class='flag-5'>編程</b>邏輯<b class='flag-5'>錯(cuò)誤</b>

    服務(wù)器錯(cuò)誤是怎么回事?常見錯(cuò)誤原因及解決方法匯總

    服務(wù)器錯(cuò)誤是怎么回事?最常見原因分有六個(gè),分別是:硬件問題、軟件問題、網(wǎng)絡(luò)問題、資源耗盡、數(shù)據(jù)庫、文件權(quán)限問題??梢愿鶕?jù)以下具體錯(cuò)誤原因進(jìn)
    的頭像 發(fā)表于 08-12 10:11 ?1454次閱讀

    EEPROM編程常見錯(cuò)誤及解決方案

    EEPROM(電可擦可編程只讀存儲(chǔ)器)在編程過程中可能會(huì)遇到多種錯(cuò)誤。以下是一些常見的EEPROM編程錯(cuò)
    的頭像 發(fā)表于 12-16 17:08 ?420次閱讀
    RM新时代网站-首页