Amazonプライムビデオが神

突然だけど、Amazonプライムビデオは神!

www.amazon.co.jp

動画のラインナップは悪いけど、なんとスマフォに動画をダウンロードできちゃう。

長い電車移動中とかに、映画やテレビ番組を思う存分パケットを気にせず視聴できるわけです。

モバイルネットワークで動画なんて視聴したら、帯域がすぐに狼煙レベルに下がりますからね。事前にダウンロードできる、これはデカい。

HuluやNetflixやってて1ヶ月に映画なんて1本見ればよかったのが、週に数本消化するようになりました。(映画見るのに飽きたら消化しなくなるかもしれないけど…)

最初はただのプライム配送サービスのおまけと思ってましたが、今ではHuluやNetflixよりいいと思えます。

逆にHuluやNetflixが動画ダウンロードをサポートしたら、その時は考えちゃいますね。

JavaでStringのリストの内容にマッチする正規表現オブジェクトを作る

小ネタ。

たとえば下記のようなStringのリストがあったとして、

List<String> strList = Arrays.asList("alpha", "beta", "gamma", "delta");

リストの文字列にマッチする正規表現を作りたいといった場合は下記のようにすればいいです。

// (alpha|beta|gamma|delta) 相当の正規表現を得る
Pattern strListRegexp = 
  Pattern.compile("((?!.)." + strList.stream().map(Pattern::quote).map("|"::concat).collect(Collectors.joining()) + ")");

以下解説。

  • .map("|"::concat) は文字列のリスト(ストリーム)の全要素の前に | をくっつけるものです。
  • Collectors.joining() は文字列のリスト(ストリーム)を、全部くっつけるものです。
  • そういうことで、基本は文字列のリストを | でつないだものを作っています。
    • 単純に | でリストを結合して () カッコで囲めばいいところを、 ((?!.).) で囲うようにしているのは、たとえば仮に文字列のリストが空の場合、何にもマッチしない正規表現を作るためにしています。 この場合、空のリストを入力すると ((?!.).) となりこれは何にもマッチしません。ただ単に () を生成してしまうとどこにでもマッチする表現になってしまいます。 だいたいの、こういう単語などの文字列リストをマッチさせるという用途の場合、空のリストの場合は何にもマッチしない動作が望ましいと考えています。
    • (?!.).は何にもマッチしない正規表現として使用していますが、同じことができるなら他の表現でも問題ありません。 何にもマッチしない正規表現は過去のエントリにも書きました http://knjname.hateblo.jp/entry/2014/03/19/024810 が、過去のエントリの $^ だと、空の文字列にマッチしてしまうようです。($^にとって、空の文字列の開始位置は行末とも言えるし、行頭とも言えるのでマッチできる。$^. とかなら問題ないかも。 )
  • Pattern.quote(文字列) というのは正規表現のメタ文字などを無害な文字列にするメソッドで、今回のリストの文字列要素では必要にはなりませんが、一般的な文字列のリストを処理する場合必要です。 https://docs.oracle.com/javase/8/docs/api/java/util/regex/Pattern.html#quote-java.lang.String-

最近の生活傾向

あまりにも太りすぎたので、最近ダイエットしてます。

毎日Withing社のWi-Fi体重計にのって、風呂以外の時は同Withing社のActiviteというスマートウォッチを身につけることにより、体重・体脂肪率、睡眠時間や傾向、時間ごとの歩数が自動で記録されてグラフになります。これは面白い。自分の睡眠時間の短さを自覚しつつあります。

ダイエットはじめてからiPhoneのヘルスケアという機能にはじめて気付きましたが、今のところはWithingsだけで満足かなあという感じ。

運動としては、とりあえず毎日1時間ぐらい散歩をしています。散歩の様子はRunKeeper(非プロ)でGPSトラックしてます。RunKeeperはWithingsと連携させているので、双方にデータが反映されるようになってます。

散歩中は考え事をするなどして過ごしていましたが、最近はNHKラジオ講座とか聞きつつ、シャドーイングしたりして適度に脳みそを使うようにしてます。こうすると歩いている距離をほとんど意識しなくていいです。これからの季節、暑い中歩くのどうしようと今から対策を考えています。

食事は朝昼晩わりと食べています。ただ間食、極端に脂っこいもの、あと炭水化物は基本避けています。炭水化物を避けていると書きましたが、ご飯は1日1回は食べることにしています。家での食事は、品数増やしつつ、汁物は絶対に欠かさないように(満腹感が違う)。

