いづいづブログ

娘2人をもつ札幌のシステムエンジニア。自分で勉強したこととか日常のこととかを残しています。アジャイル札幌スタッフ。パクチー大好き。激辛大好き。

SauceTreeでSSHキーを作成し、Backlog上のリポジトリと接続したい

SauceTreeでSSHキーを作成する方法。 あとでまとめる。

キーを作成するときにマウスをぐるぐるしないとならないところと、 「SSHエージェントを起動」を選んでも全然起動しない(実際にはタスクバーに追加されていたのだけど)ところがわからなくて このサイトをみたらよくわかった。

あと、パスフレーズの入力は必須ではない。

www.granfairs.com

他の気持ちはさておき、感謝だけは伝えたほうがいいよ

と思う出来事があったので、技術エントリーじゃないですけど書いておきます。

「出会いと別れの時期」といえば3月4月を連想することが多いと思いますが、私のまわりでは今がそのタイミングだったりします。

先日も海外に転勤になる友達の壮行会で集まったら、集まったメンバーの中に今秋東京へ行くことが決まった人がいました。かくいう私も7月で今の職場を退職し、8月から新しい場所で働くことが決まっています。

「みんな変化するんだなぁ。」

誰かがそんなことをつぶやいてました。そう、みんな変化するんだ。

f:id:izumii-19:20190614203102j:plain:w400
壮行会で食べたステーキ

自分の退職について進んで人に話しはしませんでした。隠してはいなかったので聞かれたら素直に答えてましたが。

これについて特に理由はありませんが、しいていうなら「有名でもなんでもないただのSEが辞めることをあえて自分から触れ回る必要もないのでは」と思ったからです。

知らなくて誰も困らないでしょうし、時が来ればみんな自然に知るでしょうし、という気持ちです。

そして最終出社日が近づくにつれ除々に周知されていくわけですが、思いがけなく色々な方にお言葉をかけていただきました。その時に、

感謝の気持ちを伝えてもらうということはこんなにも嬉しいことなんだな

というのを感じました。


最後だからこそなのか、最後じゃない日々の中では伝えそびれてしまうような感謝、期待、さびしさ、応援の入り混じった言葉をたくさんかけていただきました。

逆に言うと、こういうふうに思っていてくれてたんだなぁ、こんなふうに自分は役に立っていたんだなぁというのを、最後になるこのタイミングまであまり実感することなく過ごしてきたということになります。

これに関してはすごくもったいない気持ちがあり、自分の働きが誰かの役に立てているんだということを日々感じながら過ごせていたとしたら、自分の選択はまた変わっていたかもしれません。

これまで私も照れくさい気持ちがあったり、私からの感謝なんてそもそも欲しがってないのではという自虐的な想いもあったりで感謝を人に伝えるということを積極的にしてきませんでした。

ただ、「自分が誰の役に立っているのか」「なにかに貢献できているのか」というのが仕事に対するモチベーションの維持や自分の存在意義を確かめるきっかけになり得るとしても、それを自分ひとりで実感することはあまりにも難しく、だからこそ誰かからかけてもらう言葉が大切なんだと思います。

なにより、私自身そういう言葉をかけてもらい心から嬉しかったし、自分一人では感じることのできない気持ちに気づかせてもらうことができました。

と同時に、もう少し日常的に実感できていたらなぁという後悔もあります。

もしも、誰かに対する感謝の気持ちを日々持っていながらもなんとなく伝えられていない人がいれば、然るべきタイミングで伝えるのではなく普段から伝えてあげてほしいなぁと思いました。

その人が近くにいるうちに。

TDDBC札幌2019を開催しました

f:id:izumii-19:20190617134151p:plain
TDDBC札幌2019


6/15(土)はt_wadaさんをお迎えして札幌で8年ぶりのTDDBCでした。

当日はわざわざ仙台からi_takehiroさんがお手伝いで来てくださり、t_wadaさんの基調講演&ライブコーディング、そしてi_takehiroさん名物(?)レビューのマサカリが炸裂するというめちゃくちゃエモいイベントでした。

agilesapporo.doorkeeper.jp

当日様子はnemorineがtogetterにまとめてくれましたので、こちらをご覧いただければ会場のアツい感じが伝わるかと思います。

togetter.com

本イベントのメイン担当のKO_SHO14もブログにまとめてくれていますのでこちらもご覧ください。

ko-sho.hatenablog.com

ちょっと余韻に浸っている間に他の参加者の方達がぞくぞくと参加レポートを書いてくれていて、しかもすごくよくまとまっているものばかりですので、私の方はちょっとした感想とスタッフとしての裏話を書こうと思います。

