初歩のシェルスクリプトで遊ぶ[シェルスクリプトの注意点はどこだろう(5)]

最初の覚え方・序盤に覚えるとお得な事柄

  • 最初の覚え方は、説明書からズレすぎないほうがよい。
  • 序盤に、長く使える手軽な方法を覚えておいたほうがよい。いくら汎用性があっても、そのぶん慣れと勉強が必要な手段だけに絞らないほうがよい。

if文はコマンドを普通に実行する

「コマンドの終了ステータスで分岐するのがif文だ」というのは目新しい話ではありませんが、言いたいのは最初から説明書を併読したほうがよい、という点です。入門記事だけで勉強すると、勘違いが起きやすくなります。

if list; then list; [ elif list; then list; ] ... [ else list; ] fi
最初に if list が実行されます。list の終了ステータスが 0 ならば、then list が実行されます。 そうでなければ elif list がそれぞれ順番に実行され、 list の終了ステータスが 0 ならば、対応する then list が実行され、コマンドが終了します。そうでなければ、else list が (もし存在すれば) 実行されます。終了ステータスは最後に実行されたコマンドの終了ステータスとなります。どの条件も真と評価されず、コマンドが全く実行されなかった場合、終了ステータスは 0 となります。

https://linuxjm.osdn.jp/html/GNU_bash/man1/bash.1.html

