汝、コマンドプロンプトを愛せよ

汝、コマンドプロンプトを愛せよ

Windowsの自動化の基本となるものにコマンドプロンプトと、そこから実行できるバッチファイルがあります。

Windowsのバッチファイルは古いマシンから新しいマシンまで使用できます。

今回は、それをまとめてみようと思いますが、百番煎じくらいになるので、なるべくMicrosoftが提供するドキュメントをベースになるように話を進めたいと思います。
文中のリンクは基本的に公式サイトへのリンクになっているので必要に応じて参照してください。

※注意
なにがなんでもバッチファイルで書けという趣旨の記事ではありません。最近はPowerShellも使えますし、レガシーの環境ではWSHという選択肢もあります。

汝、ヘルプを愛せよ

コマンドプロンプトでなんらかの操作を行う場合、手っ取り早くコピペですますのもよくあることですが、基本的にはヘルプを確認するようにしましょう。コピペ元のコマンドやオプションをよくわからないままつかって、動かなかったり、ひどい目にあうことは、よくあることです。

結局、自分自身がこれから行う作業にとって、実行するコマンドやオプションが適切かどうか判断するには公式が提供するヘルプをみるしかないのです。

では、そのとっかかりとして、コマンドプロンプトで以下のコマンドを実行してみましょう。

help

コマンドプロンプトで使用できる主だったコマンドの一覧が取得できると思います。
ここで特定のコマンドの詳細を確認したい場合は以下のようにhelpの後にコマンド名を実行します。

help dir

dirコマンドの詳細の使い方が表示されました。
helpコマンドはコマンドプロンプトやバッチファイルで使用するコマンドの詳細を表示してくれます。しかしながら、すべてのコマンドのヘルプをサポートしているわけではありません。

もし、「help コマンド名」でヘルプが表示されなかったら「コマンド名 /?」を実行してみてください。

ヘルプで不十分だった場合は?

実際のところ、helpコマンドで表示されるヘルプに全ての情報がのっているわけではありません。
次に読むべきものはMicrosoftが提供するドキュメントです。
たとえば、以下からコマンドのリファレンスのリンクが存在します。

https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/windows-commands

Microsoftが提供するインターネット上のドキュメントは昨今、機械翻訳になっていたり、そもそも日本語対応していなかったり、数年たったらなんか消えている場合があったりしますが、もっとも尊重すべきドキュメントの一つです。

この文章を含めた個人サイトを読むのは、あくまでおまけとして、公式が何を言っているかをつかむようにしましょう。

コマンドプロンプトの起動方法

コマンドプロンプトは「Winキー+R」を押した後に「cmd.exe」と入力すると起動します。
ここではコマンドプロンプトの起動についての話をいくつかしようと思います。

特定のディレクトリをカレントディレクトリとしてコマンドプロンプトを起動したい

たとえば、特定のディレクトリをカレントディレクトリとしてコマンドプロンプトを起動したい場合がよくあると思います。
その場合はショートカットを使用すると楽です。

  1. ショートカットを新規に作成します。任意のディレクトリで右クリックして新規作成を選択するとショートカットが作成できます
    image.png

  2. 項目の場所には「%windir%\system32\cmd.exe」を入力します。
    image.png

  3. ショートカット名はわかりやすい任意の名前をつけてください。
    image.png

  4. 作成されたショートカットのプロパティを開きます。
    image.png

  5. 作業ディレクトリに「%CD%」と入力して適用を押下します。
    image.png

6.以後ショートカットを起動すると、ショートカットを作成したディレクトリをカレントディレクトリとしてコマンドプロンプトが起動します。
image.png

もし、このショートカットでの起動時に特定のコマンドを実行したい場合はリンク先を以下のように記載します。

%windir%\system32\cmd.exe /k C:\dev\python3\py3env.bat

kオプションの後に実行したいバッチファイルを指定してください。
指定したバッチファイルを実行した後に、コマンドプロンプトが操作可能になります。
これは特定の環境変数を定義してあるコマンドプロンプトを使用するのに便利なテクニックになります。

起動したコマンドプロンプトすべてで初期処理を実行したい場合

下記のレジストリに実行したいバッチファイルのパスを記載することで起動時の初期処理を記載できます。

  • HKEY_LOCAL_MACHINE\Software\Microsoft\Command Processor\AutoRun
  • HKEY_CURRENT_USER\Software\Microsoft\Command Processor\AutoRun

HKEY_LOCAL_MACHINEはローカルPCすべてのユーザで実行される処理で、HKEY_CURRENT_USERは現在のユーザのみで実行される処理になります。
実行順番としてはHKEY_LOCAL_MACHINE→HKEY_CURRENT_USERとなります。

cmd.exeを実行する場合にdオプションをつけることで、AutoRunを無視することが可能です。

REM 初期処理が実行してからt.batが動作する
cmd /c t.bat

REM 初期処理が実行されないでt.batが動作する
cmd /d /c t.bat

コマンドプロンプトの操作

履歴操作

コマンドラインで実行したコマンドは履歴として保持しており、再実行することが可能です。

F7キーによる履歴の表示

F7キーを押下することで履歴を表示可能です
image.png

またカーソルを上下にうごかして、Enterキーを押すことで、再実行が可能になっています。

doskeyコマンドによる履歴の表示

doskeyコマンドのhistoryオプションを使用することで履歴の一覧が表示されます。

doskey /history

カーソルキーとPageUp、PageDownによる履歴の選択

キーボード 説明
現在表示されている前の履歴のコマンドを実行します
現在表示されている後の履歴のコマンドを実行します
PageUp 履歴の最古のコマンドを呼び出します
PageDn 履歴の最新のコマンドを呼び出します

カーソルキーの↑または↓を押下することで過去の履歴を選択することが可能です。

前方一致による履歴の選択

過去に実行したコマンドを途中まで入力した時点でF8を押下すると過去の履歴から前方一致するコマンドを実行します。複数ある場合は、もう一度F8を押下することで切り替えられます。

image.png

F8の押下
image.png

履歴の番号による選択

F9を入力することで数値で履歴を選択できます。

image.png

image.png

image.png

履歴の消去

Alt+F7で履歴をすべて消去します。

マクロ

doskeyコマンドを使用することでマクロを登録可能です。
これにより複数の操作を一つのコマンドで実行したり、PowerShellでいうようなAliasのような使い方ができます。
なお、下記のヘルプにあるようにコマンドのヘルプにあるようにdoskeyコマンドで作成したマクロはバッチファイルから実行できません。

https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/doskey

Running a doskey macro

To run a macro, type the macro name at the command prompt, starting at the first position. If the macro was defined with \$\* or any of the batch parameters \$1 through \$9, use a space to separate the parameters. You cannot run a doskey macro from a batch program.

マクロの登録

コマンドでの登録

doskeyコマンドにマクロ名とコマンド名を指定することで使用することでマクロを登録することが可能です。

以下はWindowsのコマンドプロンプトでlsをサポートする場合の例になります。

doskey ls=dir $*

$*はパラメータを意味して以下のようなコマンド実行が可能になります。

ls /s c:\ 

$tを使用することで複数のコマンドを実行することが可能になります。

doskey prog=echo %ProgramFiles(x86)%$techo %ProgramFiles%

登録されたマクロはコマンドプロンプトを終了するまで有効になります。

また先に述べたようにdoskeyコマンドの制約のため、マクロをバッチファイル中で使用することはできませんが、バッチファイルで登録は可能です。

ファイルで登録

doskeyコマンドの/MACROFILEオプションを使用することでファイルからマクロを登録できます。

まず以下のようなファイルを用意します。

macro.txt

ls=dir $*
t=echo test

このファイルを用いてファイルを登録するには以下のようにします。

doskey /MACROFILE=C:\dev\bat\macro.txt

マクロの一覧表示

doskeyコマンドに/macrosオプションを付与することで現在登録中のマクロの一覧を取得できます。

REM マクロをすべて表示
doskey /macros

マクロの削除

Alt+F10で登録されているマクロが削除されます。

Windows10での新機能

コマンドプロンプトはバージョンアップを続けており、Windows10やWindowsServer2016では新しい機能が色々とサポートされています。

What's New in the Windows Console in Windows Server 2016 Technical Preview
https://docs.microsoft.com/en-us/previous-versions/orphan-topics/ws.11/mt427362(v=ws.11)?redirectedfrom=MSDN

【まとめ】Windows 10で強化/追加されたコマンドプロンプトの機能
https://www.atmarkit.co.jp/ait/articles/1508/28/news038.html

さらに一部の文字を色を変更して出力することも可能になっています。