平日、昼以外は自炊率ほぼ100%なのですが、帰宅時スーパーに行くのも面倒だし、何よりレジに並んだりとロクなものじゃないので、毎週土曜日にネットスーパーで1週間分食材をまとめ買いして、まとめて調理したり下ごしらえしてZipLocコンテナにつめまくって1週間の食事をまかなっています。

ネットスーパーといえば、とうとうドラッグストアやらに寄るのも嫌になってきたので、日用品も通販で済ますようになりました。ステマじゃないですけど、ヨドバシの通販は品揃えは控えめですが、日時指定できるのでアマゾンより使いやすいです。

日々軽く料理はしますが、基本的に作り置き料理で大部分カバーできるように工夫しています。小松菜なんか切って冷凍して解凍するだけおひたしになりますから、それに鰹節だのかければ栄養まるごとおいしくいただけます。ダイエット始めてから普段使わない食材を使うようになったような気がします。

そういえば、セブンイレブンとかで売ってるサラダチキン。ダイエットにもおすすめです。ダイエットじゃなくてもおやつにいい感じ。200円、糖質もなくタンパク質が豊富で130kcal程度、まあまあ腹にたまります。

まあ、最近だいたいそんな感じ。

knjname式 Excelマクロブックスタイリング

みなさんExcelマクロでツールを作るとき、ユーザ入力を受け取るために意味もなくダイアログ開いてませんか?取り扱い説明書を別に作っていませんか?

ちょっと力んでいますね。そのマクロツールプログラマ向けであるなら、力む必要はないのです。

力を抜いて、このスタイリングに従えば幸せになれます。

f:id:knjname:20150410015151p:plain

このスタイリング方法は、私がいつもやっている方法で、今のところ不満を聞いたことがありません。

Javaソースコードへのよくある指摘 その1

僕の仕事の一環で他人のJavaソースを見ることがあるので、その時によく出る指摘をちょっと書き出してみた。

とーってもレベルの低い記事なのでJavaをまともにやっている皆さんは読む必要無しです。ただの愚痴エントリだよ。

不要な代入

String name = null; // ここの代入は不要
if (conditionA) {
  name = "foo";
} else {
  name = "bar"
}

これまじやめて。

C言語(C89)から来た人はローカル変数を一番上に宣言したり、初期値を代入しなきゃいけないものだと思ってて困る。 またそういうC言語特有のローカルルールをJavaの初心者に教え込んで嘘を広めていて困る。 (もうC言語から来る人はそうそう出てこないと思うが。)

だいたい、nameに代入しわすれるバグも引き起こすのでやめてほしい。

if分岐の都合上、ありえないけどnameに代入されない分岐パスが出てくるなら、例外でも投げればいい。 まさしく例外だから遠慮せずに投げればいい。

String name;
if (conditionA) {
  name = "foo";
} else if (conditionB) {
  name = "bar"
} else {
  throw new AssertionError("Unexpected condition!");
}

System.out.println(name);

ループで初期代入されるって?上手なかわし方を考えるか、諦めてください。

同じ式

String feces = kuso.getUnko().getShit().get("poo");
String urine = kuso.getUnko().getShit().get("piss");

こういうの本当に多い。似た式は変数かメソッドにまとめてください。そういう作業がいちいち面倒ならIDEの使い方覚えて下さい。

思考停止の StringBuilder

// ファッ!?
StringBuilder sb = new StringBuilder();
sb.append("The specified value = [");
sb.append(hoge);
sb.append("] is illegal.");
String result = sb.toString();

// これでいい。(あるいは String.format("[%s]", hoge) 使いましょう。 )
String result = "The specified value [" + hoge + "] is illegal."

見づらいのでやめていただきたい。

きっとどこかでStringの結合演算子(+)使うなっていうコーディングルール見て、その使うな、だけが脳みそに残ったか、単純にそのコーディングルールがイカれていたかなんでしょう。

ちなみにStringBuilderの多くの使いどころはループ処理で文字列結合する場合だと思いますが、Java8だと以下のようにListからStringに変換できたりするので、使う機会は減ると思います。

// Java7まで
StringBuilder sb = new StringBuilder();
for(Elem e : list){
  if(sb.length() > 0) sb.append(", ");
  sb.append(e.getVal());
}
String result = sb.toString();