お弁当

お弁当付きのイベントだったので私はお弁当係りでした。

幕の内かちらし二段弁当からお選びいただける感じにしました(幕の内は撮るの忘れました)。

実物は写真でみるより小ぶりです。スタッフ集合時間に遅刻して朝ごはんを食べずに参加した私としてはもう少し大きかったらよかったかなぁ。この後パン1つ食べました。

f:id:izumii-19:20190615124753j:plain:w500
弁菜亭のちらし二段弁当

午前中は基調講演だったこともあり参加者どうしで自己紹介したり会話するきっかけがまだないままお弁当タイムへ突入だったので、ちょっと静かなランチタイムとなりました。

基調講演

ざっくりなタイムテーブルを説明すると、午前はTDDの基調講演+ライブコーディング、午後はペアによるTDD体験でした。

基調講演ではこちらにそってt_wadaさんからわかりやすい説明をしていただいたあとに、実際のTDDをライブコーディングで見せていただきました。

私は、事前にTDDについて調べた時に「要件や説明文をTODOリスト化する」というところが一番悩みそうだったのですがいくつかコツを掴むことができました。

  • テストが大変そうなところ、やりたくなさそうなところを後ろに回して、テストをしたら効果がありそうなところを残すようにしてわける。
  • ただし書きがある場合は、そこから基本の動作を読み解くことができる。
  • リスト化していくときは、表記揺れがないように言葉や言い回しも揃えていくと良い。
  • 重要なところはテストしやすくし、テストしにくいところには大事なロジックを置かない。
  • TODOリストは最初からパーフェクトに作らなくてよい。うまくTODOリスト化できていないとテストや実装で手が止まるが、手が止まるのは考え抜けていないということなので、そうなったらまたTODOリストに戻ってくればいい。

この辺の大事なことは時と共に忘れてしまいそうだったのでしっかり動画に収めました。

ワーク

午後のワークで、私はRuby+ Test-Unitで整数の閉区間のお題に挑戦しました。

f:id:izumii-19:20190615173340j:plain:w400

実際にやってみてめちゃくちゃ感じたのは「TDDはやらずして習得できない」ということです。TDD習得したいと思っている人はTDD本読むだけではなくてほんとやったほうがよくわかります。*1

私も実際ここに来るまでは間違って覚えていたことがたくさんありました。

自分の場合は、どうしてもRed(失敗するテストを書く)からrefactoring(動くきれいなコード)に飛びがちで、green(失敗するテストを通すためのプロダクトコードを書く)が抜けそうになりました。私だけではなくテストを書く習慣がない人はけっこう同じ感じだったんじゃないかなーと思います。

あとはred⇒green⇒refactoringをまわす速さというかリズム感みたいなものはやってみないとわからない部分だなぁと感じました。

たとえばred⇒greenにする時に、greenにするためのプロダクトコードがうまく書けない(このテストをgreenにしようとすると他のテストが死ぬ)とか、red⇒greenまでが適当すぎてrefactoringでやたら時間かかるとか、そういうリズムは書いて詰まって初めて「ああ、テストコードの構成がよくないんだな」とか「名前適当すぎてこのテストとあのテストの区別がわからん」とかが見えてくるという感じでした。

ワークの後はいくつかのペアからピックアップして公開レビューをするのですが、ここでi_takehiroの鋭いマサカリが炸裂します。

f:id:izumii-19:20190615151823j:plain:w300
公開レビューをうけるnemorinellanowar19j
f:id:izumii-19:20190615151836j:plain:w300
レビューのマサカリで切り刻まれるnemorineのふくよかな体

llanowar19jは「おっしゃる通りです」を10回以上連呼してた気がします。

私はまだヒヨコすぎてマサカリを受けたら即死だと思いますが、指摘がすごく的確だったのでこういったレビューを受けることができるというのも良い機会だなと思いました。

スタンドのスタンド

nemorineのこのひとことからすべては始まりました。

f:id:izumii-19:20190616085925j:plain:w300

デザインは私、企画と製作はnemorineで作ったスタンドのスタンドがこちらです。

likeが今でも伸び続けていて1000を超えました! ベルトはよく見たらTDDになっていることにみなさんお気づきでしょうか。

みなさん会場に入る前に写真を撮ってSNSにあげてくれたり、t_wadaさんとの記念撮影に使ってくれたりしたので作ってよかったなと思いました。

f:id:izumii-19:20190615175744j:plain:w400
スタンドのスタンド製作チームとt_wadaさん

スタンドのスタンドの裏話

まずはボツ案。

