第6章 6-4 / シェルの力

引数と関数、実用スクリプト

このページで叩くコマンドと到達点

前提:6-3が完了し、~/scriptshello.shgreet.shjudge.shloop.shがあり、ホームディレクトリにいる状態から始めます。このページでは、スクリプトの外側から値を渡す引数と、処理をひとまとまりの部品にできる関数を学びます。そして最後に、これまで覚えたすべてを組み合わせて、~/practiceにたまった古いダミーファイルを自動で掃除する実用スクリプトlogclean.shを作ります。「自分の手作業を、自分の書いたスクリプトに肩代わりさせる」という、Linuxを仕事で使う感覚に一番近い体験ができるページです。

このページではSET 1〜3、合計30行のコマンドを上から順に叩きます。手打ち推奨(コピーは確認用)です。

SET 1 ― 引数($1 $2 $# $0)を受け取る

ubuntu@lightsail: ~
  1. $cd ~/scripts
  2. $nano args.sh
  3. (#!/bin/bash から始まる下のスクリプトを入力し保存)
  4. $cat args.sh
  5. #!/bin/bash
  6. echo "スクリプト名: $0"
  7. echo "1番目の引数: $1"
  8. echo "2番目の引数: $2"
  9. echo "引数の個数: $#"
  10. echo "引数すべて: $@"
  11. $chmod +x args.sh
  12. $./args.sh yumi devteam
  13. $./args.sh
  14. $./args.sh yumi devteam ubuntu
  15. 引数すべて: yumi devteam ubuntu
  16. $./args.sh "linux drill"
  17. 1番目の引数: linux drill
  18. $echo $0
  19. -bash
  20. $ls -l args.sh
解説 ― SET 1 で何をしたか

2行目で書くargs.shは、スクリプトを実行するときに一緒に渡した値を確認するためのお試しスクリプトです。$0$1$2$#はどれも位置パラメータ※1と呼ばれる特別な変数で、シェルがあらかじめ用意してくれています。$0はスクリプト自身の名前、$1は1番目に渡された引数、$2は2番目に渡された引数、$#は渡された引数の個数を表します。5行目の$@は渡された引数を全部まとめて表す記号で、個数がいくつあっても対応できる書き方です。

4行目のchmod +xのあと、5行目の./args.sh yumi devteamのように、スクリプト名のあとにスペース区切りで値を並べると、それがコマンドライン引数※2としてスクリプトに渡されます。実行してみると、$1にはyumi$2にはdevteam$#には2が入っていることが確認できます。6行目のように引数を何も渡さずに実行すると、$1$2は空、$#0になります。

7行目のように3つ引数を渡すと、$1$2には最初の2つしか入りませんが、$@を使えば出力例のとおり渡した引数すべてを一度に確認できます。8行目の./args.sh "linux drill"のように、スペースを含む値を1つの引数として渡したいときは、6-1で学んだクォートで囲みます。クォートを付けずに./args.sh linux drillと打つと、linuxdrillという2つの別々の引数として扱われてしまうので注意しましょう。

9行目のecho $0は、スクリプトの中ではなく今のシェルそのものに対して$0を尋ねた例です。出力例のとおり-bashのように、今使っているシェル自身の名前が返ってきます。$0が「今動いているプログラム自身の名前」を指す変数だということが、この違いからよくわかります。最後に10行目のls -l args.shで、このスクリプトにも実行権限が付いたままであることを確認しておきます。

この仕組みのおかげで、スクリプトを「毎回中身を書き換えて使うもの」から「毎回違う値を渡して使い回せる道具」へと成長させることができます。次のSETでは、この引数が渡されなかったときにエラーで止める安全装置を作ります。

POINT

$1$9は引数の順番、$0はスクリプト自身の名前、$#は引数の総数です。「シャープ(#)は数を数えるときによく使う記号」と結びつけて覚えると$#を忘れにくくなります。

ゆみちゃん
ゆみ

引数って最初は「なんでスクリプト名の後ろに値を並べるだけで伝わるの?」って不思議だったけど、LINEでスタンプ送るときに単語をポンポン並べる感覚に近いかも。スクリプト側は$1で1個目、$2で2個目、って順番に受け取ってるだけなんだよね。

SET 2 ― 引数チェックと関数

ubuntu@lightsail: ~/scripts
  1. $nano welcome.sh
  2. (#!/bin/bash から始まる下のスクリプトを入力し保存)
  3. $cat welcome.sh
  4. #!/bin/bash
  5. if [ $# -eq 0 ]; then
  6. echo "エラー: 名前を引数で指定してください"
  7. exit 1
  8. fi
  9. say_hello() {
  10. echo "ようこそ、$1さん!"
  11. }
  12. say_hello "$1"
  13. $chmod +x welcome.sh
  14. $./welcome.sh
  15. エラー: 名前を引数で指定してください
  16. $echo $?
  17. 1
  18. $./welcome.sh yumi
  19. ようこそ、yumiさん!
  20. $echo $?
  21. 0
  22. $./welcome.sh devteam
  23. ようこそ、devteamさん!
  24. $./welcome.sh yumi devteam
  25. ようこそ、yumiさん!
  26. $wc -l welcome.sh
  27. 9 welcome.sh
解説 ― SET 2 で何をしたか

1行目で書くwelcome.shの3〜6行目、if [ $# -eq 0 ]; then 〜 fi引数チェック※3です。6-3で学んだif$#を組み合わせ、「引数の個数が0だったら」エラーメッセージを表示してexit 1でスクリプトを終了させています。exitはスクリプトをその場で打ち切るコマンドで、続く数字は終了コード(0は正常終了、0以外は異常終了を表す慣習)です。これを入れておくことで、必要な情報が無いまま処理を進めてしまう事故を防げます。

7〜9行目のsay_hello() { 〜 }関数※4の定義です。関数名() { 処理 }という形で、ひとまとまりの処理に名前を付けて登録しておけます。関数の中でも$1が使えますが、これはスクリプト全体への引数ではなく、その関数を呼び出したときに渡された引数を指します。

10行目のsay_hello "$1"が実際の呼び出しです。関数は定義しただけでは動かず、このように名前を書いて呼び出して初めて実行されます。ここではスクリプト全体の$1(実行時に渡された名前)を、関数say_hello$1としてそのまま渡しています。4行目のように引数を付けずに実行すると出力例のとおりエラーで止まり、6行目のようにyumiを渡すと関数が呼ばれて挨拶が表示されます。

5行目のecho $?で、引数無しで終了した直後の終了コードを確認すると、出力例のとおり1が返ってきます。3行目で書いたexit 1がそのまま反映されている証拠です。7行目のように、正常に処理が終わった直後のecho $?0になります。6-3で学んだ終了コードの考え方が、自分で書いたスクリプトにもそのまま応用できることがわかります。8行目のように別の名前devteamを渡すと、その名前で挨拶が返り、9行目のように2つ引数を渡しても、関数が受け取るのは$1だけなので最初のyumiに対する挨拶だけが表示されます。最後に10行目のwc -lで、スクリプトが9行で構成されていることを確認します。

POINT

関数は「同じ処理を何度も書きたくないとき」「処理のまとまりに名前を付けて読みやすくしたいとき」に使います。数行の短いスクリプトではありがたみが薄くても、行数が増えてくるほど威力を発揮します。

SET 3 ― 実用スクリプト logclean.sh を作る

ubuntu@lightsail: ~/scripts
  1. $touch -d "10 days ago" ~/practice/old_log.txt
  2. $touch -d "3 days ago" ~/practice/mid_log.txt
  3. $touch ~/practice/new_log.txt
  4. $ls -l ~/practice
  5. $find ~/practice -type f -mtime +7
  6. /home/ubuntu/practice/old_log.txt
  7. $nano logclean.sh
  8. (#!/bin/bash から始まる下のスクリプトを入力し保存)
  9. $cat logclean.sh
  10. #!/bin/bash
  11. TARGET_DIR=~/practice
  12. echo "${TARGET_DIR} の中の7日以上前のファイルを削除します"
  13. find "$TARGET_DIR" -type f -mtime +7 -exec echo "削除対象: {}" \;
  14. find "$TARGET_DIR" -type f -mtime +7 -delete
  15. echo "掃除が完了しました"
  16. $chmod +x logclean.sh
  17. $./logclean.sh
  18. $ls -l ~/practice
解説 ― SET 3 で何をしたか

1行目のtouch -d "10 days ago" ~/practice/old_log.txtは、第4章4-3で学んだtouch-d(日付指定)を付けて、あえて「10日前に更新された」ことになっているダミーファイルを作る小技です。2行目では7日以内の「3日前」のファイルも作り、3行目では対照用に「今」更新されたnew_log.txtも作り、4行目のls -lで3つの更新日時の違いを確認します。この3つのファイルを使って、これから作るスクリプトが「7日以上前」だけを正しく見分けられるかをテストします。

5行目のfind ~/practice -type f -mtime +7は、スクリプトを書く前にfind単体で条件を試している行です。出力例のとおりold_log.txtだけが該当し、3日前のmid_log.txtと今日のnew_log.txtは7日を経過していないため対象外になることが先に確認できます。このように、スクリプトに組み込む前にコマンド単体で動作確認しておくのは、実務でも欠かせない手順です。

6行目で書くlogclean.shが、このページの集大成です。8行目のfind "$TARGET_DIR" -type f -mtime +7 -exec echo "削除対象: {}" \;は、第4章4-3で学んだfindコマンドに-mtime +7(更新から7日以上経過)という条件を付け、該当するファイルを-execで1つずつechoに渡して「これから消す対象」を先に表示させています。いきなり削除する前に対象を確認できるようにしておくのは、実務でも徹底したい安全策です。

9行目のfind "$TARGET_DIR" -type f -mtime +7 -deleteが実際の削除です。-deleteオプションを付けることで、条件に合ったファイルをその場で削除できます。8行目で確認した対象と、9行目で削除される対象は同じ条件なので、事前に見せた削除対象がそのまま消える、という流れになっています。

8行目のchmod +xのあと9行目で実行すると、10日前の日付を持つold_log.txtだけが「削除対象」として表示されたあと削除され、3日前のmid_log.txtと今日付のnew_log.txtは7日を経過していないため残ります。10行目のls -lで、mid_log.txtnew_log.txtだけが~/practiceに残っていることを確認してください。logclean.sh~/scriptsに残しておき、第7章の総合演習でも再利用します。

ゆみちゃん
ゆみ

自分の手で書いたスクリプトが、実際に「いらないファイルだけ」をちゃんと見分けて片付けてくれたの、地味だけどめちゃくちゃ感動するポイントだよ! これって毎回自分でファイルを1個ずつ確認して消してた作業を、まるごと肩代わりしてくれてるってことだからね。ここまで来たら、もう「黒い画面が怖い」なんて言わせない!

まとめ

6-4では、引数($1・$#など)でスクリプトの外から値を受け取る方法、関数で処理をまとめる方法を学び、最後に実際に使える掃除スクリプトlogclean.shを完成させました。このページで叩けるようになったコマンド・構文を一覧にまとめます。

コマンド / 構文何をするか覚え方
$1 $21番目・2番目に渡された引数を参照する渡した順番の番号
$0スクリプト自身の名前を参照する0番目=自分自身
$#渡された引数の個数を参照する# = 数を数える記号
$@渡された引数すべてを参照する個数を問わず全部まとめて
exit 数字スクリプトをその場で終了する0は正常、それ以外は異常のサイン
関数名() { 処理 }処理をまとめた関数を定義する名前付きの処理のかたまり
touch -d "日付" ファイル名更新日時を指定してファイルを作成/変更する-d = date(日付指定)
find ... -mtime +7更新から7日以上経過したファイルを探すmtime = modified time
find ... -deletefindで見つけたファイルを削除する見つけて即削除

次のページ「6-5. エイリアスとプロンプト設定」では、ここまで作ってきたコマンドやスクリプトをもっと手軽に呼び出せるように、自分だけの操作環境を整えていきます。

脚注 ─ 用語解説
  1. 位置パラメータ$0$1$2…のように、スクリプトに渡された引数を順番で参照できる特別な変数。
  2. コマンドライン引数 … コマンドやスクリプトの実行時に、名前の後ろにスペース区切りで渡す値のこと。
  3. 引数チェック … スクリプトの冒頭で$#などを使い、必要な引数が渡されているかを確認する処理。無い場合はエラーで止めるのが定石。
  4. 関数関数名() { 処理 }の形で定義する、名前を付けて呼び出せる処理のまとまり。同じ処理を繰り返し書かずに済む。