tech.guitarrapc.cóm

Technical updates

はてなブログの全エントリーURLを取得する

このブログ、実はhttpのままです。 はてなブログをやめるか考えているのですが、いったんhttps対応を進めましょう。

困るのがmixed content なのですが、とっかかりとしてこのブログの全URLを取得します。

目次

sitemap の取得

全記事のURLを取得するときに考えるのが、Google Web Master でどうやってクローラーにヒントを出しているかです。ご存知の通り、こういう時に使うのがサイトマップです。

ということで、全記事のURL取得は安直にサイトマップから辿ればいいでしょう。

はてなブログのsitemap は、ブログURL + /sitemap.xml でpagenation付sitemapindexを取得できます。 このブログならこのような感じです。

<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <sitemap>
    <loc>http://tech.guitarrapc.com/sitemap.xml?page=1</loc>
    <lastmod>2019-01-06</lastmod>
  </sitemap>
  <sitemap>
    <loc>http://tech.guitarrapc.com/sitemap.xml?page=2</loc>
    <lastmod>2019-01-06</lastmod>
  </sitemap>
  <sitemap>
    <loc>http://tech.guitarrapc.com/sitemap.xml?page=3</loc>
    <lastmod>2019-01-06</lastmod>
  </sitemap>
  <sitemap>
    <loc>http://tech.guitarrapc.com/sitemap.xml?page=4</loc>
    <lastmod>2019-01-06</lastmod>
  </sitemap>
  <sitemap>
    <loc>http://tech.guitarrapc.com/sitemap.xml?page=5</loc>
    <lastmod>2019-01-06</lastmod>
  </sitemap>
</sitemapindex>

あとは、各sitemapごとにアクセスして、記事URLを拾うだけです。

<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url>
    <loc>http://tech.guitarrapc.com/entry/2015/11/19/030028</loc>
    <lastmod>2015-11-19</lastmod>
  </url>
  <url>
    <loc>http://tech.guitarrapc.com/entry/2015/11/11/032544</loc>
    <lastmod>2016-09-24</lastmod>
  </url>
   <!-- ..... continue -->
</urlset>

結果がurl一覧で取れればいい感じに使えそうです。

http://tech.guitarrapc.com/ 
http://tech.guitarrapc.com/about 
http://tech.guitarrapc.com/entry/2019/01/05/060326 
http://tech.guitarrapc.com/entry/2019/01/05/044741 
http://tech.guitarrapc.com/entry/2018/12/22/235927 
http://tech.guitarrapc.com/entry/2018/09/29/165215 
http://tech.guitarrapc.com/entry/2018/09/29/154004 
http://tech.guitarrapc.com/entry/2018/09/29/151114 
..... continue

sitemap は所定のフォーマットに沿っているxmlなので適当に処理します。

今回はPowerShell、C#、Golang それぞれで書いてリポジトリにおいておきました。順にみていきます。

github.com

PowerShell

PowerShell 6.0以降 で動作します。(5.0で動かす場合は、Invoke-WebRequest-UseBasicParsing スイッチを追加するといいでしょう)

gist.github.com

特別に難しいことはないのですが、PowerShellの場合はXML型が担保できれば要素名をキーとして辿ることができます。 nullの取り扱いがゆるいこともあり、シンプルに書けます。

    [xml]$index = $res.Content
    $sitemaps = $index.sitemapindex.sitemap.loc

CSharp

C# 7.3 w/.NET Core 2.2 で動作します。*1

gist.github.com

C# でXMLだと、namespaceを毎度指定することになるのですこし面倒な感じがあります。

XNamespace ns = "http://www.sitemaps.org/schemas/sitemap/0.9";
var sitemaps = XElement.Parse(res).Descendants(ns + "loc").Select(x => x.Value).ToArray();

Golang

go-sitemapを使うとシンプルになります。

github.com

gist.github.com

練習なので、既存のパッケージを使わずstructを定義して、xmlのUnmarshal で割り当ててみます。

gist.github.com

安直に書いてみたのですが、こんな感じなのでしょうか? slice で要素数が事前にわからないので拡張に任せるのがいやなのと、要素確保をした場合に最初のappendで無駄になるのはどうするといいのかなぁ。

まとめ

とりあえず全ブログ記事URLが取れたので、次はmixed content の警告です。

*1:C# 8.0 / .NET Core 3.0 でも動作します。

PowerShell 本を出版するまでの反省

PowerShell本を書いたのですが、当然多くの反省があります。

tech.guitarrapc.com

どれも自分の苦手とすることへの直視を求められるのでメモしておきます。

プログラミング系の本を書くときの参考になれば幸いです。

目次

そもそもなぜ本を書いたの

本を書く動機はいろいろなケースがあると思います。

私の場合、本を書きたいというより「本を通してPowerShell に感じる悩ましいと思わせる部分への一定の解消を図りたい」という思いで書きました。 結果は読者のみぞ知るので分かりません。

もともと私が持っていたPowerShellの課題に「学習コストが高すぎる」というのがあります。ただコマンドを実行するだけならいいのですが、適当に書いたスクリプトがよくわからないけど動かない、意図した結果にならないというのは、今でもそこかしこで見ます、聞きます。「PowerShellは学の大変だから触るのすら忌避する」という声をよく耳にするたびに、ナルホドタシカニと感じてきました。

自身、今まであったPowerShellの本で自分の知りたいことが知れず、手さぐりで学んできた部分が大きいです。PowerShell InActionは良い本ですが、この本でPowerShellに感じる苦しみは解消されませんでした。配列やスコープは理解できず、モジュールも何それです。英語出版本も10冊近く持っていますが、どれもスコープは小さく、知っている前提、レール上で使う前提でした。そのため、自分がやりたいことをやるためにPowerShell Utils などの小ネタ置き場を作ったり、モジュールを公開し、シェル芸を書き、一つひとつ学んだことを記事にしたり、MSDNやDocs が更新されると読んで新しいことを学び、誤りに気付き、理解のすり合わせを行う繰り返しをしています。