口を開けて吠えているのはt_wadaさんのスタンドっぽくないということで却下。なるほど。

f:id:izumii-19:20190527231929j:plain:w300
ちょっと笑っているように見える

最終的に大きく投影したときに、バランスが悪くならないようにちゃんと6等身を意識するよう気をつけました。

f:id:izumii-19:20190528085346j:plain:w300
下書きの下書き

f:id:izumii-19:20190601132103j:plain:w300
まだ裸族。最初はロン毛でした

f:id:izumii-19:20190601141359j:plain:w300
最終バージョン

ちなみに当初案では体毛が生えてましたが、最終的には脱毛しました。

f:id:izumii-19:20190609205114j:plain:w300
プロジェクターで投影して書きます

実際に現場で完成版をみると手直ししたいところがたくさんありますが、まあまあ満足です。*2

まとめ

プログラマの三大美徳の気持ちをもっと持とうと思いました。*3

動作するきれいなコードはあらゆる意味で価値があり、だけといきなりやるのは難しい。まずは動作するコード、それからきれいなコードへ。そしてそれを一度にやろうとせず1つずつ倒すことが大切だなぁと思いました。

文章からテストを起こす(文章がテストを駆動する)とか

失敗するテストを通すためのプロダクトコードを書く(テストが開発を駆動する)とか

テストが書けなくて手が止まったらTODOリストに戻る(テストがテストを駆動する)とか

greenのテストが意図せずredになったらテストを見直す(開発がテストを駆動する)

みたいに相互的に作用していて、この開発方法がテスト駆動開発と呼ばれる理由が腑に落ちた気がします。

逆にテストを先に書いても、キレイな動くコードを書くことに貢献できていないのならただのテストファーストでTDDではないということかな。

まぁちょっとまとめが雑ですが私としては学びがたくさんあってよかったです。

参加してくださったみなさんにも同じように感じてもらえていたら嬉しいです。

もしブログを見てアジャイル札幌のイベントに興味を持ちましたらぜひ来てください!

agilesapporo.doorkeeper.jp

*1:テスト駆動開発は自習用に向いている。

*2:最終的にはふんどしっぽいところにt_wadaさんのサインを入れていただきました。

*3:https://www.garunimo.com/program/linux/linux82.php

イラスト #1

趣味でちょっとしたイラストを書いたりしていて、これは札幌スクラムトゥギャザーリングを開催したときに書いたものです。

izumii19.hatenablog.com

まずイベントの案内ポスター。 ポスターといってもA4の小さい貼りもので、参加者が迷わずに会場までこれるための案内に使うやつです。

イラストの人物は@kawagutiさんです。 この頃はまだ面識がなかったのでネットの写真を見ながら書きましたが、あれから何度もお会いしているので今ならもっといい感じに書けるのになぁという気持ちです。

f:id:izumii-19:20190610100912p:plain:w400
@kawagutiさん。もっとオサレな感じに書けばよかった。

もうひとつは紙粘土ワークをやった時にスクラムマスターがつける腕章。

f:id:izumii-19:20190610095933p:plain:w600
スクラムマスターの腕章

手前から

です。トラ以外は北海道らしさを添えてみました。トラは、前日にトラを書く用事があって手が覚えていたのでさっと書いただけです。

イベント開始直前のお仕事だったので10分位で書きました。

これからも時々、書いたイラストを公開していこうと思います。

TDDの基本

f:id:izumii-19:20190606155447p:plain:w600


20019/6/15(土)に札幌では8年ぶりのTDD Boot Camp(TDDBC)が開催されます。

agilesapporo.doorkeeper.jp

8年前のTDDBCには私も参加していたはずなのですが、そのときはまだTDDについて全然理解していなかったのと、その後TDDを実践する機会がゼロだったので、ほぼすべてを忘れてしまいました。

というわけでTDDの基本から。

@t_wadaさんの[動画で解説]和田卓人の“テスト駆動開発”講座全20回分の内容を自分なりにまとめました。

この動画おすすめです。@t_wadaさん以外の観点も入っていたりコメントでリアルツッコミが入ったりしていて面白いし、入門としてはすごくいいんじゃないかなー。

gihyo.jp

第12回目までがTDDの入門的な内容になっているのでそこまでをまとめました。

テスト駆動開発(TDD)

