【Powershell】BoxDriveのパスを加工する

# はじめに
メンバ間でBox上ファイルのパスをやり取りするとき、Box.comのパス(URL)ではなく、Windows端末上のBoxDriveでのパス（以降、ローカルパスと表記）を加工して連携しています。

ローカルパスにはユーザIDが含まれるため、ユーザごとに異なります。
また、権限付与の仕方によってはユーザによってもパスが異なります。

これらの差異を考慮した連携用パスの作成や、自分が開けるパスに加工するためのマクロをPowershellで作ることにしました。


# パスの加工処理

## 1. パスの共通化
### 連携するパスの加工
ユーザIDの差異に対応するため、連携用パスは環境変数 `%USERPROFILE%` を使う。

**Step1. パスの定義**

* 自分用パス：「`$env:USERPROFILE` + "\\Box"」で始まるパス
* 連携用パス：「"%USERPROFILE%\\Box"」で始まるパス

```powershell:パスの定義
# BOX LocalPath 実path
$BoxBasePath = ( Join-Path $env:USERPROFILE "Box" )

# BOX LocalPath 環境変数Path
$BoxBasePathEnv = "%USERPROFILE%\Box"
```
:::note info
環境変数
Windowsの環境変数は変数名を `%` で囲む形式（`%USERPROFILE%`など）だが、
Powershellで環境変数を使用する場合は「`$env:` + 変数名」（`$env:USERPROFILE`など）で参照する。
:::


**Step2. パスの置換**

```powershell:パスの置換（自分用パス → 連携用パス）
$newText = $targetPath
if ( $targetPath.StartsWith($BoxBasePath) ) {
    # BOX LocalPath
    $newText = $targetPath.Replace($BoxBasePath,$BoxBasePathEnv)
}
```

