ソフトウェアII 第7回(2023/01/26)
本日のメニュー
- シェルスクリプト入門
本日は気楽に聞いてください。おそらく最大でも1コマ分程度の時間で終わると思います。(その場合は適宜任意の質問タイムにしますので、試験勉強がんばってください。)
Slidoはこちら
シェルスクリプト入門
今日はこれまで学んできたC言語から少し離れて、これまで「コンパイル」と「プログラムの実行」を担ってきたターミナルを少ししっかりと使うことを考えてみましょう。
コマンドラインシェル
ターミナルに入力されたユーザから文字列を解釈し、それを適切にOSに伝える役割を果たすのが「コマンドラインシェル」です。直感的にはユーザが入力したコマンドを順次実行していく様子を記述するプログラミング言語とみなすことができます。ターミナルから何らかの作業をしたいという観点に立った時、コマンドラインシェルを使うことは以下の点で有用です。
- ファイルの操作やプログラムの実行を簡単に書くことができる
- すでに存在する複数のプログラムを組み合わせて利用する
例えばファイルをあるディレクトリから別のディレクトリに移すという操作をしたい場合に、C言語でこれをわざわざ記述するのは現実的ではありません。例えばディレクトリAにあるhoge.txt というファイルをディレクトリBに移動したい場合、
#include <stdio.h>
int main(void){
char old[] = "A/hoge.txt";
char new[] = "B/hoge.txt";
rename(old, new);
return 0;
}
というコードをコンパイルして実行すれば確かに要件を満たしますが、コマンドmv
を知っている上で、
mv A/hoge.txt B
とすればより簡単です。Unix系を扱う上では標準とされているコマンドは先人たちの需要に基づいて取り込まれてきたものですので、これらを知ってさっと使えることは非常に価値があります。これらに加えて、コマンドラインシェルにもC言語などと同様に制御構造が存在し、例えば「似たようなことを変数を変えてN回繰り返す」といった処理もコマンドラインシェルで記述することができます。
今日はこのようなUnix環境におけるコマンドラインシェルの中でも、特に現在の多くの環境で標準となっているBash を使ってみましょう。今時はzsh だろとか fish が良いよねとか先祖からの教えでtcshを使うことになっているとかを語れる方は今日は気楽に聞いてください。 Unix系環境を想定しているので、Windowsユーザの場合はWSLやCygwin、msys などを想定しています。Powershell は残念ながら互換性がないので、別扱いです。
コマンド
これまでもソフトウェア1/2を通して基本的なコマンドにはいくつか触れてきたと思います。少し復習しましょう。
ls
: ファイル一覧表示pwd
: 現在のディレクトリを表示cd
: ディレクトリ の移動cat
: 引数で与えられたファイルを連結し標準出力へ(1ファイルだと中身の表示)mkdir
: ディレクトリ 作成mv
: ファイルの移動/リネームcp
: ファイルのコピーrm
: ファイルの削除
上記はおもにファイル自体を取り扱うコマンドでした。
他に頻出のコマンドとしてecho
があります。
echo
: 引数の文字列をそのまま標準出力へ
もうちょっとコマンド
基本的なコマンドをいくつかざっくり紹介します。
head
: ファイルの先頭から指定行数表示tail
: ファイルの末尾から指定行数表示sed
: ストリームエディタ。よく使うのは文字列置換。nl
: 行頭に行番号を付与find
: 指定条件を満たすファイルを探してくるxargs
: 受け取った入力をコマンドライン引数にするsort
: 指定列の条件に基づいてファイルの行を並べ替えuniq
: ファイル中ユニークな行を取り出してくる
上記は主にファイルを操作したりするコマンドですが、もちろん他にも基本として知っておくべきコマンドは数多く存在します。触っていくうちに慣れていきましょう。
素早くターミナルを操作してみる
ターミナルを操作する際に、矢印だけを使っていないでしょうか。bashで使えるショートカットを使ってみましょう。以下の表ではCtrl
キーとの同時押しをC-x
、Meta
キー(WindowsだとAlt / Macだと esc が標準)との同時押しをM-x
と記載します。
ショートカット | 機能 |
---|---|
C-a | 行頭へ移動 |
C-e | 行末へ移動 |
C-f | 1文字進む |
C-b | 1文字戻る |
M-f | 1単語進む |
M-b | 1単語戻る |
C-l | 現在行を残して画面クリア |
C-k | 行末まで切り取り削除 |
C-u | 行頭まで切り取り削除 |
C-y | 上記で切り取ったものを貼り付ける |
C-r | コマンド履歴のインクリメンタルサーチ |
これらのショートカットとファイル名などのTAB補完を効果的に用いることでタイピング速度はそれほどでなくても、かなり効率的に作業が進められるようになります。
連続する数字を出力する
例えば、整数引数を受け取って、それが3の倍数のときに「 3で割り切れるので、3で割り切れます」と出力する以下のC言語のプログラムがあったとします。
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char **argv){
if (argc != 2){
fprintf(stderr, "usage: %s integer\n", argv[0]);
exit(1);
}
int num = atoi(argv[1]);
if ( num % 3 == 0)
printf("(Shinjiro): %dは 3で割り切れるので、3で割り切れます。\n", num);
return 0;
}
上記のプログラムをコンパイルして、プログラムを実行可能にします。
gcc -o shinjiro shinjiro.c
さてこのプログラムが正しく動きそうか、1から1000 ぐらいまで試したいとします。これまではこのような繰り返しはCのプログラムの方に書いていたかと思います。もしくはターミナルに変数を変えながら1000回Enterを叩くという人もいたかもしれません。しかし、よくあるシチュエーションとして実験用のプログラムを汎用的に作ったのち、それをテストする場合は、変化させるパラメータなどはシェルで制御する方が簡単なケースが多いです。
bashで連続する数字を出力する場合、いくつかの方法がありますが、ここではforループを使う方法を示します。
for i in {1..1000}; do ./shinjiro $i ; done
上記の構文はbash で繰り返しを書く際によく書く構文です。i は変数であり、in 以下の要素を順次代入します。変数名はiに限りません。実際に実行されるのは do から done に囲まれた部分で、for のところでセットされたi を $i という形で参照しています。なお、{1..100}
という書き方はbash において、空白区切りで数字の列を出力するものです。
echo {1..10}
などとするとどのような出力が得られるでしょうか?この{..}
の記述は便利で、例えばaからzまでの系列を得たい場合には
echo {a..z}
とすると空白区切りで出力できます。一つ注意点として、開始・終了の文字や数字と..
の間に空白を入れてはいけません。
スクリプトを書いてみる
シェルスクリプトというものを書いてみましょう。シェルスクリプトは文字通りコマンドラインシェルをベースにしたスクリプト言語です。スクリプトといっても一番最初は、一回の処理で使うコマンドを羅列するだけで立派なシェルスクリプトになります。以下の内容をhelloworld.sh
という名前で保存してみましょう。
#!/bin/bash
# シェルスクリプトでは #以降がコメントになる
echo "Hello, World"
echo "Hello, Exam"
echo "Good bye Software2"
一行目にある#!/bin/bash
はシェバン とよばれ、OSにこのスクリプトはこの言語で実行してねということを指示するおまじないです。Pythonを書いたことのある人は#!/usr/bin/python
といった書き方をしたことがあるかと思います。
最も基本的な実行法はbash
で呼び出すことです。
# bash で 今書いたスクリプトを実行する
bash helloworld.sh
ただ、実際にはスクリプト単独で実行するケースが多いので、一工夫します。まずは現在のファイルのパーミッションを確認してみましょう。ファイルのパーミッションとは、今のユーザがそのファイルに対して何ができるのかの情報です。helloworld.sh があるディレクトリで以下を実行します。
# ls に -l オプションをつけて詳細情報を表示
ls -l
以下のような結果が得られるかと思います。
-rw-r--r-- 1 dsk_saito staff 79 1 19 09:53 helloworld.sh
先頭にあるr``w
等がファイルに対する権限を表して、このファイルは
- 所有者(最初のブロック)は読み取り
r
と 書き込みw
ができる - 同じグループのユーザ(2番目のブロック)は読み取り
r
のみできる - グループも異なる他のユーザ(3番目のブロック)は読み取り
r
のみできる
この状態ではこのシェルスクリプト単体で実行することができません。
以下のコマンドでスクリプトに実行権限を付与します。
chmod u+x helloworld.sh
ls -l
# -rwxr--r-- 1 dsk_saito staff 79 1 19 09:53 helloworld.sh
このコマンドは所有者(ユーザ: u)に実行権限(x)を付与する(+)という意味になります。
この状態で以下のように実行ができます。
./helloworld.sh
- 上記の手順でhelloworld.sh の写経と実行をしてみましょう。
シェルスクリプトにおける変数
シェルスクリプトにおける変数は基本的に全て文字列を扱います。幸か不幸か型はありません。文字列ですがダブルコーテーションで囲む必要はありません。シェルスクリプトにおける変数の最大の注意点は以下です。
- 変数を代入する際は空白を開けずに=で代入する。$ はつけない
- 変数を使うとき(参照するとき)は$をつける
例をみてみましょう。
#!/bin/bash
teacher=situ
comment="Good afternoon"
echo $comment, ${teacher}
変数参照は$をつける他にも、${} で囲むことも可能です。
計算する
シェルスクリプトにおける変数は基本的に文字列なので、数字も文字列として解釈されます。たとえば以下の例を実行してみましょう
#!/bin/bash
x=10
echo $x
echo $x+2
ただし、数字を扱いたい場合もあるので、その場合は$(()) という変数展開を使います。
#!/bin/bash
x=10
echo $x
echo $((x+2))
$(( )) は内部で演算を行ってくれる便利な変数展開です。これを使う場合は中のx
は$が不要です。なお$(()) 内部は空白に対して寛容です。注意点としてbash の場合は整数演算のみです。zshと呼ばれるbashより高機能なシェル(bashの構文はほぼ使える)では、実数の演算も受け付けます。
変数展開をもう少し詳しく
bashの変数は様々な形で展開することができます。以下に例を示します。一つずつみていきましょう。こちらの変数展開では{} で変数を囲みつつ必要な演算をかきます。
#!/bin/bash
var="ThisSampleisBoring.jpg"
echo ${var}
echo ${#var} # 文字数を出力
echo ${var:0:4} # 0番目から4文字取り出し
echo ${var:12:6} # 12番目から6文字取り出し
echo ${var%.jpg} # 末尾から.jpg を取り除く
echo ${var#This} # 先頭からThis を取り除く
echo ${var%s*} # 末尾からsで始まる任意長の文字列を最短一致で取り除く
echo ${var%%s*} # 末尾からsで始まる任意長の文字列を最長一致で取り除く
echo ${var#*s} # 先頭からsで終わる任意長の文字列を最短一致で取り除く
echo ${var##*s} # 先頭からsで終わる任意長の文字列を最長一致で取り除く
echo ${var/Sample/Lecture} # Sample を Lecture で置換
echo ${var/is/as} # is を asで置換(1回)
echo ${var//is/as} # is を as で置換(全て)
上の変数展開を用いることで、文字列の抽出や置換が容易に実現できます。例えばファイルの拡張子部分を除いて別のファイルを作るなどの場合に用いることができます。
ここまでのシェルスクリプトを写経して実行してみましょう。
コマンドの結果を代入する
コマンドの結果を代入する方法は二つあります。一つは$() で囲む方法です。
#!/bin/bash
# printf コマンドで0埋めで4桁表示した結果をvar に代入
var=$(printf %04d 5)
echo $var
もう一つはバックコーテーション` で囲む方法です。
#!/bin/bash
# printf コマンドで0埋めで4桁表示した結果をvar に代入
var=`printf %04d 5`
echo $var
for ループをもう一度
forループを使うケースはいくつかあります。以下に例を示します。
#!/bin/bash
# 決まった回数(1から10まで)実行
for n in {1..10}; do
echo "Hello, world ($i)"
done
ちょっと便利なコマンドにseq
があります。
seq 1 10 # 1から10まで表示
seq 0 2 10 # 0から10まで2個飛ばしで表示
これを利用してforループを書くと以下のようになります
#!/bin/bash
for i in $(seq 0 2 10); do
echo "Hello, world ($i)"
done
ディレクトリ以下にあるファイルそれぞれに操作をしたい場合をやってみましょう。下準備としてディレクトリ の下に100個ファイルを作ります。各ファイルにはランダムな数字が入るとします。シェルでは環境変数$RANDOM が、毎回異なる乱数を出してくれるのでこれを利用します。
#!/bin/bash
# 0 から99までファイルを作る
# ファイル名は00.txt , 01.txt, ....
for i in $(seq 0 99);do
var=$(printf %02d $i)
echo $RANDOM > $var.txt
done
ディレクトリを見てみると100個ファイルができていることがわかります。
続いてこれらのファイルに、データ0 を追記したいとします。
#!/bin/bash
# * でそのディレクトリ以下のファイル全てを指定する。拡張子で制約することもできる
# >> で追記する
for i in *.txt ; do
echo 0 >> $i
done
最後に拡張子.txt を除いてみましょう。
#!/bin/bash
for i in *.txt ; do
mv -v $i ${i%.txt}
done
if 文と演算子
例えば、決められたファイル名を並べて、該当するファイルが存在するときだけls
するようなシチュエーションを考えましょう。シェルスクリプトにもif文による制御が存在します。
#!/bin/bash
for i in {a.txt,b.txt,c.txt};do
if [ -e $i ];then
ls -l $i
fi
done
シェルスクリプトのif 文は非常に空白にうるさいのですが、条件文を空白を開けて [ と ] で囲みます。-e はその名前のファイルが存在するかどうかをチェックする演算子です。他にも以下の表のような演算子があります。
ファイル演算子 | 役割 |
---|---|
-d file | ディレクトリかどうか |
-f file | ファイルかどうか |
-e file | ファイルが存在するかどうか |
-L file | シンボリックリンクかどうか |
-r file | 読み取り可能か |
-w file | 書き込み可能か |
-x file | 実行可能か |
引数を取り出す
シェルスクリプトでは引数は$1、$2 といった形で取り出します。$0 は自身のプログラム名です。$# で引数の個数を取り出せます。
#!/bin/bash
if [ $# != 2 ]; then
echo "usage: $0 arg1 arg2"
exit 1;
fi
echo "0: $0"
echo "1: $1"
echo "2: $2"
ここまでのスクリプトを写経してそれぞれ実行してみましょう
まとめ
シェルスクリプトの機能をかなりざっくり説明しました。シェルスクリプトの基本機能の範囲でC言語 / Pythonで書くには複雑になることを場合によっては極少量の記述で実現可能になります(いちいちPython書くな的なシチュエーションですね)。またUNIXには先人たちの所産であるコマンド群が存在するので、それらと組み合わせることで作業の効率化を実現できます。
一方、実行速度はあくまでコマンドを一つずつ実行するのと同様ですので、お世辞にもものすごく速いとはいえません。ファイル操作等であっても本当に速さが要求される場合は、C言語で専用の処理を書く必要性もありますが、普段使いでシェルスクリプトを使いこなすことで、大幅な効率化が実現できます。
今回紹介しなかった機能として
- ファイルリダイレクト・パイプの詳細
- プロセスのファアグラウンド・バックグラウンド処理
- 配列
- プロセス置換
などがあります。興味がある人はキーワードをもとにぜひ春休みの期間に少しターミナルに慣れ親しみつつ進めるとよいと思います。
アンケート
UTAS上で工学部が出している授業評価アンケートがありますので、そちらに回答をお願いします。成績には一切影響しません。
またソフトウェア2について、別途、より詳細なアンケートをとろうと思います。こちらも成績には一切影響しません。試験で忙しいと思うので、落ち着いた頃にかSlackかITC-LMSにて連絡します。こちらもぜひ忌憚なくコメントをお願いします。
フリーディスカッション
適当な時間、講義全体に対して自由に質問コメントを受け付けます。