echo <ESC>[34mtest<ESC>[0m

にはESCの文字コード「0x1B」を入力してください。

image.png

How to echo with different colors in the Windows command line
https://stackoverflow.com/questions/2048509/how-to-echo-with-different-colors-in-the-windows-command-line

バッチファイル

バッチファイルの実行方法

拡張子batのファイルをエクスプローラーからダブルクリックするか、コマンドプロンプトで以下のように実行するとバッチファイルが実行できます。

たとえば以下のようなバッチファイルが存在します。

sub.bat

echo sub.bat.....

これを実行するには以下のようにします。

C:\dev\bat\func>C:\dev\bat\func\sub.bat

C:\dev\bat\func>echo sub.bat.....
sub.bat.....

バッチファイルを実行すると、バッチファイル内のコマンドがコマンドプロンプトに表示されてしまいます。
バッチファイル内のコマンドをコマンドプロンプトに表示しないようにするには行の先頭に@を付与します。

sub.bat

@echo sub.bat.....

このバッチファイルを実行すると@を付けたコマンドがコマンドプロンプトに表示されていないことが確認できます。

C:\dev\bat\func>C:\dev\bat\func\sub.bat
sub.bat.....

大量のコマンドがある場合、いちいち@を付けるのは面倒です。
この場合、echoコマンドを使用してコマンドエコー機能をオフにします。

sub.bat

@echo off
echo sub.bat.....

バッチファイル中から別のバッチファイルを実行する

バッチファイルの中から別のバッチファイルを実行する場合はcallコマンドを使用します。

main.bat

@echo off
echo 呼び出し前
call sub.bat
echo 呼び出し後

これを実行すると以下のようになります。

>main.bat
呼び出し前
sub.bat.....
呼び出し後

もし以下のようにcallコマンドを使用せずにバッチを実行した場合、呼び出し元の処理に戻りません。

main.bat

@echo off
echo 呼び出し前
sub.bat
echo 呼び出し後
>main.bat
呼び出し前
sub.bat.....

バッチファイル内のラベル

バッチファイル内に「:」を使用してラベルを宣言することが可能です。
ラベルにはgotoコマンドをジャンプすることができます。

label.bat

@echo off
echo p1
goto label1
echo p2

:label1
echo p3

上記のバッチファイルを利用するとgotoで指定したラベルに処理がジャンプすることが可能です。

>label.bat
p1
p3

指定したラベルはcallコマンドを使用することで関数のように使用することが可能です。

label2.bat

@echo off
call :label2
echo p4
exit /b

:label2
echo p5
exit /b

上記のバッチファイルの実行結果は以下のようになります。

>label2.bat
p5
p4

p4をエコーする前にlabel2にジャンプしてp5をエコーしていることが確認できます。

コメントの使用方法

remコマンドを使用することでコメントを使用することができます。

REM  コメント1 (コマンドがエコーされます)
@REM コメント2 (コマンドがエコーされません)

ラベルを利用したコメント

以下のようにラベル(:)を利用したコメントの方法が紹介されている場合があります。

::コメント

これはあくまでラベルを利用したトリックでマイクロソフトで公式のドキュメントで定義されたコメントの仕様ではありません。
たとえば以下のケースではエラーになります。

@echo off
IF EXIST C:\ (
    :: Comment line 1
    ECHO Do something
    :: Comment line 2
)

下記のページではラベルを使用したコメントについて制限や複数行コメントについてのトリックが述べられています。

Rob van der Woude's Scripting Pages -Comments-
https://www.robvanderwoude.com/comments.php

別プロセスでバッチファイルを実行する

別プロセスでバッチを実行するにはstartコマンドを利用します。

REM 非同期で実行
start cmd.exe /c sleep.bat

REM 同期で実行
start /wait cmd.exe /c sleep.bat

バッチファイルの終了コード

exitコマンドを使用することで、バッチファイルの終了コードを指定できます。
終了コードは環境変数ERRORLEVELに格納されます。

test.bat

@echo off
echo main
exit /b 9999
>test.bat
main

>echo %ERRORLEVEL%
9999

/bオプションを付けないで実行するとcmd.exeが終了します。

バッチファイルのパラメータ

バッチファイルはパラメータを使用することができ、バッチファイル中に環境変数を利用してパラメータを参照することができます。

バッチファイル内でパラメータを利用するには%1~%9を使用します。

sub.bat

@echo off
echo sub.bat..... %1 %2 %3

バッチファイルをパラメータ付きでよびだすには、パラメータを空白、「,」、「;」で区切ります。

>sub.bat abc def jh
sub.bat..... abc def jh

>sub.bat abc,def,jh
sub.bat..... abc def jh

>sub.bat abc;def;jh
sub.bat..... abc def jh

複数コマンドの実行

バッチファイルやコマンドは条件付き処理記号を使用して実行することができます。

文字 構文 定義
& [...] Command1 &Command2 単一のコマンド ライン上の複数のコマンドを区切るために使います。1 つ目のコマンドが実行された後に、2 つ目のコマンドが実行されます。
&& [...] Command1 &&Command2 && 記号の前にあるコマンドが正常に終了した場合にのみ、後ろにあるコマンドを実行するために使います。1 つ目のコマンドが実行され、そのコマンドが正常に終了した場合にのみ、2 つ目のコマンドが実行されます。
|| [...] Command1 || Command2 || 記号の前にあるコマンドが失敗した場合にのみ、後ろにあるコマンドを実行するために使います。1 つ目のコマンドが実行され、そのコマンドが失敗した場合 (コマンドから返されるエラー コードが 0 よりも大きい場合) にのみ、2 つ目のコマンドが実行されます。
( ) [...] ( Command1 & Command2) 複数のコマンドをまとめたり、ネストするときに使います。

条件付き処理記号の使用例を以下に記述します。

ok.bat

@echo off
echo ok.bat
exit /b 0

ng.bat

@echo off
echo ng.bat
exit /b 1
>REM &の使用例
>call ok.bat & call ng.bat
ok.bat
ng.bat

>call ng.bat & call ok.bat
ng.bat
ok.bat

>REM &&の使用例
>call ok.bat && call ng.bat
ok.bat
ng.bat

>call ng.bat && call ok.bat
ng.bat

>REM ||の使用例
>call ok.bat || call ng.bat
ok.bat

>call ng.bat || call ok.bat
ng.bat
ok.bat

>REM ()の例
>(call ok.bat || call ok.bat) && (call ng.bat)
ok.bat
ng.bat

リダイレクト

リダイレクト演算子を使って、既定の場所から別の場所に、コマンドの入出力ストリームをリダイレクトできます。

エラー出力

コマンドの出力には標準出力とエラー出力の2種類があります。
多くのコマンドは正常に終了した場合は標準出力に出力し、エラー終了した場合はエラー出力に出力します。

[echoコマンド]()を使用する標準出力に出力しますが、以下のようにすることで標準出力の内容をエラー出力に出力することが可能です。

echo error 1>&2

出力のリダイレクト

ほとんどのコマンドの出力はコマンドプロンプトのウィンドウに送ります。
「>」演算子を使用することで出力をファイルに送ることができます。

以下のコマンドはdirコマンドの出力結果をret.txtファイルに送ります。

dir > ret.txt

もし存在しないパスを選択してエラーになった場合、dirコマンドはエラー出力に出力しますが、これをファイルに送るには以下のようにします。

dir z:\ 2> error.txt

ファイルに出力際に「>>」演算子を使用することでファイルの末尾に出力することができます。

echo test1 > ret.txt
echo test2 >> ret.txt

この結果で出力されるファイルは以下のようになります。

test1
test2

入力のリダイレクト

キーボードからの入力をファイルからリダイレクトするには「<」演算子を使用します。
たとえばsortコマンドはキーボードから入力した内容を昇順に並び替えることができますが、これをファイルから入力するには以下のようにします。

たとえば以下のようなファイル「test.txt」が存在したとして、その内容を並び替かえる例を示します。

test.txt

あがg
zZZDfsgsagas
asdfasdf
w235235
12414
agdaga
あああたた
>sort < test.txt
12414
agdaga
asdfasdf
w235235
zZZDfsgsagas
あああたた
あがg

パイプの使用

あるコマンドからの出力を読み取り、別のコマンドの入力に書き込むには「|」演算子を使用します。
たとえば、dirコマンドの出力結果をsortの入力に与えるには以下のようにします。

>dir | sort

               2 個のディレクトリ  246,303,686,656 バイトの空き領域
               3 個のファイル                 116 バイト
 C:\dev\bat\redirect のディレクトリ
 ドライブ C のボリューム ラベルは OS です
 ボリューム シリアル番号は ECD8-AD19 です
2019/10/02  17:18                66 test.txt
2019/10/02  18:07                34 error.txt
2019/10/02  18:10                16 ret.txt
2019/10/02  18:14    <DIR>          .
2019/10/02  18:14    <DIR>          ..

環境変数

すべてのプロセスは環境変数を使用できます。
環境変数にはユーザ毎のユーザ環境変数と、すべてのユーザ共通のシステム環境変数の2種類があります。
デフォルトの挙動では子プロセスは親プロセスの環境変数を継承します。

GUIでの環境変数の確認と設定

コントロールパネルの「システムとセキュリティ」を選択します。
image.png

「システム」を選択します
image.png

「システムの詳細設定」を選択します
image.png

システムのプロパティが表示されるので「環境変数」を押下します。
image.png

ユーザ毎の環境変数とシステムの環境変数が確認と変更が行えます。
image.png

コマンドプロンプトでの環境変数

環境変数の確認方法

SETコマンドを実行することで、現在の環境変数を確認できます。

すべての環境変数の一覧を確認するには下記のようなコマンドを実行します。

SET

特定の環境変数の内容を確認するには引数に環境変数名を指定します。

>SET SystemDrive
SystemDrive=C:

環境変数の設定方法

現在のプロセス中での環境変数を設定するにはSETコマンドに変数名と値を指定することで可能です。

SET test=ABCDE

もしコマンドプロンプト中の特殊文字「<、>、| 、&、^」を使用する場合はエスケープ文字である^を使用するか引用符で囲んで文字列として扱います

>set test=a^&n
>set test
test=a&n

>set test="a&n"
>set test
test="a&n"
環境変数の削除

設定した環境変数を削除するには以下のように環境変数だけ入力して値を空にします。

set test=
ユーザに環境変数を設定してもらう

ユーザに環境変数を入力させたい場合は以下のように/pオプションを使用します。

set /p test=

上記のコマンドを入力するとユーザの入力待ちとなり、入力した文字がtest環境変数の値として登録されます。

式を使用して環境変数を設定する

/aオプションを使用することで式を利用して環境変数を設定することが可能です。

>set /a test=10+1
11

数値が使用できますが、これは10進数ばかりでなく、16進数と8進数の使用が可能です。
負の数は使用可能ですが、小数点を使用することはできません。

>set /a test=0x10+1
17
>set /a test=010+1
9

また四則演算、論理演算、ビットシフト等がサポートされています。

>REM 足し算
>set /a test=1+2
3

>REM 引き算
>set /a test=1-2
-1

>REM 割り算
>set /a test=9/3
3

>REM 掛け算
>set /a test=3*2
6

>REM 剰余の例
>set /a test=9%4
1

>REM インクリメントの例
>set /a test=1
1
>set /a test+=1
2

>REM ビット演算の例
>REM 論理積
>set /a test="0x10&0x11"
16
>REM 論理和
>set /a test="0x10|0x11"
17
>REM 排他的論理和
>set /a test="0x10^0x11"
1

>REM ビットシフト
>set /a test="0x10>>1"
8
>set /a test="0x10<<1"
32

環境変数の有効範囲

子プロセスへの環境変数の継承

startコマンドでバッチファイルを呼び出し元プロセスの子プロセスとして実行した場合、実行された子プロセスには親プロセスの環境変数を引き継ぎます。

これは以下のようなコマンドで確認できます。

呼び出し元

@echo off
set ver1=testver
set ver1
start /wait cmd /d /c sub0.bat

sub0.bat(呼び出し先)

@echo off

echo sub... %ver1%
pause

呼び出し元のバッチを実行すると、新しいコマンドプロンプトが開いて下記の内容が表示されます。

sub1... testver

つまり、呼び出し元の親プロセスで指定した環境変数が子プロセスに反映されていることが確認できます。

cmdコマンドでのバッチ実行時の環境変数の検証

cmdコマンドでバッチファイルを呼び出した場合、呼び出し先の環境変数は呼び出し元のものが引き継がれますが、呼び出し先の環境変数は呼び出し元には反映されません。

これは以下のようなコマンドで確認できます。

呼び出し元

@echo off
set ver1=testver
set ver1
echo begin
cmd /d /c sub.bat
echo end
set ver1

sub.bat(呼び出し先)

@echo off

echo sub1... %ver1%
set ver1=subver
echo sub2... %ver1%

これの結果は以下のようになります。

ver1=testver
begin
sub1... testver
sub2... subver
end
ver1=testver

呼びだし元で設定した環境変数は呼び出し先に反映されることが確認できます。
逆に呼び出し先で設定した環境変数は呼び出し元には反映されていないことが確認できます。

callコマンドを使用した場合の環境変数の継承

callコマンドでバッチファイルを実行した場合、呼び出し元で設定した環境変数は呼び出し先でも反映されます。

これは以下のようなコマンドで確認できます。

呼び出し元

@echo off

set ver1=testver
set ver1
echo begin
call sub.bat
echo end
set ver1

呼び出し先はcmdコマンドでのバッチ実行時の環境変数の検証で使用したものと同じになります。
これの実行結果は以下のようになります。

ver1=testver
begin
sub1... testver
sub2... subver
end
ver1=testver

呼び出し元が設定した環境変数が呼び出し先に反映される事が確認できます。
また呼び出し先で設定した環境変数が呼び出し元に反映されることが確認できます。

環境変数の反映を制限する

setlocalコマンドを実行することで、バッチファイルの終了時に環境変数の内容を前の状態に戻すことが可能です。

この挙動を確認するには以下を試してみてください。

呼び出し元のサンプル

@echo off
set ver1=testver
set ver1
echo begin
call sublocal.bat
echo end
set ver1

sublocal.bat(呼び出し先)

@echo off
setlocal
echo sub1... %ver1%
set ver1=subver
echo sub2... %ver1%

これの実行結果は以下のようになります。

ver1=testver
begin
sub1... testver
sub2... subver
end
ver1=testver

setlocalコマンドがない場合は呼び出し元の環境変数が変更されていましたが、setlocalコマンドを実行することで呼び出し元の環境変数が呼び出し先の変更の影響を受けないことを確認できます。

setlocalコマンドを実行後に変更された環境変数はendlocalコマンドで戻すことが可能です。
endlocalコマンドの挙動を確認するには下記を実行してみてください。

@echo off
set ver1=testver
set ver1

setlocal
set ver1=testlocal
set ver1
endlocal

set ver1

この結果は以下のようになります。

ver1=testver
ver1=testlocal
ver1=testver

endlocalコマンド実行後に環境変数の内容が戻ることが確認できます。

endlocalコマンドはバッチファイルの外では有効ではありません。
また、バッチファイルの最後に暗黙的なendlocalコマンドが実行されます。

恒久的な環境変数の設定方法

前述のSETコマンドで設定した環境変数はプロセスを再起動すると再使用できません。
環境変数を恒久的に登録する場合は、SETXコマンドを使用します。

ユーザ環境変数を登録するには以下のようにコマンドを実行します。

setx test testvalue

システム環境変数を登録するには/mオプションを使用します。管理者権限のコマンドプロンプトで下記を実行してください。

setx  test testvalue /m

なお、SETXコマンドで追加したユーザ環境変数、システム環境変数はコマンドプロンプトを再起動した場合に反映されます。

また、SETXコマンドで環境変数を削除することはできません。

以下のようなコマンドで削除できるという非公式な言説もあります。

setx MYVAR ""

実際のシステム設定の環境変数情報からは削除されていませんし、公式のドキュメントで消せないと明言しているので、やめましょう。

You cannot use the setx command to remove values that have been added to the local or system environments.

https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/setx

リモートコンピュータの環境変数の設定

setxコマンドはリモートコンピュータ上の環境変数も変更可能です。
これを使用するにはリモートコンピュータ上の「Remote Registry」サービスを有効にする必要があります。

/sオプションでコンピュータ名、/uオプションでユーザ名、/pオプションでパスワードを指定することでリモートコンピュータの環境変数を変更できます。

setx /s IEWIN7 /u IEUser /p Passw0rd! test testvalue

変更された環境変数はログインをし直すことで有効になります。

なお、「Remote Registry」サービスが起動していない場合、以下のエラーが発生します。

エラー: 指定された操作を完了できませんでした。

環境変数の展開方法

環境変数名を%で囲うことで変数の値が展開されます。

たとえばechoコマンドを使用して、環境変数の値を展開して表示することが可能です。

>echo %SystemDrive%
C:

なお、環境変数は大文字、小文字を区別しないため以下のような記載をしても同じ値が取得できます。

>echo %SYSTEMDRIVE%
C:

なお、この項目についての公式情報は「help set」で確認できます。

環境変数を置換して展開

環境変数の特定の文字を置換して展開することが可能です。
その場合の構文は以下のようになります。

%環境変数:置換前=置換後%

たとえば環境変数PATHの「c:\」を「d:\」に変換するには以下のようにします。

echo %PATH:c:\=d:\%

また置換前の最初の一文字アスタリスクを使用することが可能です。
この場合の挙動は最初に見つけた文字だけが置換されます。実例を以下で紹介します。

>set test=ABCDEFG_ABCDEFG_ABCDEFG
>REM *を使用しない場合、すべての文字が置換されます。
>echo %test:CD=__%
AB__EFG_AB__EFG_AB__EFG

>REM *を使用した場合、発見した最初の文字のみ置換されます。
>echo %test:*CD=cd%
cdEFG_ABCDEFG_ABCDEFG

なお、置換後の文字を空文字とすることで、特定の文字を除去する用途にも使用できます。

環境変数の一部のみ展開
>set test=ABCDEFG_ABCDEFG_ABCDEFG
>REM 6文字目から取得する
>echo %test:~5%
FG_ABCDEFG_ABCDEFG

>REM 6文字目から2文字取得する
>echo %test:~5,2%
FG

>REM 最期の5文字を取得する
>echo %test:~-5%
CDEFG

>REM 最期の5文字を除いて取得する
>echo %test:~0,-5%
ABCDEFG_ABCDEFG_AB

動的な環境変数

SET によって表示される変数の一覧には表示されない動的な環境変数が存在します。
以下の環境変数は実行時に動的に展開されます。

変数名 説明
%CD% 現在のディレクトリ文字列に展開します。
%DATE% DATEコマンドと同じフォーマットで現在の日付に展開します。
%TIME% TIMEコマンドと同じフォーマットで現在の時刻に展開します。
%RANDOM% 0 から 32767 の間の任意の 10 進数に展開します。
%ERRORLEVEL% 現在の ERRORLEVEL の値に展開します。
%CMDEXTVERSION% 現在のコマンド プロセッサ拡張機能のバージョン番号に展開します。たとえば「cmd /k xxxx.bat」で起動したコマンドプロンプトでこの環境変数を展開した場合、「cmd /k xxxx.bat」となります。
%CMDCMDLINE% コマンド プロセッサを起動したオリジナル コマンド ラインに展開します。
%HIGHESTNUMANODENUMBER% このコンピューター上の最大の NUMA ノード番号に展開します。

バッチファイルにおける環境変数

バッチファイルの引数は%1~%9で取得できます。
すべてのパラメータを取得するには「%*」を使用します。
%0を指定した場合はバッチファイル名が取得できます。

以下で、その実行例を確認します。

param1.bat

@echo off
echo %*
echo %0
echo %1
echo %2
echo %3
echo %4
echo %5
echo %6
echo %7
echo %8
echo %9
>param1.bat p1 p2 p3 p4 p5 p6 p7 p8 p9
p1 p2 p3 p4 p5 p6 p7 p8 p9
param1.bat
p1
p2
p3
p4
p5
p6
p7
p8
p9
10個以上のパラメータを使用する方法

%には0~9しか指定できません。
10以上のパラメータを使用する場合はshiftコマンドを使用します。

shiftのパラメータとして/2を与えると2番目の引数から右にシフトしていきます。
この実行例は下記のようになります。

@echo off
shift /2
echo %*
echo %0
echo %1
echo %2
echo %3
echo %4
echo %5
echo %6
echo %7
echo %8
echo %9
>param2.bat p1 p2 p3 p4 p5 p6 p7 p8 p9 p10 p11 p12
p1 p2 p3 p4 p5 p6 p7 p8 p9 p10 p11 p12
param2.bat
p1
p3
p4
p5
p6
p7
p8
p9
p10
バッチファイルのパラメータで修飾

バッチファイルのパラメータは以下の修飾を行うことが可能です。

バッチパラメータ 説明
%~1 %1を展開し、周囲の引用符("")を削除します。たとえばパラメータに"abc"を与えた場合、abcに変換されます。
%~f1 %1を完全修飾パスに展開します。たとえば相対パスを指定した場合に絶対パスに変換されます
%~d1 %1をドライブ文字のみに展開します。
%~p1 %1をパスのみに展開します。
%~n1 %1をファイル名のみに展開します。
%~x1 %1をファイル名拡張子のみに展開します。
%~s1 %1をショートネームのみを含む完全修飾パスに展開します。
%~a1 %1に存在するファイルを指定した場合、そのファイル属性に展開します。
%~t1 %1に存在するファイルを指定した場合、ファイルの更新日時刻に展開します。
%~z1 %1に存在するファイルを指定した場合、ファイルのサイズに展開します。
%~$PATH:1 PATH環境変数にリストされているディレクトリを検索し、%1で指定したファイルを最初に見つけたディレクトリのフルパスに展開します。環境変数名が定義されていないか、検索でファイルが見つからない場合、この修飾子は空の文字列に展開されます。

これらの修飾子は組み合わせて使用できます。
たとえば「%~dp0」と記載した場合、バッチファイルの親ディレクトリに展開できます。

動的環境変数を上書きするのは避けてください。
たとえば、以下のように環境変数のERRORLEVELを書き換えても実際の最後に実行したコマンドのエラーコードを書き換えることはできませんし、環境変数を上書きすることで不整合が生じます。

rem this next command sets the error level to zero

CMD /C EXIT 0

set ERRORLEVEL=1

if ERRORLEVEL 1 echo Does this print?

ERRORLEVEL is not %ERRORLEVEL%より

もしERRORLEVELをクリアしたい場合は任意の成功するコマンドを実行する必要があります。

例:

>cmd /c exit /b 0
>ver >nul

https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/cmd

遅延環境変数の展開

環境変数は実行時には展開されません。
たとえば以下のバッチファイルを実行したとします。

@echo off
set VAR=before
if "%VAR%" == "before" (
    echo if1.....
    set VAR=after
    if "%VAR%" == "after" echo If 2...
    echo %VAR%
)

このバッチファイルの意図としては2つのIF文が共に真になる意図ですが、実際の結果は以下のようになります。

>tien.bat
if1.....
before

1つ目のIFの内での環境変数の設定した内容が反映されていないことがわかります。
実行時に環境変数を展開するためには感嘆符(!)を使用して遅延環境変数を使用する必要があります。

@echo off
set VAR=before
if "%VAR%" == "before" (
    echo if1.....
    set VAR=after
    if "!VAR!" == "after" echo If 2...
    echo !VAR!
)

既定の挙動では遅延環境変数は展開されません。
これを有効にするには2種類の方法があります。

cmd実行時に/vオプションを利用する

cmdマンド実行時に/vオプションを使用することで遅延環境変数を展開可能です。

>cmd /v:on /c tien2.bat
if1.....
If 2...
after
setlocalでENABLEDELAYEDEXPANSIONを指定する

setlocalコマンドにenabledelayedexpansionを指定することで遅延変数の展開が可能になります。
この設定はcmdのパラメータより優先されます。

@echo off
SETLOCAL ENABLEDELAYEDEXPANSION
set VAR=before
if "%VAR%" == "before" (
    echo if1.....
    set VAR=after
    if "!VAR!" == "after" echo If 2...
    echo !VAR!
)

このバッチファイルの実行結果は下記のようになります。

>tien3.bat
if1.....
If 2...
after

分岐

IFコマンドを使用することで分岐を記述できます。

ERRORLEVELの分岐

「IF ERRORLEVEL 番号」と記載することで最後のプログラムの実行で指定された番号以上の終了コードが返されたときに、条件が真になるように指定します。

>cmd /c exit 1
>IF ERRORLEVEL 1 (echo NG) ELSE (echo OK)
NG
>cmd /c exit 0
>IF ERRORLEVEL 1 (echo NG) ELSE (echo OK)
OK

ERRORLEVELの分岐として環境変数のERRORLEVELを使用することができます。
この場合は以下のいずれかの演算子を指定する必要があります。

  • EQU - 等しい
  • NEQ - 等しくない
  • LSS - より小さい
  • LEQ - 以下
  • GTR - より大きい
  • GEQ - 以上
>cmd /c exit 1
>IF %ERRORLEVEL% equ 0 (echo OK) ELSE (echo NG)
NG
>cmd /c exit 0
>IF %ERRORLEVEL% equ 0 (echo OK) ELSE (echo NG)
OK

ファイルの存在チェック

「IF EXIST ファイル名」と記載することでファイルの存在をチェックすることができます。

>IF EXIST 存在するファイル.txt (echo 真) ELSE (echo 偽)
真
>IF NOT EXIST 存在するファイル.txt (echo 真) ELSE (echo 偽)
偽
>IF EXIST 存在しない.txt (echo 真) ELSE (echo 偽)
偽
>IF NOT EXIST 存在しない.txt (echo 真) ELSE (echo 偽)
真

環境変数の存在チェック

「IF DEFINED 環境変数名」と記載することで環境変数の存在をチェックすることができます。

>set test=
>if DEFINED test (echo 真) ELSE (echo 偽)
偽

>if NOT DEFINED test (echo 真) ELSE (echo 偽)
真

>set test=abc
>if DEFINED test (echo 真) ELSE (echo 偽)
真

>if NOT DEFINED test (echo 真) ELSE (echo 偽)
偽

文字列の比較

==演算子を使用して文字列の比較が可能です。
既定では大文字小文字を区別しますが、/iオプションを使用することで大文字小文字を区別しないで比較をします。

>set test=abc
>IF %test% ==abc (echo 真) ELSE (echo 偽)
真
>IF %test% ==ABC (echo 真) ELSE (echo 偽)
偽
>IF /I %test% ==ABC (echo 真) ELSE (echo 偽)
真

以下のいずれかの演算子を使用して比較することも可能です。

  • EQU - 等しい
  • NEQ - 等しくない
  • LSS - より小さい
  • LEQ - 以下
  • GTR - より大きい
  • GEQ - 以上
>set test=abc
>IF  %test% equ abc (echo 真) ELSE (echo 偽)
真
>IF  %test% equ ABC (echo 真) ELSE (echo 偽)
偽
>IF NOT  %test% neq abc (echo 真) ELSE (echo 偽)
真
>IF NOT  %test% neq ABC (echo 真) ELSE (echo 偽)
偽
>IF /I  %test% equ ABC (echo 真) ELSE (echo 偽)
真

EQUなどの演算子を使用すると数値としても使用することが可能です。

>set num=10
>IF %num% GEQ 9 (echo 真) ELSE (echo 偽)
真

>IF %num% GEQ 10 (echo 真) ELSE (echo 偽)
真

>IF %num% GEQ 11 (echo 真) ELSE (echo 偽)
偽

>IF %num% LEQ 9 (echo 真) ELSE (echo 偽)
偽

>IF %num% LEQ 10 (echo 真) ELSE (echo 偽)
真

>IF %num% LEQ 11 (echo 真) ELSE (echo 偽)
真

繰り返し

forコマンドを使用することで繰り返し処理を記述できます。

セットの繰り返し

for {%%|%}<変数名> in (セット) do コマンド [コマンドライン引数]

in句の後で指定したセットを繰り返すことが可能です。
コマンドラインで実行する場合の例は以下のようになります。

>for %a in (ab,cd,ef,eataeta) do @(echo %a)
ab
cd
ef
eataeta

IN句のセットの書き方は以下のようにも記載できます。

>for %a in (ab;cd;ef;eataeta) do @(echo %a)
>for %a in (ab cd ef eataeta) do @(echo %a)

バッチファイルを使用する場合は以下のようになります。

dataset.bat

@echo off
for %%a in (ab,cd,ef,eataeta) do (
    echo %%a
)
>dataset.bat
ab
cd
ef
eataeta

ワイルドカードの使用

セットにはワイルドカードを指定してファイルを取得することができます。
バッチファイルで使用する場合、変数には「%%」を指定してください。

>for %a in (*.txt) do @(echo %a)
error.txt
list.txt
result.txt
ret.txt

>for %a in (c:\dev\bat\*) do @(echo %a)
c:\dev\bat\init.bat
c:\dev\bat\initCur.bat
c:\dev\bat\t.bat
c:\dev\bat\test.bat
c:\dev\bat\testchoise.bat
c:\dev\bat\testLs.bat
c:\dev\bat\testLs2.bat

既定ではディレクトリの取得はできません。

ディレクトリの取得

/dオプションを使用するとディレクトリを取得することができます。
バッチファイルで使用する場合、変数には「%%」を指定してください。

>for /d %a in (c:\dev\bat\*) do @(echo %a)
c:\dev\bat\bunki
c:\dev\bat\file
c:\dev\bat\func
c:\dev\bat\redirect
c:\dev\bat\run
c:\dev\bat\test
c:\dev\bat\test2
c:\dev\bat\ver

ファイルを再帰的に取得

/rオプションを使用すると指定したディレクトリを再帰的に探査可能です。
バッチファイルで使用する場合、変数には「%%」を指定してください。

>for /r c:\dev\ %a in (*.txt) do @(echo %a)
c:\dev\akanechan_powerpoint\Slide1_0.txt
c:\dev\akanechan_powerpoint\Slide1_1.txt
c:\dev\akanechan_powerpoint\Slide1_2.txt
c:\dev\bat\bunki\error.txt
c:\dev\bat\bunki\list.txt
c:\dev\bat\bunki\result.txt
c:\dev\bat\bunki\ret.txt
c:\dev\bat\file\1.txt
c:\dev\bat\file\2.txt

変数の展開の修飾子

ループで使用する変数については以下の修飾子が使用できます。
以下の説明は%Iを変数とした場合の修飾子の例になります。

    %~I         - すべての引用句 (") を削除して、%I を展開します。
    %~fI        - %I を完全修飾パス名に展開します。
    %~dI        - %I をドライブ文字だけに展開します。
    %~pI        - %I をパス名だけに展開します。
    %~nI        - %I をファイル名だけに展開します。
    %~xI        - %I をファイル拡張子だけに展開します。
    %~sI        - 展開されたパスは短い名前だけを含みます。
    %~aI        - %I をファイルの属性に展開します。
    %~tI        - %I ファイルの日付/時刻に展開します。
    %~zI        - %I ファイルのサイズに展開します。
    %~$PATH:I   - PATH 環境変数に指定されているディレクトリを
                   検索し、最初に見つかった完全修飾名に %I を
                   展開します。
                   環境変数名が定義されていない場合、または検索
                   してもファイルが見つからなかった場合は、この
                   修飾子を指定すると空の文字列に展開されます。

使用例は以下のようになります。

>for %I in (*.txt) do @echo %~fI %~dI %~pI %~nI %~xI %~sI %~aI %~tI  %~zI
C:\dev\bat\bunki\error.txt C: \dev\bat\bunki\ error .txt C:\dev\bat\bunki\error.txt --a-------- 2019/09/28 16:02  0
C:\dev\bat\bunki\list.txt C: \dev\bat\bunki\ list .txt C:\dev\bat\bunki\list.txt --a-------- 2019/09/28 02:50  81
C:\dev\bat\bunki\result.txt C: \dev\bat\bunki\ result .txt C:\dev\bat\bunki\result.txt --a-------- 2019/09/28 03:59  17
C:\dev\bat\bunki\ret.txt C: \dev\bat\bunki\ ret .txt C:\dev\bat\bunki\ret.txt --a-------- 2019/09/28 16:49  3
C:\dev\bat\bunki\te st.txt C: \dev\bat\bunki\ te st .txt C:\dev\bat\bunki\te st.txt --a-------- 2019/10/02 21:38  92
C:\dev\bat\bunki\test.txt C: \dev\bat\bunki\ test .txt C:\dev\bat\bunki\test.txt --a-------- 2019/10/02 21:38  92

値の範囲を繰り返す

/Lオプションを使用することで値の範囲を指定することができます。
書式としては以下のようになります。

for /l {%%|%}<変数名> in (開始値,増分,終了値) do コマンド [コマンドライン引数]

ループカウンタに使用する変数はコマンドラインで実行する場合は「%変数名」となり、バッチファイル中で使用する場合は「%%変数名」となります。
in句の後にはループカウンタの開始値、増分、終了値の順番で指定します。

コマンドプロンプトで実行する場合の例は以下のようになります。

>for /L %a in (0, 2, 9) do @(echo %a)
0
2
4
6
8

バッチファイルの場合は以下のようになります。

loop.bat

@echo off
for /L %%a in (0, 2, 9) do (
    echo %%a
)
>loop.bat
0
2
4
6
8

ファイルの解析

/Fオプションを使用することでファイルの内容を1行づつ解析できます。
たとえば以下のようなファイルが存在するとします。

test.txt

1234,5678,9abc
2345,6789,abcd
3456,789a,bcde
;aaa,bbb,cccコメント
ああああ,いいい,ううう

これを一行づつ処理するには以下のようにします。

>FOR /F %a in (test.txt) do @echo %a
1234,5678,9abc
2345,6789,abcd
3456,789a,bcde
ああああ,いいい,ううう

一行づつechoコマンドが実行されます。
ただし、;で開始している行はコメント行として処理されてループの対象となりません。
もしバッチファイル内で使用するには%aを%%aと置き換えます。

/Fオプションの後に以下のキーワードを使用することが可能です。

  • eol
  • skip
  • usebackq
  • delims
  • tokens

eolキーワード

eolで指定した1文字がコメント行として解釈されます。
eolを指定しない場合は「;」で開始される行がコメント行として処理されます。

>FOR /F "eol=1" %a in (test.txt) do @echo %a
2345,6789,abcd
3456,789a,bcde
;aaa,bbb,cccコメント
ああああ,いいい,ううう

skipキーワード

skip行で指定した行数を飛ばしてループの対象とします。
たとえば以下の場合、先頭の2行をスキップして行をループします。

>FOR /F "eol=# skip=2" %a in (test.txt) do @echo %a
3456,789a,bcde
;aaa,bbb,cccコメント
ああああ,いいい,ううう

usebackqキーワード

セットに指定するファイル名に二重引用符で囲むことを許可します。
これにより空白のファイル名を指定できます。

>FOR /F "usebackq" %a in ("te st.txt") do @echo %a
1234,5678,9abc
2345,6789,abcd
3456,789a,bcde
ああああ,いいい,ううう

delims、tokensキーワード

delimsキーワードは区切り文字を指定できます。デフォルトはスペースまたはTabになります。
tokensキーワードはどのトークンをループにわたすかを指定できます。

たとえば「,」区切って1つ目と3つ目を表示するには以下のように指定します。

>FOR /F "delims=, tokens=1,3" %a in (test.txt) do @echo %a %b
1234 9abc
2345 abcd
3456 bcde
ああああ ううう

たとえば「,」区切って2つ目~3つ目を表示するには以下のように指定します。

>FOR /F "delims=, tokens=2-3" %a in (test.txt) do @echo %a %b
5678 9abc
6789 abcd
789a bcde
いいい ううう

tokensに*を指定することで、解析される最後のトークンの後の行に残っているテキストを受け取ります。

>FOR /F "delims=, tokens=1,*" %a in (test.txt) do @echo %a %b
1234 5678,9abc
2345 6789,abcd
3456 789a,bcde
ああああ いいい,ううう

コンソールの操作

コンソールのクリア

clsコマンドを使用することでコンソールの内容をクリアできます。

タイトルの変更

titleコマンドを使用することでコマンドプロンプトのタイトルを変更できます。

title タイトルの変更

image.png

ポーズコマンド

pauseコマンドを使用することでキーボードの入力待ちが行えます。

>pause
続行するには何かキーを押してください . . .

ユーザ入力

choiceコマンドを使用することでユーザ入力を行えます。
ユーザが選択した結果は%ERRORLEVEL%に格納されます。

>choice /m 選択してください
選択してください [Y,N]?Y

>echo %ERRORLEVEL%
1

>choice /m 選択してください
選択してください [Y,N]?N

>echo %ERRORLEVEL%
2

既定ではY,Nの選択になりますが、/cオプションを使用することで選択項目をカスタマイズできます。

>choice /c ync1234567890 /m 選択してください
選択してください [Y,N,C,1,2,3,4,5,6,7,8,9,0]?2

>echo %ERRORLEVEL%
5

ファイル操作

カレントディレクトリの設定

カレントディレクトリの移動には2種類の方法があります。

cdまたはchdirによるカレントディレクトリの変更

cdコマンドまたはchdirコマンドでカレントディレクトリを変更します。

ドライブを変更する場合は/dオプションを使用してください。

cd c:\
chdir c:\dev
chdir /d e:\
cd /d c:\

スタックを使用したカレントディレクトリの変更

pushdコマンドpopdコマンドを使用することでスタックを使用してカレントディレクトリを変更できます。
これを呼び出し先のバッチファイルで使用することで呼び出し先で変更したカレントディレクトリを呼び出し元のカレントディレクトリに戻すことが可能になります。

以下にpushdコマンドと[popdコマンド]の実行例を記述します。

c:\>pushd c:\dev

c:\dev>pushd c:\dev\bat

c:\dev\bat>popd

c:\dev>popd

pushdで変更したディレクトリがpopdで元に戻ることが確認できると思います。

空ファイルの作成

copyコマンドを使用してnulでファイルを作成すると空ファイルが作成できます。

すでにあるファイルを空にする場合は/yオプションを使用して強制的に上書きします。

copy nul test.txt
copy /y nul test.txt

ディレクトリの作成

mdコマンドまたはmkdirコマンドでディレクトリを作成します。

中間ディレクトリがない場合は作成されます。
すでに存在するディレクトリを指定した場合はエラーとなります。

md test\test\test
mkdir test2\test\test

リンクの作成

mklinkコマンドを使用してシンボリックリンク、ハードリンク、ジャンクションが作成可能です。

これらの違いは下記を参照してください。

Windowsのシンボリックリンクとジャンクションとハードリンクの違い
https://www.atmarkit.co.jp/ait/articles/1306/07/news111.html

シンボリックリンクの作成

ディレクトリのシンボリックリンクを作成する場合は/Dオプションを使用してください。
またディレクトリのシンボリックリンクを作成する場合は管理者権限で実行する必要があります。

mklink slink_a.txt c:\dev\ps\file\link\a.txt
mklink sdir c:\dev\ps\file\link\sub2 /D

ハードリンクの作成

ハードリンクを作成するには/hオプションを使用してください。

mklink /H hardlink_a.txt c:\dev\ps\file\link\a.txt

ジャンクションの作成

/jオプションを使用することでジャンクションを作成できます。

mklink /j junction_dir c:\dev\ps\file\link\sub1

ファイルとディレクトリの列挙

一覧表示

dirコマンドを使用するとサブディレクトリの一覧を表示します。

ディレクトリを指定する場合、または?のワイルドカードが使用できます。は任意の文字列、?は任意の一文字以下をあらわします。
たとえばディレクトリは以下に以下のファイルが存在したとします。

  • abcde.txt
  • abcd.txt
  • abc.txt
  • ab.txt
  • a.txt
  • a.text
  • .txt

「*.txt」を指定して検索した場合以下のファイルが列挙されます。

  • abcde.txt
  • abcd.txt
  • abc.txt
  • ab.txt
  • a.txt

「????.txt」を指定した場合以下のファイルが列挙されます。

  • abcd.txt
  • abc.txt
  • ab.txt
  • a.txt

/aオプションを使用することで隠しファイルやシステムファイルも表示可能になります。
/sオプションを使用することでサブディレクトリの内容を表示します。

コマンドの使用例は以下のようになります。

REM 拡張子がtxtのファイルを列挙
dir C:\dev\ps\file\link\*.txt

REM 拡張子がtxtのファイルを列挙
dir C:\dev\ps\file\link\????.txt

REM 隠しファイル含めて列挙
dir C:\dev\ps\file\link\sub1 /a

REM サブディレクトリを列挙
dir C:\dev\ps\file\link\ /s

ツリー構造での表示

treeコマンドを使用することでディレクトリ構造をツリー構造で表示します。
/fオプションを使用することでファイルも表示可能ですが、隠しファイルを表示することはできません。

>tree /f c:\dev\bat\file
ディレクトリー パスの一覧:  ボリューム OS
ボリューム シリアル番号は ECD8-AD19 です
C:\DEV\BAT\FILE
│  .txt
│  a.text
│  a.txt
│  ab.txt
│  abc.txt
│  abcd.txt
│  abcde.txt
│
└─sub1
        test.txt
        test2.txt

ファイルの検索

whereコマンドを使用することで指定したファイルのパスを検索できます。

一致するファイルの検索パターンにはワイルドカード(*と?)が使用できます。ワイルドカードの挙動はdirコマンドで説明した挙動と同じです。ディレクトリを指定するか、環境変数を指定することが可能ですが、いずれも指定せずに検索した場合はカレントディレクトリとPATH環境変数で指定したディレクトリを検索します。
/Rオプションと組み合わせてディレクトリを指定することで特定のディレクトリ以下のサブディレクトリをすべて検索します。
隠しファイルが存在する場合、それも検索対象となります。

REMカレントディレクトリと環境変数PATH以下でtxt拡張子のファイルを検索する
where *.txt

REM環境変数Publicのディレクトリのファイルを検索する
where $PUBLIC:*.*

REM 特定のディレクトリを再帰的に検索して*.txtにマッチするファイルを検索する
where -R C:\dev\ps\file\link\sub1 *.txt

ファイルの削除

ファイルの削除にはdelコマンドを使用します。
削除対象のファイルはワイルドカード(*と?)が使用できます。ワイルドカードの挙動はdirコマンドで説明した挙動と同じです。
ゴミ箱には送らない、取り返しのつかない操作なので十分に注意して使用してください。

REM カレントディレクトリのtxt拡張子を削除する
del *.txt

REM 特定ディレクトリのtxt拡張子を削除する
del c:\dev\bat\*.txt

規定の動作では隠しファイルや読み取り専用ファイルを削除できません。
/fと/aオプションを付与して削除してください。

del /a /f *.txt

/pオプションを使用することで確認メッセージを表示可能になります。

>del /p  *.txt
C:\dev\bat\file\ab.txt を削除しますか (Y/N)? n
C:\dev\bat\file\abc.txt を削除しますか (Y/N)? n
C:\dev\bat\file\abcd.txt を削除しますか (Y/N)? n
C:\dev\bat\file\abcde.txt を削除しますか (Y/N)? n
C:\dev\bat\file\test.txt を削除しますか (Y/N)? n
C:\dev\bat\file\test2.txt を削除しますか (Y/N)? n

ディレクトリの削除

ディレクトリの削除にはrmdirコマンドまたはrdコマンドを使用します。ゴミ箱には送らない、取り返しのつかない操作なので十分に注意して使用してください。

/sオプションを指定することでサブディレクトリを含めて削除可能です。この際確認メッセージが表示されますが、確認メッセージを表示しないようにするには/qオプションを使用します。

>rmdir /s sub2
sub2、よろしいですか (Y/N)? y

>rmdir /s /q sub2

削除対象のディレクトリをカレントディレクトリとして使用しているプロセスがある場合以下のようなメッセージが表示されます。

>rmdir /s /q c:\dev\bat\file\sub2
プロセスはファイルにアクセスできません。別のプロセスが使用中です。

ファイルの改名

ファイルまたはディレクトリの改名にはrenコマンドまたはrenameコマンドを使用します。

ファイルの改名にはワイルドカードを使用することが可能です。ワイルドカードの挙動はdirコマンドで説明した挙動と同じです。ただし、隠しファイルは対象となりません。

REM c:\dev\bat\file\sub1ディレクトリをc:\dev\bat\file\sub1_renamedに変更します。
ren c:\dev\bat\file\sub1 sub1_renamed

REM 拡張子txtをdocに変更します。
ren *.txt *.doc

ファイルのコピー

copyコマンドを使用してコピー元のファイルを指定のディレクトリまたはファイルにコピーします。

REM ファイルを指定してコピー
>copy ab.doc z.doc
        1 個のファイルをコピーしました。

REM directoryを指定してコピー
>copy *.doc C:\dev\bat\file\sub2
ab.doc
abc.doc
abcd.doc
abcde.doc
test.doc
        5 個のファイルをコピーしました。

ディレクトリのコピー

xcopyコマンドを使用してディレクトリのコピーが可能です。

/sオプションでサブディレクトリのコピーも可能です。
/eオプションで空のサブディレクトリもコピーします。
/hオプションで隠しファイルのコピーも行います。デフォルトでは隠しファイルをコピーしません。
/kオプションでコピー元の読み取り専用属性をコピー先に反映させます。デフォルトでは反映されません。

REM 隠しファイルはコピーされない
xcopy c:\dev\bat\file\sub c:\dev\bat\file\target /s /e

REM 隠しファイルはコピーされる
xcopy c:\dev\bat\file\sub c:\dev\bat\file\target /s /e /h

コピーの挙動としては以下のようになります。

target 
  sub の子要素1
    孫要素
  sub の子要素2

ファイル/ディレクトリの移動

moveコマンドを使用してファイルまたはディレクトリを移動します。
/yオプションを使用することで移動先がすでに存在する場合に確認メッセージを表示しないで上書きしません。

>move ab.doc xxx.doc
        1 個のファイルを移動しました。

>move /y abcd.doc abc.doc
        1 個のファイルを移動しました。

>move sub2 sub3
        1 個のディレクトリを移動しました。

ファイルの内容表示

typeコマンドまたはmoreコマンドを使用することでファイルの内容を表示します。

typeコマンドは一度にすべて表示します。
moreコマンドは1ページづつ表示されます。

>type sjis.txt

>more sjis.txt

デフォルトのコードページ以外(日本語OSの場合はcp932)のファイルとUNICODE以外のテキストファイルを表示する場合はchcpコマンドを使用します。

REM BOMなしのUTF8を表示します。
>chcp 65001
>type utf8.txt

なお古いバージョンのWindowsだとフォントの都合で表示されません。cp932でフォントをMSゴシックを選択していれば表示される場合もありますが、正直、すべての環境での挙動が安定していないように見えます。
またchcpを使用した場合、moreコマンドを使用すると文字化けします。

SJISやUNICODE以外の文字コードを扱う場合は、PowerShellでも使った方が楽だと思います。

ファイルの連結

typeコマンドを使用するか、copyコマンドを使用します。

たとえば以下のような2ファイルがあるとします。

あいうえお
かきくけこ
さしすせそ
たちつてと
なにぬねの

1.txtの末尾に2.txtを結合するには以下のようにします。

REM 1.txtの末尾に2.txtが追記
type 2.txt >> 1.txt

REM 1.txtの後に2.txtが追記された結果がresult.txtに書き込まれる。
copy /b 1.txt+2.txt result.txt

copyコマンドの/bオプションはバイナリでの連結を意味します。これを付与しない場合、末尾に0x1A(CTRL+Z)が付与されます。

ファイルの属性変更

attribコマンドを使用することでファイルの属性を変更できます。

REM 読み取り専用にする
attrib result.txt +R

REM 読み取り専用を解除
attrib result.txt -R

REM 読み取り専用、隠しファイル、システムファイルとする
attrib result.txt +R +H +S

ファイルの所有者変更

管理者権限でicacls コマンドを使用することでファイルの所有者を変更可能です。

icacls result.txt /setowner NOTE-MAIN\mima

ファイル中の文字検索

findコマンドまたはfindstrコマンドを使用します。

>find "あい" *.txt

---------- 1.TXT
あいうえお

---------- 2.TXT

---------- RESULT.TXT
あいうえお

サブディレクトリも検索する場合はfindstrコマンドの/sオプションを使用します。

>findstr /s "あい" *.txt
1.txt:あいうえお
result.txt:あいうえお
sub\1.txt:あいうえお

ファイルの比較

compコマンドまたは、fcコマンドを使用します。

compコマンド

compコマンドはバイナリでファイルを比較します。
/mオプションを付けない場合、続けてファイルを比較するようなプロンプトを表示します。
ファイルの比較結果が同一の場合、ERRORLEVELは0となります。
ファイルの比較結果が異なる場合、ERRORLEVELは1となります。
構文エラーがある場合、ERRORLEVELは2となります。

REM ファイルが等しい場合はERRORLEVELは0となります。
>comp /m 1.txt 1.txt
1.txt と 1.txt を比較しています...
ファイルに違いはありません

>echo %ERRORLEVEL%
0

REM ファイルサイズが異なる場合
>comp /m 1.txt 4.txt
1.txt と 4.txt を比較しています...
ファイルのサイズが違います。

>echo %ERRORLEVEL%
1

REM ファイルサイズが等しくて内容が異なる場合
>comp /m 1.txt 3.txt
1.txt と 3.txt を比較しています...
OFFSET 11 で比較エラーがあります
ファイル1 = AD
ファイル2 = AF
OFFSET 1B で比較エラーがあります
ファイル1 = B5
ファイル2 = A2

>echo %ERRORLEVEL%
1

fcコマンド

fcコマンドはASCII比較またはバイナリーでの比較が行えます。

ファイルの比較結果が同一の場合、ERRORLEVELは0となります。
ファイルの比較結果が異なる場合、ERRORLEVELは1となります。
構文エラーがある場合、ERRORLEVELは2となります。

/Bオプションを使用することでバイナリの比較を実行します。
/Uオプションを使用することでUNICODEで比較を実行します。
ASCII比較時に/Nオプションを使用することで行番号を表示します。

REM 相違点のあるファイルをASCIIで比較
>fc /N  fc1sjis.txt fc2sjis.txt
ファイル fc1sjis.txt と FC2SJIS.TXT を比較しています
***** fc1sjis.txt
    4:  あいうえお
    5:  かきくけこ
    6:  a
***** FC2SJIS.TXT
    4:  あいうえお
    5:  かきけっけけこ
    6:  a
*****

***** fc1sjis.txt
   10:  35235235235
   11:  3252353efwrwerwrwr
***** FC2SJIS.TXT
   10:  35235235235
   11:  ああああ
   12:  3252353efwrwerwrwr
*****

>echo %ERRORLEVEL%
1

REM 相違点のないファイルを比較
>fc /N  fc1sjis.txt fc1sjis.txt
ファイル fc1sjis.txt と FC1SJIS.TXT を比較しています
FC: 相違点は検出されませんでした

>echo %ERRORLEVEL%
0

REM バイナリモードで比較を行う
>fc /B  1.txt 3.txt
ファイル 1.txt と 3.TXT を比較しています
00000011: AD AF
0000001B: B5 A2

>echo %ERRORLEVEL%
1

C:\dev\bat\file>fc /B  1.txt 4.txt
ファイル 1.txt と 4.TXT を比較しています
00000010: 82 61
00000011: AD 82
REM 略
00000021: BB 82
FC: 4.TXT は 1.txt より長いファイルです

>echo %ERRORLEVEL%
1

REM UNICODEのファイルを比較する
>fc /N /U  fc1unicode.txt fc2unicode.txt
ファイル fc1unicode.txt と FC2UNICODE.TXT を比較しています
***** fc1unicode.txt
    4:  あいうえお
    5:  かきくけこ
    6:  a
***** FC2UNICODE.TXT
    4:  あいうえお
    5:  かきけっけけこ
    6:  a
*****

***** fc1unicode.txt
   10:  35235235235
   11:  3252353efwrwerwrwr
***** FC2UNICODE.TXT
   10:  35235235235
   11:  ああああ
   12:  3252353efwrwerwrwr
*****

レジストリの操作

レジストリの操作はregコマンドを使用しておこないます。

レジストリの詳細は下記を参照してください。
https://needtec.sakura.ne.jp/wod07672/?p=9279

レジストリの参照

regコマンドqueryを用いることでレジストリの参照が可能です。

/vオプションを使用することで値を指定してレジストリの内容を表示します。
/sオプションを使用することで再帰的にレジストリキーの内容を表示します。

REM レジストリ―キーの内容を確認する
>reg query HKCU\test\a\b\

HKEY_CURRENT_USER\test\a\b
    (既定)    REG_SZ
    data    REG_SZ    test2
    test    REG_EXPAND_SZ    %PATH%

REM 特定のレジストリ―キーの値の内容を確認する
>reg query HKCU\test\a\b\ /v test

HKEY_CURRENT_USER\test\a\b
    test    REG_EXPAND_SZ    %PATH%

REM レジストリキーを再帰的に確認する
>reg query HKCU\test\ /s

HKEY_CURRENT_USER\test\a
    x    REG_SZ    abc

HKEY_CURRENT_USER\test\a\b
    (既定)    REG_SZ
    data    REG_SZ    test2
    test    REG_EXPAND_SZ    %PATH%

リモートコンピュータ側のRemote Registryサービスを起動することでコンピュータ名を指定してレジストリの参照が行えます。

>reg query \\IEWin7\HKLM\SOFTWARE

レジストリの登録

regコマンドaddを用いることでレジストリの登録が可能です。

レジストリキーを登録する場合は以下を実行します。

reg add HKCU\test\a\b

指定したキーの途中が存在しない場合でもエラーとならず作成されます。
またすでにキーが存在する場合は下記の確認メッセージが表示されます。

値  は存在します。上書きしますか? (Yes/No)

レジストリの値を追加するには以下のようになります。

reg add HKCU\test\a\b /v data /t REG_SZ /d test2

指定したキーが存在しない場合でもエラーとならず作成されます。
またすでにキーが存在する場合は確認メッセージが表示されます。

リモートコンピュータ側のRemote Registryサービスを起動することでコンピュータ名を指定してレジストリの追加が行えます。

>reg add \\IEWin7\HKLM\SOFTWARE\test\a\b /v data /t REG_SZ /d test2

レジストリのエクスポート

regコマンドexportを用いることでレジストリの内容をファイルにエクスポートすることが可能です。

reg export HKCU\test\ export.reg

レジストリのインポート

regコマンドimportを用いることでファイルの内容をレジストリにインポートすることが可能です。

reg import  export.reg

スリープ処理

コマンドプロンプトやバッチファイルでスリープ処理を入れる方法は色々あります。

  • timeoutコマンド
  • pingコマンド
  • powershellでスリープ

timeoutコマンド

timeoutコマンドを使用することでスリープ処理がおこなえます。

5秒待機する場合は以下のように記載します。

timeout /t 5 /nobreak > nul

なお、timeoutコマンドを以下のように記載することでpauseコマンドかわりにしようできます。

>timeout /t -1

続行するには何かキーを押してください ...

pingコマンド

pingコマンドのタイムアウトを使用する方法もよくつかわれます。
timeoutと違いミリ秒で指定できます。

5秒待機する場合は以下のように存在しないホストに対してPingを実行します。

ping 111.111.111.111 -n 1 -w 5000 > nul

powershellでスリープ

PowerShellが入っている環境ではPowerShellのStart-Sleepコマンドを使用するといいでしょう。

5秒待機する場合は以下のように記載します。

powershell -command "Start-Sleep 5.0"
powershell -command "Start-Sleep -Milliseconds 5000"

コンピュータの操作

OSのバージョンを表示する

verコマンドでオペレーションシステムのバージョンが取得できます。

>ver
Microsoft Windows [Version 10.0.17134.1006]

ホスト名を表示する

hostコマンドでホスト名を表示します。

>hostname
NOTE-MAIN

ネットワーク構成の情報を表示

[ipconfigコマンド]を使用することで現在のすべてのTCP / IPネットワーク構成値を表示できます。

>ipconfig

Windows IP 構成

イーサネット アダプター VirtualBox Host-Only Network:

   接続固有の DNS サフィックス . . . . .:
   リンクローカル IPv6 アドレス. . . . .: fe80REMdd98:fe1c:2a3c:ff4d%21
   IPv4 アドレス . . . . . . . . . . . .: 192.168.56.1
   サブネット マスク . . . . . . . . . .: 255.255.255.0
   デフォルト ゲートウェイ . . . . . . .:

システム情報

systeminfoコマンドを使用することでローカルコンピュータまたはリモートコンピュータのシステム情報を表示します。

REM ローカルコンピュータのシステム情報
>systeminfo

REM リモートコンピュータのシステム情報を表示する
>systeminfo /S IEWin7 /U IEUser /P Passw0rd!
ホスト名:               IEWIN7
OS 名:                  Microsoft Windows 7 Enterprise
OS バージョン:          6.1.7601 Service Pack 1 ビルド 7601
OS 製造元:              Microsoft Corporation
OS 構成:                スタンドアロン ワークステーション
OS ビルドの種類:        Multiprocessor Free
登録されている所有者:
登録されている組織:     Microsoft
プロダクト ID:          00392-972-8000024-85432
最初のインストール日付: 2018/03/06, 19:59:43
システム起動時間:       2019/10/01, 16:02:36
システム製造元:         VMware, Inc.
システム モデル:        VMware Virtual Platform
システムの種類:         x64-based PC
プロセッサ:             1 プロセッサインストール済みです。
                        [01]: Intel64 Family 6 Model 142 Stepping 9 GenuineIntel ~2712 Mhz
BIOS バージョン:        Phoenix Technologies LTD 6.00, 2019/07/29
Windows ディレクトリ:   C:\Windows
システム ディレクトリ:  C:\Windows\system32
起動デバイス:           \Device\HarddiskVolume1
システム ロケール:      ja;日本語
入力ロケール:           en-us;英語 (米国)
タイム ゾーン:          (UTC-08:00) 太平洋標準時 (米国およびカナダ)
物理メモリの合計:       2,047 MB
使用できる物理メモリ:   1,609 MB
仮想メモリ: 最大サイズ: 4,095 MB
仮想メモリ: 使用可能:   3,659 MB
仮想メモリ: 使用中:     436 MB
ページ ファイルの場所:  C:\pagefile.sys
ドメイン:               WORKGROUP
ログオン サーバー:      N/A
ホットフィックス:       219 ホットフィックスがインストールされています。
                        [01]: KB2849697
                        [02]: KB2849696
~~~~~~略~~~~~~~~~~
                        [218]: KB982018
                        [219]: KB4516065
ネットワーク カード:    1 NIC(s) インストール済みです。
                        [01]: Intel(R) PRO/1000 MT Network Connection
                              接続名:           Local Area Connection 2
                              DHCP が有効:      はい
                              DHCP サーバー:    192.168.80.254
                              IP アドレス
                              [01]: 192.168.80.128
                              [02]: fe80REM151b:4382:b14b:d9e8
Hyper-V の要件:         VM モニター モード拡張機能: いいえ
                        ファームウェアで仮想化が有効になっています: いいえ
                        第 2 レベルのアドレス変換: いいえ
                        データ実行防止が使用できます: はい

タスクの一覧を表示する

tasklistコマンドを使用することでタスクの一覧を表示できます。

REM ローカルコンピュータのタスク一覧を表示
>tasklist

REM リモートコンピュータのタスク一覧を表示
>tasklist /S IEWin7 /U IEUser /P Passw0rd!

イメージ名                     PID セッション名     セッション# メモリ使用量
========================= ======== ================ =========== ============
System Idle Process              0                            0         24 K
System                           4                            0        960 K
smss.exe                       256                            0      1,024 K
csrss.exe                      324                            0      4,844 K
略

/fiオプションを使用することでフィルタを使用できます。
たとえばイメージ名がchrome.exeと一致するタスクの一覧を取得するには以下のようになります。

>tasklist /fi "IMAGENAME eq chrome.exe"
フィルタ名 有効なオペレータ 有効な値
STATUS eq, ne RUNNING
IMAGENAME eq, ne イメージ名
PID eq, ne, gt, lt, ge, le PID value
SESSION eq, ne, gt, lt, ge, le Session number
SESSIONNAME eq, ne Session名
CPUTIME eq, ne, gt, lt, ge, le CPUタイムを「HH:MM:SS」形式で指定します。MMとSSは0 ~ 59、 HH は正の数値です
MEMUSAGE eq, ne, gt, lt, ge, le メモリの使用量(KB)
USERNAME eq, ne Any valid user name
SERVICES eq, ne Service name
WINDOWTITLE eq, ne Windowタイトル
MODULES eq, ne DLL名を指定してDLLを使用しているタスクの一覧を取得します

タスクを終了する

taskkillコマンドを使用してプロセスIDまたはイメージ名を指定してタスクを終了できます。

REM プロセスIDを指定して終了する
>taskkill /pid 26220 /pid 30392
成功: PID 26220 のプロセスに強制終了のシグナルを送信しました。
成功: PID 30392 のプロセスに強制終了のシグナルを送信しました。

REM イメージ名を指定して終了する
>taskkill /im notepad.exe
成功: プロセス "notepad.exe" (PID 30396) に強制終了のシグナルを送信しました。
成功: プロセス "notepad.exe" (PID 36840) に強制終了のシグナルを送信しました。

REM イメージ名にワイルドカードを指定して終了する
>taskkill ./im note*
成功: プロセス "notepad2.exe" (PID 31492) に強制終了のシグナルを送信しました。
成功: プロセス "notepad.exe" (PID 1140) に強制終了のシグナルを送信しました。

REM リモートコンピュータのタスクを終了する
>taskkill /s IEWin7 /u IEUser /p Passw0rd! /im notepad.exe
成功: プロセス "notepad.exe" (PID 996) は強制終了されました。

またtasklistコマンドと同様に/fiオプションを使用してフィルタをかけて一括でタスクを終了できます。

>taskkill /fi "IMAGENAME eq notepad.exe"

シャットダウン、再起動、ログオフ

shutdownコマンドでシャットダウン、再起動、ログオフが可能です。

REM ログオフをする
shutdown /l

REM ただちに再起動をする
shutdown /r /t 0

REM ただちにシャットダウンする
shutdown /s /t 0

まとめ

以上、簡単にコマンドプロンプトの操作やバッチファイルの使用例を公式ドキュメントベースでまとめました。

意外と大きな会社がやっているサイトのホームページでも、コマンドプロンプトやバッチファイルの使い方についての記事で参考文献に公式ドキュメントのリンクはっていない場合があるので、レビュアーが公式ドキュメント厨のときには攻略に役に立つと思います。

また、現在はPowerShellを使った方が楽だと思いますが、コマンドプロンプトでの操作やバッチファイルは自動化の基本となるものなので、軽く覚えておいた方が便利だと思います。

おまけ:「PowerShellが実行できないよ!」

どういう状況かをいっているか認識があっているかわかりませんが、初期状態でスクリプトが実行ポリシーの問題で実行できないといっていると仮定します。

http://totech.hateblo.jp/entry/2017/09/29/162411

上記にあるようにやり方としては管理者権限をもって実行ポリシーを変更するか、以下のようなコマンドでスクリプトを実行します。これには管理者権限は不要です。

powershell -ExecutionPolicy RemoteSigned .\test.ps1

C#やPowerShellで画面上の特定の画像の位置をクリックする方法

まえがき

以前、こんな記事を書いたことがあります。
色々な方法でWindowsのGUIの自動操作を行う方法を記載しましたが、PowerShellで画像認識を利用した自動操作については逃げました。

今回は宿題として残っていたPowerShellとOpenCVを使用して画像認識での自動操作を行ってみます。

考え方としてはスクリーンキャプチャした内容をMatに変換してTemplate Matchingを行うだけです。

OpenCVの.NET用のラッパー

OpenCVには.NET用のラッパーとしてOpenCvSharpが存在します。
https://github.com/shimat/opencvsharp/releases

このライブラリをNugetまたは上記のページからダウンロードしてください。
注意点として、ネイティブのDLLを使うことになるので32bit、64bitのどちらのプロセスで動作しているか、意識してDLLを利用してください。

C#のサンプル

VisualStudio 2019の.NET Framework4.0で作成したサンプルは以下のようになります。

using OpenCvSharp;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Runtime.InteropServices;
using System.Windows.Forms;

namespace OpenCv
{
    public class GuiAuto
    {
        // https://culage.hatenablog.com/entry/20130611/1370876400
        [DllImport("user32.dll")]
        extern static uint SendInput(uint nInputs, INPUT[] pInputs, int cbSize);

        [StructLayout(LayoutKind.Sequential)]
        struct INPUT
        {
            public int type;
            public MOUSEINPUT mi;
        }

        [StructLayout(LayoutKind.Sequential)]
        struct MOUSEINPUT
        {
            public int dx;
            public int dy;
            public int mouseData;
            public int dwFlags;
            public int time;
            public IntPtr dwExtraInfo;
        }

        const int MOUSEEVENTF_LEFTDOWN = 0x0002;
        const int MOUSEEVENTF_LEFTUP = 0x0004;

        static public void Click()
        {
            //struct 配列の宣言
            INPUT[] input = new INPUT[2];
            //左ボタン Down
            input[0].mi.dwFlags = MOUSEEVENTF_LEFTDOWN;
            //左ボタン Up
            input[1].mi.dwFlags = MOUSEEVENTF_LEFTUP;
            //イベントの一括生成
            SendInput(2, input, Marshal.SizeOf(input[0]));
        }

        static public void Move(int x, int y)
        {
            var pt = new System.Drawing.Point(x, y);
            System.Windows.Forms.Cursor.Position = pt;
        }

        public class TemplateResult
        {
            public int TargetWidth { set; get; }
            public int TargetHeight { set; get; }

            public List<OpenCvSharp.Point> MatchList { set; get; }

            public TemplateResult()
            {
                this.MatchList = new List<OpenCvSharp.Point>();
            }
        }

        static public TemplateResult MatchTemplate(int ScreenNo, string targetPath, double threshold) 
        {
            TemplateResult result = new TemplateResult();
            var screen = Screen.AllScreens[ScreenNo];

            Bitmap bitmap = new Bitmap(screen.Bounds.Width, screen.Bounds.Height);
            Graphics graphics = Graphics.FromImage(bitmap as Image);
            graphics.CopyFromScreen(screen.Bounds.X, screen.Bounds.Y, 0, 0, bitmap.Size);

            using (var targetImg = Cv2.ImRead(targetPath))
            using (var img = OpenCvSharp.Extensions.BitmapConverter.ToMat(bitmap))
            using (var img3ch = img.CvtColor(ColorConversionCodes.BGRA2BGR))
            {
                result.TargetWidth = targetImg.Width;
                result.TargetHeight = targetImg.Height;

                var tmplRet = img3ch.MatchTemplate(targetImg, TemplateMatchModes.CCoeffNormed);
                double minVal, maxVal;
                OpenCvSharp.Point minLoc, maxLoc;
                tmplRet.MinMaxLoc(out minVal, out maxVal, out minLoc, out maxLoc);
                Mat thresholdRet = tmplRet.Threshold(threshold, 1.0, ThresholdTypes.Tozero);
                while (true)
                {
                    thresholdRet.MinMaxLoc(out minVal, out maxVal, out minLoc, out maxLoc);
                    if (maxVal < threshold)
                    {
                        break;
                    }
                    result.MatchList.Add(maxLoc);
                    thresholdRet.FloodFill(maxLoc, 0);
                }
            }
            return result;
        }

        static public bool ClickImg(int ScreenNo, string targetPath, double threshold, int offsetX, int offsetY)
        {
            TemplateResult tmplRet = MatchTemplate(ScreenNo, targetPath, threshold);
            if (tmplRet.MatchList.Count == 0)
            {
                return false;
            }
            var screen = Screen.AllScreens[ScreenNo];

            Move(screen.Bounds.X + tmplRet.MatchList[0].X, screen.Bounds.Y + tmplRet.MatchList[0].Y);
            Click();
            return true;
        }
        static public bool ClickImg(int ScreenNo, string targetPath, double threshold)
        {
            TemplateResult tmplRet = MatchTemplate(ScreenNo, targetPath, threshold);
            if (tmplRet.MatchList.Count == 0)
            {
                return false;
            }
            var screen = Screen.AllScreens[ScreenNo];

            Move(screen.Bounds.X + tmplRet.MatchList[0].X + tmplRet.TargetWidth/ 2, screen.Bounds.Y + tmplRet.MatchList[0].Y + tmplRet.TargetHeight / 2);
            Click();
            return true;
        }
    }

    class Program
    {

        static void Main(string[] args)
        {
            Console.ReadLine();
            var targetPath = @"target.bmp";
            GuiAuto.ClickImg(0, targetPath, 0.75);
        }
    }
}

このサンプルはスクリーン上に存在するtarget.bmpの画像を検索してクリックするものとなっています。
やっている内容としてはOpenCvのチュートリアルのTemplate Matchingと似たようなことです。
MatchTemplateは複数の類似画像の位置を取得できるようにFloodFillを実施してループしていますが、常に最も一致した画像だけを取得するならループは不要です。

あとは、取得した位置をもとにマウスを移動してクリックしています。
なお、マルチディスプレイを考慮しているので、ClickImgのScreenNoを変更することで別のスクリーンを検索することが可能です。

スクリーン上の画像の取得は.NETのよくあるキャプチャ処理で、取得したBitmapオブジェクトはOpenCvSharp.Extensions.BitmapConverter.ToMatで行っています。

OpenCvSharpは.NET2.0でも動作するのですが、どうも.NET2.0ではOpenCvSharp.Extensions.dllを提供していないようです。
自前でBitmapConvert.csと同様な処理を実装すればできるかもしれませんが、.NET3.5までは簡単にできましたが、.NET2.0ではうまくいきませんでした。

PowerShell 5.1の例

Windows10 Home + PowerShell5.1でもC#と同様のことが行えます。

まず、DLLを以下のように配置します。
image.png

OpenCvSharpExtern.dllは使用するPowerShellがx86の場合はx86,x64の場合はx64を使用してください。

次に以下のようなスクリプトを記述して実行します。

$source = @"
using OpenCvSharp;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Runtime.InteropServices;
using System.Windows.Forms;
public class GuiAuto
{
    // https://culage.hatenablog.com/entry/20130611/1370876400
    [DllImport("user32.dll")]
    extern static uint SendInput(uint nInputs, INPUT[] pInputs, int cbSize);

    [StructLayout(LayoutKind.Sequential)]
    struct INPUT
    {
        public int type;
        public MOUSEINPUT mi;
    }

    [StructLayout(LayoutKind.Sequential)]
    struct MOUSEINPUT
    {
        public int dx;
        public int dy;
        public int mouseData;
        public int dwFlags;
        public int time;
        public IntPtr dwExtraInfo;
    }

    const int MOUSEEVENTF_LEFTDOWN = 0x0002;
    const int MOUSEEVENTF_LEFTUP = 0x0004;

    static public void Click()
    {
        //struct 配列の宣言
        INPUT[] input = new INPUT[2];
        //左ボタン Down
        input[0].mi.dwFlags = MOUSEEVENTF_LEFTDOWN;
        //左ボタン Up
        input[1].mi.dwFlags = MOUSEEVENTF_LEFTUP;
        //イベントの一括生成
        SendInput(2, input, Marshal.SizeOf(input[0]));
    }

    static public void Move(int x, int y)
    {
        var pt = new System.Drawing.Point(x, y);
        System.Windows.Forms.Cursor.Position = pt;
    }

    public class TemplateResult
    {
        public int TargetWidth { set; get; }
        public int TargetHeight { set; get; }

        public List<OpenCvSharp.Point> MatchList { set; get; }

        public TemplateResult()
        {
            this.MatchList = new List<OpenCvSharp.Point>();
        }
    }

    static public TemplateResult MatchTemplate(int ScreenNo, string targetPath, double threshold) 
    {
        TemplateResult result = new TemplateResult();
        var screen = Screen.AllScreens[ScreenNo];

        Bitmap bitmap = new Bitmap(screen.Bounds.Width, screen.Bounds.Height);
        Graphics graphics = Graphics.FromImage(bitmap as Image);
        graphics.CopyFromScreen(screen.Bounds.X, screen.Bounds.Y, 0, 0, bitmap.Size);

        using (var targetImg = Cv2.ImRead(targetPath))
        using (var img = OpenCvSharp.Extensions.BitmapConverter.ToMat(bitmap))
        using (var img3ch = img.CvtColor(ColorConversionCodes.BGRA2BGR))
        {
            result.TargetWidth = targetImg.Width;
            result.TargetHeight = targetImg.Height;

            var tmplRet = img3ch.MatchTemplate(targetImg, TemplateMatchModes.CCoeffNormed);
            double minVal, maxVal;
            OpenCvSharp.Point minLoc, maxLoc;
            tmplRet.MinMaxLoc(out minVal, out maxVal, out minLoc, out maxLoc);
            Mat thresholdRet = tmplRet.Threshold(threshold, 1.0, ThresholdTypes.Tozero);
            while (true)
            {
                thresholdRet.MinMaxLoc(out minVal, out maxVal, out minLoc, out maxLoc);
                if (maxVal < threshold)
                {
                    break;
                }
                result.MatchList.Add(maxLoc);
                thresholdRet.FloodFill(maxLoc, 0);
            }
        }
        return result;
    }

    static public bool ClickImg(int ScreenNo, string targetPath, double threshold, int offsetX, int offsetY)
    {
        TemplateResult tmplRet = MatchTemplate(ScreenNo, targetPath, threshold);
        if (tmplRet.MatchList.Count == 0)
        {
            return false;
        }
        var screen = Screen.AllScreens[ScreenNo];

        Move(screen.Bounds.X + tmplRet.MatchList[0].X, screen.Bounds.Y + tmplRet.MatchList[0].Y);
        Click();
        return true;
    }
    static public bool ClickImg(int ScreenNo, string targetPath, double threshold)
    {
        TemplateResult tmplRet = MatchTemplate(ScreenNo, targetPath, threshold);
        if (tmplRet.MatchList.Count == 0)
        {
            return false;
        }
        var screen = Screen.AllScreens[ScreenNo];

        Move(screen.Bounds.X + tmplRet.MatchList[0].X + tmplRet.TargetWidth/ 2, screen.Bounds.Y + tmplRet.MatchList[0].Y + tmplRet.TargetHeight / 2);
        Click();
        return true;
    }
}
"@
$dllPath = Split-Path $MyInvocation.MyCommand.Path
Set-Item Env:Path "$Env:Path;$dllPath"

Write-Host $currentDir
$assemblies = @(
    "$dllPath\OpenCVSharp.dll", 
    "$dllPath\OpenCvSharp.Extensions.dll", 
    "System.Runtime", 
    "System.Windows.Forms", 
    "System.Drawing"
)
Add-Type -TypeDefinition $source -ReferencedAssemblies $assemblies
Add-Type -Path "$dllPath\OpenCVSharp.dll"
Add-Type -Path "$dllPath\OpenCVSharp.Extensions.dll"
[GuiAuto]::ClickImg(0, "C:\dev\ps\opencv\target.bmp", 0.75)

実行結果

target.bmp
image.png

画面の状態
image.png

初期状態のWindows7のPowerShellでできないか?

難しいです。
理由として初期状態のWindows7では.NET3.5とPowerShell2.0が入っていますが、このPowerShell2.0はどんな新しい.NET Frameworkが入っていても.NET2.0を使用してしまいます。

PowerShellでdllを読み込む際の注意点
https://qiita.com/icoxfog417/items/e0d29bed109071888f19

このため、BitmapConvert.csと同様の処理が、うまく実装できませんでした。

やるなら、.NET Framework3.5でコマンドラインツールを作成して、PowerShellから呼び出す用な形になると思います(当然、起動時にオーバーヘッドがかかります)

まとめ

画像認識とかいうと難しく考えがちですが、OpenCvを利用すれば、わりと簡単に画像を利用した自動操作を自前でつくれます。
ただし、あまり古すぎる環境だと辛いです。

Webアプリケーションを自動で操作してみよう

はじめに

Webアプリケーションに対してある種の繰り返しの操作を行ったり、定型処理を定期的に自動実行したい場合がよくあります。
大きくわけてWebアプリケーションの自動化には3種類のやり方が存在します。

1つ目はブラウザのGUI上の操作をプログラム上で真似して自動化する方法
2つ目はブラウザから送信しているデータを真似する方法
3つ目はWebアプリケーションが提供しているAPIを利用する方法

1つ目のブラウザのGUI上の操作をプログラム上で真似して自動化する方法は直観的にわかりやすいと言われますが、実際は最も難しい自動化の方法になります。また、アプリケーションのバージョンアップに伴い自動化用のプログラムが動作しなくなる可能性があります。

2つ目のブラウザから送信しているデータを真似する方法はプログラムで実装しやすいやり方ではありますが、Webアプリケーションがどのようなデータを送信しているかなどを調べる必要があります。また、1つ目と同様にアプリケーションのバージョンアップに伴い自動化用のプログラムが動作しなくなる可能性があります。

3つ目のWebアプリケーションが提供しているAPIを利用する方法が最も簡単でかつ正確に自動操作を行えます。ただし、WebアプリケーションのAPIが提供されているかどうかは自動操作対象のWebアプリケーションの仕様次第です。

実験環境

Windows10+PowerShell5.1
Visual Studio 2019 + .NET Framework 4.6
Python 3.7.4
NodeJs v10.16.0
UiPath 2019.8.0-beta 83

ブラウザのGUI上の操作をプログラム上で真似して自動化する方法

Windowsの場合、ブラウザを操作して自動化する方法も大きく4つの方法があります。

1つ目の方法は[InternetExploreのCOMを利用する方法](#Internet ExploreのCOMを使用した自動化)です。WindowsのInternetExploreに対してならば、なにもインストールすることなく、ブラウザの自動操作が可能になっています。ただし、InternetExploreの寿命自体が、長く持たない可能性があるので注意が必要です。(InternetExplore自体が持っても、対応するWebアプリケーションがInternetExploreのサポートを切る可能性が高いです)

2つ目の方法としてSeleniumを使用する方法です。外部のライブラリが必要になりますが、多くのブラウザがサポートされています。

3つ目の方法としてはブラウザが提供している拡張機能を作成する方法です。ChromeとFirefoxの場合、JavaScriptで作成できるので、外部のライブラリを導入する必要はありません。

4つ目の方法としてRPAツールを使用する方法です。ブラウザ上の要素について深く考えなくても、GUIベースで自動化が行えますが、コストの面で問題になります。

HTMLの要素を調べ方

どのような方法でブラウザを操作するとしても、HTMLがどのような要素で構成されているかを調べる必要があります。
ここではChromeでGoogleで検索する場合を例として画面上の要素を調べる方法を説明します。

image.png

1.ChromeにてF12キーを押下して開発者ツールを開きます。その後、「Elements」タブを選択してください。

image.png

2.[CTRL]+[Shift]+[C]を押下するか、下記のアイコンをクリックします。
image.png

3.調べたい要素にマウスを移動させます。
image.png

4.Elementsタブに選択した要素の内容が表示されます。今回の場合、以下のような内容が表示されます。

<input class="gLFyf gsfi" maxlength="2048" name="q" type="text" jsaction="paste:puy29d" aria-autocomplete="both" aria-haspopup="false" autocapitalize="off" autocomplete="off" autocorrect="off" role="combobox" spellcheck="false" title="検索" value="" aria-label="検索" data-ved="0ahUKEwiC0u6iu4nlAhXwyIsBHWwTBHcQ39UDCAQ">

inputタグの属性が以下のようになっていることがわかります。

属性
class gLFyf gsfi
name q

自動操作を行う場合、id、name、classなどを利用して要素を指定することになるので、属性値をメモしておきましょう。

5.同様にボタンについても属性を調べます。その結果は以下のようになります。

<input class="gNO89b" value="Google 検索" aria-label="Google 検索" name="btnK" type="submit" data-ved="0ahUKEwiC0u6iu4nlAhXwyIsBHWwTBHcQ4dUDCAo">
属性
class gNO89b
name btnK

ここで調べた属性を利用して要素を特定して自動操作を行うことになります。。
また、今回はChromeでのやり方を紹介しましたが、他のブラウザでも同様のことが可能です。同じWebアプリケーションを使用していてもブラウザによって出力される内容が異なる可能性もあるので、自動操作を行うブラウザを使用して要素を調べるようにしましょう。

InternetExplore11の場合

IE11でもF12キーを押すことで開発者ツールが表示されます。
そこで「DOM Explore」タブを開いて要素の選択を行うことで要素の属性を調べることが可能です。

image.png

Edgeの場合

Edgeは将来Chromeベースのものに置き換わる可能性があります。
今回は旧EdgeとChromeベースの新Edge両方について説明します。

旧Edge

F12キーで開発者ツールが表示されます。
そこで「要素」タブを開いて要素の選択を行うことで要素の属性を調べることが可能です。

image.png

新Edge

2019年10月時点でベータ版としてリリースされているEdgeの場合、F12キーで開発者ツールが表示されます。
Chromeと同様の操作で要素の属性を調べることが可能です。

image.png

Firefoxの場合

F12キーで開発者ツールが表示されます。
そこで「インスペクター」タブを開いて要素の選択を行うことで要素の属性を調べることが可能です。

image.png

HTMLの要素を調べる方のまとめ

多くのブラウザは開発者ツールをサポートしており、要素の属性を調べることが可能です。
Webアプリケーションの自動化では、この要素を特定して操作する必要があるので、お使いのブラウザでの要素の調べ方は覚えておきましょう。

実際の自動操作の例は次章から解説します。

Internet ExploreのCOMを使用した自動化

Internet ExploreのCOMであるmshtmlを経由することでInternetExploreを自動操作できます。

COM(Component Object Model)
Microsoftが提唱した再利用を目的とした技術で、COMを用いて開発した部品はプログラム言語に依存せずに利用できるようになります。たとえば、説明したInternetExploreの操作やOfficeアプリケーションなどが外部から利用できるのは、COMのおかげです。

IE操作の単純な例

実際のGoogleのトップページで任意の単語を検索するサンプルを見てみましょう。

WSHやVBAでのIE操作の単純な例

WSHのVBScritpで以下のように実装可能です。
以下コードはWScript.EchoをDebug.Print等に置き換えることでVBAでも流用できると思います。

Dim ie
Set ie = CreateObject("InternetExplorer.Application")
ie.Visible = True 
call ie.Navigate("https://www.google.com/")

'ページが読み込まれるまで待機
Do While ie.Busy = True Or ie.readyState <> 4
    WScript.Sleep 100        
Loop

Dim doc
Set doc = ie.Document
Dim txt
Set txt = doc.getElementsByName("q")
txt.item(0).value = "ドリフターズ"

Dim btn
Set btn = doc.getElementsByName("btnK")
btn.item(0).click()

'ページが読み込まれるまで待機
Do While ie.Busy = True Or ie.readyState <> 4
    WScript.Sleep 100        
Loop
Set doc = ie.Document

Dim list
Do While True
    Set list = doc.getElementsByClassName("LC20lb")

    If Not list is Nothing Then
        If list.length > 0 Then
            Exit Do
        End If
    End If
    WScript.Sleep 100 
Loop

Dim item
For Each item In list
    WScript.Echo item.innerText
Next
ie.Quit

このサンプルではNavigateで指定のURLに移動したのち、getElementsByNameを使用して属性を取得して、値の設定とクリック操作を行っています。
その後,BusyreadyStateを監視してページの切り替え完了を待ちます。
その後、検索結果の要素が出現するまで待機して、その要素の内容を出力します。

「起動されたオブジェクトはクライアントから切断されました。」エラーが出る場合

多くの場合、上記の方法で自動操作ができますが同じページを同じソースコードで実行してもエラーが発生する場合と発生しない場合があります。

起動されたオブジェクトはクライアントから切断されました。


このエラーがでた場合は表示対象ページのプロパティを確認してみてください。
![image.png](https://needtec.sakura.ne.jp/wod07672/wp-content/uploads/2020/03/f9d65178-f0f6-54bb-32dd-d538eb9fff02.png)

![image.png](https://needtec.sakura.ne.jp/wod07672/wp-content/uploads/2020/03/49f10d41-c7f1-8e6f-524d-bf214b3bb61a.png)

保護モードが無効になっているかと思います。

1つのIEのプロセスには整合性レベル(IL)が1つしかないため、インターネット(保護モード、LowIL)とイントラネット(非保護モード、MediumIL)を同時に動かすことはできず、、Internet Explorerはデフォルトのタブプロセスが[低整合性]タブであると想定しているため、保護モードがオフのサイトに移動した時点で制御を失います。

これに対応するには非保護モードのIEを起動する必要があります。

```vb
Dim ie
'CLSIDを直接使用して非保護モードのIEを起動
Set ie = GetObject("new:{D5E8041D-920F-45e9-B8FB-B1DEB82C6E5E}")

ie.Visible = True 
call ie.Navigate("https://www.google.com/")

'ページが読み込まれるまで待機
Do While ie.Busy = True Or ie.readyState <> 4
    WScript.Sleep 100        
Loop

Default Integrity Level and Automation
https://blogs.msdn.microsoft.com/ieinternals/2011/08/03/default-integrity-level-and-automation/

PowerShellでのIE操作の単純な例

実はInternetExploreのCOM操作についてPowerShellやC#などの.NETでの実装は厄介です。結論から言えばやめといた方がいいです。
たとえば、よく見かける実装でGoogleとはてなブックマークを検索するスクリプトieng1.ps1とieng2.ps1を用意しました。

Googleの検索

$ie = New-Object -ComObject InternetExplorer.Application  # IE起動

$ie.Visible = $true
$ie.Navigate("https://www.google.com/")

# ページが読み込まれるまで待機
while ($ie.Busy -or $ie.readyState -ne 4) {
    Start-Sleep -Milliseconds 100
}
$doc=$ie.Document

# 検索文字入力
$txt=$doc.getElementsByName("q")
$txt[0].value = "ドリフターズ"

# ボタン押下
$btn=$doc.getElementsByName("btnK")
$btn[0].click()

# ページが読み込まれるまで待機
while ($ie.Busy -or $ie.readyState -ne 4) {
    Start-Sleep -Milliseconds 100
}
$doc = $ie.Document

# LC20lbが表示されるまで待機
while($true) {
  $list = $doc.getElementsByClassName('LC20lb')
  if($list -and $list.length -ge 1) { break }
  Start-Sleep -Milliseconds 100
}

foreach($i in $list){
  Write-Host($i.innerText)
}
$ie.Quit()
Write-Host("OK")

はてなの検索

$ie = New-Object -ComObject InternetExplorer.Application  # IE起動

$ie.Visible = $true
$ie.Navigate("https://b.hatena.ne.jp/")

# ページが読み込まれるまで待機
while ($ie.Busy -or $ie.readyState -ne 4) {
    Start-Sleep -Milliseconds 100
}
$doc=$ie.Document

# 検索文字入力
$txt=$doc.getElementsByName("q")
$txt[0].value = "ドリフターズ"

# ボタン押下
$btn=$doc.getElementsByClassName("gh-search-button")
$btn[0].click()

# ページが読み込まれるまで待機
while ($ie.Busy -or $ie.readyState -ne 4) {
    Start-Sleep -Milliseconds 100
}
$doc = $ie.Document

# LC20lbが表示されるまで待機
while($true) {
  $list = $doc.getElementsByClassName('centerarticle-entry-title')
  if($list -and $list.length -ge 1) { break }
  Start-Sleep -Milliseconds 100
}

foreach($i in $list){
  Write-Host($i.innerText)
}
$ie.Quit()
Write-Host("OK")

この実装はいくつか問題を起こす可能性があります。

PowerShellでのIE操作の問題点
環境によっては動作しない

同じコードをPowerShell2.0+Windows7で動かそうとしたところ下記のエラーを表示して動作しません。

PS C:\share\webctrl> $ie.Document.getElementsByName("q")
"getElementsByName" のオーバーロードで、引数の数が "1" であるものが見つかりません。
発生場所 行:1 文字:31
+ $ie.Document.getElementsByName <<<< ("q")
    + CategoryInfo          : NotSpecified: (:) []、MethodException
    + FullyQualifiedErrorId : MethodCountCouldNotFin

この問題は下記のフォーラムで議論されていますが、解決はしていません。

COMの解放処理を行っていない

上記のコードはCOMを.NETから利用しているにも関わらずReleaseComObjectを行っていません。
類似の問題として以下を参照してください。

.NETを使ったOfficeの自動化が面倒なはずがない―そう考えていた時期が俺にもありました。
https://qiita.com/mima_ita/items/aa811423d8c4410eca71

解放処理を行ったコードは以下のようになります。

    $ie = New-Object -ComObject InternetExplorer.Application  # IE起動

    $ie.Visible = $true
    $ie.Navigate("https://www.google.com/")

    # ページが読み込まれるまで待機
    while ($ie.Busy -or $ie.readyState -ne 4) {
      Start-Sleep -Milliseconds 100
    }
    $doc=$ie.Document

    # 検索文字入力
    $txt=$doc.getElementsByName("q")
    $txt[0].value = "ドリフターズ"
    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($txt) | Out-Null
    Remove-Variable txt -ErrorAction SilentlyContinue

    # ボタン押下
    $btn=$doc.getElementsByName("btnK")
    $btn[0].click()
    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($btn) | Out-Null
    Remove-Variable btn -ErrorAction SilentlyContinue

    ## ページが読み込まれるまで待機
    while ($ie.Busy -or $ie.readyState -ne 4) {
        Start-Sleep -Milliseconds 100
    }
    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($doc) | Out-Null
    $doc = $ie.Document

    # LC20lbが表示されるまで待機
    while($true) {
      $list = $doc.getElementsByClassName('LC20lb')
      if($list -and $list.length -ge 1) { 
        break 
      }
      if ($list) {
        [System.Runtime.Interopservices.Marshal]::ReleaseComObject($list) | Out-Null
      }
      Start-Sleep -Milliseconds 100
    }

    for($i=0; $i -lt $list.length; $i++) {
      $item = $list[$i]
      Write-Host($item.innerText)
      [System.Runtime.Interopservices.Marshal]::ReleaseComObject($item) | Out-Null
      Remove-Variable item -ErrorAction SilentlyContinue
    }
    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($list) | Out-Null
    Remove-Variable list -ErrorAction SilentlyContinue

    $ie.Quit()
    Write-Host("OK")

    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($doc) | Out-Null
    Remove-Variable doc -ErrorAction SilentlyContinue

    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($ie) | Out-Null
    Remove-Variable ie -ErrorAction SilentlyContinue

    [System.GC]::Collect()
    [System.GC]::WaitForPendingFinalizers()
    [System.GC]::Collect()
同一プロセスで複数起動した場合にエラーになる

先にあげた2つのスクリプトはPowerShellを再起動した直後には、それぞれ動作しますが以下のように続けて実行するとエラーになります。

正常に動作する
>powershell ./ieng1.ps1
>powershell ./ieng2.ps1

2つ目のスクリプトの実行でエラーになる
>./ieng1.ps1
>./ieng2.ps1

エラーの内容は下記の通りです。

HRESULT からの例外:0x800A01B6
発生場所 C:\dev\ps\webctrl\ieng2.ps1:14 文字:1
+ $txt=$doc.getElementsByName("q")
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : OperationStopped: (:) [], NotSupportedException
    + FullyQualifiedErrorId : System.NotSupportedException

この問題は以下の記事で言及されています。

getElementsByNameをIHTMLDocument3_getElementsByNameに置き換えて、getElementsByClassNameをInvokeMemberで呼び出すようにすれば回避できるようです。

Googleの検索(修正版)

    $ie = New-Object -ComObject InternetExplorer.Application  # IE起動

    $ie.Visible = $true
    $ie.Navigate("https://www.google.com/")

    # ページが読み込まれるまで待機
    while ($ie.Busy -or $ie.readyState -ne 4) {
      Start-Sleep -Milliseconds 100
    }
    $doc=$ie.Document

    # 検索文字入力
    $txt=$doc.IHTMLDocument3_getElementsByName("q")
    $txt[0].value = "ドリフターズ"
    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($txt) | Out-Null
    Remove-Variable txt -ErrorAction SilentlyContinue

    # ボタン押下
    $btn=$doc.IHTMLDocument3_getElementsByName("btnK")
    $btn[0].click()
    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($btn) | Out-Null
    Remove-Variable btn -ErrorAction SilentlyContinue

    ## ページが読み込まれるまで待機
    while ($ie.Busy -or $ie.readyState -ne 4) {
        Start-Sleep -Milliseconds 100
    }
    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($doc) | Out-Null
    $doc = $ie.Document

    # LC20lbが表示されるまで待機
    while($true) {
      $list = [System.__ComObject].InvokeMember("getElementsByClassName", [System.Reflection.BindingFlags]::InvokeMethod, $null, $doc, "LC20lb")

      if($list -and $list.length -ge 1) { 
        break 
      }
      if ($list) {
        [System.Runtime.Interopservices.Marshal]::ReleaseComObject($list) | Out-Null
      }
      Start-Sleep -Milliseconds 100
    }

    for($i=0; $i -lt $list.length; $i++) {
      $item = $list[$i]
      Write-Host($item.innerText)
      [System.Runtime.Interopservices.Marshal]::ReleaseComObject($item) | Out-Null
      Remove-Variable item -ErrorAction SilentlyContinue
    }
    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($list) | Out-Null
    Remove-Variable list -ErrorAction SilentlyContinue

    $ie.Quit()
    Write-Host("OK")

    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($doc) | Out-Null
    Remove-Variable doc -ErrorAction SilentlyContinue

    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($ie) | Out-Null
    Remove-Variable ie -ErrorAction SilentlyContinue

    [System.GC]::Collect()
    [System.GC]::WaitForPendingFinalizers()
    [System.GC]::Collect()

はてなの検索(修正版)

    $ie = New-Object -ComObject InternetExplorer.Application  # IE起動

    $ie.Visible = $true
    $ie.Navigate("https://b.hatena.ne.jp/")

    # ページが読み込まれるまで待機
    while ($ie.Busy -or $ie.readyState -ne 4) {
      Start-Sleep -Milliseconds 100
    }
    $doc=$ie.Document

    # 検索文字入力
    $txt=$doc.IHTMLDocument3_getElementsByName("q")
    $txt[0].value = "ドリフターズ"
    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($txt) | Out-Null
    Remove-Variable txt -ErrorAction SilentlyContinue

    # ボタン押下
    $btn=[System.__ComObject].InvokeMember("getElementsByClassName", [System.Reflection.BindingFlags]::InvokeMethod, $null, $doc, "gh-search-button")
    $btn[0].click()
    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($btn) | Out-Null
    Remove-Variable btn -ErrorAction SilentlyContinue

    ## ページが読み込まれるまで待機
    while ($ie.Busy -or $ie.readyState -ne 4) {
        Start-Sleep -Milliseconds 100
    }
    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($doc) | Out-Null
    $doc = $ie.Document

    #表示されるまで待機
    while($true) {
      $list = [System.__ComObject].InvokeMember("getElementsByClassName", [System.Reflection.BindingFlags]::InvokeMethod, $null, $doc, "centerarticle-entry-title")

      if($list -and $list.length -ge 1) { 
        break 
      }
      if ($list) {
        [System.Runtime.Interopservices.Marshal]::ReleaseComObject($list) | Out-Null
      }
      Start-Sleep -Milliseconds 100
    }

    for($i=0; $i -lt $list.length; $i++) {
      $item = $list[$i]
      Write-Host($item.innerText)
      [System.Runtime.Interopservices.Marshal]::ReleaseComObject($item) | Out-Null
      Remove-Variable item -ErrorAction SilentlyContinue
    }
    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($list) | Out-Null
    Remove-Variable list -ErrorAction SilentlyContinue

    $ie.Quit()
    Write-Host("OK")

    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($doc) | Out-Null
    Remove-Variable doc -ErrorAction SilentlyContinue

    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($ie) | Out-Null
    Remove-Variable ie -ErrorAction SilentlyContinue

    [System.GC]::Collect()
    [System.GC]::WaitForPendingFinalizers()
    [System.GC]::Collect()
非保護モードのIEをPowerShellで操作する場合

VBSと同様に非保護モードのIEはこのままでは操作できません。
非保護モードのIEを以下のようにして起動する必要があります。

    # 非保護モードのIE起動
    $clsid = New-Object Guid "D5E8041D-920F-45e9-B8FB-B1DEB82C6E5E"
    $type = [Type]::GetTypeFromCLSID($clsid)
    $ie = [Activator]::CreateInstance($type)

    $ie.Visible = $true
    $ie.Navigate("https://www.google.com/")

    # ページが読み込まれるまで待機
    while ($ie.Busy -or $ie.readyState -ne 4) {
      Start-Sleep -Milliseconds 100
    }

Accessing COM Objects without ProgID
https://community.idera.com/database-tools/powershell/powertips/b/tips/posts/accessing-com-objects-without-progid

あるいは非保護モードのIEのオブジェクトを取り直す手法もあります。

    $ie = New-Object -ComObject InternetExplorer.Application  # IE起動

    $ie.Visible = $true
    $ie.Navigate("https://www.google.com/")

    # 非保護モード対応
    $hwnd = $ie.HWND
    [System.Reflection.Assembly]::LoadFrom("C:\Program Files (x86)\Microsoft.NET\Primary Interop Assemblies\Microsoft.mshtml.dll")
    $shell = New-Object -ComObject Shell.Application
    while ($ie.Document -isnot [mshtml.HTMLDocumentClass]) {
        $ie = $shell.Windows() | ? {$_.HWND -eq $hwnd}
    }

    # ページが読み込まれるまで待機
    while ($ie.Busy -or $ie.readyState -ne 4) {
      Start-Sleep -Milliseconds 100
    }

PowershellでInternetExplorerを操作する
https://qiita.com/flasksrw/items/a1ff5fbbc3b660e01d96

PowerShellでのInternetExploreの操作のまとめ

IEのCOM操作は辞めといた方がいいでしょう。

回避策を書いているページは色々見つかりますが、何故その事象が発生しているか、そしてなぜ回避することができたかを説明できているサイトは見つからず、ためしてみたら動作した以上のものではありません。
固定の環境で動かすスクリプトならばともかく、不特定多数の環境で動作するスクリプトでは採用を避けるべきでしょう。また、多くの場合、VBSやVBAで代替できるのでPowerShellにこだわる理由はないでしょう。

どうしても、PowerShellでIEのCOM操作を行いたい場合は、IE操作をしたプロセスを終了するようにすると安定しそうです。

なお、PowerShellでWebのGUIの自動操作を行いたい場合は、後述のSeleniumを利用した方が安定すると思います。

すでに起動しているブラウザの操作方法

すでに起動しているIEを操作するにはIEのウィンドウに対してRegisterWindowMessageでWM_HTML_GETOBJECTメッセージを定義して送信することでHTMLDocumentを取得することになります。

Cant create HTML document from Hwnd using C#
https://stackoverflow.com/questions/20873885/cant-create-html-document-from-hwnd-using-c-sharp

Win32 APIを直接実行できないWSHでは実現が難しいです。

VBAでの実装例

下記を参考にしてください。

VBAでInternetExplore上のJavaScriptを無理やり動かすよ!
https://qiita.com/mima_ita/items/fdff129a8db1153c9940

C#での実装例

.NET経由になるのでCOMの解放処理を入れる必要があります。

(1)参照マネージャーのCOMタブでMicrosoft HTML Object Libraryを追加します。
image.png

これにより「Interop.MSHTML.dll」が作成されます。
Interop~.dllはtlbImpコマンドを使用することでコマンドラインで作成できますが、VisualStudioなどの開発ツールをインストールしていないと使えないと思います。

(2)以下のような実装をします。下記のコードは.NET2.0でも動作します。

using System;
using System.Collections.Generic;
using System.Text;
using System.Runtime.InteropServices;
using MSHTML;
using System.Diagnostics;

namespace iesample
{
    class Program
    {

        [DllImport("user32.dll", EntryPoint = "GetClassNameA")]
        public static extern int GetClassName(IntPtr hwnd, StringBuilder lpClassName, int nMaxCount);

        /*delegate to handle EnumChildWindows*/
        public delegate int EnumProc(IntPtr hWnd, ref IntPtr lParam);

        [DllImport("user32.dll")]
        public static extern int EnumWindows(EnumProc lpEnumFunc, ref IntPtr lParam);

        [DllImport("user32.dll")]
        public static extern int EnumChildWindows(IntPtr hWndParent, EnumProc lpEnumFunc, ref IntPtr lParam);

        [DllImport("user32.dll", EntryPoint = "RegisterWindowMessageA")]
        public static extern int RegisterWindowMessage(string lpString);

        [DllImport("user32.dll", EntryPoint = "SendMessageTimeoutA")]
        public static extern int SendMessageTimeout(IntPtr hwnd, int msg, int wParam, int lParam, int fuFlags, int uTimeout, out UIntPtr lpdwResult);

        [DllImport("OLEACC.dll")]
        public static extern int ObjectFromLresult(UIntPtr lResult, ref Guid _riid, int wParam, ref MSHTML.IHTMLDocument2 _ppvObject);

        public const int SMTO_ABORTIFHUNG = 0x2;
        public Guid IID_IHTMLDocument = new Guid("626FC520-A41E-11CF-A731-00A0C9082637");

        private static List cacheList = new List();

        public static MSHTML.IHTMLDocument2 FindBrowser(string title)
        {
            MSHTML.IHTMLDocument2 ret;
            if (cacheList.Count == 0)
            {
                RefreshBrowserCache();
            }
            ret = FindBrowserInCache(title);
            if (ret != null)
            {
                return ret;
            }
            RefreshBrowserCache();
            return FindBrowserInCache(title);
        }
        public static MSHTML.IHTMLDocument2 FindBrowserInCache(string title)
        {
            MSHTML.IHTMLDocument2 ret;
            foreach(MSHTML.IHTMLDocument2 item in cacheList)
            {
                if (item.title.Contains(title))
                {
                    return item;
                }
            }
            return null;
        }

        public static void RefreshBrowserCache()
        {
            foreach (IHTMLDocument2 item in cacheList)
            {
                Marshal.ReleaseComObject(item);
            }
            cacheList.Clear();
            EnumProc proc = new EnumProc(EnumIEWndProc);
            IntPtr lparam = IntPtr.Zero;
            EnumWindows(proc, ref lparam);
        }

        private static int EnumIEWndProc(IntPtr hWnd, ref IntPtr lParam)
        {
            StringBuilder className = new StringBuilder(128);
            GetClassName(hWnd, className, className.Capacity);
            EnumProc proc = new EnumProc(EnumIEServerWndProc);
            if (className.ToString().Equals("IEFrame") || className.ToString().Equals("TabWindowClass"))
            {
                IntPtr lparam = IntPtr.Zero;
                EnumChildWindows(hWnd, proc, ref lparam);
            }
            return 1;
        }
        private static int EnumIEServerWndProc(IntPtr hWnd, ref IntPtr lParam)
        {
            StringBuilder className = new StringBuilder(128);
            GetClassName(hWnd, className, className.Capacity);
            if (className.ToString().Equals("Internet Explorer_Server"))
            {
                IHTMLDocument2 doc = GetHTMLDocument(hWnd);
                if (doc != null)
                {
                    cacheList.Add(doc);
                }
            }

            return 1;
        }
        private static IHTMLDocument2 GetHTMLDocument(IntPtr hWnd)
        {
            int nMsg = RegisterWindowMessage("WM_HTML_GETOBJECT");
            if (nMsg == 0)
            {
                return null;
            }
            UIntPtr lRes;
            SendMessageTimeout(hWnd, nMsg, 0, 0, SMTO_ABORTIFHUNG, 1000, out lRes);
            if (lRes == UIntPtr.Zero)
            {
                return null;
            }

            Guid IID_IHTMLDocument = new Guid("626FC520-A41E-11CF-A731-00A0C9082637");
            IHTMLDocument2 doc = null;
            int hr = ObjectFromLresult(lRes, ref IID_IHTMLDocument, 0, ref doc);
            return doc;
         }

        static void Main(string[] args)
        {
            IHTMLDocument3 doc = FindBrowser("Google") as IHTMLDocument3;
            IHTMLInputElement txt = doc.getElementsByName("q").item(0) as IHTMLInputElement;
            txt.value = "ドリフターズ";

            IHTMLElement btn = doc.getElementsByName("btnK").item(0) as IHTMLElement;
            btn.click();

            Marshal.ReleaseComObject(btn);
            Marshal.ReleaseComObject(txt);
            Marshal.ReleaseComObject(doc);

        }
    }
}

様々なコントロールを含むサンプルの例

様々なコントロールを含む以下のページの入力の自動化について考えてみます。
操作対象として以下のページを使用します。
http://needtec.sakura.ne.jp/auto_demo/form1.html

このページは「登録する」ボタンを押下することで確認メッセージが表示されて、「OK」の場合に登録処理を行うものとします。※
※実際はSleepしているだけでなにもしていないです。

WSHやVBSでの操作例は以下のようになります。

Dim ie
Set ie = CreateObject("InternetExplorer.Application")
ie.Visible = True 
call ie.Navigate("http://needtec.sakura.ne.jp/auto_demo/form1.html")

'ページが読み込まれるまで待機
Do While ie.Busy = True Or ie.readyState <> 4
    WScript.Sleep 100
Loop

Dim doc
Set doc = ie.Document
' INPUTBOX
doc.getElementsByName("name").item(0).value = "名前太郎"
doc.getElementsByName("mail").item(0).value = "test@co.jp"

' テキストエリア
doc.getElementsByName("comment").item(0).innerText = "猫猫子猫" & vbCrLf & "犬犬子犬"

' チェックボックス
doc.getElementsByName("q1[]").item(0).Checked = True
doc.getElementsByName("q1[]").item(1).Checked = True

' ラジオボタン
doc.getElementsByName("men").item(1).Checked = True

' 複数選択リスト
Dim objSelect
Set objSelect = doc.getElementsByName("osi[]").item(0)
objSelect.getElementsByTagName("option").item(1).Selected = True
objSelect.getElementsByTagName("option").item(2).Selected = True

' ボタン押下
' 確認メッセージ処理を偽造する
doc.parentWindow.ExecScript("confirm = function () { return true; }")
doc.getElementsByTagName("input").item(8).click()

確認ダイアログを突破する方法は色々ありますが、上記でやったようなJavaScirptのconfirmやalertを上書きしてしまうのが最も楽だと思います。

確認メッセージの処理を上書きしたくない場合

UIAutomation等を使用します。
確認メッセージが表示されるまで待機してボタンを押下するような処理を別スレッドかプロセスで起動します。WSHやVBAの場合は別プロセスでやった方が楽です。

まず、確認メッセージを監視するようなスクリプトを記載します。
これはPowerShellで書いた方が楽だと思います。

Add-Type -AssemblyName UIAutomationClient
Add-Type -AssemblyName UIAutomationTypes
Add-type -AssemblyName System.Windows.Forms

$source = @"
using System;
using System.Windows.Automation;
public class AutomationHelper
{
    public static AutomationElement RootElement
    {
        get
        {
            return AutomationElement.RootElement;
        }
    }

    public static AutomationElement GetWindowByTitle(string title) {
        var rootCond = new PropertyCondition(AutomationElement.ClassNameProperty, "Alternate Modal Top Most");
        var cond = new PropertyCondition(AutomationElement.NameProperty, title);
        var elementCollection = RootElement.FindAll(TreeScope.Children, rootCond);
        foreach(AutomationElement mainForm in elementCollection) {
            var win =  mainForm.FindFirst(TreeScope.Children, cond);
            if (win != null) {
                return win;
            }
        }
        return null;
    }

    public static AutomationElement WaitWindowByTitle(string title, int timeout = 10) {
        DateTime start = DateTime.Now;
        while (true) {
            AutomationElement ret = GetWindowByTitle(title);
            if (ret != null) {
                return ret;
            }
            TimeSpan ts = DateTime.Now - start;
            if (ts.TotalSeconds > timeout) {
               return null;
            }
            System.Threading.Thread.Sleep(100);
        }
    }

}
"@
Add-Type -TypeDefinition $source -ReferencedAssemblies("UIAutomationClient", "UIAutomationTypes")

# 5.0以降ならusingで記載した方が楽。
$autoElem = [System.Windows.Automation.AutomationElement]

# ウィンドウ以下で指定の条件に当てはまるコントロールを1つ取得
function findFirstElement($form, $condProp, $condValue) {
    $cond = New-Object -TypeName System.Windows.Automation.PropertyCondition($condProp, $condValue)
    return $form.FindFirst([System.Windows.Automation.TreeScope]::Element -bor [System.Windows.Automation.TreeScope]::Descendants, $cond)
}

# 指定のAutomationIDのボタンを押下
function pushButtonById($form, $id) {
    $buttonElem = findFirstElement $form $autoElem::AutomationIdProperty $id
    $invElm = $buttonElem.GetCurrentPattern([System.Windows.Automation.InvokePattern]::Pattern) -as [System.Windows.Automation.InvokePattern]
    $invElm.Invoke()
}

#
$dialog = [AutomationHelper]::WaitWindowByTitle("Web ページからのメッセージ", 30)
if ($dialog -eq $null) {
    Write-Host "タイムアウト"
} else {
    pushButtonById $dialog "1"
    Write-Host "終了"
}

あとはWSH側のボタン押下処理を以下のように修正します。

' ボタン押下
' 確認メッセージ処理を別プロセスで行う
Dim shell
Set shell = CreateObject("WScript.Shell")
shell.Run "C:\Windows\System32\WindowsPowerShell\v1.0\powershell -ExecutionPolicy RemoteSigned -File C:\dev\ps\webctrl\wait_confirm.ps1", 0, False

doc.getElementsByTagName("input").item(8).click()

Internet ExploreのCOMを使用した自動化のまとめ

VBAやVBSなどの何処でもはいっていそうなプログラミング言語で自動化できるのは強みです。
PowerShellでも行えますが、.NET経由だとCOMの解放処理が面倒だったり、動作が安定しない環境もあるので環境を制御できる場合のみに使用したほうがいいでしょう。

また、InternetExploreのサポートがいつまで続くかわからない以上、外部のツールを導入可能である場合は、お勧めしません。

Seleniumを使用した自動化

様々なOS上の様々なブラウザを様々なプログラミング言語で自動操作するためのツールです。
Webアプリケーションをブラウザ操作で自動化する場合、もっともよく使われるツールになります。
Selenium実践入門などの良書が流通しているので、この章は飛ばしてそっちを読んだ方がいいと思います。

SeleniumIDEを使用する例

ブラウザの操作を記憶する録画機能が提供されておりGUIベースで自動操作の処理を記載できます。録画した操作内容はスクリプトとして記録され、後で修正が可能です。また、別のプログラム言語で記載されたテストコードに変換することもできます。

以下はGoogle検索の操作をキャプチャした例になります。

auto.gif

SeleniumIDEはChromeまたはFirefoxの拡張機能として提供されています。

「あれ?SeleniumIDEって終わったんじゃなかったっけ?」という人は下記の経緯を参照してください。

Webブラウザ自動化ツール「Selenium IDE」の今までとこれから
https://www.valtes.co.jp/qbookplus/509

ChromeでSeleniumIDEを使用する

(1)Chrome用のSeleniumIDEを拡張機能として追加します。

(2)ブラウザの上部にSeleniumIDEのアイコンが表示されるのでクリックします。
image.png

(3)SeleniumIDEのポップアップが表示されるので「Record a new test in new project」を選択します。
image.png

(4)「Name your new project」ダイアログが表示されるので任意のプロジェクト名を入力して「OK」ボタンを押します。
image.png

(5)「Set your projects's base URL」ダイアログが表示されるので操作元になるURLを入力します。たとえばGoogle検索をする例だと「https://www.google.com」を入力して「START RECORDING」ボタンを押下します。

image.png

(6)操作の記録が始まると右下に「Selenium IDE is recording...」と書かれた新しいブラウザが開きます。
image.png
このブラウザを使用して記録したい任意の操作を行います。

(7)操作の記録を終了したい場合、「Selenium IDE」ウィンドウの右上の「Stop recording」アイコンを押します。
image.png

(8)「Name your new test」ポップアップが表示されるので任意のテスト名を入力して「OK」を押します。
image.png

(9)SeleniumIDE ウィンドウに今回操作した内容がスクリプトとして記録されます。
image.png

(10)記録したスクリプトは「Run Current Test」アイコンを押すことで再実行可能です。
image.png

また、JUnit や pytest、 JavaScript Mochaといった他のプログラミング言語のユニットテストとしてエクスポートすることが可能です。

FirefoxでSeleniumIDEを使用する

Firefox用のSeleniumIDEを拡張機能として追加します。
あとの操作はChromeと同じです。

ただし、記録される操作はFirefoxとChromeで差異があります。
Firefoxの場合、ブラウザのスクロール操作が記録されていましたが、Chromeでは記録されていませんでした。
image.png

なお、手で同じコマンドを追加すると、再生はChromeでも動作しました。

プログラムからSeleniumを利用する

C#の場合

まず、NuGetでSelenium.WebDriverとSelenium.Supportに加えて操作したいブラウザのDriverを入手します。今回はChromeを操作したいので、Selenium.Chrome.WebDriverを入手します。

image.png

image.png

C#でのSeleniumの操作例は以下のようになります。

using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;
using System;
using System.IO;
using System.Reflection;

namespace chromeSample
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var driver = new ChromeDriver(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location)))
            {
                driver.Url = "http://needtec.sakura.ne.jp/auto_demo/form1.html";
                driver.FindElementByName("name").SendKeys("名前太郎");
                driver.FindElementByName("mail").SendKeys("test@co.jp");
                driver.FindElementByName("comment").SendKeys("猫猫子猫\n\r犬犬子犬");

                // チェックボックス
                var chks = driver.FindElements(By.Name("q1[]"));
                chks[0].Click();
                chks[2].Click();

                // オプションボタン
                var opts = driver.FindElements(By.Name("men"));
                opts[1].Click();

                // 複数選択
                var sel = new OpenQA.Selenium.Support.UI.SelectElement(driver.FindElement(By.Name("osi[]")));
                sel.DeselectAll();
                sel.SelectByIndex(1);
                sel.SelectByIndex(2);

                // 登録ボタン押下 
                driver.FindElement(By.XPath("//input[@value='登録する']")).Click();
                // OKボタンを押す
                var confirm = driver.SwitchTo().Alert();
                confirm.Accept();

                // 結果の出力
                var results = driver.FindElementsByTagName("tr");
                foreach(var rec in results)
                {
                    Console.WriteLine(rec.Text);
                }
                Console.ReadLine();
                driver.Quit();
            }
        }
    }
}

ページの切り替え時に待機処理をいれていませんが、暗黙的にタイムアウトまでDOMをポーリングしています。このタイムアウトについては下記を参照してください。

The default value of timeouts on selenium webdriver
https://stackoverflow.com/questions/30114976/the-default-value-of-timeouts-on-selenium-webdriver

PowerShellの場合

PowerShellでもC#と同様な実装が可能です。
下記のページのSelenium Client & WebDriver Language BindingsからC#のクライアントと操作したいブラウザのWebDriverをダウンロードしてください。

https://www.seleniumhq.org/download/

image.png

image.png

クライアントをダウンロードすると以下のようなファイルが入っています。

  • Selenium.Support.3.14.0.nupkg
  • Selenium.WebDriver.3.14.0.nupkg
  • Selenium.WebDriverBackedSelenium.3.14.0.nupkg

これは圧縮されているファイルなので拡張子をzipに変更すればDLLを取り出せます。
サポートしているバージョンが.NET3.5以上なのでWindows7に初期から入っているPowerShellでは動作しません。

PowerShellでのサンプルは以下のようになります。

# 以下参考
# https://tech.mavericksevmont.com/blog/powershell-selenium-automate-web-browser-interactions-part-i/
$dllPath = Split-Path $MyInvocation.MyCommand.Path
Add-Type -Path "$dllPath\WebDriver.dll"
Add-Type -Path "$dllPath\WebDriver.Support.dll"

# chromedriver.exeがあるディレクトリを指定
$driver = New-Object OpenQA.Selenium.Chrome.ChromeDriver("C:\tool\selenium\chromedriver_win32\")
$driver.Url = "http://needtec.sakura.ne.jp/auto_demo/form1.html"
$driver.FindElementByName("name").SendKeys("名前太郎")
$driver.FindElementByName("mail").SendKeys("test@co.jp")

# テキストエリア
$comment = @"
猫猫子猫
犬犬子犬
"@
$driver.FindElementByName("comment").SendKeys($comment)

# チェックボックス
$chks = $driver.FindElements([OpenQA.Selenium.By]::Name("q1[]"))
$chks[0].Click()
$chks[2].Click()

# オプションボタン
$opts = $driver.FindElements([OpenQA.Selenium.By]::Name("men"))
$opts[1].Click()

# 複数選択
$selElem = $driver.FindElement([OpenQA.Selenium.By]::Name("osi[]"))
$sel = New-Object OpenQA.Selenium.Support.UI.SelectElement -ArgumentList $selElem
$sel.DeselectAll();
$sel.SelectByIndex(1);
$sel.SelectByIndex(2);

# ボタン押下
$driver.FindElement([OpenQA.Selenium.By]::XPath("//input[@value='登録する']")).Click()
$confirm = $driver.SwitchTo().Alert();
$confirm.Accept()

# 結果表示
$results = $driver.FindElementsByTagName("tr");
foreach($rec in $results)
{
    Write-Host $rec.Text
}
$driver.Quit()
$driver.Dispose()

Write-Host("OK")

Pythonの場合

PythonでもSeleniumは使用可能です。まずpipコマンドでseleniumをインストールします。

pip install -U selenium

サンプルコードは以下のようになります。

from selenium import webdriver
from selenium.webdriver.support.ui import Select

driver = webdriver.Chrome("C:\\tool\\selenium\\chromedriver_win32\\chromedriver.exe")
driver.get("http://needtec.sakura.ne.jp/auto_demo/form1.html")
driver.find_element_by_name("name").send_keys("名前太郎");
driver.find_element_by_name("mail").send_keys("test@co.jp");
driver.find_element_by_name("comment").send_keys("猫猫子猫\n\r犬犬子犬");

# チェックボックス
chks = driver.find_elements_by_name("q1[]")
chks[0].click()
chks[2].click()

# オプションボタン
opts = driver.find_elements_by_name("men")
opts[1].click()

# 選択
sel = Select(driver.find_element_by_name("osi[]"))
sel.deselect_all()
sel.select_by_index(1)
sel.select_by_index(2)

# ボタン押下
driver.find_element_by_xpath("//input[@value='登録する']").click()
driver.switch_to.alert.accept()

# 結果
results = driver.find_elements_by_tag_name("tr")
for rec in results:
    print(rec.text)

driver.close()

NodeJsの場合

NodeJsでもSeleniumの操作は可能です。npmコマンドを使用してseleniumをインストールします。

npm install selenium-webdriver

簡単なサンプルは以下のようになります。

// 以下参考
// https://qiita.com/tonio0720/items/70c13ad304154d95e4bc
// https://stackoverflow.com/questions/26191142/selenium-nodejs-chromedriver-path
// https://seleniumhq.github.io/selenium/docs/api/javascript/index.html
const webdriver = require('selenium-webdriver');
const chrome = require('selenium-webdriver/chrome');
const path = 'C:\\tool\\selenium\\chromedriver_win32\\chromedriver.exe';
const service = new chrome.ServiceBuilder(path).build();
chrome.setDefaultService(service);

(async () => {
  const driver = await new webdriver.Builder()
                            .withCapabilities(webdriver.Capabilities.chrome())
                            .build();
  await driver.get('http://needtec.sakura.ne.jp/auto_demo/form1.html');
  await driver.findElement(webdriver.By.name("name")).sendKeys("名前太郎");
  await driver.findElement(webdriver.By.name("mail")).sendKeys("test@co.jp");
  await driver.findElement(webdriver.By.name("comment")).sendKeys("猫猫子猫\n\r犬犬子犬");

  // チェックボックス
  let chks = await driver.findElements(webdriver.By.name("q1[]"));
  await chks[0].click();
  await chks[2].click();

  // オプションボタン
  let opts = await driver.findElements(webdriver.By.name("men"));
  await opts[1].click();

  // 複数選択
  let sel = await driver.findElements(webdriver.By.xpath("//select[@name='osi[]']/option"));
  await sel[1].click();
  await sel[2].click();

  // ボタン押下
  await driver.findElement(webdriver.By.xpath("//input[@value='登録する']")).click();
  await driver.switchTo().alert().accept();

  // 結果取得
  let results = await driver.findElements(webdriver.By.tagName("tr"));
  for (let i = 0; i < results.length; ++i) {
    console.log(await results[i].getText());
  }

  driver.quit();
})();

Seleniumで既に起動しているブラウザの操作は行えるか?

Seleniumを介さず起動していたブラウザの自動操作については公式にはサポートしていません。
下記に幾つかの回避法が紹介されていますが、あくまで非公式の内容になります。

Can Selenium interact with an existing browser session?
https://stackoverflow.com/questions/8344776/can-selenium-interact-with-an-existing-browser-session

Chromeのremote-debugging-portを使用する例

remote-debugging-portを指定して起動したChromeに対しての自動操作の例が以下のページで紹介されています。

How to connect Selenium to an existing browser that was opened manually?
https://cosmocode.io/how-to-connect-selenium-to-an-existing-browser-that-was-opened-manually/

  1. まずChromeを以下のオプションを付けて起動してください。
"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe" --remote-debugging-port=9222 --user-data-dir=C:\dev\python3\selenium\tmp
  1. 起動したChromeの画面を操作します。今回の例ではTwitterの画面にログインしたと仮定します。
  2. その後、下記の操作を行うことで2で起動中の画面を使用してツイートが可能です。
from selenium import webdriver
from selenium.webdriver.chrome.options import Options

# https://cosmocode.io/how-to-connect-selenium-to-an-existing-browser-that-was-opened-manually/
# TODO remote-debugging-portを指定してChromeを起動後、「https://twitter.com/home」を表示
# "C:\Program Files (x86)\Google\Chrome\Application\chrome.exe" --remote-debugging-port=9222 --user-data-dir=C:\dev\python3\selenium\tmp
chrome_options = Options()
chrome_options.add_experimental_option("debuggerAddress", "127.0.0.1:9222")
# Change chrome driver path accordingly
chrome_driver = "C:\\tool\\selenium\\chromedriver_win32\\chromedriver.exe"
driver = webdriver.Chrome(chrome_driver, chrome_options=chrome_options)

driver.find_element_by_xpath("//a[@aria-label='ツイート']").click();
driver.find_element_by_xpath("//div[@aria-label='テキストをツイート']").send_keys('あいうえお')
driver.find_element_by_xpath("//div[@data-testid='tweetButton']").click();

# Chromeを閉じたい場合
# driver.close()

Seleniumを使用する方法のまとめ

Seleniumを使用することで様々なブラウザを様々なプログラミング言語で操作できることができます。
またSeleniumIDEを使用することでプログラミングをせずにブラウザの自動操作がおこなえます。

ただし、Flashページなどの画像認識を必要とする操作の場合は他の方法を検討してください。
たとえば以下のような方法があります。

Sikulix1.1.4を使って画面の自動操作をする
https://qiita.com/mima_ita/items/8f653042ac9140e5023f

C#やPowerShellで画面上の特定の画像の位置をクリックする方法
https://qiita.com/mima_ita/items/f7d2c38767bda8b35cbd

拡張機能を作成する方法

ChromeやFirefoxで利用できる拡張機能を使用して表示中のページを自動操作することが可能です。

Chromeの拡張機能
https://developer.chrome.com/extensions

Firefoxの拡張機能
https://developer.mozilla.org/ja/docs/Mozilla/Add-ons/WebExtensions/Your_first_WebExtension

以下はChromeの拡張機能を使用してページを自動操作後、操作結果をメッセージボックスで表示しています。

auto2.gif

操作対象のページ
http://needtec.sakura.ne.jp/auto_demo/form1.html

拡張機能を使った自動操作の仕組みは下記の通りです。
image.png

default_popupからcontent_scriptsに対して自動操作の開始指示をメッセージを使用して行います。
入力ページのcontent_scriptsは項目の入力とボタンの押下を行います。
出力ページのcontent_scriptsは出力ページの内容を取得してメッセージを使用してdefault_popupに内容を送信します。

なお、ChromeとFirefoxの拡張機能は同じような実装で作成できます。

Chromeの拡張機能で自動操作

Chrome拡張機能での自動操作のサンプル
https://github.com/mima3/auto_sample/tree/master/auto_chrome_sample

以下が実際の自動操作を行っているcontent_scriptになります。

chrome.runtime.onMessage.addListener(
  function(request, sender, sendResponse) {
    console.log(request);
    let nameElem = document.getElementsByName('name');
    nameElem[0].value = '名前太郎';
    let mailElem = document.getElementsByName('mail');
    mailElem[0].value = 'test@co.jp';
    let commentElem = document.getElementsByName('comment');
    commentElem[0].value = '猫猫子猫\n犬犬子犬';
    // チェックボックス
    let chkElem = document.getElementsByName('q1[]');
    chkElem[0].click();
    chkElem[2].click();
    // ラジオボタン
    let radioElem = document.getElementsByName('men');
    radioElem[1].click();
    // 選択項目
    var itr = document.evaluate("//select[@name='osi[]']/option", document, null, XPathResult.UNORDERED_NODE_ITERATOR_TYPE, null );
    var node = itr.iterateNext();
    while(node) {
      if (node.textContent === '千鶴さん' || node.textContent === 'さおりん') {
        node.selected = true;
      }
      node = itr.iterateNext();
    }
    // ボタン押下
    // contents.jsでwindows.confirmを書き換えてもブラウザ側の処理に影響を与えない
    // そのため、window.confirmを書き換えるscritpをタグとして挿入する
    // 考え方は以下を参考
    // https://qiita.com/suin/items/5e1aa942e654bce442f7
    let scr = document.createElement("script");
    scr.setAttribute('type', 'text/javascript');
    scr.innerText = 'window.confirm = function () { return true; }';
    document.body.appendChild(scr); 
    setTimeout(function(){ 
      //ここでやってもブラウザ上のwindow.confirmは影響ない。
      var btnElem = document.evaluate("//input[@value='登録する']", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
      btnElem.singleNodeValue.click();
    }, 0);
  }
);

JavaScriptのDOM操作で入力項目を設定後、登録ボタンを押下します。
この際、confirmで確認ダイアログが表示されるため、window.confirmの処理を上書きしてダイアログが出ないようにしています。
content_scriptからブラウザで使用しているJavaScriptを更新するため、scriptタグを埋め込んでいます。
この考え方は下記を参考にしました。

Chrome拡張開発: 拡張からページにJavaScriptを送り込みたい
https://qiita.com/suin/items/5e1aa942e654bce442f7

Firefoxの拡張機能で自動操作

Firefox拡張機能での自動操作のサンプル
https://github.com/mima3/auto_sample/tree/master/auto_firefox_sample

拡張機能での自動操作のまとめ

ブラウザの拡張機能を利用することでブラウザの自動操作が行えます。
この方法のメリットとしてはインターネットの接続がなくてもテキストエディタのみで自動操作を行うためのスクリプトが作成できることです。(ブラウザを開発者モードで動かしていいという条件は必要)

もし、自動操作中にネイティブのアプリと連携が必要になった場合はNative Messagingを使用してください。このNaitiveMessageを使用したサンプルは以下にあります。

RPAツールを使用する方法

お高いRPAツールはブラウザの操作をサポートしている商品が多いです。
今回は小規模事業や個人利用なら無料でしようできるUiPath Communityを利用してChromeの操作を行います。

ChromeをUiPathで操作する場合、Chromeの拡張機能をインストールする必要があるので、下記を参考にインストールしてください。
https://docs.uipath.com/studio/lang-ja/docs/installing-the-chrome-extension

(1)UIPathで新規プロジェクトを作成します。言語はC#を選択します。
image.png

(2)「ブラウザを開く」アクティビティを追加します。
image.png

プロパティ
url "http://needtec.sakura.ne.jp/auto_demo/form1.html";
ブラウザの種類 Chrome

(3)「文字を入力」アクティビティを追加して「画面上で指定」でブラウザ上のテキスト入力項目を指定します。

image.png

image.png

image.png

(4)「文字を入力」アクティビティのプロパティを設定します。
image.png

(5)(3)~(4)を繰り返して「名前:」、「メールアドレス:」、「コメント:」を入力します。
image.png

プロパティ
表示名 文字を入力 'INPUT-名前'
テキスト "名前太郎"
フィールド内を削除 ON
ウィンドウメッセージを送信 OFF
入力をシミュレート OFF
プロパティ
表示名 文字を入力 'INPUT-メールアドレス'
テキスト "test@mail.co.jp"
フィールド内を削除 ON
ウィンドウメッセージを送信 ON ※デフォルトの挙動だとIMEが有効となり全角で入力されてしまう
入力をシミュレート OFF
プロパティ
表示名 文字を入力 'TEXTAREA-コメント'
テキスト "猫猫子猫\n\r犬犬子犬"
フィールド内を削除 ON
ウィンドウメッセージを送信 OFF
入力をシミュレート OFF

(6)「クリック」アクティビティを追加して「画面上で指定」でブラウザ上のクリックが必要な項目をを指定します。
image.png

image.png

image.png

(7)「クリック」アクティビティのプロパティを設定します。
image.png

(8)(5)~(6)を繰り返して「その1」、「その3」、「そば」をクリックします。
image.png

プロパティ
表示名 クリック 'INPUTーその1'
ウィンドウメッセージを送信 ON(デフォルトだと挙動が安定しない)
入力をシミュレート OFF
プロパティ
表示名 クリック 'INPUTーその3'
ウィンドウメッセージを送信 ON(デフォルトだと挙動が安定しない)
入力をシミュレート OFF
プロパティ
表示名 クリック 'INPUTーそば'
ウィンドウメッセージを送信 ON(デフォルトだと挙動が安定しない)
入力をシミュレート OFF

(9)リストを複数選択するために「JSスクリプトを挿入」アクティビティを追加します。
image.png

選択したJSスクリプトは下記の通りです。

function selectmulti(e, aryStr) {
  var ary = JSON.parse(aryStr);
  var itr = document.evaluate("//select[@name='osi[]']/option", document, null, XPathResult.UNORDERED_NODE_ITERATOR_TYPE, null );
  var node = itr.iterateNext();
  while(node) {
    if (ary.indexOf(node.textContent) >= 0) {
      node.selected = true;
    }
    node = itr.iterateNext();
  }
}
プロパティ
スクリプトコード select_multi.js
入力パラメータ "[\"千鶴さん\",\"さおりん\"]"

CTRLキーを押しながらのリスト項目をクリックする操作や、「複数の項目を選択」アクティビティを使用した実装だと動作しない場合がありました。

Web上のリストボックスで複数選択したい
https://forum.uipath.com/t/web/113531/9

また、ここで指定したJavaScript中で日本語やハングルは使用しないでください。文字化けします。日本語などが必要な場合は引数で渡すようにしてください。
たとえば「alert("千鶴さん");」とかいうコードを埋め込むと以下のようになります。

image.png

(10)登録ボタンを押下するために「クリック」アクティビティを追加します。
image.png

プロパティ
表示名 クリック 'INPUT-登録'
ウィンドウメッセージを送信 ON(デフォルトだと挙動が安定しない)
入力をシミュレート OFF

(11)登録ボタン押下後の確認メッセージを閉じるために「画像をクリック」アクティビティを追加します。

image.png

なお、「select_multi.js」に以下のコードを追加してconfirm関数を上書きして確認メッセージを表示させないことも可能です。

  let scr = document.createElement("script");
  scr.setAttribute('type', 'text/javascript');
  scr.innerText = 'window.confirm = function () { return true; }';
  document.body.appendChild(scr);

(12)登録後のページのデータを取得するために「データスクレイピング」を行います。

「データスクレイピング」アイコンを押下します。
image.png

「取得ウィザード」で「次へ」ボタンを押下します。
image.png

要素の選択が可能になるのでテーブルのセルを選択します。
image.png

「表全体からデータを抽出しますか?」の確認メッセージには「はい」を選択します。
image.png

「取得ウィザード」で「終了」ボタンを押下します。
image.png

「次へのリンクを指定」の確認メッセージには「いいえ」を選択します。
image.png

「データスクレイピング用のアクティビティが追加されます。
image.png

(13)「構造化データを抽出」アクティビティの「出力」プロパティに対してCTRL+Kを押下してresult変数を追加します
image.png
image.png

(14)「繰り返し(各行)」アクティビティを追加します。この際、コレクションには「result」変数を指定してください。
image.png

(15)「繰り返し(各行)」アクティビティに「一行を書き込み」アクティビティを追加します。

image.png

プロパティ
Text row[0].ToString() + " " + row[1].ToString()

(16)これまでの操作を再生すると以下のようになります。
auto3.gif

UiPathでのブラウザの自動操作のまとめ

UiPathを使用したメリットは以下の通りです。
・要素を画面から選択できる
・画像認識による自動操作ができる
・ブラウザ以外の自動操作が同じ操作感で行える。
・今回は説明してませんがUiPath Orchestratorで資産管理が容易になる

逆にデメリットは以下の通りです。
・GUIでのプログラミングになるので、複雑な実装が困難である
・GUIなので変更点の差分を見るのが困難で、コードレビューが負担になる。※結局はxamlなのでテキストで差分はとれるが…
・なれないとハマるポイントが多い。
・UiPathの操作でDOMの要素を変更したりしているのでシステム試験等で使用する場合、妥当性を考える必要がある。
例:UiPathで操作した要素には以下のように「uipath_custom_id」という属性が追加されている。
image.png

UiPathは外部プログラムを呼び出す機能やPowerShellの実行が可能なのでブラウザの操作は別の手法で行うことも可能です。

なお、RPAで未経験者でもお手軽自動操作とかいう言説が大きくなっていますが、正直、WebアプリケーションやWindowsアプリケーションを組んだことのない人が簡単に使えると言われると大きな疑問が残ります。
逆にRPAツール不要論もありますが、UiPath Orchestratorの存在や、簡単なフローが頻繁に変わる業務形態における仕事の分担という観点で、そのRPA不要論についても絶対的な真理とは言えないでしょう。

状況にあわせた組み合わせが必要と思います。

ブラウザのGUI上の操作をプログラム上で真似して自動化する方法のまとめ

ここまでで、ブラウザのGUI上の操作をプログラム上で真似して自動化する方法について紹介しました。

RPAツールを使える環境の場合、RPAツールは自動化を行う上で便利ではありますが、それを使うことを目的にしない方がいいです。必要に応じて別の方法をミックスして使うようにしましょう。

外部ライブラリを使用できる環境の場合、Seleniumを採用するのが一番楽だと思います。ChromeやFirefoxならSeleniumIDEで録画機能もついているので生産性は高いでしょう。

外部ライブラリが使用できない環境の場合、InternetExploreのCOM又は、ブラウザの拡張機能を使用することになります。
IEを使用する場合、操作対象のWebアプリケーションのサポート状況をよく確認しましょう。

いずれの方法でブラウザ操作を自動化するにせよ以下の点は気をつけてください。

安易なSleepを使用しない

たとえば、適当に2秒待つという処理をいれた場合、ネットワークやPCの負荷状況によって動作しない可能性があります。
Sleepよりも以下で判断するようにしましょう。

・ document.readyStatusなどを活用する
・ 特定の要素が出現したか、消えたかを見て判断する。
 →SeleniumやUiPathでは、要素の出現消滅の検出をサポートする機能が提供されている

テキスト入力は手動入力と異なる挙動をする可能性がある

テキスト入力を行う場合、手動で入力した場合と異なる挙動をする可能性があります。
たとえばキーボード操作のイベントで何らかの処理していたり、フォーカスの移動で何らかの処理をしていたりする場合です。
UiPathの場合、入力モードに以下の3種類があるので必要に応じて使いわけてください。

・デフォルト:デバイスドライバ経由なので手入力にもっとも近い
・WindowsMessage:Windowsのメッセージを利用してテキストを入力している。
・Simulate:コントロールを直接操作している

それ以外の場合は、JavaScriptのコード上でアプリが期待するイベントを無理やり起こす必要があります。

必要に応じてJavaScriptを利用する

複雑なUIの場合、画面要素を一々クリックするより、WebアプリケーションのJavaScriptを直接実行した方が早い場合があります。
また、いままでの例にでてきたように、alertやconfirmのような自動操作がし辛いポップアップの出現を抑止することが可能になります。

テストの自動化についてはテスト方針をよく確認する

ツールをつかったテストは強力ですが、それはユーザが動かしたものと全く同一にならないことに注意してください。
たとえば、先にあげたJavaScriptを呼び出して処理を行った場合、それがテストとして妥当かどうかはテストの方針や観点しだいになります。

UIの軽微な変更で動作しなくなることを忘れないこと

ブラウザの自動テストはUIの軽微な変更で簡単に動かなくなります。
たとえばリストの2番目と3番目の項目を選択するという実装だと、リストの項目が追加された場合に簡単に動作しなくなります。
これがなるべく影響を受けないような書き方をすることも可能ですが、限界はあります。

もしブラウザの自動化スクリプトを重要な業務で使用している場合は、前に動いたスクリプトだからと安心せずWebアプリケーションの変更にともなって定期的に以前に書いた自動化スクリプトが動作するか確認するようにしてください。

ブラウザから送信しているデータを真似する方法

ブラウザの送受信データの確認方法

ブラウザから送信しているデータを真似して自動化する前にブラウザからどんなデータを送受信しているか調べる方法を説明します。

下記のページで登録ボタンを押した場合にどのようなデータを送信しているか確認してみましょう。
http://needtec.sakura.ne.jp/auto_demo/form1.html

Chromeでの送受信データの確認方法

(1)F12で開発者ツールを開き、「Network」タブを選択します。
image.png

(2)画面上で入力操作を行い「登録する」ボタンを押します。しばらくすると受信ファイルの一覧が表示されます。
net1.gif

(3)「regist1.php」などの受信ファイルを選択後に、Headersタブを選択すると送信データが確認できます。
image.png

(4)Responseタブを選択すると受信内容が確認できます。
image.png

Firefoxでの送受信データの確認方法

(1)F12で開発者ツールを開き、「ネットワーク」タブを選択します。
image.png

(2)画面上で入力操作を行い「登録する」ボタンを押します。しばらくすると受信ファイルの一覧が表示されます。
image.png

(3)「regist1.php」などの受信ファイルを選択します。
image.png

(4)パラメータタブでFormの送信情報を確認できます。
image.png

(5)応答タブでサーバーからのレスポンスデータを確認できます。
image.png

IE11での送受信データの確認方法

(1)F12で開発者ツールを開き、「ネットワーク」タブを選択します。
image.png

(2)画面上で入力操作を行い「登録する」ボタンを押します。しばらくすると受信ファイルの一覧が表示されます。
image.png

(3)「regist1.php」などの受信ファイルを選択します
image.png

(4)本文タブを選択し、さらに「要求本文」タブを選択するとFormの送信情報を確認できます。
image.png

(5)本文タブを選択し、さらに「応答本文」タブを選択するとサーバーからのレスポンスデータを確認できます。
image.png

旧Edgeでの送受信データの確認方法

(1)F12で開発者ツールを開き、「ネットワーク」タブを選択します。
image.png

(2)画面上で入力操作を行い「登録する」ボタンを押します。しばらくすると受信ファイルの一覧が表示されます。
image.png

(3)「regist1.php」などの受信ファイルを選択します
image.png

(4)本文タブを選択し、さらに「要求本文」タブを選択するとFormの送信情報を確認できます。
image.png

(5)本文タブを選択し、さらに「応答本文」タブを選択するとサーバーからのレスポンスデータを確認できます。
image.png

新Edgeでの送受信データの確認方法

Chromeと同じです。
image.png

単純なFormデータの送信例

下記のページのような単純なフォームのデータの送信例を説明します。
http://needtec.sakura.ne.jp/auto_demo/form1.html

curlコマンド

macやlinux系のOSならcurlコマンドを使用することで単純なフォームデータをPOSTすることが可能です。
windos10でもプリインストールされるようになったようですが、文字コードの問題があるので注意が必要です。また、PowerShellを使用しているとcurlコマンドが利用できますが、これはInvoke-WebRequestの別名です。

以下はCentOS7でcurlコマンドを実行した例となります。

>curl  -F "name=名前太郎" -F "mail=test@co.jp" -F "comment=コメント" -F "q1[]=その1" -F "q1[]=その3"  -F "men=soba" -F "osi[]=千鶴さん" -F "osi[]=さおりん"   http://needtec.sakura.ne.jp/auto_demo/regist1.php



sample


名前名前太郎
メールアドレスtest@co.jp
コメントコメント
チェック その1, その3
めんsoba
おし千鶴さん,さおりん

powershellの例

PowerShellではInvoke-WebRequestを利用してFormデータを送信可能です。

    $data = @{
      name='名前太郎';
      mail='test';
      comment=@"
    猫猫子猫
    犬犬子犬
"@;
      'q1[0]'='その1';
      'q1[1]'='その3';
      men='soba';
      'osi[0]'='千鶴さん';
      'osi[1]'='さおりん'
    }
    $ret = Invoke-WebRequest http://needtec.sakura.ne.jp/auto_demo/regist1.php -Method POST -Body $data -ContentType "application/x-www-form-urlencoded"
    $html = $ret.ParsedHtml
    $list = $html.getElementsByTagName("tr")
    for($i=0; $i -lt $list.length; $i++) {
      $item = $list[$i]
      Write-Host($item.innerText)
      [System.Runtime.Interopservices.Marshal]::ReleaseComObject($item) | Out-Null
      Remove-Variable item -ErrorAction SilentlyContinue
    }
    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($list) | Out-Null
    Remove-Variable list -ErrorAction SilentlyContinue

    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($html) | Out-Null
    Remove-Variable html -ErrorAction SilentlyContinue

    [System.GC]::Collect()
    [System.GC]::WaitForPendingFinalizers()
    [System.GC]::Collect()

ParsedHtmlはmshtmlになっているので、構文解析を容易に行えます。
なお、mshtmlはCOMなのでReleaseComObjectを実施して解放処理をしておいた方が無難です。

なお、ページによっては文字化けする場合があります。この場合は以下のように文字コードを変換して出力します。

# 以下参考
# https://qiita.com/zaki-lknr/items/1ae3258d7b77c5e2a2ba
$ret = Invoke-WebRequest http://needtec.sakura.ne.jp/auto_demo/form1.html
$content = [System.Text.Encoding]::UTF8.GetString( [System.Text.Encoding]::GetEncoding("ISO-8859-1").GetBytes($ret.Content) )
Write-Host $content

VBAまたはVBSの場合

[MSXML2.XMLHTTP](https://docs.microsoft.com/en-us/previous-versions/windows/desktop/ms759148(v%3dvs.85)?ranMID=24542&ranEAID=je6NUbpObpQ&ranSiteID=je6NUbpObpQ-.nTP.GKYEngxVvAZbklXFg&epi=je6NUbpObpQ-.nTP.GKYEngxVvAZbklXFg&irgwc=1&OCID=AID2000142_aff_7593_1243925&tduid=(ir__19oa1xfwk0kfrlkrkk0sohzx0m2xghgc2yhm6a1a00)(7593)(1243925)(je6NUbpObpQ-.nTP.GKYEngxVvAZbklXFg)()&irclickid=_19oa1xfwk0kfrlkrkk0sohzx0m2xghgc2yhm6a1a00)又は[Windows HTTP](https://docs.microsoft.com/en-us/windows/win32/winhttp/winhttp-start-page)を利用することでFormデータを送信可能です

受信したHTMLはMSHTML.HTMLDocumentできます

以下はVBSのサンプルになっていますがWScript.EchoをDebug.Print等に置き換えることでVBAでも流用できると思います。

MSXML2.XMLHTTPの例

' 参考:
' https://outofmem.tumblr.com/post/63052619242/vbaexcel-vba%E3%81%A7http%E9%80%9A%E4%BF%A1
' https://stackoverflow.com/questions/9931429/parse-html-file-using-mshtml-in-vbscript
Dim httpReq
Set httpReq = CreateObject("MSXML2.XMLHTTP")

Call httpReq.Open("POST", "http://needtec.sakura.ne.jp/auto_demo/regist1.php", False)
Call httpReq.setRequestHeader("Content-Type", "application/x-www-form-urlencoded;charset=utf-8")
Dim postData
postData = "name=名前太郎&mail=test@co.jp&comment=猫猫" & vbCrLf & "子犬&q1[]=その1&q1[]=その2&men=soba&osi[]=千鶴さん&osi[]=さおりん"
Call httpReq.Send(postData)

Dim objHtml
Set objHtml = CreateObject("htmlfile")
call objHtml.write(httpReq.responseText)

Dim list
Set list = objHtml.getElementsByTagName("tr")
Dim item 
For Each item In list
  WScript.Echo item.innerText
Next

Windows HTTPの例

'略
Set httpReq = CreateObject("WinHttp.WinHttpRequest.5.1")
'略

CreateObjectに与える値を変更するだけで切り替えが可能です。
MSXML2.XMLHTTPとWindows HTTPで挙動に違いはこの時点ではありません。(後述のCookieを使用する場合に違いがでてきます。)
この違いについては下記で話題にあがっています。 

Pythonの例

http.clientを使用してFormデータを送信して結果を受信可能です。html.parserを利用することでHTMLの解析も行えます。おそらく、python3xが入っている環境ならどこでも使えると思います。

import http.client, urllib.parse
from html.parser import HTMLParser

# 結果ページを解析するパーサー
class ResultParser(HTMLParser):
  def __init__(self):
      HTMLParser.__init__(self)
      self.flag = False

  def handle_starttag(self, tag, attrs):
      if tag == "td":
          self.flag = True

  def handle_data(self, data):
      if self.flag:
          print (data)
          self.flag = False

conn = http.client.HTTPConnection('needtec.sakura.ne.jp')

headers = {
  'Content-type': ' application/x-www-form-urlencoded'
}

data = {
  'name': '名前太郎',
  'mail': 'test@co.jp',
  'comment' : '猫猫子猫\n\r犬犬子犬',
  'q1[0]' : 'その1',
  'q1[1]' : 'その3',
  'men' : 'soba',
  'osi[0]' : '千鶴さん',
  'osi[1]' : 'さおりん'
}
# r = requests.post('http://needtec.sakura.ne.jp/auto_demo/regist1.php', data=data)
# print(r)
# print(r.text)

params = urllib.parse.urlencode(data)

conn.request('POST', '/auto_demo/regist1.php', params, headers)
response = conn.getresponse()
print(response.status, response.reason)
parser = ResultParser()
# trの内容を出力
parser.feed(response.read().decode())
conn.close()

外部ライブラリを使う場合

requestsパッケージを使うとデータの送受信が、Beautiful Soupを使うとHTMLの解析が楽になります。

import requests
from bs4 import BeautifulSoup

data = {
  'name': '名前太郎',
  'mail': 'test@co.jp',
  'comment' : '猫猫子猫\n\r犬犬子犬',
  'q1[0]' : 'その1',
  'q1[1]' : 'その3',
  'men' : 'soba',
  'osi[0]' : '千鶴さん',
  'osi[1]' : 'さおりん'
}
r = requests.post('https://needtec.sakura.ne.jp/auto_demo/regist1.php', data=data)
print(r.status_code, r.reason)
soup = BeautifulSoup(r.text)
for tr in soup.find_all('tr'):
  print('------------------------')
  print(tr.text)

認証があるページの例

単純なフォームの送信例はPOSTを一回送信するだけで済みましたが、認証処理やページの不正遷移防止が行われているWebアプリについてはサーバからの情報を受け取ってそれを基にデータを送信する必要があります。

今回はbitnamiから取得できるRedmineのVMでチケット登録を行うサンプルを見てみます。
VMのもろもろの設定はTestLinkで設定したときと同様に行えます。

Redmineでログインしてチケットを登録するには以下の手順を踏む必要があります。

  • ログインページを取得する。
  • サーバーはヘッダーにセッションID、HTML中に認証トークン文字を埋め込んでログイン用のページを返す。
  • セッションIDをリクエストヘッダに設定し、リクエストボディに認証トークン、ユーザ名、パスワード、ログイン後の遷移ページ(チケット登録画面)を指定してログイン処理を行う。
  • サーバーはログインに成功したらチケット登録画面を返す。この際、認証トークン文字が新しいものに変更される。
  • セッションIDをリクエストヘッダに設定し、リクエストボディに認証トークン、チケット情報を指定してチケット登録処理を行う。

PowerShellの例

PowerShellでRedmineのチケットを登録するには以下のようになります。

# エラーが起きたらとめる
$ErrorActionPreference = "Stop"

# サーバから取得したCookieの値からキーを指定して値を取得する
function get_key_value($value, $key) {
  $tmp = $value.substring($value.indexof($key) + $key.length)
  $ret = $tmp.substring(0, $tmp.indexof(';'))
  return $ret
}

# DOMを解析して指定の名前の指定の属性を取得する
function get_attribyte_value($html, $elem_name, $attr_name) {
  $elems = $html.getElementsByName($elem_name)
  $elem = $elems[0]
  $attrs = $elem.attributes
  $attr = $attrs[$attr_name]
  $ret = $attr.value
  [System.Runtime.Interopservices.Marshal]::ReleaseComObject($elems) | Out-Null
  [System.Runtime.Interopservices.Marshal]::ReleaseComObject($elem) | Out-Null
  [System.Runtime.Interopservices.Marshal]::ReleaseComObject($attrs) | Out-Null
  [System.Runtime.Interopservices.Marshal]::ReleaseComObject($attr) | Out-Null
  return $ret
}

##################################
# Redmineの設定値
##################################
$redmine_host = "192.168.0.200"  # サーバ名
$redmine_project = "test1"       # プロジェクト名
$username = "user"
$password = "pass"

##################################
# ログインページを初回アクセスしてセッションIDとcsrf-tokenを取得する
##################################
$ret = Invoke-WebRequest "http://$redmine_host/login" -Method GET

# セッションID取得
$cookie = $ret.Headers['Set-Cookie']
$session_id = get_key_value $ret.Headers['Set-Cookie'] '_redmine_session='

# ログインページのcsrf-token取得
$html = $ret.ParsedHtml
$csrf_token = get_attribyte_value $html 'csrf-token' 'content'
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($html) | Out-Null

# セッション情報作成
$session = New-Object Microsoft.PowerShell.Commands.WebRequestSession
$cookie = New-Object System.Net.Cookie 
$cookie.Name = "_redmine_session"
$cookie.Value = $session_id
$cookie.Domain = $redmine_host
$session.Cookies.Add($cookie);

##################################
# ログイン処理。
# ログイン後はチケット登録画面へ
##################################
$login_data = @{
  authenticity_token = $csrf_token;
  back_url = "http://$redmine_host/projects/$redmine_project/issues/new";
  username = $username;
  password = $password;
}
$ret = Invoke-WebRequest  "http://$redmine_host/login" -Method POST -WebSession $session -Body $login_data -ContentType "application/x-www-form-urlencoded"

# csrf-token取得
$html = $ret.ParsedHtml
$csrf_token = get_attribyte_value $html 'csrf-token' 'content'
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($html) | Out-Null

##################################
# チケット登録
##################################
Write-Host "チケット登録........................................................"
$title = Get-Date -format "yyyyMMddHHmmss"
$ticket_data = @{
  'utf8' = '✓';
  authenticity_token = $csrf_token;
  'issue[is_private]' = 0;
  'issue[tracker_id]' = 1;
  'issue[subject]' = "自動登録 $title";
  'issue[description]' = "わっふるわっふる";
  'issue[status_id]' = 1;
  'was_default_status' = 1;
  'issue[priority_id]' = 2;
  'issue[start_date]' =  '2019-10-10';
  'issue[due_date]' =  '';
  'issue[done_ratio]' = 0;
  'commit' = '作成'
}
$ret = Invoke-WebRequest  "http://$redmine_host/projects/$redmine_project/issues" -Method POST -WebSession $session -Body $ticket_data -ContentType "multipart/form-data"
$html = $ret.ParsedHtml
Write-Host $html.title
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($html) | Out-Null

VBAまたはVBSの場合

MSXML2.XMLHTTPを使用した場合、レスポンスデータのSet-Cookieは削除されてしまいます。(WireSharkなどのパケットアナライザーでみるとデータ自体はきているがライブラリで消している)

Windows Httpを使用することになるのですが、リダイレクトを含むページがある場合に自動でリダイレクトをさせると_redmine_sessionのやりとりがうまくいかないようです。このことを考慮して実装したコードが以下のようになります。
以下はVBSのサンプルになっていますがWScript.EchoをDebug.Print等に置き換えることでVBAでも流用できると思います。

'---------------------------------------------------------------------------
' Redmineの設定
'---------------------------------------------------------------------------
Const redmineHost = "192.168.0.200"
Const redminePrj = "test1"
Const user = "user"
Const password = "pass"

'
' エスケープ処理
'https://needtec.sakura.ne.jp/wod07672/?p=9277
'http://www.xtremevbtalk.com/showthread.php?t=152882
Public Function escape(ByVal StringToEncode )
    Dim i 
    Dim acode 
    Dim char 
    escape = StringToEncode

    For i = Len(escape) To 1 Step -1
        acode = AscW(Mid(escape, i, 1))
        Select Case acode
            'VBAだと動くがVBSだと動かない。エンコードしても問題ないのでよしとする
            'Case 48 To 57, 65 To 90, 97 To 122
            '    ' don't touch alphanumeric chars
            '
            Case 32
                ' replace space
                escape = Left(escape, i - 1) & "%20" & Mid(escape, i + 1)

            Case Else
                ' replace punctuation chars with "%hex"
                char = Hex(acode)
                If Len(char) > 2 Then
                    If Len(char) = 3 Then
                        char = "0" & char
                    End If
                    escape = Left(escape, i - 1) & "%u" & char & Mid(escape, i + 1)
                Else
                    If Len(char) = 1 Then
                        char = "0" & char
                    End If
                    escape = Left(escape, i - 1) & "%" & char & Mid(escape, i + 1)
                End If
        End Select
    Next
End Function

' サーバから取得したCookieの値からキーを指定して値を取得する
Function GetKeyValue(ByVal v, ByVal key)
  Dim tmp
  tmp = Mid(v, InStr(v, key) + Len(key))
  GetKeyValue = Mid(tmp, 1, InStr(tmp, ";") - 1)
End Function

' 特定の特定の名前を持つ要素の属性を取得
Function GetAttributeValue(ByVal objHtml, ByVal elemName, Byval attrName)
  Dim elems
  Set elems = objHtml.getElementsByName(elemName)
  GetAttributeValue = elems.Item(0).GetAttribute(attrName)
End Function

Dim httpReq
Set httpReq = CreateObject("WinHttp.WinHttpRequest.5.1")
httpReq.Option(6) = True ' WinHttpRequestOption_EnableRedirects リダイレクトを無効にする

'-------------------------------------------------------------
' ログインページに遷移
'-------------------------------------------------------------
Call httpReq.Open("GET", "http://" & redmineHost & "/login", False)
Call httpReq.Send

' セッションID取得
Dim sessionId
sessionId = GetKeyValue(httpReq.getResponseHeader("Set-Cookie"), "_redmine_session=")

Dim objHtml
Set objHtml = CreateObject("htmlfile")
call objHtml.write(httpReq.responseText)

Dim csrfToken
csrfToken = GetAttributeValue(objHtml, "csrf-token" ,"content")
objHtml.Close

WScript.Echo "ログインページ取得-----------------------------"
WScript.Echo httpReq.Status
WScript.Echo "_redmine_session:" & sessionId
WScript.Echo "csrfToken:" & csrfToken

'-------------------------------------------------------------
' ログイン処理
'-------------------------------------------------------------
Call httpReq.Open("POST", "http://" & redmineHost & "/login", False)
Call httpReq.setRequestHeader("Content-Type", "application/x-www-form-urlencoded")
Call httpReq.setRequestHeader("Cookie", "_redmine_session=" & sessionId)
Call httpReq.setRequestHeader("Host", redmineHost)

Dim loginData
loginData = "authenticity_token=" & escape(csrfToken) & _
            "&back_url=" & "http://" & redmineHost & "/projects/" & redminePrj & "/issues/new" & _
            "&username=" & user & _
            "&password=" & password
Call httpReq.Send(loginData)

WScript.Echo "ログイン実行-----------------------------"
WScript.Echo httpReq.Status
sessionId = GetKeyValue(httpReq.getResponseHeader("Set-Cookie"), "_redmine_session=")
WScript.Echo "_redmine_session:" & sessionId

Dim redirectUrl
redirectUrl = ""
If httpReq.Status = 302 Then
  redirectUrl = httpReq.getResponseHeader("Location")
End If

If redirectUrl <> "" Then
  '-------------------------------------------------------------
  ' チケット登録ページへリダイレクト
  '-------------------------------------------------------------
  WScript.Echo "リダイレクト:" & redirectUrl 
  Call httpReq.Open("GET", redirectUrl, False)
  Call httpReq.Send()
  WScript.Echo httpReq.Status
End If

' トークンの再取得
call objHtml.write(httpReq.responseText)
csrfToken = GetAttributeValue(objHtml, "csrf-token" ,"content")
objHtml.Close

'-------------------------------------------------------------
' チケット追加
'-------------------------------------------------------------
WScript.Echo "チケット登録-----------------------------"
sessionId = GetKeyValue(httpReq.getResponseHeader("Set-Cookie"), "_redmine_session=")
WScript.Echo "_redmine_session:" & sessionId
WScript.Echo "csrfToken:" & csrfToken

Call httpReq.Open("POST", "http://" & redmineHost & "/projects/" & redminePrj & "/issues", False)
Call httpReq.setRequestHeader("Content-Type", "multipart/form-data")
Call httpReq.setRequestHeader("Cookie", "_redmine_session=" & sessionId)

Dim ticketData
ticketData = "authenticity_token=" & escape(csrfToken) & _
             "&commit=作成" &_
             "&issue[is_private]=0" &_
             "&issue[tracker_id]=1" & _
             "&issue[subject]=自動登録VBS" & Now() & _
             "&issue[description]=わっふるわっふる" & _
             "&issue[status_id]=1" & _
             "&was_default_status=1" & _
             "&issue[priority_id]= 2" & _
             "&issue[start_date]=2019-10-10" &_
             "&issue[done_ratio]=0"
Call httpReq.Send(ticketData)
WScript.Echo httpReq.Status
If httpReq.Status = 302 Then
  redirectUrl = httpReq.getResponseHeader("Location")
End If

If redirectUrl <> "" Then
  '-------------------------------------------------------------
  ' チケット登録後のページへリダイレクト
  '-------------------------------------------------------------
  WScript.Echo "リダイレクト:" & redirectUrl 
  Call httpReq.Open("GET", redirectUrl, False)
  Call httpReq.Send()
  WScript.Echo httpReq.Status
End If

call objHtml.write(httpReq.responseText)
WScript.Echo objHtml.title
objHtml.Close

Pythonの例

HTMLの解析がしんどいのでBeautiful Soupを使用した方がいいでしょう。

import requests
from bs4 import BeautifulSoup
import datetime

##############################################
# redmineの情報
##############################################
redmine_host = "192.168.0.200"  # サーバ名
redmine_project = "test1"       # プロジェクト名
username = "user"
password = "password"

# セッションの作成
session = requests.session()

# ログインページの取得
ret = session.get('http://' + redmine_host + '/login')
print(ret.status_code, ret.reason)
ret.raise_for_status()

session_id = ret.cookies['_redmine_session']

soup = BeautifulSoup(ret.text, 'html.parser')
elem_token = soup.find('meta', {'name': 'csrf-token'})
csrf_token = elem_token['content']

# ログイン処理
cookies = {
  redmine_host : session_id
}
login_data = {
  'authenticity_token' : csrf_token,
  'back_url' : "http://' + redmine_host + '/projects/' + redmine_project + '/issues/new",
  'username' : username,
  'password' : password
}
ret = session.post('http://' + redmine_host + '/login', data=login_data, cookies=cookies)
print(ret.status_code, ret.reason)
ret.raise_for_status()

# チケット登録
print('チケット登録................................')
soup = BeautifulSoup(ret.text, 'html.parser')
elem_token = soup.find('meta', {'name': 'csrf-token'})
csrf_token = elem_token['content']

ticket_data = {
  'utf8' : '✓',
  'authenticity_token' : csrf_token,
  'issue[is_private]' : 0,
  'issue[tracker_id]' : 1,
  'issue[subject]' : "自動登録 " + str(datetime.datetime.now()),
  'issue[description]' : "わっふるわっふる",
  'issue[status_id]' : 1,
  'was_default_status' : 1,
  'issue[priority_id]' : 2,
  'issue[start_date]' :  '2019-10-10',
  'issue[due_date]' : '',
  'issue[done_ratio]' : 0,
  'commit' : '作成'
}
ret = session.post('http://' + redmine_host + '/projects/' + redmine_project + '/issues', data=ticket_data, cookies=cookies)
print(ret.status_code, ret.reason)
ret.raise_for_status()

soup = BeautifulSoup(ret.text, 'html.parser')
print(soup.title)

ブラウザから送信しているデータを真似する方法のまとめ

ブラウザを介さないのでブラウザより簡単にかつ高速で自動操作が行えます。
同時にそれは、デメリットになる場合があります。
たとえば、JavaScriptでクライアントサイドで動的にページを作成している場合、その処理は動作しません。つまり、ブラウザで操作したときと同様のDOMの構成が返ってくるとは限りません。

この手法を結合試験やシステム試験で使用する場合は、注意してください。
仮にテストデータの入力に使う場合であっても、システム上本来作成できないデータが作成されてしまう場合があるからです。試験の観点に合わせて慎重に導入してください。

また、ブラウザの自動操作と同様にWebアプリケーションの変更によって今まで動いていた自動化スクリプトが動作しなくなるリスクはあるので注意してください。

Webアプリケーションが提供しているAPIを利用する方法

もっともリスクの少ないWebアプリケーションの自動化の方法です。
ただしWebアプリケーションがAPIを提供しているかどうかは個別の仕様次第になります。

Redmineのチケット登録の例

これまでにRedmineでチケット登録を行うサンプルをいくつか記述しました、Redmineが提供しているAPIを利用することでシンプルに実装することができます。

まずRedmineの管理画面でRESTAPIを有効にしてください。
image.png

すると個人設定画面でAPIキーが表示されます。このAPIを使用してRedmineを操作します。
image.png

PowerShellの例

チケット用のXMLを作成してPOSTするだけです。
この際、APIキーをヘッダに付与して送信します。

##################################
# Redmineの設定値
##################################
$redmine_host = "192.168.0.200"  # サーバ名
$apikey = "60076966cebf71506ae3f2391da649235a2b1d46"

$title = Get-Date -format "yyyyMMddHHmmss"
$xml = @"

    1
    RESTAPIテスト $title
    試験

"@
# セッション情報作成
$headers = @{
  'X-Redmine-API-Key' = $apikey;
  'Content-Type' = 'text/xml';
}

# 文字化けして登録されるなら以下をいれる
$sendData = [System.Text.Encoding]::UTF8.GetBytes($xml)

$ret = Invoke-WebRequest http://$redmine_host/issues.xml -Headers $headers -Method POST -WebSession $session -Body $sendData
Write-Host $ret.Content

VBAまたVBSの場合

以下はVBSのサンプルになっていますがWScript.EchoをDebug.Print等に置き換えることでVBAでも流用できると思います。

'---------------------------------------------------------------------------
' Redmineの設定
'---------------------------------------------------------------------------
Const redmineHost = "192.168.0.200"
Const apikey = "60076966cebf71506ae3f2391da649235a2b1d46"

Dim httpReq
Set httpReq = CreateObject("WinHttp.WinHttpRequest.5.1")

Call httpReq.Open("POST", "http://" & redmineHost & "/issues.xml", False)
Call httpReq.setRequestHeader("Content-Type", "text/xml")
Call httpReq.setRequestHeader("X-Redmine-API-Key", apikey)

Dim xml
xml = "1 RESTAPIテスト VBS試験"

Call httpReq.Send(xml)
WScript.Echo httpReq.Status
WScript.Echo httpReq.responseText

Pythonの場合

import requests
import datetime

xml = """

    1
    RESTAPIテスト python {0}
    試験

""".format(str(datetime.datetime.now()))

headers = {
  'X-Redmine-API-Key' : '60076966cebf71506ae3f2391da649235a2b1d46',
  'Content-Type' : 'text/xml'
}

r = requests.post('http://192.168.0.200/issues.xml', data=xml.encode('utf-8'), headers=headers)
print(r.status_code, r.reason)
print(r.text)

なお、PythonでやるならPython Redmineあたりのライブラリを使用したほうが楽だと思います。

共通的な注意事項

ここまででWebアプリケーションの自動化の方法についていくつか方法を説明しました。
最後に共通的な注意事項を述べておきたいと思います。

できることと、やっていいことは違う

おそらくここまでで、多くのWebアプリケーションを自動で操作することが可能になったと思います。
しかしながら、できることと、やっていいことは違うということを常に心がけてください。

API経由以外の自動操作はWebアプリケーション側が想定していない操作になる可能性があります。つまり、いつ動かなくなってもおかしくありませんし、仮に動くからといってやっていい操作とは限りません。
場合によっては規約違反に問われることになります。たとえば広告ブロックして云々とか、複数人で遊ぶブラウザゲームの自動化は、かなりの確率で規約違反になります。
Webアプリケーションを自動化する際は必ず規約を確認してから行うようにしましょう。

また、そういった規約が明記されておらず、不正に当たらないと考えられる場合であっても自動操作はWebアプリケーション側に想定外の負荷を与えることがあります。たとえば、2010年には情報取得目的に図書館の蔵書検索システムに高頻度(1秒に1アクセス程度)のリクエストを送信して偽計業務妨害容疑で逮捕された岡崎市立中央図書館事件があったことは心に留めておくべきでしょう。

特に社内システムの場合、品質が悪い傾向があるので、根回しをしつつやっておくか、すぐに停止てきる状況かで実施し始めた方が無難です。

武器や流派にこだわるな

「武器や流派にこだわるな」という格言は、およそ20年前の名著「アジャイルソフトウェア開発 (The Agile Software Development Series) 」の「付録B3 武蔵」の項目にでてきた格言です。

今回、色々な自動化の方法を紹介はしましたが、それは自動化を行うための選択肢を増やして「こだわりを捨ててもらう」意図がありました。

RPAツールは素晴らしく自動化の手助けになります。しかしながら、あきらかに別の方法でやったほうが楽な場合でもRPAツールにこだわるケースがよくみられます。例えばブラウザ画面を介しての自動操作に慣れ親しんだ人はcurlコマンドで済むようなことまで慣れ親しんだという理由だけで困難な技法を選択してしまうケースをよく見かけます。
逆にcurlコマンドでは行うのが困難なことを、それだけでやろうとするケースも同じくらいよく見ます。

普段は使用しない技法であっても必要があるなら採用すべきですし、逆に最も自分が慣れ親しんだ技法であっても状況にそぐわなければ捨てるべきです。

自動化スクリプトの管理方法を考えよう

1度動かせばすむスクリプトなのか、定期的に動かすスクリプトなのかによって、スクリプトの管理方法が変わります。
定期的に動かすスクリプトの場合、常にWebアプリケーションのバージョンアップで動作しなくなるというリスクがあります。
このリスクをどう扱うか考えましょう。

たとえば、実際やってエラーとなった時点で修正する時間的余裕のあるものであれば、その時に考えればいいでしょう。
しかし、そういう時間的余裕が確保できないようなスクリプトの場合は、事前にそれを検出する必要があります。
定期的にスモークテストを行う計画を立てるか、Webアプリケーションのリリースノートをチェックする工数をとるか、いずれにせよなんらかの対策が必要になります。

あとは、自動化スクリプトの意図を複数の人間が理解して、メンテナンスできる体制を作るよう必要があります。人間は割と簡単にいなくなります。一誰も意図が分からない自動化スクリプトが動き続ける状態にならないように気を付けましょう。

自動化のコストを見積もる場合、これらの作ったあとのメンテナンスのコストについて忘れずに考えておきましょう。

自動化を目的にするのはやめよう

慣れてくると、なんらかの方法で多くのことが自動化できるようになりますが、それを目的とするのはやめましょう。
重大な障害対応を放置して、優先度の低い自動化スクリプトを書いても意味はありません。
全体の状況をみて、効果のありそうなところを自動化しましょう。

無理ならあきらめよう

どうしても自動化できないこともあります。
素直にあきらめて別の事をしましょう。

参考

IEが死んだ世界

はじめに

最近ますます、以下の記事のようにIE死すべき慈悲はないという時代になってまいりました。

新Edgeブラウザ登場に伴うIEサポート終了についてチームのコンセンサスを得るためのシンプルなテンプレ
https://qiita.com/uupaa/items/ad1f0f64191dbec56889

今回はIEが本当に死んだ場合、なにが困るかを考えてみようと思います。

検証環境

・Windows10
・Edge Beta Version 77.0.235.25 (Official build) beta (64-bit)
 ※アンインストールも再インストールもさせないEdgeですが、Beta版は上書きでインストールしていないので、飽きたらBeta版をアンインストールすることで元にもどせました。
・VisualStudio2019

検討項目

  • 外部からの自動操作
  • ClickOnceの実行
  • WindowsFormへの埋め込み
  • ShowModalDialogの使用
  • ActiveXの使用
  • XBAPの使用

外部からの自動操作

ブラウザを外部から自動操作する際に大きく3つの方法が考えられると思います。

・Seleniumで自動実行する
・InternetExploreのCOMを利用する
・UiPathなどのRPAツールを使う

Seleniumで自動実行する

すでに新Edge用のWebDriverが公開されているようなので、おそらくは問題なく移行できると思います。
https://developer.microsoft.com/en-us/microsoft-edge/tools/webdriver/

InternetExploreのCOMを使う。

これは一切の外部ツールを使わずにブラウザを自動操作できる方法でした。
仮に旧Edgeだったとしても以下の方法で無理やりInternet Explorer_Serverを取得して動かすことが可能でした。

VBAでInternetExplore上のJavaScriptを無理やり動かすよ!
https://needtec.sakura.ne.jp/wod07672/?p=9210

当然、新Edgeでは動作しません。
すなわち、ダウンロード禁止縛り環境ではブラウザの自動操作はできなくなります。

(補足:UiAutomationで代替できないか?)

Inspectで新EdgeのGoogle開いているページを解析したところ以下のようになります。
image.png
ControlTypeがちゃんと設定されているのでボタン押下ぐらいはUiAutomationで操作可能かもしれません。
ただ、HTMLの構造の解析は難しいと思うので基本、外部ツールなしで自動操作は厳しいと思います。

UiPathなどのRPAツール

UiPathで自動操作をする場合、Chromeの拡張機能をインストールしていたと思います。
この拡張機能とUiPathが連携してブラウザの自動操作を行っています。

実は新EdgeになるとChromeの拡張がそのまま使えるようなので、恐らく、ChromeをサポートしているRPAツールは対応してもらえる可能性が高いと思います。
ユーザ側の対応作業としてはアクティビティの置き換えと各RPA端末に拡張機能をインストールするという工数くらいでしょう。

新Microsoft EdgeはChrome拡張機能をサポート - 阿久津良和のWindows Weekly Report
https://news.mynavi.jp/article/20190402-800458/

ClickOnceの実行

ClickOnceは.NETで作成したバイナリをWebに配置しておき、ブラウザがクリックすると、配布、実行してくれるという仕組みです。
この機能はIE11,ならびに旧Edgeで動作します。

残念ながら、新EdgeについてはEdge Beta Version 77の段階では正常に実行できませんでした。

ただし、下記のブログを見る限り2019 10月に対応するような旨がかかれているので将来的には解消されそうです。

[What we're hearing from you](https://techcommunity.microsoft.com/t5/Discussions/What-we-re-hearing-from-you/m-p/811931?ranMID=24542&ranEAID=je6NUbpObpQ&ranSiteID=je6NUbpObpQ-IjeaeLYGG3YAGIIEGxxSNg&epi=je6NUbpObpQ-IjeaeLYGG3YAGIIEGxxSNg&irgwc=1&OCID=AID2000142_aff_7593_1243925&tduid=(ir__19oa1xfwk0kfrlkrkk0sohzx0m2xgxdntqhm6a1a00)(7593)(1243925)(je6NUbpObpQ-IjeaeLYGG3YAGIIEGxxSNg)()&irclickid=_19oa1xfwk0kfrlkrkk0sohzx0m2xgxdntqhm6a1a00)

Planned to be addressed in the Canary channel in October 2019:

  • Inking on PDFs
  • When you sign-in to the browser, your sign-in profile picture will accurately be kept up to date.
  • When you have more than one profile, better handling for opening links and attachments in the appropriate profile.
  • An option to set your own photos as the background image on the New Tab Page
  • Enable search in the extensions store
  • A bug fix for users who receive an “Administrator Mode Detected” notification (advising them to close and relaunch the browser in non-administrator mode) each time they launch Edge
  • ClickOnce deployment of Windows applications from web pages

WindowsFormへの埋め込み

IE11ならびに旧EdgeではMicrosoft.Toolkit.Wpf.UI.Controls.WebView"や"Microsoft.Toolkit.Forms.UI.Controls.WebView"を使用することで以下のように.NETの画面にブラウザを埋め込めます。

image.png

以下のページによると最近まではIEのみでEdgeは埋め込めなかったようです。

WPFやWindowsフォームでEdgeのWebViewを使うには?[Windows 10 1803以降]
https://www.atmarkit.co.jp/ait/articles/1807/04/news017.html#winform

今後、新Edgeになったとき、この機能が使えるかどうか、経過を見る必要があるでしょう。

showModalDialogの使用

IE11では呼び出し元の画面を触らせないように新規Windowを表示するため、showModalDialogがjavascriptから使用できていました。
しかし、時代はかわり、この機能は非推奨となり、旧Edgeでもサポートしないと明言しています。

"showModalDialog" is not working in IE Edge
https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/18235925/

何らかの方法に置き換える必要があり,StackOverflowでは以下のようなやり方が紹介されています。

showModalDialog alternative?
https://stackoverflow.com/questions/24400388/showmodaldialog-alternative

どう対応するかと、画面数しだいですが、地味にテスト工数がかなりかかると思います。

ActiveXの使用

ブラウザからファイルやレジストリの操作をActiveXで行っているデンジャラスなシステムもありますが、それらは動かなくなります。

基本、ブラウザでそんな物騒な操作をさせていることがおかしいわけなので、これを機に設計を見直したほうがいいと思いますが、どうしても、これを置き換えるというなら、拡張機能を作ってローカルのEXEと通信する方法が考えられます。

Chromeの拡張機能であるNative Messageを使用した回避案

Chromeの拡張機能、また、それが動作する新EdgeではNative Messageという機能を使用して端末上のEXEと通信をすることが可能です。

Native Messaging
https://developer.chrome.com/apps/nativeMessaging

Chrome Native Messaging Example
https://medium.com/@svanas/chrome-native-messaging-example-5cf8f91cdce6

C# native host with Chrome Native Messaging
https://stackoverflow.com/questions/30880709/c-sharp-native-host-with-chrome-native-messaging

端末上のEXEとブラウザ上からやり取りできるのであれば、ActiveXと勝るとも劣らない無茶なことが可能です。

今回は以下のようなEXEと通信するChromeの拡張機能が新IEでも使用できるか検証しました。

image.png

ContentScriptは以下のようになります。
ページ読み込み時にボタンを作成し、このボタンが動作したらbackgroundにメッセージを送信します。
また、backgroundからのメッセージをonMessageで処理します。

backgroundの処理は以下のようになります。

content.js

window.addEventListener("load", function(event) {
  var button=document.createElement("button");
  button.innerText = "ボタン";
  button.id = "btnExtContents";
  document.body.appendChild(button); 
  button.addEventListener("click", function() {
    window.sendMessage('click');
  });
}, false);

window.sendMessage = function sendMessage(message) {
  chrome.runtime.sendMessage(message); 
};

chrome.runtime.onMessage.addListener(function(message) {
  console.log('onMessage...');
  console.log(message);
  alert(JSON.stringify(message));
});

window.sendMessageという関数をここで定義していますが、元のページのJavaScriptからは実行できませんでした。
そのため、元ページでは以下のようにクリックイベントを実行する必要があります。

document.getElementById('btnExtContents').click();

引数が必要ならば適当な要素に格納しておくといいでしょう。

backgroundのスクリプトは下記のようになります。
content_scriptsからのメッセージをNativeメッセージでEXEに渡し、
Exeから受信したメッセージをcontent_scriptsに渡します。

background.js


var port = null;
chrome.runtime.onMessage.addListener(function(message, sender) {
  console.log(message);
  if (!port) {
    var hostName = "com.google.chrome.example.echo";
    port = chrome.runtime.connectNative(hostName);
    port.onMessage.addListener( function (rcv) {
      console.log('onNativeMessage');
      console.log(rcv);
      chrome.tabs.sendMessage(sender.tab.id ,rcv);
    });
    port.onDisconnect.addListener( function () {
      console.log('onDisconnect');
      port = null;
    });
  }
  port.postMessage({"text": message});
});

EXEと拡張機能の通信は標準入力と標準出力で行います。
先頭4バイトに送受信のバイト数を入力し、その後に文字列をJSON形式で格納します。
EXEの実装はどのプログラミング言語でおこなってもいいですが、JSONの取り扱いと、バイナリの取り扱いが楽なものを選択した方がよいでしょう。

ChromeとEdge Beta Version 77.0.235.25 でローカルのExeと通信できた拡張機能のサンプルを以下に置きます。
https://github.com/mima3/hello_sample

WebScoketを使用した回避案

下記の方法でもネイティブアプリと通信できそうです。

Webアプリの限界を超える方法
https://qiita.com/tekka/items/1bf440ccd50bb4171886

Webアプリの限界を超える方法(セキュリティ編)~ActiveXを葬る~
https://qiita.com/tekka/items/d9f6fd2e30c1f778b5aa

ようするにユーザーのマシンに立てたWebSocketサーバーと通信してしまおうという考えです。

XBAPの使用

IEではXBAPを使用することでブラウザに.NETで作った画面を埋め込むことができました。
またセキュリティの設定によってはファイル操作でもレジストリの操作でもできました。

image.png

これに関しては旧Edgeの時点でXBAPのサポートはしないと言い切っています。
https://stackoverflow.com/questions/31895766/xbap-support-in-ie-edge

代替はないとおもいます。
ClickOnceかブラウザいずれかに倒して実装するかなさそうです…いずれにせよ完全に作り直しだと思いますが。

HTAの使用

HTAを使用するとInternetExploreの機能を利用してスタンドアローンのGUIを作成することができます。(Electronのような感じ)

image.png

test.hta

<html>
<head>
 <title>ボタンをおすのです</title>
</title>
<body>
  <input type="button" name="buttonA" value="ボタン" onClick="buttonA()"/>

<script language="VBScript">
  sub buttonA
    MsgBox "わっふる"
  End Sub

</script>
</body>
</html>

IEが亡くなった場合、この機能も必然的につかえなくなるので、拡張子htaで画面を開いている場合は改修が必要になると考えられます。

IEモードによる延命

ここまではIEが完全に死んだ場合を考えてみましたが、慈悲深いマイクロソフトはIEモードという延命処置を用意してくれています。
https://docs.microsoft.com/en-us/deployedge/edge-ie-mode

このモードになることでActiveXすらもサポートしますが、IE11またはMicrosoft Edge F12開発者ツールをサポートしないので、開発時にたぶん苦労します。

…Console.logすら使えないとこで、うまく実装できるイメージがまったくわかん。

まとめ

どういう対応になっても地獄しかみえないので心当たりのある方は、震えて眠りましょう。

古典を読む~オブジェクト指向の落とし穴

はじめに

「オブジェクト指向の落とし穴」は、1995年頃に発売されました。
当時流行で、ある種の熱に浮かれていた「オブジェクト指向ブーム」に水をかけるような内容になっています。

image.png
Bruce F. Webster (原著), 細井 拓史 (翻訳)
https://www.amazon.co.jp/dp/4894712164

本書の構成はPartⅠでオブジェクト指向開発について、その概念と用語を開発者、管理者、部門責任者、経営者といった幅広い人たちが理解を共通してもらえるような簡潔な説明をしています。

PartⅡではオブジェクト指向の落とし穴を以下の構成で解説しています。

  • 前兆:落とし穴にはまりかかっていることを見分けるための知識
  • 結果:そのままだと生じてしまう結果
  • 検知法:落とし穴にはまっているかどうかを調べる方法
  • 回復法:落とし穴から抜け出すための方法
  • 予防法:落とし穴を事前にさける方法

そしてPartⅢではこれらの落とし穴を踏まえた上でのプロジェクト運営のヒントについて記述しています。

本書で紹介されている落とし穴

  • 概念にまつわる落とし穴
    • 間違った理由でオブジェクト指向開発を始める
    • オブジェクト技術がタダで手に入ると思う
    • オブジェクト技術がすべての問題を解決すると思う
    • オブジェクト技術が成熟したものだと考える
    • 宣伝文句を概念と混同する
    • ツールを方針と混同する
    • 見かけを方法論と混同する
    • トレーニングを実戦と混同する
    • プロトタイプを完成品と混同する
  • 政治にまつわる落とし穴
    • 管理職へ教育や支援要請を事前に行わない
    • 反発を過小評価する
    • 技術を過大に売り込む
    • オブジェクト指向開発を崇める
    • アーキテクチャに関する政治的側面を認めない
    • 機能要求の海に溺れてゆく
    • オブジェクト技術に社運を賭ける
  • 管理にまつわる落とし穴
    • はっきりした目的をもたないままオブジェクト技術を採用する
    • 開発者の喉にオブジェクト技術を流し込む
    • 有用なソフトウェア工学手法を捨ててしまう
    • 有効な方法論を定義して活用しない
    • 準備もせずに多くのことをやろうとする
    • 開発が線形ですすむと思い込む
    • 合意のないまま、仕様が変更されるに任せる
    • 新しい機能が忍び込む(あるいはなだれ込む)ことを許す
    • プロトタイプの機能を完成品の機能であると誤認する
    • 開発サイクルの相対コストを見積もり損なう
    • リスクの管理を怠る
    • 自分自身や他人に嘘をつく
    • 間違った尺度を使う
    • 間違った開発者を使う
  • 分析と設計にまつわる落とし穴
    • 分析と設計の必要性を過小評価する
    • 分析と設計の難しさを過小評価する
    • 古い皮手袋に新しいワインを入れる(あるいはその逆)
    • 自分の盲点に気が付かない
    • 解決策が一般的すぎる、あるいは完全すぎる
    • アーキテクトの数を間違える
    • ことを複雑にしすぎる
    • パターンを全て列挙して設計を行う
    • アーキテクチャの再設計を頻繁に、間違った理由で行う
    • アーキテクチャの再設計をほとんど行わない
    • 喜ばせる相手を間違える
    • 新しいパラダイムをユーザに強制する
  • 環境、言語、ツールにまつわる落とし穴
    • 商用アプリケーションのターゲット環境の選択を誤る
    • 社内に導入する環境の選択を誤る
    • オブジェクト指向に関するベンダーの言葉を信じる
    • C++を使う
    • C++を使わない
    • サポートツールとトレーニングにお金をかけない
  • 実装にまつわる落とし穴
    • あわててコーディングを始める
    • カプセル化さえあれば、設計と実装の標準は不要と考える
    • プロジェクトの進行のはやさにだまされる
    • 時間もないのに多くの約束をしすぎる
    • 詳細を後回しにする
    • 軽はずみにサブシステムを書き換える
    • 重要な概念や決定事項をドキュメントに残さず、忘れる
    • 暗黒面に誘惑される
  • クラスとオブジェクトにまつわる落とし穴
    • is-a関係、has-a関係、is-implemented-using関係の3つを混同する
    • インターフェイスの継承を実装の継承と混同する
    • 不当な継承の使い方をする
    • 基底クラスの機能が多すぎたり少なすぎたりする
    • 基底クラスの不変条件を保存しない
    • オブジェクト指向でないコードをそのままオブジェクトに変換する
    • オブジェクトを膨れ上がらせる
    • オブジェクトが崩れるに任せる
    • "スイスアーミーナイフ"のようなオブジェクトを作る
    • "超スパゲッティ"なオブジェクトやサブシステムを作る
  • コーディングにまつわる落とし穴
    • オブジェクトをコピーする
    • オブジェクトの同値性と一意性を調べる
    • オブジェクトを追跡しない
    • メモリの消費に無頓着である
    • switch文と多態を混同する
  • 品質保証にまつわる落とし穴
    • 組み合わせ数が爆発的に増えることを忘れる
    • 単体テストを行わない
    • テストを後回しにする
    • テストやサポートの必要性とそのコストを過小評価する
  • 再利用にまつわる落とし穴
    • 再利用の難しさを理解しない
    • 非現実的な期待を持ったり持たせたりする
    • コードの再利用にばかり目を奪われる
    • 再利用のための投資を惜しむ
    • プロジェクトが終わってから一般化を行う
    • オブジェクト間の接続が多すぎる
    • 循環依存を許す

ここで列挙したオブジェクト指向の落とし穴といわれているものは、オブジェクト指向開発特有のものではなく、ソフトウェア開発にとって一般的なものがあります。

これら一般的な話が、本書で取り上げられた理由としては、オブジェクト指向開発においており大きなダメージを与える類の落とし穴。一般にオブジェクト指向開発のおかげで排除されると考えられていたが、そうではなかった落とし穴。そして、オブジェクト指向開発を破綻させる落とし穴であるためです。

これらの教訓は四半世紀後の視点で読んでみても、今だ役に立ちます。

また、「オブジェクト指向」ではなく「RPA」や「AI」などの現在流行りの単語に置き換えてこれらの教訓は再利用できます。

たとえば、前述の落とし穴を「オブジェクト指向技術がすべての問題を解決すると思う」という落とし穴を「RPA技術がすべての問題を解決すると思う」言い換えて、本書を読み返しても重要な教訓を我々に与えてくれるでしょう。

Amazonのレビューでは2003年時点で、「~オブジェクト指向の本の中でもこの本は古めに属する。」とか言われていますが、同時に「内容は今でも通用することばかり」と言っています。

そして、15年以上たった令和時代においても、より古くはなっていますが、内容は今でもなお通用します。

古典を読む~パターンによるソフトウェア構成管理

はじめに

パターンによるソフトウェア構成管理はソフトウェア開発課題のシリーズとして日本では2006年、原著は2002年に発売されたソフトウェアの構成管理に関する書籍です。

パターンによるソフトウェア構成管理 (IT Architects’ Archive―ソフトウェア開発の課題)  
ステファン・P・バーチャック (著), ブレッド・アップルトン (著), 宗 雅彦 (翻訳)
https://www.amazon.co.jp/dp/4798112593

image.png

発表時期からわかる通り、当時は今のようにとりあえずGitかSvnでも使っていればいいだろうという時代ではありませんでした。様々なバージョン管理ツールが群雄割拠をつげており、それぞれ一長一短ありました。その中で「ツールの使い方」ではなく構成管理をどうすべきかを論じる本著は貴重な資料でした。

本書では構成管理を成功に導くいくつかのパターンが紹介されています。
巻末のオマケとして当時存在したバージョン管理ツールで、これらのパターンをどう扱うかが書かれてはいますが、あくまでオマケにすぎません。

こういった性質なためAmazonのレビューでは、以下のように評している人もいます。

導入本として読むにしては、ソフトウェア工学的な経験がないと行間が読み取りにくく、現場での問題解決に使うには具体性に欠ける。

この意見には私も同意で、お手軽にバージョン管理ツールを導入したいからやり方を調べたいという用途に使用するには向いていません。いくつかのバージョン管理ツールを知っている人間が自分のプロジェクトで構成管理をどう進めるかを考えるヒントとするための本です。

残念ながら発売時期的に分散バージョン管理が出る前の本なので、巻末のオマケだけを見る分には古いです。
しかしながら、本質的な部分は今だ使えるものとなっています。

今回は「成功を導くいくつかのパターン」の概要を説明したうえで、現在のGitなどの分散バージョン管理ツールで、どう扱われるかを考えてみることにします。

構成管理のパターン

構成管理を成功に導くパターンとして説明されているものを簡単に抜粋します。
これらのいくつかはすでにバージョン管理ツールを使用している人にとっては、無意識に実現していることも含まれます。

パターン名 説明
Mainline 実際に開発作業が行われているコードラインの数を管理可能な数に抑えます。ブランチのベースとなるような、またブランチへの作業をマージする先となるような作業の中心となるコードラインを保ちます
Active Developement Line 急速に発展していくコードラインについてどう使える状態にするかを、安定と活発さのバランスをとったポリシーを定めます
Private Workspace 他人の変更が自分に影響を与えない、自分の作業が他人に影響を与えないワークスペースを作ります
Repository 適切なコンポーネント適切なバージョンを使用して適切な開発用のワークスペースを構築するためのRepositoryを用意し一元管理します
Private System Build Private Workspaceで変更した自分の更新をRepositoryに反映する前に最新のコードベースでも自分の変更を含めてビルドできるかを確認します
Integration Build 多くのワークスペースにおいて各人が行った変更を反映したコードベースが必ずビルドされるかどうかを確認します
Third Party Codeline サードパーティのソースコードやライブラリのバージョンと自製品のバージョンとの調整を行うため、サードパーティー用のコードラインを用意します
Task Level Commit 作業単位でバージョン管理システムへのコミットします
Codeline Policy 自分のコードをコードラインに反映する際に、どういうテストをすべきか、どのタイミングで反映するかなどのルールをコードラインごとに定めて周知します
SmokeTest 変更を加えた後もシステムが問題なく動くことを最低限度確認するテストを実施します
Unit Test 変更を加えた後のモジュールが要求仕様通り動作することを詳細にテストします
Regression Test 変更を加えた後の既存コードが悪くなっていないことを確認します
Private Versions 作業者個人の小さい変更を他人に影響を与えずに行い、作業履歴として利用できる方法を提供します
Release Line リリース済みの保守用のコードラインを用意します。
Release Prep Code Line リリース前の準備用のコードラインを用意します。このコードラインはリリース後、Release Lineとして使用します
Task Branch 長期にわたり複数人が変更する必要があるタスクを現在のコードラインに影響を与えないように、そのタスク専用のコードラインを作成します

これらのパターンの関連性をあらわしたものが下記の図になります。

image.png
(裏表紙より)

Mainline

実際に開発作業が行われているコードラインの数を管理可能な数に抑えます。ブランチのベースとなるような、またブランチへの作業をマージする先となるような作業の中心となるコードラインを保ちます。

このMainlineの例は以下のようになります。

image.png

たとえば、リリース用にReleaseAやReleaseBのブランチを作成したとします。
それぞれのブランチはMainLineを基に作成し、それぞれのブランチで発生した変更はMainLineにマージします。
これにより、ブランチが無秩序に増加することを防ぎ、また、マージにかかる作業を最小限にすることができます。

SubversionではMainline考え方をtrunkで実現しようとしています。

Active Developement Line

複数の人間が共同作業をしていく場合、作業の同期をとって各個人の作業内容をまとめる必要があります。
そのため、他人の変更が自分のテストを不合格にすることもありますし、自分の変更が他人のテストを不合格にする場合があります。それを避けるには十分にテストされたコードラインを基に変更を行う必要があります。

このため、各開発者はPrivate Workespaceを作成して作業の分離を行い、自分が行った変更がコードラインを不安定にしないことを確認するためにPrivate System Buildを行い、Unit TestやSmoke Testをできるようにする必要があります。場合によってはRegression Testが必要になるかもしれません。
たしかに十分なテストを行えば、コードラインは安定しますが、それは開発速度の低下とのトレードオフになります。リリース前で許容できる不安定さとリリース後で許容できる不安定さはことなると思います。このようなコードラインの不安定と迅速さを考慮したうえで、各作業者がどうふるまうかをCodeline Policyで決めておく必要があります。

Private Workspace

他人の変更が自分に影響を与えない、自分の作業が他人に影響を与えないワークスペースを作ります。
このワークスペースにはビルドに必要なコンポーネントや変更元となるコードが必要です。

Private Workspaceにはリスクがあります。作成時は適切なバージョンだとしても、長い時間作業をしている間、取得したコンポーネントや変更元のコードが古くなるかもしれません。
このため定期的にPrivate System Buildを行いワークスペースを更新して最新にたもつ必要があります。

Subersionでは、Private Workspaceを作成する場合、checkoutコマンドで自分専用のワークスペースを作成することができます。
また、ワークスペースを更新するにはupdateコマンドになります。

gitの場合、Private Workspaceを作成する場合はcloneコマンドで自分のマシンにリポジトリを作成することになります。
また、ワークスペースを最新にするにはpullコマンドを利用します。

Repository

適切なコンポーネント適切なバージョンを使用して適切な開発用のワークスペースを構築するためのRepositoryを用意し一元管理します。

以下の図のようにワークスペースには様々なものが含まれます。

image.png

自分が作業しているコードはもちろん、依存しているjarやdll、設定ファイルやデータファイル…
もし、自分のワークスペースを作成、更新するのに多くの手作業が必要ならば、いずれめんどくさくなっていくでしょう。

そのためワークスペースの構築・更新を容易にするため、ワークスペースを構築するためのコンポーネントをリポジトリにまとめて一元管理します。

image.png

これにより、ワークスペースを作成する際はリポジトリから適切なバージョンを取ってくるだけとなります。

Private System Build

Private Workspaceで変更した自分の更新をRepositoryに反映する前に最新のコードベースでも自分の変更を含めてビルドできるかを確認します。
image.png

多くの場合、ビルド後にSmokeTest等を実施します。

ビルド時の検討要素として、クリーンビルドを行うか差分ビルドを行うかが問題になります。
時間が許せばクリーンビルドの方が望ましいでしょう。
また、どのアプリケーションをビルドすべきかという問題もあります。
可能ならば、自分が知っているものすべてが望ましいでしょう。

しかし、これらは完全である必要はありません。作業者全員が無限の時間をかけることはできないのです。こういったものはIntegration Buildでフォローしてもいいでしょう。

Integration Build

多くのワークスペースにおいて各人が行った変更を反映したコードベースが必ずビルドされるかどうかを確認します。

実行のトリガーとしては日次だったり、コミットイベントだったりします。
ビルドの頻度はビルドの所要時間と変更の発生量に基づいて決める必要があります。

これを実施することにより、コミット漏れなどの事象を検知することができます。
ビルド後にSmokeTestやRegressionTestが実施できるかを検討した方がよいでしょう。

Third Party Codeline

サードパーティのソースコードやライブラリのバージョンと自製品のバージョンとの調整を行うため、サードパーティー用のコードラインを用意します。

サードパーティ用のコードラインを自製品に取り込むにはSubersionの場合、 svn:externals (外部参照) を使用し、gitの場合はsubmoduleを使用することになります。

Task Level Commit

作業単位でバージョン管理システムへのコミットします。
これにより、コミットの頻度をあげて、他の開発者との競合を減らすことができます。
また、特定の作業の変更を差し戻すときも、複数の作業を一度にコミットしたときより、作業ごとにコミットしておいた方が楽です。

具体的には障害レポート単位だったりチケット単位だったりします。
これらを発展させた思想がチケット駆動開発になります。

RedmineではSubversion等のコミットログに特別なキーワードをチケット番号を記述してチケットとコミットの紐づけを行えるようにしています。

チケットとSubversion等のリポジトリへのコミットを関連づける
http://redmine.jp/faq/repository/association_ticket_revision/

Codeline Policy

自分のコードをコードラインに反映する際に、どういうテストをすべきか、どのタイミングで反映するかなどのルールをコードラインごとに定めて周知します

SmokeTest

変更を加えた後もシステムが問題なく動くことを最低限度確認するテストを実施します。
あくまで最低限度なので、詳細にわたる結合テストではないことに注意してください。

理想をいえば自動化すべきです。

Unit Test

変更を加えた後のモジュールが要求仕様通り動作することを詳細にテストします。

Regression Test

変更を加えた後の既存コードが悪くなっていないことを確認します。
回帰テストの実行は仮に自動化しても実行に長い時間がかかるのでどのタイミングで行うかは検討する必要があります。

Private Versions

業者個人の小さい変更を他人に影響を与えずに行い、作業履歴として利用できる方法を提供します。

Subversionの場合、これを実現するには個人用のブランチを用意する必要があります。
Gitの場合、分散型バージョン管理という性質上、ローカルの環境で自由に作業履歴を利用できます。

Release Line

リリース済みの保守用のコードラインを用意します。
多くの場合、MainLineからリリース毎にブランチを作成することになります。

ReleaseLineでは、そのリリースに対する障害対策をおこないます。
適度なタイミングで修正したバグをMainLineに反映させる必要があるでしょう。

Release Prep Code Line

リリース前の準備用のコードラインを用意します。このコードラインはリリース後、Release Lineとして使用します。

Task Branch

長期にわたり複数人が変更する必要があるタスクを現在のコードラインに影響を与えないように、そのタスク専用のコードラインを作成します。

Git Flowと構成管理のパターン

gitのブランチモデルとして以下の文章が公開されています。

A successful Git branching model
http://keijinsonyaban.blogspot.com/2010/10/a-successful-git-branching-model.html

このブランチモデルの導入を補助するGit Flowというプラグインが存在します。

GitFlowで実現する各ブランチの例は以下になります。

GitFlow Examples
https://gitversion.readthedocs.io/en/latest/git-branching-strategies/gitflow-examples

今回はGitのブランチモデルが構成管理のどのパターンにあてはまるかみてみます。

master
リリースされたタイミングでタグをつけるmasterは強いていうなら、構成管理モデルのRelease Lineパターンに当てはまります。

develop
構成管理モデルのMainlineパターンにあたります。

Feature branches
構成管理モデルのTask Branchパターンにあたります。

hotfix
本番リリースにたいする故障対応になるのでRelease Lineパターンに当てはまります。
masterを元に作成したTask Branchパターンともいえるかもしれません。

Release branches
リリース準備のために作成して、リリースしたらmasterとdevelopにマージして削除しているので構成管理パターンのRelease-Pre Code lineになります。
ただし、リリース後のリリース保守はmasterに任せているようです。

さて、「A successful Git branching model」を読んで一つ疑問が出ると思います。
たとえば、Ver1.0とVer2.0がリリースされて、Ver1.0に対する障害対応が必要になった場合どうするかという疑問です。※これは構成管理のパターンとしてはRelease Lineパターンになります。

これについてはGitFlowのサンプルにSupport Branchesがあります。

リリース後、サポートが必要なバージョンについてはRelease branchesではなくSupport branchesとしてサポートが必要なあいだブランチを消さずに生存させます。
もし障害対応が必要になったら、Support Branchesをもとにhotfix用のブランチを作成します。

まとめ

今回はパターンによるソフトウェア構成管理に記載されているパターンについて簡単に説明しました。
15年以上前の古い本ではありますが、その考え方自体は現在の構成管理の方法にもつながっていると思います。
もし、ツールのHowToでなく構成管理をどのようにすべきかを考えたい人は一読して損はないでしょう。

なお、こういう本に影響を受けて共有フォルダでの管理を改善しようとしてダメだった場合は以下を参考にしてください。

共有フォルダ教が支配する世界に転生した場合の対応案
https://needtec.sakura.ne.jp/wod07672/?p=9173

参考

パターンによるソフトウェア構成管理 (IT Architects’ Archive―ソフトウェア開発の課題)
https://www.amazon.co.jp/dp/4798112593

A successful Git branching model
http://keijinsonyaban.blogspot.com/2010/10/a-successful-git-branching-model.html

GitFlow Examples
https://gitversion.readthedocs.io/en/latest/git-branching-strategies/gitflow-examples

Following git-flow how should you handle a hotfix of an earlier release?
https://stackoverflow.com/questions/16386323/following-git-flow-how-should-you-handle-a-hotfix-of-an-earlier-release

WinSCPを自動化しても別にかまわんのだろ

まえがき

どこの環境でもだいたいはいっているWinSCPですが、これをコマンドラインベースで自動操作できることを知っている人は、そんなに多くありません。

今回はWinSCPの自動化について話をしようと思います。
主な話題としては以下の通りです。

・CUIでのWinSCPの実行
・スクリプトファイルを利用したWinSCPの実行
・PowerShellからのWinSCPの実行

コマンドラインでの実行方法

CUIでのWinSCPの実行

WinSCPをインストールしたディレクトリの「WinSCP.com」を実行するか「WinSCP.exe」に-consoleオプションを付与して実行します。

"C:\Program Files (x86)\WinSCP\winscp.com"
or
"C:\Program Files (x86)\WinSCP\winscp.exe" /console

以下のようなコンソールが起動して対話ベースでWinSCPの操作が行えます。

image.png

使用できるコマンドは下記の通りです。help \<command>でその詳細を確認できます。
5.15.3で使用できるコマンドは下記の通りです。

コマンド 説明
call 任意のリモート コマンドを実行
cd リモート ディレクトリの変更
checksum リモート ファイルのチェックサムを計算する
chmod リモート ファイルの権限を変更
close セッションを閉じる
cp リモート ファイルを複製
echo その引数をメッセージとして表示します
exit すべてのセッションを閉じプログラムを終了する
get リモートからローカルにファイルをダウンロード
help ヘルプを表示
keepuptodate ローカル ディレクトリの変更をリモートに反映
lcd ローカル ディレクトリを変更
lls ローカル ディレクトリの内容を表示
ln リモートにシンボリックリンクを作成
lpwd ローカル ディレクトリの表示
ls リモート ディレクトリの内容を表示
mkdir リモート ディレクトリを作成
mv リモート ファイルを削除または名前変更
open サーバに接続
option スクリプトオプションの値を設定/表示
put ローカルからリモートにファイルをアップロード
pwd リモート ディレクトリの表示
rm リモート ファイルを削除
rmdir リモート ディレクトリを削除
session 接続しているセッションの表示またはアクティブなセッションの選択
stat リモート ファイルの属性を表示
synchronize リモートとローカルのディレクトリを同期

対話中には制限付きで環境変数が使用できます。

以下に簡単なアップロードとダウンロードのサンプルを示します。

# 指定のサーバーに接続を行う
winscp> open user:password@192.168.80.131

# タイムスタンプでディレクトリを作成
winscp> mkdir %TIMESTAMP%

# 作成されたディレクトリ名を任意の方法で確認

# 作成したディレクトリにファイルをアップロードする
winscp> put C:\dev\winscp\upload ./20190911173052
C:\dev\winscp\upload      |            0 B |    0.0 KB/s | binary |   0%
C:\dev\winscp\upload\test |            0 B |    0.0 KB/s | binary |   0%
C:\...\test\test.txt      |            4 B |    0.0 KB/s | binary | 100%
C:\...\upload\test.txt    |            4 B |    0.2 KB/s | binary | 100%
C:\...\upload\アップロード.txt  |            4 B |    0.5 KB/s | binary | 100%

# ls コマンドでアップロードした内容を確認
winscp> ls ./20190911173052
drwxrwxr-x   3 xxxxxxxx xxxxxxxx        64 Sep 11 17:32:29 2019 .
drwx------  33 xxxxxxxx xxxxxxxx      4096 Sep 11 17:30:51 2019 ..
drwxrwxr-x   2 xxxxxxxx xxxxxxxx        22 Sep 11 17:32:29 2019 test
-rw-rw-r--   1 xxxxxxxx xxxxxxxx         4 Sep 11 17:28:17 2019 test.txt
-rw-rw-r--   1 xxxxxxxx xxxxxxxx         4 Sep 11 17:28:09 2019 アップロード.txt

# getコマンドでダウンロードする
winscp> get ./test2 C:\dev\winscp\download
test2                     |            0 B |    0.0 KB/s | binary |   0%
test.txt                  |            6 B |    0.0 KB/s | binary | 100%
test                      |            0 B |    0.0 KB/s | binary |   0%
aaa.txt                   |            6 B |    0.4 KB/s | binary | 100%

# セッションを全て閉じる
winscp> exit

スクリプトファイルを利用したWinSCPの実行

まずスクリプトファイルをBOM付きのUTF8かUTF16で作成します。

script.txt

# 接続
open %1%:%2%@192.168.80.131
mkdir ./%3%

# アップロード
put C:\dev\winscp\upload ./%3%
exit

スクリプトに記載した%1%や%2%は実行時のパラメータで決定されます。
このスクリプトを実行する例は以下の通りです。

C:\dev\sakura>"C:\Program Files (x86)\WinSCP\winscp.com" /script=C:\dev\winscp\script.txt /parameter ユーザ名 パスワード フォルダ名 /log=C:\dev\winscp\log.txt
サーバを探索中・・・
サーバに接続しています・・・
認証しています・・・
ユーザ名"ユーザ名" を使用中
入力済みパスワードで認証中
認証されました
セッションを開始しています・・・
セッションを開始しました
アクティブ セッション: [1] ユーザ名@192.168.80.131
C:\dev\winscp\upload      |            0 B |    0.0 KB/s | binary |   0%
C:\dev\winscp\upload\test |            0 B |    0.0 KB/s | binary |   0%
C:\...\test\test.txt      |            4 B |    0.0 KB/s | binary | 100%
C:\...\upload\test.txt    |            4 B |    0.0 KB/s | binary | 100%
C:\...\upload\アップロード.txt  |            4 B |    0.5 KB/s | binary | 100%

C:\dev\sakura>

PowerShellからのWinSCPの実行

WinSCPnet.dllが.NETのライブラリとなっておりPowerShellから実行可能です。AnyCPUでビルドしているようなのでPowerShellはx64/x86どちらで動作させても動きます。

ダウンロードとアップロードの例

try
{
    # Load WinSCP .NET assembly
    Add-Type -Path 'C:\Program Files (x86)\WinSCP\WinSCPnet.dll'

    # Setup session options
    $sessionOptions = New-Object WinSCP.SessionOptions -Property @{
        Protocol = [WinSCP.Protocol]::Sftp
        HostName = "192.168.80.131"
        UserName = "xxxxxxxx"
        Password = "xxxxxxxx"
        GiveUpSecurityAndAcceptAnySshHostKey = $True
    }

    $session = New-Object WinSCP.Session

    try
    {
        # Connect
        $session.Open($sessionOptions)

        # Force binary mode transfer
        $transferOptions = New-Object WinSCP.TransferOptions
        $transferOptions.TransferMode = [WinSCP.TransferMode]::Binary

        # Download file to the local directory d:\
        # Note use of absolute path
        $transferResult =
            $session.GetFiles("/home/xxxxxxxx/test2", "C:\dev\winscp\download", $False, $transferOptions)

        # Throw on any error to emulate the default "option batch abort"
        $transferResult.Check()

        # Directoryの作成
        $session.CreateDirectory("/home/xxxxxxxx/test2/powershell")
        $transferResult = 
            $session.PutFiles("C:\dev\winscp\upload", "/home/xxxxxxxx/test2/powershell", $False, $transferOptions)
        $transferResult.Check()
    }
    finally
    {
        # Disconnect, clean up
        $session.Dispose()
    }

    exit 0
}
catch
{
    Write-Host "Error: $($_.Exception.Message)"
    exit 1
}

クラスの使用方法は下記を参照してください。
https://winscp.net/eng/docs/library

まとめ

コマンドラインベースでWinSCPを自動化する方法を紹介しました。

GUIを一生懸命たたいて目視で確認とかいう、苦行を繰り返すくらいならスクリプトを書いた方が楽でミスもないと思います。

なお、単純実行ならともかく、分岐等が必要ならPowerShellでやった方が楽でしょう。

参考

https://winscp.net/eng/docs/scripting
https://winscp.net/eng/docs/library_from_script

TeraTermのマクロをためしてみる

まえがき

どの現場にもあるTeraTermですが、マクロを実行することが可能です。
https://ttssh2.osdn.jp/manual/ja/macro/

TTLファイルにマクロを記述して、TeraTermのインストールディレクトリにあるttpmacro.exeから実行することでマクロが動作します。

実行例

特定のファイルを監視する

tailコマンドで特定のファイルを監視するサンプルです。
基本的に接続したり、コマンドを送信したらプロンプトの文字列の受信を待つことになります。

コマンドの成否は「echo $?」を実行してエラーコードを読むことになると思いますが、waitだと「1」と「130」の区別がつかなかったのでwaitregexで行頭と単語の区切りを明確にしてチェックすることにしています。

user = "username"

prompt = "["
strconcat prompt user
strconcat prompt "@centos7"

constr = '192.168.80.131:22 /ssh /auth=password /passwd=pass /user="
strconcat constr user
connect constr

; プロンプトが表示されるまで待機
wait prompt

logopen "C:\dev\teraterm\test.log" 0 1
logwrite 'Log start'#13#10

; コマンド実行
; https://ttssh2.osdn.jp/manual/ja/macro/command/index.html
sendln 'tail -f /home/username/test/test.txt'

; プロンプトが表示されるまで待機
wait  prompt

; 直前に終了された終了ステータス
; https://www.gnu.org/software/bash/manual/html_node/Special-Parameters.html#Special-Parameters
sendln "echo $?"

; waitだと1に必ず引っかかってしまう
; https://ttssh2.osdn.jp/manual/ja/reference/RE.txt
waitregex  "^130\b" "^1\b"
if result = 2 then
    messagebox "not found" "error"
endif

logclose

; 確認せずに閉じる
disconnect 0

ファイルの送受信

scpsendでファイルの送信、scprecvでファイルの受信を行います。
あくまで単一ファイルの送受信なのでフォルダを選択しても無駄です。

user = "username"

prompt = "["
strconcat prompt user
strconcat prompt "@centos7"

constr = '192.168.80.131:22 /ssh /auth=password /passwd=pass /user="
strconcat constr user
connect constr

; プロンプトが表示されるまで待機
wait prompt

; ファイル送信完了を確認する
SOURFILE = 'C:\dev\teraterm\send\test.txt'
DESTFILE = '~/test/test.txt'

sendln "rm ~/test/test.txt"
;; ファイル送信
scpsend SOURFILE DESTFILE
;; ファイル送信プロセス確認
do
  mpause 5000
  sprintf2 str 'ps -ef |grep -v grep |grep -c scp'
  sendln str
  waitln '0' '1'
loop while result != 1
;; ファイル送信が完了すると次のマクロを実行
sendln 'echo SCP finish'

scprecv '/home/username/squash-tm.war.back' "C:\dev\teraterm\rcv\tmp"

;; マクロ終了
end

受信時は以下のダイアログが表示される。
image.png

あとがき

単純なコマンド実行するだけならともかく、エラー処理等が面倒です。
やるなら、シェルスクリプトを転送して、難しい処理はそこで実行して、結果を受信するとかの方が楽そうです。
また、大量のファイルを送受信する場合はWinSCPの方が簡単そうです。

WinMergeのコマンドラインオプション

前書き

SIerをディスる記事が登場する度、流れ弾が飛んでくるWinMergeですが、実際のところ、その機能を十分つかいこなせている現場は少ないです。

たとえば、WinMergeはコマンドラインから実行できます。これにより外部プログラムの実行が容易になっているのですが、これはあまり使いこなせていません。
https://manual.winmerge.org/Command_line.html

実はドキュメント化されていないパラメータがいくつかあります。
たとえば、「-or」というレポートを出力するパラメータについては実装はされていますが、ドキュメント化されてはいません。
https://github.com/WinMerge/winmerge/blob/30a5674030a8318a9239893a3cc0a4e8798a6256/Src/MergeCmdLineInfo.cpp

コマンドラインのパラメータ

2.16.4.0でサポートしているパラメータは下記のようになります。
これは下記を参考に作成しています。
・ヘルプ
https://github.com/WinMerge/winmerge/blob/30a5674030a8318a9239893a3cc0a4e8798a6256/Src/MergeCmdLineInfo.cpp
https://sourceforge.net/p/winmerge/support-requests/109/

オプション 説明
-? WinMergeヘルプを開きます
-o "outputfilename" マージした結果のファイルを保存するオプションの出力フォルダを指定します。 もし、出力パスを指定した場合、あるペインを変更後保存すると、変更は出力パスのファイルに保存され、 元ファイルは前の状態のままになります。
-or "reportfilename" レポートの出力を出力するパスを指定します。この機能はドキュメントにのっていません。
dl "desc" 左側タイトルバーの説明を指定します。 デフォルトのフォルダやファイル名テキストに上書きされます。例: /dl "Version 1.0" や /dl WorkingCopy. スペースを含む説明はダブルクォーテーションマークで括ってください。
-dm "desc" /dlと同様に中央タイトルバーの説明を指定します。
-dr "desc" /dlと同様に右側タイトルバーの説明を指定します。
-e EscキーでWinMergeが閉じるようにします。 WinMergeを外部比較アプリケーションとして使用する場合に便利です。 (ダイアログのようにすばやくWinMergeを閉じることができます) この引数を指定しなかった場合、すべてのウインドウを閉じるのに何回もEscキーを 押さなければならないことになるかもしれません。(2つ以上タブが開かれている場合、一回のESCキーの押下でWinMergeが終了してしまうのを期待している人はいないような気がしたので、日本語版ではこのオプションを指定しても2つ以上タブが開かれている場合は1つのタブを閉じるだけにし、タブが1つの時またはタブが一つもない時にWinMergeを終了するようにしました)
-f "mask" 比較を制限するために、指定したフィルタを適用します。 フィルタは.h .cppのようなファイルマスクか、 XML/HTML Develのようなファイルフィルタの名前です。 スペースを含むフィルタマスクやフィルタ名はダブルクォーテーションマークで括ってください
-r すべてのサブフォルダ内のすべてのファイルを比較します(再帰比較)。 ユニークフォルダ (片方のみ存在するフォルダ)は、分離された項目として比較結果内にリストされます。 サブフォルダまで含めるとかなり比較時間が増大してしまうことに注意してください。 このパラメータを指定しなかった場合、WinMergeは比較するフォルダ内のファイルとトップレベルのサブフォルダのみリストします。 サブフォルダの中までは比較しません。
-s WinMergeウインドウを1つのインスタンスに制限します。 例えば、WinMergeが既に実行中ならば、新しい比較は同じインスタンス内で実行されます。 この引数を指定しなかった場合、複数のウインドウが開かれる可能性があります: 設定によっては、新しい比較が既に存在するウインドウで実行されることも新しいウインドウで 実行されることもあります。
-noninteractive 比較やレポート出力後にWinMergeを終了します。この機能はドキュメントにのっていません。
-noprefs レジストリの読み書きをしません。設定値はデフォルトを使用します。この機能はドキュメントにのっていません。
-minimize minimize 最小化状態でWinMergeを開始します。 このオプションは長時間かかる比較を行う場合に便利です。
-maximize 最大化状態でWinMergeを開始します。
-prediffer predifferプラグイン 指定されている場合はpredifferプラグインを取得します(それ以外の場合、predifferプラグインは空白になります。これはデフォルトです)。この機能はドキュメントにのっていません。
-wl 読み取り専用として左側を開きます。 比較時、左側を変更したくない場合に使用してください。
-wm 読み取り専用として中央を開きます。 比較時、中央を変更したくない場合に使用してください。
-wr 読み取り専用として右側を開きます。 比較時、右側を変更したくない場合に使用してください
-ul 左側パスが最近使用した項目(MRU)リストに追加されるのを防ぎます。 外部アプリケーションは、ファイルまたはフォルダの選択ダイアログのMRUリストにパスを 追加するべきではありません
-um 中央のパスが最近使用した項目(MRU)リストに追加されるのを防ぎます。 外部アプリケーションは、ファイルまたはフォルダの選択ダイアログのMRUリストにパスを 追加するべきではありません。
-ur 右側パスが最近使用した項目(MRU)リストに追加されるのを防ぎます。 外部アプリケーションは、ファイルまたはフォルダの選択ダイアログのMRUリストにパスを 追加するべきではありません。
-u または -ub(非推奨) /u (または/ub) 各々(左、右、中央)のパスが最近使用した項目(MRU)リストに追加されるのを防ぎます。 外部アプリケーションは、ファイルまたはフォルダの選択ダイアログのMRUリストにパスを 追加するべきではありません。
-fl 起動時、左側にフォーカスを当てます
-fm 起動時、中央にフォーカスを当てます
-fr 起動時、右側にフォーカスを当てます
-al 起動時、左側で自動マージします。
-am 起動時、中央で自動マージします。
-ar 起動時、右側で自動マージします。
-x 同一ファイルの比較をしたときにWinMergeを閉じます。 (情報ダイアログを表示した後) このパラメータは比較後に効果がなくなります。 例えば、もしファイルがマージか編集の結果として同一となった場合です。 このパラメータは、WinMergeを外部アプリケーションとして使用したり、 差異のないファイルを無視することによって余分なステップを取り除きたい場合に便利です。
-xq -x に似ていますが、同一ファイルであってもメッセージボックスを表示しません
-cp コードページ 比較時のコードページを指定します。この機能はドキュメントにのっていません。
-ignorews 空白を無視します。この機能はドキュメントにのっていません。
-ignoreblanklines 空白行を無視します。この機能はドキュメントにのっていません。
-ignorecase 大文字小文字を無視します。この機能はドキュメントにのっていません。
-ignoreeol 改行の無視します。この機能はドキュメントにのっていません。
-ignorecodepage コードページの違いを無視します。この機能はドキュメントにのっていません。
-cfg 設定情報または-config 設定情報 設定情報をコマンドラインから指定します。この機能はドキュメントにのっていません。詳細は[設定情報をコマンドラインで渡す])#設定情報をコマンドラインで渡す)を参照してください

設定情報をコマンドラインで渡す

以下のような記載で設定情報をコマンドラインから渡すことが可能です。

"C:\Program Files\WinMerge\WinMergeU.exe" C:\dev\winmerge\a.txt C:\dev\winmerge\b.txt -cfg "Font/Height=32" -cfg "Font/FaceName=MS 明朝" -cfg "Font/Underline=1"

image.png

-cfgで指定できる設定項目の種類は設定値からエクスポートしたものと同じになります。

・編集→設定でひらくオプション画面で「エクスポート」を行う
image.png

・作成されるINIファイルは下記の通り
image.png

ファイルの比較結果をレポートとして出力する例

/orを使用してレポートにファイルを出力します。
この際、同時に/noninteractiveを指定してレポート出力後にWinMergeを終了します。

"C:\Program Files\WinMerge\WinMergeU.exe" C:\dev\winmerge\test1.txt C:\dev\winmerge\test2.txt  /minimize /noninteractive /u /or C:\dev\winmerge\out.html

作成されたレポート
image.png

フォルダの比較結果をレポートとして出力する例

-orを使用してレポートにファイルを出力します。
-noninteractiveを指定してレポート出力後にWinMergeを終了します。
-rを使用してサブフォルダを含めて比較を行います。
-cfgを指定して下記の設定を適用します。
 Settings/DirViewExpandSubdirs=1 …オプションの比較>フォルダー>自動的にサブフォルダーを展開する
 ReportFiles/ReportType=2 …フォルダ比較レポート>スタイル:シンプルなHTML形式
 ReportFiles/IncludeFileCmpReport=1 …フォルダ比較レポート>ファイル比較レポートを含める

"C:\Program Files\WinMerge\WinMergeU.exe" C:\dev\winmerge\abc C:\dev\winmerge\acc -minimize -noninteractive -noprefs -cfg Settings/DirViewExpandSubdirs=1 -cfg ReportFiles/ReportType=2 -cfg ReportFiles/IncludeFileCmpReport=1 -r -u -or C:\dev\winmerge\out2.html

作成されたレポート(ディレクトリの比較結果)
image.png

※注意:2.14だと-minimizeがついているとレポートが常に差分なしになってしまう。

コマンドラインからマージする例

"C:\Program Files\WinMerge\WinMergeU.exe" base.c remote.c local.c /ar /u /o C:\dev\winmerge\out.c /noninteractive

残念ながら、以下で止まるのでコマンドラインだけの操作で自動化はできません。
またコンフリクトが発生した場合も、マージできません。

image.png

どうしても自動化したかったら以下のPowerShellを使います。

automerge.ps1

Add-Type -AssemblyName UIAutomationClient
Add-Type -AssemblyName UIAutomationTypes
Add-type -AssemblyName System.Windows.Forms

$source = @"
using System;
using System.Windows.Automation;
public class AutomationHelper
{
    public static AutomationElement RootElement
    {
        get
        {
            return AutomationElement.RootElement;
        }
    }

    public static AutomationElement GetWindowByTitle(string title) {
        var rootCond = new PropertyCondition(AutomationElement.ClassNameProperty, "WinMergeWindowClassW");
        var cond = new PropertyCondition(AutomationElement.NameProperty, title);
        var elementCollection = RootElement.FindAll(TreeScope.Children, rootCond);
        foreach(AutomationElement mainForm in elementCollection) {
            var win =  mainForm.FindFirst(TreeScope.Children, cond);
            if (win != null) {
                return win;
            }
        }
        return null;
    }

    public static AutomationElement WaitWindowByTitle(string title, int timeout = 10) {
        DateTime start = DateTime.Now;
        while (true) {
            AutomationElement ret = GetWindowByTitle(title);
            if (ret != null) {
                return ret;
            }
            TimeSpan ts = DateTime.Now - start;
            if (ts.TotalSeconds > timeout) {
               return null;
            }
            System.Threading.Thread.Sleep(100);
        }
    }

}
"@
Add-Type -TypeDefinition $source -ReferencedAssemblies("UIAutomationClient", "UIAutomationTypes")

# 5.0以降ならusingで記載した方が楽。
$autoElem = [System.Windows.Automation.AutomationElement]

# ウィンドウ以下で指定の条件に当てはまるコントロールを1つ取得
function findFirstElement($form, $condProp, $condValue) {
    $cond = New-Object -TypeName System.Windows.Automation.PropertyCondition($condProp, $condValue)
    return $form.FindFirst([System.Windows.Automation.TreeScope]::Element -bor [System.Windows.Automation.TreeScope]::Descendants, $cond)
}

# 指定のAutomationIDのボタンを押下
function pushButtonById($form, $id) {
    $buttonElem = findFirstElement $form $autoElem::AutomationIdProperty $id
    $invElm = $buttonElem.GetCurrentPattern([System.Windows.Automation.InvokePattern]::Pattern) -as [System.Windows.Automation.InvokePattern]
    $invElm.Invoke()
}

# 5秒間ダイアログがでるのを待機しているが、あまりよくない。
$dialog = [AutomationHelper]::WaitWindowByTitle("変更されたファイルを保存しますか?", 5)
if ($dialog -eq $null) {
    Write-Host "マージできませんでした"
} else {
    pushButtonById $dialog "1"
    Write-Host "マージでしました"
}

このPowerShellを以下のような感じで実行します。

start cmd /c "C:\Program Files\WinMerge\WinMergeU.exe" base.c remote.c local.c /ar /u /o C:\dev\winmerge\out.c /noninteractive
powershell -ExecutionPolicy RemoteSigned -File  ./automerge.ps1

タイトルバーの説明 /dl /dm /dr

/dl /dm /drはタイトルバーに説明を追加します。

WinMergeU.exe C:\dev\winmerge\test1.txt C:\dev\winmerge\test2.txt C:\dev\winmerge\test3.txt /dl "Version 1.0" /dm "Version 1.1" /dr "Version 1.2"

image.png

ファイルフィルタの設定 /f

指定したフィルタを設定して比較対象のファイルを制限します。
スペースで区切ることで複数のファイルを指定することが可能です。

以下の例ではcppとhのみをフィルタした結果となります。

WinMergeU.exe C:\dev\winmerge\abc C:\dev\winmerge\acc -f "*.cpp *.h"

image.png
※他の拡張子が表示されない

末尾の空白の無視 /ignorews

末尾の半角スペースとタブを無視します。

空白を無視しない場合:
image.png

空白を無視する場合:

"C:\Program Files\WinMerge\WinMergeU.exe" C:\dev\winmerge\a.txt C:\dev\winmerge\b.txt  /ignorews

image.png

空白行の無視 /ignoreblanklines

ignoreblanklinesオプションを使用することで空白行の差異を無視することができます。

空白行を無視しない
image.png

空白行を無視する

"C:\Program Files\WinMerge\WinMergeU.exe" C:\dev\winmerge\a.txt C:\dev\winmerge\b.txt  /ignoreblanklines

image.png

大文字小文字を無視 /ignorecase

ignorecaseオプションを利用することで大文字小文字の差異を無視することが可能です。

無視しない場合
image.png

無視する場合

"C:\Program Files\WinMerge\WinMergeU.exe" C:\dev\winmerge\c.txt C:\dev\winmerge\d.txt   /ignorecase

image.png

改行文字の無視 /ignoreeol

行末の改行文字の違いを無視します。

無視しない場合
image.png

無視する場合

"C:\Program Files\WinMerge\WinMergeU.exe" C:\dev\winmerge\a.txt C:\dev\winmerge\b.txt  /ignoreeol

image.png

コードページの無視 /ignorecodepage

/ignorecodepageでコードページの差異を無視できます。
ただし、2.16.4.0で実験した限り、このパラメータを無効にしてかつ、下記の設定であっても、コードページの違いを無視して比較しているようです。

image.png

image.png
※コードページの違いを無視する設定で、文字コードのことなるファイルを比較しても差異がでてこない。

まとめ

コマンドラインを利用することで自動でレポートの作成が行えます。
ただし、パッチの作成や、マージ結果の作成は自動でできないようです。

UIPathでPowerShellを実行する方法

はじめに

UiPathでPowerShellを起動することを考えてみます。

アクティビティの一覧を見ていると2つほど案が思いつくと思います。

1つ目はStartProcessアクティビティでプロセスを開始する方法
2つ目はInvokePowerShellアクティビティで実行する方法です。

今回は以下のようなPowerShellを実行する方法を考えてみます。

list.ps1

Param(
    [String]$Path
)
$ErrorActionPreference="Stop"
if ($Path -eq "") {
    Set-Content C:\share\test\result.txt -Value "error "
    throw "Error"
}
try {
    $ret = Get-ChildItem -Path $Path -Filter *.txt
    Set-Content C:\share\test\result.txt -Value $ret
    return $ret
} catch {
    Set-Content C:\share\test\result.txt -Value $_
    throw "Error"
}

動作環境

・UiPath Community Edition 2019.6.0
・C#プロジェクト
・Windows 10
・PowerShell5.1

StartProcessアクティビティ

単純な例では以下のようなフローになります。
image.png

プロセスを開始

プロパティ名 設定値
ファイル名 @"C:\Windows\System32\WindowsPowerShell\v1.0\powershell"
引数 @"-ExecutionPolicy RemoteSigned -File C:\share\test\list.ps1 C:\share\"

これでPowerShellが起動してスクリプトが実行されます。
しかし、StartProcessアクティビティは非同期な処理でプロセスの終了を待ちません。
このことは、PowerShellスクリプト実行後に後続の処理がある場合に問題になります。
※list.ps1の例だと作成されたresult.txtを利用してなにかをする場合

この問題の回避策は下記で議論されています。

Make Start Process wait until the end of the command in cmd
https://forum.uipath.com/t/make-start-process-wait-until-the-end-of-the-command-in-cmd/15551

ここで挙げられた回避策は以下の3つになります。

  • 案1:画面要素を監視してスクリプトの終了を検知する
  • 案2:もしファイルを出力するスクリプトであるなら、そのファイルの更新を検知する
  • 案3:PowerShellのWait-Process を使用してプロセスの終了を検知する。

案1はOnUiElementVanishアクティビティを使用することになりますが、非同期で動作しているためこのアクティビティに到達した時点で、すでに画面が終了している可能性もありますし、逆にこのアクティビティを通過してしまった後に画面が表示される可能性があります。

案2はMonitorEventsアクティビティ内にFileChangeTriggerアクティビティを配置してファイルの最終更新日やサイズを監視して変化があったら終了とみなす方法ですが、これもイベント監視の前にファイルの操作が終わる可能性があります。

案3はStartProcessアクティビティで対応する方法でなく、InvokePowerShellアクティビティで対応する方法になります。

つまり結論を述べると、StartProcessアクティビティでPowerShellを起動して終了を待つのは適していません。

InvokePowerShellアクティビティ

TypeArgumentプロパティについての注意

InvokePowerShellアクティビティを使用する場合、TypeArgumentプロパティには注意をする必要があります。

これはスクリプト内で返却されたオブジェクトの型にキャストできるものでなければなりません。
たとえば、「コマンドテキスト」プロパティに以下のコマンドレットを記述したとします。

Get-ChildItem

FileInfoDirectoryInfoのコレクションが返却されます。TypeArgumentプロパティにはコレクション内の型がキャスト可能な値、すなわちベースクラスであるObjectやFileSystemInfoを指定する必要があります。

もしこのことを守らないと以下のようなエラーが発生します。
image.png

次に、以下のようなコマンドレットを記述します。

Get-Process
Get-ChildItem

またスクリプトの入力にはチェックを付けてください。

image.png

これを実行すると、System.Diagnostics.Processのアイテムが続いたあとに、FileInfoDirectoryInfoのアイテムが続くコレクションが返されます。

つまり、スクリプト内で出力されたオブジェクトは全て受け取れることを意味しており、また、そのオブジェクトを受け入れられる型をTypeArgumentに指定する必要があります。

PowerShellのスクリプトファイルパスを指定

以下にlist.ps1のPowerShellのスクリプトを実行してその結果をデバッグ出力するサンプルを記載します。

image.png

PowerShellを呼び出し

プロパティ名 設定値
TypeArgument System.IO.FileSystemInfo
スクリプト入力 チェックON
コマンドテキスト @"C:\share\test\list.ps1 C:\share\"
パラメータ なし
出力 result※ctrl+kで変数を作成

繰り返し(コレクションの各要素)

プロパティ名 設定値
TypeArgument System.IO.FileSystemInfo
コレクション値 result

image.png

1行を書き込み

プロパティ名 設定値
テキスト item.FullName

InvokePowerShellアクティビティの「コマンドテキスト」プロパティにはファイル名を与えることができます。
この場合、「パラメータ」プロパティによるスクリプトによるパラメータの設定はできないようなので、「コマンドテキスト」プロパティのスクリプト名に続けてスクリプトに与えたいパラメータを記載します。

PowerShellのスクリプトの内容を指定

スクリプトの内容をあらかじめ読み込んでおいて、実行することも可能です。
この場合、「パラメータ」プロパティを利用してPowerShellのスクリプトのパラメータを指定することができます。
PowerShellのスクリプトファイルパスを指定する場合と異なり、.NETの型をそのまま渡せるメリットがあります。

image.png

テキストファイルを読み込む

プロパティ名 設定値
ファイル名 @"C:\share\test\list.ps1"
出力 script※ctrl+kで作成

PowerShellを呼び出し

プロパティ名 設定値
TypeArgument System.IO.FileSystemInfo
スクリプト入力 チェックON
コマンドテキスト script
パラメータ 名前:Path 型:String 値:@"C:\share"
出力 result※ctrl+kで変数を作成

繰り返し(コレクションの各要素)
PowerShellのスクリプトファイルパスを指定する場合と同じ

まとめ

UiPathでPowerShellを起動する場合はInvokePowerShellアクティビティを使用します。
ファイルパスを直接与えるか、一旦、スクリプトファイルを読み込んでから渡すかは、パラメータを文字で渡すかオブジェクトで渡すかで検討した方がいいでしょう。