読者です 読者をやめる 読者になる 読者になる
スポンサーリンク

ユーザ配布用のbashシェルを作成するための 17 のコマンド

bash linux x個のy


ユーザ配布用の,linux上のシェルスクリプトを作成する。

そのために必要なコマンドの入門知識。


開発者やアプリが実行するのではなく,配布先の一般のlinuxユーザが実行するシェルである,という点がポイント。

そうすると,求められるのは

  • 利用者の打つコマンドが少ないこと
  • 不測の事態(ディレクトリが無いとか)を自動的に判断して,処理を正常に進めること

扱うコマンド類は,下記の17個。

  • (1)前準備
    • (1−1)cp, tar
    • (1−2)shebang
    • (1−3)変数宣言,date
    • (1−4)test,exit
    • (1−5)test, mkdir,rm
    • (1−6)/etc/init.d/-- stop
    • (1−7)sh
  • (2)メイン処理
    • (2−1)for, find, grep
    • (2−2)head, tail
    • (2−3)sed
    • (2−4)patch, diff
    • (2−5)外部製品のCUI呼び出し
    • (2−6)tar
  • (3)事後処理
    • (3−1)touch
    • (3−2)/etc/init.d/-- start
    • (3−3)echoで終了通知
    • (3−4)リリース

(1)前準備

(1−1)cp, tar

まず,ユーザがシェルを渡された時にすべきことを考えてみる。

  • tar.gzを渡される。
  • ユーザは,2つのコマンドを実行する。
    • まず,tar.gzを指定フォルダに移動。
    • 次に,tar.gzを解凍。
    • 次に,解凍された資材の中のメインシェルを起動。


資材を解凍

# コピー
cp -p /root/shizai.tar.gz /tmp/shizai.tar.gz

# 解凍
cd /tmp/
tar xvzf shizai.tar.gz

# メインシェルを起動
sh shizai/main.sh

これらはユーザに打たせる唯一のコマンド。

あとの処理はシェル内に書く。


(1−2)shebang

先頭のおまじないを覚える。

#!/bin/bash

shebangのshはsharp。なので先頭は#。

先頭がsharpなので,Perlではコメント行。

※PerlのshebangはWindowsでは無視される

小人さんへの置き手紙 shebang とは
http://www2.u-netsurf.ne.jp/~alt/mt/a...


その下に,

  • スクリプトの処理概略
  • 呼び出し方
  • 作成日付,作成者

などの情報をコメントで書く。

(1−3)変数宣言,date

先頭に,リソース系の設定情報を書いておく。

# 設定項目
target_filename="hoge.csv"
target_filepath="/tmp/${target_filename}"


変数の中に「現在時刻を含めたい」という場合もある。

例えば,一時ディレクトリを作るときに,他のディレクトリの名前と衝突したくないとか。

# hoge_年-月-日_時-分-秒 の形式で変数名を定義
varname="hoge_$(date +%Y-%m-%d_%H-%M-%S)"


参考:

Super Technique 講座:bash 超プログラム術
http://www.nurs.or.jp/~sug/soft/super...

  • シェル言語での変数の内容は「単なる文字列」であり、それが「$」による参照の結果、その内容でコマンドライン行の中にシンボルを置換し、実際に実行すべきコマンドラインを生成する。
  • シェル言語の変数には「型」はなく、単なる文字列だけ

規約として:

  • 文字列変数を宣言する際には,引用符でくくること。
  • $varではなく,可読性と保守性とバグ防止のため,常に${var}とすること。

(1−4)test,exit

処理を始めるかどうか判定する。


実行済みであることを表すファイルが存在するなら,実行中止する。

# jikkou_zumiというファイルがあれば,スクリプトの実行を中止。
[ -f /tmp/jikkou_zumi ] || exit


もしくは,その事を通知する。

lock_file_path="/tmp/jikkou_zumi"

