TIL

Did you learn anything today?

シェルスクリプトまとめ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 番出口の接続先になっているファイル書き込み用の口に流れるように口がつなぎ替わる