[Exp2019]シェルスクリプト

スケジュール表・各回資料 (08/06)

さて, vi の使い方を大雑把に理解したところで今日のメインである シェルスクリプト について学習しましょう.

◎ シェルスクリプト入門

これまで見てきたように, シェルはユーザとコンピュータの橋渡しをして, ユーザがコマンドを端末から打ち込む毎に, それを解釈し実行します. 一方, 「プログラムが可能である」という, もう一つの面もあります. この, プログラムとして手続きを書き込んだファイルを「スクリプト・ファイル」と言います. スクリプト(script)とは劇の「台本」のことであり, 台本を事前に決めていてそれに沿って行わせるためにこの名前があります.

シェル以外にも多くのスクリプト言語, 例えば Perl, Ruby などがあります. もちろん全部を覚える必要は全くありませんが, 一つでも使いこなせると非常に便利です. 機械的な作業を膨大に繰り返す場合, あるいは普段頻繁に行う一連の作業は機械に作業の手順を教えて人間は楽をするのが賢い方法です. すぐにこういった膨大な作業をする必要にせまられることは無いかも知れませんが, 早いうちに慣れておくと, いざ必要になったとき楽なものです. ここでは Linux で標準シェルである Bash を使ったシェルプログラミングを学習することにします.

具体的にどのようなものか, 簡単な例を見てみましょう.

次のような一連の仕事を行うコマンドを作ることを考えます.

  • 日付・時刻を表示する.
  • 自分の名前を表示する.
  • 自分のログインシェルを/etc/passwd から調べて変数に代入する.
  • 変数の内容を表示する.
  • ユーザにかけ声をかけて励ます.

それぞれの働きをするコマンドは

  • date
  • echo $USER (注: $USER はユーザ名を示す環境変数( [1.6.2]参照) )
  • loginshell=`grep ^$USER: /etc/passwd | cut -d: -f 7` (注: 「cut コマンド」 各行から特定の部分を抽出する. 詳しい使用法を知りたい場合は man で調べよう.)
    • (注: コマンドを括る引用符がシングルクォーテーション「'...'」や ダブルクォーテーション「"..."」ではなく, バッククォーテーション「`...`」であることには意味があります. 詳しく知りたい場合は 引用符 を調べてみましょう).
  • echo $loginshell

です.

vi を使って次のようなファイルを作成しよう.

$ vi sample.sh
      ...

[ スクリプトファイル:sample.sh ]

date
echo "I am $USER."
loginshell=`grep ^$USER: /etc/passwd | cut -d: -f 7`
echo "My login shell is $loginshell"
echo "Hey $USER !!" 

編集・保存したら,

$ bash sample.sh

とすると, アカウント名が hoge の場合次のように出力される.

2018年  8月 7日 火曜日 10:57:04 JST
I am hoge.
My login shell is /bin/bash
Hey hoge !!

この場合, bash をわざわざ起動して, その引数としてファイル名を指定し, そのファイルを bash が解釈して実行しています.

いちいちこのようなことをするのは非効率でもあるので, 以下の手順で先程のファイル (sample.sh) を実行可能なファイルにします.

  • ファイル sample.sh の先頭行に "#!/bin/bash"という おまじないを書き込む.

 

#!/bin/bash
date
echo "I am $USER."
loginshell=`grep ^$USER: /etc/passwd | cut -d: -f 7`
echo "My login shell is $loginshell"
echo "Hey $USER !!"
  • ファイル sample.sh に実行パーミッションを加える.

    $ chmod 744 sample.sh

もしくは

$ chmod u+x sample.sh
  • ls コマンドでパーミッションを確認.

    $ ls -lF sample.sh

    -rwxr--r-- 1 hoge hoge 133 8月 7 10:57 sample.sh*

所有者のパーミッションのところに, 実行権限である「x」がついている ことを確認する.

こうすることでスクリプトのファイル名を入力するだけで実行することができます. ※但し, パスには注意して下さいね.

$ ./sample.sh

この場合, 自動的に(自分が使っている login シェル)が, スクリプトファイルを bash に解釈させ実行させています.

○ シェルスクリプト応用

引数処理

引数 (ひきすう) とは, 実行時にそのシェルスクリプトに教えて上げたい追加情報であるとも言えます.

たとえば, コマンド ls は, そのままではカレント・ディレクトリ内のファイルをリストアップしますが, 引数として特定のディレクトリを指定すると, 指定されたディレクトリ内のファイルをリストアップします. これはコマンド ls に, 引数として指定したディレクトリについて知りたいんだよ, と追加情報を与えているわけです.