// Java8から
String result = list.stream().map(Elem::getVal).collect(Collectors.joining(", "));

tryでcatchしなきゃいけないものだと思っている

try文にcatchは必須ではありません。 try {} finally {} だけでもいいのです。

// (そもそもtry-with-resources使えよっていうツッコミは無しで)

Connection c = null;
try {
  c = acquireConnection();
  transactionalProcessing(c);
} catch (HogeException e) {
  throw e; // 何もしないのにcatchして例外をもっかい投げるのは無駄
} finally {
  closeQuietly(c);
}

final配列を定数だと思っている

finalつけりゃいいってもんじゃないんだよ。時代はイミュータブルなんだよ。

// これ定数じゃない! CONSTANT_LIST[0] = "Unko" で更新できてしまう。
public static final String[] CONSTANT_LIST = {"One", "Two", "Three", "Four"};

// これが定数!
public static final List<String> CONSTANT_LIST = Collections.unmodifiableList(Arrays.asList("One", "Two", "Three", "Four"));

Collections.unmodifiableListArrays.asList の組み合わせはよく使うので、どっかにそれ用のユーティリティ用意しておくのが普通です。

nullありのObjectの比較

java.util.Objects クラス使いましょう。

// String lhs;
// String rhs;

if (Objects.equals(lhs, rhs)) {
  // lhs == null && rhs == null の場合もOK
}

番外編・メソッドを上手にEclipseで抽出する方法

メソッドのまとめ方ですが、Eclipseならこういうふうにできます。あんまりこういうやり方を解説している人が少ないので、一応解説しておきます。

以下のソースを見て下さい。

// Map<String, String> barMap, fooMap, bazMap;

if ( Objects.equals(barMap.get("hoge"), fooMap.get("hoge")) ) {
  bazMap.put("hoge", retrieveValue("hoge"));
}

if ( Objects.equals(barMap.get("fuga"), fooMap.get("fuga")) ) {
  bazMap.put("fuga", retrieveValue("fuga"));
}

これがこうなったらわかりやすそうです。

private void setRetrievedValueIfEq(String key, Map<String, String> barMap, Map<String, String> fooMap,  Map<String, String> bazMap) { ... }

setRetrievedValueIfEq("hoge", barMap, fooMap, bazMap);
setRetrievedValueIfEq("fuga", barMap, fooMap, bazMap);

上記のようなコードにさっさとEclipseで変更するには、まずは下記のように変数を抽出(Alt-Shift-l)し、

// Map<String, String> barMap, fooMap, bazMap;

String key = "hoge";
if ( Objects.equals(barMap.get(key), fooMap.get(key)) ) {
  bazMap.put(key, retrieveValue(key));
}

String key2 = "fuga";
if ( Objects.equals(barMap.get(key2), fooMap.get(key2)) ) {
  bazMap.set(key2, retrieveValue(key2));
}

if部分を選択してメソッドを抽出すれば(Alt-Shift-m)下記のようになります。同じ構造しているものもメソッドに抽出されます。

要するにパラメータにしたいものをローカル変数として抽出してIDEが見つけられるようにしておくのがメソッド抽出のミソなんですね。

private void setRetrievedValueIfEq(String key, Map<String, String> barMap, Map<String, String> fooMap,  Map<String, String> bazMap) { ... }

String key = "hoge";
setRetrievedValueIfEq(key, barMap, fooMap, bazMap);
String key2 = "fuga";
setRetrievedValueIfEq(key2, barMap, fooMap, bazMap);

それぞれのパラメータになった変数は不要なのでインライン化(Alt-Shift-i)しましょう。

setRetrievedValueIfEq("hoge", barMap, fooMap, bazMap);
setRetrievedValueIfEq("fuga", barMap, fooMap, bazMap);

これらの操作をもうちょっとショートカットを駆使しながらほぼ無意識でできるようになるとJavaに対する見方がかわるでしょう。

(識別子をたぐる言語に見えてくるはず。)

総評

大概、知識が手習いで学んだJava1.4やってた頃でとまってる。

今日のところはこれぐらいにしておいてやるよ!

JenkinsのSSHスレーブが文字化けする場合

たとえば、DockerでテキトーにSSHスレーブを構成したりすると、スレーブでのビルドでUTF-8文字列が化けたりします。 ロケールがズレてるからですね。

