Office製品では自動化やバッチ処理でVBAやWSH(JScript)を使うことができるのだが、最近流行りのREST APIやAdd-inを書くのに使えるTypescriptと比べると書きにくい印象を受ける(きっとアレルギー?)。それでも組織のセキュリティの都合でVBAやWSHしか選択肢が無い場合がある。(産業スパイのお手伝いをするわけにはいかないから…)
とはいえVBAは今どきやりたくないし、Powershellは独特だし、JScriptは型チェックが無くて何気に書きにくいし…何かないかと思っていたら、WSHをTypescriptで書くことができるという大分古い記事を見つけた(TypeScriptでWSHを書いちゃおう)。
調べてみると、Node.jsを普段使っている人であれば普段と変わらない環境でTypescriptとWebpackを活用してWSHを書くことができることが分かった。これにより、Node.js
を入れられない一般ユーザ向けにスクリプトを書く時でも、言語を変えなくて済むようになる。(以下はOutlookでメールを作成する例)
(追記)色々がんばってみたがWSHはやっぱり貧弱だった...残念。
とりあえず単純な処理だけやらせられればOKだけど。
(追記2)Powershellをやってみたら意外といけた。以下に、Powershellで定期的にOutlookの予定を収集させ、ConfluenceのContent-Propertyに投稿するという、とてもニッチなスクリプトを載せたので、今度記事に書く予定。
https://github.com/stm32p103/jira-confluence/tree/master/ps
let outlook = new ActiveXObject( 'Outlook.Application' );
const message = outlook.CreateItem(Outlook.OlItemType.olMailItem);
message.Subject = 'Hello, world!';
message.Save();
ここでは、TypescriptとWebpackでWSHを作るときに必要なこと・気を付けることをまとめる。
#TypescriptでWSHを書く
まずはTypescriptで書き、JScriptに変換し、実行する手段を用意する。といっても、Node.jsを使っているなら特に新しいことはない。
必要なファイルを用意する
いつも通りpackage.json
を作って、必要なパッケージをインストールして、tsconfig.json
を書いて、tsc
でトランスパイルすればよい。以下は、既に型情報を追加した状態のpackage.json
。
{
"name": "wsh-sample",
"main": "index.ts",
"devDependencies": {
"typescript": "^3.6.3"
},
"dependencies": {
"@types/typescript": "^2.0.0",
"@types/activex-outlook": "^14.0.5",
"@types/activex-scripting": "^1.0.7",
"@types/windows-script-host": "^5.8.3"
}
}
tsconfig.json
(トランスパイラtscの設定)では、2つ気を付けた方が良いことがある。それ以外はあまり重要ではなさそうだった。
- lib
-
scripthost
は設定しない。この後インストールするactivex-scripting
などと定義が重複するため。 -
dom
は設定しない。WSHにはconsole
は存在しないから。
-
- target
- WSHが対応するJavascriptの言語仕様は古いため、
es3
にする必要がある。 - es3ではファイルごとにスコープが分かれていないらしく、グローバルスコープで名前が衝突すると
Cannot redeclare block-scoped variable
というエラー出てしまうため注意が必要。
- WSHが対応するJavascriptの言語仕様は古いため、
{
"compileOnSave": false,
"compilerOptions": {
"baseUrl": ".",
"module": "es2015",
"outDir": "dist",
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"target": "es3",
"sourceMap": false,
"moduleResolution": "node",
"strictNullChecks": false,
"allowSyntheticDefaultImports": true,
"typeRoots": [
"node_modules/@types"
],
"lib": [
"es6",
"es2015"
]
},
"exclude": [ "node_modules", "dist" ]
}
複数ファイルに分割するならWebpackを使う
Typescriptが扱えるようts-loader
をインストールし、設定ファイルwebpack.config.js
を用意する。この辺もNode.jsとは特に変わりがない。(ただし、Node.js上で動く前提のパッケージはWSH上で動かないので、あまりうれしくないかもしれない)
npm install --save-dev webpack webpack-cli ts-loader
module.exports = {
mode: "development",
entry: "./index.ts",
output: {
filename: "index.js"
},
resolve: {
extensions: [".ts", ".tsx", ".js", "json" ]
},
module: {
rules: [
{ test: /\.tsx?$/, loader: "ts-loader" },
]
}
};
webpack
(デフォルトで設定ファイルwebpack.config.js
を参照)コマンドで必要なファイルをトランスパイルすれば、tsconfig.json
で設定した出力フォルダ(ここではdist
)にwebpack.config.js
に指定したoutput
のファイル名(ここではindex.js
)が生成される。
自動でトランスパイルする
特に試行錯誤している段階では、ソースを編集後tsc
やwebpack
コマンドを実行するのが面倒な場合がある。そんな時は、tsc --watch
やwebpack --watch
を使うことで、変更を検知して自動でトランスパイルするようにしておくと楽になる。
型情報を入手する
Typescriptでは@types/*に型情報が登録されていると、Eclipseなどで入力補完することができるようになる。型情報はTypeSearchで検索することができる。
型情報をインストールした後で.tsファイルを見ると、入力補完が使えるようになる。
型情報の例
ActiveXObject("Outlook.Application")
の型情報はactivex-outlook
という風に、「activex-<ドットで区切られる先頭の部分>」と命名されている。例えばMsxml2.DOMDocument
ならactivex-msxml2
になる。TypescriptはMicrosoftが作っているためか、型情報がちゃんと用意されているようだった。
- WSH: https://www.npmjs.com/package/@types/windows-script-host
- Scriptiong: https://www.npmjs.com/package/@types/activex-scripting
- Outlook: https://www.npmjs.com/package/@types/activex-outlook
- Excel: https://www.npmjs.com/package/@types/activex-excel
- MSXML2: https://www.npmjs.com/package/@types/activex-msxml2
npm install --save-dev @types/windows-script-host
npm install --save-dev @types/activex-scripting
npm install --save-dev @types/activex-outlook
npm install --save-dev @types/activex-excel
npm install --save-dev @types/activex-msxml2
###OLE Programmataic Identifierの調べ方
ActiveXObject
でオブジェクトを作る際に指定する文字列(例えばOutlook.Application
)は、OLE Programmatic Identifierという。この文字列はactivex-outlook
などの型情報の中で定義されていて、ActiveXObject(identifier)
の戻り値の型を調べるのに使われている。
その定義は@types/activex-*/index.d.ts
のActiveXObjectNameMap
インターフェースにある。このインターフェースのメンバはOLE Programmatic Identifiers(Outlook)に載っているものと一致している。
例えばactivex-outlook
なら以下のようになっていて、
interface ActiveXObjectNameMap {
'DOCSITE.DocSiteControl': Outlook._DocSiteControl;
'Outlook.Application': Outlook.Application;
'Outlook.OlkBusinessCardControl': Outlook.OlkBusinessCardControl;
...
activex-msxml2
なら以下のようになっている。
interface ActiveXObjectNameMap {
'Msxml2.DOMDocument': MSXML2.DOMDocument60;
'Msxml2.DOMDocument.6.0': MSXML2.DOMDocument60;
'Msxml2.FreeThreadedDOMDocument': MSXML2.FreeThreadedDOMDocument60;
...
ActiveXObject
の引数で引用符('')を打てば、入力補完が働くようになっている。(引用符を打たないとダメだったので、初めは補完が働かないものと誤解していた)
実行する
単一ファイルならtsc
、複数ファイルならwebpack
を使ってトランスパイルした結果(ここではindex.js
)を、cscript
かwscript
で実行する。間違ってNode.jsで実行してもエラーになってしまうので注意すること。
例えば、以下のindex.ts
をトランスパイルして実行すると、
WSH.Echo( 'Hello.' );
let a = 1;
let b = 2;
let c = a + b;
WSH.Echo( 'a + b = ' + c );
以下のように、期待した文字が表示される。
>cscript dist\index.js
Microsoft (R) Windows Script Host Version 5.812
Copyright (C) Microsoft Corporation. All rights reserved.
Hello.
a + b = 3
cscript
の代わりにwscript dist\index.js
を実行すると、以下のようにウィンドウが表示される。
なお、WSH.Echo
はconsole.log
と比べると貧弱で、文字列しか出力できない。複雑なオブジェクトを表示したければ、Visual Studioのデバッガを使う。
cscript dist\index.js //x
デバッガの画面ではindex.js
のソースが表示され、ブレークポイントを設定したり、ステップ実行したりできる。
Officeを操作する
幾つかのお題で試してみる。
Outlookでメッセージを作る
ActiveXObject()
でOutlookのオブジェクトを作り、CreateItemメソッドを実行することで、新しいアイテムを作る。アイテムの種類はOlItemType
列挙で指定する。メッセージの場合は、Outlook.OlItemType.olMailItem
となる。(列挙名はPascalCase、メンバ名はcamelCaseの模様)
let outlook = new ActiveXObject( 'Outlook.Application' );
const message = outlook.CreateItem(Outlook.OlItemType.olMailItem);
message.Subject = 'Hello, world!';
message.Save();
トランスパイルして実行すると、デバッガ上でメッセージを作成しているところが覗ける。
Outlookを開くと、下書きに以下のようなメッセージができていることが確認できる。
Outlookで予定の情報を取得する
以下のような予定が1件しかない時に、その情報を出力してみる。
予定表はGetDefaultFolder
からolFolderCalendar
を指定することで参照できる。予定表に対応するフォルダのオブジェクトcalendarFolder
から、プロパティItems
を取得することで、フォルダ内の予定を参照できるようになる。最初の要素はGetFirst()
メソッドで取得できる。配列だろうと思ってentries[0]
と書くと、以降の処理で「Nullまたはオブジェクトではありません」とエラーが発生してしまうので注意する。
let outlook = new ActiveXObject( 'Outlook.Application' );
const mapi = outlook.GetNamespace( 'MAPI' );
const calendarFolder = mapi.GetDefaultFolder( Outlook.OlDefaultFolders.olFolderCalendar );
let entries = calendarFolder.Items;
let entry = entries.GetFirst();
if( entry ) {
WSH.Echo( `${entry.Subject}@${entry.Location}: ${entry.Start} - ${entry.End}` );
}
- GetNamespaceメソッド
- NameSpaceオブジェクト('MAPI'しか指定できない?)
- GetDefaultFolderメソッド
- OlDefaultFolders列挙
- Folderオブジェクト
トランスパイルして実行すると、予定の情報が取得できていることが分かる。
>tsc
>cscript dist\index.js
Microsoft (R) Windows Script Host Version 5.812
Copyright (C) Microsoft Corporation. All rights reserved.
サンプル@どこか: Mon Sep 23 08:00:00 UTC+0900 2019 - Mon Sep 23 08:30:00 UTC+0900 2019
Itemの型情報を得る
上記のスクリプトは動くには動くのだが、実はentries.GetFirst()
の戻り値はany
型になっているので、メンバの入力補完はできない。マニュアルでもItem
が何かを調べてからでないとメンバにアクセスしてはならないと書いてある。
既定では、予定表フォルダーにはAppointmentItem オブジェクトとMeetingItem オブジェクトを含めることができ、連絡先フォルダーにはContactItem オブジェクトとdistlistitem オブジェクトを含めることができます。 通常、フォルダー内のアイテムを列挙する場合は、フォルダー内のアイテムの種類を想定しないでください。アイテムに適用されるプロパティにアクセスする前に、アイテムのメッセージクラスを確認してください。
Folder
のItems
プロパティのサンプルコードによると、Class
メンバを見てからメンバにアクセスしていることが分かる。判定に使っているolContact
はOlObjectClass
列挙のメンバなので、この種類を調べればよいということらしい。
If (myItem.Class = olContact) Then
MsgBox myItem.FullName & ": " & myItem.LastModificationTime
End If
なので、「予定表」から「会議」を取得するなら、以下のようにItem
がolAppointment
であることを調べた後Type Assertionすることで、以後の処理でメンバを参照する際に入力補完や型チェックができるようになる。
let outlook = new ActiveXObject( 'Outlook.Application' );
const mapi = outlook.GetNamespace( 'MAPI' );
const calendarFolder = mapi.GetDefaultFolder( Outlook.OlDefaultFolders.olFolderCalendar );
let entries = calendarFolder.Items;
let tmp = entries.GetFirst();
while( tmp ) {
if( tmp.Class == Outlook.OlObjectClass.olAppointment ) {
const entry = tmp as Outlook.AppointmentItem;
WSH.Echo( `${entry.Subject}@${entry.Location}: ${entry.Start} - ${entry.E}` );
tmp = entries.GetNext();
}
}
- Folderオブジェクト: 上記説明の引用元
- Itemsプロパティ:サンプルコードにメッセージクラスの確認方法が載っている
- AppointmentItemオブジェクト: 会議
- MeetingItemオブジェクト: 会議の案内や応答(出席依頼やキャンセル連絡、承諾・欠席の回答など)
-
OlObjectClass: Itemの区別に使う。
olAppointment
ならAppointmentItem
クラスと判断できる
型の互換性はあるか?
例えば日付は、VarDate
という型で、TypescriptのDate
と異なる。そのままでは互換性が無いものの、変換は用意されている。activex-interop
(他のActiveXObjectの型情報から参照される)で、varDate
⇔Date
の様な変換があることが分かる。
/** Automation date (VT_DATE) */
declare class VarDate {
private constructor();
private VarDate_typekey: VarDate;
}
// ここで、VarDateからDateに変換する
interface DateConstructor {
new(vd: VarDate): Date;
}
// ここで、DateからVarDateに変換する
interface Date {
getVarDate: () => VarDate;
}
試してみると、差が無いことが分かる。
let outlook = new ActiveXObject( 'Outlook.Application' );
const mapi = outlook.GetNamespace( 'MAPI' );
const calendarFolder = mapi.GetDefaultFolder( Outlook.OlDefaultFolders.olFolderCalendar );
let entries = calendarFolder.Items;
let tmp = entries.GetFirst();
while( tmp ) {
if( tmp.Class == Outlook.OlObjectClass.olAppointment ) {
const entry = tmp as Outlook.AppointmentItem;
const startDate: Date = new Date( entry.Start );
const startVarDate: VarDate = startDate.getVarDate();
WSH.Echo( `Date : ${startDate}` );
WSH.Echo( `varDate: ${startVarDate}` );
tmp = entries.GetNext();
}
}
>cscript dist\index.js実行結果
Microsoft (R) Windows Script Host Version 5.812
Copyright (C) Microsoft Corporation. All rights reserved.
Date : Mon Sep 23 08:00:00 UTC+0900 2019
varDate: Mon Sep 23 08:00:00 UTC+0900 2019
まとめ
WSHをTypescriptで書く方法と例を紹介した。おおよそ以下が分かっていれば、VBSのサンプルをそのままTypescriptに置き換えられるようになる。Officeに限らず、ActiveXが提供されたツールで定型作業を自動化する場面で活用してみようと思う。
- プログラムを書く
- ターゲットをES3に設定してトランスパイルするだけ
- ファイルを分割して開発し、Webpackで1つにすることができる
- 型情報は
activex-*
(*: Outlook.Applicationなどのドットで区切られる先頭の文字列)で得られる - OLE Programmatic Identifierは型情報の
index.d.ts
のActiveXObjectNameMap
で調べられる
- 実行
- 実行:
cscript <script>
かwscript <script>
- デバッグ:
cscript <script> //x
- 実行:
- Officeを操作する
- マニュアルに書いてあることに気を付ける(例えば、メンバにアクセスする前にクラスを調べる)
-
Class
プロパティがOlObjectClass
のどれかを調べ、Type Assertionする
-
- JavascriptとActiveXObjectの型は、変換が用意されている
- マニュアルに書いてあることに気を付ける(例えば、メンバにアクセスする前にクラスを調べる)