C言語の航海日誌(2)

今日は3点のサンプルコードを写経しました。

一つは以下。

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    int score[100];
    int status;
    int temp_score;
    int score_count;
    int i;

    /* 配列に値を入力する */
    score_count = 0;
    while (1) {
        status = scanf("%d", &temp_score);
        if (status == EOF) {
            break;
        } else if (status != 1) {
            fprintf(stderr, "入力エラーです\n");
            exit(1);
        }
        if (score_count >= 100) {
            fprintf(stderr, "データ件数が多すぎます\n");
            exit(1);
        }
        score[score_count] = temp_score;
        score_count++;
    }

    /* 配列の内容を表示する */
    for (i = 0; i < score_count; i++) {
        printf("score[%d]: %d\n", i, score[i]);
    }

    return 0;
}

動かすとこんな感じです。

f:id:note103:20160520012215g:plain

(ちなみにコマンドプロンプトの「2$」の「2」はカレントディレクトリ。今は第2章のサンプルコードを書いているので「2」というディレクトリ内で作業している)

「8, 9...」というふうに数字を入れていくのは前回同様ですが、今回は終了の合図を「-1」ではなく、EOFで示しています。
上記DEMOでは、標準入力からコントロール+DでEOFを伝えています。

ファイルのリダイレクトで同様のことする場合は、こんな感じ。

f:id:note103:20160520012814g:plain

標準入力版に戻って、数字ではなく文字を入れてしまうと……

f:id:note103:20160520012852g:plain

はい。

そのEOFですが、Perlだとあまり聞かないなあ、せいぜいヒアドキュメントの時に例としてそういう文字列が出てくるぐらいだけど、じゃあPerlで同様にEOFを示したい場合ってどうするんだろ?

と思ったんですが、ざらっと調べたかぎりでは、「Perlでも一応 eof という組み込み関数が用意されているけど、勝手にEOFを認識して終わってくれるから使うことはほとんどない」みたいな説明が多く、まあ確かにそのようにも思えるんだけど、じゃあ「ファイルが終わったら(あるいは標準入力からの入力が終了したら)メッセージ出してから終わらせたい」と思った場合どうするんだろ? みたいな疑問が湧いてきて……

こんなコードを考えてみました。

#!/usr/bin/env perl
use strict;
use warnings;
use feature 'say';

print ">>> ";
while (my $t = <STDIN>) {
    print $t;
    print ">>> ";

    say 'bye' if (eof);
}

動かすと、こんな感じ。

標準入力版

f:id:note103:20160520014920g:plain

※「7」を入力した後にコントロール+Dを入れています。

リダイレクト版

f:id:note103:20160520015031g:plain

まあ確かに、特殊といえば特殊、あまり使わないケースかもしれないですが……。

とはいえ、これまで知っているようでまったく理解していなかったEOF/eofについて(図らずもPerlを含めて)知見を得られて良かったです。

今日はその他、プリプロセッサとマクロの違いについて学べるサンプルコードなども写しましたが、たしかに言われたとおりに動いていることは確認できたものの、ちょっと難しかったかな……。

次回からは上記のサンプルコードをさらに発展させた、ソートをテーマにしたコードに入っていきそうです。
今日の記事では触れませんでしたが、今回のサンプルコードからfprintfを使ったり、scanfの戻り値を利用するようになったりと、ページが進むごとに結構目覚ましくトピックが増えていくので、次も楽しみ&大変そう……な感じですが。

C言語の航海日誌(1)

以下にも書いたように、
note103.hateblo.jp

最近はC言語の学習を進めています。

といっても、実際にはそれができるほど気持ちに余裕のある時間というのはなかなか無くて、業務をしなくていい時間があってもついダラダラしてしまいがちだったりするので、ちょっとでも進捗があればブログに残しておこうかなと。

今日(5/18・水)は以下の本から、2本、サンプルコードの写経をしました。

C言語体当たり学習 徹底入門 (標準プログラマーズライブラリ)

C言語体当たり学習 徹底入門 (標準プログラマーズライブラリ)

進捗的には、全P340ぐらいの本で、今日終わったのがP85ぐらい。
すでに一回通読しているので、前に読んだときに比べれば馴染みを感じる気もします。
というかまあ、一度Perlの基礎を見回っているので、そんなにハマりまくるということもないのですが……。

と言って早々にナンなのですが、今日写経したサンプルコード、すごくシンプルなんだけどそれでも実はハマって。

写したのはこんな感じ。

2-3.c
#include <stdio.h>

int main(void)
{
    int score[100];
    int score_count;
    int temp_score;
    int index;

    /* 配列に値を入力する */
    score_count = 0;
    while (1) {
        scanf("%d", &temp_score);
        if (temp_score == -1) {
            break;
        }
        score[score_count] = temp_score;
        score_count++;
    }

    /* 配列の内容を表示する */
    index = 0;
    while (index < score_count) {
        printf("score[%d]: %d\n", index, score[index]);
        index++;
    }

    return 0;
}