コマンドやシェルスクリプトに引数を指定することを, "引数を渡す"とも表現します. シェルスクリプトに渡された引数を処理したい場合は, $1, $2, ..., $9 というシェル変数を利用します.

例えば, 引数に渡された単語をそのまま表示するシェルスクリプトを考えてみましょう. 次のようなシェルスクリプト(test.sh)を作成してみてください.

$ vi test.sh        <-- test.sh というシェルスクリプトを vi で編集する

#!/bin/bash
echo $1             <-- シェルスクリプト変数 $1 を表示する

$ chmod u+x test.sh <-- test.sh に実行権限を与える 

$ ./test.sh Hello!  <-- 引数に Hello! を渡しtest.sh を実行
Hello!              --> 引数に渡した Hello! を返す

ここでは, 引数の文字を画面に出力するコマンド echo に, シェル変数 $1 を引数として渡しています. このシェル変数 $1 には, シェルスクリプト test.sh に渡された引数 Hello! が入ります. 結果, 画面には Hello! と表示されるでしょう.

引数を参照するシェル変数は9つまで使うことが出来ます. それぞれ, $1, $2, ... $9 です. ※引数のデフォルト値などを設定するには 文字列演算子等を利用します.

配列

配列とは, 変化する値を入れる「複数の箱」のようなものです.

配列の生成

ある配列 array を生成する

array=("a" "b" "c")

また, declare というコマンドを使って生成することもできます.

declare -a array=("a" "b" "c")
declare の使い方

書式

declare [option] [name[=value]]

オプション

-a, 変数を配列として作成する
-i, 変数を整数型(integer)として作成する
-l, 変数の全ての小文字を大文字に変換する
-u, 変数の全ての大文字を小文字に変換する

※ 詳しくは

man declare 

を参照してください.

declare -a array                <- 配列 array を定義(要素数は任意)
declare -a array[3]             <- 要素数 3 の配列 array を定義
declare -a array=("a" "b" "c")  <- 要素数 3 で初期値 a, b, c という値をもつ配列 array を定義

declare -a date                 <- 変数 date を定義(中身はない)
declare -a year=365             <- 変数 year を定義(初期値 365 をもつ)(入れる値は整数にする)

配列の要素数の確認

現在の配列の要素数を表示します. ある配列 array の現在の要素数を表示するには以下のようにします.

echo ${#array[@]}
echo ${#array[*]}

上記の 2 つに差はありません.

配列に値を入れる

ある配列 array において, 各要素は array[0], array[1], array[2], ... のように表されます. ここで, 0 番目から始まることに注意してください.

配列 array に値を入れるには, 以下のようにします.

array[0]=0                     <- 0 番目の要素に 0 を入れる
array[1]="a"                   <- 1 番目の要素に "a" を入れる

配列に値を追加する

現在の配列の先頭と末尾に値を追加することができます.

例えば, 配列 array=("a" "b" "c") の先頭と末尾に値を追加してみましょう. 先頭に追加する場合は以下のようにします.

array=(0 "${array[@]}")  <- 配列の中は array=(0 "a" "b" "c") となる

末尾に追加する場合は以下のようにします.

array=("${array[@]}" "d")  <- 配列の中は array=("a" "b" "c" "d") となる 
array+=( "d" )

末尾に追加する方法は 2 つ示しましたが, どちらの方法でも問題はありません.

配列の値を参照する

配列の要素をプログラムで使用する場合を考えます. その場合は, ${配列} のように記述します.

例えば, 配列 array=("a" "b" "c") の 1 番目の要素を表示するには, 以下のようにします.

echo ${array[1]}

配列の参照は, 以下の処理制御と組み合わせて使用することもできます.

処理の流れを制御する - 順序構造

より複雑な処理をこなす場合でも, 大抵は次の3つの処理の組み合わせで表現できます.

  • 順序構造
  • 選択構造
  • 繰り返し構造

順序構造は, 順番にコマンドを実行するものですから, 特別の制御文は必要ありません.

選択構造には, if, case を使います. 繰り返し構造には, for, while, until を使います.

処理の流れを制御する - 選択構造

例として挙げているシェルスクリプトは, vi で実際に編集 (コピーアンドペースト) して実行してみてください.

if

書式

if テストコマンド 1
then
  コマンド 1
[elif テストコマンド 2
then
  コマンド 2]
          ...
[else
  コマンド 3]
fi

#!/bin/bash
echo a=?                 <-- 'a=?' と表示
read a                   <-- a を読み込む
echo b=?                 <-- 'b=?' と表示
read b                   <-- b を読み込む
if [ $a = $b ]           <-- もし, $a と $b が
then
  echo same         <-- 等しければ 'same' と表示
else
  echo different    <-- そうでなければ(異なっていれば) 'different' と表示
fi

解説

if 文は if と fi で対になって, いれ子を形成しています. まず, if に続く 「テストコマンド 1 」を実行し, それが「真」 (0) を返したら then に続く 「コマンド 1 」が, 「偽」 (0 以外の値) を返したら elif に続く「テストコマ ンド 2 」が実行されます. 「テストコマンド 2 」が「真」なら「コマンド 2 」 が, 「偽」なら 次の elif が…と続いていき, 全てのテストコマンドが「偽」 なら 最後に else に続く「コマンド 3 」が実行されます.

elif と「コマンド 2 」, else と「コマンド 3 」の部分は省略可能です. また, 「コマンド 1 」, 「コマンド 2 」…には, 複数のコマンドからなる コマンド 列を記述することも出来ます. if .... fi のいれ子は多重に 重ねることが出 来ます.

if (elif) に続くテストコマンドの文と then に続くコマンドの文は 普通改 行します. どうしても 1 行にまとめて書きたければ,

if テストコマンド; then コマンド ; fi

のようにセミコロンを挟みます.

case

書式

case 文字 in
    パターン 1)    コマンド 1  ;;
    パターン 2)    コマンド 2  ;;
      ...
      ...