この状況は、ブログを書いていても「調べることができる人」という条件がつくためリーチ層が限定されます。そこで、編集者さんからお話をいただいた時に、リーチできない層に対して、次のニーズを満たすものを伝えるものがないなら書こう、それを書けせてもらえるなら書くというのがこの本の根幹です。

  • 学ぶコストを減らすため一冊で全然書いたことがない人、書いたことあるけどただコピーしただけ、ちょっと書いてる、普段から書いてる、めっちゃ書いているをある程度網羅する *1
  • とりあえずこれ読めば罠とか理解して回避方法もわかる
  • どう書けばいいかのサンプルが分からない状況にサンプルを提供する
  • 後でわからない時にでも読みなおせる
  • 途中まで読んで、分かるようになったときに先も行ける、

執筆期間

本どれぐらいで書いたのか度々聞かれます。だいたい2カ月です。

内訳は、1-2月の深夜、3月半月 + 4月半月 + 5月半月。 3月以降は仕事がないので一日12-18時間ぐらい当ててたようです。*2

仕事をしながら書ける人、心底尊敬します。気持ちの切り替えがポイントなのですが、難しいと感じました。

反省点

いずれも自分への甘えと見込みの甘さが露呈することになります。

書き始めるまでの重さ

最も反省するべきは、エンジンがかかるまでが遅いという悪い癖が露骨に出て足を引っ張りました。

本は書きたい、締め切りもあり書かなきゃなのですが、どうしてもVS Codeを開くまでの気持ちを持っていくのがムズカシイ時期がありました。 仕事をしている時の深夜、締め切りを意識していない時、意識し始めた時それぞれでまんべんなくあるので、私の欠点です。

対策

  • 時間を消費していたことをしようとしたときに閉じる (主に読書とYoutube が大きかったように思います)
  • 音楽を決める

もともとYoutubeでバックグラウンドミュージックを書けて、プログラムや記事を書く習慣があります。が、この時期に動画の音楽を流すことをしてたせいで動画を見る、映像に注目する悪い癖がつきました。そのため、Youtubeで一日の時間が溶けることもあり、開くのをやめました。小説は気分転換がずるずると、パターンだったのでそもそも開かないことにしました。

音楽は意識を集中させるために使っているので、実はなくても書けるのが分かっています。事実、音楽が途中で切れてても一日中書いていることも多いです。しかし音楽を使って意識を書く方に集中させる、書くことの環境であると自己暗示書けることに使っているためある方がスムーズに書き出します。そこで、Playlistを決めて、エンドレスに流すことで、音楽と執筆を結びつけました。主に "Put Your Hearts Up"が多かったです。

一日の書ける量

色々な方が書かれていますが「ブログと全然違って全然書けない」です。「適当を書けない」という当然の事実がびっくりするぐらい書けなくしました。まさか一日10P程度も書けないことがあるとは思いませんでした。*3結果、締め切りを延長したにも関わらず直前まで修正していたので、本当に余裕ありませんでした。

文章構造も書きにくさの原因でした。私が読書をしていて一番嫌いなのが、事前に知らないことを先に出して後から説明されることです。*4知らない概念を先に出した場合、思考のジャンプが生じるためかなり嫌っています。今回、自分で書いていて発生したため、文章構造を5回書き換えています。始めに目次とプロットを書いていても発生したので、レビュー時に考えることにしてある程度割り切りました。*5

私は、RE:View を使っていたのですが、初めてのRE:Viewによる書きにくさは一週間程度ありました。その後もプレーンテキスト書くよりもRE:View書式当てることの手間はありますが書ける量はそこに起因していないように思います。

対策

  • プロットを細かく出す
  • 今日はここまでという目標スケジュールを紙に大きく書いて共有しました
  • 文章構造をブロックごと入れ替え、つなぎはレビューまで割り切る
  • 事実に基づいて淡々と書いてから肉付けをする

プロットと一日に書けるページ数(10P-50P)をある程度同期させることでスケジュール管理しました。同期が崩れると進捗が不明で完了が未定になるので、これはマストな約束事として自分の中で持てました。

目標の達成度を見える化することで、プレッシャーを外部から与えてもらいました。案の定レビュー時に入り乱れて機能しなくなったのですが、文章を書いている分には、自分のダメ具合が露呈するのでオススメです。

「文章構造を考えながら書く」のと「ブロックごとのテーマについて書く」では、難易度がけた違いです。ブロックごとのテーマに集中するのは非常に簡単で、ブログが書きやすい理由はこのテーマが決まっていることにあります。本を書くときに「全体の構成を考えながら書く」のは、章ごとに分離しても難易度が高いため辞めました。ブロックごとに書くようにしたことでかなり書きやすくなりました。

文章に肉付けされていると前後の文脈が作られてしまい、ブロックごとの移動をする修正が困難です。淡々とした文章で書いておいて、文脈を整えるようにすると移動が楽になりました。文章を書く基本なのでしょうが、なかなか気づけず無駄に時間を使いました。

表現の難しさ

網羅するというのが本当に厳しく、文言の表現でも悩みがずっと解消できなかった部分です。そのため、文章に日本人じゃないんじゃないか、というレビューはそうですねと感じます。誤字脱字の多さもレビュー時点から指摘を受けています。編集者さんの修正が結構入っているのですが、それでも誤字脱字あるので元がまずいのは明らかです。

対策

  • もっとすっきりした文体にする

最終版でも、文体がくどいためすっきりしていいと感じます。

ページの超過

当初、編集者さんと合意した目標ページは400Pです。蓋を開けると624Pでした。まさかの224P超過、いいわけもありません。Goを出してくださった編集者さんにはお礼しかありません。

対策

  • プロットをきっちりねる

