読者です 読者をやめる 読者になる 読者になる

週報のひな形ファイルをPerlとシェルスクリプトで生成する(sentaku, peco)

前提: 最近の週報システム

昨年3月から自チームで進行中の週報システム、見事に継続1年を達成しました。
メンバーの皆さん、おめでとう&ありがとうございます。

その週報、毎週アンケート的に5つの質問に答えてもらう感じでやっていますが、内容的にはおよそこんな。

投稿日: 2016-03-31
対象期間: 2016-03-24 - 2016-03-30
名前: **

  • Q1. 前回立てたタスクのうち完了したものを書いてください。
  • Q2. 前回立てたタスク以外の完了したことを書いてください。
  • Q3. 前回立てたタスクのうち完了しなかったものを書いてください。
  • Q4. これから一週間のうちにやることを書いてください。
  • Q5. これまでの作業、または今後行う作業に関して、上記以外のことを2個以上書いてください。

Q1, Q2が完了事項。Q3が未達成事項。Q4が次週の予定。Q5が備考というか雑感というか、懸念や不安、自分を含むメンバー礼賛などのいわゆる「その他」事項。

冒頭の日付なども含めて、基本このようなテンプレートがあって、毎週これを提出してもらった後、僕の方で次週分のテキストファイルを人数分(現在は僕を含めて4名)、その最新提出分のファイルをコピーして作っておきます。

メンバーはそれができ次第、好きなタイミングでいつでも次週分を書き始めてヨシ。
まあ実際には、皆さん大抵、提出日(毎週木曜)に書いている感じだと思いますが(笑)僕はけっこうマメに&数日おきに書き溜めています。その方が負担が少ないので。

上記のように、週報を書くテキストファイルは、前週分のファイルをコピーしたものなので、書く人は前週書いた回答を消して(上書きして)今週分の内容を書いていくことになります。
こういう方式をとるのは、テンプレートや日付を最大限そのまま生かせるから、ということもあるけど、それ以上にメンバーそれぞれが前週の提出内容を見ながら書くことによって、自分の中で「差分」を意識できると思うから。

本来なら、必要なのは上記5項目の質問だけなので、時間のあるときに数カ月分先までファイルを作っておく、ということも可能ではあるのだけど、やはりその「前週分の内容を見ながら&消しながら今週分のを書く」という作業は地味に意味があるかなあ、と。

あと、先々の分を一気に作ってしまうと、途中で上記のテンプレ質問内容をちょっと修正したい、というときに(けっこうよくある)、その時点でもう前もって作っておいたファイルは全部作り直しになってしまうので、その意味でも結局、毎週新たに作ったほうが効率いい、というのが現時点での印象です。

やりたいこと&ボツ案: MacのFinderからコピペ

さて、そこまでが前置きで、とはいえこれを毎週、それぞれのメンバーのファイルから新たにコピーしていく、というのはもちろん面倒なことで。

あらためて要件を整理すると、たとえば以下の4ファイルを元に、

2016-03-24_Aさん.txt
2016-03-24_Bさん.txt
2016-03-24_Cさん.txt
2016-03-24_Dさん.txt

その1週間後の日付の4ファイルを作る、ということなので、イメージ的にはこんな。

2016-03-24_Aさん.txt → 2016-03-31_Aさん.txt
2016-03-24_Bさん.txt → 2016-03-31_Bさん.txt
2016-03-24_Cさん.txt → 2016-03-31_Cさん.txt
2016-03-24_Dさん.txt → 2016-03-31_Dさん.txt

で、これを実現するための一番単純&案外ラクな方法は、MacのFinderから手でコピペすること。
対象となる4ファイルを選択してコピペすると、こんなふうに……

2016-03-24_Aさん.txt → 2016-03-24_Aさん.txt のコピー
2016-03-24_Bさん.txt → 2016-03-24_Bさん.txt のコピー
2016-03-24_Cさん.txt → 2016-03-24_Cさん.txt のコピー
2016-03-24_Dさん.txt → 2016-03-24_Dさん.txt のコピー

「〜のコピー」という無粋なファイル名のコピーができるので、それを元にちまちまと、

2016-03-24_Aさん.txt のコピー → 2016-03-31_Aさん.txt

という感じで書き換えていく。

まあ、これが30人分とか200人分とかだったら明らかに死ぬにせよ、現状チームの4人分程度ならこれでも良いと言えばいい。
良いのだけど、しかしこれの最大の問題は、やってて「つまらない」ということ。

砂漠に水をまくような、深い穴を掘ってからその土を穴に戻すことをひたすら繰り返させる拷問のような、とりあえず人生が苦しくなっていく作業です。

代替案: コマンドラインシェルスクリプトのfor文を回す

で、そんな小さな作業でも何とか面白味を見出すために、いろいろ調べて実践してみた代案が、シェルスクリプトでfor文を打ち込んでみる、というもの。

