技術者になりたい何か

技術者になりたい何かの覚書的な何かです

cronでn秒おきに実行するジョブを中途半端な一定期間だけ走らせたい

お題
cronで秒単位で実行しているコマンドを、任意の時間帯だけ走らせたい。
21:50-22:40,22:50-23:40の50mx2の間だけ、n秒おきにとある情報を取るコマンドを走らせたい。

というのを少し苦労したけど、実行ユーザのcronをrootのcronで置き換えるという力業(?)でなんとかできました。

■crontabの基本的な使い方

$ crontab -l

内容表示

$ crontab -e

内容編集

$ crontab -r

内容削除
オプションで - u username をつけるとそのユーザのcronを編集できる。-uつけない場合は現在のユーザのcronに対する操作になる。

ちなみにeとrはキーが隣り合っているため、操作ミスで削除してしまわないようにcrontab -eで編集はしないように、とかエイリアスでcrontab -rを置き換えたり無効にしたりするべきという議論があるけど、ここでは触れない。
自分としては編集前に必ずcrontab -lをしておくことで、間違えて消してしまった場合にすぐ復旧できるようにしておけば良いような気がしているし、実際そうしている。

cronのファイル自体は/var/spool/cron 配下にユーザ名でおいてある(RedHat 7系の場合)ので、そのファイル自体を編集するという手もある。

■crontabの書き方

設定項目は左から「分」「時」「日」「月」「曜日」

* * * * * [実行コマンド]

曜日については0-7で、0も7も日曜日。
リストでの書き方 0,10,20,30 分フィールドの場合0,10,20,30分に実行する
範囲での書き方 1-5 時間フィールドの場合1,2,3,4,5時に実行する。
間隔を指定する */10 分フィールドの場合、10分おきに実行する。

上記のように、デフォルトでは分が最小単位のため、秒指定の実行はできない。
そこでn秒おきに実行するためによく使われるのが、 seq コマンドを使ってfor文を回し、sleepコマンドを使用するもの。

10秒おきの場合はこのように。

* * * * * for i in `seq 0 10 59`;do (sleep ${i}; コマンド) & done;

seqコマンドの動きはこのような感じ。

$ seq 0 10 59
0
10
20
30
40
50
$ 

上記の場合、0~59の範囲内で10個おきに番号を取得するため、出力としては0,10,20,30,40,50となる。
これを変数iに入れて、sleep $i すると、

sleep 0 実行コマンド
sleep 10 実行コマンド
sleep 20 実行コマンド
sleep 30 実行コマンド
sleep 40 実行コマンド
sleep 50 実行コマンド

となるので、10秒おきに実行が実現できる。
これを毎分実行するので、毎分00秒になったらseq でsleepする秒を取得し、sleepしてから指定コマンドを実行となる。

■本題-n秒おきに実行するコマンドを、指定時間内だけ実行したい。

ここから本題。
切りの良い時間(1-3時)とか、特定日だけとかなら時間フィールド、日フィールドの記述でなんとかなりそうだし、
張り付いていられる時間なら指定時間にコメントアウトを解除、再度コメントアウトで行けそうだけども。

ちなみに今回のお題は、21:50-22:40,22:50-23:40と非常に中途半端な時間が2回という残念な感じ。

それからどうした

幸いなことにこのcronを実行するのはとある一般ユーザだったので、rootのcronで指定時間にhogeのcronを書き換え、指定時間に書き戻すことで対応できました。

具体的には

1.n秒おきに実行する行を書き加えたcrontab(10s_cron)と、もとのままのcrontab(default_cron)をどこか(/home/tmin/cron)においておく。
$ ls -l /home/tmin/cron
10s_cron
default_cron
2.rootのcronで予め用意したcronを指定時間に書き換え、指定時間に書き戻し。
50 21 * * * cp -p /home/tmin/cron/10s_cron /var/spool/cron/tmin
40 22 * * * cp -p /home/tmin/cron/default_cron /var/spool/cron/tmin
50 22 * * * cp -p /home/tmin/cron/10s_cron /var/spool/cron/tmin
40 23 * * * cp -p /home/tmin/cron/default_cron /var/spool/cron/tmin

