それマグで!

知識はカップより、マグでゆっくり頂きます。 takuya_1stのブログ

習慣に早くから配慮した者は、 おそらく人生の実りも大きい。

bashの条件判断の関数名を美しくする

Bashの関数名に?を使う

bash の関数名には ? が使えます。つまりまぁまぁ美しい関数名が作れます

if の中が美しくない例

# sf-pwgenが存在したら alias をいれる
if type  sf-pwgen > /dev/null  2>&1  ; then 
        alias sf-pwgen-alphanum-12char="sf-pwgen -a alphanumeric -l 12 -c 1"
        alias sfpwgen="sf-pwgen -a alphanumeric -l 12 -c 1"
fi 

関数を使うべき

if の条件文の箇所は、ひと目見てわかりやすい関数名にするべきだと思う。

関数にする。
function cmd_is_exists {
  type "$1"  > /dev/null  2>&1
  return $?
}
if cmd_is_exists sf-pwgen ; then 
  alias sf-pwgen-alphanum-12char="sf-pwgen -a alphanumeric -l 12 -c 1"
  alias sfpwgen="sf-pwgen -a alphanumeric -l 12 -c 1"
fi 

条件文だとひと目で分かる関数名にする。

? を関数の名前に使えるので、これをもう少しわかりやすい関数名にすることが出来る。

function cmd_exists? {
  type "$1"  > /dev/null  2>&1
}

よく使いそうなものも関数にしておくと便利

たとえば、数字かどうかのチェック

function is_int?  {
  [[ $1 =~ ^-?[0-9]+$ ]]
}

たとえば、bash の文字列の変数置換も書式を覚えづらいので関数にしておくとか

function upcase {  echo ${@^^}; }
function downcase {  echo ${@,,}; }
function Capitalize {  echo ${@^}; }

たとえば、LinuxOSXか判別し、同じbashrcを使いまわせるようにするなど。

function is_osx? {
    plat_name=$(uname -a )
    plat_name=$(downcase $plat_name)
    [[ $plat_name == darwin* ]]
}
function is_linux? {
    plat_name=$(uname -a )
    plat_name=$(downcase $plat_name)
    [[ $plat_name == linux* ]]
}

if is_osx?  ; then
   source ~/.bashrc.osx
elif is_linux?; then 
   source ~/.bashrc.linux
fi 

このように、関数とその名前を上手に使える。記号だらけで読みにくいシェルスクリプトだって、工夫次第でかなり読みやすいコードになると思う。

参考資料

https://github.com/azet/community_bash_style_guide

bashの正規表現マッチで後方参照

bash正規表現マッチで後方参照。

