はじめに
ファイルサーバーからSharePoint Online(SPO)への移行に伴い、社内に大量に散在しているショートカット(.lnk)ファイルの扱いに困りました。
.lnkはWindowsショートカットのバイナリ形式のため、ブラウザ経由でクリックしても実行されず単なるダウンロードになってしまいます。一方.url([InternetShortcut]形式のテキストファイル)はブラウザがネイティブに解釈できる形式なので、.lnkのリンク先パスの先頭にfile://を付与した.urlファイルへ一括変換するスクリプトを作成しました。
検証環境: Windows PowerShell 5.1 / Microsoft Edge
前提:ブラウザはfile://リンクを標準ではブロックする
Edgeでは既定でfile://リンクの直接オープンが無効化されています。事前に以下の設定を全端末へ配布しておく必要があります。
- レジストリパス:
HKLM\SOFTWARE\Policies\Microsoft\Edge - 値:
IntranetFileLinksEnabled(REG_DWORD)=1 - ローカルイントラネットゾーンに以下を追加登録
- 対象サイトのURL(例:
https://〇〇.sharepoint.com) file://
- 対象サイトのURL(例:
変換イメージ
リンク先パスの区切り文字やエンコードには手を加えず、単純に先頭へfile://を付与するだけのシンプルな変換です。
- 変換前の
TargetPath:C:\Users\user\OneDrive - 変換後の
.urlの中身:
[InternetShortcut]
URL=file://C:\Users\user\OneDrive
変換スクリプト
# --- 設定 ---
$targetRoot = "C:\Users" # .lnk を探索するルート(必要に応じて変更)
$deleteOriginal = $false
$logCsv = "C:\work\lnk_convert_log_$(Get-Date -Format 'yyyyMMdd_HHmmss').csv"
$resumeSkipExisting = $true
$shell = New-Object -ComObject WScript.Shell
$results = New-Object System.Collections.Generic.List[object]
Write-Host "スキャン中: $targetRoot ..."
$lnkFiles = Get-ChildItem -Path $targetRoot -Filter *.lnk -Recurse -File -ErrorAction SilentlyContinue -ErrorVariable scanErrors
foreach ($e in $scanErrors) {
$results.Add([pscustomobject]@{ Path=$e.TargetObject; Status="ScanError"; Detail=$e.Exception.Message; NewUrl="" })
}
Write-Host "対象 .lnk 件数: $($lnkFiles.Count) / スキャンエラー: $($scanErrors.Count)"
$i = 0
foreach ($lnk in $lnkFiles) {
$i++
if ($i % 500 -eq 0) { Write-Host "進捗: $i / $($lnkFiles.Count)" }
$urlPath = [System.IO.Path]::ChangeExtension($lnk.FullName, ".url")
if ($resumeSkipExisting -and (Test-Path $urlPath)) {
$results.Add([pscustomobject]@{ Path=$lnk.FullName; Status="SkippedExisting"; Detail=""; NewUrl="" })
continue
}
try {
$sc = $shell.CreateShortcut($lnk.FullName)
$target = $sc.TargetPath
if ($target) {
# 単純に先頭へ file:// を付与するだけ(区切り文字やエンコードは変更しない)
$newUrl = "file://$target"
$content = "[InternetShortcut]`r`nURL=$newUrl`r`n"
Set-Content -Path $urlPath -Value $content -Encoding UTF8 -ErrorAction Stop
if ($deleteOriginal) { Remove-Item $lnk.FullName -Force -ErrorAction Stop }
$results.Add([pscustomobject]@{ Path=$lnk.FullName; Status="OK"; Detail=$target; NewUrl=$newUrl })
} else {
$results.Add([pscustomobject]@{ Path=$lnk.FullName; Status="NoTarget"; Detail=""; NewUrl="" })
}
} catch {
$results.Add([pscustomobject]@{ Path=$lnk.FullName; Status="Error"; Detail=$_.Exception.Message; NewUrl="" })
}
}
$results | Export-Csv -Path $logCsv -NoTypeInformation -Encoding UTF8
Write-Host "完了。ログ: $logCsv"
$results | Group-Object Status | Select-Object Name, Count | Format-Table
$resumeSkipExistingについて
大量の.lnkを一括処理していると、途中でエラーが出たりネットワーク断・タイムアウトなどで処理が中断することがあります。そこから最初にやり直すと、既に変換済みのファイルまで再処理してしまい時間がかかったり、意図しない上書きが起きたりする可能性があります。
$resumeSkipExisting = $trueにしておくと、対応する.urlが既に存在する.lnkはスキップし(Status = SkippedExistingとして記録)、未処理分だけを処理する、いわば再開実行ができるようになります。
| 状況 |
$trueのとき |
$falseのとき |
|---|---|---|
.urlが未作成 |
通常通り変換する | 通常通り変換する |
.urlが既に存在する |
スキップする | 上書きで再変換する |
スクリプトを修正した後に全件作り直したい場合は、一時的に$falseにするか、既存の.urlを事前に削除してから実行してください。
ログ出力について
処理結果はStatus列付きでCSVへ出力し、最後に件数を集計します。
-
OK:変換成功 -
NoTarget:.lnkにリンク先が設定されていない -
Error:書き込み失敗など -
ScanError:フォルダ列挙時の権限エラーなど -
SkippedExisting:既に.urlが存在したためスキップ
ErrorやScanErrorが多い場合は、対象フォルダへのアクセス権限を確認する目安になります。
実行前の確認事項
-
file://C:\Users\...のようにバックスラッシュのままfile://を付けた形式は、一般的なfile:///C:/Users/...(スラッシュ区切り・スラッシュ3つ)というfile URIの正規形式とは異なります。実際にブラウザやエクスプローラーで開けるか、数件サンプルを作って実機で動作確認してから一括変換に進むことを推奨します。 -
IntranetFileLinksEnabledのGPO設定が対象端末に適用されていないと、SPO上で.urlをクリックしてもリンクが開けません。
PowerShell 5.1での動作について
上記スクリプトはPowerShell 7限定の構文(-Parallelやnull合体演算子??など)を使用していないため、そのままWindows PowerShell 5.1で動作します。
-
Export-Csv -Encoding UTF8はPS5.1ではBOM付きUTF-8で出力される(PS7はBOMなし)。他ツールでCSVを読ませる場合は注意。 - COMオブジェクト(
WScript.Shell)は.NET Framework版のPS5.1の方が親和性が高く安定して動く。 - 実行ポリシーが未設定の環境では、事前に確認・設定しておく。
Get-ExecutionPolicy -List
# 必要なら(管理者権限で)
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process
まとめ
-
.lnkはブラウザ経由では機能しないため、.url(InternetShortcut形式)へ変換する。 - 今回は複雑なパス変換・マッピングは不要で、リンク先パスの先頭に
file://を付けるだけのシンプルな変換で対応。 -
$resumeSkipExistingで大量件数処理時の再開性を確保。 - 実運用前に、変換後の
.urlが実際にブラウザ・エクスプローラーで開けるか少数サンプルで検証しておくことが重要。