結果的にうまく行きました!!

ちなみにrootのcronで/var/spool/cron/root自体を書き換えられるかは未検証。
よくよく考えたらsed置換とかでも良かったよね。

なんとなくもっとスマートなやり方はあるような気がします。

そのshの正体は

busyboxのshはashで配列が使えないという話です。

業務でシェル芸的ワンライナーを作ることが多くなってきてたのですが、どハマリしたのがshがbashではない環境。
具体的にはESXiのデフォルトシェルはどうやらbusyboxのashらしく、配列が使えないという話。

■そのシェルなんでしょう

今使ってるシェル、というかshで呼び出されるシェルはwhichとlsで分かるようです。
・Archの場合

$ which sh
/usr/bin/sh
$ ls -l /usr/bin/sh
lrwxrwxrwx 1 root root 4  64 17:54 /usr/bin/sh -> bash
$ ls -l /usr/bin/bash
-rwxr-xr-x 1 root root 866600  64 17:54 /usr/bin/bash

shは/usr/bin/shで、この実体はbashへのシンボリックリンクになってます。
念の為/user/bin/bashをls -lしてみると、こちらは実行権限付きのファイルになってるので、これが実体。

Debian(9.5)の場合

$ which sh
/bin/sh
$ ls -l /bin/sh
lrwxrwxrwx 1 root root 4  124  2017 /bin/sh -> dash
$ which sh
/bin/sh
$ ls -l /bin/dash
-rwxr-xr-x 1 root root 117208  124  2017 /bin/dash

同じように調べてみると、shは/bin/shを呼び出して、/bin/shはdashへのシンボリックリンクになっています。
念の為/bin/bashをls -l してみると、こちらが実行権限付きのファイルになっています。

ちなみにDebianでもbashは入ってるので、明確にbashを指定すればbashでしか通じない文法のシェルでも問題なく動くでしょう。

$ ls -l /bin/dash
-rwxr-xr-x 1 root root 117208  124  2017 /bin/dash

実はシェルは他にもいろいろあるので、気になる人はこの辺見てください。(ちょっと情報古いけど)
シェル

■実は配列が使えないシェルがある

これがドハマったところでした。
実はシェルの配列ってbashの機能だったんですね。

具体的にはESXiのコマンドラインで、コマンド結果を配列に突っ込んで、順に取り出しながら代入してうんたらかんたら、みたいなことをワンライナーで書いてたときにそれは起こりました。
sh: syntax error: unexpected "("
シンタックスエラー??
書き方が悪いと思って調べながら何度修正してもだめ。というか、以前にRedHatLinux上で動かしたワンライナー配列のコマンドをもとにして書いてたので、本当に迷宮入りしそうでした。
が、実はESXiのシェルって/bin/busybox ashが実体だったらしいです。(busyboxのデフォルトシェルはashらしい。)


dash,bash,busyboxすべて入っているDebianで試してみましょう。
ashに変更

tmin@tminserver:~$ /bin/busybox ash


BusyBox v1.22.1 (Debian 1:1.22.0-19+b3) built-in shell (ash)
Enter 'help' for a list of built-in commands.

~ $ 

さてこれで配列が使えるかというと

~ $ array=(a b c d) ; for i in ${array[@]} ; do echo $i ;done
sh: syntax error: unexpected "("

だめです。

bashに戻して配列を使うワンライナーを流す。配列をa b c d として、一個ずつ取り出してechoするだけのワンライナー

~ $ /bin/bash
tmin@tminserver:~$ array=(a b c d) ; for i in ${array[@]} ; do echo $i ;done
a
b
c
d

これが想定の動きです。

dashならどうか?どうやらdashはashがベースらしいですが。

tmin@tminserver:~$ /bin/dash
$ array=(a b c d) ; for i in ${array[@]} ; do echo $i ;done
/bin/dash: 1: Syntax error: "(" unexpected

だめですね。