if [ -f ${lock_file_path} ]; then
  echo "このスクリプトはもう実行済みです。再度実行する必要はありません。"
    # ls -l ${lock_file_path}
  exit
fi


exitは,呼び出し元に対して「戻り値」を返せる。

main.sh

sh hoge.sh
if [ $? = 0 ]
then
  echo "処理は成功しました。"
else
  echo "処理は失敗しました。"
fi


hoge.sh

if then
  echo "正常終了です。" 
  exit 0
else
  echo "エラーが発生しました。"
  exit 1
fi

シェル呼び出しがexitで終了したケースに限らず,

直前のコマンドが正常に実行されたかどうかを確かめるためにいつも「$?」で終了コードを拾える。

0なら正常終了とみなせる。


(1−5)test, mkdir,rm

作業開始に当たって,作業環境の整備。

資材を解凍したり,一時ファイル置き場確保が必要。


「無ければ作る」という処理:

okiba_dir="/tmp/okiba/dayo"

# 資材置き場のディレクトリが無ければ,ディレクトリを新規作成
[ -d ${okiba_dir} ] || mkdir -p ${okiba_dir}

mkdirには,常にpオプションをつけ,親ディレクトリが存在しないという不測の事態に備えておく。
防衛的にコーディングする。


「有れば消す」という処理:

temp_dir="/tmp/okiba/garbages"

# 作業用の一時フォルダと同名のディレクトリが存在したら,ディレクトリごと削除する
[ -d ${temp_dir} ] && rm -rf ${temp_dir}


これで作業用のフォルダが整い,資材をコピーしたり解凍したりする。



なお,ディレクトリを消す際に「空ディレクトリかどうか判定したい」(もし空ではないなら,削除したくない)という場合がある。

その書き方:

## カレントディレクトリが空かどうか判定する

# ファイル数を取得する
file_num_str=$(ls -1 | wc -l)
file_num=`expr ${file_num_str}`

# ファイル数が0より大きいかどうか判定
if [ ${file_num} -gt 0 ] ; then
  echo "空ディレクトリではありません。"
else
  echo "空ディレクトリです。"
fi

※もっとシンプルに,ls -A を使ってもよい。

# ディレクトリが空かどうか判定
if [ "$(ls -A /tmp/kesitai/foruda )" ]
then
  echo "空ディレクトリではありません。"
else
  echo "空ディレクトリです。"
fi

Bash Shell Check Whether a Directory is Empty or Not
http://www.cyberciti.biz/faq/linux-un...

  • [ "$(ls -A /tmp)" ] && echo "Not Empty" || echo "Empty"


bashのif文でアンドやオア条件判定できますか?
http://okwave.jp/qa/q280252.html

  • -a で&&, -o で || になる

