私PowerShellだけど「送る」からファイルを受け取りたい（コンテキストメニュー登録もあるよ）

# はじめに

PowerShellはポリシーの都合でファイルをダブルクリックして実行という操作で実行するのは少し面倒です。

そこで、任意のファイルでスクリプト実行する方法であるエクスプローラーの「送る」で、PowerShellスクリプトを実行できるか調べてみました。

送られたファイルに日時の文字列をつけてコピーするスクリプトをサンプルとして作成したのでまとめておきます。

#### 2019/01/30 更新
括弧を含むパスの場合の問題点と対策をページ下に追記しました。


# 「送る」機能について

エクスプローラーの右クリックで表示される「送る」の動作の説明をしておくと、

- SendToのフォルダにショートカットを登録すると「送る」の項目に表示される
- 送られたファイルのパスは実行時のパラメータとして渡される

という２つの動作になっています。
ショートカットの作成処理とファイルを受け取る側の２つに分けて解説します。

# 「送る」のショートカット作成

[こちら](https://qiita.com/magiclib/items/cc2de9169c781642e52d)の記事でスタートアップにショートカットを作成していますが、これとほぼ同じです。
sendtoのパスを取得して、ショートカットを作成します。


```powershell:create_sendto_shortcut.ps1

# 日本語のショートカットを作成する場合は UTF-8 with BOM で保存すること.

function create_sendto_shortcut($script_path, $name){
    # sendtoのパスを取得.
    $sendto_path = [Environment]::GetFolderPath([Environment+SpecialFolder]::SendTo)
    $sendto_shortcut_path = [System.IO.Path]::Combine($sendto_path, $name + ".lnk" )

    # ショートカットの有無確認.
    if(![System.IO.File]::Exists($sendto_shortcut_path)){
        # ショートカット生成.
        $WshShell = New-Object -ComObject WScript.Shell
        $ShortCut = $WshShell.CreateShortcut($sendto_shortcut_path)
        $ShortCut.TargetPath = "powershell.exe"
        $ShortCut.Arguments = "-NoProfile -ExecutionPolicy Unrestricted " + $script_path
        # $ShortCut.WindowStyle = 7 # 最小化する場合はコメント外す
        $ShortCut.Save()
    }
}

# 実行するps1ファイルのフルパス.
$current_path = Split-Path -Parent $MyInvocation.MyCommand.Path
$sendto_script = [System.IO.Path]::Combine($current_path, "sendto.ps1")

create_sendto_shortcut $sendto_script "日時追加コピー"
```


# 送られたファイルを受け取る

PowerShellでは実行時のパラメータは$Argsにパラメータが格納されているそうなので、こちらを使います。

先頭パラメータが $Args[0]に格納されているはずなのですが、動作を確認したところ、スペースを含むパスの場合は別のパラメータと認識されてしまうので、連結させる必要がありました。

```powershell:sendto.ps1


# パラメータ配列をパス別に再構築.
$files_args = New-Object System.Collections.ArrayList
foreach($arg in $Args){
    if([System.IO.Path]::IsPathRooted($arg)){
        $files_args.Add($arg) | Out-Null
    }
    else{
        # ファイルパスの先頭でない文字列は前の文字列の後にスペースで結合.
        $files_args[$files_args.Count-1] = $files_args[$files_args.Count-1] + " " + $arg
    }
}

# 日時文字列.
$datetime = (Get-Date).ToString("yyyyMMddHHmmss")

foreach($file_path in $files_args){
    Write-Output ("file_path:"+$file_path)
    if(Test-Path $file_path){
        # パスの分解.
        $folder = [System.IO.Path]::GetDirectoryName($file_path)
        $file = [System.IO.Path]::GetFileNameWithoutExtension($file_path)
        $ext = [System.IO.Path]::GetExtension($file_path)

        # コピー先パスの作成.
        $new_file_path = [System.IO.Path]::Combine($folder, $file + "_" + $datetime + $ext )

        # ファイルコピー.
        Copy-Item $file_path $new_file_path -Recurse
    }
}
```



## 動作確認

![sendto.gif](https://qiita-image-store.s3.amazonaws.com/0/264524/c5af9a95-d689-f4a5-bfc8-4ad5adc3866f.gif)

# ファイルドロップに応用

ショートカットにファイルをドロップする操作は、「送る」と同じ動作になります。
デスクトップにショートカットを置くことでドロップ用に使う場合は以下のようにします。


```powershell:create_desktop_shortcut.ps1

# 日本語のショートカットを作成する場合は UTF-8 with BOM で保存すること.

function create_desktop_shortcut($script_path, $name){
    # Desktopのパスを取得.
    $desktop_path = [Environment]::GetFolderPath([Environment+SpecialFolder]::Desktop)
    $desktop_shortcut_path = [System.IO.Path]::Combine($desktop_path, $name + ".lnk" )

    # ショートカットの有無確認.
    if(![System.IO.File]::Exists($desktop_shortcut_path)){
        # ショートカット生成.
        $WshShell = New-Object -ComObject WScript.Shell
        $ShortCut = $WshShell.CreateShortcut($desktop_shortcut_path)
        $ShortCut.TargetPath = "powershell.exe"
        $ShortCut.Arguments = "-NoProfile -ExecutionPolicy Unrestricted " + $script_path
        # $ShortCut.WindowStyle = 7 # 最小化する場合はコメント外す
        $ShortCut.Save()
    }
}

# 実行するps1ファイルのフルパス.
$current_path = Split-Path -Parent $MyInvocation.MyCommand.Path
$sendto_script = [System.IO.Path]::Combine($current_path, "sendto.ps1")

create_desktop_shortcut $sendto_script "日時追加コピー"
```


## 動作確認

![drop.gif](https://qiita-image-store.s3.amazonaws.com/0/264524/5218e173-9a94-d692-daec-eac7db629cd8.gif)





# おわりに

「送る」やファイルドロップでのファイル受け渡しは、ユーザービリティ向上が大きいので覚えておいて損はない小技だと思います。


# 致命的な問題がありました(2018/12/27追記)

括弧()を含むファイル名だと送るときに、PowerShell側がファイル名文字列と認識せずにエラーになっていることに気づきました。
エスケープすれば動作するのですが、ショートカットから渡す引数の文字列が編集できないので、回避するのは難しそうです。

## 回避案：コンテキストメニューに登録



次の案としては、コンテキストメニューに登録して、引数のファイル名をエスケープぜずに渡す方法です。

管理者権限がなくても登録可能なHKCU:\Software\Classes\*\shell\ のレジストリに追加します。
regファイルで追加してもいいのですが、せっかくなのでPowerShellで登録します。

```powershell:add_context_menu.ps1

    # 実行するps1ファイルのフルパス.
    $current_path = Split-Path -Parent $MyInvocation.MyCommand.Path
    $sendto_script = [System.IO.Path]::Combine($current_path, "sendto.ps1")
    # 登録するコマンド. ※%1をシングルクォートで括る
    $context_command = "powershell.exe -NoProfile -ExecutionPolicy Unrestricted " + $sendto_script + " '%1'"

    # レジストリ登録. HKCUは管理者権限不要.
    New-Item HKCU:\Software\Classes\*\shell\日時追加コピー
    Set-ItemProperty HKCU:\Software\Classes\*\shell\日時追加コピー -name "(default)" -value "日時追加コピー"
    New-Item HKCU:\Software\Classes\*\shell\日時追加コピー\command
    Set-ItemProperty HKCU:\Software\Classes\*\shell\日時追加コピー\command -name "(default)" -value $context_command

```

この方法だと最小化できないのでウィンドウが表示されてしまうのが課題です。


# batファイルを利用した対策(2019/01/30追記)

名前に括弧が含まれるファイルを送ることができない問題をbatファイルを経由することで解決しました。



括弧が含まれるとエラーになる例

```powershell

powershell -NoProfile -ExecutionPolicy Unrestricted .\sendto.ps1 .\aa(b).txt
```

実行結果

```
b : 用語 'b' は、コマンドレット、関数、スクリプト ファイル、または操作可能なプログラムの名前として認識されません。名前
が正しく記述されていることを確認し、パスが含まれている場合はそのパスが正しいことを確認してから、再試行してください。
発生場所 行:1 文字:71
+ ... ell -NoProfile -ExecutionPolicy Unrestricted .\sendto.ps1 .\aa(b).txt
+                                                                    ~
    + CategoryInfo          : ObjectNotFound: (b:String) [], CommandNotFoundException
    + FullyQualifiedErrorId : CommandNotFoundException
```

この問題に対してbatファイルでシングルクォートを付けることで回避します。
余計なファイルが増えるし、経由するので遅くなりますが、これ以上の方法が思い浮かばなかった。



## 対策版のコード

batファイルに渡されたパラメータpathを'path'の形式にします。
複数パラメータ渡された場合はスペース区切りの１つの文字列にします。
この文字列をps1ファイルのパラメータとして渡します


```sendto.bat

@echo off
set ARGS_PARAM=

:loop
if "%~f1" == "" goto end
set ARGS_PARAM=%ARGS_PARAM% '%~f1'
shift
goto loop

:end
powershell -NoProfile -ExecutionPolicy Unrestricted ".\sendto.ps1" %ARGS_PARAM%
```


sendto.bat内で相対パスでps1を呼ぶために、ショートカットを作るときに作業フォルダーを指定しておきます。

```powershell:setup_sendto.ps1

function create_sendto_shortcut($script_path,$link_name,$work_path){
    # sendtoのパスを取得.
    $sendto_path = [Environment]::GetFolderPath([Environment+SpecialFolder]::SendTo)
    $sendto_shortcut_path = [System.IO.Path]::Combine($sendto_path, $link_name + ".lnk" )

    # ショートカットの有無確認.
    $is_shortcut = (Test-Path $sendto_shortcut_path)
    if(!$is_shortcut){
        # ショートカット生成.
        $WshShell = New-Object -ComObject WScript.Shell
        $ShortCut = $WshShell.CreateShortcut($sendto_shortcut_path)
        $ShortCut.TargetPath = $script_path
        $ShortCut.WorkingDirectory = $work_path
        # $ShortCut.WindowStyle = 7 # 最小化する場合はコメント外す
        $ShortCut.Save()
    }
}


# 実行するbatファイルパス.
$current_path = Split-Path -Parent $MyInvocation.MyCommand.Path
$sendto_script = [System.IO.Path]::Combine($current_path, "sendto.bat")

create_sendto_shortcut $sendto_script "日時追加コピー" $current_path

```

$Argsには必ず'で括られたパスの配列が渡される前提で実装します。

```powershell:sendto.ps1
$files_args = $Args

# 日時文字列.
$datetime = (Get-Date).ToString("yyyyMMddHHmmss")

foreach($file_path in $files_args){
  Write-Output ("file_path:"+$file_path)
  if(Test-Path $file_path){
    # パスの分解.
    $folder = [System.IO.Path]::GetDirectoryName($file_path)
    $file = [System.IO.Path]::GetFileNameWithoutExtension($file_path)
    $ext = [System.IO.Path]::GetExtension($file_path)

    # コピー先パスの作成.
    $new_file_path = [System.IO.Path]::Combine($folder, $file + "_" + $datetime + $ext )

    # ファイルコピー.
    Copy-Item $file_path $new_file_path -Recurse
  }
}
```

これで括弧を含むファイル名を送られても処理することが可能になりました。


# 私PowerShellだけど…シリーズ

[私PowerShellだけど、君のタスクトレイで暮らしたい](https://qiita.com/magiclib/items/cc2de9169c781642e52d)
[私powershellだけどタスクトレイの片隅でアイを叫ぶ](https://qiita.com/magiclib/items/9d0cd35814b6fcff2e44)
[私PowerShellだけど子を持つ親になるのはいろいろ大変そう](https://qiita.com/magiclib/items/ef0ab5c1fd8e0260b39d)
[私PowerShellだけどあなたにトーストを届けたい（プログレスバー付）](https://qiita.com/magiclib/items/12e2a9e1e1e823a7fa5c)
[私Powershellだけど日付とサイズでログを切り替えたい(log4net)](https://qiita.com/magiclib/items/4fe5bab3e879dd8de951)
[私PowerShellだけどスクリプトだけでサービス登録したい](https://qiita.com/magiclib/items/b4e2e919f02af1b04b58)
[私PowerShellだけどスクリーンショットを撮影したい](https://qiita.com/magiclib/items/91f7d32cbd766b39b293)
[私PowerShellだけどマウスカーソル付きのスクリーンショットを撮影したい](https://qiita.com/magiclib/items/c867525aa83db5a00ebb)
[私PowerShellだけどスリープ抑制したい](https://qiita.com/magiclib/items/352e46f86d3d0e2e9c7b#%E7%A7%81powershell%E3%81%A0%E3%81%91%E3%81%A9%E3%82%B9%E3%83%AA%E3%83%BC%E3%83%97%E6%8A%91%E5%88%B6%E3%81%97%E3%81%9F%E3%81%84)

[サンプルソース置き場](https://github.com/part-time-penguin/qiita_powershell_samples)