0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

bat&PowerShellでファイル検索ツールをgeminiと作ってみた。

0
Last updated at Posted at 2026-02-15

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 }
    }
}

使い方

  1. 上記コードを保存した .bat ファイルを、検索したいフォルダに置きます。
  2. ダブルクリックで起動します。
  3. 検索ワードを入力してEnter。
  • スペース区切りでAND検索されます。
  1. 結果に出た [番号] を入力すると、そのファイルがあるフォルダを開き、ファイルを自動で選択状態にしてくれます。
  • 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ユーザでファイルの検索に悩んでいるけどインストールが面倒な方、よければ使ってみてください。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?