(1−6)/etc/init.d/** stop

バッチ処理中に余計な横槍が入ってこないように,止められるものを止めておく。

たとえば,スクリプトの実行が終了するまでの間に,Webからサーバ内へのアクセスを防止したい場合

# Apacheを停止
/etc/init.d/httpd stop

Linux起動の仕組みを理解しよう[rcスクリプト編]
http://www.atmarkit.co.jp/flinux/rens...

  • rcスクリプトは、Windowsのautoexec.batに相当する
  • start()がプログラムのスタート時に行う処理、stop()がプログラム終了時に行う処理

(1−7)sh

トップレベルのスクリプトにはメイン処理の概略を示す。

あるいはメイン処理内では,サブのシェルを順番に呼び出してゆく。(コードを構造化・階層化する)

sh /tmp/hoge.sh

(2)メイン処理

(2−1)for, find, grep

複数のファイルを操作するようなシェルであれば,まずはファイルスキャンするはず。

ファイルパスやファイル名,ファイル内容などで,フィルタを通すのである。


ディレクトリ上:

# ディレクトリ直下に存在するhoge_1.html, hoge_2.html, ... に対して処理を行います。
# hoge_0.htmlは対象外です。

for file_name in $(ls -1 /tmp/html_files | grep "hoge_.*\.html" | grep -v "hoge_0\.html" )
do
  echo "${file_name}を処理します。"
  
  # 個別ファイルの処理
  # 〜
  
done


ディレクトリ内を再帰的に:

# /tmpと全子フォルダ内に存在するhoge_1.html, hoge_2.html, ... に対して処理を行います。
# hoge_0.htmlは対象外です。

for file_name in $(find /tmp -name "hoge_*.html" -print | grep -v "hoge_0\.html" )
do
  echo "${file_name}を処理します。"
  
  # 個別ファイルの処理
  # 〜
  
done


特定の子フォルダを無視したい場合:

findの部分を以下のように書き換え

# /tmp/mushi内のファイルを無視
find /tmp -path /tmp/mushi -prune -o -name "hoge_*.html" -print

findに複数条件を渡したい場合は,aオプションでAND条件を設定できる。

# 名前が「hoge」で始まるディレクトリを全て検索
find /tmp -name "hoge*" -a -type d -print

マッチした物の個数を数えたい場合:

find /tmp -name "hoge_*.html" -type f -print | wc -l

find の -prune の使い方
http://d.hatena.ne.jp/mtv/20100424/p1
findの-print オプションはいつも省かないほうがよい


find で特定ディレクトリを対象から外す
http://www.furyu.atnifty.com/cgi-bin/...


ManPage of find
http://www.linux.or.jp/JM/html/GNU_fi...

grepとfindをマスターした人がプロジェクト内に1人いると心強い。


(2−2)head, tail

操作対象のファイルが見つかったら,次にファイル内容の解析に取り掛かる。

解析のためには,まず抽出が必要。
(ユーザインタフェースではなくシェルの処理なので,「表示」ではなく「抽出」とか「探索」なのだ。)



ファイルの内容を抽出して保存

# 先頭の2行を抽出
cat hoge.txt | head -n 2 > hoge_head.txt

# 末尾の2行を抽出
cat hoge.txt | tail -n 2 > hoge_tail.txt

# 4行目から5行目までを抽出
cat hoge.txt | head -n 5 | tail -n 2 > hoge_4_to_5.txt

「先頭から」と「末尾から」を組み合わせて,「中間だけ抽出」を実現している。

bash ファイルの特定の行へジャンプ
http://www.atmarkit.co.jp/bbs/phpBB/v...

  • head|tailコンボはある程度速い

(2−3)sed

抽出したファイルの内容を,加工して保存する。


同名のファイルを上書きしたい場合:

# ハイフンを置換して上書き保存
sed -i "s/-/(゚∀゚)/g" hoge.txt


別名のファイルに保存したい場合:

# ハイフンを置換して別名保存
cat hoge.txt | sed "s/-/(゚∀゚)/g" > hoge_hyphen.txt


保存時に,置換ではなく,文字コードを変換したい場合:

#UTF8からSJISに変換して別名保存
cat hoge.txt | nkf -W -s > hoge_sjis.txt

bashと sedで複数ファイルのテキストを置換する方法
http://vikslinuxtips.blogspot.com/200...
find . -name "*.拡張子" -exec sed -i "s/元文字列/置換後文字列/" {} \;


Linux上で文字コードを変換できるコマンドnkfのオプション一覧
http://it.kndb.jp/entry/show/id/745
大文字が入力,小文字が出力

(2−4)patch, diff

パッチ適用。

  • ファイルのアップグレード
  • ファイルの修復

のような複雑な編集・置換処理を,前もって.patchファイル内に定義しておく。



パッチの作り方:

diff -u 旧ファイル 新ファイル > パッチファイル名

できあがったパッチの適用方法:

修正対象ファイルの存在するディレクトリ上で

patch < パッチファイル名

UNIXの部屋 コマンド検索: diff
http://x68000.q-e-d.net/~68user/unix/...

(2−5)外部製品のCUI呼び出し

シェルスクリプトは,ファイル操作のDSL。

それ以外のことは専用のアプリケーションに任せる。


たいていは,シェルを実行しているマシン上に何かをインストールするような

「インストーラ」的な処理を記述することになるだろう。


ということは,パッケージ管理コマンドとして

「rpm -ivh RPMファイル名」と書く。


もしインストールに依存関係上のエラーが発生するようなら,

「rpm -ivh --force RPMファイル名」

とすればうまくいったりする。



RPMではなく,JDKをインストールする場合なんかは,インストールの最後にEnterキーを押すように要求される仕様だ。

これを自動化するためには,「yes ''」というコマンドを使えばよい。

How can I avoid press “Enter” during Java Sun (Oracle) 1.6 installation on Linux (CentOS)
http://serverfault.com/questions/2492...

  • yes '' | ./jdk-6u24-linux-x64.bin


その他,インストール処理以外にも,いろんなものをシェルから呼び出せる。

# Ruby on Railsのモデルクラスを呼び出したい場合
cd ${rails_root_dir}
ruby script/runner "MyModel.hoge_method('foo')"


# SQLを実行したい場合
export PGUSER=user_name
export PGPASSWORD=user_pass
export PGDATABASE=my_db_name
export PATH=$PATH:/usr/local/pgsql/bin
psql my_db_name < /tmp/foo.sql

Railsアプリで ActiveRecordを使ったバッチ処理 その2
http://brass.to/blog/rails_batch_2.html

  • script/runnerは引数の文字列をRubyスクリプトとして解釈し、Rails環境で実行する

シェルからパスワード無しにデータベースに接続するには
http://www.ksknet.net/postgresql/post...

  • 各ユーザの .bash_profile に環境変数を書き込む


perlのワンライナーを書いてもいい。

Linux上でPerlで開発するための bash コマンド集
http://language-and-engineering.hatenablog.jp/entry/20100125/p1

(2−6)tar

ファイルを収集した場合,固める。

# 圧縮前の準備
[ -f result.tar.gz ] && rm -rf result.tar.gz

# カレントディレクトリ内を圧縮
tar -cvzf result.tar.gz ./*

固める前に必ず,念のため「先客が有れば消す」という処理を入れておく。



(3)事後処理

(3−1)touch

スクリプトの誤動作を防ぐために,「実行済みだよ」という証跡を残す。

「installed」みたいな名前のファイルを作ったり。

# このスクリプトが実行されたことの証跡を残す
touch /tmp/jikkou_zumi

スクリプト実行時のログを近辺に残してもよい。


(3−2)/etc/init.d/** start

横槍が入ってこないように止めていたサービスを再起動する。

# Apacheを起動
/etc/init.d/httpd start

(3−3)echoで終了通知

終了した旨,通知する。


処理が終わったことの「証跡」を表示しても良い。

  • 目的とするファイルが作成されたかどうか→対象ディレクトリでls
  • 目的とするモジュール類がインストールされたか→そのモジュールのマネージャに問い合わせてみる(Rubyならgen whichとか)


シェルの実行後にユーザが実行しそうなコマンドを,シェル自身が打ってあげるのだ。

# 重い処理の後でechoすると,大抵表示順が崩れる・・・
sleep 1

echo "終了しました。"

(3−4)リリース

ここまで作ったシェルをリリースして,利用可能にする。

その手順も別途シェルにしておき,開発メンバ内で自動化する。


フォルダ構成は

  • ./materials/ : シェルの資材を置く
  • ./release/ : ユーザに提供する,リリース版のシェルを置く
  • ./build.sh : リリース用の圧縮ファイルを作成するバッチ


build.sh :

#!/bin/sh

cd ./materials

# 以前の成果物を削除
rm -f ../release/result.tar.gz

# 固めなおす
tar cvzf ../release/result.tar.gz * --exclude ".svn"

svnの隠しファイルは除外して圧縮している。