bashのTipsというかCheatSheetというか備忘メモというかよくある系のネタというかごった煮というかまとまったらpostしようと思ってたらタイミング逃して膨らんじゃった落書きというか、、、っていうかタイトル何文字行けるんだろう。最初の方の消えちゃったりするのかな。え?マジでこんなに入るの!?ウヒョーーーー!!!

  • 643
    いいね
  • 5
    コメント
この記事は最終更新日から1年以上が経過しています。

概要

えっと、タイトル通りですw
bashの記法は独特なものが多く毎回ググってしまうのでまとめて(と言いつつまとまりがないですが。。。)おこうと思います。
ある程度まとまってからpostしようとか思ってたらごちゃごちゃになっちゃいました。
bashで使えるという意味なのでposixシェル共通のネタも混ざってます。
随時更新します。参考になれば幸いです。
タイトルの割に普通ですいませんw

追記

  • 2015年10月9日 - GLOBIGNOREを追記しました。
  • 2016年2月17日 - タイトルの「ウヒョーーーー」にビックリマークを追加しましたw

参考

カッコ色々

bashでは色々なカッコがありますが、よく違いが分からず使っていたりするのでまとめてみます。

[]と[[]]

[]はtestコマンドのaliasです。[[]]じゃないとできないこととしては、以下のようなものがあります。

  • 空白を含む文字列をクォートしなくてOK

    クォート不要
    var='abc 123'
    
    # []の中だとクォートしないとエラーになる
    [ $var = 'abc 123' ] && echo 'ok'
    -> brace.sh: line 6: [: too many arguments
    [ "$var" = 'abc 123' ] && echo 'ok'
    -> ok
    
    # [[]]の中だとクォートしなくてもOK
    [[ $var = 'abc 123' ]] && echo 'ok'
    -> ok
    
  • =~で正規表現が使える

    正規表現が使える
    var='abc 123'
    # [[]]の中だと正規表現が使える
    [[ $var =~ [a-z]{3}[[:space:]][1-9]{3} ]] && echo 'ok'
    -> oke
    

    bashの正規表現についてはこちらが参考になります。

  • パターンマッチもできる

    パターンマッチもできる
    var='abc 123'
    # [[]]の中だとパターンマッチができる
    [[ $var = abc* ]] && echo 'ok'
    -> ok
    [ $var = abc* ] && echo 'ok'
    -> brace.sh: line 18: [: too many arguments
    
  • AND条件OR条件に&&や||が使える

    OR条件に||が使える
    # []の中だとOR条件は-oを使う
    [ "$var" = 'abc 234' -o "$var" = 'abc 123' ] && echo 'ok'
    -> ok
    
    # [[]]の中だとOR条件に||が使える
    [[ $var = 'abc 234' || $var = 'abc 123' ]] && echo 'ok'
    -> ok
    

その他詳しくはこちら
ちなみに基本的には[]はPOSIXシェル準拠で[[]]はPOSIX準拠ではないです。
が、[[]]もbash以外にzshやkshでも実装されているものが多いので移植性を考えて利用を控えるとかはしなくても大丈夫なんじゃないかと思っています。
あと、[]は配列のインデックスを指す場合にも使いますが、そちらは説明するまでもないと思うので省略します。

()と(())

  • ()は配列を定義するときに使います。

    配列定義
    var=(a b c)
    echo ${var[2]}
    -> c
    
  • (())は演算に使います。

    for文のカウンタとして使う
    for ((i=0;i<3;i++))
    do
        echo $i
    done
    -> 0
       1
       2
    
    インクリメント
    var=1
    ((var++))
    echo $var
    -> 2
    

    個人的によく間違えちゃうんですが、変数を定義するときは(())から含める必要があります。
    ((var2=$var + 3))と書くのが正しい($varはvarでもOKです)んですが、よくvar2=(($var + 3))とか書いてエラーになっちゃいます。
    この辺りは$(())のところでも後述します。

{}と{{}}

  • 変数展開
    {}は$を付けて変数参照やら置換に使いますね。詳しくは後述します。

  • 様々な展開
    あとは変数に限らず色々な展開ができます。
    例えば以下のようにfor文のカウンタを展開することができます。

    for文
    for i in {a..c}
    do
        echo $i
    done
    -> a
       b
       c
    

    この展開は結構便利でcpやmv使うときに重宝します。
    例えばファイルのバックアップを取るときは以下のようにできます。

    cpと{}を使ってバックアップ
    cp /path/to/file{,_old}
    

    とかやると/path/toにfileのコピーとしてfile_oldが出来上がります。
    フルパス2個打つとか無駄なことをしなくて済みます。

  • コマンドをまとめる
    {}を使うと複数のコマンドをひとまとめにできます。

    コマンドをまとめる
    {
        echo hoge
        echo fuga
        echo piyo
    } > echo.log
    

    ログ出力とかで重宝します。
    全部のコマンドの後ろにリダイレクトを書くよりもキレイにできます。

続いて{{}}ですが、これは見たことないです。。。これって何かありますでしょうか?

$()と$(())

  • $()はコマンド展開です。``と同じ意味です。
    たとえば/path/toにある各ファイルに対して何らかの処理を行う場合は以下の通りです。

    コマンド展開
    for i in $(ls /path/to)
    do
        echo $i
    done
    
  • $(())は演算で使います。(())と違ってインラインで使えます。

    $(())の演算
    echo $((1+3))
    -> 4
    # echo ((1+3))はエラーになる
    
    var=1
    var2=$(($var + 2))
    echo $var2
    -> 3
    # var2=((var + 2))はエラーになる
    

    この辺りは混同しがちなので要注意ですね。

''と""

カッコではないですが''と""の違いは変数が展開されないかとされるかの違いです。

''と""
var='abc'
echo '$var'
-> $var
echo "$var"
-> abc

宣言色々

bashの変数はデフォルトではグローバルです。
まずは以下を例にします。

sample.sh
#!/bin/sh

var1='var1 initial'

function func1 {
    var1='var1 changed'
    var2='var2 initial'
    echo $var1 # 3. 関数外で定義したグローバルな変数var1を書き換えたため'var1 changed'が表示される
    echo $var2 # 4. 関数内で定義したグローバルな変数var2の初期値'var2 initial'が表示される
}

echo $var1 #1. この時点では関数呼び出し前のため'var1 initial'が表示される
echo $var2 #2. この時点では関数呼び出し前のため''(空文字)が表示される
func1
echo $var1 #5. グローバルなvar1変数をfunc1関数内で書き換えたため、'var1 changed'が表示される
echo $var2 #6. func2内で定義したグローバルな変数var2の値が参照できるため、'var2 init'が表示される

挙動はコメントに書いたとおりですが、実行すると以下の結果が得られます。

sample.shの実行結果
var1 initial

var1 changed
var2 initial
var1 changed
var2 initial

変数宣言にlocalをつけて関数内localな変数にする

変数はlocalで宣言することでローカル変数とすることができます。
上記のスクリプトを以下のように書き換えます。

local.sh
#!/bin/sh

var1='var1 initial'

function func1 {
    local var1='var1 changed'
    local var2='var2 initial'
    echo $var1 # 3. 関数で定義したローカル変数var1の値'var1 changed'が表示される
    echo $var2 # 4. 関数内で定義したローカルな変数var2の初期値'var2 initial'が表示される
}

echo $var1 #1. グローバルな変数var1の値'var1 initial'が表示される
echo $var2 #2. var2は未定義のため''(空文字)が表示される
func1
echo $var1 #5. func1内で書き換えたのはローカル変数のため、グローバルなvar1変数の値は変わらず'var1 initial'が表示される
echo $var2 #6. func2内で定義したvar2はローカル変数のため、値が参照できず''(空文字)が表示される

これまた挙動はコメントに書いたとおりですが、実行すると以下の結果が得られます。

local.shの実行結果
var1 initial

var1 changed
var2 initial
var1 initial

変数宣言時に型指定する

変数宣言時にdeclare -i var1等とすると型を指定することができます。(ローカル変数の場合local -i var1等とできます。)
-iはintegerの意味で整数値を割り当てるようになります。

declareで整数を割り当てる
declare -i var1=123
echo $var1  # 123
var1=hoge
echo $var1  # 0

文字列を代入した場合にエラーにしてくれないのはちょっと残念ですね。

ちなみに代入する値が数式の場合は計算されます。

演算
var1=4/2
echo $var1  # 4/2
declare -i var1=4/2
echo $var1  # 2

その他使えるオプションはhelp declareで見ることができます。
(ちなみにbashのビルトインコマンドはmanでは出てこないのでhelpで見ます。cdとかもそうですね。)

変数宣言時にreadonlyを付けて読み取り専用変数(定数)にする

変数はreadonlyとして宣言することで読み取り専用にすることができます。

例えば、以下のスクリプトがあったとき、

readonly.sh
#!/bin/sh

readonly var1='hoge'
echo $var1

var1='fuga'
echo $var1

上記を実行すると、以下の結果が得られます。

readnoly.shの実行結果
hoge
readonly.sh: line 6: var1: readonly variable
hoge

var1の値は書き換えられずエラーになり、2回目のecho $var1では元の値が表示されます。
ちなみにdeclare -rとして宣言してもreadonlyと同じ効果が得られます。

exportを使って別のシェルプロセスに変数を渡す

通常、あるシェルで定義した変数は別のシェルでは読めません。
しかし、exportを付けて宣言することで他のシェルに渡すことが可能になります。

たとえば以下のスクリプトがあったとき、

export.sh
#!/bin/bash
echo $var1

シェルから上記のスクリプトを実行すると以下のような結果になります。

export.shの実行結果
var1='hoge'
bash export.sh

#var1の値は別のシェルには渡らないため何も表示されない

export var1='hoge'
bash export.sh
-> hoge
#exportした変数var1の値は別のシェルに渡るため、親のシェルで設定した値が表示される

sourceを使って別のシェルから変数をもらう

上と逆のパターンです。

例えば以下のスクリプトがあった場合、

source.sh
#!/bin/bash
var1='hoge'

実行結果は以下のようになります。

source.shの実行結果
$ bash source.sh
$ echo $var1

# source.shで定義した変数は受け取れないため何も表示されない

$ source source.sh
$ echo $var1
->hoge
#sourceコマンドで読み込んだスクリプト内で定義した変数var1の値が表示される。

シェル上での実行というよりは複数ファイル間で変数の受け渡しをする際によく使いますね。
ちなみにsource.で代替できます。

変数色々

bashの変数には様々なものがあります。

$系

基本的には引数やらコマンド実行結果やらを示します。
以下のdollar.shを./dollar.sh abc 123と実行した場合をコメントで示します。

dollar.sh
#!/bin/bash
echo $0
# 実行コマンド。今回の場合はdollar.sh

echo $1
# 一つ目の引数。今回の場合はabc

echo "$*"
# 全ての引数。今回の場合はabc 123

echo "$@"
# 全ての引数。今回の場合はabc 123。上記との違いは後述

echo $#
# 引数の数。今回の場合は2

echo $$
# 実行中のプロセスID

echo $!
# 前回実行コマンドのプロセスID。今回の場合dollar.shを実行前のコマンドのPID

echo $?
# 前回実行コマンドの実行結果。成功の場合は0

echo $-
# スクリプト内で有効になっているフラグを表示します。

echo $_
# 実行シェルが取れます。./doller.shと実行していれば./doller.shが、bash doller.shと実行していれば/bin/bashが取れます。

フラグはこちらを参照してください。
$*$@の違いは前者は引数全てを一つの文字列として扱うのに対して、後者はそれぞれ別に扱う点です。
基本的には$@を使うと良いと思います。

asterisk.sh
#!/bin/bash
for i in "$*"
do
    echo $i
done
実行結果
./asterisk.sh 123 abc
-> 123 abc
at.sh
#!/bin/bash
for i in "$@"
do
    echo $i
done
実行結果
./at.sh 123 abc
-> 123
   abc
  • さらにコマンドラインでは以下のような$変数が使えます。

    • !$で前回実行コマンドの最後の引数を取得

      前回実行コマンドの最後の引数を取得
      less /path/to/file
      # とりあえず参照したけどやっぱり編集したいとかなったときは以下のようにする
      vi !$
      
    • !!で前回実行コマンドの全体を取得

      前回実行コマンド全体を取得
      service httpd restart
      # とか実行したけどsudoしなきゃダメだったときは以下のようにする
      sudo !!
      

      !!は$入ってないですねw

それ以外

$系以外にも色々な組み込み変数があります。
有名なのだと$HOMEとかですね。
書ききれないのでこの辺を参考にしてもらえればと思います。

展開色々

変数操作

bashでは${}の中で色々な展開や置換等の変数操作が行えます。

例として、path=/hoge/hoge/fuga/FUGA/piyoのとき、

  • 文字数を取得

    echo ${#path}
    -> 25
    
  • 部分文字列を取得

    echo ${path:1:4}
    -> hoge
    # 2文字目(インデックスが0始まりのため1は2文字目)から4文字を取得
    
  • 一つ目のhogeをhageに置換

    echo ${path/hoge/hage}
    -> /hage/hoge/fuga/FUGA/piyo
    
  • 全てのhogeをhageに置換

    echo ${path//hoge/hage}
    -> /hage/hage/fuga/FUGA/piyo
    
  • 小文字を大文字に変換

    echo ${path^^}
    -> /HOGE/HOGE/FUGA/FUGA/PIYO
    
  • 大文字を小文字に変換

    echo ${path,,}
    -> /hoge/hoge/fuga/fuga/piyo
    
  • ファイル名取得

    echo ${path##*/}
    -> piyo
    
  • ディレクトリ取得

    echo ${path%/*}
    -> /hoge/hoge/fuga/FUGA
    

リダイレクトとパイプ色々

基本

>がファイルへのリダイレクト、>>が上書き、|がパイプというのはさすがに説明不要かと思います。

  • 標準エラー出力をリダイレクト
    ls /home/bin 2> hoge.log

  • 標準出力と標準エラー出力の両方をリダイレクト
    ls /home/bin /home/user1 &> hoge.log
    もしくは
    ls /home/bin /home/user1 >& hoge.log
    もしくは
    ls /home/bin /home/user1 > hoge.log 2>&1

  • teeで標準(エラー)出力とファイル両方に出力
    ls /home/user1 | tee hoge.log

execを使った色々

  • 出力をファイルに書く。
    exec &>fileとすると以降のコマンド実行結果は全てfileに書かれます。
    ただしエコーバックがなくなるので後述のプロセス置換、もしくはそもそもscriptコマンドを使ってしまった方が良いです。

  • 任意のファイルディスクリプタを開く。(出力)
    3>等とすることで出力用に任意のファイルディスクリプタを開くことができます。

    コマンド実行結果をファイルに書く
    exec 3>file
    echo "hoge" >&3
    exec 3>&-
    

    上記のように実行するとhogeがfileに書かれます。
    最後は3>&-として忘れずにファイルディスクリプタを閉じましょう。
    なお、3は標準出力(1)と標準エラー出力(2)以外の任意の数字でOKです。

  • 任意のファイルディスクリプタを開く。(入力)
    3<等とすることで入力用に任意のファイルディスクリプタを開くことができます。

    fileからgrepする
    exec 3<file
    grep "hoge" <&3
    exec 3<&-
    

    fileからhogeという文字列をgrepしています。
    最後は忘れずに閉じましょう。

  • 任意のファイルディスクリプタを開く。(入出力)
    3<>等とすることで入出力用に任意のファイルディスクリプタを開くことができます。

    入出力
    echo "hoge" > file
    echo "fuga" >> file
    echo "piyo" >> file
    exec 3<> file
    read var1 <&3
    echo $var1  #hoge
    echo 'puyo' >&3
    exec 3>&-
    exec 3<&-
    

    とやるとfileの中身は

    hoge
    puyo
    piyo
    

    になります。readで読んだ位置からpuyoを書くためです。

  • webページをgetする
    上記を応用するとこんなことができます。

    yahooをget
    exec 3<>/dev/tcp/www.yahoo.co.jp/80
    echo -e "GET / HTTP/1.1\n\n" >&3
    cat <&3
    

実は順番はどうでも良い

もはや小ネタレベルですが、実は下の3つはどれも同じ意味です。

同じ意味
echo 'hoge' >hoge.txt
>hoge.txt echo 'hoge'
echo >hoge.txt 'hoge'

read色々

readコマンドを使うとファイルとか変数から値を読むことができます。

read.txt
field1 \field2 field3 fiedl4
f1 \f2 f3 f4
read.sh
#!/bin/bash
while read var1 var2
do
    echo $var1
    echo $var2
done < read.txt
field1
field2 field3 fiedl4
f1
f2 f3 f4

いらないフィールドを捨てる

ファイルの中に不要なフィールドある場合は_で捨てることができます。
(_じゃなくても任意の文字列で大丈夫ですがgolangっぽく_にしてみましたw)

read.sh
#!/bin/bash
while read var1 var2 _
do
    echo $var1
    echo $var2
done < read.txt
実行結果
field1
field2
f1
f2

エスケープ回避

上記の通りテキストに\が含まれる場合、エスケープされてしまいます。
テキストに書かれている通り出力する場合は-rを付けます。

read.sh
#!/bin/bash
while read -r var1 var2 _
do
    echo $var1
    echo $var2
done < read.txt
実行結果
field1
\field2
f1
\f2

変数を読む

ヒアストリング(後述)とかを使うと変数から値を読むことができます。

read.sh
#!/bin/bash
var='a b c d'
read -r var1 var2 _ <<<$var
echo $var1
echo $var2

その他

  • IFSを使うと区切り文字を変えることができます。
    IFS=- read -r var1 var2 <<< "a-b-c-d-e"

  • -aを使うと値を配列に読み込むことができます。
    IFS=- read -ra parray <<< "a-b-c-d-e"

ということで例によって詳しくはhelp readでお願いしますw

&&や||で条件が書ける

簡単な条件判定なら&&や||を使うのがオススメです。

&&や||を使った条件式
# 条件がtrueの場合に実行
[ 1 -lt 2 ] && echo "1 is less than 2"
-> 1 is less than 2

# 条件がfalseの場合に実行
[ 1 -gt 2 ] || echo "1 is NOT greater than 2"
-> 1 is NOT greater than 2

# if else的な
[ 1 -gt 2 ] && echo "1 is greater than 2" || echo "1 is NOT greater than 2"
-> 1 is NOT greater than 2

ただし複雑な条件を書く場合は可読性を考えてif文を使ったほうが良いと思います。

ヒアドキュメントとヒアストリング

ヒアドキュメント

<<はヒアドキュメントと呼ばれ、文字列リテラルをコマンドに渡したりすることができます。
sqlplusやmysqlコマンド等でインラインでSQL実行したい場合等によく使います。

  • 基本
    基本的な使い方は以下の通りです。_EOF_は任意の文字列でOKです。
    mysqlのインタラクティブなシェルは開かず、sqlが実行されて結果がターミナルに返ります。

    ヒアドキュメント
    mysql -u user -ppassword -D database <<_EOF_
    select * from test_table
    where id=1
    _EOF_
    
  • リダイレクトとパイプ
    リダイレクトとパイプは以下のようにします。

    リダイレクト
    mysql -u user -ppassword -D database <<_EOF_ > output.log
    select * from test_table
    where id=1
    _EOF_
    
    パイプ
    mysql -u user -ppassword -D database <<_EOF_ | grep hoge
    select * from test_table
    _EOF_
    
  • エスケープ
    ヒアドキュメントには変数を渡すこともできます。

    変数渡し
    id=1
    mysql -u user -ppassword -D database <<_EOF_
    select * from test_table where id=${id}
    _EOF_
    

    変数をエスケープして文字列リテラルとして渡したい場合は以下のよう_EOF_の前に\を付けます。

    エスケープ
    id=1
    mysql -u user -ppassword -D database <<\_EOF_
    select * from test_table where id=${id}
    _EOF_
    
  • インデント
    インデントしたい場合、以下のように書くとエラーになってしまいます。

    インデントできない
    for i in {1..2}
    do
        cat << _EOF_
        loop $i
        _EOF_
    done
    

    インデントしたい場合は以下のように<<-とします。

    インデント
    for i in {1..2}
    do
        cat <<- _EOF_
        loop $i
        _EOF_
    done
    

ヒアストリング

<<<はヒアストリングと呼ばれ、ヒアドキュメント同様文字列をコマンドに渡したりすることができます。

ヒアストリング
mysql -u user -ppassword -D database <<<"select * from test_table"

ただしインデントを付けたりはできません。(実は方法があったら教えてください)
可読性等を考えると基本的には1行のとき以外はヒアドキュメントの方が良いと思っています。
ちなみにリダイレクトとパイプは問題なくできます。

ヒアストリングのリダイレクトとパイプ
mysql -u user -ppassword -D database <<<"select * from test_table" > output.log
mysql -u user -ppassword -D database <<<"select * from test_table" | grep hoge

プロセス置換

プロセス置換とはプロセスの結果を一時ファイルのようにコマンドに渡したりできる機能です。

<()を使って一時ファイル不要のdiff

一番よく使うのがこのケースだと思います。
こんな感じのことができます。

一時ファイル不要のdiff
diff <(sort file1.txt|uniq) <(sort file2.txt|uniq)

file1の中身とfile2の中身をsortしてuniqしたものを比べてます。
プロセス置換を使わないと一時ファイルに吐き出したものをdiffする必要があります。

>()を使ってコマンド出力結果をログに出力

>()は個人的にはあまり使いません。
以下のようにすると以降のコマンド実行結果は全てログにも出力されるようになります。

ログ出力
# 標準出力
exec 1> >(tee -a out.log)
# 標準エラー出力
exec 2> >(tee -a err.log >&2)

>()はこれくらいでしか使わない上、この用途もscriptコマンド使ってしまうことが多いです。
標準出力と標準エラー出力を分けたい(もしくはどちらかのみ取りたい)ケースでたまに使います。
一番よく使うケースとしてはstraceの実行結果をログに出すときに標準エラー出力に出すパターンで使ったりします。

コマンド履歴

historyに日時を残す

HISTTIMEFORMAT="%Y-%m-%d %H:%M:%S "
historyコマンドの結果が変わります。
~/.bash_historyのエントリーには#付きでUNIXタイムが刻まれます。

現在のセッションの履歴を残さない

unset HISTFILE && exit

履歴全削除

HISTFILESIZE=0 && exit

スクリプトにセットする推奨オプション

上でも少し出てきましたが、setコマンドを使うとbashの挙動を制御することができます。
bashのスクリプトを実行するときに個人的によく付けるオプションはset -euo pipefailです。
それぞれ以下の通りです。

-e

スクリプト内のコマンドで1つでもエラー(0以外の戻り値を返すもの)があるとスクリプトをそこで停止します。

-eフラグ無し
#!/bin/bash
ls /path/to/nonexist
echo hoge
-> ls: cannot access /path/to/nonexist: No such file or directory
   hoge # lsでエラーになってもhogeが出力される
-eフラグあり
#!/bin/bash
set -e
ls /path/to/nonexist
echo fuga
-> ls: cannot access /path/to/nonexist: No such file or directory
   # lsがエラーでスクリプトが終了するためecho fugaは実行されない

ここで問題になるのが、正常終了でも戻り値が0でないコマンドです。
また、実行結果がエラーでも一時的に無視したいケースもあるかと思います。
そんなときはコマンドの後ろに|| trueを付けるか、一時的に-eを無効にします。

一時的にエラーが出ても進みたい場合
# 回避パターン1
/command/that/returns/nonzero || true
# 上は必ず戻り値($?)が0になる

# 回避パターン2
set +e
/command/that/returns/nonzero
set -e

-u

未定義の変数呼び出しを行うとエラーとします。変数名の打ち間違い等で意図した挙動にならないなんてことを防いでくれます。

-uフラグ
/bin/bash
var='123'
var2=${var1}'abc' #間違えてvarではなくvar1としてしまった

echo $var2
# var1は空なのでabcが出力されてしまう

set -u

var2=${var1}'abc' #ここで'var1: unbound variable'というエラーになる
echo $var2 # set -eもつけていればここが出力されずに停止する

ここで困るのが$1等のコマンドラインからの引数を受け取る際に、引数が任意の場合です。
必ず引数を渡すなら良いですが、任意の場合は$1などを使った際に未定義でエラーになってしまい使い勝手が悪いです。
そんなときはデフォルト値を使うと良いです。

default.sh
#!/bin/bash

set -u

var=${1:-default value}
echo $var

上記を./default.shと実行した場合はdefault valueが表示され、./default.sh hogeと実行した場合はhogeと表示されます。

-o pipefail

パイプでコマンドを繋げた時にパイプのどれか1つでも0以外の戻り値を返したらその戻り値(複数あった場合は最も右側)を返すというものです。
デフォルトだと最後の戻り値が返るため、エラーハンドリングが正しくできなかったりします。

pipefailフラグ
sort /path/to/nonexist | uniq
echo $?
# uniq自体は成功するため0が返る

set -o pipefail

sort /path/to/nonexist | uniq
echo $?
# sortが失敗した戻り値の2が返る

スクリプトを書く際は上の3つを付けるようにしています。
他にもデバッグ時には-xや-vも付けます。

EXITをフックする

スクリプト内で作った一時ファイルの削除等、エラーが起きた場合でも必ず実行したい処理というものはよくあると思います。
このような場合はEXITをフックして処理を実行することができます。
例えば以下のスクリプトは処理開始時に作成した一時ファイル格納ディレクトリをスクリプト終了時に必ず削除します。

EXITをフックする例
#!/bin/bash

# 一時ファイルを格納するディレクトリを作成
tmpfile=$(mktemp -d)

# スクリプト終了時に必ず実行したい処理を記述
function finally {
    rm -rf $tmpfile
}
# trapコマンドでEXITシグナル受信時にfinally関数が実行されるようにする
trap finally EXIT

echo 'start' > $tmpfile/file1
cat $tmpfile/file1

補完

bash completion

bashの補完は非力なのでbash-completionを導入すると様々なコマンドの補完ができるようになります。
sshコマンドやserviceコマンド等の補完ができるようになるため導入必須かと思います。
対応しているコマンドは/usr/share/bash-completion/completions/以下に設定ファイルが置かれます。
手元の環境(Ubuntu14.04)で見ると500コマンド以上が対応しているようです。
独自コマンドを追加したい場合はこちらにやり方が書かれています。
結構簡単に追加できます。

rlwrap

sqlplusやmysql、redis等、独自のシェルが起動するコマンドを実行するとコマンド履歴や補完が使えなくなります。
rlwrapを導入すると履歴や補完が使えるようになります。
導入はapt-get install rlwrapyum install rlwrapするだけです。(CentOSの場合はepelレポジトリが必要です)
rlwrapは文字通りreadlineをラップするだけなのでデフォルトだと補完といっても標準のシェルのコマンド補完しかできません。
なので例えばsqlplusでsqlを補完したい場合などはこちらの拡張を入れたりするとより便利になります。

GLOBIGNORE

GLOBIGNOREを使うとワイルドカード利用時の除外ファイルを指定できます。
例えば、末尾にorgがついてるファイルを除外してlsしたいとか、testって文字が入ったファイルを除外してvimで開きたいとかって時に便利です。

GLOBIGNOREの使い方
$ ls
fuga.go  fuga_test.go  hoge.go  hoge_test.go

$ GLOBIGNORE="*test*"

$ vi *
2 files to edit
-> hoge.goとfuga.goだけ開ける

それでも不満があれば

実行速度とか機能で不満が出てきたらzshとかに移行しましょうw

以上です。
随時更新します。