メインコンテンツに移動

はじめてのBASH

BASHは,よく使われるスクリプト言語です.(詳細はこちらにある

計算機クラスタで「ジョブ」を投入するとき,ほとんどのスーパーコンピューターで「bashのスクリプトを与える」ことになっています. 
というわけで,BASHのスクリプトを「ジョブ文」とか「ジョブスクリプト」と考える向きも多いでしょうね.

コマンドにも用いられます.プログラムだと思っていたら, 
     head `which コマンド` 
で最初の方を表示したら,最初に 
     #!/bin/bash 
と書いてある,なんてのは全部BASHのスクリプトです.

どうやって起動するかというと,Macで【ターミナル】を起動すると

これで起動しています.

2019年以降, Macのデフォルト設定が変更になり,ターミナルを開くと,BASH ではなく, よく似た ZSH が起動するようになりました.ZSHの場合,こんな感じになります:

szs

どこが違うのか?というと,カーソルの前が $ ではなく % になっている!のが大きな違いです.少し文法が違います. が,まあたいして変わらんので気にしない気にしない.

というわけで,あなたに2通りの選択があります.

いつもBASHを使いたい

LinuxやスパコンではBASHの場合も多いし,以下の説明は全部BASH用に書いてあります. 毎回 BASH が起動するように設定してしまいましょう:

ass

これで,今後はターミナルをひらけば黙ってBASHです.

たまにBASHを使いたい

今後はZSHが主流になるという読みであれば, ZSHのままでも良いですね.それなら, 使いたい時だけ,毎回 exec bash と入力すれば良いのです:

ba

なんでこんなめんどくさいことになったのかというと, bashはオープンソースで開発されているのだが, GPL2ライセンスをGPL3ライセンスに変更してしまったので, Appleが怒った.ということですね.

GPL2の場合:オープンソースの資源に自社の特許つきソフトを加えて商品を開発した.開発商品のソースコードを公開するのであれば,商品にして対価を受け取っても良い. 
GPL3の場合:オープンソースの資源に自社の特許つきソフトを加えて商品を開発した.開発商品のソースコードを公開するのであれば,商品にして対価を受け取っても良い.ただし,その特許も公開したものと解釈する(つまり特許料は今後0円, 開発してもお金は取れない) 
で・・・GPL3に企業が反発して騒ぎになっている,ということらしい. やっぱGNUはGNUだということか.

BASHの基本的機能

BASHの基本的機能は,

  • 1行入力を受け取ると,実行する

というわけで,コマンドを入力すると実行するわけです:

多くの人がここまでしか使いませんが,BASHの機能はずっと多く,ちゃんとしたプログラミング言語になっています.プログラミング言語の基本は,変数に値を代入できることです. つまり

X=12

これは, 変数Xに値12を代入しています.BASHで用いられる変数は,整数と文字列の2種類があります.文字列を代入するには

 Y="Hello World"

実際に値を持っているかどうか確かめるには,

echo "$Y $X" 
printf "文字列は[%s] 整数は[%d]\n" "$Y" $X

などのコマンドが使えます. 空白を含む文字列は, コマンドの境目の空白と区別がつかないので, "" で囲っていることに注意.

コマンドを実行するbashならではの変数が $? です. これは,直前のコマンドの終了コードを与えます. 普通は正常終了なら0, 他は1とかです.

$PATHも面白い変数です.

echo $PATH 
/usr/bin:/bin:/usr/sbin:/sbin:/opt/X11/bin:/Applications/inkscape.app/Contents/MacOS:/opt/ImageMagick/bin:/Applications/Ghostscript.app/bin:/usr/ 
texbin:/usr/local/bin:/Users/sugimoto/bin:/usr/texbin

これは,PATHに含まれるフォルダーにあるプログラムファイルは, /opt/ImageMagick/bin/display なんて長々と入力しなくても, display と打てば実行できる,という変数です.

変数には実は2種類あります.普通の変数と,その上位である「環境変数」です.普通の変数を export すると環境変数になります: 
    export X 
環境変数の特徴は,起動したプログラムから参照できる点です.例えば, C++なら 
    std::string PATH=getenv("PATH"); 
で環境変数 PATH の値を取得できます.export していないタダの変数は, 取得できません.

計算

整数は次のように計算できます.

(( i=0 )) 
(( i++ )) 
(( i=i-32 ))

文字列は結合したりできます

A="$Y"AHO 
echo $A    ← Hello WorldAHO と表示されます 

繰り返しの制御

繰り返し同じ作業を繰り返すには, for文, while文, until文を用います.

for ((開始設定;終了条件;継続作業));do 
   コマンド 
   .... 
   コマンド 
done

例えば

sugimoto $ for((i=0;i<3;i++)); do echo $i; done 


2

集合に対するforもあります.

for ELEMENT in SET; do コマンド;...;コマンド; done 
例: 
for file in *; do echo "FILE=$file";done 
FILE=mix3.log 
FILE=mix3d 
FILE=mix3d.cfg 
FILE=mix3d.pid

こんな感じです. ちなみに * というのは,「そこにあるファイルやフォルダーの集合」です.もちろん *.log とやると,「そこにある .log ファイルの集合」です.
while文は次の通り

while 条件;do コマンド;.....;コマンド;done

配列

配列データは, 次のように定義します.

X=( x_1 x_2 ..... x_N)

あるいは成分を直接指定します.

X[2]="Centurion IV"

普通の変数Xがあるときに X[2] を定義すると, X[0]が元の値, X[1]が未定義, X[2]が"Centurion IV"になります.値を利用するときには ${変数名[引数]} です.

配列は,繰り返しの集合として用いることができます.ただし方法で微妙な違いが出ます:

未定義のX[1]でループは起動しません.空白を含むX[2]のせいで,想定通りに動いているのは for x in "${変数[@]}" だけですね.${変数[@]} は空白で分けられてしまい, "${変数[*]}" では, 配列ではなく「全文字列」で1回起動するだけです.X[1]が未定義なため,要素数は2になっています:

${#変数名[@]}    定義された要素の数

なんか,添字が整数の意味がないような気がします.それはその通りで,実はbashの配列は「連想配列」であり,定義域の値から値域の値への写像を定義するだけです.上の例は,単に,定義域の値に,まあ好みで整数を用いただけです.引数を(利点が不明ですが)整数に限定した「普通の配列」は, すでに伝説になりました.というわけで

X["sugimoto"]="BOKE"

などと定義できます.

条件判断

基本の条件分岐は if 文です:

if 条件; then 
    コマンド; 
    ..... 
    コマンド; 
else 
   コマンド; 
   .... 
   コマンド; 
fi

条件の一つは((数式))です:

ですが,まあ,bashの主要目的であるデータファイルやフォルダーの処理という点では,[ ファイル関係 ]が便利です.

  • [ -f ファイル ]    ファイルが存在したらtrue. [ ! -f ファイル ] の場合, ファイルが存在しなければtrue
  • [-d フォルダー ]  フォルダーが存在したらtrue
  • [-x コマンド ]     コマンドが実行可能ならtrue
  • [ 条件 -a 条件 ]   2条件のAND
  • [ 条件 -o 条件 ]   2条件のOR
  • [ ! 条件 ]            条件のNOT

もう一つの条件分岐は case 文です. 

case 値 in 
パターン) コマンド 
              ..... 
              コマンド 
              ;; 
パターン) コマンド 
              ...... 
              コマンド 
              ;; 
