1 から 12 の数字を順番に表示する
1 2 3 ... 12
$ seq 1 12
3 から 6 の数字を順番に表示する
3 4 5 6
n + 2
n=0 while [ $n -lt 4 ]; do n=$(( n + 1 )) a=$(( n + 2 )) echo $a done
シェルスクリプトにおいて = の意味は,右辺の値を左辺に代入する,である(「n = n + 1 は間違ってる」とか言わないように).
y=$(( x + 1 ))
n=1 y=3 echo $y while [ $n -lt 4 ]; do n=$(( n + 1 )) x=$y y=$(( x + 1 )) echo $y done
n=1 y=3 echo $y while [ $n -lt 4 ]; do n=$(( n + 1 )) y=$(( y + 1 )) echo $y done
5 から 2 の数字を順番に表示する
5 4 3 2
一般項と漸化式,どっちを使ってもよい. どっちを使ってもできるなら,一般項を使う場合と漸化式を使う場合の2通りで書いてみるのが,上達の近道でしょう.
数列
11 14 17 ... 98
数列の和を計算する
n=0 while [ $n -lt 10 ]; do n=$(( n + 1 )) a=$(( 2 * n )) done
n=0 s=0 while [ $n -lt 10 ]; do n=$(( n + 1 )) a=$(( 2 * n )) s=$(( s + a )) done echo $s
数列の和を計算する
九九の表を作るスクリプトを書く.
以下のような出力ができたらよい.
1 2 3 4 5 6 7 8 9 2 4 6 8 10 12 14 16 18 3 6 9 12 15 18 21 24 27 4 8 12 16 20 24 28 32 36 5 10 15 20 25 30 35 40 45 6 12 18 24 30 36 42 48 54 7 14 21 28 35 42 49 56 63 8 16 24 32 40 48 56 64 72 9 18 27 36 45 54 63 72 81
とりあえず,列が揃っていなくてもよい(ちょっとした小技を知っていると列を揃えるのは簡単であるが,小技を知らずに列を揃えるのは面倒だと思う).
echo Hello echo World
改行しないようにするにはオプション -n をつける
echo -n Hello echo -n World
echo ' '
1 1 1 2 1 3 2 1 2 2 2 3
と出力するスクリプトを書き換えることを考える
#/bin/bash i=0 while [ $i -lt 2 ]; do i=$(( i + 1 )) j=0 while [ $j -lt 3 ]; do j=$(( j + 1 )) k=$(( i * j )) echo $k done done
#/bin/bash i=0 while [ $i -lt 2 ]; do i=$(( i + 1 ) j=0 while [ $j -lt 3 ]; do j=$(( j + 1 )) k=$(( i * j )) echo -n $k done echo done
#/bin/bash i=0 while [ $i -lt 2 ]; do i=$(( i + 1 ) j=0 while [ $j -lt 3 ]; do j=$(( j + 1 )) k=$(( i * j )) echo -n ' ' echo -n $k done echo done
Fibonacci sequence (フィボナッチ数)
\[ F_{0} = 0, \ F_{1} = 1 \] \[ F_{n+2} = F_{n} + F_{n+1} \]
c=$(( a + b ))
n=0 c=0 echo $c n=$(( n + 1 )) b=$c c=1 echo $c n=$(( n + 1 )) a=$b b=$c c=$(( a + b )) echo $c n=$(( n + 1 )) a=$b b=$c c=$(( a + b )) echo $c n=$(( n + 1 )) a=$b b=$c c=$(( a + b )) echo $c
n=0 c=0 echo $c n=$(( n + 1 )) b=$c c=1 echo $c while [ $n -lt 10 ]; do n=$(( n + 1 )) a=$b b=$c c=$(( a + b )) echo $c done
トリボナッチ数 \[ T_{0} = T_{1} = 0, \ T_{2} = 1 \] \[ T_{n+3} = T_{n} + T_{n+1} + T_{n+2} \]
0, 0, 1, 1, 2, 4, 7, 13, 24, 44, 81, 149, 274, ...
テトラナッチ数 \[ T_{0} = T_{1} = T_{2} = 0, \ T_{3} = 1 \] \[ T_{n+4} = T_{n} + T_{n+1} + T_{n+2} + T_{n+3} \]
0, 0, 0, 1, 1, 2, 4, 8, 15, 29, 56, 108, 208, 401, ...
Fibonacci sequence (フィボナッチ数)
$(( x + y ))
から
$( echo "$x + $y" | bc )
に変更してみる.
計算機内部で数値は2進数を使って表される. 8桁の2進数は0または1を8つ並べたもので(例えば,00000000とか10101010とか),2の8乗すなわち256通りの数値を表すことができる. 8桁の2進数を10進数にすると,00000000は0,00000001は1,・・・11111111は255になる. 各8桁の2進数はそのまま10進数に対応するものとするなら,8桁の2進数で0から255までの整数を表すことになる.
ここで,正の数だけでなく負の数も含めて8桁の2進数で表す場合を考える. 計算機内部にあるのは0と1だけで,マイナスの符号はない. 0と1だけで負の数を表すにおいては,2の補数表現というものが使われる. 2の補数表現では,最上位の桁が符号を表し,最上位が0である00000000から01111111は正の数,最上位が1である10000000から11111111は負の数を表す. 正の数については,そのまま2進数を10進数に読み替えた数を表すものとし,00000000から01111111は0から127までの整数となる. 一方,負の数は,2の補数表現を使うとき,10000000は-128,10000001は-127,・・・11111111は-1を表す. ちょっと不思議な感じに見えるかもしれないが,これはこれで使い勝手がよかったりする. 例えば -128 + 1 は,2の補数表現を用いた場合 10000000 + 00000001 = 10000001 となり,たしかに-127となる. 同様に -1 + 1 は,11111111 + 00000001 = 100000000 となるが,数値を8桁で表すことにしている場合,桁上がりで生じた9桁目が消えて,00000000 となり,めでたく0が得られる.
ここまできたら,フィボナッチ数の計算結果に負の数が出てきた理由もわかったのではないかと思う. フィボナッチ数は正の数と正の数を足し算しているだけなので,負の数は絶対に出てこない. にも関わらず負の数が出てきたのは,計算機の内部で2の補数表現が使われていたからである. 数が小さくて最上位の桁に影響が出ないうちは問題ないのだが,数が大きくなって最上位の桁が1になると,負の数になってしまうのである. 上にあげた8桁の場合で言うと,127 + 1 が 01111111 + 00000001 = 10000000 で-128になってしまう,ということである. もともと,8桁では-128から127までしか扱えないところ,扱える範囲の外にある数が出てきてしまったのがいけない. 計算してはいけない範囲の計算をさせたのが悪いのである.
扱える範囲がどこまでであるのかは,処理系や使っているコマンドに依存するため,計算してみないとわからない場合もあったりする. 計算機は何も考えずにただ計算して結果を返すだけなので,計算したら必ず結果を見てその妥当性を評価しなければならない.
/mnt/raid/skymonitor/pub 以下に保存されている,スカイモニターで撮影された画像の枚数を数える
20161014 255 20161015 1092 20161016 1093 20161017 1362 20161107 351 20161108 426 ...
#!/bin/bash days=$( ls /mnt/raid/skymonitor/pub/2016/10 ) for yyyymmdd in $days ; do echo $yyyymmdd done
実行したら
20161014 20161015 20161016 20161017
#!/bin/bash days=$( ls /mnt/raid/skymonitor/pub/2016/10 ) for yyyymmdd in $days ; do num=$( ls /mnt/raid/skymonitor/pub/2016/10/${yyyymmdd}/*.jpg | wc -l ) echo $yyyymmdd $num done
実行したら
20161014 255 20161015 1092 20161016 1093 20161017 1362
yyyy=2016 mm=10 days=$( ls /mnt/raid/skymonitor/pub/${yyyy}/${mm} ) for yyyymmdd in $days ; do num=$( ls /mnt/raid/skymonitor/pub/${yyyy}/${mm}/${yyyymmdd}/*.jpg | wc -l ) echo $yyyymmdd $num done
#!/bin/bash yyyy=2016 months=$( ls /mnt/raid/skymonitor/pub/${yyyy} ) for mm in $months ; do days=$( ls /mnt/raid/skymonitor/pub/${yyyy}/${mm} ) for yyyymmdd in $days ; do num=$( ls /mnt/raid/skymonitor/pub/${yyyy}/${mm}/${yyyymmdd}/*.jpg | wc -l ) echo $yyyymmdd $num done done
#!/bin/bash years=$( seq 2016 2021 ) for yyyy in $years ; do months=$( ls /mnt/raid/skymonitor/pub/${yyyy} ) for mm in $months ; do days=$( ls /mnt/raid/skymonitor/pub/${yyyy}/${mm} ) for yyyymmdd in $days ; do num=$( ls /mnt/raid/skymonitor/pub/${yyyy}/${mm}/${yyyymmdd}/*.jpg | wc -l ) echo $yyyymmdd $num done done done
ls: cannot access '/mnt/raid/skymonitor/pub/2019/07/20190705/*.jpg': No such file or directory
のようなメッセージが吐き出されたかもしれない.メッセージをみたらだいたいわかると思うが,指定したファイルは存在しないと言われている.ディレクトリはあるけどファイルがない,という場合があるということである.
num=$( ls /mnt/raid/skymonitor/pub/${yyyy}/${mm}/${yyyymmdd}/*.jpg 2>/dev/null | wc -l )
2016 21682 2017 499525 2018 428194 2019 377719 2020 520706 2021 523357 2022 523560 2023 363684
num=$( ls /mnt/raid/skymonitor/pub/${yyyy}/*/*/*.jpg | wc -l )
のようなことをしたかもしれない.理屈は正しくて,これでうまくいくべきなのであるが,実際には
Argument list too long
のようなメッセージが表示されたりして,うまくいかない.メッセージを見たら察しはつくと思うが,これはファイルの数が多すぎてダメと言われている.それくらいなんとかしてくれと思わなくもないが,ダメなものはダメなので,違う方法(小分けにして溢れないようにして,足し算する)を考えてください.
次のスクリプトは2016年10月の各日の枚数を数える.
#!/bin/bash yyyy=2016 mm=10 days=$( ls /mnt/raid/skymonitor/pub/${yyyy}/${mm} ) for yyyymmdd in $days ; do num=$( ls /mnt/raid/skymonitor/pub/${yyyy}/${mm}/${yyyymmdd}/*.jpg | wc -l ) echo $yyyymmdd $num done
各日の枚数を足して,2016年10月の総枚数を数えるように書き換えてみる.
#!/bin/bash yyyy=2016 mm=10 numnum=0 days=$( ls /mnt/raid/skymonitor/pub/${yyyy}/${mm} ) for yyyymmdd in $days ; do num=$( ls /mnt/raid/skymonitor/pub/${yyyy}/${mm}/${yyyymmdd}/*.jpg | wc -l ) numunm=$(( numnum + num )) done echo $yyyy $mm $numnum
月の総枚数を格納する変数として numnum を使う. 最初に numnum を 0 にセットして,各日の枚数を足していく. 数え終わったら最後に出力する. 出力は for ループの外に出す(そうしないと日毎に出力されてしまう).
#!/bin/bash yyyy=2016 months=$( ls /mnt/raid/skymonitor/pub/${yyyy} ) for mm in $months ; do days=$( ls /mnt/raid/skymonitor/pub/${yyyy}/${mm} ) for yyyymmdd in $days ; do num=$( ls /mnt/raid/skymonitor/pub/${yyyy}/${mm}/${yyyymmdd}/*.jpg | wc -l ) echo $yyyymmdd $num done done
やり方は同じで,各日の枚数を足して,2016年の総枚数を数えるように書き換える.
#!/bin/bash yyyy=2016 numnum=0 months=$( ls /mnt/raid/skymonitor/pub/${yyyy} ) for mm in $months ; do days=$( ls /mnt/raid/skymonitor/pub/${yyyy}/${mm} ) for yyyymmdd in $days ; do num=$( ls /mnt/raid/skymonitor/pub/${yyyy}/${mm}/${yyyymmdd}/*.jpg | wc -l ) numnum=$(( numnum + num )) done done echo $yyyy $numnum
01 298138 02 280678 03 277888 04 259877 05 237614 06 258635 07 262659 08 309722 09 275537 10 266661 11 251640 12 279378
00 136055 01 135862 02 135881 03 135849 04 135833 05 135816 06 135705 07 135563 08 135484 09 135674 10 135735 11 135615 12 135551 13 135263 14 135171 15 134940 16 135060 17 135556 18 135880 19 136426 20 136577 21 136469 22 136336 23 136126