スレーブの環境変数パネルに LC_ALL=en_US.UTF-8 を設定しても無駄だったりしますので、 スレーブ自体の java プロセスの環境変数LC_ALL=en_US.UTF-8 が入るようにしましょう。

Jenkinsのノードの設定値の画面で下記のように起動コマンドのPrefixとSuffixを設定してあげましょう。

# Prefix Start Slave Command
sh -c 'export LC_ALL="en_US.UTF-8";

# Suffix Start Slave Command (シングルクォートのみ書く)
'

参考

SSH Slaves plugin - Jenkins - Jenkins Wiki

JenkinsでVBAを動かす際のあれこれ

かれこれ1年以上、JenkinsでExcel VBAを動かしているので、そろそろたまっていることを出そうと思いました。

Excel VBAをサービス内で起動させる方法

Excel VBAは素直にJenkinsで動くようにできていません。Jenkinsで動くのは基本的には応答のいらないコマンドライン操作だけです。

Windowsでいえば、Jenkinsですんなり動くのは対話無しのバッチファイルかPowerShellスクリプトといったものということですね。

Excel VBAはこのように便利なコマンドラインとはかけ離れた性質を持っており、一般的に対話が必要(対話がいるマクロは死んで欲しい)だったり、実行するにもコマンドラインから素直には実行できなかったりします。

そもそも、一般的にはWindowsでJenkinsやJenkinsスレーブを動かすにあたって、それらをサービスとしてインストールすることが多いと思いますが、 Windowsのサービスで立ち上がるプロセスは普通のクライアントセッション(普通のデスクトップ環境)のプロセスとは違い、 Session 0 Isolationとも呼ばれる対話無しの環境で動作するため、通常のGUIアプリケーションが動かなかったり奇妙な動作をしはじめたりします。

Excelも例に漏れず無対策だと動きませんが、ここらへんは過去に書いたエントリに記載した通り、下記のフォルダを作ってやればちゃんと動くようになります。

C:\Windows\system32\config\systemprofile\Desktop

# (x64環境で32bit Officeを動かしている場合)
C:\Windows\SysWOW64\config\systemprofile\Desktop

(ちなみにログインしているデスクトップ環境のセッションからのプロセスでJenkinsスレーブを開くようにしても、この問題は回避可能です。 ここらへんのWindowsサービスから作成されたプロセス特有の問題はWindowsのブラウザやGUIのテストでもぶちあたると思います。)

Excel VBAコマンドラインから起動させる方法

Excel VBAコマンドラインから起動させる方法については、たぶん以下の方法が考えられます。

  • コンパイルのいる言語でコンソールアプリケーションを作って、そこからCOMを一生懸命呼び出す。
  • WSHで呼び出す。(cscript.exe とかで)
  • PowerShellで呼び出す。

僕はPowerShellで呼び出す、という選択肢を選びました。

Windows7以降は標準でバージョン ≧2.0 が使えますし、Jenkinsのビルドプロセス中、VBAの呼び出しといったタスク以外にも、 PowerShellを使った複雑な処理が多少は必要になる、あるいは複雑な処理とVBAが絡みあう可能性もあり、 PowerShellがCOMと.NETがあわさった手軽かつ強力なスクリプティング環境になっているからですね。

PowerShellExcelを呼び出すのは以下のコードだけでできます。

$excelApp = New-Object -com "Excel.Application"

そこからマクロを呼ぶなら、下記のコードでできます。macroProcedureというマクロを引数2つつけて呼んでいる例です。

$macroBook = $excelApp.Workbooks.Open(マクロブックへの絶対パス)
# ' でマクロブック名を囲む必要はありませんが、スペースなどがある場合に備えて囲んでおきましょう。
$excelApp.Run("'$($macroBook.name)'!macroProcedure", "arg1", "arg2")

こういうマクロを呼び出すPowerShellスクリプトをJenkinsから呼び出せば、 結果的にExcel VBAをJenkinsから呼び出していることになります。

Jenkins上でのPowerShellの呼び出しはPowerShell用のプラグインがJenkinsにあるのでそれを使いましょう。

ただし、Jenkinsはテンポラリの*.ps1スクリプトをその場で生成してそれを動かそうとするので、 そのテンポラリのスクリプトを動かせるよう、 Set-ExecutionPolicyでポリシーを変更しておきましょう。 (システム管理者権限が必要。)

