複数のショートカットファイルのリンク先を 一発修正するPowerShellスクリプト


概要

「ファイルサーバのアドレスが変更になった」など、
ショートカットファイルのリンク先を簡単に変更したい、
でもショートカットファイルをテキストエディタで開いてもバイナリファイルになっていてGrep置換不可。

こんな経験がある方がいらっしゃるかも知れません。

WSHでスクリプト化したものはWebで見掛けますが、
引数または対話式で対象ファイルと置換文字列を指定するスクリプトを自分で作ってみました。
例により今回もPowerShellです。

但し、対象のショートカットファイルを1個1個指定する必要がある点、置換条件を都度指定する必要がある点で不便に感じるかも知れません。
そのような場合には、ファイル一覧記述したファイルと置換条件を記述したファイルそれぞれを読み込んで処理する設計にしたModLnkMの方を是非お試しください。

環境

Windows10, Windows 7

使用方法

  • リンク先を変更したいショートカットファイルを選択し、"ModLnk.bat" にDrag&Dropします(ファイルまたはフォルダのパスが引数に指定されてバッチが起動します)。

  • ファイルへのD&Dが困難な場合は、バッチファイルを単体で起動するとフォルダパスの入力を求められるため、ファイルをD&Dして [Enter]キーを押下します。ファイルパス(複数ファイル指定不可)をペーストする方法もあります。

  1. 上記いずれかの方法で、変更対象のファイルを指定して起動します。
  2. 検索文字列および置換文字列を問われますので それぞれ入力して [Enter]キーを押下します。
  3. エクスプローラにて、ショートカットファイルのプロパティを開き、リンク先が変更されたことを確認します。

補足事項

通常、バッチファイル起動後に対話形式で検索文字列および置換文字列を入力しますが、
下記構文でオプションが使用可能です。

ModLnk.bat [-search 検索文字列] [-replace 置換文字列] [-target] [-argument] [-work] [1ファイル目のパス [2ファイル目のパス ...]]
  • -search で検索文字列、-replaceで置換文字列を予め指定可能です。
  • -target を入れるとショートカットファイルのリンク先プログラム、-argumentを入れるとその引数、-workを入れると作業フォルダが置換対象になり、必要に応じて置換対象を複数選択可能です。これらのオプションをいずれも入れない場合は、3箇所とも置換対象になります。
  • オプションの識別子は、-searchreplace-targetworkのように、同一引数に複数オプションを含めることが可能です。

ソースファイル

「マジック生成」するには、本ページ全体を選択してコピー後にB642FHT.batを起動して下さい。
その後、生成したZIPファイルを解凍して任意の場所へ配置して下さい。

コード

ModLnk.bat
@ECHO OFF
PowerShell -ExecutionPolicy RemoteSigned -File %~dpn0.ps1 %* "nul" "nul"
EXIT /B
ModLnk.ps1
$Host.UI.RawUI.ForeGroundColor = "Green"

"############## ModLnk.ps1 ##############"
"# Replace the target of shortcuts      #"
"#                                      #"
"#   1st release: 2019-07-24            #"
"#   Last update: 2019-07-25            #"
"#   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
$flagCont = 1
$flagOpt = 1
$arrFlag = 7,5,3,2
$strSearch = $null
$strReplace = $null
$iArg = 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 "-*search*") -Or ($Args[$iArg] -like "-*src*")) {
    $strSearch = $Args[$($iArg + 1)]
    $iArgOffset = 2
  }
  if (($Args[$iArg] -like "-*replace*") -Or ($Args[$iArg] -like "-*dst*")) {
    $strReplace = $Args[$($iArg + 1)]
    $iArgOffset = 2
  }
  $iArg += $iArgOffset
}

if ($Args[$iArg] -ne "nul") {
  $flagCont = 0
}
if ($flagOpt -le $arrFlag[3]) {
  $flagOpt *= $arrFlag[0] * $arrFlag[1] * $arrFlag[2]
}

