動機
次のような特定の日時に任意のスクリプトを実行できる at
コマンドというものがある:
at [-V] [-q queue] [-f file] [-u username] [-mMlv] timespec
しかし次の点を不便に感じていた:
- スクリプトファイルを入力とするため、実行コマンドと引数を直接書けない
- 対話的にジョブスクリプトが書けるのでそこでコマンドを書くことはできる
- 日付フォーマットが馴染まない
-
MMDDYY
で書く必要があるが、日本人なら年月日の順で書きたい
-
-
atd
デーモンが必要で、ただ待ちたいだけなのに大袈裟
sleep
コマンドや timeout
コマンドのようなシンプルなものが欲しかった。
方針
clock_nanosleep
(man) という、指定日時まで待つまんまな API があるためこれを使う。
また、簡単のため日付のフォーマットは秒にする。日付フォーマットに不満を述べたばかりだが、日付を扱うのは簡単ではない。gnulibを取り込めば date
コマンドで使われている parse_datetime
が手に入るが、拡張子が .y
な時点で億劫だしライセンスも縛られてしまう。とりあえず秒にしておけば date
コマンドで日付から秒に変換出来る。
コマンドの実行は execvp
(man) で行う。argv
をコマンドの開始位置までずらせばいいだけなので簡単。
作った
duntil @エポック秒 コマンド
で指定日時にコマンドを実行できる。
duntil +ウエイト秒 コマンド
で一定の秒数待ったあとにコマンドを実行できる。
#include <cerrno>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <limits>
#include <time.h>
#include <unistd.h>
namespace {
void usage()
{
std::puts("usage: duntil @POSIXTIME COMMAND [ARGS..]");
std::puts(" duntil +SECONDS COMMAND [ARGS..]");
}
// double で表現可能な time_t の上限
// time_t が32ビットの環境と64ビット環境で概ね動けば良い
const double k_time_t_limit =
double(std::numeric_limits<time_t>::max())
* (1.0 - std::numeric_limits<double>::epsilon());
// dateStr を解釈して t と isAbsolute を返す
int parseDate(const char* dateStr, timespec& t, bool& isAbsolute)
{
switch ( dateStr[0] ) {
case '+':
isAbsolute = false;
break;
case '@':
isAbsolute = true;
break;
default:
return EINVAL;
}
dateStr += 1;
char* next = nullptr;
double secs = std::strtod(dateStr, &next);
if ( errno == ERANGE ) {
return ERANGE;
}
if ( next[0] != '\0' ) {
return EINVAL;
}
double isecs = 0;
double mod1 = std::modf(secs, &isecs);
if ( isecs < 0 || k_time_t_limit < isecs ) {
return ERANGE;
}
t.tv_sec = time_t(isecs);
t.tv_nsec = long(mod1 * 1000000000.0);
return 0;
}
} // namespace
int main(int argc, char* argv[])
{
if ( argc < 3 ) {
usage();
return 1;
}
const char* dateStr = argv[1];
argv += 2;
// 時刻を解釈
timespec date{};
bool isAbsolute = false;
int err = parseDate(dateStr, date, isAbsolute);
if ( err ) {
std::fprintf(stderr, "%s\n", std::strerror(err));
usage();
return 1;
}
// 指定された時間まで待つ
int flags = isAbsolute ? TIMER_ABSTIME : 0;
if ( clock_nanosleep(CLOCK_REALTIME, flags, &date, NULL) ) {
std::perror("clock_nanosleep");
return 1;
}
// コマンドを実行
int status = execvp(argv[0], argv);
if ( status == -1 ) {
std::fprintf(stderr, "%s\n", std::strerror(errno));
}
return status;
}
肝は clock_nanosleep
の1行がやってくれるため、コードの大半は parseDate
になっている。前述したように parseDate
と言いつつ単位は秒だ。ここを拡張すれば多彩な日付フォーマットを受け付けることもできる。
使用例
2022年12月25日0:00になったら「Merry Christmas!」を出力する:
$ duntil @$(date +%s -d "2022-12-25 0:00") echo "Merry Christmas!"
日付を秒に直すのは date
コマンドにやらせている。
3分後に「ラーメン出来た!」を出力する:
$ duntil +180 echo "ラーメン出来た!"
あとがき
at
コマンドよりシンプルにした結果、at
+ atd
のコード より1/10以下の分量で不満を大きく改善出来た。デーモンを動かす必要がないためセキュリティの心配も少ない。
sleep
コマンドや timeout
コマンドのように小粒で使いやすいものになったと思う。
上記ソースコードは https://github.com/yoffy/duntil で公開している。