つまり#!/bin/sh ではなく#!/bin/bash って書いとけばシェルなら大丈夫。ワンライナーなら確実にbashに切り替えてから実行すればおk。
でもDebianと違って、ESXiのデフォルトではbashがないのですよ!!

■解決法

1.配列+forによる繰り返しをやめてwhileでなんとかする。
何も根本的解決になってないですけど。コマンド結果を配列に入れて、一個ずつ取り出して代入してうんたらかんたら、っていうくらいのことならwhile使えばいいよ。
※問題点
複数の配列を使いたかった処理の場合これでは目的を達成できない。
達成しようとした場合、一撃で終わらせたかった処理が2撃以上になるか、シェルが冗長になるかどちらか。
とはいえ、今回自分がハマっていた処理についてはこれで行けた。(lsの結果を配列に突っ込んでうんたらかんたら、みたいなのだったと記憶している)

2.set -- $arrayを使う
https://stackoverflow.com/questions/26091758/arrays-in-shell-script-not-bashstackoverflow.com

さすがStack Over Flow。

$ array="0 1 2 3" ; set -- $array ; for i in $array ; do echo $i ;done
0
1
2
3
~ $ 

でも複数配列うまく使うのはまた難しそう。

■結論

bashない環境はきっつい。
なんで動かないのかよくわからないエラーが出てるときはそれほんとにbashか疑うのもときに必要。
あといろんなシェルでビミョーに挙動が違うのってたまにしかでくわさないしむずかしーです。

私的シェル芸入門

シェル芸的なものを書くことが多くなってきたのでいろいろまとめてみます。

■シェル芸とは

シェル芸の定義バージョン1.1

マウスも使わず、ソースコードも残さず、GUIツールを立ち上げる間もなく、あらゆる調査・計算・テキスト処理をCLI端末へのコマンド入力一撃で終わらすこと。あるいはそのときのコマンド入力のこと。

https://b.ueda.tech/?page=01434シェル芸 | 上田ブログ


※以降の記述はあくまで個人的なシェル芸的なものにたいするあれこれなので、厳密にはシェル芸の定義や考え方に反する記述が含まれているかもしれません。
 厳密なシェル芸?を学びたい方はなんか他で確認してください。

■シェル芸のための考え方(個人的)

・一個ずつポチポチやってく繰り返し作業がめんどい→それシェルでできない?
ex)1~100までのhostname,日付、連番のファイルを作成

$ for i in {1..100} ;do touch $(hostname)_$(date +_%Y%m%d)_$i.txt ;done

みたいな

Teratermのブロードキャストで一斉に時刻入りのディレクトリ作りたい。
けどあとでログ収集サーバとかに送るからホスト名とかで区別つけときたい

$ mkdir $(hostname)_$(date +_%Y%m%d%H%M)

・とあるディレクトリの中の一定時間経過したファイルを探して削除したい

$ find . -type f -mmin +10 | xargs rm -fv

■よく使うコマンドつなぎ方

・$(command) ()内のコマンドを展開する。上記例の中で$(hostname)は$ hostnameの結果をその場所に入れる。

$ echo $(hostname)
tmin_Arch
$ hostname
tmin_Arch

ファイル名に入れたいときとか、grepの検索ワードに使いたいときとかに重宝する。他にも多々。
基本的な動きは`command`と同じ。

・; 前のコマンド終了したら次のコマンドを実行する。

$ hostname; date
tmin_Arch
2018815日 水曜日 23:15:39 JST

ちなみにワンライナーでforとかwhileとかの繰り返しを使うときは、シェルで使うときの改行の代わりにこれを使ったりする。

$ for i in {1..100} ;do touch $(hostname)_$(date +_%Y%m%d)_$i.txt ;done

シェル的には

for i in {1..100} ; do
  touch $(hostname)_$(date +_%Y%m%d)_$i.txt
done

みたいな感じ。

・| 前のコマンドの結果を後ろに渡す。cat した結果を後ろに渡してgrepしてそれを後ろに渡してawkで整形したり、
findで探したファイルを| xargsで後ろに渡して後ろのコマンドの引数にしたりする。

