ファイルサーバ内のショートカットファイルのリンク先を 複数条件で一括修正するPowerShellスクリプト

##概要
「ファイルサーバを移行した」等の際に、
「ショートカットファイルのリンク先が旧いままで困った」
という時に役立つスクリプトです。

[**以前の投稿**](https://qiita.com/X-IA/items/cb9c6d74793eb0edd39d)で公開したスクリプトでは、下記のように不便な点がありました。

* 対象のショートカットファイルを1個1個指定する必要がある
* 置換条件を都度指定する必要がある

今回は、ファイル一覧記述したファイルと置換条件を記述したファイルそれぞれを読み込んで処理する設計にしました。

##環境
Windows 10
※Windows 7のMicrosoft社によるサポート終了が近いため、今後はWindows 10で動作確認を行います。

##使用方法
1. [**ファイルサーバのファイルを一覧化するスクリプト**](https://qiita.com/X-IA/items/2dc5d1d497231ddbd9a0)を使用して、「ファイル一覧」ファイルを作成します。
* 「置換条件」（検索文字列と置換後の文字列）をタブ区切り形式で記述したファイルを作成します。
* 次のいずれかの方法で、指定してスクリプトを実行します。
 * コマンドラインから、「ファイル一覧」「置換条件」ファイルをそれぞれ引数で指定して（下記補足事項参照）"ModLnkM.bat" を実行します。
 * "ModLnkM.bat" を単体で起動し、プロンプト上で対話形式で「ファイル一覧」ファイル、「置換条件」ファイルを指定します。
* エクスプローラにて、ショートカットファイルのプロパティを開き、リンク先が変更されたことを確認します。

###補足事項
#####「ファイル一覧」の書式例#####

```
<任意の列名>	FullName
○○○○○○○○	\\file-server01.address\ShareFolder\path\FileName.lnk
□□□□□□□□	Z:\MyDocs\PJfolder\FileName.lnk
```

* [**LsFile**](https://qiita.com/X-IA/items/2dc5d1d497231ddbd9a0)で生成したファイルをそのまま使用可能です。
* "FullName" 列さえあれば、他の列は不要です。
* ショートカットファイル以外が含まれていても問題ありません。スクリプト内で拡張子により処理対象が絞り込まれます。

#####「置換条件」の書式例#####

```
file-server01	file-server02
mytemp	sharedtemp	3
<検索文字列>	<置換後文字列>	<オプション>
```

* 「置換条件」ファイルでは、ヘッダー行は不要です。
* 3列目のオプションは、個別に指定したい場合に下記要領で数字を指定します。
 * リンク先プログラムを置換対象とする場合は、7の倍数にします。
 * 引数を置換対象とする場合は、5の倍数にします。
 * 作業フォルダーを置換対象とする場合は、3の倍数にします。
 * 置換条件を正規表現で解釈させる場合は、2の倍数にします。
 * このオプションを指定しない場合、リンク先プログラム、引数、作業フォルダーとも対象になります。

#####コマンドラインオプション#####

```
ModLnkM.bat [-target] [-argument] [-work] [-conv 置換条件ファイルのパス] [ファイル一覧ファイルのパス]
```

* ```-target``` を入れるとショートカットファイルのリンク先プログラム、```-argument```を入れるとその引数、```-work```を入れると作業フォルダが置換対象になり、必要に応じて置換対象を複数選択可能です。これらのオプションをいずれも入れない場合は、3箇所とも置換対象になります。<br/>但し、「置換条件」ファイルの3カラム目でオプションを指定した場合は、それが優先されます。
* ```-reg``` を入れると、置換条件のキーワードが正規表現で解釈されます。
* オプションの識別子は、```-targetwork```、```-argumentarget``` のように、同一引数に複数オプションを含めることが可能です。

##ソースファイル
「マジック生成」するには、本ページ全体を選択してコピー後に[**B642FHT.bat**](https://qiita.com/X-IA/items/1db1929775e34ac7ef01)を起動して下さい。
その後、生成したZIPファイルを解凍して任意の場所へ配置して下さい。

###コード
```:ModLnkM.bat
@ECHO OFF
PowerShell -ExecutionPolicy Unrestricted -File %~dpn0.ps1 %* "nul" "nul"
EXIT /B
```

```powershell:ModLnkM.ps1
$Host.UI.RawUI.ForeGroundColor = "Green"

 "############# ModLnkM.ps1 ##############"
 "# Replace the target of shortcuts      #"
 "#                     multiple edition #"
 "#                                      #"
 "#   1st release: 2019-07-24            #
 "#   Rearrange  : 2019-10-26            #"
 "#   Last update: 2019-11-09            #"
 "#   Author: Y. Kosaka                  #"
 "#   See the web for more information   #"
 "#   https://qiita.com/x-ia             #"
 "########################################"

$nameFileScript = (Get-ChildItem $MyInvocation.MyCommand.Path).BaseName
$dirPathScript = Split-Path $MyInvocation.MyCommand.Path -Parent
$extLnk = ".lnk"
$extBak = ".bak"
$extLog = ".log"
$Host.UI.RawUI.WindowTitle = $nameFileScript
$pathFileLog = $dirPathScript + "\" + $nameFileScript + $extLog
$flagOpt = 1
$arrFlag = 7,5,3,2
$strSearch = $null
$strReplace = $null
$iArg = 0
$i = 0
$numIn = 0
$numOut = 0
$timePause = 1500

while ($Args[$iArg] -like "-*") {
  $iArgOffset = 1
  if (($Args[$iArg] -like "-*target*") -Or ($Args[$iArg] -like "-*tgt*")) {
    $flagOpt *= $arrFlag[0]
  }
  if (($Args[$iArg] -like "-*argument*") -Or ($Args[$iArg] -like "-*args*")) {
    $flagOpt *= $arrFlag[1]
  }
  if (($Args[$iArg] -like "-*work*") -Or ($Args[$iArg] -like "-*dir*")) {
    $flagOpt *= $arrFlag[2]
  }
  if ($Args[$iArg] -like "-*reg*") {
    $flagOpt *= $arrFlag[3]
  }

  if (($Args[$iArg] -like "-*conv*") -Or ($Args[$iArg] -like "-*criteri*")) {
    $pathTableConv = $Args[$($iArg + 1)]
    $iArgOffset = 2
  }
  $iArg += $iArgOffset
}

if ($flagOpt -le $arrFlag[3]) {
  $flagOpt *= $arrFlag[0] * $arrFlag[1] * $arrFlag[2]
}

  $pathListFile = $Args[$iArg]
  if ( $pathListFile -eq "nul") {
    Write-Host "`r`nEnter the path of the list file of shortcut files to modify the link target."
    Write-Host "To exit, hit the Enter key w/o any characters."
    $pathListFile = Read-Host
    if (($pathListFile -eq $null) -Or ($pathListFile -eq "")) {
      Write-Host "`r`nTerminated by user.`r`n"
      break
    }
  } else {
    Write-Host "`r`nThe list file to be processed:`r`n$pathListFile`r`n"
  }
  $listFile = (Import-Csv $pathListFile -Encoding Default -Delimiter "`t" | `
    Where-Object {$_.FullName -like "*.lnk"} | `
    Select-Object -Property "FullName" `
    )
  $numAll = @($listFile).Length

  if ($pathTableConv.Length -le 7) {
    Write-Host "`r`nEnter the path of the list file of keywords to search & replace targets for."
    Write-Host "To exit, hit the Enter key w/o any characters."
    $pathTableConv = Read-Host
    if (($pathTableConv -eq $null) -Or ($pathTableConv -eq "")) {
      Write-Host "`r`nTerminated by user.`r`n"
      break
    }
  } else {
    Write-Host "`r`nThe list file of search criteria :`r`n$pathTableConv`r`n"
  }
  $tableConv = (Get-Content $pathTableConv -Encoding Default | `
    ConvertFrom-CSV -header keySearch,keyReplace,flagOpt `
    -Delimiter "`t")

for ($i=0; $i -lt $numAll; $i++) {
  $pathFile = $listFile[$i].Fullname

  if ((Test-Path $pathFile.Replace('"', '')) -ne $true) {
    continue
  }

  if ((Get-Item $pathFile).Extension -ne ".lnk") {
    continue
  }

  ++$numIn

    $flagChg = 1

    $objSh = New-Object -ComObject WScript.Shell
    $objLnk = $objSh.CreateShortcut($pathFile)  ## Open the lnk

    $pathTarget = $objLnk.TargetPath
    $strArgs = $objLnk.Arguments
    $pathWork = $objLnk.WorkingDirectory

    Write-Host "Current target: $pathTarget"

    $timeStampNow = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss.fff")
    $strLog = $pathTarget + "`t" + $strArgs + "`t" + $pathWork

    for ($j=0; $j -lt @($tableConv).Length; $j++) {
      $strSearch = $tableConv[$j].keySearch
      $strReplace = $tableConv[$j].keyReplace
      if ( [int]::TryParse($tableConv[$j].flagOpt, [ref]$null) ) {
        $flagOpt = $tableConv[$j].flagOpt
      }

      if ($flagOpt % $arrFlag[0] -eq 0) {
        if ($flagOpt % $arrFlag[3] -eq 0) {
          Write-Host "$strSearch, $strReplace"
          $tmp = $pathTarget -ireplace $strSearch, $strReplace
        } else {
          $tmp = $pathTarget -ireplace [regex]::escape($strSearch), $strReplace
        }
        if ($tmp -ne $pathTarget) {
          $flagChg *= $arrFlag[0]
          $pathTarget = $tmp  ## Re-enter
        }
      }

      if ($flagOpt % $arrFlag[1] -eq 0) {
        if ($flagOpt % $arrFlag[3] -eq 0) {
          $tmp = $strArgs -ireplace $strSearch, $strReplace
        } else {
          $tmp = $strArgs -ireplace [regex]::escape($strSearch), $strReplace
        }
        if ($tmp -ne $strArgs) {
          $flagChg *= $arrFlag[1]
          $strArgs = $tmp  ## Re-enter
        }
      }

      if ($flagOpt % $arrFlag[2] -eq 0) {
        if ($flagOpt % $arrFlag[3] -eq 0) {
          $tmp = $pathWork -ireplace $strSearch, $strReplace
        } else {
          $tmp = $pathWork -ireplace [regex]::escape($strSearch), $strReplace
        }
        if ($tmp -ne $pathWork) {
          $flagChg *= $arrFlag[2]
          $pathWork = $tmp  ## Re-enter
        }
      }
    }

    if ($flagChg -ne 1) {
      Write-Host "Shortcut file: $pathFile"
      $pathBak = $pathFile + $extBak
      Move-Item $pathFile $pathBak  ## Rename the original before rewriting
      Copy-Item $pathBak $pathFile  ## Copy the new before rewriting

      $objLnk.TargetPath = $pathTarget  ## Make changes
      $objLnk.Arguments = $strArgs  ## Make changes
      $objLnk.WorkingDirectory = $pathWork  ## Make changes
      $objLnk.Save()  ## Save

      $strLog += "`t" + $pathTarget + "`t" + $strArgs + "`t" + $pathWork
      "$timeStampNow`t$nameFileScript`t$pathFile`t$strLog" | `
        Out-File $pathFileLog -Encoding Default -Append
      Write-Host "New target: $pathTarget"
      Write-Host "New arguments: $strArgs"
      Write-Host "New working directory: $pathWork`r`n"
      ++$numOut
    } else {
      Write-Host "No required for modifying the shortcut file.`r`n"
    }

    $Host.UI.RawUI.WindowTitle = "$nameFileScript $numOut/$numIn/$numAll"

}
Start-Sleep -milliseconds $timePause
```

###バイナリ (Base64 encoding)
```
ModLnkM.zip
---
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
---
```