で、ほとんどこれ本にあるままぴったり書いているのだけど、なぜか思うように動かない……。

f:id:note103:20160519013325g:plain

というところで、しばらくエディタと本とを行ったり来たり、交互に見返して、ん〜……どこも間違ってないように見えるが……と思いながら数字を入れてみると、

f:id:note103:20160519013705g:plain

ああ、できてる。

ようするに、最後に「-1」を入れたら終端の印、という仕様で書いているコードだとはわかっていたんだけど、そもそもこれは整数を受け取る前提で書かれていた、ということを完全に忘れていたんですね〜……。
だから普通に文字列で「test」とか入れても動いてくれなかった、という。

ちなみに、上記DEMOで「2-3.c」というファイル名が出てくるのは、同書のサンプルコードに付けられた番号です。
これは第2章の3つめのサンプルコードなので、「2-3」。

これがこのまま第6章の「6-3」まで行くとひとまず1冊終わりです。
(1章あたりのサンプルコードの数はまちまち。第2章だと7個ある)

今日は「2-3」と「2-4」を写した&実行したので、次の機会でとりあえず第2章のぶんを終わらせたい……。

バッチ処理で複数ファイルを一気にリネームするPerlスクリプトを書いた

昨日の業務中、「普段はあまり遭遇しないがまれに生じる面倒な手作業」に遭遇し、「あ〜この作業、前から専用スクリプトで一括で済ませたいと思ってたんだよな〜」と思ったので、一旦作業の手を止めて猛然とコードを書いてみたら、それで問題をクリアできてしまった。すごい。

普段ならわざわざ作業の手を止めてまでコードを書くなんてことはなくて、「あ〜これコードで自動化したい……けどそんなの作り始めたらいつ終わるかわかんないから、残念だけど今はとりあえず手でやってしまって、あとで時間ができたらこれをお題に書いてみよう〜」なんて後回しにするものの、実際には当の問題が解消されてしまうとなかなかそのモチベーションも湧かなくて、結局いつまでたっても「あ〜これいつもの面倒なやつ……でも今はとりあえず手でやっておいて……」などと同じつらさの繰り返しに陥るところ、どうもこの数ヶ月は自分で書いたツールを使う機会が以前よりだいぶ増えてきたので、やればできるかなあ、という淡い自信に後押しされてガッと作ってみた。

やったことは大まかに言って、最近自分の中で流行ってる「ファイルのリネーム」なのだけど、以前にここで紹介した同類のツールだと、
note103.hateblo.jp

あまり大がかりな変更はできないというか、基本的には数文字ちょろっと変えたい、みたいな用途で生きてくるものなので、今回の目的からはちょっとズレていた。

じゃあ今回の目的って何だったのか、ということを少し具体的に説明すると、たとえば以下のようなファイル群があるとして、

01-06 りんご.mp3
2 ばなな.m4a
14-03 みかん.mp3
58 れもん.txt
108 ぶどう.md

これを以下のようにリネームしたい、みたいな。

1_りんご.mp3
2_ばなな.m4a
3_みかん.mp3
4_れもん.txt
5_ぶどう.md

ファイルの種類(mp3, txt, mdなど)はそのままで、名前もほとんど同じなんだけど、変更する際の規則性が弱いので、これを上記のツールでやろうとするとそれはそれで面倒というか、この状況ならコード使わずに手でやっちゃったほうがマシ……という感じにどうしてもなってしまう。

とはいえ、上記はあくまでサンプルなのでほんの数ファイルしかないけれど、昨日の本番状況ではこういうリネームしたいファイルが20点以上あったので、これ……このまま手でやるのは不毛すぎる……頑張って終わらせてもあまり達成感なさそう……というか人間がやってはいけない……などと思ってコードに向かってみたという。

実践編

以上のような前提を踏まえて、まず考えたのは、今回は以前のような標準入力方式(ターミナルに対話形式で打ち込むやつ)ではなく、リネーム後の文字列を事前にファイルに記述しておいて、それを読み込ませて動かすバッチ処理方式でいくべきだろう、ということ。

で、そのためにすぐ着手できそうなのは、とりあえず変換後のテキストファイル(バッチファイル)を作ること。

手順としては、上述のこの情報をもとに、

01-06 りんご.mp3
2 ばなな.m4a
14-03 みかん.mp3
58 れもん.txt
108 ぶどう.md

これが書かれたファイルを作る。

1_りんご.mp3
2_ばなな.m4a
3_みかん.mp3
4_れもん.txt
5_ぶどう.md

