指定するファイルに対してコメントを記録するコンソールアプリです。
一つの外部コマンドで、コメントの追加・変更・削除を行います。
以前記事にしたC#で作成したプログラムをPowerShellスクリプトにしてみました。若干機能を追加していて手元の環境ではC#版より高速に動作しています。
導入方法
以下のスクリプトをテキストエディタなどで$PROFILE
へ追記
スクリプト
# ===== Comment tools in $PROFILE =====
# ルートとインデックスファイル
$Global:CommentRoot = Join-Path ([Environment]::GetFolderPath('MyDocuments')) 'comment'
$Global:CommentIndexFile = Join-Path $Global:CommentRoot 'index.txt' # 1行=正規化パス
$Global:Utf8 = [System.Text.UTF8Encoding]::new($false)
# メモリ常駐インデックス: hashHex(lower) -> canonicalPath
$Global:CommentIndex = @{}
# 共有ロック(複数シェル/プロセス対策の簡易Mutex)
function Invoke-WithCommentIndexLock([scriptblock]$Action) {
$m = [System.Threading.Mutex]::new($false, 'Global\CommentIndexLock')
try { $null = $m.WaitOne(5000); & $Action } finally { $m.ReleaseMutex(); $m.Dispose() }
}
function ConvertTo-CanonicalPath([string]$Path) {
[System.IO.Path]::GetFullPath($Path).TrimEnd('\','/').ToLowerInvariant()
}
function Get-Md5HexLower([string]$Text) {
$md5 = [System.Security.Cryptography.MD5]::Create()
try {
($md5.ComputeHash([Text.Encoding]::UTF8.GetBytes($Text)) | ForEach-Object { $_.ToString('x2') }) -join ''
} finally { $md5.Dispose() }
}
function Get-CommentFilePathByHash([string]$HashHex) {
Join-Path $Global:CommentRoot ("{0}.txt" -f $HashHex)
}
function Get-CommentFilePathByCanonical([string]$Canonical) {
Get-CommentFilePathByHash (Get-Md5HexLower $Canonical)
}
# 起動時ロード(index.txt を読み→各行のMD5を計算→連想配列へ)
function Initialize-CommentEnv {
New-Item -ItemType Directory -Path $Global:CommentRoot -Force | Out-Null
if (-not (Test-Path $Global:CommentIndexFile)) {
New-Item -ItemType File -Path $Global:CommentIndexFile -Force | Out-Null
}
$Global:CommentIndex.Clear()
foreach ($line in Get-Content -LiteralPath $Global:CommentIndexFile -Encoding UTF8) {
$canon = $line.Trim()
if (-not $canon -or $canon.StartsWith('#')) { continue }
$hash = Get-Md5HexLower $canon
$Global:CommentIndex[$hash] = $canon
}
}
Initialize-CommentEnv
# 追記(追加のみ)&メモリ更新
function Add-CommentIndex([string]$Canonical) {
$hash = Get-Md5HexLower $Canonical
$Global:CommentIndex[$hash] = $Canonical
Invoke-WithCommentIndexLock {
Add-Content -LiteralPath $Global:CommentIndexFile -Value $Canonical -Encoding UTF8
}
}
# 削除はテキストに追記(メモリからは外す)。定期的に圧縮で掃除。
function Remove-CommentIndex([string]$Canonical) {
$hash = Get-Md5HexLower $Canonical
[void]$Global:CommentIndex.Remove($hash)
Invoke-WithCommentIndexLock {
Add-Content -LiteralPath $Global:CommentIndexFile -Value ("#DEL " + $Canonical) -Encoding UTF8
}
}
# 圧縮(index.txt を作り直し:重複/実体なしを除去)
function Compress-CommentIndex {
$tmp = $Global:CommentIndex.Keys |
Where-Object { Test-Path (Get-CommentFilePathByHash $_) } |
ForEach-Object { $Global:CommentIndex[$_] } |
Sort-Object -Unique
$tmpPath = "$($Global:CommentIndexFile).tmp"
Set-Content -LiteralPath $tmpPath -Value ($tmp -join [Environment]::NewLine) -Encoding UTF8
Invoke-WithCommentIndexLock {
Move-Item -Force $tmpPath $Global:CommentIndexFile
}
Initialize-CommentEnv
}
# --- コア機能(C#版と互換IF) ---
function Get-PathComment {
[CmdletBinding()]
param([Parameter(ValueFromPipeline)][string[]]$Path, [switch]$Raw)
process {
foreach ($p in $Path) {
if (-not $p) { continue }
$canon = ConvertTo-CanonicalPath $p
$cfile = Get-CommentFilePathByCanonical $canon
$text = (Test-Path $cfile) ? [IO.File]::ReadAllText($cfile, $Global:Utf8) : $null
if ($Raw -and $Path.Count -eq 1) { $text }
else { [pscustomobject]@{ Path=$p; Canonical=$canon; Comment=$text } }
}
}
}
function Set-PathComment {
[CmdletBinding(SupportsShouldProcess)]
param([Parameter(Mandatory,ValueFromPipeline)][string[]]$Path,
[Parameter(Mandatory)][AllowEmptyString()][string]$Comment)
process {
foreach ($p in $Path) {
$canon = ConvertTo-CanonicalPath $p
$cfile = Get-CommentFilePathByCanonical $canon
$tmp = "$cfile.tmp"
if ($PSCmdlet.ShouldProcess($p, "Write comment")) {
[IO.File]::WriteAllText($tmp, $Comment, $Global:Utf8)
if (Test-Path $cfile) { [IO.File]::Replace($tmp, $cfile, $null) } else { [IO.File]::Move($tmp, $cfile) }
Add-CommentIndex $canon
[pscustomobject]@{ Path=$p; Comment=$Comment }
}
}
}
}
function Remove-PathComment {
[CmdletBinding(SupportsShouldProcess)]
param([Parameter(Mandatory,ValueFromPipeline)][string[]]$Path)
process {
foreach ($p in $Path) {
$canon = ConvertTo-CanonicalPath $p
$cfile = Get-CommentFilePathByCanonical $canon
if ((Test-Path $cfile) -and $PSCmdlet.ShouldProcess($p,"Delete comment")) {
Remove-Item $cfile -Force
Remove-CommentIndex $canon | Out-Null
[pscustomobject]@{ Path=$p; Deleted=$true }
}
}
}
}
function comment {
[CmdletBinding()]
param([Parameter(Mandatory,Position=0,ValueFromPipeline)][string]$Path,
[Parameter(Position=1)][AllowNull()][AllowEmptyString()][string]$Text)
process {
if ($PSBoundParameters.ContainsKey('Text')) {
if ($null -ne $Text -and $Text.Length -eq 0) { $Path | Remove-PathComment | Out-Null }
else { $Path | Set-PathComment -Comment $Text | Out-Null }
} else {
Get-PathComment -Path $Path -Raw
}
}
}
# 全文検索:comment/*.txt を走査 → ファイル名(=hash)をインメモリ逆引き
function Search-Comment {
[CmdletBinding()]
param(
[Parameter(Mandatory)][string]$Query,
[switch]$IgnoreCase = $true, # 既定は大文字小文字を無視
[switch]$All # 全マッチを出す(既定: 各ファイル1件だけ)
)
# Select-String 用のパラメータはハッシュでスプラット
$ss = @{
Pattern = $Query
SimpleMatch = $true # 正規表現ではなく単純一致
Encoding = 'utf8'
}
if (-not $IgnoreCase) { $ss['CaseSensitive'] = $true }
if (-not $All) { $ss['List'] = $true } # 1ファイルにつき最初の1件だけ
Get-ChildItem -LiteralPath $Global:CommentRoot -Filter *.txt -File |
Select-String @ss |
ForEach-Object {
$hash = [IO.Path]::GetFileNameWithoutExtension($_.Path).ToLowerInvariant()
if ($Global:CommentIndex.ContainsKey($hash)) {
[pscustomobject]@{
Path = $Global:CommentIndex[$hash]
Match = if ($_.Matches) { $_.Matches[0].Value } else { $Query }
Line = $_.Line
}
}
}
}
# ── 1) 出力に Comment 列を付けるデコレータ ─────────────────────────────
function Add-CommentColumn {
[CmdletBinding()]
param(
[Parameter(Mandatory, ValueFromPipeline)]
[System.IO.FileSystemInfo]$InputObject,
# インデックス($Global:CommentIndex)に無いものは出力しない
[switch]$OnlyWithComment,
# インデックスに無くてもディスク確認して読む(インデックスが古い時用)
[switch]$ScanIfMissing
)
begin {
$utf8 = [System.Text.UTF8Encoding]::new($false)
$idx = $Global:CommentIndex
}
process {
$p = $InputObject.FullName
$canon = ConvertTo-CanonicalPath $p
$hash = Get-Md5HexLower $canon
$hit = $idx.ContainsKey($hash)
if (-not $hit -and -not $ScanIfMissing) {
if (-not $OnlyWithComment) {
$InputObject | Add-Member -NotePropertyName Comment -NotePropertyValue $null -PassThru
}
return
}
$cfile = Get-CommentFilePathByHash $hash
if (Test-Path $cfile) {
$txt = [IO.File]::ReadAllText($cfile, $utf8)
$InputObject | Add-Member -NotePropertyName Comment -NotePropertyValue $txt -PassThru
} elseif (-not $OnlyWithComment) {
$InputObject | Add-Member -NotePropertyName Comment -NotePropertyValue $null -PassThru
}
}
}
# 使い方(素直):
# Get-ChildItem -File | Add-CommentColumn | Format-Table Name,Length,Comment -Auto
# 便利エイリアス
Set-Alias cmt comment -Scope Global
function cmt-ls {
[CmdletBinding()]
param(
[string]$Path='.'
)
Get-ChildItem -Path $Path |
Select-Object FullName, Length, LastWriteTime, @{n='Comment';e={(comment $_.FullName)}}
}
# ===== end of comment tools =====
コメント