*)           コマンド 
              .... 
              コマンド 
              ;; 
esac

値が変数$Xでも大丈夫です.ただし空白がありがちなら"$X"としておきましょう.最後のesacは, if が fi で終わったように, caseのおしまいがesacってわけです.このパターンが強力無比で,

1 | 2 )      1か2 
1*)          1で始まる文字列 
*4* | *5 )        途中に4があるやつか, 最後が5で終わるやつ 
[abc]* )    aかbかcで始まるやつ

などなど,いろんなパターンが使えます.

変数については,別の奇妙な条件文があります.

${X:-"ほげほげ"}     変数Xが未定義なら, ほげほげ,を出力します. 
${X:="ほげほげ"}    変数Xが未定義なら, Xにほげほげを代入します

関数

関数は, 次のように定義します:

function BOKE(){ 
    printf "%s\n" "Fuck You $1" 
}

これで関数 BOKE() が定義できます.BOKEには引数を与えることができます.引数は, 前から順に$1, $2,.... となっています.よって

BOKE T-54 
Fuck you T-54

と出力されます.引数を省略すれば$1="" になっています.もちろん,空白を含む引数は "このような 感じに" 囲わないと1セットとして認識できません.

リダイレクト

コマンドの入出力は,「キーボードで文字を打つ」「ディスプレイに出てきよる」と思えますが,これは「キーボードというファイルから入力」「ディスプレイというファイルに出力」しているだけです.ファイルですので,別のファイルを割り当てることができます.これをリダイレクトといいます.

  1. キーボード入力をmy_fileファイルから行う:   command < my_file
  2. さらにディスプレイ出力をhis_fileに行う:   command < my_file > his_file
  3. 出力をhis_fileに追記したい:       command >> his_file
  4. いやいや,入力ファイルをここで作成したい(ヒア文書というらしー): 
    command << @ 
    this.is.file contents 
    1.22 
    55 
    @
  5. なお,ヒアドキュメントの@は,他の1文字でも良いです.<< + とか, << & とか.その文字はドキュメント内のデータの戦闘文字にできなくなるから注意(あんたがデータとして書いたつもりでも,コンプータが,そこでドキュメントの終わりだと勘違いするでしょ?).

