シェルスクリプトまとめ2
定期的に行う作業を自動実行
crontab
・・・「crond に実行してほしいコマンドとその実行日時のセット(cronjob)」を管理するコマンド
cronjob を一覧表示する
$ sudo crontab -l
シェルスクリプトを実行するときは、ホームディレクトリからの相対パスか、絶対パスを記述する
$ crontab -e
MAILTO=chino@gmail.com # cronjobの実行結果が指定のメールアドレスに悪られる.(MTAが設定済みの場合) 45 11 * * 1 /home/chino/scripts/analyze_log.sh > /tmp/result_$(date +%Y-%m-%d).txt
公開鍵認証
公開鍵の登録
公開鍵の登録 = 公開鍵のファイルを所定の位置に置くこと
公開鍵のファイルをサーバ上にコピーして、その鍵ペアで認証したいユーザのホームの直下の.ssh
ディレクトリにauthorized_keys
という名前で置く。
$ ssh-keygen
$ scp id_rsa.pub chino@192.168.1.2:/tmp/
$ ssh chino@192.168.1.2
$ sudo mkdir -p ~chiya/.ssh/
$ sudo mv /tmp/id_rsa ~chiya/.ssh/authorized_keys # 認証したユーザ以外では読み書きできないように「.ssh」ディレクトリと「authorized_keys」のパーミッションを設定すれば、公開鍵の登録は完了。
$ sudo chown -R chiya:chiya ~chiya/.ssh/ # 所有者をそのユーザに、グループもそのユーザのグループに設定する。
$ sudo chmod 700 ~chiya/.ssh # 所有者のみに読み書き・閲覧を許可
$ sudo chmod 600 ~chiya/.ssh/authorized_keys # 所有者のみに読み書きを許可
公開鍵を簡単に登録したい
$ ssh-copy-id -i ~/.ssh/id_rsa.pub chino@192.168.1.2 # 公開鍵を登録するサーバ側のユーザを明示的に指定する。
-i
: 登録したい公開鍵のパスを指定する。
ログインに成功すると、サーバ上のユーザのauthorized_keys
に公開鍵が登録される。
※ パスワード認証化、登録済みの鍵ペアでそのユーザとしてログインできることが前提。
定時処理で自動的に scp したい
パスフレーズを設定しない。その代わりに実行に制限を与える。
$ scp ~/.ssh/logdl.pub chino@192.168.1.2:/tmp/
$ ssh chino@192.168.1.2
$ cat /tmp/logdl.pub >> ~/.ssh/authorized_keys
登録が完了したら設定を変更する。
$ vim ~/.ssh/authorized_keys
公開鍵の行の先頭に関連付けるコマンド列を追記する。
command="scp -f /var/log/nginx/access.log"
ファイルをダウンロードするとき
$ scp -f サーバ上のファイルのパス
ファイルをアップロードするとき
$ scp -t サーバ上のファイルのパス
試す。
$ scp -i ~/.ssh/logdl chino@192.168.1.2:/var/log/nginx/access.log /tmp/
#!/bin/bash server_log=/var/log/nginx/access.log key=~/.ssh/logdl servers="192.168.1.51 192.168.1.52 192.168.1.53" for server in servers do log=/tmp/${server}-access.log result=/tmp/${server}-result.csv scp -i $key chino@${server}:${server_log} $log ~/scripts/analyze_access.sh $log $result done
※ この方法は、簡単に設定できるが融通が利かない。scp の実行内容を変えるときはその都度、authorized_keys を修正しないといけない。パスが異なるファイルをいくつもダウンロードしたいときも、その分だけ鍵ペアを用意しないといけない。
複数のサーバのファイルを効率よく収集したい
よくわからん。
条件に当てはまるログの行数を収集したい
ページごとのアクセス数ではなく合計が求めたい。→ wc
を使おう
行数
$ cat /tmp/access.log | wc -l
単語数
$ cat /tmp/access.log | wc -w
文字数
$ cat /tmp/access.log | wc -m
grep で見つかった業の数
$ zcat /oldlog/2018/accesslog.log.1.gz | grep "/campaign" | wc -l
複数 ver
#!/bin/bash for lof in /oldlog/2018/accesslog.*.gz do zcat $log | grep "/campaign" | wc -l # zcat $log | grep "Jan/2018" | wc -l # 1月のログだけ # zcat $log | grep -E "([23][0-9]/Jan|0[0-9]|10/Feb)/2018" | wc -l # 2018年1月20から2月10日まで。複数の月をまたがるような日付の範囲は正規表現を使う。 done
#!/bin/bash total_count=0 for lof in /oldlog/2018/accesslog.*.gz do pattern="([23][0-9]/Jan|0[0-9]|10/Feb)/2018" count=$(zcat $log | grep "/campaign" | grep -E $pattern | wc -l) total_count=$(($total_count + $count)) # 算術展開。bashで計算機能を使うときは$(())と書く done echo "合計:${total_count}"
複数のテキストファイルを一括編集したい
sed
を使う。→ テキストを編集するツールの一種。ストリームエディタ。
$ sed -e "s/置換前の文字列/置換後の文字列/"
例
$ cat rabbithouse-coffee.csv | sed -e "s/購入済み/使用済み" > rabbithouse-coffee.csv.updated
※ 1 つのコマンド列の中で「ファイルを開く操作」と「そのファイルに書き込む操作」を同時にやると、ファイルが消失する。
#!/bin/bash source=/tmp/rabbithouse-coffee.csv backup=${source}.$(date +%Y-%m-%d).bak mv $source $backup cat $backup | sed -e "s/購入済み/使用済み/" > $source
正規表現を使って複数のテキストファイルを一括編集したい。
なるほど、分からん。
数字をまとめて
"s/category([0-9]+/category-\1-/"
category100a → category-100-a
category200c → category-200-c
否定形
"s#(://[^:]+)3000/#\1:13000/#"
スラッシュを正規表現の中に含めるので区切り文字にはスラッシュ以外の文字を使うとよい。
http://192.168.1.15:3000/ → http://192.168.1.15:13000/
http://rabbit-house:3000/ → http://rabbit-house:13000/
先頭だけ
字下げの深さが違う場合もまとめて置換
"s/^( +)* /\1!! /"
* コーヒー * キリマンジャロ * ブルーマウンテン * カプチーノ * 軽食 * サンドイッチ * オムライス * ハンバーグセット \* ライス/パン
↓
!! コーヒー !! キリマンジャロ !! ブルーマウンテン !! カプチーノ !! 軽食 !! サンドイッチ !! オムライス !! ハンバーグセット \* ライス/パン
prettier のせいで勝手に整形される・・・
末尾
句点があったら、その後に半角スペースを 2 つ追加する。
"s/。$/。 /"
古い日付のファイルを探して消したい
find
コマンドを使えばいい。ファイルを探すときはfind
。
1 ヶ月以上前のファイルの一覧を得る
$ find /backup/daily -ctime +30
-ctime
: 最終変更日(changed time)でファイルを探す。名前やパーミッション、ディレクトリの移動といった、あらゆる変更についての最終変更日時。
-atime
: 最終アクセス日時。誰からも長いこと参照されていないファイルを探す時に使う。
-mtime
: 最終変更日時。ファイルの内容が変更された日時。ファイルの内容の変更についてだけの最終変更日時。
大抵の場合は、-ctime
で事足りる。
#!/bin/bash remove_files="$(find /backup/daily -ctime +30)" for file in $remove_files do rm "$file" done
+
と-
の考え方
今日から出発して、まず指定の日数分だけ時間を遡るイメージ。
+
なら過去方向に進みながら古いファイルを探していく
-
なら今日(未来の方向)に向かって戻りながら新しいファイルを探していく
最終変更日が 1 年以上前の古いファイル
$ find ./ -ctime +365
最終変更日が 1 週間以内である(この一週間以内に変更された)新しいファイル
$ find ./ -ctime -8
最後に実行してから 1 週間前から 2 週間前までのファイル
$ find /logs/ -ctime +7 and -ctime -15
最後に実行してから半年前の、名前に「access」を含んだファイル
$ find /logs/ -ctime +180 and -name "*access*"
最後に実行してから 30 日前の、名前に「access」または「error」を含んだファイル
$ find /logs/ -ctime +30 and \( -name "*access*" -or -name "*error*" \)
ディスクが満杯になる前にファイルを削除したい
#!/bin/bash # 見出し行を除いた数値の部分だけ取り出す free_size=$(df /data/backup | sed -r -e "s/[^ ]+ +[^ ]+ +[^ ]+ +([^ ]+).+/\1/" | tail -n 1) required_size=$((10 * 1000 * 1000)) # 10GB if [ $free_size -lt $required_size] then files=$(find /data/backup -ctime +30) for file in $files do rm "$file" done fi
前のコマンドが完了したら次のコマンドを実行する
prepare-data && process-data && report-result
プログラムをソースからビルドしてインストールするとき
$ ./configure --prefix=$HOME/local/ && make && make install
最新の情報に基づいてパッケージを更新するとき
$ sudo apt-get install update && sudo apt-get upgrade && sudo apt-get clean
前のコマンドが失敗したら次のコマンドを実行する
$ download-data --server=primary.datastore || download-data --server=secondary.datastore || exit 1
サブシェル
サブシェル・・・シェルの分身を作って、そっちでコマンドを実行する。
#!/bin/bash cd /shared/ for dir in logs data users do (cd $dir; files=$(find ./ -name "*.bak"); for file in files; do rm "$file"; done) done
※ ;を末尾に付与してかっこの中を「コマンドを列挙したリスト」にする
※ 複数行に渡る構文を含んだ処理は、関数としてくくりだしておけばサブシェルの中でも簡単に使える。
#!/bin/bash cd /shared/ remove_files() { files=$(find ./ -name "*.bak") for file in files do rm "$file" done } for dir in logs data users do (cd $dir && remove_files) done
1 つ前の作業ディレクトリに戻る
$ cd -
case
#!/bin/bash do_at_rabbithouse() { # } # (ry case "$(hostname)" in rabbithouse) do_at_rabbithouse ;; flulu_de_rapan) do_at_flulu_de_rapan ;; amausaan) do_at_amausaan ;; starbucks*) # 正規表現も使える e.g. starbucks_01, starbucks_02 do_at_starbucks ;; doutor*|exe*) # 複数のパターンを列挙できる do_at_coffee_chain ;; esac
一定時間繰り返したい
#!/bin/bash count=0 while [ $count != 3600 ] do curl "http://..." sleep 1 # 1秒待つ。 sleep 0.5 なら0.5秒、sleep 30d なら30日間待つ count=$(($count + 1)) done
コマンドのすべての出力をログファイルに保存したい
入力は標準入力だけだけど、出力は 2 つある。
標準出力・・・全般的な出力用(1 番)。1>と書く。>は 1>の省略形。
標準エラー出力・・・エラー情報の出力用(2 番)。2>と書く。
ファイルへのリダイレクトや別のコマンドへのパイプラインは、このうちの標準出力の方だけをつなぐ。
→ 標準エラー出力から出てきたエラーメッセージは、リダイレクト先のファイルには保存されない。
解決策
既存のファイル書き込み処理の口にリダイレクトして、そこに相乗りする。
$ ./test-status.sh -u a001 1>/tmp/log.txt 2>&1
>%1
: その後に指定した番号の出力と同じ出力先にリダイレクトする。
1 番(=標準出力)から出てくる情報が/tmp/log.txt
というファイルに書き込まれるように口がつなぎ替わって、
2 番(=標準エラー出力)から出てくる情報が 1 番出口の接続先になっているファイル書き込み用の口に流れるように口がつなぎ替わる