Set-ExecutionPolicy RemoteSigned
# または
Set-ExecutionPolicy Unrestricted

PowerShellの処理について、bash-eフラグを立てた時のように、 処理中に何らかのエラーがあった場合に即時終了してほしい場合は下記のようにしておけば即時終了します。

$ErrorActionPreference="Stop"

自動化したいマクロは対話を避けよう。自動化に備えた作りにしよう。

Jenkins上のVBA内でメッセージがポップアップしたり、ダイアログが開いてしまうと、そこで処理が停止してしまいます。

自動化させたいマクロにこういう対話要素を組み込むのはやめましょう。 (ちなみにマクロでランタイムエラー等が出ても誰にも応答できません。)

個人的な好みを言わせてもらえば、対人間であってもマクロでダイアログは開いてほしくないものです。

開発者向けに作ったマクロなんかは、ダイアログはほとんどの場合邪魔になっているケースが多いと思います。

特に、ディレクトリを選ぶパスもペーストできない、あのダイアログをわざわざWin32API使って呼ぶ奴はクレイジーと断言していいでしょう。 あのディレクトリツリーを一日数十回以上ポチポチさせられる人の気持ちを想像してほしいです。

マクロの設定値が必要ならセルに書かせりゃいいんです。セルから値を拾うのが面倒なら、値の部分を名前付き範囲にすればいいんです。 (ここらへんはいつかエントリにします。)

マクロの構成としては実際、対人間用と対Jenkins用にマクロの入り口のプロシージャを下記のように分けるのが現実的でしょう。

  • 対人間用入り口 → 共通メイン処理
  • 対Jenkins用入り口 → 共通メイン処理

対Jenkins用入り口で対話的要素のフラグをOFFるための初期処理などさせればいいです。

ジョブを実行するたびに excel.exe が増えていく件

PowerShell内で起動したExcelは基本的には自前で終了させるまで起動しっぱなしです。 PowerShellが終了したからといって自動的に強制終了させられたりはしません。(そうだったらどんなに楽だったか)

メモリの肥やしを消す意味でも、また、excel.exeが掴んでいるファイルロックを解除させるためにも、 マクロ実行後のExcelの後始末はちゃんとしましょう。

少なくとも下記の条件がすべて満たされていれば、後始末完了とみなして良いようです。

  • 生成したExcel.Applicationおよび内部への参照をPowerShellスクリプトが保持していないこと。 (ただ、PowerShellスクリプトからの参照についてはPowerShellスクリプトが終了すれば全て解放されるので、あまり気にする必要はないかも)
  • $excelApp.Visible = $true にしないこと。 可視化されたExcelは解放されません。
  • 開いたブックを全て閉じていること。
  • 最後に $excelApp.Quit() が発行されていること。

後始末が完了していれば、適当なタイミングでexcel.exeが閉じられるようです。 適当なタイミングというのは、次にExcel.Applicationを作った時とか、そういう本当に適当なタイミングのようです…。

(もし中途半端に残留するexcel.exeを許せない場合、Excelのアプリから$excelApp.HWNDでハンドラをとりWin32APIでPIDを取得して Stop-Process で強制終了するといった方法がオススメです)

Excelを後始末するには、下記のコードのようにすればよいでしょう。

try {
  $excelApp = New-Object -com "Excel.Application"
  // $excelApp を使った処理
} finally {
  # 後始末
  if ($excelApp) {
    # 抵抗されると面倒なのでイベント・警告OFF、VisibleはOFFになっていると思うがOFF。
    $excelApp.EnableEvents = $false
    $excelApp.DisplayAlerts = $false
    $excelApp.Visible = $false

    # 全ブック保存なしで閉じる。保存など必要な操作はここにくるまでにされているはず。
    $excelApp.Workbooks | % { $_.Close($false) }

    # アプリケーションが終了済みとしてマーク
    $excelApp.Quit()
    # Excelに関する参照を外す(どうせこのスクリプトを終了したら外れるので、書かなくてもいい)
    $excelApp = $null
  }
}

ただ、このように後処理したとして、何かマクロでエラーが残ってしまうなどのことが起これば、プロセスはいともたやすく残留してしまうので、 1日1回程度Officeアプリケーションだのを全部殺すプロセスクリーニングジョブを流すこともおすすめしておきます。 VBAのビルドをしているマシンを毎日再起動できるなら、そうしてもいいでしょう。