なにぶんファイルですので,あるコマンドの出力を,別のコマンドの入力にすることもできます.これをパイプと言います:  

   command1 | command2

この縦棒が「パイプ」で,command1 の出力がcommand2の入力になります.凝った例では

         command1 < input_for_command1 | command_2 > output_of_command2

こうなってくると,コマンドの群れを一つで扱いたくなりますが,これは () でくくれば良いのです

      ( command1 | command2 ) < input | ( command3 | command4 | (command5 | command6))

スクリプト

ここまでは, キーボードでパチパチ入力していますが,もちろん,これをファイルに書いておくことも可能です.ファイルに書いた命令は,

. ファイル名 (見えにくいけど・・・ ドット+スペース+ファイル名)

で実行できます.ドットを入力せず,ファイル名だけで実行するように設定するには,ファイルに「実行可能ですよフラグ」をつけなければなりません.これは

chmod +x ファイル名

これで, ./ファイル名 で実行できるようになりました.ファイル名の前の ./ どっと+すらっしゅ,は,ファイルをPATHのどこかにコピーすれば,入力不要です.

bashは簡単な言語ですが, ルールが微妙ですので, 最初は戸惑うでしょうね.

並列処理

お前のノートパソコンには,複数のCPUコアが搭載されているでしょう.せっかくですので,並列計算しましょう.

例えばデータファイルが 00000.dat - 80000.dat まであり,8万個のデータをを全て描画して加工してGIFにしなければならないとします.一発1秒じゃとしても,普通にやったら8万秒じゃけん22時間かかるとよ.最新のMacbookProなら8コア16スレッドとかあるっちゃけん, 16個づつ描画したら, 1時間ちょいで完成すったい!

おれのMacbookAirは2コアだと?じゃLinuxサーバで48スレッドとか使えよ.OSはざっぱ同じ→同じプログラムでOK

とりあえずワーカースレッドで実行する関数を定義しましょう:

TMP=`mktemp -dt myXXX`  ← いや単純に一時フォルダー作っただけ
worker(){
     draw $TMP/$1.pdf  my_data/$1.dat    ←  my_dataフォルダーのデータをPDFに描画する
     beauty=`printf "%.6d" $1`    ← 1.gif と11.gif ではダサいので 00001.gif みたいにする
     convert -geometry 350x350! \
          $TMP/$1.pdf  my_image/$beauty.gif   ← $TMP/のPDFをmy_image のGIF画像にする
}
madadayooon(){
   test $STEP -lt 80000
}

ほんじゃ,いきましょう

STEP=0
while madadayooon   ← madadayooon がtrueになるまで繰り返す
do
    worker $STEP&(( STEP = STEP + 1 ));if madadayooon;then :;else break;fi ← worker & つまりバックグラウンドでworker起動.
   さらに STEPを1増やす. で, 完了してたらループを抜ける
    worker $STEP&(( STEP = STEP + 1 ));if madadayooon;then :;else break;fi
    .....     利用するスレッドだけ繰り返す
    worker $STEP&(( STEP = STEP + 1 ));if madadayooon;then :;else break;fi
    wait  ←ワーカースレッドの完了を待つ
    echo -e "[$STEP]\c"      ←現在のステップを表示
done
gm convert -delay 5 my_image/*gif animation.gif   ←面倒なので動画にする

これでOK!