esac

#!/bin/bash
case $# in                   <-- 引数が
  0) echo '引数なし' ;;      <-- 0 個の場合 '引数なし' と表示
  1) echo '引数一つ' ;;      <-- 1 個の場合 '引数一つ' と表示
  2) echo '引数二つ' ;;      <-- 2 個の場合 '引数二つ' と表示
  *) echo '引数三つ以上' ;;  <-- それ以外の場合 '引数三つ以上' と表示
esac

解説

パターンには, メタキャラクタ ?, *, [-] を使ったパターンマッチング機能 が使えます. case の右に指定される「文字」はパターン 1 から順次比較され, 一致するとそのパターンに対応するコマンド列 (括弧以降) が実行されます. コ マンド列の終わりは 2 重のセミコロン;;で区切られます.

処理の流れを制御する - 繰り返し構造

while, until

書式

while テストコマンド
do
  コマンド
done

#!/bin/bash
echo "Enter user name"
read user                       <-- user を読み込ませる
while [ $user != $USER ]        <-- $user が $USER (ログインしているユーザ) と一致しない限り, 
do                                  do と done の間に書かれたコマンドを実行し続ける
  echo 'You are not $USER!'     --> 'You are not $USER!' と表示する
  echo "Enter user name"
  read user                     <-- 再び user を読み込ませる
done

解説

while は, while の右に書かれたコマンドが真 (0) を返す限り, do と done の間に書かれたコマンド (列) を実行し続けます.

while の代わりに until と書くと, その右に指定されたコマンドが偽 (0 以外 の値) を返す限り, do と done の間に書かれたコマンド (列) を実行し続けます.

for

書式

for 変数 in 引数
do
  コマンド 
done

#!/bin/bash
for loop in '*.sh'      <-- 今いるディレクトリにある .sh ファイルの名前を
do                          loop に代入する
  echo $loop            --> $loop, すなわち代入した .sh ファイルの名前を表示
done

解説

for は, 与えられた引数の数だけ処理の繰り返しを行います. in の後に続く引数を変数に代入しながら do と done の間に記述されたコマンドを繰り返し実行します. in の後の引数がなくなり次第ループから抜けます.

処理の流れを制御する - 例

引数に指定された複数のファイル名に対し, 実在する物のみファイル名を表示する

#!/bin/bash
while test $# -gt 0
do
  if test -f $1
  then
    echo $1
  fi
  shift
done 

※ test は, それに続く式を評価して値を返すコマンドです. 与えるオプションに応じて, 数値に関するテスト, ファイルの型に関するテスト, 文字列に関するテストを行うことが出来ます. 例えば, test -f [ファイル名]とすると, ファイルが存在すれば真, 存在しなければ偽を返します.

数字の1から9を表示する

#!/bin/bash
number=1
while test $number -ne 10
do
    echo $number
    number=`expr $number + 1`
done 

