某PCショップ店員の覚書

勤務中に作成したプログラムやスクリプトのまとめ

Powershellの「Write-Output」と「Write-Host」、結局どっちがいいの問題

まさかの2連更新です。

「Write-Output」と「Write-Host」の違い

Write-OutputとWrite-Hostの違いについては、別の方が詳しく説明されているので端折りますが、結局のところ使い分けが大事です。

・パイプライン処理をさせたり、結果を変数へ保存したい場合、Write-Output
・コンソール画面に文字を出したいだけなら、Write-Host

これに尽きます。
Write-Outputは本来、パイプライン処理を行う際に使うのでコンソール画面への表示は行いません。(ただし、特別な仕組みによって実際にはコンソール画面へ表示される)
対してWrite-Hostの場合、コンソール画面への出力を主目的としているので文字に色を付けて(-ForegroundColor)表示させたり、文字の背景色を指定する(-BackgroundColor)こともできます。
注意点として、あまりありませんが「Write-Hostの結果を変数に保存する」ことはできません。

ex)

Wirte-Host の場合、結果を変数へ保存することはできない


Write-Output の場合、結果を変数へ保存できる


なので、コンソール画面へ文字を出力したいだけであれば「Write-Host」を、結果を保存して後々利用する場合は「Write-Output」を利用しましょう。

.ps1ファイルを管理者権限で実行する

前回の記事で
「次回以降はオススメBTOパソコンを紹介する」
と言いましたが、先に書きたいことができましたので次回以降で!

.ps1ファイルを管理者権限で実行したい

.ps1ファイルを管理者権限で実行するには、大きく分けて2パターンあります。

  1. batファイルから管理者権限へ昇格させて.ps1を実行する
  2. スクリプト内に管理者権限へ昇格させるコードを記述する

1.の場合はbatファイルを別に用意しなければならないので、配布するファイル数が多くなってしまいます。
配布ファイルサイズを削減したい方にはあまり向きません。更に2.の方法と比べるとあまりメリットもありません。
なので今回は2.の方法。