中盤にプロットを練りなおした時に誤差が小さくなったことからも、当初の見込みの甘さは明らかです。 この影響は多きく、最も面白いであろう5章がページ相対量が少なくなっているのは完全な失敗でした。

サンプルコードの担保

自分が読んでてほしいので、サンプルコードをふんだんに入れましたが、その動作確認をWindows PowerShell / PowerShell Core (Windows / macOS / Linux) の書く環境で行うことが思った以上にコストになりました。

対策は次の通りです。

  • サンプルコードに依存する文章だけコードの変更がないように気を配り、他はコード修正を前提にする

本質的には、マトリックスでクロスプラットフォームテストをかけるのがいいのですが、執筆中にテストを用意するのが過負担でした。誰かにお願いすればよかったです。

兼業はできない

1-3月を通して日中仕事している時に夜書くのは私にはムリでした。1章は深夜1カ月 + 3月の数日を当ててようやく書きましたが、非常に苦しかったことを覚えています。

また、退職と無職期間がまるまる執筆で費やされたのでのびのびできなかったのが悔やまれます。2018年の最優先課題だったのでシカタナイのですが、無駄に溶かした時間を考えると反省しかないです。

編集者との文章共有

GitHub + RE:View で書いていたのですが、編集者さんの印刷用割り付けとGitHub が連動できなかったことが悔やまれます。 編集者さん側の文章をGitHubにわざわざ反映してもらう必要があり、2度手間どころではありませんでした。 RE:View を用いたにも関わらず、RE:View で書籍製本もできなかったので、いろんな意味で悔いが残ります。

対策

  • 編集者さんに合わせる重要性を理解してもらう
  • 編集者さん側の文書フォーマットに合わせるなり、完全に同一フォーマットを用いる
  • Diff を活用する

なに使っていますか? これでいいですか? というすり合わせをはじめにやっていますが、こちらの自由でいいといわれた時に、この問題を予見できなかったのは自分の未熟を感じます。

手間をなくすためには、同一フォーマットを使うのが最善です。これはWeb寄稿ではなかった課題で、結構難しいと感じました。GitHub + RE:View or Markdown が標準になると、私は嬉しいですがどうなんでしょうか。

Diff を活用は実はしていたのですが、フォーマットが同じでないので漏れをお互いにシステム的になくすことができなかったのはムリゲー感じます。

重版予定は?

今はありません。本が分厚く余り出ない一方で、電子書籍が良く売れているということで鈍器なんでしょう。

もし重版が出るなら誤字脱字や表現上の修正、PowerShell Core 6.1/6.2の対応をしたいのですが、予定は未定です。

*1:この時点で欲張り

*2:ぜんぜん遊べておらず仕事よりハードでした。

*3:ブログの文章換算で甘く見てました

*4:言葉を置き換えて先に出すのはいい

*5:案の定レビューで指摘を受け、粛々と直しました

PowerShellでマルチプラットフォームに動くモジュールの作成と継続的なテスト

この記事は、PowerShell Advent Calendar 2018 の 22日目です。

qiita.com

今年は、PowerShell Coreについて本を書いたのですが、その中で書ききれなかった.NET Core と .NET Framework の両方で動くPowerShellモジュールの実用的なサンプルです。

目次

目的

このサンプルを作った目的は、単純に自分が書いていて自分のために用意した環境が汎用的に使えるものだったのでオープンにしただけです。(正直)

ただ、その背景にはいくつか動機があります。

  • 最近.NET は .NET Coreでしか作らないこと
  • PowerShell モジュールを作ろうという機会があった
  • CIで継続的に実行可否をテストしたい
  • ビルド環境につかれたのでDockerでビルドしたい

これらの例は本で提供するには紙面の見せ方も難しく、あの本でやるにはスコープを外していました。 今回の記事はそういう意味では本の補足記事になります。

Utf8BomHeader

非常にこじんまりとしたモジュールを作りました。実装は Utf8BomHeader.psm1 ファイル1つ、PowerShell Gallery で公開するためにUtf8BomHeader.psd1 を一つ追加しただけです。

github.com

このモジュールはファイルのUtf8Bomを扱うだけのもので、Windows 10、MacOS (mojave), ubuntu上で動作を確認しています。PowerShell 5.1以上、PowerShell Core 6.0 以上で動作します。

動機

Windows と Linux の相互対応、あるいはWindowsでクロスプラットフォームで動くものを動かす時、そのどちらでも課題になるのがファイルのエンコーディングです。 特に、Utf8 の BOMはWindowsでBOMが意図せずつくことがあったりして厄介度が上がっているように思います。

皆さんも Docker for Windows でDockerにファイルをmountで渡したときにファイルエンコーディングで怒られた経験をお持ちなのではないでしょうか? あるいは、GoやPythonでクロスプラットフォームに動く前提で作られたものが求めるファイルエンコーディングも通常は Utf8noBomです。

モジュールができること

このモジュールが提供するのは4機能だけです。

  • Test : utf8bom の有無をテストします。BOMがあれば trueです
  • Get : ファイルを先頭から読んでバイナリを表示します。デフォルトは3バイトです(BOM分)
  • Remove : ファイルのBOMを削除します
  • Add : ファイルにBOMを追加します
  • Coimpare : 2ファイル間でそれぞれのBOM結果を出します

実装について

少し実装について触れましょう。 当初PowerShell のみで書いていたのですが、ファイルをストリームで読むためのDisposeのために関数用意するのも冗長になるのも嫌です。 サンプルとして、Test-Utf8BomHeade のPowerShell 版は次のようにFileStream を開いてヘッダを読むことになります。

gist.github.com

本でも書きましたが、リソースの自動破棄は簡単な Using-Dispose 当りの糖衣関数を書いてもいいのですが、あまりスクリプトブロック内部で例外を起こしたくもありません。

そのため、今回のモジュールはインラインで定義した C# で実装を書いて、PowerShell はAdd-Typeでオンザフライにコンパイルしています。