bash[[ コマンドで、正規表現マッチが使える話は以前書きました。

そのときには、意図的に書きませんでしたが、bash正規表現マッチでは後方参照が使えます。bash rematch の変数に格納されます。

ちなみに、rematchは re:match ではなく、regex matchのre だと思います。

bash正規表現マッチと後方参照

とりあえず、動作例を見てください。

takuya@:~$ [[ takuya =~ taku(.+) ]]
takuya@:~$ echo ${BASH_REMATCH[@]}
takuya ya

後方参照は、BASH_REMATCH

BASH_REMATCH の変数に、配列で返ってきます。コレだけ覚えておけば十分。

配列は、 BASH_REMATCH[0] がマッチ全体で、BASH_REMATCH[n] が後方参照にマッチをした箇所

後方参照は意外に使える。

正規表現マッチだけじゃなく、後方参照で文字列を抜き出すのは意外に使えると思います。

[[ $name =~  $regex ]]
if [[ -v BASH_REMATCH[1] ]]  ; then 
  echo ${BASH_REMATCH[1]}
  str=${BASH_REMATCH[1]} 
if 

などとして、正規表現マッチを使って文字列を抜き出したり加工したり出来るのは重宝しそうですね。

なので、extglobの正規表現っぽい glob パターン を使うのは避け、正規表現をガンガン使ったほうがマシなのではないでしょうか。

extglob でも十分かもしれませんが、extglob はon/off が設定環境依存になるのに対し、正規表現は設定無しでいけます。

grep正規表現で後方参照したくてもできないときに活用してはいかがでしょうか。

参考資料

bash の for と パイプ(xargs) の一番大きな違い

bash の似たものシリーズ

初心者が躓きそうなbash で似て紛らわしいものの違いをはっきりさせようとさせるシリーズ。

bash に於ける xargs / for には違いがあるのか?

あります。結構大きな違いが有ります。

それぞれのループの回しかたを復習しておきましょう。

for ループでコマンド結果を回す
for e in some_command ;  echo $e; done 
xargs パイプでコマンド結果を回す
some_command | xargs -I @ echo @; 

この2つの、コマンドの実行には違いが在るのでしょうか。またコマンドの実行の速度には違いが出るでしょうか?

2つの違い

  1. xargsはサブシェルを起動するので変数の名前空間が違う
  2. コマンドの終了を待つfor と 終了を待たない xargs

こんかいは、この後者の違いについて着目したいと思います。

毎秒カウントを出力するスクリプトを作ります。

まず、終了に10秒かかるスクリプトを作ります。

このコマンドは、毎秒1回カウント回数を出力します。

10.times.loop.sh
#!/usr/bin/env bash

for (( i=0; i<10; i++   )) ; do
  echo $i;
  sleep 1 ;
done

単体で実行すると、1秒ずつカウントします。

0    
# sleep 1
1
# sleep 1
2
....
9
xargs で実行したとき。
takuya@~$ bash  10.times.loop.sh | xargs -I@ echo xargs @
xargs 0
xargs 1
xargs 2
xargs 3
xargs 4
xargs 5
xargs 6
xargs 7
xargs 8
xargs 9

for で実行したとき。

takuya@~$ for  i in $( bash 10.times.loop.sh ) ; do   echo for $i;   done ;
for 0
for 1
for 2
for 3
for 4
for 5
for 6
for 7
for 8
for 9

ブログで書くとおなじみに見えます。

ブログで書くと同じに見えます。が、全然違います。

動画で見てください。

xargs の例

youtu.be

for の例

youtu.be

動作の違い

xargs
xargs は 前のコマンドから出力が出るたびに処理される。
for
for だと、前のコマンドが終了するまで何も出来ない。

for とxargs

それは for はコマンドの実行終了を待って処理しているから。 xargs は出力があるたびに stdin から読み込んで逐次処理をするから。

for i in COMMAND; do  echo $i ;done

この場合、for は COMMAND が終了するまでイテレーションの要素数が決まらないので、何も出来ない。

COMMAND | xargs -I @ echo @

この場合、xargs は前のコマンドから受け取って、入力が来たら逐次処理をする。

これは sed などでも同じこと

パイプライン処理の有能さは、非同期に実行できるところにある。

takuya@~$ bash 10.times.loop.sh| sed -e  "s/\([0-9]\)/sed \1/"
sed 0
sed 1
sed 2
sed 3
sed 4
sed 5
sed 6
sed 7
sed 8
sed 9

もしコレを for で回してしまうと・・・コマンドの終了待ちが出てしまう。

for  i in $( bash 10.times.loop.sh )  ; do
      echo $i | sed -e  "s/\([0-9]\)/sed \1/;
 done ;
## for の引数のコマンドの終了待ちが出る

awk でも同じ

takuya@~$ bash 10.times.loop.sh| awk '{ print gensub(/([0-9])/, "awk \\1",1, $0) } '
awk 0 # 逐次処理される。
awk 1
awk 2
awk 3
awk 4
awk 5
awk 6
awk 7
awk 8
awk 9

gensub は gnu awk の機能名ので osxbsd awk にはないけど。

まとめ パイライン

このことは、パイプライン処理では、10.times.loop.sh の標準出力と xargs 標準入力を繋いでから起動することに起因する。

パイプライン処理などで、STDIN/STDOUTを接続するときは、接続される側する側の両方を起動して繋いでコマンド実行しているからだったと思う。詳しくは exec 族 のC言語関数を見ればわかったと思う

時間のかかる処理の結果をループで回すときは、for よりも パイプライン処理をしたほうが無駄が少なくて嬉しい。

bashの似てて紛らわしいもの [[ / [ / test  はどこが違うの?

似てて紛らわしいものシリーズ  [[ / [ / test

bash の記号で初心者泣かせの、似てて紛らわしかったり、使い分けがわからなかったり、読み方を間違えてパニックになる記号について書く

[ / [[ の違い

結論から言います [[ / [ は同じものです。 [[ が新しい書き方で機能強化されています。

[ / test の違い

違いはありません。同じものです。*1

[[ は [ とほぼ同じ機能と目的を持ちます。

個人的な意見ですが、 bashを書く上においては [[ でいいと思います。

というか bash が動かない環境なんてもうなくなったんだし、 [[ でいいと思います。個人的な意見ですが。

[/ [[ のヘルプを見てみます。

ちなみに bash で「返す」といえば終了ステータスのことです。

help [[
[[ ... ]]: [[ expression ]]
    条件式のコマンドを実行します。

    条件式 EXPRESSION の評価結果に基づいて 0 または 1 を返します。
     (略
    終了ステータス:
    EXPRESSION の値に基づいて 0 または 1 を返します。
help [
[: [ arg... ]
    条件式を評価します。

[POSIXの古い書き方*2UNIXの考古学をする人なんかが好きそうです。

機能面でも違う

おさらいです。

  • [ は古い
  • [[ は出来ることが増えた

bash を使う限りにおいて 積極的に [[ を使って構いません。っていうか [[ つかえ*3

[ は古いが役に立つ。

[ 後方互換性(つまり古いシェルスクリプトや sh だけに書かれたもの)を実行するときに今でも使われるようです。

dash / sh などのインタプリタや、POSIX互換原理主義に染まってる方々は使うことが多いようです。

なんで [[ なんてものが導入されたのか

TODO:このへんもう少し詳細に書くべき、いつから、なんのために、どうして、どこで使えるのか

便利だから。

[[ は [ に比べてどの辺が便利?

[[ は && が使える
[[ は || が使える
[[ は -e FILEが使える
[[ は -eq が使える
[[ は = でglob マッチが使える
[[ は =~ の 正規表現マッチが使える
[[ は < で文字列長さ比較ができる

[[[ で出来ることは全てできるのです。*4

[ には出来なかった事が[[にはできるのです。

なぜ [ では &&が出来ないのか?

bash のコマンド記号に解釈されてしまうから。

たとえば、<記号は リダイレクトとかぶります。

command  < finename

たとえば && 記号は コマンドの並列実行とかぶります

command && command

たとえば || 記号は パイプライン処理やコマンドの並列記述とかぶります

command || command

たとえば & 記号は バックグラウンド起動とかぶります

command &

これらはすべて、比較処理で衝突を引き起こします。

[ varname  < varname ]            # STDINリダイレクトに見える
[ varname  > varname ]            # STDOUTリダイレクト に見える
[ command || command ]        # パイプに見える
[ command &  command ]       # バックグラウンドに見える

[ を使うとエスケープ処理が必須!?

&& < >| といった記号がシェル・コマンドでで使われます。

そのため、[ を使うとエスケープ処理が必要になっていました。

## リダイレクトに解釈させない
[ varname  \< varname ]            
[ varname  \> varname ]            

エスケープ処理がとってもイメージしにくいですね!!!!

この<めんどくさい>エスケープ処理から私達を開放してくれる。

エスケープの解放軍は我らが [[ なのです。

エスケープ処理の有無による違いの例
$ [ aaaaa \< aaa ]; echo $?
$ [[ aaaaa < aaa ]]; echo $?

どちらのほうが読みやすいですか。少なくとも初心者には後者だと思います。

ね? [[ でいいでしょ?

存在を認知して使い分けできたら、 test/[ なんて捨てちゃえよ。POSIX互換なシェルスクリプトを書くときだけ[[[ に書き換えたらいいでしょ *5

[ / test の更に困ったこと。

[ には2種類あります。

一つが bash のビルトイン関数 [ 。もう一つが /usr/bin/[ です。

[ はそのファイル名のコマンドでもあります。

2種類の [、1つだけの [[

つまり、 [ は二種類あります。次の2つです。

  • function [
  • /usr/bin/[

それに対し [[ は関数のみです

  • builtin function [[

関数とコマンドどちらが優先されるのでしょうか?

優先度は ビルトイン>ファイル

bashにおける優先度は次のようになります。*6

関数
PATH

そのため通常はビルトイン関数が優先度が高いので ビルトイン関数が使われます。

GNU Bashをメインで使う私達にとって、/usr/bin/[ は互換性のためにあると行っても過言ではないと思います。

[test はほぼ同じものです

[ / test は次のすべてが候補になります。

  • ビルトイン関数 [
  • /usr/bin/[
  • /usr/bin/test
  • ビルトイン関数 test

4つもある・・・互換性のために test / [ がいくつもあって更に混乱を招くことになっているのです。

わけわからんわ。 わたしは [[ でいく!みなさんもどうですか?

わたしは、test は help test で比較条件を見るときだけ使うことにしました。

騙されたと思って [[ だけで書くといいですよ。

[[ を使うときの 一つだけ注意

[[ を使うときは test -a / test -o は使わないこと。これでスッキリしますよ

takuya@~$ [[ 0 = 0 && 1 == 1 ]] ; echo $?
0
takuya@~$ [[ 0 = 0 -a 1 == 1 ]] ; echo $?
-bash: 条件式に構文エラーがあります
-bash: `-a' 周辺に構文エラーがあります
takuya@~$

*1: 強いて言うなら [ がペアとなる ] を最後に必要として、test はいらないところ。

*2: [[ もPOSIX予約語ではあるんだけどね 。

*3:意見には個人差が有ります

*4: -a -o は出来ないけど && || がその代わりに使えるから問題なし。

*5:意見には個人差があります

*6: 正確には alias - hash - function - PATH の順番だったと思います

bashの似てて紛らわしいもの =/==と= 代入&比較の注意点

似てて紛らわしいものシリーズ  =/==と=

bash の記号で初心者泣かせの、似てて紛らわしかったり、使い分けがわからなかったり、読み方を間違えてパニックになる記号について書く

=/==と= の違い

=はいくつかのパターンで出てきます。

  • 比較演算子として
    • = で比較
    • == で比較(=と同じ
    • =~ で正規表現マッチ
    • 数値の比較
  • 代入指示子として
    • 変数(文字列)の代入
    • 数値の代入

これらのそれぞれで微妙な差異があるのです。めんどくさいよねー

大きな違い。両端スペース

=のスペースの関係。他のプログラミング言語を触った人はこのへんで、頭が???になると思う。

変数に代入するときはvarname=value のように書く、スペースは許されない。

文字列を比較するときは [[ / [ の中で使われる。スペースの有無は問われない。

変数に代入する場合(文字列)
vaname=value
スペースは許されない
vaname = value # コマンド引数に見えてしまう。
比較で使う場合(文字列
# どちらも可
[[ $vaname = value ]] ; echo $?
[[ $vaname=value   ]] ; echo $?

数値を扱う場合。

数値を扱う場合、文字列の場合と違って= は 代入、 ==は比較と明確に区別されます。

ただし、 let(( )) で記述が異なります。

こちらも=のスペースの関係が異なるので、頭の中をスッキリさせないと混乱します。

変数に代入する場合
let i=0
(( i=0 ))
(( i = 0 ))
let でスペースは許されない(重要
# 次の記述は出来ない
let i = 0
let i= 0
let i =0
(( )) ではスペースは許される
### 次の記述はすべて許される。
(( i = 0 ))
(( i= 0 ))
(( i =0 ))

比較演算子としての=

文字列の比較演算子として使うとき = / == のどちらも同じ役目を果たす。

さらに、= / == の右辺値はglob のパターンとして扱われる。$ から始まる文字列($nameなど)は比較前に変数が展開される。

さらに、glob は shopt extglob で拡張パターンを使うことが出来る。

拡張パターンは正規表現っぽくて反って分かり辛いので、正規表現比較 =~ を使ったほうがわかりやすい。

=/== の比較の例
[[ $name =  takuya ]] ;   echo $?  # == と同じ
[[ $name == takuya ]] ;   echo $?  # = と同じ
[[ $name == $username ]]; echo $?  # 両方に変数が使える。
[[ $name == taku* ]] ;    echo $?  # 右辺値は常にglobと解釈されるです。
[[ a =  [a-z]  ]]    ;    echo $?  # 正規表現ではなくglob です
[[ a =~ [a-z] ]]   ;    echo $?  # これは正規表現

とくに extglob on でのみ許される [[ = [a-z] ]] はshopt 環境に左右されるので怖いね

まとめ

ややこしいよね。とくに =前後のスペースはハマりどころだわ。

出現箇所/opr REPL [[ / [ (( / $(( let
a=0 代入 比較 代入 代入
a = 0 cmd:a args:= 0に解釈 比較 代入 syntax error
a==0 a='=0' 比較 比較 比較
$a=$name 変数展開+代入 変数展開+比較 変数展開+代入 変数展開+代入
a=str 代入 比較 変数展開+代入 変数展開+代入

注意点

この記事においては、コマンドの PS1があると非常に見づらいので、PS1='' にした

$ (( 1==1 ))   #$(( 1==1 )) に見えてしまう。
(( 1==1 ))   #こうした
$ varname=string   #$varname=string に見えてしまう。
varname=string    #こうした

参考資料

  • bash -x による実行により確認した。

bashのジョブ(bg/fg)とお手軽kill

bash のジョブの概念とコントロール

bash のジョブの話とkill の話。

Ctrl+zでプロセスはどこへ行くのか?

takuya@~$ ログアウト
停止しているジョブがあります。
takuya@~$ 

停止してるジョブってなんだよ!!!ってなる人が多い。

ジョブはどこに行った。

bash のプロセスはどこで管理されているのか。

ジョブの停止を知らないと、「停止してるジョブがでるから」、とタブ開きまくってませんか? screen / tmux でバンバンとタブ開けてませんか?

そのタブ本当に必要ですか?ジョブの停止と再開を知らないだけではありませんか?

実は、bash で起動したプログラムは ジョブとして いま使ってたその、bash が管理しています。

また、ジョブはSTDOUT/STDIN を繋いでるだけなので切り替えて使えます。

ジョブは恥だが役に立つ。

ジョブを切り替えていると、 tmux も screen もタブもほとんど使いません。
ある人に言わせるとダサいと揶揄されます。恥ずかしい気持ちにされることが有ります。

でも、わたしは、tmuxの代わりにジョブ切り替えをメインで使っています。

周囲にはジョブを切り替えてバンバン使う人を余り見かけないのですが。実はすごく役に立つ。

ジョブ使用例:ファイルの編集中にファイルを実行したい

ファイルを編集しているときに。

vim my_sample.sh

つくった スクリプトシンタックスチェックや、ちょっと実行したいときにどうします?

編集中にちょっと実行したい。

bash my_sample.sh と ちょっと実行したいとき。みなさんはどうしますか?

別タブで開く?tmux 使う? vimproc 使う?それとも根本的にVimをつかわずIDEでやりますか?

実はその殆どの処理は・・・・ジョブ管理で終わるのです。

そのタブちょっと待って、それ job で済むかもよ?

覚えるコマンドはつぎの3つです。

  • fg
  • bg
  • jobs

操作方法を覚える

そしてジョブの操作のために覚えるものは

  • Ctrl+z
  • kill
  • command &

です。

Ctrl+z で ジョブをバックグラウンドに入れて、kill することが出来る。

$ vim my_sample.php 
CTRL+Z
$ php my_sample.php
$ fg 

この切替にいちいちタブを使う必要はありません。

そんなことしなくても ctrl+z で一時停止すればいいのです。

vim 中に Ctrl+Z で一時停止

実行中に Ctrl + Z でvim を一時停止し、スクリプトを実行し、fg でもとに戻します。

ジョブコントロールの概念

vim のモードと同じように bash のジョブコントロールには3つのモードが有ります。

f:id:takuya_1st:20170101174002j:plain:w500

動いているものを Ctrl+z  で一時停止へ

一時停止をすると一時停止します。

一時停止から bg で バックグラウンドで起動

複数のプロセスがバックグラウンドにある場合。

$ bg 2  # job 2 をバックグラウンドにする

job の読み方

複数のコマンドを起動して切り替えて使うのに jobs を使いましょう。

jobs を実行すると次のように、現在実行(停止中)の実行中プログラム一覧が出てきます。

まずはじめに、ジョブを起動して一時停止してみます。

takuya@Desktop$ man bash

[1]+  停止                  man bash
takuya@Desktop$ man vim

[2]+  停止                  man vim
takuya@Desktop$ man gawk

[3]+  停止                  man gawk

man を実行して、表示しているときに Ctrl-Z を押すと、コマンドが一時中断されて、シェルプロンプトに戻ってきます。

この状態で、jobsコマンドを叩きます。すると。。。

takuya@~$ jobs
[1]   停止    man bash 
[2]-  停止   man  vim 
[3]+  停止   man gawk

これら、3つのジョブはすべて一時停止されていることがわかります。

ここから、次のようにすると、コマンドを再開することができます。

fg 番号
  • fg 3 とすれば、 3 つまり man gawk が最前面に来ます。
  • fg 2 とすれば、2 つまり man vim が最前面に来ます。
  • fg 1 とすれば、2 つまり man bash が最前面に来ます。
fg 1 と fg  は同じです。

単にfg と打てば、 + がついてるものが出てきます。

これは、スタックと呼ばれるもので、上に積んだもの(最後に使ったもの)が最上位に来ています。次はこれについて見てきます。

ジョブスタックについて

ジョブは、番号とスタックの両面で管理されています。

先程のジョブ一覧をもう一度よく見てみます。

takuya@~$ jobs
[1]   停止    man bash 
[2]-  停止   man  vim 
[3]+  停止   man gawk

数字の他に記号がついていることに気づくと思います。これがスタックを示しています。

スタック(最後に使った順)
No.1 +
No.2 -
No.3 なし

単純に、fg bg を繰り返したとき、最初に出てくるものは、このスタック順に取り出されているのです。

いちいち kill pidしてませんか?

シェルで実行し、バックグラウンドプログラムを終了するのに、プロセス番号を指定してませんか。jobsをちゃんと理解してない人にありがちです。

job 管理されているなら、 ps で pid を見なくても kill 出来るんです。

kill %3

でジョブ3を削除できます!便利すぎる。

もういちいち、PID確認しなくていいんですよ。

job 放置したらどうなるの

放置したまま、ログアウトしたらどうなるのか。

HUPします。

ジョブ使いましょう。

ここまでやってみてどうでしたでしょうか。Ctrl-Zの意味はわかりましたか?ジョブを複数起動して切り替えるという意味がわかったでしょうか。

複数のコマンドを起動するのであれば、ジョブが一番お手軽で便利です。

vim でファイルを編集しながら他のファイルを見たり実行したりするのもお手軽ですし、なにもインストールが必要ありません。bashだけで完結します。

ターミナルのタブ機能、またscreenやtmuxに頼りがちな人は、最初にジョブを習いそびれたのではないでしょうか。

2019-08-23

記述を修正

bashの複数タブ間コマンド履歴(ヒストリ)共有とPROMTO_COMMAND変数について

bash でもコマンド履歴の即時反映をしたい

複数タブでターミナルを使ったり、複数ウインドウでターミナルを使うと、bashの履歴が共有されて無くて、悲しいことがある。

zsh の機能で紹介されることも多いですが。それ bash でも出来るよ。

共有方法
export PROMPT_COMMAND="history -a"

以上!

これで、複数ターミナル。タブ、( 設定次第では screen tmux ) でヒストリを共有することが出来る。

Mac OSXの場合は iTerm.appTerminal.app でも共有できるんだな

GNU Screen で使える設定

さらに強力に即時反映をするならば、screen の場合は即時反映をするために

export PROMPT_COMMAND="history -a; history -c; history -r; $PROMPT_COMMAND"

が必要になる。。。。Enterを打てばすぐに反映される。わたしゃここまでやらないけど

PROMPT_COMMAND とは?

PROMPT_COMMAND は コマンドが実行されて、次のPS1が表示されるまでに実行されるもの。

関数を書けば、関数を実行してくれる。

man によると
       PROMPT_COMMAND
              設定されていると、プライマリプロンプトを出す前に毎回、 この値がコマンドとして実行されます。

PROMPT_COMMANDを遊んでみる

function my_prompt_command (){
     status=$?
     echo command last exit status is $status
}
export PROMPT_COMMAND="my_prompt_command"

ヒストリ共有って便利ですか?

じつは、個人的には、ヒストリ共有は余り使わない。GNUScreenではオフにしてる。複数ウインドウ限りオンにしてます。

私の場合、複数タブやscreen などを起動するときは、「用途ごと」にタブを開くので、コマンド履歴がすべて同じなってしまうとむしろ邪魔で仕方がない。

私の PROMPT_COMMAND='history -a'
history -a

これだけで、十分なことが多い。

history -a ここまでのヒストリを .bash_history をファイルに追記してくれる。

PROMPT_COMMANDで履歴管理の他の使い方

たとえば、間違って入力したコマンドはヒストリに保存したくないなど。

typo した汚いコマンド履歴が残るのが恥ずかしくて嫌な時もあります。そういうときは次のようにすれば回避できます。

function my_prompt_command (){
     status=$?
     echo last_status $?
     if ((  status  == 127 ));  then
          history -r
     else
          history -a
     fi
}
export PROMPT_COMMAND="my_prompt_command"

まぁ、これも、一見すると便利そうだが、間違って入力したコマンドは再編集するほうが圧倒的に多く、保存しないのは不便なので余り役に立たないと思いますが。 こういうことが出来ますよーって程度に見ておいてください。

参考資料

  • help history
history: history [-c] [-d offset] [n] または history -anrw [filename] または history -ps arg [arg...]
    ヒストリ一覧を表示または操作します。

    行番号をつけてヒストリを表示します。操作した各項目には前に`*'が付きます。
    引数 N がある場合は最後の N 個の項目のみを表示します。

    オプション:
      -c    ヒストリ一覧から全ての項目を削除します。
      -d offset    OFFSET 番目のヒストリ項目を削除します。

      -a    このセッションからヒストリファイルに行を追加します
      -n    ヒストリファイルからまだ読み込まれていない行を全て読み込みます
      -r    ヒストリファイルを読み込み、内容をヒストリ一覧に追加します
      -w    現在のヒストリをヒストリファイルに書き込みます。そしてそれらを
        ヒストリ一覧に追加します

      -p    各 ARG に対してヒストリ展開を実行し、結果をヒストリ一覧に追加し
        しないで表示します
      -s    ARG を単一の項目としてヒストリ一覧に追加します

    FILENAME を与えた場合、FILENAME がヒストリファイルをして使用されます。それが
    無く、$HISTFILE に値がある場合その値が使用されます。そうでなければ
    ~/.bash_history が使用されます。

    もし $HISTTIMEFORMAT 変数が設定され、NULL で無ければ、strftime(3) の書式
    文字列として各ヒストリ項目の時刻を表示する際に使用されます。それ以外は
    時刻は表示されません。

    終了ステータス:
    無効なオプションが与えられるかエラーが発生しない限り成功を返します。

bashの何もしない特殊コマンド : コロン

: はコマンドです。

え?コマンド? と思うかもしれません。はい、コマンドです。記号一文字がコマンドです。

: がコマンド(関数)の証拠
takuya@~$ type :
: はシェル組み込み関数です
: コマンドを実行した結果
takuya@~$ : ; echo $?
0

: は何に使うの?

1文字コマンド : はtrue (終了ステータス0)を常に返します。 /bin/truetrue ( 関数 ) もコレと同じ仲間になります。

いつもtrueになります。こんなのなんの役に立つの?いえいえ、いつもtrueで文字数が少ない。これが重要なのです。

たとえば無限ループに使います。

無限ループの例*1
while : ;
do
     echo 1 ;
     sleep 1 ;
done

その他の活用

次のように、if でシンタックスエラーの防止の為に使われます。

シンタックスエラーになる例
if command ; then
else
     do some thing
fi

if の then の中身が空だと?シンタックス・エラーになります。コレを防げます。

: を使ってシンタックスエラー防止
if command ; then
     : # なにもしない : を使う
else
     do some thing
fi

なぜこんなのが出来たの?

だって、シェルスクリプトは、「終了ステータス」で判断するから。 関数が戻り値を 0 か0以外しか返せません。

だから プログラミング言語でいう 1 / true のかわりに :も使えるようになっています。

他にもこんなことが出来ます。

次の例はtouch と同じです。これは、: が必ず成功し0バイトを出力するので、空ファイルが作成されます。

: > output.txt

コメント代わりに 使うことも!

ヒアドキュメントと組み合わせると、ブロックコメント代わりに使うことも!!

bash のブロック・コメントを擬似的に実現する。
: << 'SKIP'
コメント
コメント
SKIP

なんと : は引数とります。

不思議なことですが、 : に引数を与えることが出来ます。

でも引数を渡しても何も起きません。だから、引数を無視する役目を見出すことが出来ます。 なのでコメント代わりに使えます。

例:引数をコメントとして使う。
$ : ここは引数

コメント代わりに使われることが多い

stack over flow では cronjob のコメントに使ってますね。 コメントアウト代わりに使う人が多いようです。

for command ; : ここの実行を待つ ; do
     : ここは何もしない
done

この例みたいにif 文の中に埋め込みコメントに使えますよーって。めっちゃかっこいいwww

変数の初期化とかにも使えますね。そうだね

参考資料=#変数参照の副作用を利用する

同じようなものに true (関数)があります。

takuya@~$ help true
true: true
    結果として成功を返します。

    終了ステータス:
    常に成功です。

同じです。しかし : のようにコメントなどに使うと意味が変わってしまいますよね?

# コメントの例
: ここは何もしない
true ここは何もしない

true は流石に使いづらいよ

なんの役にも立たない?

何の役にも立たないことが役に立つときもあるのです。

2019-08-23

記述を修正

参考資料

*1: 「 : 」 を強調するため doを改行から始めてますが、普通はdo の手前で改行はしません。

bashの似てて紛らわしいもの [[ / ((

似てて紛らわしいものシリーズ [[ / ((

bash の記述で初心者泣かせの、似てて紛らわしかったり、どう使っていいかわからなかったり、読み方を間違えてパニックになる記号について。今回は [[ / ((について

[[ / (( の違い

[[ / (( は if 文の条件判断の中で使われることが多いですが、それ単体でコマンドです。

これらはコマンドなので終了ステータスを持ちます。

  • [[ は new スタイルの testコマンドです。これで文字列比較やります。
  • (( は new スタイルの数値計算です。これで数値比較やります。

どちらも if 文でよく使われる

どちらも、if 文のなかで使われることが多いんですが、違うものです。似ているものだけど違うものです。

一番大きな違いは 以下の点にあります。

  • [[ は文字列比較で使われる
  • (( は数値比較で使われる。
[[ 0 == 0 ]] # 文字列として比較
(( 0 == 0 )) # 数値として比較

なぜんこんなことを書くのか。

==で数字の一致チェックだと正しく動く、数字文字列比較も数値比較も全くおなじに見えるからです。

どちらも true になる比較が次の例です。

[[ での等価比較
var=0
if [[ $var == 0 ]] ; then
    echo yes
fi
(( での0と等価比較
var=0
if (( $var == 0 )) ; then
    echo yes
fi

大小比較すると明確に違う

var=1000
[[ $var > 11 ]] ; echo $? # exit code 1 つまり false
(( $var > 11 )) ;echo $?  # exit code 0 つまり true
  • [[ の比較は辞書順。1000 > 11 は11 が大きくなる。
  • (( の比較は数値順。1000 > 11 は1000 が大きくなる。

ここは油断すると酷い目に遭うので注意が必要。 原理をしらず== で比較で使ってた。

わたしは数年前に手痛い目にあって覚えた・・・辛い。

組み合わせたらどうなるのか?

組み合わせられない。よくわからないことになる。試したけどちょっと何ががなんだか。

$ (( [[ 0 ]]  ));echo $? #=> 1 / false
-bash: ((: [[ 0 ]]  : 構文エラー: オペランドが予期されます (エラーのあるトークンは "[[ 0 ]]  ")
1
$ [[ (( 1/0 )) ]]; echo $?

0で除算がエラーにならない??もうわけわからんね・・・

bashの似てて紛らわしいもの (( / $(( の丸括弧

似てて紛らわしいものシリーズ (( / $((

bash の記述で初心者泣かせの、似てて紛らわしかったり、どう使っていいかわからなかったり、読み方を間違えてパニックになる記号について

(( / $(( の違い

(( / $(( は数字を計算する方法です。どちらも同じ機能を持ちますが・・・

  • (( はコマンド
  • $(( は展開表現

コマンドとは実行されるもの、展開表現とは実行される前に置換されるもの。

$(( {1..10}$varname と同じく、インタプリタが解釈した時点で評価され文字に置き換わります。

(( はコマンド として実行され、終了ステータスを返します。

どちらも数値計算をする点は同じ

(( / $((は、どちらも次のように、代入を解釈したり、四則演算や比較評価をする点においては同じです。次のような計算ができます。

i++
i=0
i=i+1
i%=10
i==9

実行前に解釈される $((

$(( 実行前に計算されて文字列として結果を返します。 *1

$(( の例

$ echo $(( 1 + 1 )); echo $?#=>0
2

よくある間違い

$  $(( 1 + 1 )) # コレは間違い

とくにわけがわからなくなるのが、この間違いの記述。実行前に解釈されて文字列に置き換わる、そして出力結果を実行しようとする。

$  $(( 1 + 1 )) # コレは
$  2 # こうなって
$  2 # コマンドが見つからない・・・

$((の意味は $( に近いです。 echo $( command ) が先にcommandが実行され、結果に置き換わるのと同じです。

echo $(( 1+1)) と書いたときも、 先に (( が実行され、結果の文字列 に置換されます。その結果echo 2 となりコレが実行されるのです。

コマンドとしての ((

(( はコマンドとして実行されます。

正しく数値計算ができたら終了ステータス0、比較結果がfalse なときも終了ステータス1を返します。

あと、ゼロ除算など数値計算が出来ない時は終了ステータス1を返します。

コマンドなので、コマンドが書ける所なら、どこにでも記述できます。つまり if 文や while などの条件(コマンド)として使えるのです。

コレは間違い
$ echo (( 1 + 1  )) # 間違い echo はコマンドを引数に取らない
コレらは全て正しい記述
## 足し算
$ (( 1 + 1 ))  ; 
## 変数の計算
$  i=0; (( i++ ))  ; echo $i ; 
## 変数の比較
$ (( i = 1 )); (( i > 0 )) && echo $i; 
## while 内で加算と比較
$ i=0; while (( i++ , i < 10 )); do echo $i ; done ;

true と false に注意

(( expression )) の 比較は(( 1 > 0 )) true になるが、終了ステータスは0である。文字は返ささない

$(( expression )) の比較は$(( 1 > 0 )) は true になり、文字列として1が返ってくる。

補足すると、$(( expression ))は展開されるので終了ステータスは存在しない

true の扱いについては油断してると頭の中を持っていかれるので注意が必要。

*1: 実行前という表現が正しいかわかりませんが、コマンドとして実行される前という意味です。

bashの似てて紛らわしいもの ( / ((

似てて紛らわしいものシリーズ

bash の記述で初心者泣かせの、似てて紛らわしかったり、どう使っていいかわからなかったり、読み方を間違えてパニックになる記号について

(((

全然意味が違うので、間違えると大変。また見た目は似ているので似たようなものだろうと、タカをくくってると痛い目を見ます。

  • (( は算術計算。
  • ( はサブプロセスで実行(サブシェル起動)

とそれぞれ、全然意味が違います。

(( は算術計算

C言語のようなスタイルで、「算術計算」や「比較」「変数の加算」が出来ます。((計算結果を表示しません

不可能な計算をすると 終了ステータス1を投げてエラー・メッセージを出します。

(( の記述の例
((  i=0, i++ )) 
((  i%10 )) 
(( i > 0 ))
(( i +1  > 0 ))
(( i << 1 ))

((は if 文の中で使われるとは限りません。変数をインクリメント・デクリメントしたり let と同等なので算術計算にも使えます。

他にも

  • 四則計算と剰余* / % + -
  • 数値比較< > <= >= == !=
  • ビット演算 ~ << >> & ^ |
  • インクリメント 系id++ id-- ++id --id
  • 三項演算子 num = num ? var : num
    などが使えます。さすがに多いので、すべての例は別の記事にまとめようと思います。

( はサブシェルを起動

( の中に書かれるのは、コマンドです。コマンドはbash とは違う環境で実行される。サブ・プロセスのbash中で起動されます。

変数は引き継がれますが、サブ・プロセス中での変数変更は呼び出し元には影響しません。

これはパイプライン処理で変数の変更を維持できないのと同じこと(プロセス起動による)だと考えられます。

$ echo i=$i ;  ( ((i++));echo i=$i ; ) ; echo i=$i ;
i=1
i=2
i=1

cd なども同じです。

( を使うとよくハマるのが、ワーキングディレクトリの変更です。サブシェルの中での変更は、メインに戻ってきたとには捨てられています。

 $ pwd; ( cd /etc ; pwd ) ; pwd
/Users/takuya
/etc
/Users/takuya

((( を組み合わせたらどうなるのか

(( 1+1 ))
((( 1+1 )))
(((( 1+1 ))))

もうわけがわからないよ!!!

(( 1+1 ))      # (( 1+1 ))
((( 1+1 )))    # ((  ( 1+1 )  ))
(((( 1+1 ))))  # ((  (( 1+1 ))  ))

こんな使い方はやめましょう・・・

参考資料

`

   (list) list  is  executed in a subshell environment (see COMMAND EXECUTION ENVIRONMENT below).  Vari-
          able assignments and builtin commands that affect the shell's environment  do  not  remain  in
          effect after the command completes.  The return status is the exit status of list.
   ((expression))
          The  expression  is  evaluated according to the rules described below under ARITHMETIC EVALUA-
          TION.  If the value of the expression is non-zero, the  return  status  is  0;  otherwise  the
          return status is 1.  This is exactly equivalent to let "expression".

`

bashのwhile/until ループ構文について

bash の while 文について

bash で書ける while文について触れておきます。

while 文の基本構文

while <COMMANDS> ; do 
  <COMMANDS>
done;

ここでの <COMMANDS>は 複数行のコマンドリストのことを指しています。 COMMANDSに含まれるものは次のとおりです

COMMANDS
( COMMANDS;  )
{ COMMANDS ; }
command ; command;
command && command
command || command
command

ここで書いている command は一般コマンドのことで (( [[[ もcommandに含めます。

while の; の書き方

改行で ; が自動的に挿入されることを利用し、while を次のように記述する人も多いみたいです。

while <COMMANDS>
do 
  <COMMANDS>
done

このように、; の代わりに改行コードを書いたwhileでは、 do のぶんだけ1行多くなる。 なので、この1行を減らそうと、改行を削除するももの; の概念に気付かずSyntaxErrorで詰まる人をよく見かけたので注意してほしい。

色々みていたら、 bash を書くときは do で一行浪費するより ; do と書くことのほうが多いようです。*1

while の break

while には break が書ける。

while <COMMANDS> ; do 
  <COMMANDS>
  break;
done;

break は while や for のネストの数に応じて何個抜けるか書くことが出来る。 break N で N段のネストを抜ける。

while <COMMANDS> ; do 
   while <COMMANDS> ; do 
      <COMMANDS>
      break 2;
   done;
done;

while の continue

while文では continue も使える。continue を使うと、現在ループを途中で中断して次のループに入る。

while <COMMANDS> ; do 
  if <COMMANDS>; then 
        continue
  fi 
  <COMMANDS>
done;

untile構文

while と not を使えばいいので、あんまり出番がない。do-until はいまのところ存在しないみたい

until <COMMANDS> ; do
  <COMMANDS>
done

until を使わずとも、 whileでも同じことです。

while ! <COMMANDS> ; do
  <COMMANDS>
done

until における break や continue も while と同じです。

until使う意味あんの。。。

参考資料

http://wiki.bash-hackers.org/syntax/ccmd/while_loop

*1: ちなみに、昔のブログなどの記述を見てると do を改行するほうが多いような気がした。

bashの関数定義には「リダイレクト」も含まれる

bash の関数定義について

bashの関数定義の記事で触れていないことが有ります。というか意図的に省きました。

bash の関数定義には「リダイレクト」が含まれます。

リダイレクトを含めたbash関数定義

bashにおける関数定義は、正しくは実は次のようになっています。

はい。そうです。リダイレクト先が含まれます。

function FUNCNAME () { COMMANDS } > REDIRECTIONS
function FUNCNAME  { COMMANDS } > REDIRECTIONS
FUNCNAME ()  { COMMANDS } > REDIRECTIONS

リダイレクトも含めると楽なことがある。

以下のような記述は、まとめて書くことが出来る。一行に書くときや、エラーログを統一したいときに便利ね。

before
function MyFunc () {
   some_command1 > my.log
   some_command2 > my.log
   some_command3> my.log
}
after
function MyFunc() {
  some_command1;
  some_command2;
  some_command3;
} >  my.log

そもそもコードブロックをリダイレクト出来る

Bashの関数定義ですこし触れましたが、 {} のコードブロックは関数の定義の一部と考えると理解しやすいです。

リダイレクトについても同じで{ } のブロックのすべてのリダイレクトを一括して指定出来る。

{
  commandA 
  commandB
  commandC
} > /dev/null

これは、if などの条件分岐を書くときにちょっと便利。

if { commandA && commandB } > /dev/null ; then some_command; fi 

コレを使うことで、/dev/null 地獄から脱出できる

コードブロックを上手に使うとこんなことが出来る

php など言語からshell 呼び出しをするときにバックグラウンド起動で関数やコードブロックを渡すことが出来る。

<?php
shell_exec ( "{sleep 100;sleep 100;} > /dev/null 2>/dev/null &; ");

phpで複数行のシェル呼び出しをバックグラウンドにする - それマグで!

また if 文の条件をまとめて関数にすることが出来る。

function MyConditions() {  some_command1;  some_command2;} >  /dev/null

if MyConditions ; then 
  do_something;
fi 

私は馬鹿なのでコレくらいしか、利用方法が思い付かないが、実はすごくいい活用方法があるのかもしれない。

参考資料

The classic test command [Bash Hackers Wiki]

bashでのif/while条件判断 に使える[[ / (( 条件の一覧<コマンド・ファイルの存在などで条件分岐>

bash ではいろいろな条件で分岐が出来る。

ifで使えるいろいろな条件を見てみたいと思う。

数値判断系

数字を比較するときは以下を覚えておけばいい。

(( 1 > 0  ))
(( 1 >= 0  ))
(( 1 < 0  ))
(( 1 <= 0  ))
(( 1 == 0  ))
(( 1 != 0  ))
((  0  )) # 終了ステータス 1 /if では false
(( 1  ))  # 終了ステータス0 /if ではtrue
((  ! 0  )) # 終了ステータス 0 /if では true
(( 1 && 1 ))
(( 0 ||1 ))

数値を比較するときは if (( 1!=0 )) ; then echo yes ; fi と書きます。この数値判断系の出来ることはlet と全く同じです。

(( ))のdouble parensis での記述方法は また別記事もっと詳細に書きます。

繰り返しになりますが (( もコマンドです。お忘れなきよう。

new スタイル test コマンド [[

test コマンド系です。

[[ -e /etc/pass ]]のようなダブルブラケット [[の記述は 新しいtest のスタイルです。

[[ が導入されたので、私も test も [ も使わず [[ で書くことが多い。

[[ による test コマンドは機能追加されている以外 [ と同等です。

そのため、紹介例も [[ で書きます。 [ / [[ / test の違いはまた別の記事に書きます。

繰り返しになりますが [[ もコマンドです。お忘れなきよう。

[[で導入された新しい書き方
[[ $name = takuya ]]     #  文字列マッチ 
[[ $name == takuya ]]   # 文字列マッチ
[[ $name == taku* ]]     # glob マッチ
[[ $name =~ ta.+ ]]       # 正規表現
[[ '' ]]       # 空文字は exit status 1 で if 中は false 扱い
[[ 'aaa' < 'aaaa' ]]    # 文字列の長さ比較
[[ 'aaa' > 'aaaa' ]]    # 文字列の長さ比較
[[ !$name =~ ta.+ ]]  # not による否定
[[ $var1 ||  $var2   ]]     #  ORによる条件
[[ $var1 && $var2 ]]     #  ANDによる条件
[[ で使える昔からの条件:ファイルチェック編
[[ -e <FILE> ]] #  パスが存在するか、存在したら true  / -a でも可だが、-a が AND と混同する恐れあり
[[ -f <FILE> ]] # パスが存在する and 通常ファイルであれば true
[[ -d <FILE> ]] # パスが存在する and ディレクトリであればtrue
[[ -c <FILE> ]] # パスが存在する and テキストファイルであれば true
[[ -b <FILE> ]] # パスが存在する and ブロックファイルであれば true
[[ -p <FILE> ]] # パスが存在する and 名前付きpipe であれば true
[[ -S <FILE> ]] # パスが存在する and ソケット であれば true
[[ -L <FILE> ]] # パスが存在する and Symlinkであれば true
[[ -h <FILE> ]] # パスが存在する and Symlinkであれば true
[[ -g <FILE> ]] # パスが存在する and sgid 付きであれば true
[[ -u <FILE> ]] # パスが存在する and suid 付きであれば true
[[ -r <FILE> ]] # パスが存在する and 読込可能であれば true
[[ -w <FILE> ]] # パスが存在する and 書込可能であれば true
[[ -x <FILE> ]] # パスが存在する and 実行可能であれば true
[[ -s <FILE> ]] # パスが存在する and サイズが0以上であれば true
[[ <FILE1> -nt <FILE2> ]]   # 最終更新日mtimeの比較。<FILE1> が <FILE2> より新しいとtrue
[[ <FILE1> -ot <FILE2> ]]   # 最終更新日mtimeの比較。<FILE1> が <FILE2> より古いとtrue
[[ <FILE1> -ef <FILE2> ]]   # <FILE1> と <FILE2> が同じinodeを指してたら true 
[[ -t <fd>     ]]     #   ファイルディスクリプタなら true
[[ / [ で使える昔からのtest条件:文字列
[[ -z $var ]] # 文字列が長さゼロ、つまり空文字 '' なら true
[[ -n $var ]] # 文字列が入っていたら true

ただし、これらは [[ を使う限りにおいて [[ !$var ]][[ $var ]] と同じなので明示する意図以外で使わないはず。

[[ / [ で使える昔からのtest条件:変数
[[ -v varname ]] # 変数が宣言されていたら true  / 配列の場合は array[n]
[[ -r varname ]] # 変数が未宣言なら true

コマンドの実行結果を判断に使う

繰り返しになりますが、if 文はコマンドの実行結果を使って判断しています。 コマンドの終了ステータスが 0 を見ています。

if command ;  then 
   do_something;
fi
複数行のコマンド実行結果

複数行のコマンドを併せて考えることが出来ます。

command && command 
command || command

if や while にかくと次のようにになります。

if command && command ; then ...
if command || command ; then ...
while command  && command ; do ...
複数コマンドを実行して最終実行で判断
command ; command ;
{ command ; command ; }
( command ; command  );

( ) はサブシェルを起動するので速度が遅いのと、変数空間が異なるので注意。

最後に。これら条件の確認方法。

while や if で使う条件のこれらの構文の動作チェックをしたいときは、次のようにチェックすると楽ですよ

[[ -e /etc/passwd ]];  echo $?

結果が 0なら、終了ステータス0なのでtrue になります。

これを幾つか組み合わせることで、bashの条件判断(と比較演算子)で何が起きているのか理解することが出来ます。

(( 0 )) ; echo $? #=> 1

また、別の記事書きますが、数字の比較と文字列の比較についてのよくあるミスも確認することも出来ます。

[[ 1000 < 11  ]] ; echo $?  #-> 0

この記事で紹介した条件は多岐にわたるので、自分のターミナルで試してみると安心して使えます。

参考資料

bashで条件分岐 - if

bash で if の分岐について

bash で if の条件分岐は、C言語プログラマif 構文を一見して理解できるように書かれています。が、ちょっとコツが実は必要なのです。

bash における条件分岐

前回は、bash は条件分岐を「コマンドの終了ステータス・コード」で判別しているという話をしました。

そして、条件分岐につかう構文も「コマンド」でその終了ステータスを見て判断されます。&& を使うとコマンドの成功を条件に続けてコマンドをすることが出来ます。

前回のおさらい

だいじなので前回のおさらいをしておきます。

終了ステータスの確認方法
[[ takuya == taku* ]]; echo $? #=> 0 
終了ステータスを使ったコマンド実行
[[ takuya == taku* ]] &&  echo comand success;

さらに、コマンドは{} で複数書くことが出来るので、複数行を書くことが出来ます。

複数コマンドの実行
[[ takuya == taku* ]] && {
    echo Hello ;
    echo takuya;
    echo  sann;
}

よく見ると、この記述は C言語の if に似てませんか?わたしは似ていると思います。

この書き方のように if 文を bash で書けるとC言語系の if 文が分かる人に読みやすくなります。それが bash のif です。

bash の 条件分岐と if 文の比較

bash における if は基本的に、command && { comand ; } の構文と同じことをしています。一点だけ違うところは、 && が成功時(または失敗時)の実行に対し、 if 文は elif 節、else節が使えるというところです。

まずは、普通のif 文と比較して見ていきたいと思います。if と && がほとんど同じ書き方であると理解できると思います。

&& で書いた場合
name=takuya
[[ $name == takuya  ]] && {
    echo Hello ;
    echo takuya;
    echo  sann;
}
上記を if で書いた場合
name=takuya
if [[ $name == takuya  ]] ; then 
    echo Hello ;
    echo takuya;
    echo  sann;
fi

上記の2つは似てますよね?ほぼ同じです。 if が導入されることで 読みやすくなったと思います。if は 引数にコマンドを取るコマンドと理解してもいいかもしれません。

if 文の構文についておさらい

if 文は次のように、if comand ; then ; command ; fi で書きます。改行を入れても大丈夫。

if command ; then 
    command;
fi

ポイントは、「条件式もコマンドである」というところです。条件式がコマンドで、その終了ステータスで判断しているという点です。

else を入れた場合
if command ; then 
    command;
else 
    command; 
fi 
elif を入れた場合
if command ; then 
    command;
elif command; then
    command; 
else;
    command; 
fi 

よくある間違い

bash が「コマンド」をベースにしているので 実行するコマンドがないとエラーになります。

コマンドを実行するので、コマンドの直後でthen の直前に ; がないとエラーになります。

「if がコマンドを実行し結果で判断する」するとおぼえておけば、 ; が必要なわけも理解できると思います。; がないとすべてが引数になってしまいます・・・悲しい

if command ; then 
   # 何も書かないのだめ。コメントだけも駄目
fi

よくある間違い2

if command  then 
        command
fi

;がないと全部引数に見えてしまうお

$ if command  then command  fi 

上記の例を見れば、 ; then と then の直前に必要な理由がわかります。

then の直前の ; は省略できる。
## then の直前に改行がある
# これは動く
if command 
then 
        command
fi

bash は改行が来たらコマンドの終了と解釈する(または ; を補完したと解釈してもいい)。そのため、 then を改行から始めると ; が無くても動きます。

この辺の改行に有無も<ゆらぎ>が C言語系のプログラマbashがわかりにくい理由だと思います。

これも「if はコマンドを実行している」と強く意識していれば迷うことはないでしょう。

if 文のコマンドは正確には「コマンドリスト」

if の条件は コマンド リストで書くことが出来る。

takuya@:~$ if echo 1 ;echo 1 ; echo 1 ; then echo true; fi
1
1
1
true

複数コマンドの結果を併せて考えることも出来ます。 && でコマンドを結合すれば条件を複雑に書くことも出来ます。

次の例は&& でコマンドを結合したことがわかりやすいように { コマンド && コマンド }で括っています。

例:&& で複数コマンドの結果を条件にする
if {  grep takuya *.txt  &&   grep takuya *.php ;} ; then
    echo ok;
fi
#=> ok

まとめ

  • bash の if 文は「終了ステータスで判断する」
  • if 文は「コマンド」を実行している
  • if に書けるコマンドは複数行書くことが出来る。

このことを頭に入れておけば、if の構文が、プログラミング言語と若干違うことに戸惑いを感じること無く、そしてシンタックスエラーをもう二度と出すことはないと思います。

参考資料

  • help if
takuya@:~$ help if
if: if COMMANDS; then COMMANDS; [ elif COMMANDS; then COMMANDS; ]... [ else COMMANDS; ] fi
    条件に従ってコマンドを実行します。

    `if COMMANDS' を実行します。この終了ステータスが 0 の場合、`then COMMANDS'
    を実行します。そうでない場合は、各 `elif COMMANDS' を順番に実行し、その
    終了ステータスが 0 の場合に、関連した `then COMMANDS' を実行し、if 文が
    完了します。それ以外の場合、 `else COMMANDS' が存在する場合には実行され
    ます。文全体の終了ステータスは、最後に実行したコマンドの終了ステータスか、
    または、テストした条件に true となるものが無い場合は 0 です。

    終了ステータス:
    最後に実行したコマンドの終了ステータスを返します。