※expr コマンドはその引数を1つの式として評価し, その結果を表示します. これを使うと UNIX で簡単な計算を行い, シェルスクリプトの中に算術演算処理を含めることが出来ます. なお, number の式の右辺はクオーテーション(')ではありません. 注意して下さい. (次節参照)

引用符

シェルスクリプトを作成する際, 引用符を用いることがあります. 引用符には以下の3種類があり, それぞれ意味が異なります(bash).

引用符

記号呼称意味
'...'シングルクォーテーション内部の文字列をそのままの文字列として返す.
"..."ダブルクォーテーション内部に含まれる変数等 、 `...`、 \ を解釈した結果の文字列を返す
`...`バッククォーテーション内部にあるコマンド(群)を実行し、 その標準出力を文字列として返す.

実際に, 使ってみて違いを確認してみましょう.

$ var=ls             <-- シェル変数 var に ls を代入

$ echo '$var'
$var                 --> '' 内の文字列をそのまま返す

$ echo "$var"
ls                   --> "" 内の変数の値を返す

$ echo `$var`
sample.sh test.sh    --> `` 内の変数をコマンドとして実行した結果を返す

より詳しい情報は man bash として bash のマニュアルを参照することで得ることも出来ます. 分からないコマンドや単語は, どんどん調べよう.

△ 練習問題

  • 「Hello World![改行]」を 5 回表示させてください. 次に, 実行時引数で繰り返し表示させる回数を受け取って, 任意の回数表示できるように改造してみてください.
  • a_{0} = 2, a_{n+1} = 2 * a_{n} + 1 を満たす漸化式があります. この漸化式を計算し, a_{10} を表示するスクリプトを作成してください. 次に, 実行時引数を用いて任意の数を計算できるように改造してみてください.
  • ITPASS サーバの

    /home/itpass/dc-arch/exp/fy2018/180807/exercise

    に 95 個のテキストファイルがあります. まずは, 手元の情報実験機にディレクトリごとコピーしてきて下さい.これらは全て「text_xx.txt」(xx は数字)という形式のファイル名になっていますが, これらを「xx.txt」というファイル名に変えて下さい.

  • 100 未満のフィボナッチ数列を出力して下さい.
  • 解答例

付録 [特殊記号のおさらい]

シェルスクリプト内でも, シェルで用意された特殊な記号を利用することができます. 既に習ったものを含め bash で使用される特殊な記号を以下にまとめました.

メタキャラクタ

シェルにはファイル名が全部わからなくてもファイル名のパターンで検索できる機能があります. このファイル名とパターンの照合に用いられる特殊な記号を "メタキャラクタ"(ワイルドカード)と言います. メタキャラクタを上手に用いると, タイプする文字数を減らすことができ, 効率的な入力が行えます.

代表的なメタキャラクタ

メタキャラクタ意味用例用例の意味
*任意の文字列を表す.ls /dev/sd*ディレクトリ "/dev/" 以下にある始めの2文字が"sd"で始まるファイル全てを表示せよ.
?任意の1文字を表す. ?? は任意の2文字になる.ls -d /???ディレクトリ"/" 以下にある3文字からなるディレクトリ全てを表示せよ.
[ ][ ] 内に含まれる文字にマッチする. 例えば [a-c]* は abc のいずれかで始まる任意の文字列を表す. []内の先頭に "^" を付けると "否定" の意味になる.ls -d /[a-c]* ls -d /[^a-c]*ディレクトリ "/" 以下にある "a,b,c" のいずれかで始まる (始まらない) ディレクトリを表示せよ.
{ }{ }内に含まれる文字列にマッチする. 例えば test.{pl,gif,f} は test.pl test.gif test.f と入力したことになる.ls /dev/{sda,sdb}*ディレクトリ "/dev/" 以下にある "sda,sdb" で始まるファイル全てを表示せよ.

リダイレクションとパイプ

UNIX のコマンドは必ず標準入力と呼ばれる共通した入力の受け付け口と, 標準出力と呼ばれる共通した出力の生成, 標準エラー出力と呼ばれる共通したエラーメッセージの生成を行う機能が備えられています. 複数のコマンドの標準入出力を繋ぎ合わせたり, ファイルへ/からの出力を行うために使う記号はリダイレクションとパイプ(パイプライン)と呼ばれます.

代表的な入出力リダイレクタ

記号意味
|パイプと呼ばれる. program1|program2 とすると program1 の標準出力を program2 の標準入力につなぐ.
>program >file とすると標準出力を file につなげる.
>>>>file とすると標準出力を file の末尾に追加する.
2>program 2>file とすると標準エラー出力を file につなげる.
<program < file とするとfileを標準入力につなげる.
<< stringヒア・ドキュメント. string と書かれた行が次に現れるまで、 この後に続く行を標準入力にする.

その他

入出力リダイレクションとパイプ

記号意味
;コマンドの区切り. program1;program2 とすると、 まず program1 が実行され、 次に program2 が実行される.
&;と似ている. ただし program1 の終了を待たない.
(...)... のコマンド(群)をサブシェルのなかで実行する.
{...}... のコマンド(群)をカレントシェルのなかで実行する.
$0実行したシェルスクリプトファイルの名前が代入される.
$1、 $2、 etcシェルスクリプト実行時の引数が代入される.
$varシェル変数 var の値.
${var}シェル変数 var の値. テキスト部分と連結されたときの混乱を防ぐために ${var} を用いる.
\エスケープ記号. c がシェルのメタキャラクタである場合、 \c は c の特別な意味を打ち消し、 単なる文字としての c にする.
var=value変数 var への値 value の代入
p1 && p2p1 を実行し、 もしうまくいったら p2 を実行する.
p1 || p2p1 を実行し、 もしうまくいかなかったら p2 を実行する.

文字列演算子

基本的に文字列演算子構文では, 操作を指示する特殊な文字列を変数と右ブレース(})の間に挿入する. 演算子が引数を取る場合は演算子の右側に挿入する. 文字列操作演算子最初のグループは変数の存在を評価し, ある条件のもとでデフォルトの値を置換する.