https://github.com/guitarrapc/Utf8BomHeader/blob/94af0c001495a9a49a6bce53f207f5df09cf54a5/src/Utf8BomHeader.psm1#L1-L104

C# はNET Coreで動作することだけ気を付ければたいがい問題ありません。読み込んだC# クラスをPowreShell から呼び出すようにすると先ほどの実装が次のようになります。

gist.github.com

やりたいことだけ、という感じでシンプルになります。 さて、インラインでC#コードを文字列で書くのはよみにくいですね? 私も同意です、PowerShell の外に C# コードを定義したファイルはGitHub上でコードハイライトが利いたり、VS Codeで単品で動かせるメリットがあります。しかしビルド、展開、利用を考えるとファイルは少ない方が取り扱いが圧倒的に楽なため、インラインに定義できるならそうするのをオススメします。

今回はC# で実装を書きましたが、もちろんPowerShell で書くのほうが楽なことも多いです。気を付けるといいのはPowerShell Core と Windows PowerShellで両方動くようにするための抽象化は余り頑張らないようにした方が「書くのが苦しくない」ということです。いちいち互換性を気を付けることも、互換性の抽象化レイヤーを書くのもどちらもやりたいことではありません。私は苦しいのが嫌なので楽をするためにもC# で書きました。C# なら言語レベルで .NET Framework / .NET Core で気を付けることはかなり少ないので選択したということです。

CI

今回はCIとして Appveyor を用いました。 2つのパターンでビルドを用意しました。

  • appveyor 提供の ubuntu18.04 での .NET Coreを使ったビルド
  • Microsoft/PowerShell 提供のDockerを使ったビルド

自前のDockerイメージを使うとどこのCIという縛りはありません。

PowrShell でコンテナを使ったビルド? 大げさでしょうか?ビルド、テストにコンテナを使うことで、普通のプログラミング言語のビルドと同様にビルド環境を選ばなくなります。自分のコードの実行を各種環境で検証することも容易になり、ビルドしたらもうその環境はクリーンなDisposableが担保されます。いろんな環境への可搬性を考慮するとコンテナを用いないという選択はないでしょう。

Appveyor 提供のビルドイメージを使ったビルド

さて、AppVeyor はビルド環境イメージが定義されています。

www.appveyor.com

このイメージにはあらかじめ開発ツールがインストールされていおり、PowerShell Core も ubuntuイメージに含まれています。

www.appveyor.com

仮にappveyor提供のubuntuイメージでやる場合、これぐらいのカジュアルな内容でいけます。 今回は、cd を使っているあたりなんというかそういう感じって感じです。

gist.github.com

では独自のdockerイメージではどうなるでしょうか?

独自Dockerイメージを用いた場合のビルド

Dockerイメージは、Microsoft/PowerShell 提供のmcr.microsoft.com/powershell:6.1.0-ubuntu-18.04を使います。Pesterだけ入れて、プロジェクトを突っ込みましょう。

gist.github.com

appveyor.yml は次のようになります。

gist.github.com

先ほどのAppveyor提供のubuntuイメージくらべると、各種コマンド部分がそのままDockerコンテナでの実行に代わっただけです。こういった簡便な違いで済む割に、コンテナ内部の依存性はDockerfileで統一されるので敷居は低いでしょう。

ただし、mcr.microsoft.com/powershell:6.1.0-ubuntu-18.04 で1つ気になるのが、dotnet が叩けず、Publish-Moduleが実行できないことです。 Artifactなので外に出せばいいので別にいいというわけにもいかない場合があるのでちょっとどうしようかという感じです。

Appveyor のdockerを用いる場合の注意

AppveyorでDocker Imageをビルドする時は、services定義とbefore_build を使うのがプラクティスになっています。dockerは、linux image でdockerサービスの起動をかけることで利用できるので、servicesセクションを appveyor.yml 冒頭に定義しておきます。

services:
- docker

install: よりも後でしかservice startさればいため、docker buildbeffore_build:あたりでします。この実行タイミングはドキュメントになくてここで説明されています。

Build failed: Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running? / Problems / Discussion Area - AppVeyor Support

テスト

テストは、PowerShell の標準的なテストツールである Pester を用いたユニットテストとしました。 ここまで小さい機能だと、ユニットテストが使うシーンそのままなのでシナリオテストにもなります。

Utf8BomHeader/Utf8BomHeader.Tests.ps1 at master · guitarrapc/Utf8BomHeader · GitHub

重要なのはテストを書くことではなく、継続的にテストが回ることです。 テストはCIと一緒にやることで初めてスタート地点に立つのはPowerShell モジュールでも変わりません。

なお、テストカバレッジは Peseterで出せるので、適当にこんなコードでREADME.md を書き換えたりもできます。

gist.github.com

あとは適当に remote push しましょう。

environment:
  access_token:
    secure: zYCOwcOlgTzvbD0CjJRDNQ==

on_success:
  - git config --global credential.helper store
  - ps: Add-Content "$HOME\.git-credentials" "https://$($env:access_token):x-oauth-basic@github.com`n"
  - git config --global user.email "Your email"
  - git config --global user.name "Your Name"
  - git commit ...
  - git push ...

成果物の取得

ビルドして生成された成果物は Artifacts でアップロードしてあげればいいでしょう。

https://ci.appveyor.com/project/guitarrapc/utf8bomheader/branch/master/artifacts

ビルド結果

ログがすぐに吹き飛ぶのでキャプチャで。

Build started
git clone -q --branch=master https://github.com/guitarrapc/Utf8BomHeader.git /home/appveyor/projects/utf8bomheader
git checkout -qf ecf403c5fa0975688503c2c74a15a4837ed62e93
Running "install" scripts
pwsh --version
PowerShell 6.1.1
Starting 'services'
Starting Docker
Running "before_build" scripts
docker build -t utf8bomheader_build:latest .
Sending build context to Docker daemon  175.6kB
Step 1/5 : FROM mcr.microsoft.com/powershell:6.1.0-ubuntu-18.04
6.1.0-ubuntu-18.04: Pulling from powershell
124c757242f8: Pulling fs layer 
Status: Downloaded newer image for mcr.microsoft.com/powershell:6.1.0-ubuntu-18.04
 ---> bcea40fb0698
