シェルスクリプトに挑戦しよう(12)制御構文[その4]――forによる繰り返し処理(2)“応用力”をつけるためのLinux再入門(32)

前回に続き「for文」を解説します。今回は、if文と組み合わせてファイルをバックアップするスクリプトを作成します。

» 2018年12月26日 05時00分 公開
[西村めぐみ@IT]
「“応用力”をつけるためのLinux再入門」のインデックス

“応用力”をつけるためのLinux再入門

バックアップ用のスクリプトを作成する

 今回は、前回の最後に作成した、指定したファイルを「ファイル名.年-月-日」にコピーする「filebackup」スクリプトを加工します。

 例えば、“指定したファイルを「ファイル名.年-月-日.gz」という名前で圧縮する”という内容にするならば、「cp」コマンドでファイルをコピーし、その後、「gzip」コマンドで圧縮するという流れで組み立てることができます。

 cpとgzipの両方で「ファイル名.年-月-日」という名前を使用することになるので、いったん変数「backupname」に保存します。このようにしておくことで、cpとgzipで異なった名前を使用してしまうというミスを防ぐことができます。また、バックアップファイル名を変更したくなった場合でも、1箇所だけ修正すればよいので加工もしやすくなります。

 ちなみに、ここではバックアップのファイル名に「年-月-日」を使っているので、同じ日に、同じファイルに対してfilebackupスクリプトを実行した場合は「ファイル名.年-月-日」が再度コピーされます()。