if文は条件の真偽で処理を分岐する、と説明されるのを見かけます。「[」と区別が曖昧なまま解説されるのもしばしばです。

if   [ 条件 ]
then 真の処理
else 偽の処理
fi
  • 実は「-le -ge」はシェル独特のif文の演算子だ。
  • 実は「]」の左には空白が必要だ
  • 実は「[」はコマンドだ
  • 実は「[」は「test」とほぼ同じだ
  • 実は「[」以外のコマンドも使えるのだ

「条件によって処理を分岐する」と聞くと、実際に実行されるコマンドは分岐した後の処理だけのようにも見えます。しかし説明書にも書いてあるように、「最初に if list が実行されます」。
最初から、説明書のとおりに「if文はコマンドを普通に実行するものだ」と理解しておくほうが簡単です。

if    コマンド1-1      # コマンドやシェル関数を、普通に実行する
      コマンド1-2      # 「実行したらどうなるか予想する」のではなく、本当に実行する
      コマンド1-3 

then  コマンド2-1      # ifのコマンドの実行が終わったときの終了ステータスが0なら実行する
      コマンド2-2
      コマンド2-3

else  コマンド3-1      # (elifが無い場合、)ifのコマンドの実行が終わったときの終了ステータスが0でなければ実行する
      コマンド3-2
      コマンド3-3

fi

最初からこれくらいでも、分かりにくいことはないはずです。
ifの後には、コマンドが1個以上置いてある。
というのが要点で、実行したコマンドの終了ステータス、終了コードの値で分岐します。
それを最初に理解しておけば、

[       $var -eq $foo ]
コマンド 引数 引数 引数 引数

というように、普通のコマンドの書き方と同じだとわかります。

thenやelseの中に複数のコマンドを置くのと同じように、ifの後にも複数のいろんなコマンドを置くことができます。いろんなコマンドに「[」も含まれます。コマンドどうしをパイプで繋いだりもできます。自作したシェル関数を入れることもできます。これらは応用扱いする必要はありません。
たとえば、意味もなくコマンドを入れることもできるわけで……

#!/bin/sh

if
# 普通に日時を出した
  date
# カレンダーに行番号を付けた
  cal | nl
# 終了ステータス1
  false

then
  echo 'thenのコマンドを実行した'

else
  echo 'elseのコマンドを実行した'

fi

普通にdateコマンドを実行すれば日時が出るし、カレンダーにパイプで行番号をつけることもできます。どのコマンドも終了ステータス0ですから、このままならthenに飛びます。
でも最後にfalseが実行されて、終了ステータスが1になり、elseに飛んでしまう。途中はどうあれ、ifの中身のコマンドをすべて実行した時点での終了ステータスで分岐します。

さらに、サブシェルとexit、forループも入れてみます。

#!/bin/sh
# 外部スクリプトファイルやシェル関数でif文を分岐するイメージ

if
  (                      # サブシェルに移動
  for Num in `seq 1 3`   # 3秒待つ
  do
    echo "${Num}"
    sleep 1
  done

  exit 1                 # 終了ステータス1
  )

then
  echo 'thenの処理'

else
  echo 'elseの処理'

fi
1
2
3
elseの処理

このサブシェルの括弧の中を、外部スクリプトやシェル関数だと思えば、自作のスクリプトやシェル関数でif文の分岐が問題なくできる、ということがなんとなくわかります。


変数展開を早めに覚えておく

変数展開にも色々とあります。bash専用だったり、if文で代用できるものは、優先順位が低くても構いません。でも、以下の四つは序盤に覚えるのがお勧めです。
特に、sedシェルスクリプトも一から覚えなきゃならない人の場合は、sedより変数展開を先に覚えたほうがいいと思う。

var=${var#}
var=${var##}
var=${var%}
var=${var%%}
  • case文と似たような書き方ができると思えば、正規表現より簡単に覚えられる。
  • bashだけでなくdashでも使える。
  • 軽い。初心者向け機能で性能は劣る、なんてことはない。
#!/bin/sh
# 

echo '---- 複数行のパス'
var='/home/username/folder/file1.txt
/home/username/folder/file2.txt
/home/username/folder/file3.txt'
echo "${var}"

echo '  sed'
echo "${var}" | sed -e 's/\/[^/]*$//'

echo '  変数展開'
echo "${var%/*}"

echo '  dirname'
dirname -- "${var}"

echo '---- 改行を含むパス'
foo='/home/username/fol
der
/fi
le1.txt'
echo "${foo}"

echo '  sed'
echo "${foo}" | sed -e 's/\/[^/]*$//'

echo '  sed'
echo "${foo}" | sed -e '#n
1{ # 1行目はホールドスペース(HS)にコピー
  h
  d
}
$!{ # 2行目から最終行直前行まではHSに追記
  H
  d
}
${ # 最終行
  H                 # HSに追記 HSに改行含めたパスを再構築
  x                 # HSとパターンスペースを交換
  s/\/[^/]*$//      # ファイル名部分を削除
  p                 # print
}
'

echo '  変数展開'
echo "${foo%/*}"

echo '  dirname'
dirname -- "${foo}"

echo '---------------'
---- 複数行のパス
/home/username/folder/file1.txt
/home/username/folder/file2.txt
/home/username/folder/file3.txt
  sed
/home/username/folder
/home/username/folder
/home/username/folder
  変数展開
/home/username/folder/file1.txt
/home/username/folder/file2.txt
/home/username/folder
  dirname
/home/username/folder/file1.txt
/home/username/folder/file2.txt
/home/username/folder
---- 改行を含むパス
/home/username/fol
der
/fi
le1.txt
  sed
/home/username
der

le1.txt
  sed
/home/username/fol
der

  変数展開
/home/username/fol
der

  dirname
/home/username/fol
der

---------------

変数展開も注意が要らないというわけではありませんが、それを差し引いても序盤に覚えておくと便利です。

# dashとbashで動作が異なる例 ------------------------
hoge='あいうえお'

# 文字数 dashでは15,bashでは5
echo "${#hoge}"

# dashでは1バイト削れるが、bashではひらがな1文字を削れる。
echo "${hoge#?}"

意外と早期に知ってもよい事柄

IFSの中身と、位置パラメータが配列のように使えることは、早めに知っていると便利に、長く使えます
「配列として使う」というと裏技のように聞こえますが、もともと普通に引数を受け渡すときにも配列として使っているはずです。引数の渡し方、受け取り方、位置パラメータの再設定、シフト、「${@}」と「${*}」、IFS、これらを覚えることは、配列としての使い方もついでに覚えているのと同じです。

  • IFSの中身の文字
    • 半角空白、タブ、改行の順番に入っている。1文字目は特に重要
    • IFSのいちばん最初の文字は、「"${*}"」の区切りに使われる。デフォルトでは半角空白。
      • 半角空白以外の文字をIFSに追加して使うこともできる
  • 位置パラメータは配列
    • シェル関数の位置パラメータも配列として使える
    • シェル関数の位置パラメータは(サブシェルにしなくても)ローカル変数っぽく使える
    • 変数名に数字を付けてevalするよりは読みやすい

個人的な好みが多めな話

bashを使う人でもdashでスクリプトを書いたほうが楽

bash専用の機能を使えば楽になる、とは限りません。むしろ、まずはdashで書いたほうが楽です。

  • dashで使える機能は比較的少ないが、
    • 少ないといっても、算術式や変数展開の一部は使える。足し算でexprを呼び出すまではしなくてもいい。
    • dashの機能はbashでも使える。
    • 同じスクリプトなら大抵、bashより速い。ものによっては5倍くらい速い。
    • つまりbashを使う人にとっても、勉強が無駄になりにくい。勉強する順番の目安になる。
  • bash専用機能を使いまくったあとにdash用に書き換えるのは大変。でもその逆は、割と簡単。
  • スクリプトを部品単位で分離して書いているなら、部品の一部だけbashにすることもできる。bashを捨てるわけではない。
シェルスクリプトの高速化の方法の一つは「まじめな処理を文字並べパズルに置き換える」こと

初歩のシェルスクリプトで遊ぶ[ファイルのリネームツール、のようなもの?_(5)] - HatenaDiary id:Narr
複数ファイルの一括リネームツールを作ったとき、ファイル名の重複予測をするスクリプトを組み込んだんですが。
試しにファイルのリネームを配列上で模擬して、一回ずつ「sort | uniq」で重複検査していくと、かなり遅かった。せいぜい数百ファイルしか使えない。
最終的には、ファイル名に数字を付加したあと、joinコマンドでくっつけて数字を見る、という方式にしました。これだと数千以上のファイルでも、一瞬で終わる。

シェルスクリプトでよく使うコマンドは、文字並べパズルのコマンドがいくつもあります。joinや、pasteやcutもそうです。データの形を工夫して、文字並べのコマンドで処理できたなら、びっくりするくらいうまくいくように感じてます。

もしできたなら、でしかないんですが。できたときは楽しい。