Step 2/5 : WORKDIR /app
 ---> Running in e03532abc50f
Removing intermediate container e03532abc50f
 ---> 40e5cd66d690
Step 3/5 : COPY . .
 ---> a391219f8b2e
Step 4/5 : RUN pwsh -c Install-Module Pester -Scope CurrentUser -Force
 ---> Running in 20dab6bbeaf2
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 Installing package 'Pester'                                                        Downloaded 0.00 MB out of 0.69 MB.                                              [                                                                    ]                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      Removing intermediate container 20dab6bbeaf2
 ---> 8bee724ae30b
Step 5/5 : ENTRYPOINT [ "pwsh", "-c" ]
 ---> Running in a365d19ff2e5
Removing intermediate container a365d19ff2e5
 ---> 138aa32dca7b
Successfully built 138aa32dca7b
Successfully tagged utf8bomheader_build:latest
docker image ls
REPOSITORY                     TAG                  IMAGE ID            CREATED                  SIZE
utf8bomheader_build            latest               138aa32dca7b        Less than a second ago   369MB
mcr.microsoft.com/powershell   6.1.0-ubuntu-18.04   bcea40fb0698        2 months ago             367MB
Running "build_script" scripts
docker run --rm -v "${APPVEYOR_BUILD_FOLDER}/publish:/app/publish" utf8bomheader_build:latest "./build_psd1.ps1 -Version ${APPVEYOR_BUILD_VERSION}"
    Directory: /app/publish
Mode                LastWriteTime         Length Name
----                -------------         ------ ----
d-----         12/22/18   7:16 PM                Utf8BomHeader
7z a "${APPVEYOR_BUILD_FOLDER}/publish/Utf8BomHeader_${APPVEYOR_BUILD_VERSION}.zip" "${APPVEYOR_BUILD_FOLDER}/publish/Utf8BomHeader/"
7-Zip [64] 16.02 : Copyright (c) 1999-2016 Igor Pavlov : 2016-05-21
p7zip Version 16.02 (locale=C.UTF-8,Utf16=on,HugeFiles=on,64 bits,2 CPUs Intel(R) Xeon(R) CPU @ 2.30GHz (306F0),ASM,AES-NI)
Scanning the drive:
  0M Scan  /home/appveyor/projects/utf8bomheader/publish/                                                         1 folder, 4 files, 17125 bytes (17 KiB)
Creating archive: /home/appveyor/projects/utf8bomheader/publish/Utf8BomHeader_1.0.69.zip
Items to compress: 5
  0%    
Files read from disk: 4
Archive size: 5053 bytes (5 KiB)
Everything is Ok
Running "test_script" scripts
docker run --rm utf8bomheader_build:latest "Invoke-Pester -CodeCoverage src/Utf8BomHeader.psm1"
Executing all tests in '.'
Executing script /app/tests/Utf8BomHeader.Tests.ps1
  Describing Utf8BomHeader
    [+] Test should be pass for bom 1.2s
    [+] Test should be fail for bomless 75ms
    [+] Add should append BOM 45ms
    [+] Add should not operate when BOM already exists 44ms
    [+] Add -Force should append BOM even already exists 26ms
    [+] Remove should not contains BOM 33ms
    [+] Remove should not operate when BOM not exists 20ms
    [+] Remove -Force should remove BOM even already not exists 38ms
    [+] Compare should return == when both contains BOM 25ms
    [+] Compare should return <= when reference not contains BOM 28ms
    [+] Compare should return => when difference not contains BOM 16ms
    [+] Compare should return <> when reference & difference not contains BOM 17ms