■よく使うコマンド(※他にも多々あり)

cat ファイルの中身を見る。というか、ファイルの中身を標準出力に表示する。
grep 指定の検索文字に引っかかった行を表示する。ちなみに-Eで正規表現でor検索とか、-vで除外とか色々できる。
   fgrepとかegrep使えよって話もあるけどめんどいのであれ。
find 指定条件のファイル(or ディレクトリ)を探す。名前で絞ったり、時間で絞ったり色々できる。
xargs パイプと組み合わせて使うと、前のコマンド結果が複数のときにうまいこと後ろのコマンドの引数として渡してくれる。
awk 出力の整形、特にnカラム目を抜き出すのによく使う。
sed 出力の整形、特にある文字列を置き換えたり削除したりするのによく使う。
echo 文字列を標準出力に表示する。だけのはずだが変数食わせたりコマンド結果食わせたりすることもできる。
ls 指定ディレクトリの中身の一覧を出す。ls | grep hoge* とかしたのをxargsで渡してううんたらかんたら
tail 最後のn行の出力
head 最初のn行の出力

■マスターすると?

※メリット
・シェルっぽいことをシェル作らなくてもできる。のでシェルを残しにくい環境で一撃でなにかの処理を終わらせることができる。
・シェルっぽいことをしぇる作らなくてもできる。のでシェルスクリプト規約とかめんどくせーっていう環境で「これはコマンドです!!」と言い張ることができるかもしれない。
・ループ処理とか使うと同じようなコマンドを何十回と打って、みたいなケアレスミスが起きやすい状況を回避できるかもしれない。
・このコマンドの結果をここに代入して・・・の繰り返しみたいな手順を簡略化できるかもしれない。
・楽しい。なんかかっこいいかもしれない。
・達成感が得られるかもしれない。

※デメリット
・再現性に乏しいため、エビデンス残してないともっかい作るのがめんどい。
・複雑になってくると自分であとから読み返して解読に時間がかかるときが稀によくある。
・長くなってきたときにちゃんと検証しないと途中でなにか間違ってたときに想定外の動きをする可能性が増える。

やってみよー1

シェル芸の定義 – 上田ブログ

$ curl http://blog.ueda.asia/?page_id=1434 2> /dev/null | grep -A 1 h2 | tail -n 1 | sed 's;;;g' | tr -d '[:space:]' | awk '{print}'
マウスも使わず、ソースコードも残さず、GUIツールを立ち上げる間もなく、あらゆる調査・計算・テキスト処理をCLI端末へのコマンド入力一撃で終わらすこと。

curl先変わっててうまく動かないので、書き直してみたよ。

awkで整形したの

$ curl https://b.ueda.tech/?page=01434 2> /dev/null | grep -A 1 'id="1.1"' | tail -n 1 | awk -F '>' '{print $2}' | awk -F '<' '{print $1}'
マウスも使わず、ソースコードも残さず、GUIツールを立ち上げる間もなく、あらゆる調査・計算・テキスト処理をCLI端末へのコマンド入力一撃で終わらすこと。あるいはそのときのコマンド入力のこと。

sedで整形したの

$ curl https://b.ueda.tech/?page=01434 2> /dev/null | grep -A 1 'id="1.1"' | tail -n 1 | sed -e 's/<p>//' -e 's/<.p>//'
マウスも使わず、ソースコードも残さず、GUIツールを立ち上げる間もなく、あらゆる調査・計算・テキスト処理をCLI端末へのコマンド入力一撃で終わらすこと。あるいはそのときのコマンド入力のこと。

curlしたのをh2でgrepするともう一つ引っかかるものがあるため、id="1.1"でgrepして-A 1で後ろ一行。

$ curl https://b.ueda.tech/?page=01434 2> /dev/null | grep -A 1 'id="1.1"'
<h2 id="1.1">シェル芸の定義バージョン1.1</h2>
<p>マウスも使わず、ソースコードも残さず、GUIツールを立ち上げる間もなく、あらゆる調査・計算・テキスト処理をCLI端末へのコマンド入力一撃で終わらすこと。あるいはそのときのコマンド入力のこと。</p>