また、あるワークスペース上にてExcelのプロセスが中途残留したことにより、たとえばワークスペース内のExcelファイルにファイルロックが残りっぱなしになってしまうなどの事象については、 基本的に読み取り専用でワークスペース内のファイルおよびマクロブックを開くように組んでおくことをおすすめします。 (ワークスペースのファイルの属性を読み取り専用に変更してしまってもいいでしょう)

JenkinsからPowerShellスクリプトをアボート(停止)させたらどうなるの?

Jenkinsでは走行中のビルド処理を×ボタンを押すことによりビルドを中断(強制終了)させることができます。

ただその場合、大変残念ですが、PowerShellはそこで即座に強制終了させられ、内部で起動していたexcel.exeは死にません。走り続けてしまいます。

https://wiki.jenkins-ci.org/display/JENKINS/Aborting+a+build に書いてあるように、アボート時は子孫プロセスも殺すということで、 PowerShell内で起動したExcelなども死にそうですが、ExcelPowerShellの子孫として起動しません。

じゃあfinally句あたりでExcelを殺しにいけばいいじゃないかというのも、 JenkinsのWindows版のプロセスキラーの実装(https://github.com/kohsuke/winp/blob/master/native/winp.cpp)が TerminateProcesshttps://msdn.microsoft.com/ja-jp/library/cc429376.aspx)を呼んでPowerShellを強制抹殺しにいくので不可能です。

try {
  // 処理…
} finally {
  ' 強制終了させられたらfinallyもクソもなくPowerShellは終了。動作しているExcelのお掃除はできずに終了である。
}

このタイプの強制終了のトラップ(捕捉)はWindows上では不可能です。何もできません。

これで問題になるのは、たとえば、ブックやドキュメントを処理中に読み取り専用で開いたりしていなかった場合、アボートしたジョブのプロセスがファイルにロックを持ったままになってしまうことですね。 残ったままのプロセスをどうにかして殺さない限りロックが解除されることはありません。次のジョブが同じワークスペースで動いた時、問題になるでしょう。

そもそもロックなんか気にしなくて済むように、読み取り専用で開いて済むものはなるべくそれで処理するのが一番ですが、何かいい方法ないんですかねー。

これに限らずWindows上でのJenkinsというのは、だいたいファイルのロックに悩まされるのが定番です。 最悪の場合はプラグインか何かのせいで、Jenkinsのプロセス自体がワークスペース内のファイルにロックを持ってしまうこともあります。 特定のフォルダ以下のロックの強制解除方法探したほうがはやいかもしれません。

そういう事故に備えて、前回起動したExcelのPIDをワークスペースのどこかにおいておき、次にジョブがそのワークスペースで起動した時にそのPIDを強制終了しにいけばいいんじゃないの?

確かにそうなのですが、残念ながらWindowsは以前使ったPIDを再利用します。OS起動後、プロセスごとに必ずユニークなPIDが振られると思ったら大間違い。

正常終了した後でもそのPIDが前回残留したものという保証はありません。ということで、一筋縄ではうまくいかないでしょう。

ここらへんの確実にプロセスを殺す手法についてはいろいろ考え中です。子孫関係のないプロセス、たとえばスケジューラとかにPowerShellが死んだらExcelを殺すように依頼するのが手っ取り早そうですね。 https://wiki.jenkins-ci.org/display/JENKINS/Spawning+processes+from+build も参考になるかも。

マクロ呼び出しに相対パスは使えません

PowerShellから起動されたExcelインスタンスはシェルの相対パスなど理解してくれないので、PowerShellから与えるパス関連の情報は全て絶対パスで与える必要があります。

以下はパスに関する処理の、駄目な例と良い例。

# 駄目な例 (相対パスは使えない)
$macroBook = $excelApp.Workbooks.Open("macroBook.xlsm")
$excelApp.Run("$($macroBook.name)!macroProcedure", ".\targetFolder", ".\some\destionation\output.xlsx")


# 良い例 (一度相対パスで資材の場所を変数に格納しておき、絶対パスにあとで変換する)
function Make-Absolute(){
  param($relativePath)

  Join-Path (pwd).path $relativePath
}
$macroBookPath = "macroBook.xlsm"
$targetFolder = ".\target"
$destination = ".\some\destination\output.xlsx"

$macroBook = $excelApp.Workbooks.Open((Make-Absolute $macroBookPath))
$excelApp.Run("'$($macroBook.name)'!macroProcedure", (Make-Absolute $targetFolder), (Make-Absolute $destination))

ちなみにUnixなどのシェルスクリプトでもそうですが、作業中広域にわたってcdしてカレントディレクトリを切り替えるのは混乱のもとになるので、やめましょう。

少なくともJenkinsでのスクリプティングで、カレントディレクトリがワークスペースフォルダ直下以外になることは原則ないというように処理したほうがいいでしょう。

Excel VBAから処理進捗のコンソールログを出力したい

PowerShellからExcel VBAを起動できるのはわかった。

でも、無言のままジョブが走っていても今何が処理されているのかわからない。もしかしたら止まっているのかも?今何をしているのかVBA側からコンソールに表示したい。

これは当然の要求ですね。

これをさせるためには、PowerShellのコンソールに出力するメソッドを持つオブジェクトをVBAに渡してあげれば、VBA側からPowerShellのコンソールに出力させることができます。 (そのPowerShellスクリプトがJenkins上で動いていれば、VBAからの出力がJenkinsのコンソール画面で見られることになります。)

' VBA (Book1.xlsmというブックにマクロモジュールとして下記プロシージャを中身にしたものを宣言)

Private consoleObj As Object

' PowerShell側から最初に呼んで conosleObj の中身をもらう関数
Public Sub RegisterConsole(ByVal envConsoleObj As Object)
  Set consoleObj = envConsoleObj
End Sub

' VBA内でDebug.Printのかわりに使う関数
Public Sub DebugPrint(ByVal logMessage$)
  If Not consoleObj Is Nothing Then
    ' ちなみに親のPowerShellだけ上記に書いたアボートなどで一方的に死ぬとここで失敗こきます (TypeName(consoleObj) は "Object"を返す)
    consoleObj.WriteLine logMessage$
  End If
  Debug.Print logMessage$  
End Sub


Public Sub SomeProcessing()

  DebugPrint "処理開始しました!"

  ' :
  ' :
  ' :

  DebugPrint "処理終了しました!"  

End Sub
# PowerShell

# AlternativeConsole というクラスをPowerShellで動的生成。
Add-Type -TypeDefinition @"
using System;

public class AlternativeConsole
{
    public void WriteLine(string line)     { Console.WriteLine(line); }
    public void Write(string line)         { Console.Write(line); }
    public void ErroWriteLine(string line) { Console.Error.WriteLine(line); }
    public void ErroWrite(string line)     { Console.Error.Write(line); }
}
"@

try {
  $excelApp = New-Object -com "Excel.Application"

  # 末尾の後ろ2つの引数は: UpdateLinks := $false, ReadOnly = $true
  $wb = $excelApp.Open((Make-Absolute "Book1.xlsm"), $false, $true)

  # AlternativeConsole インスタンスをインジェクト
  $excelApp.Run("'$($wb.name)'!RegisterConsole", (New-Object AlternativeConsole))

  $excelApp.Run("'$($wb.name)'!SomeProcessing")
  # ここで下記のようにPowerShell側のコンソールに出力される:
  #   処理開始しました!
  #   処理終了しました!

} finally {
  if ($excelApp) {
    $excelApp.EnableEvents = $false
    $excelApp.DisplayAlerts = $false
    $excelApp.Visible = $false

    $excelApp.Workbooks | % { $_.Close($false) }
    $excelApp.Quit()
  }
}

VBA内で Debug.Print を呼んでいる箇所を自前の DebugPrint 関数に置き換えるといい具合にコンソールログが出ると思います。

まとめ

要するに下記課題に対応できれば、JenkinsでVBAをスムーズに起動できるでしょう。

  • Excel VBA自体をサービスプロセス上で動かせるようにする or そもそもサービスプロセス上で起動させない
  • マクロをコンソール仕様にあわせる。
    • マクロに応答要素を入れない。
    • マクロからコンソールに処理状況を出力させる。
  • 基本的にマクロとのやり取りは絶対パス
  • ファイルのロックやプロセスの残滓が残らないようにする。

もし同じことをもっとうまくやっている人がいたら、是非どうやっているか私に教えてほしいです…。