do {
  $arg = $Args[$iArg]
  if ($arg -eq "nul") {
    Write-Host "`r`nEnter the path of a shortcut file to modify the link target."
    Write-Host "To exit, hit the Enter key w/o any characters."
    $arg = Read-Host
    if (($arg -eq $null) -Or ($arg -eq "")) {
      Write-Host "`r`nTerminated by user.`r`n"
      break
    }
    if ((Test-Path $arg.Replace('"', '')) -ne $true) {
      Write-Host "`r`nFile not exists.`r`n"
      continue
    }
  } else {
    Write-Host "`r`nThe file to be modified:`r`n$arg`r`n"
  }

  $pathFile = $arg.Replace('"', '')
  ++$numIn

  while ($true) {
    if ((Test-Path $pathFile) -ne $true) {
      Write-Host "The files to be modified not exists.`r`n"
      break
    }

    $pathBak = $pathFile + $extBak
    Copy-Item $pathFile $pathBak  ## Get the lnk we want to use as a template
    $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"

    if ($strSearch -eq $null) {
    Write-Host "`r`nEnter the string to search for."
      $strSearch = Read-Host
    }
    if ($strReplace -eq $null) {
    Write-Host "`r`nEnter the string to replace into."
      $strReplace = Read-Host
    }

    $timeStampNow = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss.fff")
    $strLog = $null

    if ($flagOpt % $arrFlag[0] -eq 0) {
      if ($flagOpt % $arrFlag[3] -eq 0) {
      Write-Host "$strSearch, $strReplace"
        $objLnk.TargetPath = $pathTarget -ireplace $strSearch, $strReplace  ## Make changes
      } else {
        $objLnk.TargetPath = $pathTarget -ireplace [regex]::escape($strSearch), [regex]::escape($strReplace)  ## Make changes
      }
      $strLog += $pathTarget
    }

    if ($flagOpt % $arrFlag[1] -eq 0) {
      if ($flagOpt % $arrFlag[3] -eq 0) {
        $objLnk.Arguments = $strArgs -ireplace $strSearch, $strReplace  ## Make changes
      } else {
        $objLnk.Arguments = $strArgs -ireplace [regex]::escape($strSearch), [regex]::escape($strReplace)  ## Make changes
      }
      $objLnk.Arguments = $strArgs -ireplace [regex]::escape($strSearch), $strReplace  ## Make changes
      $strLog += "`t" + $strArgs
    } else {
      $strLog += "`t"
    }

    if ($flagOpt % $arrFlag[2] -eq 0) {
      if ($flagOpt % $arrFlag[3] -eq 0) {
        $objLnk.WorkingDirectory = $pathWork -ireplace $strSearch, $strReplace  ## Make changes
      } else {
        $objLnk.WorkingDirectory = $pathWork -ireplace [regex]::escape($strSearch), [regex]::escape($strReplace)  ## Make changes
      }
      $strLog += "`t" + $pathWork
    } else {
      $strLog += "`t"
    }

    $objLnk.Save()  ## Save

    Write-Host "Done modifying the target of a shortcut file.`r`n"
    ++$numOut
    break
  }

  "$timeStampNow`t$nameFileScript`t$pathFile`t$strLog`t$strSearch`t$strReplace" | `
  Out-File $pathFileLog -Encoding Default -Append
  $Host.UI.RawUI.WindowTitle = "$nameFileScript $numOut/$numIn"

  if ($Args[$iArg] -ne "nul") {
    ++$iArg
  }
} while (($($Args[$iArg]) -ne "nul") -Or ($flagCont -eq 1))
Start-Sleep -milliseconds $timePause
ModLnkM.ps1
$Host.UI.RawUI.ForeGroundColor = "Green"

############# ModLnkM.ps1 ##############
# Replace the target of shortcuts      #
#                                      #
#   1st release: 2019-07-24            #
#   Rearrange  : 2019-10-26            #
#   Last update: 2019-10-28            #
#   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$pathList`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) {
#    Write-Host "The file to be modified not exists.`r`n"
    continue
  }

  if ((Get-Item $pathFile).Extension -ne ".lnk") {
#    Write-Host "`r`nFile not a shortcut file.`r`n"
    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) {
      $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 "Done modifying the target of a shortcut file.`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)

ModLnk.zip
---
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
AAASBgAAAAA=
---