Windowsのエクスプローラー標準の検索、遅くないですか?
かといって常駐するソフトを入れるのも社内規定とかで面倒くさいし、インデックス作成で待たされるのも嫌だなぁ…なんて
というわけで、「余計なソフト不要」「バッチファイル1つで完結」「爆速」 な検索ツールをGemini君にお願いして作ってもらいました。
個人的にかなり使い勝手が良かったので共有します。
中身は Batch + PowerShell のハイブリッド構成です。
どんなツール?
-
セットアップ不要:コードをコピペして
.batにするだけ。 -
速度:PowerShellの
Get-ChildItemではなく .NETのクラスを直接叩いているのでかなり速いです。 -
除外設定:
node_modulesや.gitなどを自動でスキップする「高速モード」と、全部探す「全検索モード」があります - 対応環境:windows10以降であれば使えると思います。
ソースコード
以下のコードをメモ帳などにコピペして、拡張子を .bat (例:スーパー賢作くん.bat)にして保存してください。文字コードは ANSI (Shift-JIS) でも動きますが、念のため UTF-8 (BOMなし) で保存するのが無難です。
<# :
@echo off
cls
set "MY_DIR=%~dp0"
powershell -NoProfile -ExecutionPolicy Bypass -Command "Invoke-Expression ([System.IO.File]::ReadAllText('%~f0'))"
exit /b
#>
# --- P/Invoke (ファイル操作用) ---
try {
$code = @"
using System;
using System.Runtime.InteropServices;
public class ShellUtils {
[DllImport("shell32.dll", SetLastError = true)]
public static extern int SHOpenFolderAndSelectItems(IntPtr pidlFolder, uint cidl, IntPtr apidl, uint dwFlags);
[DllImport("shell32.dll", CharSet = CharSet.Unicode)]
public static extern IntPtr ILCreateFromPath(string pszPath);
[DllImport("ole32.dll")]
public static extern void CoTaskMemFree(IntPtr pv);
[DllImport("user32.dll")]
public static extern bool SetForegroundWindow(IntPtr hWnd);
[DllImport("kernel32.dll")]
public static extern IntPtr GetConsoleWindow();
}
"@
Add-Type -TypeDefinition $code -ErrorAction SilentlyContinue
} catch {}
# --- ハイライト関数 ---
function Write-Highlight {
param(
[string]$Text,
[string[]]$Keywords,
[ConsoleColor]$BaseColor
)
if ($Keywords.Count -eq 0) {
Write-Host $Text -NoNewline -ForegroundColor $BaseColor
return
}
$escapedKeys = $Keywords | ForEach-Object { [regex]::Escape($_) }
$pattern = "(" + ($escapedKeys -join "|") + ")"
$parts = $Text -split $pattern
foreach ($part in $parts) {
if ($part -eq "") { continue }
$isMatch = $false
foreach ($k in $Keywords) {
if ($part.Equals($k, [StringComparison]::OrdinalIgnoreCase)) { $isMatch = $true; break }
}
if ($isMatch) {
Write-Host $part -NoNewline -ForegroundColor White -BackgroundColor DarkMagenta
} else {
Write-Host $part -NoNewline -ForegroundColor $BaseColor
}
}
}
# --- 設定 ---
$scriptPath = $env:MY_DIR.TrimEnd('\')
$defaultExclude = @(".git", ".svn", ".vs", "node_modules", "bin", "obj", "packages", "dist", "build")
$isExcludeMode = $true
$results = [System.Collections.Generic.List[string]]::new()
function Show-Header {
Clear-Host
Write-Host " ==================================================" -ForegroundColor Yellow
Write-Host " ス ー パ ー 賢 作 く ん ver2.2" -ForegroundColor Yellow
Write-Host " ==================================================" -ForegroundColor Yellow
}
Show-Header
while ($true) {
Write-Host ""
Write-Host " --------------------------------------------------"
Write-Host " 検索場所: $scriptPath"
if ($isExcludeMode) {
Write-Host " 除外設定: " -NoNewline
Write-Host "ON (システムフォルダ除外中)" -ForegroundColor DarkCyan -NoNewline
Write-Host " -> 'all' で全検索モード" -ForegroundColor Gray
} else {
Write-Host " 除外設定: " -NoNewline
Write-Host "OFF (全てのフォルダを検索)" -ForegroundColor Red -NoNewline
Write-Host " -> 'fast' で高速モード" -ForegroundColor Gray
}
Write-Host " [番号] : 開く [c番号] : パスコピー [exit] : 終了"
Write-Host " --------------------------------------------------"
$inputLine = Read-Host " 検索ワード"
if ($inputLine -eq "exit") { break }
if ([string]::IsNullOrWhiteSpace($inputLine)) { Show-Header; continue }
if ($inputLine -eq "all") { $isExcludeMode = $false; Show-Header; Write-Host " >> 全検索モード (制限なし)" -ForegroundColor Red; continue }
if ($inputLine -eq "fast") { $isExcludeMode = $true; Show-Header; Write-Host " >> 高速モード (除外あり)" -ForegroundColor DarkCyan; continue }
# --- アクション ---
$tempInput = $inputLine
$zen = "0123456789"; $han = "0123456789"
for ($i=0; $i -lt $zen.Length; $i++) { $tempInput = $tempInput.Replace($zen[$i], $han[$i]) }
if ($tempInput -match "^(c?)(\d+)$" -and $results.Count -gt 0) {
$mode = $Matches[1]
$index = [int]$Matches[2] - 1
if ($index -ge 0 -and $index -lt $results.Count) {
$targetPath = $results[$index]
if ($mode -eq "c") {
Set-Clipboard -Value $targetPath
Write-Host " コピーしました: $targetPath" -ForegroundColor Blue
} else {
Write-Host " フォルダを開いています..." -ForegroundColor Gray
$pidl = [ShellUtils]::ILCreateFromPath($targetPath)
if ($pidl -ne [IntPtr]::Zero) {
[ShellUtils]::SHOpenFolderAndSelectItems($pidl, 0, [IntPtr]::Zero, 0) | Out-Null
[ShellUtils]::CoTaskMemFree($pidl)
}
}
} else { Write-Host " 番号が無効です" -ForegroundColor Red }
continue
}
# --- 検索実行 ---
Show-Header
$keywords = $inputLine -split "\s+" | Where-Object { $_ -ne "" }
Write-Host " >> [$($keywords -join ' ')] の検索結果" -ForegroundColor Blue
Write-Host ""
$results.Clear()
if (-not [string]::IsNullOrEmpty($scriptPath)) {
try {
$fullPath = [System.IO.Path]::GetFullPath($scriptPath)
$stack = [System.Collections.Generic.Stack[string]]::new()
$stack.Push($fullPath)
$counter = 1
while ($stack.Count -gt 0) {
$currentDir = $stack.Pop()
try {
foreach ($file in [System.IO.Directory]::EnumerateFiles($currentDir)) {
$fName = [System.IO.Path]::GetFileName($file)
# ファイル名($fName)のみを対象にチェックする
$matchAll = $true
foreach ($k in $keywords) {
if ($fName.IndexOf($k, [StringComparison]::OrdinalIgnoreCase) -lt 0) {
$matchAll = $false; break
}
}
if ($matchAll) {
$results.Add($file)
# 1行目: [番号] + ファイル名 (黄+ハイライト)
Write-Host "[$counter] " -NoNewline
Write-Highlight -Text $fName -Keywords $keywords -BaseColor Yellow
Write-Host ""
# 2行目: フォルダパス (白) ※検索対象外なのでハイライトもしない
$dPath = [System.IO.Path]::GetDirectoryName($file)
if (-not $dPath.EndsWith("\")) { $dPath += "\" }
Write-Host " $dPath" -ForegroundColor White
Write-Host ""
$counter++
}
}
foreach ($dir in [System.IO.Directory]::EnumerateDirectories($currentDir)) {
$dName = [System.IO.Path]::GetFileName($dir)
if ($isExcludeMode) {
if ($defaultExclude -notcontains $dName) { $stack.Push($dir) }
} else {
$stack.Push($dir)
}
}
} catch {}
}
if ($counter -eq 1) { Write-Host " 見つかりませんでした。" -ForegroundColor Gray }
}
catch { Write-Host " エラー: $_" -ForegroundColor Red }
}
}
使い方
- 上記コードを保存した
.batファイルを、検索したいフォルダに置きます。 - ダブルクリックで起動します。
- 検索ワードを入力してEnter。
- スペース区切りでAND検索されます。
- 結果に出た
[番号]を入力すると、そのファイルがあるフォルダを開き、ファイルを自動で選択状態にしてくれます。
-
c7のようにcをつけるとパスをクリップボードにコピーします。
※あくまで自分用として生成してもらったコードなので、使用は自己責任でお願いします。
技術的な解説(AIがどう書いたか)
中身を見て「おっ」と思ったポイントをいくつか解説します。
gemini Proにあれこれ無茶振りした結果、結構面白い構成になっています。
1. バッチとPowerShellのハイブリッド構造
冒頭のこの部分です。
<# :
@echo off
...
powershell ... "Invoke-Expression ([System.IO.File]::ReadAllText('%~f0'))"
exit /b
#>
これは「Polyglot(多言語)」なスクリプトの常套手段らしいです。
バッチファイルとしては <# : は「リダイレクトとして無視される行」として処理され、そのままPowerShellを呼び出します。
一方、PowerShellとして読み込まれると <# ... #> はコメントアウト扱いになるので、エラーにならずに以降のPowerShellコードが実行されるという仕組みです。
これで「拡張子は .bat だけど中身はフル機能のPowerShell」を実現しています。
2. P/Invokeで「フォルダを開いてファイルを選択」
検索結果の番号を選んだときの挙動は、単なる Invoke-Item(ファイル実行)や Explorer.exe 起動ではバグってしまったので
C# のコードを Add-Type で埋め込んで、Win32 APIの SHOpenFolderAndSelectItems を叩いています。
[DllImport("shell32.dll", SetLastError = true)]
public static extern int SHOpenFolderAndSelectItems(...);
これにより、「フォルダを開くだけでなく、対象のファイルを反転表示(選択状態)にする」 という、Windows標準検索と同じ挙動を再現しています。
PowerShell標準コマンドだけだとこの「選択状態で開く」が意外と面倒なんですが、ここをサクッとC#埋め込みで解決してくるあたりが賢いです。
3. 高速化のための .NET クラス直叩き
PowerShellでファイル検索というと Get-ChildItem -Recurse が一般的ですが、ファイル数が多いと遅いです。
このスクリプトでは [System.IO.Directory]::EnumerateFiles を使っています。
foreach ($file in [System.IO.Directory]::EnumerateFiles($currentDir)) { ... }
これによって、全ファイルを配列に展開してから処理するのではなく、見つかった順に逐次処理(列挙)していくので、メモリ消費も少なく動作が軽快です。
また、再帰処理(リカーシブ)を自前の Stack を使った while ループで書いているため、フォルダ階層が深くてもスタックオーバーフローしにくい設計になっています。
4. node_modules 回避
開発者にとって一番重いフォルダ、node_modules や .git をデフォルトで除外する設定が入っています。
$defaultExclude = @(".git", ".svn", ".vs", "node_modules", "bin", "obj", "packages", "dist", "build")
ここを書き換えれば自分の環境に合わせられますが、デフォルトのままでも大抵のプロジェクトフォルダで快適に動くようになってるかなと思います。
まとめ
「ちょっとファイル探したいだけなのに」という時に、サッと起動して使えるので重宝しています。
繰り返しになりますが、使う際は自己責任で。。。
geminiくんにあれこれ命令して書かせたものなので、改造、再配布等はご自由にどうぞ。
windowsユーザでファイルの検索に悩んでいるけどインストールが面倒な方、よければ使ってみてください。