アジャイル開発宣言に携わったうちの1人であるKent Beckが考案したプログラム開発手法の一種。 以下の3つの基本ステップを1サイクルとして、繰り返しながらコードをより良くしていく方法。

  1. これから書く機能に対するテストを1つ書き、テストが失敗することを確認する(レッド)
  2. 失敗するテストを通すための”最低限のコード”を実装する(グリーン)
  3. リファクタリングを行う(リファクタリング

「最初にテストを1つ書き、次にそのテストがグリーンになるような最低限のコードを実装する」ということは、言いかえるとテストがあってコードが生まれるということ。

すなわちテストが開発を駆動していくから、テスト駆動開発という。

1つのテストに対してこの3つのステップを回し、それが終わったら次のテストを書き…を繰り返しながらコードをより良くしていく。

ここで注意したいのは最初からいくつもテストを書かないということ。

「小さい単位で確実にそしてスピーディーに、レッド⇒グリーン⇒リファクタリングを積みかさねる」というのがとても大事。

小さく早くステップを実行することで、息を吸うようにテストコードを書き、息を吐くようにプロダクトコードを書く。「テストがあって開発が駆動する」というのを習慣にするということを目指す。

そのためにはこの1つずつテストを書くというのがとても大事。

"テスト"という言葉の定義

"テスト"という言葉には、実は以下のように目的によって複数の意味合いに分かれている場合が多い。が、そもそもこの分類を意識していない場合使い分けられていない。 ここで"テスト"という言葉の分類と、TDDにおける"テスト"とはなにかを考える。

Developer Testing

開発者が開発者(未来の自分も含む)やチームのために行うテストで、自分で書いたコードを自分でテストする。

こうすることですばやくフィードバックを得られ、その結果コード、しいては開発全体がより良いものになっていく。

単体テスト結合テストはDeveloper Testingに属する。

Customer Testing

お客様側からみたときにその機能が要件を満たしている/満たしていないを判断するためのテスト。 このテストが通らなければ、お客様側から見たときに「機能は完成していない」という判断になる。 テストが通らない = 受け入れることはできないというのが明確なので、進捗や受け入れ状態が明確になる。

受け入れテストはCustomer Testingに属する。

QA Testing

品質管理、品質保証を目的としたテスト。 上2つは機能に対するテストであるのに対し、QA Testingでは非機能の観点でのテストも含まれる。 「機能」という単位に限らず、システム全体としての品質を保証するためのテスト。

非機能テストはQA Testingに属する。

TDDで指し示す"テスト"はDeveloper Testingである。

TDDをどうやって学んでいけばいいか

@t_wadaさんのおすすめの学び方。

1.写経。おすすめ本は、 テスト駆動開発や『WEB+DB PRESS Vol.35』『WEB+DB PRESS Vol.37』*1

2.セミナーとか経験者とのペアプロやレビュー。たとえばテストを書くリズムとかサイクルとか、そういった写経ではわからないことが体感できる。

サイクルをどうまわせばよいか

1. 受け入れテストを書く

最初にやるのは「自分のための受け入れテスト」を書くこと。(=大きいテスト)

プログラムレベルのテストを考えるのではなく、「もしこの機能ができあがったとしたらこういうことができてほしい(=ToBe)」という外側の視点から書く。ブラックボックスに近い。

テスティングフレームワークを使って行う。この時点ではプログラムは存在しないので当然レッドになる。

2. 大きいテストを満たすタスクを洗い出す

大きな機能を実現するために必要な内部の設計行っていく。1. の受け入れ条件を満たすために必要となる小さな機能をタスク(箇条書き)にしていく。

3. 小さな機能に対し3ステップを実施する

先に「大きな機能の受け入れテスト」という大きい視点で考え、それを実現するための小さなタスクを考えて、そのテストを1つずつ書く。(=小さいテスト) これを繰り返す形でテスト駆動していく。

言い換えると「小さいテストをグリーンにする」を積み上げることで、最初はレッドだった大きいテストをグリーンにするということを目指す。

f:id:izumii-19:20190606125205j:plain:w400
小さいテストの総和 = 大きいテスト

テストの資産価値

動画を最後まで読んでいくとこの「テストの資産価値」について考えておくことはとても大事なことなんだと感じたので、まとめる。

「小さいテストがグリーンになることの総和=大きいテストがグリーンになる」は前述のとおりだが、ここで問題。 仕様変更などによって小さいテストが1つでもレッドになると=大きいテストがレッドになるということであり、あまりにテストの単位を小さくしてしまうとすぐ真っ赤なテストになってしまう。

そうなった場合に小さなテストを1つ1つ直していく必要があるのか?(これはけっこう現場あるあるだなぁ。)

考え方としては、まず小さなテストが赤くなった理由を考える。

  • 仕様変更によって、もうこのテストが存在する意味がないのであれば外す。
  • 仕様変更によって、テストの内容を変更する必要があるのであれば、テストをメンテナンスしてグリーンにする。

いきなり「赤くなった小さいテストをどうするか」に注目するのではなく、仕様変更後の大きいテストの受け入れテストを満たすために小さなテストはどうなっているべきかを考える。

その上で、価値のなくなった小さいテストを外し、必要な小さいテストをグリーンにすることで、仕様変更後の大きいテストをメンテナンスしていく感じ。

どうもうまく説明できない。けどすごく大事な気がする。

リファクタリングをどこまでやるか

目安としては2つある。1つは「重複がなくなるまで」。これはDRY(Don't repeat yourself)⁠に基づくもの。 次の目安は「開発者の不安がなくまるまで」。例えばコードを書いていてここがモヤっとするとか、この名前がしっくりこないなどの不安に対してリファクタリングを行う。

リファクタリングそのものはお客様にとって機能を提供するものではなく、お客様への価値は変わらないため、やめどきを決めておくことが大切である。*2

*1:Vol.35とVol.37はもう販売していないがWEB+DB PRESS総集編[Vol.1~102] にまとめられている

*2:「開発者にリファクタリングさせたらいつまでもやり続けるため永遠にプロダクトが完成しない」と言われるほど、開発者は無限にリファクタリングしたがります。

wcコマンドのソースコードを読んでみた

f:id:izumii-19:20190603235249p:plain:w400


Rubyの学習の一貫でwcコマンドと同等のものをRubyで書くことにチャレンジしているんですが、なぜだか-wオプションを指定した時の挙動(単語数のカウント)が一致しないのです。

じゃあwcコマンドのCのソースコード読めばいいじゃんってなるんですけどね、Cに苦手意識があって読むのがおっくうでした。

ただこのままじゃ完成させられない気がしてきたので意を決して内容を理解することにしました。自分用に内容をまとめておきます。

そもそもwcコマンドってなに?という人はググったらたくさん良記事が出てくるのでそちらを参考にしてください。

前提

一口にwcコマンドといっても、シェルのコマンドにはGNU版とBSDFree版の2種類があります。 macOSBSD UNIXベースなので、macで動作するwcコマンドはBSDFree版になります。

ちなみにGNU版とBSDFree版は互換性があるわけではないので同じコマンドでも少し振る舞いが違ったりすることがあります。

今回読んだBSDFree版wcコマンドのソースコードはこちら。⇒ wc.c

以降の内容は、ソースコードを一部抜粋して載せているだけで全体は載っていないので、必要であればこのコードを並べながらブログを読み進めるとわかりやすいかもです。

関数の構成

関数はシンプルに3つ。

  • main() … メイン関数。
  • cnt() … 指定したオプションに応じて件数を算出し画面に表示する。
  • usage() … 使用方法を画面に出力する。*1

ソースコードを解析する

ここからは実際にソースコードを解析(というほどでもないけど)していきます。

オプションとフラグの関係

wcコマンドで受け取るオプションによってフラグ(doXXX)を立て、以降はそのフラグで処理を分岐しているところが多いです。

なのでオプションとフラグの関係を最初にまとめておきます。

フラグ/
オプション
dochar
バイト数
domulti
文字数
doline
改行数
doword
単語数
なし 1 0 1 1
-c 1 0 0 0
-m 0 1 0 0
-l 0 0 1 0
-w 0 0 0 1

main()

main()の中身を読んでいきます。

   while ((ch = getopt(argc, argv, "clmw")) != -1)
        switch((char)ch) {
        case 'l':
            doline = 1;
            break;
        case 'w':
            doword = 1;
            break;
        case 'c':
            dochar = 1;
            domulti = 0;
            break;
        case 'm':
            domulti = 1;
            dochar = 0;
            break;
        case '?':
        default:
            usage();
        }
    argv += optind;
    argc -= optind;

まずはgetopt()を使ってコマンドライン引数のオプション(ハイフン'-'で始まる文字)を解析し、オプションごとにフラグを立てます。

getopt()では指定値(上のコードでは"clmw")以外の値が入ってきた場合「?」を返すので、「?」の場合はusage()をコールし使用方法を表示しておしまい。



   errors = 0;
    total = 0;
    if (!*argv) {
        if (cnt((char *)NULL) != 0)
            ++errors;
        else
            (void)printf("\n");
    }
    else do {
        if (cnt(*argv) != 0)
            ++errors;
        else
            (void)printf(" %s\n", *argv);
        ++total;
    } while(*++argv);

while()を抜けたあとのargv[]にはファイル名が残っているはずなので、そのファイル名を引数にしてcnt()を実行。

ちなみにファイル名が存在しない場合(if (!*argv) { ~側の処理)でもエラーと判断せずに、引数にNullを指定してcnt()を実行しています。

これは「ファイル名が存在しない場合は、標準入力からの入力データをインプットとして処理を実行する」ためです。(ちなみに、引数にファイル名が渡ってこない、かつ標準入力からのインプットもない場合はcnt()内でエラー処理をしています)。

cnt()は、引数に指定されているファイル数分実行され、そのたびにtotalがインクリメントされていきます。



   if (total > 1) {
        if (doline)
            (void)printf(" %7ju", tlinect);
        if (doword)
            (void)printf(" %7ju", twordct);
        if (dochar || domulti)
            (void)printf(" %7ju", tcharct);
        (void)printf(" total\n");
    }

total > 1であれば最後にtotal行を表示します。

つまりmain()の処理はなんてことはなくて、(1)オプションを解析し、(2)ファイルの数分だけcnt()をコールしているだけ。

実際に件数を算出する処理はcnt()でやっているようなのでこのあとはcnt()の中身を理解していきます。

cnt()

cnt()はファイル名を引数として該当するオプションに応じた件数を算出し、正常終了(=0)かエラー終了(=1)かを返却する関数です。

   if (file == NULL) {
        file = "stdin";
        fd = STDIN_FILENO;
    } else {
        if ((fd = open(file, O_RDONLY, 0)) < 0) {
            warn("%s: open", file);
            return (1);
        }
    }

引数にファイル名が渡ってこない場合は標準入力をインプットとします。

引数にファイル名が渡ってきた場合は、そのファイルを読み取り専用で開きます。

ファイルオープンでエラーだったら、エラー(=1)を返却して処理を終了します。



   if (doword || (domulti && MB_CUR_MAX != 1))
        goto word;
  • doword が立っている => オプションなしか、-wオプション指定
  • domulti && MB_CUR_MAX != 1 => -mオプション指定、かつ MB_CUR_MAX != 1

MB_CUR_MAXは 現在のロケールでのマルチバイト文字の最大長のことです。

つまり、オプションなしか、-wオプション指定か、「-mオプション指定、かつ マルチバイト文字の最大長が1以外」だったらword:ラベルまで処理をスキップします。

2バイト以上のマルチバイト文字が含まれる場合は、文字数をカウントするときに単純に1バイト=1文字としてカウントできないので、それ専用の処理をword:ラベル以降でやっているということですね。

マルチバイト、ちょっとめんどくさい。

とりあえずword:ラベル以降の処理は後半で記載するとして、次。



   if (doline) {
        while ((len = read(fd, buf, buf_size))) {
            if (len == -1) {
                warn("%s: read", file);
                (void)close(fd);
                return (1);
            }
            charct += len;
            for (p = buf; len--; ++p)
                if (*p == '\n')
                    ++linect;
        }
        tlinect += linect;
        (void)printf(" %7ju", linect);
        if (dochar) {
            tcharct += charct;
            (void)printf(" %7ju", charct);
        }
        (void)close(fd);
        return (0);
    }

ここはdolineフラグが立っている(オプションなしか-lオプション指定)の場合の処理です。

ファイルの中身をbuf_sizeずつ読み込み(while ((len = read(fd, buf, buf_size))) { ~ 側の処理)、\nをみつけたらlinectをインクリメントします。このとき同時にtotal用の変数tlinectもインクリメントしていきます。

つまり、-lオプションの「行数を表示する」というのは、正確にいうと行数を数えているわけではなく改行の数を数えているということです*2

同時にdocharフラグも立っている場合、バイト数も同時にカウントします。ここでもtotal用の変数tcharctを同時にインクリメントしています。

読み込みに失敗した場合はワーニングを表示してエラー(=1)で終了。



   if (dochar || domulti) {
        if (fstat(fd, &sb)) {
            warn("%s: fstat", file);
            (void)close(fd);
            return (1);
        }
        if (S_ISREG(sb.st_mode)) {
            (void)printf(" %7lld", (long long)sb.st_size);
            tcharct += sb.st_size;
            (void)close(fd);
            return (0);
        }
    }

ここはdocharかdomultiフラグが立っている場合(オプションなしか、-cオプション指定か、-mオプション指定」)の場合の処理で、バイト数をカウントします。

domultiフラグに関してはあらかじめここで処理を分岐しているため、1文字=1バイトの場が対象になります。

1文字=1バイトなのでバイト数と文字数は同じカウントの方法で良いというわけですね。

cnt() - word:ラベル以降の処理

       while (len > 0) {
            if (!domulti || MB_CUR_MAX == 1) {  …(1)
                clen = 1;
                wch = (unsigned char)*p;
            } else if ((clen = mbrtowc(&wch, p, len, &mbs)) ==  
                (size_t)-1) {              …(2)
                if (!warned) {
                    errno = EILSEQ;
                    warn("%s", file);
                    warned = 1;
                }
                memset(&mbs, 0, sizeof(mbs));
                clen = 1;
                wch = (unsigned char)*p;
            } else if (clen == (size_t)-2)     …(3)
                break;
            else if (clen == 0)
                clen = 1;
            charct++;
            len -= clen;
            p += clen;
            if (wch == L'\n')       …(4)
                ++linect;
            if (iswspace(wch))     …(5)
                gotsp = 1;
            else if (gotsp) {       …(6)
                gotsp = 0;
                ++wordct;
            }
        }

word:ラベル以降の処理でキモになるのはこの2つ目のwhile文の中。1つずつ順番に確認していきます。

(1)~(3)までの処理では条件に応じて、mbrtowc()でマルチバイト文字が何バイトかを検査しclenに値を格納しています。同時にマルチバイト文字(のポインタ)をwch格納しています。

順番にifによるclenの値を見ていきます。

  • !domulti || MB_CUR_MAX == 1`

  dowordフラグが立っているか、
  「domultiフラグが立っているかつ MB_CUR_MAX == 1」だったら、clen =1

  • (clen = mbrtowc(&wch, p, len, &mbs)) == (size_t)-1

  mbrtowc()の結果、不正なマルチバイト列に遭遇した場合、clen =1

  • clen == (size_t)-2

  mbrtowc()の結果、完全なマルチバイト文字を解析できなかった場合、clen = (size_t)-2*3

  • clen == 0

  L'\0' ワイド文字を認識した場合、clen =1

  • 上記以外

  clen = mbrtowc()が返却したバイト数

clenにバイト数を取得したらcharctをインクリメントして文字数をカウントしていきます。 そしてclenのバイト数分だけポインタを進めてまた次の文字を解析していき、lenが0になるまでループを継続します。

(4)は他のところでもでてきたように\nをみつけたら行数カウントlinectをインクリメントする処理。

(5)と(6)は単語数のカウントをしています。

(5)はiswspace()を使ってwchがホワイトスペース文字かどうかをチェックしています。Trueが返ってきたらスペース取得フラグ(gotsp) を立てていて、

(6) では、iswspace()を使ってwchがホワイトスペース文字かどうかを検査し、ホワイトスペース文字ではない場合かつスペース取得フラグがOn(gotsp=1)の場合、単語数カウントwordctをインクリメントします。その後スペース取得フラグをリセット(gotsp=0)。

ちなみにiswspace() がTrueを返すのは

  • 空白 (0x20, ' ')
  • 改頁 (0x0c, '\f')
  • 改行 (0x0a, '\n')
  • 復帰 (0x0d, '\r')
  • 水平タブ (0x09, '\t')
  • 垂直タブ (0x0b, '\v')

だとここ書いてあったのですが、自分のmacでwcコマンドを実行するかぎりでは

  • ノーブレークスペース (0xa0)*4

もTrueと判断されているようなので、ロケールによって多少違いがある模様。

この後linect、wordct、charctをそれぞれ標準出力に出力する処理がありますが、ここは割愛します。

以上でwc.cの中身はひととおり解析できました。

所感

自分の書いたRubyのコードと実際のwcコマンドで単語数のカウントが一致しなかったのは、このノーブレークスペース (0xa0)をホワイトスペース文字としてカウントしているかどうかの違いだったっぽいです。

ノーブレークスペースを考慮しなくてよければ、単語数のカウントは単純にsplit()するだけでうまくいったんだけどなぁ。

とりあえず、なぜwcコマンドと自作wcコマンドの単語数カウントが一致しないかはcのソースコードを読むことで仕組みがわかり、最終的に同じように実装することができました。

何度も何度もこのコードを読みましたが(多分20回以上読んでいる)、読めば読むほど理解できるようになったので大変だったけど解析できてよかったです。

*1:usage()の中身については見ればわかるような内容なのでブログでの解説はしません。

*2:けっこういろんなところの説明に「行数」を表示すると書いてある

*3:完全なマルチバイト文字を解析できなかった場合というのは、マルチバイト文字の途中という意味かな?

*4:"だ"という文字を16進数に変換すると"0xe3 0x81 0xa0"となり、この"0xa0"がノーブレークスペース。

Bundlerとは

f:id:izumii-19:20190509125243p:plain:w500

gemをインストールするときにはBundlerを使うらしいのでBundlerについてまとめました。

毎日勉強していると知らないことが多すぎて悲しくなります。

前書き

"gem"というと

  1. Ruby言語用のライブラリ
  2. RubyGemsというパッケージ管理システムをインストールすると使えるようになるコマンド

の大きく2種類の意味で使われることが多いので、このエントリーでは以下のように使い分ける。

  1. Ruby言語用のライブラリ ⇒ gem
  2. RubyGemsというパッケージ管理システムをインストールすると使えるようになるコマンド*1 ⇒ gemコマンド

gemコマンド

gem installコマンドを使うとgemをインストールすることができる。(アンインストールはgem uninstall { gem名 }

$ gem install "sinatra"

これ以外にもgemコマンドを使用するとgemに対する様々な操作ができるがここでは割愛する。

gemの依存関係

Bundlerの話の前にgemの依存関係について。

gemの中には依存関係があるものがある。例えば、

  • Aというgemを使うにはBというgemのVer1.0を使う必要がある
  • BというgemのVer1.0はCというgemのVer3.0を使う必要がある
  • BというgemにはVer1.0とVer2.0があるが、BをVer2.0にしてしまうとAは動かない

とこんな感じに。

チームで開発している場合、誰か一人だけバージョンをあげたりすると開発しているプロダクトがうまくうごかなくなる可能性もあるのでチーム内で合わせておく必要がある。

ところが、gem installを使用してgemをインストールする場合、こういった依存関係を把握した上で必要なgemを入れていく必要がある。

もう、間違いが起きる予感しかない。

Bundler

ここでBundlerの登場。

gem間の依存関係を解決しながら必要なgemを一括でインストールできる仕組みのこと。

簡単に説明すると「"Gemfile"というファイルを作成してそこに指定されているgemをインストールする」といったことができるのだが、後述するGemfile.lockのおかげでけっこう柔軟にバージョン管理ができて便利。

ちなみにBundlerもgemの1つ。

インストール

BundlerはBundlerでインストールできないので、ここはgemコマンドを使ってインストールする。

$ gem install bundler

インストールしたら下記のコマンドで最新バージョンが入っていることを確認すること。

$ bundler -v

Gemfileを作成する

Gemfileはインストールしたいgemを書いておくファイル。

以下のコマンドを実行するとGemfileの雛形が作成されるので出力された雛形にgemの情報を記述していく(編集はvimなどのエディタでやる)。

$ bundle init

書く内容はざっくりとこんな感じ。

source "https://rubygems.org" # gemインストール元

ruby "1.9.3" # Rubyのバージョン

gem "nokogiri"    # インストールするgemを指定
gem "RedCloth", ">= 4.1.0", "< 4.2.0"    # バージョンを指定することもできる
gem "rspec", :group => :test    # こんな指定もできるっぽい
…

記述方法はBundler本家のサイトに書いてるので割愛。

bundle install

作成されたGemfileをもとにgemのインストールを実行する。

$ bundle install --path {ディレクトリ名}

--pathでインストール先を指定することが可能。

一度path指定でインストールすると、次回以降はpath指定無しでも同じpathが選択される。この時Gemfileがある場所と同じところに"Gemfile.lock"というファイルが作成される(初回のみ)。

Gemfile.lockについて

Gemfile.lockにはインストールしたgemの名前とバージョンが記載されている。

実はbundle instarllを実行するときには、このGemfile.lockファイルがある場合とない場合で挙動が少し違う。

Gemfile.lockがない場合(初回実行時)

インストールしたgemの名前とバージョンをGemfile.lockに出力する。

Gemfile.lockがある場合(2回目以降) Gemfile.lockにかかれているバージョンのgemがインストールされる。Gemfile.lockは更新されない。

この仕組をうまく利用し、Gemfile.lockをチーム内に配布してbundle installするようにすれば、他の端末でも同様の環境が用意できる。便利!

Gemfileではバージョン指定はゆるめにしておき、gemのバージョン管理はGemfile.lockに任せるというやりかたがよさそう(その場合はGemfile.lock をGitに登録しておくといいかも)。

bundle update

bundle updateは、rubygems.orgから最新版を取得しGemfile.lockを更新する。gemのバージョンを更新したい時に使用する。

まとめ

gemのインストールはgemコマンドではなくBundlerでやろう!

参考

Bundler: The best way to manage a Ruby application's gems

bundle install と bundle updateの違いについて - Qiita

橋本商会 » Ruby書くならBundler使え

*1:Ruby のバージョン 1.9 以降 RubyGems は標準添付となっているので最近だとわざわざインストールとかしない。