tail -n 1 で定義の方

タグで囲まれた部分を抽出。

$ curl https://b.ueda.tech/?page=01434 2> /dev/null | grep -A 1 'id="1.1"' | tail -n 1 |
<p>マウスも使わず、ソースコードも残さず、GUIツールを立ち上げる間もなく、あらゆる調査・計算・テキスト処理をCLI端末へのコマンド入力一撃で終わらすこと。あるいはそのときのコマンド入力のこと。</p>

あとは<p></p>をなんとか削除する。

awk版はawkで区切り文字を>指定して2カラム目を抜き出す&区切り文字<を指定して1カラム目を抜き出す。
sed版はsedで単純に<p>と</p>を一個ずつ削除。

正規表現自由自在マンならもうちょいエレガントにかけるかも。

やってみよー2
ディスクのパーティション情報一括取得ワンライナー
/dev/sd*で認識されてるsd*を抽出→while read LINEで出力をLINEに格納してループ開始。
→echoで$LINEを出力 ; partedにわたしてMB単位で情報出力。

# ls /dev/sd* | grep -v [0-9] | while read LINE ; do echo "******$LINE******" ; parted $LINE unit mib print ;done
******/dev/sda******
モデル: ATA WDC WDS120G1G0A- (scsi)
ディスク /dev/sda: 114473MiB
セクタサイズ (論理/物理): 512B/512B
パーティションテーブル: gpt
ディスクフラグ: 

番号  開始       終了       サイズ     ファイルシステム  名前                          フラグ
 1    1.00MiB    451MiB     450MiB     ntfs              Basic data partition          hidden, diag
 2    451MiB     551MiB     100MiB     fat32             EFI system partition          boot, esp
 3    551MiB     567MiB     16.0MiB                      Microsoft reserved partition  msftres
 4    567MiB     113970MiB  113403MiB  ntfs              Basic data partition          msftdata
 5    113971MiB  114472MiB  501MiB     ntfs                                            hidden, diag

******/dev/sdb******
モデル: ATA WDC WD10EARS-00Y (scsi)
ディスク /dev/sdb: 953870MiB
セクタサイズ (論理/物理): 512B/512B
パーティションテーブル: msdos
ディスクフラグ: 

番号  開始       終了       サイズ     タイプ    ファイルシステム  フラグ
 1    1.00MiB    100001MiB  100000MiB  primary   ext4              boot
 2    100001MiB  103801MiB  3800MiB    primary   linux-swap(v1)
 3    103801MiB  900993MiB  797192MiB  primary   ext4
 4    900993MiB  953869MiB  52876MiB   extended
 5    900994MiB  953803MiB  52809MiB   logical   ext4

******/dev/sdc******
モデル: ATA Hitachi HDS72101 (scsi)
ディスク /dev/sdc: 953870MiB
セクタサイズ (論理/物理): 512B/512B
パーティションテーブル: msdos
ディスクフラグ: 

番号  開始       終了       サイズ     タイプ   ファイルシステム  フラグ
 1    0.03MiB    100006MiB  100006MiB  primary  ntfs              boot
 2    100006MiB  953869MiB  853863MiB  primary  ntfs

******/dev/sdd******
モデル: ATA ST3500320AS (scsi)
ディスク /dev/sdd: 476939MiB
セクタサイズ (論理/物理): 512B/512B
パーティションテーブル: gpt
ディスクフラグ: 

番号  開始      終了       サイズ     ファイルシステム  名前              フラグ
 1    1.00MiB   513MiB     512MiB     fat32             EFI System        boot, esp
 2    513MiB    8705MiB    8192MiB    linux-swap(v1)    Linux swap
 3    8705MiB   59905MiB   51200MiB   btrfs             Linux filesystem
 4    59905MiB  476939MiB  417034MiB  btrfs             Linux filesystem

それではみなさん、よいbash lifeを!!