ということで、とりあえずFinderで該当ファイルを選択&コピーして……

f:id:note103:20160518004435p:plain

エディタにペーストすると……

f:id:note103:20160518004501p:plain

おっと……なにこの崩れ……。

って、ああ〜、それか〜……って。
以前、ここでちらっと呟いたのだけど、

いつも『CPANモジュールガイド』でお世話になっていますトミールさんの以下のブログ記事で解説されている、「UnicodeのNFDという正規化」の問題みたい。

Macのファイル名はNFDされている

MacOSXでは文字コードutf-8エンコーディングが使われているのだけど、UnicodeのNFDという正規化がされている。

具体的には、「だ」みたいな濁点つきカナが、「た」+「゛」の二つのUnicodeで表現されたりしてる。゛は「てん」で変換してでる濁点マーク(U+309B)ではなく、Unicodeで組み合わせる用として用意されている点(U+3099)。

Macだと時々これに遭遇して、ぐむむ……ってなる。
他のケースでありがちなのが、そのTwitterでも書いているけどPDFからテキストをコピペした時とか。

で、じつは今回の件においては、この崩れは見なかったことにして進めてしまってもとくに問題なかったりするのだけど、上記のトミールさんの記事をもとにこの崩れを直すコードも書いてみた。

実行している様子*1

f:id:note103:20160518004821g:plain

閑話休題。ではあらためて、そのように抜き出したテキストを元に、変更後のテキストを作成。

1_りんご.mp3
2_ばなな.m4a
3_みかん.mp3
4_れもん.txt
5_ぶどう.md

そいつをconv.txtとかにしておいて……このような変換スクリプトを書いてみた。

動かしてみると……。

f:id:note103:20160518005217g:plain

ん、あ、ちょっと待った。

from:
	01-06 りんご.mp3
	108 ぶどう.md
	14-03 みかん.mp3
	2 ばなな.m4a
	58 れもん.txt
to:
	1_りんご.mp3
	2_ばなな.m4a
	3_みかん.mp3
	4_れもん.txt
	5_ぶどう.md

微妙にファイルの順番が違う……。

どうやら上(from:)は辞書順、つまり行頭の数字が小さい順に並んでしまっていて、数値(108とか14とか)の順番で並んでいない。
しかし前掲のようにMacのFinderで見ると、こっちは数値順で並んでるわけで……

f:id:note103:20160518004435p:plain

これを元に変換後のリストを作ったからズレたみたい。紛らわしい……けど、もうここは僕には良い方法が見つからなかったので、諦めてバッチファイルの方をそれに合わせて並べ直すことに。

こんな感じで。

conv.txt
1_りんご.mp3
5_ぶどう.md
3_みかん.mp3
2_ばなな.m4a
4_れもん.txt

ん〜、バッドノウハウ的で腑に落ちないが、これで中身がメチャクチャになることはないはず。
で、あらためて動かしてみると……。

f:id:note103:20160518005640g:plain

OK!
って、これだとちゃんと中身が同じで名前だけ変わったのかどうか、わかりづらいですが、まあ大丈夫でした。(雑)

ちなみに、元々の目的はリネームなので、元ファイル自体の名前を変えても良かったのだけど、上記のようにバッチファイル(conv.txt)の指定が間違っていた場合、元ファイルが消えてしまうとけっこう面倒なので、元ファイルはそのまま残して、目的のファイル群は別ディレクトリ内に別名でコピーするようにしています。

これなら、万一指定を間違えてもコピー先を削除して、バッチファイルを修正後にまた処理すればよいので。

あと、バッチファイル上のファイル名と、コピー元のファイル群の数が異なる場合は、指示が間違っている可能性が高いので、処理する前にdieするようにしています。

たとえば、元のファイル群が以下で(前掲ママ)、

01-06 りんご.mp3
2 ばなな.m4a
14-03 みかん.mp3
58 れもん.txt
108 ぶどう.md

バッチファイルをこんな感じにしておくと……(6番を追加)

1_りんご.mp3
5_ぶどう.md
3_みかん.mp3
2_ばなな.m4a
4_れもん.txt
6_かき.txt

こんな感じに。

f:id:note103:20160518005738g:plain

ちゃんと途中で終わってくれる。

その他、現状だとコピー先のディレクトリにすでにコピー予定のファイル名が存在していたら上書きしてしまうので、それを避けるようにしたほうがいいんじゃないかとか、あるいは現在のコードだとmp3をテキストファイルにしたり、Excelファイルを.pdfにしたりとかいう奔放なことを平気でやるので(もちろん中身はメチャクチャになる)、拡張子をチェックしてファイルの種類が違ったらエラーを出すとかの機能も考えたのだけど、今回の仕様としては完全なリネームではなくコピーなので、そこまでやらなくていいか、と。