文字列演算子

使い方意味目的
${varname:-word}varnameが存在しNULLでない場合、 その値を返す.それ以外の場合はwordを返す.変数が定義されていない場合にデフォルトの値を返す${count:-0}は、 countが定義されていなければ0と評価される.
${varname:=word}varnameが存在しNULLでない場合、 その値を返す. それ以外の場合はvarnameにwordを設定して返す.変数が定義されていない場合にデフォルトの値を設定する.${count:=0}は、 countが定義されていなければそれに0を設定する.
${varname:?message}varnameが存在しNULLではない場合、 その値を返す. それ以外の場合はvarnameのあとmessageを出力し、 現在のコマンドあるいはスクリプトを中止する (対話型シェルではない場合). messageを省略するとデフォルトで "parameter null or not set"が 出力される.変数が定義されていない場合に発生するエラーをキャッチする.${count:?"undefined!"}は、 countが定義されて いなければ、 "count:undefined" を出力して終了する.
${varname:+word}varnameが存在しNULLでない場合、 wordを返す. それ以外の場合はNULLを返す.変数の存在を評価する.${count:+1}は、 countが定義されていなければ 1 ("真"の意味) を返す.
${varname:offset} ${varname:offset:length}サブ文字列を展開する. offsetの位置から length 文字の長さのサブ文字列を $varname の値から取り出す. 文字の位置は 0 からカウントする. lengthが省略された場合、 offsetの位置から $varname の終わりまでのサブ文字列が返される. offsetが0より小さかった場合、 開始位置は $varname の終わりからカウントされる. varname が @ の場合、 length は offset を先頭とする位置パラメータの番号になる.文字の一部を返す (サブ文字列またはスライスという).count が frogfootman と設定されている場合、 ${count:4} はfootmanを返し、 ${count:4:4}はfootを返す.

算術演算子

bash には, FORTRAN の強力な演算機能には及ばないが, ひととおりの整数演算機能が備わっている.

a=`expr <式>`

あるいは,

a=$(( <式> ))

とすると, 右辺の式を算術演算した結果が, 左辺の変数に代入される. 以下に示す四則演算子とカッコ "(" と ")" を組み合わせることで, 少なくとも有理関数の範囲で計算を行うことができる. ただし, 演算の戻り値は小数点以下切り捨てのため, 小数が計算途中にあらわれるような演算に使用することはできない.

四則演算

四則演算を行う算術演算子. $a や $b には整数しか用いることができず (小数を入れるとエラーとなる), かつ除算の戻り値は整数 (小数点以下切り捨て) となることに注意.

演算子意味
$(( $a + $b ))aとbの和
$(( $a - $b ))aとbの差
$(( $a * $b ))aとbの積
$(( $a / $b ))aとbの商
$(( $a % $b ))aとbの剰余

算術比較

if 文や while 文の条件式に, 大きい, 小さい, のように数値としての比較 (算術比較) を用いることができる.

代表的な算術比較演算子

演算子意味
$a -eq $bequal$a と $b が等しい場合に真
$a -ne $bnot equal$a と $b が等しくない場合に真
$a -gt $bgreater than$a が $b より大きい場合に真
$a -lt $blesser than$a が $b より小さい場合に真
$a -ge $bgt equal$a が $b より大きいか等しい場合に真
$a -le $blt equal$a が $b より小さいか等しい場合に真
Last modified:2019/06/03 16:09:35
Keyword(s):
References:[[Exp2019]スケジュール表・各回資料]