具体的には、こんな。

$ for i in `ls` ; do cp $i ${i/24/31} ; done 2>/dev/null

これを当該ディレクトリ内でコマンドラインから打ち込むと、上記で希望した生成が一発で出来る。

ちなみに最後の「2>/dev/null」は、検索語である「24」にマッチしないファイルがあった場合、その数だけ「not copied」が出力されて大変煩わしいので、それを消すため*1

ただ、これにも問題があって、一番困るのは入力内容が長いこと。

毎週やる、というのがけっこう曲者で、週一だけということは、普段の業務ではシェルスクリプトを使うことなんてほぼないので、構文を忘れる。忘れるから、毎回記憶をたぐってじわじわ書くことになり、かつそれが通らなければぐぐって構文を調べることになる。効率が悪い。

だったら、そのワンライナーをbashrcにエイリアスとして仕込んでおけばいいかも? と一瞬思ったりしたものの、マッチ&置換させたい語(おもに日付)は毎週変わるので、そんなものをどうエイリアス化したらいいのか、わからない。

改善案: Perlスクリプトファイル作成

ということで、それならもうスクリプトファイルを作ってしまって、それをエイリアスで短いコマンドに仕込めば、毎週最小限の打鍵で呼び出して一瞬で終わるのでは、と考えた。

上記の変数要素(検索&置換語)も、スクリプトファイルなら引数から読み込んで指定できそう、とか。
(と、ここまで書いて、それだったらシェルスクリプトの関数をbashrcに書いても同じことできそう……と今思ったが、気づかなかったことにしてこのまま書く)

で、最初はそもそもシェルスクリプトワンライナーで済ませようと思っていたぐらいだから、そのファイルもシェルスクリプトで書こうと思ったのだけど、僕は今までまとまったシェルスクリプトってほとんど書いたことがなく、かつ業務は業務で普通に詰まっているので、このためだけにイチから書き方を調べていくのもちょっとツライ。

ということで、あっさり「じゃあPerlで」という判断に落ち着いた。

最初はこんな感じのを書いたのだけど、

#!/usr/bin/env perl
use 5.12.0;
use warnings;
use Path::Tiny;

my $iter;
my $dir = '.';
my $last_dir;
my (@file, @dir, @other);

# function:

sub iter {
    my $key = shift;
    (@file, @dir, @other) = '';
    $iter = path($dir)->iterator;
    while (my $ls = $iter->()) {
        next if ($ls =~ /^\./);
        if (-f $ls) {
            push @file, "\tfile: $ls\n";
        } elsif (-d $ls) {
            push @dir, "\tdir: $ls\n";
            $last_dir = $ls;
        } else {
            push @other, "\tother: $ls\n";
        }
    }

    say 'ls:';
    if ($key eq 'dir') {
        print @dir;
    } else {
        print @dir;
        print @file;
        print @other;
    }
    print "\n";
}

# check current dir & move target dir:

iter('dir');
say "Put a dirname or Enter('.').";

$dir = <STDIN>;
if ($dir =~ /\A\n\z/) {
    $dir = $last_dir;
} elsif ($dir eq 'n') {
    say "no words\n";
} else {
    chomp $dir;
}

say "\npwd:";
my $abs = path($dir)->absolute;
my $parent = path($abs)->parent(3);
my $rel = path($abs)->relative($parent);
say "\t$rel";
iter('all');

# generate or read:

say "Put two words.";

my $catch = <STDIN>;
unless ($catch =~ /\A\n\z/) {
    chomp $catch;
    my ($before, $after);
    if ($catch =~ /\A(\S+) (\S+)\z/) {
        $before = $1;
        $after = $2;
        my $iter = path($dir)->iterator;
        while (my $path = $iter->()) {
            my $basename = $path->basename;
            if ($basename =~ /$before/) {
                my $new = $basename;
                $new =~ s/$before/$after/;
                path("$dir/$basename")->copy("$dir/$new");
            }
        }
    } else {
        die "Can't open file:$!";
    }
} else {
    say "no words\n";
}
iter('all');

使ってる様子は、こんなで。

f:id:note103:20160327021733g:plain

やっていることを簡単に解説すると、今回想定しているデータ群は、その動画のように(ここで使ってるのはデモ用に用意したサンプル)、まずメインディレクトリの中に月ごとに分かれたサブディレクトリがあって、その中から対象のディレクトリに入る。

入ったらその中のファイル名を一覧表示させて、それを見ながらコピー元の数字「24」とコピー先の「31」を指定することにより、「2016-03-24*」と同内容のファイル群「2016-03-31*」を生成している。