なにより、この内容でそもそもの目的だった20数点のファイル一括リネームもできたので、ひとまずヨシとしたいと思います。

追記/リファクタリング

コメント欄にて、いつもお世話になっております @xtetsuji さんから、

  • glob関数の使い方
  • ファイル群を読み込む際に数値順で統一する方法

などについて教えて頂きました。

ありがとうございます!!

ということで、それらを参考に以下のように直してみました。

修正版コード

batch_rename2.pl · GitHub

diff

スッキリ&直感的!になりました。

動いてるところ。

f:id:note103:20160518141012g:plain

まずバッチファイルであるところの conv.txt が数値順になってることを示した後、処理をやってます。
いい感じです!

なお、 glob, sort, grep の具体的な使い方で詰まった際、こちらもいつもお世話になっています木本裕紀さんのサイトにて、理解を補いました。

Perlに関するさまざまな情報が広く&深く&わかりやすく説明されていて、大変ありがたいです。

*1:4行目の say いらなかった。

C言語、はじめました

去年のAdvent Calendarでもちらっと予告しましたが、

note103.hateblo.jp

Perlの基礎、Linuxコマンドの基礎などを一周した感じになったので、次なるターゲットとしてC言語の学習を始めています。

直近のステイタスとしては、Perlのコードを整形するperltidy というツールがあるのですが、C言語でも同様のことをできるようにしてみました。(C関係ない)

f:id:note103:20160515120806g:plain

このためにやったことは(関係ないまま続けると)、とりあえず ClangFormat というのを brew install 。

$ brew install clang-format

で、とりあえずこれでもうターミナルからこんな感じで実行すると、

$ clang-format -i  foo.c

foo.cを適当に書き換えてくれます。

この際、いくつかコーディングスタイルの選択肢があって、その辺の指定については後述しますが、じゃあこれをVimで手軽にやろう、と思ったらこちらのVimプラグインをインストール。

その上で、vimrcにこんな感じで。

map <Leader>ct :ClangFormat<CR>

これでひとまず完了&やりたいことはできるようになっていますが、より便利な使い方については作者さんによる以下をご参照ください。
rhysd.hatenablog.com

前述のスタイル指定などについても、同記事でわかりやすく触れられていますので、あわせてどうぞ。
ちなみに、僕自身は後述の書籍のサンプルコードのスタイルがどうやらWebKit系のそれに近いようなので、そうなるようにしています*1

さて本題のC言語ですが、冒頭に挙げた昨年の記事の段階では、どんな本を中心に勉強するか、みたいなことは書いていなかったのですが、実際には以下3冊を使っています。

C言語体当たり学習 徹底入門 (標準プログラマーズライブラリ)

C言語体当たり学習 徹底入門 (標準プログラマーズライブラリ)

C言語ポインタ完全制覇 (標準プログラマーズライブラリ)

C言語ポインタ完全制覇 (標準プログラマーズライブラリ)

Head First C ―頭とからだで覚えるCの基本

Head First C ―頭とからだで覚えるCの基本

で、現在は最初に挙げた『C言語体当たり学習 徹底入門』を使って、頭から順にサンプルコードの写経をしています。

写経、というのはぼくはこれまであまり重視していなかったのですが、少し前にはてブをにぎわせた以下の記事を読んで、

誰もが知ってるけど敢えて言わない確実に身につくプログラミング学習法 - WirelessWire News(ワイヤレスワイヤーニュース)

なるほど〜、やったことないけど(というか無いだけに)説得力を感じるなあ、と思って、学習に取り入れてみています。

現時点では、あまりにも頭への負荷がかからないこの感覚、果たして効果あるかあ〜? という疑念のほうが優勢なのですが、とはいえまだ10本程度写経しただけなので結論は出せません。

=追記
以下の記事にあることを留意してやるといいのかな、と今見つけて思った。
橋本商会 » プログラムの写経
関係ないけど、同記事中の初心者がエラーを読まない理由については、もしかしたら以前に書いたこれ(のp35)と補完関係になるかもしれないです。
ぼんやりした大人が趣味でプログラミングを始めたら
=追記ここまで

ちなみに、冒頭に貼り付けたGIF動画のコードもその最近写経したサンプルのひとつです。

C言語、そもそもなんで今やるの? (古い言語なのに) みたいな話については、その去年書いた記事にけっこうイヤってぐらい要点が書いてあるので、くり返す必要はないと言えばないのですが、流れ的にやはりくり返してしまうと、それは「C言語で何か作りたいから」ではなく、今触れられるプログラミング関連の話や書籍というのが、どれもこれも(すべてでは無いにせよ、その大半が)「C言語を知ってる前提で書かれているから」です。