Tests completed in 1.58s
Tests Passed: 12, Failed: 0, Skipped: 0, Pending: 0, Inconclusive: 0 
Code coverage report:
Covered 44.62% of 65 analyzed Commands in 1 File.
Missed commands:
File               Function             Line Command
----               --------             ---- -------
Utf8BomHeader.psm1 Add-Utf8BomHeader     130 if (!$Force) {...
Utf8BomHeader.psm1 Add-Utf8BomHeader     131 $header = [FileHeader]::Read($F...
Utf8BomHeader.psm1 Add-Utf8BomHeader     132 if ($header -eq $bomHex) {...
Utf8BomHeader.psm1 Add-Utf8BomHeader     133 Write-Verbose "Bom already exis...
Utf8BomHeader.psm1 Add-Utf8BomHeader     137 Write-Verbose "-Force paramter ...
Utf8BomHeader.psm1 Add-Utf8BomHeader     139 [FileHeader]::Write($File, $Out...
Utf8BomHeader.psm1 Remove-Utf8BomHeader  175 if (!$Force) {...
Utf8BomHeader.psm1 Remove-Utf8BomHeader  176 $header = [FileHeader]::Read($F...
Utf8BomHeader.psm1 Remove-Utf8BomHeader  177 if ($header -ne $bomHex) {...
Utf8BomHeader.psm1 Remove-Utf8BomHeader  178 Write-Verbose "Bom already miss...
Utf8BomHeader.psm1 Remove-Utf8BomHeader  182 Write-Verbose "-Force paramter ...
Utf8BomHeader.psm1 Remove-Utf8BomHeader  184 [FileHeader]::Remove($File, $Ou...
Utf8BomHeader.psm1 Get-Utf8BomHeader     218 $header = [FileHeader]::Read($F...
Utf8BomHeader.psm1 Get-Utf8BomHeader     219 Write-Output $header
Utf8BomHeader.psm1 Test-Utf8BomHeader    258 $header = [FileHeader]::Read($F...
Utf8BomHeader.psm1 Test-Utf8BomHeader    259 Write-Output ($header -eq $bomHex)
Utf8BomHeader.psm1 Test-Utf8BomHeader    259 $header -eq $bomHex
Utf8BomHeader.psm1 Test-Utf8BomHeaderPS  281 if ($PSBoundParameters.Contains...
Utf8BomHeader.psm1 Test-Utf8BomHeaderPS  282 [System.IO.FileStream]$stream
Utf8BomHeader.psm1 Test-Utf8BomHeaderPS  284 $stream = $File.OpenRead()
Utf8BomHeader.psm1 Test-Utf8BomHeaderPS  285 $header = (1..3 | ForEach-Objec...
Utf8BomHeader.psm1 Test-Utf8BomHeaderPS  285 1..3
Utf8BomHeader.psm1 Test-Utf8BomHeaderPS  285 ForEach-Object {$stream.ReadByt...
Utf8BomHeader.psm1 Test-Utf8BomHeaderPS  285 $stream.ReadByte().ToString("X2")
Utf8BomHeader.psm1 Test-Utf8BomHeaderPS  286 Write-Output ($header -eq $bomHex)
Utf8BomHeader.psm1 Test-Utf8BomHeaderPS  286 $header -eq $bomHex
Utf8BomHeader.psm1 Test-Utf8BomHeaderPS  289 $stream.Dispose()
Utf8BomHeader.psm1 Test-Utf8BomHeaderPS  293 [System.IO.FileStream]$stream
Utf8BomHeader.psm1 Test-Utf8BomHeaderPS  295 $stream = [System.IO.FileStream...
Utf8BomHeader.psm1 Test-Utf8BomHeaderPS  296 $header = (1..3 | ForEach-Objec...
Utf8BomHeader.psm1 Test-Utf8BomHeaderPS  296 1..3
Utf8BomHeader.psm1 Test-Utf8BomHeaderPS  296 ForEach-Object {$stream.ReadByt...
Utf8BomHeader.psm1 Test-Utf8BomHeaderPS  296 $stream.ReadByte().ToString("X2")
Utf8BomHeader.psm1 Test-Utf8BomHeaderPS  297 Write-Output ($header -eq $bomHex)
Utf8BomHeader.psm1 Test-Utf8BomHeaderPS  297 $header -eq $bomHex
Utf8BomHeader.psm1 Test-Utf8BomHeaderPS  300 $stream.Dispose()
Collecting artifacts...
Found artifact 'publish/Utf8BomHeader_1.0.69.zip' matching 'publish/Utf8BomHeader_1.0.69.zip' path
Uploading artifacts...
[1/1] publish/Utf8BomHeader_1.0.69.zip (5053 bytes)...100%
Running "deploy_script" scripts
pwsh -file ./deploy_pagallery.ps1 -NuGetApiKey ${PS_GELLERY_RELEASE_API} -BuildBranch master -ModuleName Utf8BomHeader
"Appveyor" deployment has been skipped as "APPVEYOR_REPO_TAG_NAME" environment variable is blank
Build completed

まとめ

PowerShell Core 使っていくといいです。本にも書きましたが、PowerShell Coreは十分実用に到達して、すでにWindows PowerShell が及ばないレベルまでまともに動けるようになっています。

特に私の場合は、クロスプラットフォームで動作することを仕事でも私生活でも扱っているので、Windows PowerShell はたまに触るたびに本当につらいです。モジュールを作った動機もBOMごるぁ、だったのです。特にWindows で、PowerShell で書いていた、書きたい時にマルチプラットフォーム向けのファイルを吐き出したい時は抜群に使いやすいでしょう。

そして、CIは回しましょう。 PowerShell にはビルドがないのでCIはいらない? そんなことを言うのは3年前まではスルーでしたが、今は最低限でも担保したいテストを書いて、CIで継続的に回す方が自分にとって楽になることが多くなっています。

今後PowerShell のネタが PowerShell Core が大半を占めるようになることを祈ります。(個人の勝手なお願い

PS. 本当はAWS Lambda Runtime で PowerShell 動かして見せる。というのを書くかと思ったのですが、「毎年アドベントカレンダーを書いた後にもっとアドベントカレンダーは軽いネタをやるものだ。」と言われたので、今年はそのことを思い出して軽くしました。

オフトピック : もしも.NET Core + PowerShell Core を独自イメージにいれるなら

もし自分でインストールする場合に備えて、.NET Core / PowerShell Core のインストールステップも公開しておきます。.NET Core がubuntu で公開されているやり方にひと手間必要だったのでどぞ。

gist.github.com

.NET Core のMicrosoft Docページにもありますが、libicu60が必要なのですが .NET Coreのインストールページの説明にはなくてほげ、って感じです。apt-get install libicu60 でも入らないですし罠感はあります。

PowerShellっぽく陸上自衛隊のイラク派遣日報をまとめてダウンロードしてみる

面白い記事があったので、私もやってみます。

blog.daruyanagi.jp

毎度毎度、PowerShellっぽさとは何かなぁ思うのですが、PowerShell実践ガイドブックでもWebサイトのステータス監視などを書いたので、良い題材な気がします。

目次

C# だとどう書くのか

C#ならAngleSharpを使って次のようなコードでダウンロード処理を行うことができます。 私の自宅では、60Mbpsを維持して5分でおわりました。

gist.github.com

PowerShellだとどうなるでしょうか。

元記事の処理

元記事では次のように書いています。

$source = "https://www.asahi.com/articles/ASL4J669JL4JUEHF016.html"
$folder = "C:\Users\Hideto\pdf"

$result = Invoke-WebRequest $source -UseBasicParsing
$urls = $result.Links.href | Get-Unique |  where { $_ -match ".pdf" }

foreach ($url in $urls)
{
    $file = ($url  -split "/")[-1]
    Invoke-WebRequest -Uri $url -OutFile (Join-Path $folder $file)
}

PowerShell 6.0で、ここから何処まで手早く書きつつ、素早く処理できるか考えてみましょう。

必要な処理を抜き出す

必要な処理は、次の4つとわかります。

  • サイトの構造からリンクURLを抜き出す
  • PDFのパスのみを取得する
  • 重複があればはじく
  • ダウンロードする

インライン処理

URLや、パスに関しては、処理を括弧でくくることでインライン処理しつつプロパティにアクセスできます。 変数に保持したいかどうか次第ですが、書き捨てでコンソールで書くならこんなのでもいいでしょう。

(Invoke-WebRequest -Uri https://www.asahi.com/articles/ASL4J669JL4JUEHF016.html).Links.href

パイプラインとメソッド形式の選択

さて、PDFに絞る方法ですが、パイプラインの入力をStringクラスのEndsWithメソッドで絞るのが楽でしょう。

(Invoke-WebRequest -Uri https://www.asahi.com/articles/ASL4J669JL4JUEHF016.html).Links.href | 
Where {$_.EndsWith(".pdf")}

コレクション入力を都度処理するときは、パイプラインで書くのも楽ですがメソッド形式で書くという選択もあります。 メソッド形式については過去の記事をどうぞ。

tech.guitarrapc.com

PowerShell実践ガイドブックでは書かなかったのですが、.ForEach().Where() メソッドは、foreach構文同様にコレクションを処理する時に一度メモリにため込むため、パイプラインよりも高速に動作します。ただしメモリに保持するということは、膨大な大きさのコレクションではメモリを大量に使うため対象のサイズに注意が必要です。

(Invoke-WebRequest -Uri https://www.asahi.com/articles/ASL4J669JL4JUEHF016.html).Links.href.Where({$_.EndsWith(".pdf")})

なお、一行が長くなって改行したい場合は、.で改行します。.WhereではなくWhereとなるので気を付けましょう。

(Invoke-WebRequest -Uri https://www.asahi.com/articles/ASL4J669JL4JUEHF016.html).Links.href.
Where({$_.EndsWith(".pdf")})

あるいは{(で改行するのもいいでしょう。

(Invoke-WebRequest -Uri https://www.asahi.com/articles/ASL4J669JL4JUEHF016.html).Links.href.Where({
$_.EndsWith(".pdf")})

メソッド形式で数をフィルタ (Select-Object -First 1 に相当する処理は、Whereメソッドで指定します。

(Invoke-WebRequest -Uri https://www.asahi.com/articles/ASL4J669JL4JUEHF016.html).Links.href.
Where({$_.EndsWith(".pdf")}, "First", 1)

こういう処理を書いている時は、1個だけ試したい、というのはあるあるですからね。

一意に絞る

URLの数を見てみると、1220個ありますが、PDFを末尾に持つURLで絞ると870個です。 しかし、ここには重複したURLが含まれているため、一意(ユニーク)なURLに絞りましょう。 PDFのURLは順不同に並んでいるため、Get-UniqueではなくSort-Object -Uniqueをする必要があります。

(Invoke-WebRequest -Uri https://www.asahi.com/articles/ASL4J669JL4JUEHF016.html).Links.href | 
Where {$_.EndsWith(".pdf")} | 
Sort-Object -Unique

これで、435個に絞られました。

URLからファイル名を取る

元記事では、$file = ($url -split "/")[-1]と書いており/で分割してできた配列の最後*1をとっています。 PowerShellらしいといえばらしいのですが、Split-Pathを使うと配列を意識せず/の最後をとれます。

Split-Path PDFのURLパス -Leaf

PDFファイルのURL、ファイル名まで取れたのを確認してみましょう。 1つだけ試すならこれで。

(Invoke-WebRequest -Uri https://www.asahi.com/articles/ASL4J669JL4JUEHF016.html).Links.href.
Where({$_.EndsWith(".pdf")}, "First", 1).
ForEach({Split-Path $_ -Leaf})

パイプラインならこうです。

(Invoke-WebRequest -Uri https://www.asahi.com/articles/ASL4J669JL4JUEHF016.html).Links.href | 
Where {$_.EndsWith(".pdf") | 
Select-Object -First 1 | 
ForEach {Split-Path $_ -Leaf}

ダウンロードする

1つだけダウンロードするならこうです。

(Invoke-WebRequest -Uri https://www.asahi.com/articles/ASL4J669JL4JUEHF016.html).Links.href.
Where({$_.EndsWith(".pdf")}, "First", 1).
ForEach({Invoke-WebRequest -Uri $_ -OutFile (Split-Path $_ -Leaf)})

全てなら、一意に絞りましょう。

((Invoke-WebRequest -Uri https://www.asahi.com/articles/ASL4J669JL4JUEHF016.html).Links.href.
Where({$_.EndsWith(".pdf")}) | 
Sort-Object -Unique).
ForEach({Invoke-WebRequest -Uri $_ -OutFile (Split-Path $_ -Leaf)})

パイプラインでも、同様に書けます。

(Invoke-WebRequest -Uri https://www.asahi.com/articles/ASL4J669JL4JUEHF016.html).Links.href | 
Where {$_.EndsWith(".pdf")} | 
Sort-Object -Unique | 
ForEach {Invoke-WebRequest -Uri $_ -OutFile (Split-Path $_ -Leaf)}

また、パイプラインの場合は-PipelineVariable を使ってメソッドよりも柔軟に書くことができます。 -PipelineVariableは、自動変数を一度変数に受けて明示的な変数に割り当てることを不要にするので非常に便利です。

(Invoke-WebRequest -Uri https://www.asahi.com/articles/ASL4J669JL4JUEHF016.html).Links.href | 
Where {$_.EndsWith(".pdf")} | 
Sort-Object -Unique -PipelineVariable pdf | 
ForEach{Invoke-WebRequest -Uri $pdf -OutFile (Split-Path $_ -Leaf)}

もちろんこれでダウンロードできるのですが、1つ1つのファイルをダウンロードしたら次に行く同期処理です。 そのため、ダウンロードに時間がかかり435ファイルで6分かかります。

次にこれを高速化してみましょう。

非同期ダウンロードで高速化する

PowerShellの非同期技法は、大きく2つです。 Job機能を使うか、.NETの非同期構文です。

Jobを使った非同期処理

PowerShellらしさ、となるとJob機能ですがオーバーヘッドが大きい処理のため、必ずしも適切ではないケースがあります。今回のような「膨大な数」がまさにそのケースで、こういったときには.NETランタイムを使うと非同期処理がしやすいです。

Jobに変数を渡す時は、スクリプトブロックにparam句を使って -ArgumentList パラメーターを使って渡しますが、実行すると全くダウンロードされないことが分かるでしょう。 これがJobの問題点で、ダウンロードのような処理を大量のジョブでリソース割り当てを行うと大概うまくいきません。 ただの分散処理なら問題ないのですが、ダウンロードは陥りやすいでしょう。

$jobs = (Invoke-WebRequest -Uri https://www.asahi.com/articles/ASL4J669JL4JUEHF016.html).Links.href | 
Where {$_.EndsWith(".pdf")} | 
Sort-Object -Unique | 
ForEach{Start-Job{param($x,$file,$path) cd $path; Invoke-WebRequest -Uri $x -OutFile $file} -ArgumentList $_,(Split-Path $_ -Leaf),$pwd}
Receive-Job $jobs -Wait

Taskを使った非同期処理

.NETのTaskを使ってみましょう。HttpClient経由でダウンロードします。 先ほどのC#の処理からダウンロード部分を抜き出してヒア文字列としたら、Add-Typeでクラスをコンパイル/読み込みします。 あとは、uri一覧をPowerShellで取得してダウンロードを呼び出すだけです。

おおよそC#と同程度の時間で終わります。

gist.github.com

C#側にダウンロード、非同期ロジックを任せることができるので、PowerShellのコードがシンプルなことがわかります。

まとめ

たぶんPowerShellっぽさは、「型を必要になるまで意識せずに」、「適当にコマンドつなげたら書ける」の2点だと思います。 なので、同期処理の場合はワンライナーにしましたし、するのは違和感ないと思います。 一方で、Jobが入った瞬間難しい見た目ですね。.NETランタイム使うのも、唐突に新しい考えが入った印象が強いと思います。 自分が書きやすく、未来の自分が読むのも困らないように書くにはコツがあります。

  • コマンドレットのエイリアスはあまり使わない(Whereや%のような頻出以外は、なるべくフルで)
  • パラメーターを必ず用いる
  • パスやディレクトリを直接触らない

あとは、シェルっぽいやり方として

  • 変数にすることでDRYができるならするが、DRYにならずメンテもしないなら書き捨てる
  • コマンドはパイプラインでつなげていく

見た目が難しくなる = 読み下しが難しくなる要素は、スクリプトブロックなどの「読み手に解釈」を求めるものがあるかもしれません。 渡す順番が処理に影響する、スコープが影響するのは難しいでしょう。

  • スクリプトブロックでparamを使い、-ArgumentListパラメーターで渡す

数が少ないなら同期で十分です。 もし数が多く、非同期で書きたい場合はJobか.NETのTaskを使うといいでしょう。

gist.github.com

参考

PowerShell のStart-Jobに非同期数制御があれば、また話はべつなのですが.... 自宅の回線がネックになってるので、余り速度が速くなりませんでしたが900Mbpsとか超えている環境では顕著に変わるでしょう。

コマンドの長さと、Invoke-WebRequest がパイプラインからの入力を受け付けないのがネックで長いですね。

*1:PowerShellは配列の最後の要素に-1でアクセスできます

『PowerShell実践ガイドブック』の先行レビューアーを募集します!

刊行される「PowerShell実践ガイドブック」(通称:貝殻本)ですが、編集さんにかけあったところ先行レビューという形で書籍をプレゼントできることになりました!

tech.guitarrapc.com

マイナビBOOKSのPC書籍編集部ブログで、応募フォームが用意されています!

book.mynavi.jp

レビュワー募集終了のお知らせ

締め切りを迎えて、抽選により5名様にレビュワーの連絡と書籍が発送されました。 レビューされるのを心待ちにしています!

お願い

当選された方は、アマゾンやブログ(はてな、note、medium、など場所は問いません)でのレビュー+TwitterなどのSNSでの告知をお願いします。 書籍到着から2週間以内にレビューを公開しご連絡ください、拡散します!

「意気込み」とは、ですが、読みたいんだーと思っていただけるの本当にうれしいです。 その読みたい理由とか教えていただけると嬉しいなぁと思います。

以下、応募サイトの抜粋です!

募集内容

発売に先立って『PowerShell実践ガイドブック』をプレゼントします。当選された方は、レビューをお願いします。Amazon.co.jpのレビューやブログ(はてな、note、mediumなどなど、場所は問いません)でのレビュー+TwitterなどのSNSでの告知をお願いします。 書籍到着から2週間以内にレビューを公開し、ご連絡ください。マイナビブックスや著者などが拡散します。

募集人数

5名

募集締切

2018年5月21日 23:59

応募方法

こちらの申し込みフォームから ・名前(ハンドル) ・メールアドレス ・意気込み ・レビュー場所(の予定) を記入して、ご応募ください。 「意気込み」欄には、PowerShellをどのように使っているか、あるいは、これから学びたいと思っている理由など、本書を読みたい!という(熱い)気持ちを自由にお書きください。

当選発表

当選者にはマイナビ出版からメールをお送りします。送付先をお知らせいただければ、書籍を発送いたします。

お問い合わせ

本件についてのお問い合わせは、books-hen3[at]mynavi.jp宛てにお送りください。 ※上記の[at]のところは@に変更してお送りください。

ご応募をお待ちしています!