コードの方は恐ろしく冗長だと思うものの、それでもやりたいことはやれているし、とりあえず問題ないってぐらいまで作れたのでけっこう満足したのだけど、しかし今度は、ふと「これ……sentaku使ってディレクトリ選べるようになったら面白そうだな……」と思ってしまって。

「sentaku」というのは、以下の記事でも紹介した、「trash.sh」というツールでも使われているコマンドラインツールで。
note103.hateblo.jp

レポジトリはこちら。
github.com

詳しくは、作者さんのブログの「sentaku」タグを追うとわかると思います。
Tag: sentaku

ということで、今度はその外部ツールを組み込む方法を考え始めたのだけど、今までPerlのコードの中に他人が書いたツール、それもPerl以外で書かれたそれを連携させるなんてやったことないし……と、自分の無茶振りを前にけっこう試行錯誤したのですが、その逡巡の経過はばっさりカットして、できたのは以下。

結論(ひとまず): Perl + sentaku (peco)

#!/usr/bin/env perl
use 5.12.0;
use warnings;
use Path::Tiny;

my $iter;
my $dir = '.';
my $last_dir;
my (@file, @dir, @other);

# function:

sub iter {
    my $key = shift;
    (@file, @dir, @other) = '';
    $iter = path($dir)->iterator;
    while (my $ls = $iter->()) {
        my $basename = $ls->basename;
        next if ($basename =~ /^\./);
        if (-f $ls) {
            push @file, "\tfile: $ls\n";
        } elsif (-d $ls) {
            push @dir, "\tdir: $ls\n";
            $last_dir = $ls;
        } else {
            push @other, "\tother: $ls\n";
        }
    }

    say 'ls:';
    if ($key eq 'dir') {
        print @dir;
    }
    elsif ($key eq 'file') {
        print @file;
    }
    else {
        print @dir;
        print @file;
        print @other;
    }
    print "\n";
}

# check current dir & move target dir:
## sentaku ver.
$dir = `if [ -z "\$( ls -F | grep / )" ]; then echo '.'; else ( ls -F | grep / | sentaku -s '\n'); fi`;

## peco ver.
#$dir = `if [ -z "\$( ls -F | grep / )" ]; then echo '.'; else ( ls -F | grep / | peco); fi`;

unless ($dir eq '') {
    chomp $dir;
    $dir =~ s/\/$//;
} else {
    $dir = '.';
}

# pwd & ls:

say "\npwd:";
my $abs = path($dir)->absolute;
my $parent = path($abs)->parent(3);
my $rel = path($abs)->relative($parent);
say "\t$rel";
iter('file');

# generate or just read:

say "Put the words before & after.";

my $get = <STDIN>;
unless ($get =~ /\A\n\z/) {
    chomp $get;
    my ($before, $after);
    if ($get =~ /\A(\S+)(( (\S+))+)/) {
        $before = $1;
        $after = $2;
        my @after = split / /, $after;
        my $iter = path($dir)->iterator;

        while (my $path = $iter->()) {
            my $basename = $path->basename;
            if ($basename =~ /$before/) {
                for (@after) {
                    my $new = $basename;
                    next if ($_ eq '');
                    $new =~ s/$before/$_/;
                    path("$dir/$basename")->copy("$dir/$new");
                }
            }
        }
    } else {
        die "Can't open file:$!";
    }
} else {
    say "Nothing changes.\n";
}
iter('all');

はい。結局、連携部分はバッククォートでシェルスクリプトを囲んだだけですが。

プラス、これって用途から考えるとsentakuの部分をpecoにしても同じことができるな……と、途中で気づいたので、peco版も近いところにコメントアウトして入れてあります。

使ってる様子は、こんな。(sentaku ver.)

f:id:note103:20160327022057g:plain

コマンド名は、先の動画では「sample」としていましたが、ここからは「copy + generate」で「cpg」としています……。

まあ実際には、これってあくまで「想定内の条件下で、想定内の使い方をしている分には大丈夫」というものに過ぎないので、ちょっとでも変なことをやったらすぐクラッシュするのは明らかなのですが、とはいえとりあえずFinderでコピペしたり、ワンライナーを毎週思い出しながら&調べ直しながらやるよりは、パパッとコマンド入れて進められるのでいいかなと。

またそのクラッシュ要素というか、問題点についても一応いくつかピックアップしてあるので*2、いずれまた趣味の時間が来たら継続課題として取り組んでみたいと思い中です。

*1:後出のGIF動画で何となく見て取れると思いますが、現在のディレクトリ構成は月単位でテキストファイルをまとめているので、単純計算で1つのディレクトリに最大4人*4〜5週間=16〜20本のファイルが入ることになる。生成時の対象ファイルは4本(4人分)なので、その時点で他のファイルがあった場合はそのすべてに対して「not copied」が出てくる、ということ。

*2:途中で書いた、シェルスクリプトで全部済ませる案もクリアしておきたい。