で、今ちょっとあえて曖昧に書いたのですが、そこで言う「C言語を知ってる」のは誰か? と言うと、プログラミング関連の話をしたり、本に書いたりする人、そしてそういうアウトプットをする人が頭の中で想定している架空の受け手(話の聞き手・本の読者)です。

現在、プログラマーとして名高い人や、本などでプログラミングを教える立場にある人のうち、C言語ぜんぜん知らない、という人はほとんどいないと思います。
普段Cを書いていないとか、あまり詳しくない、という人はいるかもしれないけど、一度も書いたことないとか、まったく理解してない、という人はいないかなと。

で、そういう人の話には、否応なくCの知識というか、要素が混ざってしまっていて、その人の話を可能なかぎり理解しようと思えば、どうしてもCの知識が土台として必要になるというか、まあ無ければ無くても構わない場合もあるにせよ、知っておいたほうがいろいろラク。という印象を持っています。

つまり、C言語で何か作るためにCを勉強するのではなく、避けがたくC言語の要素に触れながら語られてしまう話を理解するためにCを勉強するという、文字通り「新たな共通言語を学ぶ目的で学習する」ということですね。

逆に言うと、もし世の中のプログラマーの大半が、「Cなんてまったく知らないし、書けないよ」という人ばかりの世界になったら、ようやくそういうことをしなくて済むかもしれないんですが、少なくとも現在はそうではないので、いろんな人の話を理解しやすいように勉強する、という。
なんというか……英語の勉強に近いものがあるかもしれないですが。

と、言いつつそれでも結局、身につけるにはその言語でなんかしら作っていかないと進まない、という気もするので、実際にはこれもPerlのときと同様、自分の生活や仕事で使える便利ツールをCで書いてみる、ということにはなるとも思うのですが。

Perlで書いたものをCで書き直す、とかもいいかもしれないですが、実際そんなことがどの程度できるものか、まだまったく未知数です。
とりあえず、今年のうちにそういう、自分が普段づかいできるツールを2つぐらい、Cで作れるといいのですが。

その他、今後の予定ですが、上に挙げた参考書3冊のうち、前橋さんによる2冊はどちらも一応最後まで通読していて、その後にヘッドファーストも通読にトライしたんだけど、p550ぐらいある本のうち、p250を越えて構造体とか共用体の話が出てきたあたりからグッと難しくなって、どうやらそれ以前の部分(とくにポインタまわり)をある程度きちんと押さえていないと、無駄に時間がかかりそうだったので、一旦ストップ。

で、あらためてその3冊の中で一番初心者向けに作られている『C言語体当たり学習 徹底入門』を、最初から読み直している感じです。
その本1冊、とりあえず全体的に理解して、少なくとも「まったくわからない部分はない(怪しいところはあるにせよ)」ぐらいになったら、次のステップに行けるかなあ、というおぼろげな期待を持っているところです。

以上、後になって「ああ〜、このとき俺、こんなこと言ってたな〜」って、自分の学習進度や差分をチェックしやすいよう、とくに進展がないにもかかわらずいろいろ細かく書いてみました。

*1:と言いながら紹介したリンク先の記事ではWebKitスタイルは紹介されてないみたい。最近追加されたのだろうか……。

Perlの正規表現で名前付きキャプチャとqr//演算子を使ってみた話

少し前にこのようなツール&記事を書いたのだけど、
note103.hateblo.jp

簡単に説明すると、「URLを渡すとMarkdownの書式でそのページタイトル&URLを返してくれる」というもの。

で、そのコードについて、Perl入学式でいつもお世話になっています @xtetsuji さんから指摘があって、この extr サブルーチンの if文にある $4 ってなに? と。

sub extr {
    my $e = shift;
    my ($x, $y);
    if ($e =~ /(.*)(\(http[:s][^\s,;>]+\))(.*)/g) {
        $x = $1 if $1;
        $y = $4 if $4;  #<-これ
        if ($x && $y) { $rest = $1.$4;  #<-これ
        } elsif ($x) { $rest = $x;
        } elsif ($y) { $rest = $y;
        }
    } elsif ($e =~ /(.*)(http[:s][^\s,;>]+)(.*)/g) {
        push @urls, $2;
        $x = $1 if $1;
        $y = $3 if $3;
        if ($x && $y) { $rest = $1.$3;
        } elsif ($x) { $rest = $x;
        } elsif ($y) { $rest = $y;
        }
    }
    extr($rest) if ($rest =~ /(.*)(http[:s][^\s,;>]+)(.*)/);
}

urls/Urls.pm at da66791eccec8fc33e0bd1c7a95994deca1c66d3 · note103/urls · GitHub