if (!([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole("Administrators")){
    Start-Process powershell.exe "-File `"$PSCommandPath`"" -Verb RunAs
    exit
}

解説します。

if (!([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole("Administrators")){

[Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()」は、PowerShellを実行している権限を取得しています。
さらに後ろに「.IsInRole("Administrators")」を付けることにより、ユーザーの所属しているグループを判定しています。ちなみに、含まれている場合は「True」が返ります。
簡単に説明すると、「今.ps1を実行している権限が「Administratorsグループ」でなければ という意味です。

    Start-Process powershell.exe "-File `"$PSCommandPath`"" -Verb RunAs
    exit
}

Start-Process powershell.exe」でPowershellを新しいプロセスとして起動しています。
その後ろについているオプション「"-File `"$PSCommandPath`""」は、起動しているファイル自身を指定するコマンドです。
-Verb RunAs」は、管理者として という意味になります。
つまり、「自分自身を新しいプロセスで、管理者として実行する」という意味になり、最初のものと合わせると
「.ps1を実行したユーザーの権限が管理者権限でなければ、自分自身を管理者権限へ昇格させて新しいプロセスとして実行する」という意味になります。

たった数行で.batを用意することもなく、簡単に管理者権限へ昇格させてから実行できるようになるので、作ったスクリプトファイルの先頭へ記述することをオススメします。

CrashHandlerを強制終了させる

皆さんお久しぶりです。
最近ネタがなく、更新できませんでした。

CrashHandlerが気持ち悪い

ウマ娘プレイ時やGoogle Driveを利用している際、まれに「CrashHandler」というプロセスが起動していることがあります。
そのまま放置していても害があるわけではありませんが、私は見知らぬプロセスが起動していると気持ち悪く感じます。
そこで今回は、この「CrashHandler」と名の付くプロセスを終了させるPowerShellプログラムを作ります。

ソースコード

# ポリシーの変更
Set-ExecutionPolicy RemoteSigned -Scope Process -Force

# プロセス一覧を取得し、Foreachで処理させる
Get-Process | ForEach-Object {
    # 上記で取得したプロセス名に拡張子がついていないので、拡張子をつける
    $process_name = $_.ProcessName + ".exe"
    # もし $process_nameに"crash"と一致する文字列がある場合
    if($process_name -like "*Crash*"){
        # $process_name(プロセス名)を強制終了させる
        taskkill /f /im $process_name
    }
}

たったこれだけです。
やってる事も簡単ですし、私にしては珍しくコメントを記載しました。
これで、ウマ娘やGoogleDriveだけでなく、他のプログラムで発生した「CrashHandler」も強制終了させてくれます。
注意点としては、$process_name内に「crash」という文字列が含まれている場合、それを強制終了させるので
「CrashHandler」でない「crash」と名のつくプロセスまで終了させてしまいます。(例えばクラッシュバンディクーとか。PC版あるのか、はたまたプロセス名に「crash」が含まれるのかわかりませんが)
その場合はソースコードを下記のように変更すれば問題ありません。

# ポリシーの変更
Set-ExecutionPolicy RemoteSigned -Scope Process -Force

# プロセス一覧を取得し、Foreachで処理させる
Get-Process | ForEach-Object {
    # 上記で取得したプロセス名に拡張子がついていないので、拡張子をつける
    $process_name = $_.ProcessName + ".exe"
    # もし $process_nameに"crash"と一致する文字列がある場合
    if($process_name -like "*Crash*"){
        # 終了させたくないプロセスを下記で指定
        if($process_name -like "任意のプロセス名"){
            continue
        }
        # $process_name(プロセス名)を強制終了させる
        taskkill /f /im $process_name
    }
}

これで終了させたくないプロセスは終了されません。
また、このプログラムを常駐し、1分毎にプロセスを取得、「CrashHandler」があれば終了させるようにしたい場合は

# ポリシーの変更
Set-ExecutionPolicy RemoteSigned -Scope Process -Force

while(1){
    Start-Sleep -s 60
    # プロセス一覧を取得し、Foreachで処理させる
    Get-Process | ForEach-Object {
        # 上記で取得したプロセス名に拡張子がついていないので、拡張子をつける
        $process_name = $_.ProcessName + ".exe"
        # もし $process_nameに"crash"と一致する文字列がある場合
        if($process_name -like "*Crash*"){
            if($process_name -like "任意のプロセス名"){
                continue
            }
            # $process_name(プロセス名)を強制終了させる
            taskkill /f /im $process_name
        }
    }
}

で問題ありません。

今回の更新は短いですが、以上となります。
次回以降はオススメなBTOパソコンの構成など書きたいと思います。

Powershellで作成されたプログラムを動かそうとすると怒られる

Powershell Policyの状態を確認しよう

皆さんはPowershellを利用していますか?
Windowsに標準で備わっている機能であり、構文も難しくなくリファレンスも豊富で大変便利な代物です。
しかし、Powershellを勉強しようとした方が最初に躓くのがポリシー関連。
Powershell ISEでスクリプトを実行しようとしてもエラー、ps1を直接叩いてもエラー。

それもそのはず、Powershellは外部プログラムを呼び出して動作させたり、引数に暗号化した悪意たっぷりのコマンドを投げ、別のps1スクリプトで復号->実行
なんてこともできてしまいます。
そうならないために初期状態のPowershellはポリシーがガッチガチです。
試しにPowershellで下記のコマンドを実行してみてください。

Get-ExecutionPolicy -List

Powershellを初めて利用するPCではほとんどがRestrictedになってるかと思われます。
これ、個別のコマンド(Powershellのプロンプト上から1個1個コマンドを入力すること)は許可しますが、ps1などのスクリプトは許可しない設定のことです。
そしてRestricted以外にも複数の状態があります。

Policyの状態

状態 説明
Restricted
スクリプトは全て実行不可
AllSigned
署名付きスクリプトのみ実行可能
RemoteSigned
ローカルのスクリプトと署名付きのリモートスクリプトのみ実行可能
Bypass
何もブロックせず、警告もプロンプトも表示しないで実行
Unrestricted
全て実行可能
Undefined
未定義(Restrictedのポリシーが適応される)

つまり、初期状態だとスクリプトの実行は全て弾かれるんです。

ポリシーを変更しよう

では、これからPowershellでプログラミングを勉強する方のためにもポリシーを変更しましょう。
下記のコマンドを実行予定のスクリプトかプロンプトに入力してください。

Set-ExecutionPolicy RemoteSigned -Scope Process -Force

これでスクリプトの実行が許可されるようになりました。
でもなんだか見慣れない引数がありますね。
ご安心ください。説明します。

最初に

Get-ExecutionPolicy -List

を実行してもらったときに表のような形で出力されたかと思います。

Scope ExecutionPolicy
MachinePolicy
UnDefined
UserPolicy
Undefined
Process
Undefined
CurrentUser
AllSigned
LocalMachine
RemoteSigned

それぞれの意味も説明します。

Scope(有効範囲)の説明

MachinePolicy
グループポリシーでそのコンピュータの全ユーザーに強制
UserPolicy
現在のユーザーに強制
Process
現在のセッションに対して適応
CurrentUser
現在のユーザーに対して適応
LocalMachine
コンピュータの全ユーザーに対して適応

つまり、先ほどのコマンドは
ローカルでのスクリプトの実行を許可する 有効範囲はスクリプトが終了するまで
という意味になります。
でもこれ、毎回スクリプトに記載するかプロンプトに入力しなければなりません。
正直面倒くさい。
ってことでPolicyのScopeを変更しましょう。

Set-ExecutionPolicy RemoteSigned -Scope CurrentUser -Force

これで毎回入力する手間が省けます。
ただし、怪しいスクリプトをDLして実行してしまう危険性もありますのでご注意を。

最後に注意とスコープのオススメ設定

なお、Scopeパラメータを指定しないでポリシー変更コマンドを実行するとLocalMachineの実行ポリシーを変更することになり、管理者として実行していない場合はエラーになります。
また、グループポリシーによって優先度の高いMachinePolicyやUserPolicyスコープがRestrictedに設定されている場合は管理者権限で頑張ってもスクリプトを実行することができません。※batやvbsからスクリプトブロックとして実行することは可能

私個人の考えですが、どれだけ対策していてもウイルスに感染する可能性を0%にすることは不可能なので、少しでもリスクを避けるのであればセキュリティ重視の

Set-ExecutionPolicy RemoteSigned -Scope Process -Force

にすることをオススメします。

PowershellでSeleniumを使う

PowershellSeleniumを使いたい人、たんさんいらっしゃるかと思われますが、現在検索しても古い情報しか載っていないので私が躓いた点も交えながら詳細を書きます。
Chromeのお話なのでEdgeやIEFireFoxOperaメインの方はごめんなさい。

記事作成段階の各ソフトウェアバージョンは下記の通りです。

Powershell - 5.1.19041.1320
Google Chrome - 97.0.4692.99(Official Build) (64 ビット)



・前提 - ダウンロード

PowershellSeleniumを使う際は必ず以下の3点が必要です。

①WebDriver
www.selenium.dev

②WebDriverSupport.dll
www.nuget.org

※上記2つは拡張子がnupkgになっているので、nupkg解凍に対応したソフトウェアを使うか
拡張子を.zipに変えて試してみてください。

③chromedriver.exe
chromedriver.chromium.org





それぞれダウンロード先を埋め込みましたのでダウンロードしてください。

WebDriver,WebDriverSupportは解凍したフォルダ内の「lib\net**\」に.dllがあります。



・前提 - 環境構築

上記3点をダウンロードしてきたら、使いやすいように1か所にまとめましょう。

私は「Automation」というフォルダを作りました。

階層は下記の通りです。

AUTOMATION
│  automation.ps1
│  
└─Data
    │  chromedriver.exe
    │  
    └─Selenium
        │  .signature.p7s
        │  Selenium.WebDriver.nuspec
        │  [Content_Types].xml
        │  
        ├─images
        │      icon.png
        │      
        ├─lib
        │  ├─net45
        │  │      WebDriver.dll
        │  │      
        │  ├─net46
        │  │      WebDriver.dll
        │  │      
        │  ├─net47
        │  │      WebDriver.dll
        │  │      
        │  ├─net48
        │  │      WebDriver.dll
        │  │      WebDriver.Support.dll
        │  │      
        │  ├─net5.0
        │  │      WebDriver.dll
        │  │      
        │  ├─netstandard2.0
        │  │      WebDriver.dll
        │  │      
        │  └─netstandard2.1
        │          WebDriver.dll
        │          
        ├─package
        │  └─services
        │      └─metadata
        │          └─core-properties
        │                  8e4661a2052a4ed982f115e944130cde.psmdcp
        │                  
        └─_rels
                .rels

今回使用するのはnet48なので、その中に「WebDriverSupport.dll」を入れてあります。

chromedriver.exeに関しては「Data」フォルダ直下に入れてます。

ここまで準備が整ったらあとはコーディングしていくだけです。



・コーディング

とりあえず私の書いたコードを載せます。



$dllPath = $null

if($host.Name -match "ise"){
    #ise上からでは$PSScriptRootが使えないため
    $dllPath = Split-Path $psISE.CurrentFile.FullPath -Parent
} else {
    #ise以外で呼び出された際の処理
    if($PSVersionTable.PSVersion.Major -ge 3){
        #power shellのversionが3以上なら
        $dllPath = $PSScriptRoot
    } else {
        #3未満なら
        $dllPath = Split-Path $MyInvocation.MyCommand.Path -Parent
    }
}
$WebDriverPath ="$dllPath\Data\Selenium\lib\net48\WebDriver.dll"
$WebDriverSupportPath = "$dllPath\Data\Selenium\lib\net48\WebDriver.Support.dll"
$ChromeDriverPath = "$dllPath\Data\"
Add-Type -Path $WebDriverPath
Add-Type -Path $WebDriverSupportPath

$driver = New-Object OpenQA.Selenium.Chrome.ChromeOptions
$chromeDriver = New-Object OpenQA.Selenium.Chrome.ChromeDriver($ChromeDriverPath,$driver)
$chromeDriver.Url = "https://www.google.co.jp"
$box = $chromeDriver.FindElement([OpenQA.Selenium.By]::Name("q"))
$box.SendKeys("powershell")
$box.SendKeys([OpenQA.Selenium.Keys]::Enter)

Start-Sleep -s 5

$chromeDriver.Quit()


それぞれ解説します。


$dllPath = $null

if($host.Name -match "ise"){
    #ise上からでは$PSScriptRootが使えないため
    $dllPath = Split-Path $psISE.CurrentFile.FullPath -Parent
} else {
    #ise以外で呼び出された際の処理
    if($PSVersionTable.PSVersion.Major -ge 3){
        #power shellのversionが3以上なら
        $dllPath = $PSScriptRoot
    } else {
        #3未満なら
        $dllPath = Split-Path $MyInvocation.MyCommand.Path -Parent
    }
}

これはPowershell ISE上で編集する際、現在のカレントディレクトリを取得する$PSScriptRootが使えないため、ISE上からプログラムを動作させた場合とそうでない場合でカレントディレクトリの取得方法を分けています。


$WebDriverPath ="$dllPath\Data\Selenium\lib\net48\WebDriver.dll"
$WebDriverSupportPath = "$dllPath\Data\Selenium\lib\net48\WebDriver.Support.dll"
$ChromeDriverPath = "$dllPath\Data\"
Add-Type -Path $WebDriverPath
Add-Type -Path $WebDriverSupportPath

ここでダウンロードしてきたdll、exeのパスを指定しています。
その後、Add-Typeでライブラリを呼んでいます。


$driver = New-Object OpenQA.Selenium.Chrome.ChromeOptions
$chromeDriver = New-Object OpenQA.Selenium.Chrome.ChromeDriver($ChromeDriverPath,$driver)

ここでオブジェクトを作成しています。


$chromeDriver.Url = "https://www.google.co.jp"

chromeを起動させた後、googleに飛びます。


$box = $chromeDriver.FindElement([OpenQA.Selenium.By]::Name("q"))
$box.SendKeys("powershell")
$box.SendKeys([OpenQA.Selenium.Keys]::Enter)

googleの検索ボックスに「powershell」と入力し、検索する処理です。
一番私が躓いた処理でもあります。
リファレンス通り

$box = $chromeDriver.FindElement(By.Name("q"))

と書いてもエラーが出て処理してくれません。
なのでSeleniumのFunctionですよと明示する必要があります。
それが([OpenQA.Selenium.By]::Nameなのです。
SendKeysで検索ボックスにテキストを送っています。
あとはEnterキーを押下する処理を送るだけです。


Start-Sleep -s 5
$chromeDriver.Quit()

結果を確認したかったのでとりあえず5秒待機させています。
その後、Quit()でWebDriverを閉じています。




SeleniumのリファレンスにはPowershellの例がないのでちゃんと動作させるのに3時間ぐらい費やしました...

Powershell、開発環境を入れられない端末でも使える言語なのですごい重宝するのですが、いかんせん凝ったプログラムはCやJavaPythonで説明しているものが多いのでなかなか大変です。

次回はPowershellつながりで、UIAutomationについて書こうかと思います。