:::note info
Replaceメソッドでの置換
`Replace` メソッドは正規表現が使えないため、パスに含まれる `\` をエスケープせずにそのまま使える。
ただし、大文字小文字を区別する点には注意が必要。

`$env:USERPROFILE` で取得したパスは `\` がエスケープされていない状態で、大文字小文字の差が発生することがないと判断し、`Replace` メソッドを使っている。
また、正規表現を使わず「`$env:USERPROFILE` + "\\Box"」で始まるパスを判断するために、加工前に `StartsWith` で文字列を判定している。

:::

### 連携されたパスの加工
連携するBoxパスの形式は決められていないため、様々な形式で連携されてくる。
以下の形式をローカルパスとして処理する。

1. Box\\~
1. %USERPROFILE%\\Box\\~
1. %HOMEPATH%\\Box\\~
1. C:\\Users\\\<userid\>\\Box\\~
   ※ユーザID（自分以外含む）が含まれるパス。\<userid\> は "\\" を含まない文字列

**Step1. 形式 2, 3, 4 を形式 1 に統一する**

```powershell:パスの統一
$text = ( $text -replace "^%USERPROFILE%\\Box","Box" )
$text = ( $text -replace "^%HOMEPATH%\\Box","Box" )
$text = ( $text -replace "^C:\\Users\\[^\\]+\\Box","Box" )
```
:::note info
replace演算子での置換
replace演算子(`-replace`)は正規表現が使える。
パスに含まれる `\` などはエスケープが必要となる。
:::

**Step2. 自分用ローカルパスする**
Step1で「Box」始まりに統一されているため、前に `$env:USERPROFILE` をつける。
```powershell:自分用パスに加工
if ( $text -match "^Box" ) {
    $text = ( Join-Path $env:USERPROFILE $text )
    # 以降省略
```



## 2. パス差異の変換

Boxのパスはユーザの所属する権限グループによって違うパスに見えることがある。

例えば、グループAのメンバが参照可能なディレクトリ「01.groupA」の下に、グループBに公開（共有）しているディレクトリ「04.groupB公開」があると、以下のように見え方に差が出る。

グループAに所属するユーザAから見たパス：
　Box\\01.groupA\\04.groupB公開

グループBに所属するユーザBから見たパス：
　Box\\04.groupB公開

この差異によって、ユーザAとユーザBは相手からもらったパスを開けない問題が発生する。
この問題を解消するため、特定のパスの場合はパス変換する。

### 変換例

ユーザAが、ユーザBから連携されたパスを使うための加工。

やっていることは [連携するパスの加工](#連携するパスの加工) と同じ。

**Step1. パスの定義**
groupA用、groupB用のパスを以下と定義
* groupA用パス：「Box\\01.groupA\\04.groupB公開」で始まるパス
* groupB用パス：「Box\\04.groupB公開」で始まるパス

ユーザAが使う場合の定義は、
変換前パスがgroupB用パス、変換後パスがgroupA用パス。


```powershell:パスの定義(groupA用)
# For groupA（groupB用パスをgroupA用パスに変換）
# 変換前
$BoxPathBefore = 'Box\04.groupB公開'
# 変換後
$BoxPathAfter  = 'Box\01.groupA\04.groupB公開'
```

**Step2. パスの置換**
ローカルパスが変換前パスで始まる場合、変換前パス部分を変換後パス用に置換する。
```powershell:パスの置換
if ( $text.StartsWith($BoxPathBefore) ) {
    # パスの加工
    $text = $text.Replace($BoxPathBefore,$BoxPathAfter)
}
```

:::note info
Replaceメソッド　OR　replace演算子
どちらを使っても置換は可能。
Windowsではパスの大文字小文字は区別しないため、置換処理でも区別しない方がより良いとも考えられるが、対象パスをコピーして連携される可能性が高く、大文字小文字が実際と異なるケースはほぼ発生しないと判断。
また、パスには必ず `\` が含まれるため、読みやすさを重視して、正規表現を使わない `Replace` メソッドで置換している。
:::



# スクリプト：連携用パス作成
自分用ローカルパスから連携用パスへ加工するスクリプト。
引数で渡されたパスまたはクリップボードに入っているパスを加工して、加工結果をクリップボードに入れる。

また、対象のパスがgroupA用パスまたはgroupB用パスの場合、groupA用とgroupB用の２つのパスを作っている。
## ソース
```powershell:getPath.ps1
<#

  .DESCRIPTION
  引数として渡されたファイル・ディレクトリのパスを加工してクリップボードに入れる。
  引数が指定されていない場合はクリップボードの文字列を加工する。

  * BoxDriveのローカルパスの場合、ユーザIDまでの部分を "%USERPROFILE%" に変更する。
  * groupA用またはgroupB用のパスの場合、groupA用とgroupB用のパスを作る。
#>

param(
    [string] $targetPath = 'Clipboard'
)

if ($targetPath -ceq 'Clipboard') {
    $targetPath = Get-Clipboard
}

# BOX LocalPath 実path
$BoxBasePath = ( Join-Path $env:USERPROFILE "Box" )

# BOX LocalPath 環境変数Path
$BoxBasePathEnv = "%USERPROFILE%\Box"

$newText = $targetPath
if ( $targetPath.StartsWith($BoxBasePath) ) {
    # BOX LocalPath
    $newText = $targetPath.Replace($BoxBasePath,$BoxBasePathEnv)
}

# ---- 差異変換 ----
# For groupA
$BoxBaseA = ( Join-Path $BoxBasePath "\01.groupA\04.groupB公開" )
# For groupB
$BoxBaseB = ( Join-Path $BoxBasePath "\04.groupB公開" )

if ( $targetPath.StartsWith($BoxBaseA) ) {
    # groupAのパス
    $targetPath = $targetPath.Replace($BoxBaseA,$BoxBaseB)
    $targetPath = $targetPath.Replace($BoxBasePath,$BoxBasePathEnv)
    $newText = @"
groupA向け：
$newText
groupB向け：
$targetPath
"@
}
elseif ( $targetPath.StartsWith($BoxBaseB) ) {
    # groupBのパス
    $targetPath = $targetPath.Replace($BoxBaseB,$BoxBaseA)
    $targetPath = $targetPath.Replace($BoxBasePath,$BoxBasePathEnv)
    $newText = @"
groupA向け：
$targetPath
groupB向け：
$newText
"@
}
# --------------------

Set-Clipboard -Value $newText

# 出力（確認用）
$newText
```
## 実行例
ユーザID nsato（Boxローカルパスが`C:\Users\nsato\Box`始まり）の場合の例

* **Case1. 引数でgroupA用パスを指定**

【入力】
C:\Users\nsato\Box\01.groupA\04.groupB公開\aaa
【出力】
groupA向け：
%USERPROFILE%\Box\01.groupA\04.groupB公開\aaa
groupB向け：
%USERPROFILE%\Box\04.groupB公開\aaa

```:1-1. 実行　groupA用パス（引数）
PS C:\MyApp\bat\ps> .\getPath.ps1 C:\Users\nsato\Box\01.groupA\04.groupB公開\aaa
groupA向け：
%USERPROFILE%\Box\01.groupA\04.groupB公開\aaa
groupB向け：
%USERPROFILE%\Box\04.groupB公開\aaa
PS C:\MyApp\bat\ps>
```
```:1-2. 結果確認（クリップボード）
PS C:\MyApp\bat\ps> Get-Clipboard
groupA向け：
%USERPROFILE%\Box\01.groupA\04.groupB公開\aaa
groupB向け：
%USERPROFILE%\Box\04.groupB公開\aaa
PS C:\MyApp\bat\ps>
```

* **Case2. 引数でgroupB用パスを指定**

【入力】
C:\Users\nsato\Box\04.groupB公開\bbb
【出力】
groupA向け：
%USERPROFILE%\Box\01.groupA\04.groupB公開\bbb
groupB向け：
%USERPROFILE%\Box\04.groupB公開\bbb

```:2-1. 実行　groupB用パス（引数）
PS C:\MyApp\bat\ps> .\getPath.ps1 C:\Users\nsato\Box\04.groupB公開\bbb
groupA向け：
%USERPROFILE%\Box\01.groupA\04.groupB公開\bbb
groupB向け：
%USERPROFILE%\Box\04.groupB公開\bbb
PS C:\MyApp\bat\ps>
```
```:2-2. 結果確認（クリップボード）
PS C:\MyApp\bat\ps> Get-Clipboard
groupA向け：
%USERPROFILE%\Box\01.groupA\04.groupB公開\bbb
groupB向け：
%USERPROFILE%\Box\04.groupB公開\bbb
PS C:\MyApp\bat\ps>
```

* **Case3. 引数でgroupA用、groupB用どちらでもないBoxパスを指定**

【入力】
C:\Users\nsato\Box\02.groupB
【出力】
%USERPROFILE%\Box\02.groupB

```:3-1. 実行　どちらでもないBoxパス（引数）
PS C:\MyApp\bat\ps> .\getPath.ps1 C:\Users\nsato\Box\02.groupB
%USERPROFILE%\Box\02.groupB
PS C:\MyApp\bat\ps>
```
```:3-2. 結果確認（クリップボード）
PS C:\MyApp\bat\ps> Get-Clipboard
%USERPROFILE%\Box\02.groupB
PS C:\MyApp\bat\ps>
```

* **Case4. クリップボードでgroupA用パスを指定**

【入力】
C:\Users\nsato\Box\01.groupA\04.groupB公開\aaa
【出力】
groupA向け：
%USERPROFILE%\Box\01.groupA\04.groupB公開\aaa
groupB向け：
%USERPROFILE%\Box\04.groupB公開\aaa


```:4-1. 実行前確認
PS C:\MyApp\bat\ps> Get-Clipboard
C:\Users\nsato\Box\01.groupA\04.groupB公開\aaa
PS C:\MyApp\bat\ps>
```
```:4-2. 実行　groupA用パス（クリップボード）
PS C:\MyApp\bat\ps> .\getPath.ps1
groupA向け：
%USERPROFILE%\Box\01.groupA\04.groupB公開\aaa
groupB向け：
%USERPROFILE%\Box\04.groupB公開\aaa
PS C:\MyApp\bat\ps>
```
```:4-3. 結果確認（クリップボード）
PS C:\MyApp\bat\ps> Get-Clipboard
groupA向け：
%USERPROFILE%\Box\01.groupA\04.groupB公開\aaa
groupB向け：
%USERPROFILE%\Box\04.groupB公開\aaa
PS C:\MyApp\bat\ps>
```

# スクリプト：パスを加工して開く
連携されたパスを自分用ローカルパスに加工して開くスクリプト。
クリップボードのパスを自分のローカルパスに加工して開く。

## ソース
```powershell:openBoxPath.ps1
<#

  .DESCRIPTION
  クリップボードにコピーされた文字列がBoxパスの場合、そのパスを開く

  * 以下で始まるパスをBoxパスとして扱う（大文字小文字の区別なし）
    * Box
    * %USERPROFILE%\Box
    * %HOMEPATH%\Box
    * C:\Users\<userid>\Box  ※ユーザのローカルパス（自分以外を含む）

  * Path違いの加工
    ※groupA用とgroupB用がある。使う場合は一方のみを残す（不要な定義はコメントアウトする）
#>

$text = Get-Clipboard

$text = ( $text -replace "^%USERPROFILE%\\Box","Box" )
$text = ( $text -replace "^%HOMEPATH%\\Box","Box" )
$text = ( $text -replace "^C:\\Users\\[^\\]+\\Box","Box" )


# Path違いの加工
# For groupA（groupB用パスをgroupA用パスに変換）
$BoxPathBefore = 'Box\04.groupB公開'
$BoxPathAfter  = 'Box\01.groupA\04.groupB公開'

<#
# For groupB（groupA用パスをgroupB用パスに変換）
$BoxPathBefore = 'Box\01.groupA\04.groupB公開'
$BoxPathAfter  = 'Box\04.groupB公開'
#>

if ( $text.StartsWith($BoxPathBefore) ) {
    # パスの加工
    $text = $text.Replace($BoxPathBefore,$BoxPathAfter)
}
# --------------------

if ( $text -match "^Box" ) {
    $text = ( Join-Path $env:USERPROFILE $text )
    # 変換後パスをクリップボードに登録
    Set-Clipboard -Value $text
    # 出力（確認用）
    $text
    if (Test-Path($text)){
      # パスが有効な場合開く
      Start ( $text )
    }
}
```

## 実行例
groupA所属のユーザID nsato（Boxローカルパスが`C:\Users\nsato\Box`始まり）の場合の例

* **Case1. groupA用 Box 始まりのパス**

```:1-1. 実行前確認
PS C:\MyApp\bat\ps> Get-Clipboard
Box\01.groupA\04.groupB公開
PS C:\MyApp\bat\ps>
```
```:1-2. 実行
PS C:\MyApp\bat\ps> .\openBoxPath.ps1
C:\Users\nsato\Box\01.groupA\04.groupB公開
PS C:\MyApp\bat\ps>
```
```:1-3. 結果確認
PS C:\MyApp\bat\ps> Get-Clipboard
C:\Users\nsato\Box\01.groupA\04.groupB公開
PS C:\MyApp\bat\ps>
```

* **Case2. groupA用 %USERPROFILE% 始まりのBoxパス**
```:2-1. 実行前確認
PS C:\MyApp\bat\ps> Get-Clipboard
%USERPROFILE%\Box\01.groupA\04.groupB公開\aaa
PS C:\MyApp\bat\ps>
```
```:2-2. 実行
PS C:\MyApp\bat\ps> .\openBoxPath.ps1
C:\Users\nsato\Box\01.groupA\04.groupB公開\aaa
PS C:\MyApp\bat\ps>
```
```:2-3. 結果確認
PS C:\MyApp\bat\ps> Get-Clipboard
C:\Users\nsato\Box\01.groupA\04.groupB公開\aaa
PS C:\MyApp\bat\ps>
```

* **Case3. groupB用 %HOMEPATH% 始まりのBoxパス**
パスのうち「Box」までの部分は、大文字小文字の区別はしていない。
小文字の `%homepath%` 始まりでも問題ない。
```:3-1. 実行前確認
PS C:\MyApp\bat\ps> Get-Clipboard
%homepath%\Box\04.groupB公開\bbb
```
```:3-2. 実行
PS C:\MyApp\bat\ps> .\openBoxPath.ps1
C:\Users\nsato\Box\01.groupA\04.groupB公開\bbb
PS C:\MyApp\bat\ps>
```
```:3-3. 結果確認
PS C:\MyApp\bat\ps> Get-Clipboard
C:\Users\nsato\Box\01.groupA\04.groupB公開\bbb
PS C:\MyApp\bat\ps>
```

* **Case4. groupB用 他ユーザのローカルパス**
```:4-1. 実行前確認
PS C:\MyApp\bat\ps> Get-Clipboard
C:\Users\tiWW12345\Box\04.groupB公開\ccc
PS C:\MyApp\bat\ps>
```
```:4-2. 実行
PS C:\MyApp\bat\ps> .\openBoxPath.ps1
C:\Users\nsato\Box\01.groupA\04.groupB公開\ccc
PS C:\MyApp\bat\ps>
```
```:4-3. 結果確認
PS C:\MyApp\bat\ps> Get-Clipboard
C:\Users\nsato\Box\01.groupA\04.groupB公開\ccc
PS C:\MyApp\bat\ps>
```

* **Case5. Boxパス以外**
Boxパスではないため、加工したパスは出力されない。クリップボードの更新もされない。
```:5-1. 実行前確認
PS C:\MyApp\bat\ps> Get-Clipboard
C:\MyApp\bat\ps
PS C:\MyApp\bat\ps>
```
```:5-2. 実行
PS C:\MyApp\bat\ps> .\openBoxPath.ps1
PS C:\MyApp\bat\ps>
```
```:5-3. 結果確認
PS C:\MyApp\bat\ps> Get-Clipboard
C:\MyApp\bat\ps
PS C:\MyApp\bat\ps>
```


## 留意事項

ローカルで開いたことがない（ダウンロードされていない）フォルダやファイルのパスの場合、ローカルにないため開くことができない。
このスクリプトはパスが見つからずに開かなかった場合でも、クリップボードに加工後パスを入れている。
スクリプト実行してパスが開かれなかった場合は、以下のような方法で確認してみる。

1. 加工後パスをそのまま開いてみる（またはスクリプトを再実行する）
2. クリップボードに入っているパスで、対象パスが間違っていなかったか確認する
3. 加工後パスから上位のフォルダを開いてみる


# 補足
チームメンバなどにスクリプトを配布する際、Powershellのスクリプトってどう使えばいいの？となる人でも使えるようにスクリプトの頭に使い方の例を書いている。
その使い方をここにも記載しておく。

## 利用準備
1. スクリプトを任意の場所に置く
2. スクリプトのショートカットを作成する
3. 作成したショートカットのプロパティから、リンク先を以下のように修正する
   
     作成時：<ファイルフルパス>
     修正後：powershell -ExecutionPolicy RemoteSigned -File <ファイルフルパス>
   
     ※変更して適用（保存）すると「powershell」の部分がパス（C:\Windows\～）に変わる

## 利用方法

どちらのスクリプトもクリップボードから対象パスを指定して処理することができる。
対象のパスをコピーして、利用準備で作成したショートカットを実行すると処理される。

連携用パス作成は、対象パスを引数として渡して処理することも可能。
作成したショートカットをSendToフォルダに配置すれば、コンテキストメニュー（右クリック）の「送る」からパスを送って実行できる。
SendToフォルダは、Explore（フォルダ）のアドレスに"sendto"と入れて開くことができる。