んん〜……何って……何だろう……?(笑)
自分でもわからない。というか、よくよく読むと、たぶん、というかどう考えてもその正規表現のパターン部分、

    if ($e =~ /(.*)(\(http[:s][^\s,;>]+\))(.*)/g) {

この http で始まる部分を囲う丸括弧のうち、エスケープしているはずの括弧もキャプチャ対象としてカウントしてしまって、本来存在しないはずの $4 というのを書いてしまった。ということだと思うのだけど、じゃあそれを正しく数え直して $3 にすると、今度はそのif文の中身と、その下のelsif文の中身がほぼ同じになってしまって、そのまま繰り返させてしまうのはさすがに具合が悪い、というか DRY(Don't Repeat Yourself) の原則に反するという新たな問題が……。

しかも、だからといって共通する数行をif文の外に持ち出すと、こんな感じになって、

sub extr {
    my $e = shift;
    my ($x, $y);
    if ($e =~ /(.*)(\(http[:s][^\s,;>]+\))(.*)/g) {
    } elsif ($e =~ /(.*)(http[:s][^\s,;>]+)(.*)/g) {
        push @urls, $2;
    } else {
        next;
    }

    $x = $1 if $1;
    $y = $3 if $3;
    if ($x && $y) { $rest = $1.$3;
    } elsif ($x) { $rest = $x;
    } elsif ($y) { $rest = $y;
    }
    extr($rest) if ($rest =~ /(.*)(http[:s][^\s,;>]+)(.*)/);
}

とりあえず $x, $y への代入部分の重複がなくなったのは良いけれど、$1, $3 をif/else文の外に持ち出すというのは何となくわかりづらいような……という問題にさらに加えて、正規表現のパターンがほぼ同じであるにもかかわらずこれだけ短い間に3回も繰り返し書かれていて、ちょっと綺麗じゃないような……とかなんとか。

などと思っていたら、@xtetsuji さんからのさらなるアドヴァイスで、前者については「名前付きキャプチャを使えば見やすくなるかも」、そして後者については「qr//で正規表現を事前定義しておくと良い場合がある」とのこと。

なるほど。じつは名前付きキャプチャについては、ぼくもちょうどその数日前にこんなツイートをしていたぐらいで、

もう少し実践的に使ってみたいと思っていた。

プラス、qr// についてもいつも q// や qq// や qw// との違いを思い出すのにいちいち時間がかかって面倒だったので、この機会にあらためて勉強してみよう、ということで、いろいろ情報を参照しつつとりあえずこんな感じに。

sub extr {
    my $e = shift;
    my ($x, $y);

    if ($e =~ /(?<prematch>.*)(\($regexp\))(?<postmatch>.*)/g) {
    } elsif ($e =~ /(?<prematch>.*)(?<match>$regexp)(?<postmatch>.*)/g) {
        push @urls, $+{match};
    } else {
        next;
    }

    $x = $+{prematch} if $+{prematch};
    $y = $+{postmatch} if $+{postmatch};
    if ($x && $y) { $rest = $x.$y;
    } elsif ($x) { $rest = $x;
    } elsif ($y) { $rest = $y;
    }
    extr($rest) if ($rest =~ /(.*)($regexp)(.*)/);
}

で、その後に別の部分もちょっと調整しつつ、ひとまず現状(この記事を書いている時点)の最新版はこんな感じで。

DEMO

で、その動いてるところ……は、挙動を改良したわけではないので前回と同じ動画ですが、こんな様子。

1本だけ渡す場合。
f:id:note103:20160505185803g:plain

複数のURLを渡す場合。(1行に複数のURLが入っててもOK)
f:id:note103:20160505185909g:plain

謝辞

今回も @xtetsuji さんには大変お世話になりました。ありがとうございます。 :bow:

また、このあたりのやり取りはPerl入学式の運営用Slackで行われたのですが、校長の @papix さんやその他メンバーの方々も時折絡んで&盛り上げてくれて楽しみながら学ぶことができました。重ねてありがとうございます。

で、そんなPerl入学式ですが、東京・大阪・沖縄の本年度第1回の開催日が近づいてきました。
ざっと日程だけ書き出すと、こんな感じ。

  • Perl入学式 in東京 第1回
    • 2016年5月14日(土)
  • Perl入学式 in大阪 第1回
    • 2016年5月28日(土)
  • Perl入学式 in沖縄 第1回
    • 2016年6月18日(土)

東京編はもう来週ですね。

その他、詳細については公式サイトをご参照ください。
www.perl-entrance.org

ちなみに、沖縄編に関しては、記念すべき第1回開催に向けて沖縄サポーターの方が書かれたブログ記事を絶賛校正中ですので、近日中に以下で公開される予定です。
(というか僕が止めてます……すみません)
perl-entrance.blog.jp

以上です!

ファイルやディレクトリを対話形式でcp, mv, deleteなどするツールをPerlで書いた

以前に以下の記事で、rcopy.pl delete.pl という小さいツールを作ったと書いたが、
note103.hateblo.jp

それはそれで結構重宝していたのだけど(ついでに言うと同時に紹介した dirmove というのもひき続きかなり使ってる)、似たような感じでファイルやディレクトリを簡単にリネームしたり、複数ファイルを一度に移動(mv)したりしたくなったので、それらをまとめて扱えるツールに書き換えてみた。

使い方としては、メインファイルであるところの crmd.pl と、後述の各機能を表す引数を一緒に指定する。
たとえば rcopy の機能を使う場合には、引数として「c」を付ける。

$ perl crmd.pl c

以下、簡単に各機能を説明するが、DEMO動画内では上記コマンドではなく、エイリアスでそれぞれ「c, r, m, d」を設定している。

rcopy

まずは前回紹介したコピー機能。ひとつの対象を、ひとつまたは複数の別名アイテムにコピーする。
この際、対象がディレクトリであれば、中身ごとコピーしていく。

DEMO: rcopy

f:id:note103:20160504174604g:plain

strawberryディレクトリをbanana, banana-bread にコピーしていく際、元々strawberryディレクトリに入っていたstrawberry.txtも一緒にコピーされていく。

rname

今回の変更の目玉というか、そもそもこれを使いたい状況が頻出したので改良に手を付けた。

上記のコピー機能の場合、元のファイルが当然残るので、「単にリネームしたいだけなんだよなあ」という時にはコピーした後にわざわざ元ファイルを削除しなければならず、それが面倒だった。
(その削除時にも一応、後述のデリート機能をドッグフーディング的に使うわけだが)

なので、変更の指定方法などは上記のコピーとほぼ同じ方式にしつつ、ただしこれはコピーではないリネームなので、上記のコピーの方が「1→多」に変換できることに対して、こっちは「変更前のファイル→変更後のファイル」という、あくまで「1→1」の変換である。

まあ、それだけといえばそれだけの道具だが、ぼくはファイルやディレクトリのリネームをかなり頻繁にやるので(なぜだ?)便利に使っている。

DEMO: rname

f:id:note103:20160504174638g:plain

rmove

当初は上記のリネーム機能が、Linuxコマンドにおけるmvと等価だと考えていたのだけど、よくよく考えてみると、前述のとおりそれは「1:1」の変換形式なので、複数の対象をひとつの場所(ディレクトリ)に移動したいという、mvコマンドのもう一つの目的を果たしたい場合のために作ったのがこれ。

これを使えば、たとえば以下のように指定した場合でも

apple.txt orange.md banana/ strawberry/

前半で指定した複数の対象(apple.txt, orange.md など)が、最後の「strawberry/」の中に収まる。
(実際には、ファイル名の一意を示す文字列を入力すればいいので、その例なら「ap or ba st」とすれば十分だが)

ちなみに、もし同じ指定を上記のrnameでやるとどうなるかと言うと、移動先として指定したつもりのファイル/ディレクトリ名をリネーム先の希望として解釈するので、「その名前のファイル/ディレクトリはすでにありますよ」と言って却下される。賢い。

DEMO: rmove

f:id:note103:20160504185700g:plain

delete

最後にデリート機能。これは以前の記事で書いたものからほぼ変わっていない。
ちょっと気になるかな、と思うのは、削除したファイルの移動場所を、systemコマンドを使ってホームディレクトリ内に自動的に作ってしまうので(現状「tmp_trashbox」というもの)、嫌がる人はいるかも。

その場合、Macなら $HOME/.Trash がゴミ箱フォルダらしいので(おぼろげな記憶です)、 Delete.pm ファイル上部に設定している変数 $trashbox を適宜変更すればよいかと思う。

DEMO: delete

f:id:note103:20160504175218g:plain

「rr」と指定すると、それに対応する果物が引っかかってくれる。

まとめ

まだまだ作っている最中なので、今後も細かく変更が入っていくと思うが、とりあえずの進捗報告的にまとめてみた。

deleteの移動先について、「気になる人がいるかも」などと書いたが、実際には他人が使っているところはあまり想定していなくて、しかし最初の方にも書いたとおり、自分ではけっこう使っていて、なかなか便利。

暴走したPerlをとめるまで

追記2

以下本文の内容に関して、 id:hkoba501 さんが考察記事を書いてくださいました。
hkoba.hatenablog.com

なんとありがたい……大変詳しく、何よりすごく勉強になります。
この後あらためて、じっくり拝読します。 id:hkoba さん、ありがとうございました!

追記1

その後、Perl入学式の先輩方といろいろ話していたら、これは他の環境では再現されないので、エディタのシンタックスチェッカーに原因があるのでは? 等の指摘があり、たしかに別のLinux環境で試してみるとまったく暴走したりはしないので、以下の解消法(?)というか考え方はアサッテかもしれません。

現象

以下のようなコードを書いていて、

#!/usr/bin/env perl
#
# main.pl

use strict;
use warnings;
use feature 'say';
use Sample;

print 'input >>> ';
chomp(my $foo = <STDIN>);

say Sample::init($foo);
# Sample.pm

package Sample {
    use strict;
    use warnings;

    sub init {
        my $bar = shift;
        if ($bar eq 'end') {
            return $bar;
        }
        else {
            print "$bar >>> ";
            chomp($bar = <STDIN>);
            init($bar);
        }
    }
}

1;

こんな感じに動作させたかったとします。

DEMO

f:id:note103:20160503152810g:plain

実際、そんな風に動くのですが、この時、後者の Sample.pm のサブルーチン init の前に、うっかり以下を入れてしまうと、

    init();

けっこう面倒なことになります。というか、なりました。

具体的にはこんなコードですが、真似しなくて大丈夫です。

# Sample.pm
#
# 失敗ヴァージョン

package Sample {
    use strict;
    use warnings;

    init(); #<= ミス

    sub init {
        my $bar = shift;
        if ($bar eq 'end') {
            return $bar;
        }
        else {
            print "$bar >>> ";
            chomp($bar = <STDIN>);
            init($bar);
        }
    }
}

1;

この状態で呼び出し側の main.pl を開くと、

  1. まずエディタ(MacVim)が固まり、
  2. 何も反応しないのでエディタを強制終了し、
  3. そのまましばらくするとマシンの容量が加速度的に減っていき、
  4. ファンもワンワン鳴って怖いのでマシンごと再起動し、
  5. 「いまの何だったんだろ……」と、検証しようと思ってまた main.pl を開いてしまい、
  6. エディタ(MacVim)が固まり、
  7. 何も反応しないのでエディタを強制終了し、(以下繰り返し)

みたいなことをしばらくやっていました。(ギャグみたいだけど本当に)

原因

原因としては、最初から書いているとおり、

    init();

が不要というか、それが無限ループ的な何かを引き起こしているのだろうと想像しますが、それに気づくまではこのケースがいくら検索しても出てこなくて、たいそう時間を使ってしまったので一応書いておきました。

ちなみに、そもそもなんでそんな1行が入ってしまったの?という疑問に対しては、「いやー、最初は1本の.plファイルに書いていたんだけど、サブルーチンの部分だけモジュールに切り分けようと思ったら、最初のファイルで書いていたサブルーチン呼び出しの行を削除し忘れてしまって」と、答えたいと思います。

とはいえしかし、そんな程度のミスなんて日常茶飯事すぎて、今までもいくらでもやってきた&今後もやっていくであろうぐらいちょっとした間違いなのに、これだけ深刻というか、面倒な暴走状態に陥ったことはほとんどなかったので、本当にミステリーでしたね……

で、なぜ普段はほとんどこんな目に遭わずに済んだのか、そして今回はそれに遭遇してしまったのか、と考えると、実際の問題は上記の「サブルーチンを呼び出す行の消し忘れ」だけでなく、当のサブルーチン(Sample.pm内のinit)の書き方自体もおかしくて、具体的には最後のelse文の中で同じサブルーチンを呼び出しているのが悪手なのだろうな……とは思っています。

その再帰的なサブルーチン呼び出しと、上述の消し忘れた呼び出し行とが悪魔合体して、あまり自分的には例のない現象に至ったのかな……と。

で、そのサブルーチン内で同じサブルーチンを呼び出すという方法は、元はといえば while文でミスって無限ループするのが嫌でなんとなく使っていた方法でしたが、こうなると問題は while文を使うことではなく、そもそも無限ループが生じてしまう仕組みを理解していない点にあると気づかざるを得ませんね。まあ、今後の課題ということで……

プロセスの切り方

ところで、今回はこの現象の原因究明や解消方法についても結構考えましたが、副次的な問題として、こういう暴走状態になったときにどう対処すればいいか、ということもちょっと調べました。

というか、上記のようにぼくはwhile文で無限ループしたり、それのちょっとタチ悪い版で今回のようにマシンを再起動せざるを得ない状況に陥りがちなので、そのたびにマシン再起動するのもつらすぎる……ということで。

で、一番参考になったのは以下で、

topコマンドからkillする - ケーズメモ

$ top

して、対象と思わしきPerlのプロセスID(PID)を見つけたら、

$ kill -9 そのプロセスID

としたら切れました。自分にとっては革命的便利さ。

さらにちなみに、killコマンドの復習はこちらで行いました。ITproにかぎらず、こういうまとめ記事はいつもありがたいです。ジャンプの途中で広告が入っても構いません。

Linuxコマンド集 - 【 kill 】 プロセスおよびジョブを強制終了する:ITpro

以上です。