【※】同じ日に別のバックアップを取りたいという場合は「年-月-日」ではなく、時刻まで指定するとよいでしょう。この場合は「date」コマンドのオプションに「%H」(時)、「%M」(分)、「%S」(秒)を追加して、「`backupname=$filename.`date +%Y-%m-%d-%H-%M-%S`」のようにします。



 そこで、gzipには「--force」オプションを付けて、圧縮ファイルが存在した場合は再作成するようにしています。「--force」は「-f」オプションと同じ意味ですが、スクリプト中なので、意味が分かりやすいようにこちらを使用しています。

 なお、ここでは「echo」コマンドで、実行する予定のコマンドを表示するだけにしています。「echo」を外すと、実際にファイルのコピーや圧縮が行われます。

#! /bin/bash
for filename in "$@"
do
  backupname=$filename.`date +%Y-%m-%d`
  echo cp "$filename" "$backupname"
  echo gzip --force "$backupname"
done
▲引数で指定したファイルを「ファイル名.年-月-日.gz」に圧縮する(filebackupを加工)
$ filebackup file1 file2
cp file1 file1.2018-12-26
gzip --force file1.2018-12-26
cp file2 file2.2018-12-26
gzip --force file2.2018-12-26
#(このようなコマンドが実行される予定)
▲実行結果(filebackup)

for文とif文と組み合わせる

 このスクリプトでは、引数でファイル名を指定します。そこで、“指定されたファイルが存在したら〜”という条件を追加してみましょう。この場合、if文を使って、「if [ -f 変数 ]」のように指定することで確認できます。「[」の後ろや「]」の前の空白を入れ忘れないように注意してください。

 if文については本連載の第26回、条件の書き方については第27回を参照してください。

 なお、このような場合、for文のブロック中のif文は、行頭に空白やタブを入れて「インデント(字下げ)」した方が、処理のまとまりが分かりやすくなるでしょう。

#! /bin/bash
for filename in "$@"
do
  if [ -f "$filename" ]; then		#ファイルが存在した場合のみ実行
    backupname=$filename.`date +%Y-%m-%d`
    echo cp "$filename" "$backupname"
    echo gzip --force "$backupname"
  fi
done
▲指定したファイルが存在した場合のみ処理する(filebackupを加工)
$ ls file?
file1  file2  file3
#(file1、file2、file3がある)
$ ./filebackup file1 file4	#引数として「file1」と「file4」を指定してみる
cp file1 file1.2018-12-26
gzip --force file1.2018-12-26
#(file1とfile4を指定したが、file1だけが処理された(※)現時点では実行したいコマンドをechoで表示している)
▲実行結果

●複数の条件を組み合わせる

 ファイルがgzipで圧縮済みの場合は処理しない、つまり、バックアップ対象としないようにしてみましょう。判定はファイル名で行い、末尾が「.gz」かどうかをチェックします。

 具体的には、“変数から「.」より前を取り除いた結果が「gz」ではない”と考えて、「${filename##*.} != gz」とします。「!=」は一致していないという意味の演算子です。

#! /bin/bash
for filename in "$@"
do
  if [ -f "$filename" ] && [ "${filename##*.}" != "gz" ]; then
    backupname=$filename.`date +%Y-%m-%d`
    echo cp "$filename" "$backupname"
    echo gzip --force "$backupname"
  fi
done
▲ファイルが存在し、かつ、「.gz」ではない場合のみ処理する(filebackupを加工)

ファイル名に空白が含まれている場合の注意点

 現状のfilebackupスクリプトでは、名前に空白を含んだファイル、例えば「01 My Song.mp3」のようなファイルでは、「cp 01 My Song.mp3 01 My Song.mp3.2018-12-11」のようなコマンドが実行されてしまいます。この場合、cpコマンドに対して指定しているファイル名は、「01」と「My」と「Song.mp3」のような意味になっています。

 これを防ぐためには「01\ My\ Song.mp3」のように空白をエスケープするか、「"01 My Song.mp3"」ファイル名全体を引用符(")で囲む必要があります。

 単にcpコマンドを実行する場合は「cp "$filename" "$backupname"」とします。ここでは、echoコマンドでコマンドを表示しているので、「"〜"」だけだと引用符が表示から消えてしまうため、「"〜"」と「'〜'」を組み合わせて「"'$変数'"」としています。

#! /bin/bash
for filename in "$@"
do
  if [ -f "$filename" ] && [ "${filename##*.}" != "gz" ]; then
    backupname=$filename.`date +%Y-%m-%d`
    echo cp "'$filename'" "'$backupname'"
    echo gzip --force "'$backupname'"
  fi
done
▲ファイル名を引用符で囲む(filebackupを加工)
$ touch "01 My Song.mp3"	#確認用に名前に空白が入ったファイルを作成
$ ./filebackup *.mp3
cp '01 My Song.mp3' '01 My Song.mp3.2018-12-11'
gzip --force '01 My Song.mp3.2018-12-11'
#(ファイル名に引用符がついた)
▲実行結果(filebackup)

コマンドラインを実行する(eval)

 実行内容が確認できたら、「echo」を「eval」に置き換えます。evalは“式を評価する”という趣旨のコマンドで、「eval 文字列」で文字列をコマンドとして実行できます。

 ここでは、echoコマンドで表示されていた内容が、そのままコマンドとして実行されることになります。

#! /bin/bash
for filename in "$@"
do
  if [ -f "$filename" ] && [ "${filename##*.}" != "gz" ]; then
    backupname=$filename.`date +%Y-%m-%d`
    eval cp "'$filename'" "'$backupname'"
    eval gzip --force "'$backupname'"
  fi
done
▲ファイル名を引用符で囲む(filebackupを加工)
$ ls *
01 My Song.mp3  file1           file2
#(3つのファイルがある)
$ ./filebackup *		#echoをevalに変更したスクリプトを実行
$ ls *
01 My Song.mp3                  file1.2018-12-11.gz
01 My Song.mp3.2018-12-11.gz    file2
file1                           file2.2018-12-11.gz
#(それぞれのファイルが日付付きの名前で圧縮された)
▲実行結果(filebackup)

 なお、本稿では、練習にechoコマンドでコマンド列を表示しながら段階を追って加工していることから、引用符の処理部分がやや複雑になっていますが、同じ処理は以下のように書くこともできます。cpの行とgzipの行はこちらの書き方の方が一般的です。

#! /bin/bash
for filename in "$@"
do
  if [ -f "$filename" ] && [ "${filename##*.}" != "gz" ]; then
    backupname=$filename.`date +%Y-%m-%d`
    cp "$filename" "$backupname"
    gzip --force "$backupname"
  fi
done
▲一般的な書き方(filebackupを加工)

 また、スクリプトを作成せずに、コマンドラインでfor文を使ってファイルを順番に処理することも可能です。実行例は、LinuxコマンドTipsの第216回および第217回で紹介しています。

筆者紹介

西村 めぐみ(にしむら めぐみ)

もともとはDOSユーザーで「DOS版UNIX-like tools」を愛用。ソフトハウスに勤務し生産管理のパッケージソフトウェアの開発およびサポート業務を担当、その後ライターになる。著書に『図解でわかるLinux』『らぶらぶLinuxシリーズ』『Accessではじめるデータベース超入門[改訂2版]』『macOSコマンド入門』など。地方自治体の在宅就業支援事業にてMicrosoft Officeの教材作成およびeラーニング指導を担当。会社などの“PCヘルパー”やピンポイント研修なども行っている。


Copyright © ITmedia, Inc. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。