2025/10/19(日)PowerShellでワイルドカード文字 [ ] を含むパスへのリンクをNew-Itemで作れない問題が修正される予定

PowerShellのワイルドカード文字 [ ] 問題

PowerShellの多くのコマンドレットは [ ] を含んだパスをワイルドカードと解釈します。

Get-Item -Path "[abc].txt"

Name
----
a.txt
b.txt
c.txt

たいていの場合これは望んでいない動作なので、ワイルドカード処理を行わない-LiteralPathを使うことがPowerShellではほぼ必須です。(あまりに残念な失敗仕様……。)

Get-Item -LiteralPath "[abc].txt"

Name
----
[abc].txt

New-Itemでのリンク作成時のワイルドカード文字問題

New-Itemでのリンク作成時の -Target (または -Value) にも同様の問題があります。厄介なことに、-LiteralPathのようなワイルドカード処理を無効化する手段がありません。

New-Item -ItemType SymbolicLink -Path "link.txt" -Target "[abc].txt"

Name
----
link.txt -> a.txt

一応、バッククォート(`)で二重エスケープすれば作れなくもありません。

New-Item -ItemType SymbolicLink -Path "link.txt" -Target "````[abc````].txt"

Name
----
link.txt -> [abc].txt

パスが変数とかに入っている場合はエスケープする関数とかを作るか[WildcardPattern]::Escapeを2回使えば同じ効果。

New-Item -ItemType SymbolicLink -Path "link.txt" -Target ([WildcardPattern]::Escape([WildcardPattern]::Escape("[abc].txt")))

ただし、この方法でも中間パスに [ ] が含まれている場合は失敗します。

New-Item -ItemType SymbolicLink -Path "link.txt" -Target ([WildcardPattern]::Escape([WildcardPattern]::Escape("[xyz]\1\2.txt")))
New-Item: Cannot find path '``[xyz``]\1\2.txt' because it does not exist.
Test-Path -LiteralPath "[xyz]\1\2.txt"
True

結局、一番確実なのは、残念なことに以下の方法です。

& cmd "/c" mklink "link.txt" "[xyz]\1\2.txt"

v7.6.0で修正予定

この問題はIssueに挙がっていて、割と最近TargetをLiteral扱いする修正が入ることになったようです。

v7.6.0-preview.5 Releaseで試したところ直っていました。たぶん次の正式リリース時には修正されそうです。

New-Item -ItemType SymbolicLink -Path "link.txt" -Target "[xyz]\1\2.txt"

Name
----
link.txt -> [xyz]\1\2.txt

2023/09/07(木)ジャンクション(NTFS)の更新日時を変更する

ジャンクション(NTFS)の更新日時を変更する

ちょっとジャンクション(NTFS)の更新日時を書き換えようとしたら、すんなりできなくて結構手間取ったのでメモ。

通常の方法ではリンク先の更新日時が変更される

まずはディレクトリとジャンクションをセットで作成。

$directory = New-Item -Path "test_directory" -ItemType Directory
$junction = New-Item -Value "test_directory" -Path "test_junction" -ItemType Junction
dir

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
d-----        2023/09/07      3:48                test_directory
d----l        2023/09/07      3:48                test_junction

そして、ジャンクションの方に更新日時を設定してみると、ジャンクションではなくリンク先のディレクトリの更新日時が書き換えられてしまいました。

$junction.LastWriteTime = "2000-01-02T03:04:05+09:00"
dir

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
d-----        2000/01/02      3:04                test_directory
d----l        2023/09/07      3:48                test_junction

仕様と言えばそうなのかも知れませんが、それだったら表示される更新日時もリンク先のものでないと一貫性が無いような……。

ジャンクションそのものの更新日時を変更する方法

まずは「Junction LastWriteTime」とかで調べてみると、英語圏で同様の疑問が挙がっていましたが手っ取り早い解決策は無さそうでした。(Windows APIを直接使う必要がありそうな感じ。)

更新日時変更をWindows APIを使って行う場合、CreateFile→SetFileTimeという手順になるのでこっちを調べてみたところ、ジャンクション(リパースポイント)をCreateFileで開くときにOPEN_REPARSE_POINTフラグを付けて開くとリンクを辿らずに開いてくれそうです。

FileTimeコピープログラム作成

OPEN_REPARSE_POINTを指定するにはWindows APIで直接指定するしか無さそうなので、自分でプログラムを作ることにしました。

日時を自由に指定できるようにするには日時文字列の解釈が必要になって面倒なので、他のファイル・フォルダの日時をコピーするプログラムです。これなら、GetFileTimeが使えるので楽。

SetFileTimeがcreationTime/lastAccessTime/lastWriteTimeを扱うので全部コピーしています。(コピーしたくなければNULLを渡せば変更されない。)

手抜きなので失敗してもメッセージとかは出ません(^_^;)

  • ソースコード
    #include <windows.h>
    #include <tchar.h>
    
    int _tmain(int argc, _TCHAR *argv[])
    {
        HANDLE hFrom;
        HANDLE hTo;
    
        FILETIME creationTime, lastAccessTime, lastWriteTime;
    
        if (argc < 3)
        {
            return 1;
        }
    
        //----------------------------------------
        // Get FILETIME from argv[1]
        //----------------------------------------
        hFrom = CreateFile(
            argv[1],
            GENERIC_READ,
            0,
            NULL,
            OPEN_EXISTING,
            FILE_FLAG_BACKUP_SEMANTICS,
            NULL);
        if (hFrom == INVALID_HANDLE_VALUE)
        {
            return 1;
        }
        if (!GetFileTime(hFrom, &creationTime, &lastAccessTime, &lastWriteTime))
        {
            return 1;
        }
        CloseHandle(hFrom);
    
        //----------------------------------------
        // Set FILETIME to argv[2]
        //----------------------------------------
        hTo = CreateFile(
            argv[2],
            GENERIC_READ | GENERIC_WRITE,
            0,
            NULL,
            OPEN_EXISTING,
            FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT,
            NULL);
        if (hTo == INVALID_HANDLE_VALUE)
        {
            return 1;
        }
        if (!SetFileTime(hTo, &creationTime, &lastAccessTime, &lastWriteTime))
        {
            return 1;
        }
        CloseHandle(hTo);
    
        return 0;
    }
    
  • コンパイル
    "C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvars32.bat"
    cl.exe /DUNICODE /D_UNICODE /source-charset:utf-8 CopyFileTime.c
    

ジャンクションへのFileTimeコピー実験

CopyFileTime.exe test_directory test_junction

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
d-----        2000/01/02      3:04                test_directory
d----l        2000/01/02      3:04                test_junction

ちゃんと日時コピーできてました。

コピー元の方はOPEN_REPARSE_POINTを付けずに開いている(リンク先を辿る)ので、

CopyFileTime.exe test_junction test_junction

としても同じ効果のはず。

2023/04/22(土)[Tampermonkey] fetch responseの割り込み取得

[Tampermonkey] fetch responseの横取り取得

Webページがfetchで動的に取得したJSONデータを観測する手段は無いかなと思って見つけた方法。

ソースコード

// ==UserScript==
// @name        Test intercept fetch
// @description Test intercept fetch
// @namespace   Test
// @version     1.0
// @match       https://example.com/*
// @run-at      document-start
// @grant       unsafeWindow
// ==/UserScript==

const proc_fetch_resp = (data, url) => {
    console.log("proc_fetch_resp:" + url, data);
}

//=====================================================
// Intercept fetch
//=====================================================
const origFetch = unsafeWindow.fetch;
//元のfetchを差し替え
unsafeWindow.fetch = async (...args) => {
    console.log("fetch called with args:", args);
    const response = await origFetch(...args);

    //レスポンスをクローンして使う
    response
        .clone()
        .json()
        .then(body => {
            proc_fetch_resp(body, response.url);
        })
        .catch(err => console.error(err))
        ;

    //元のレスポンスを返す
    return response;
}
  • @match は対象サイトのURLを指定
  • @run-at は割り込みたいfetchよりに先にfetch差し替えができるように指定
  • unsafeWindow を使わないとfetch差し替えができない
  • サンプルなのでレスポンスをJSONとして読み取ってconsole.logに出すだけ。

参考URL

2017/04/08(土)ドライブ空き容量をチェックしてトースト通知する

ドライブ空き容量をチェックしてトースト通知する

PowerShellで空き容量チェックスクリプトを書いてみました。Windows 10 のアクションセンターで通知されます(トースト通知)。

check_diskspace.png

ソースコード

# ドライブごとにしきい値を設定
$min_freespace = @{
    "C" = 30GB;
    "D" = 10GB;
    "E" = 50GB;
}

function ShowBalloonTip(
    [string] $tilte = "件名", 
    [string] $body = "本文"
) {
    #[Reference]
    # PowerShell can I use balloons, toasts and notifications?
    #   https://deploywindows.info/2015/12/01/powershell-can-i-use-balloons-toasts-and-notifications/
    # Toasts templates
    #   https://msdn.microsoft.com/en-us/library/windows/apps/hh761494.aspx

    [Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] > $null
    $template = [Windows.UI.Notifications.ToastNotificationManager]::GetTemplateContent([Windows.UI.Notifications.ToastTemplateType]::ToastText02)
    # Convert to .NET type for XML manipuration
    $toastXml = [xml] $template.GetXml()
    # Customize the toast message
    $text = $toastXml.GetElementsByTagName("text")
    $text[0].AppendChild($toastXml.CreateTextNode($tilte)) > $null
    $text[1].AppendChild($toastXml.CreateTextNode($body)) > $null

    # Convert back to WinRT type
    $xml = New-Object Windows.Data.Xml.Dom.XmlDocument
    $xml.LoadXml($toastXml.OuterXml)
    $Toast = [Windows.UI.Notifications.ToastNotification]::new($xml)

    $AppID = 'Microsoft.Explorer.Notification.{3e1c1f24-a023-49cf-98ff-90cdabb9930b}'

    [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier($AppID).Show($Toast)
}

$msg = ""
foreach ($drive_letter in $min_freespace.Keys) {
    $drive = Get-PSDrive $drive_letter
    if ( $drive.Free -lt $min_freespace[$drive_letter] ) {
        $msg += "{0}ドライブ空き容量:{1,0:0.00}GB`n" -f $drive_letter, ($drive.Free/1GB);
    }
}

if ( $msg ) { 
    ShowBalloonTip "空き容量警告" $msg
}

準備

以下のレジストリを登録しないとトースト通知を履歴に残せないようです。(通知自体はされる。)

  • HKEY_CURRENT_USER\SOFTWARE\Microsoft\Windows\CurrentVersion\Notifications\Settings\Microsoft.Explorer.Notification.3e1c1f24-a023-49cf-98ff-90cdabb9930b
    • 値の名前: ShowInActionCenter
    • 値のデータ: 1
    • 種類: REG_DWORD

コマンドで追加するなら、

reg add "HKEY_CURRENT_USER\SOFTWARE\Microsoft\Windows\CurrentVersion\Notifications\Settings\Microsoft.Explorer.Notification.{3e1c1f24-a023-49cf-98ff-90cdabb9930b}" /v ShowInActionCenter /t REG_DWORD /d 1

「Microsoft.Explorer.Notification.3e1c1f24-a023-49cf-98ff-90cdabb9930b」*1のところに特に意味は無く、ソースコード内のIDと一致していれば良いです。

自動実行

タスクスケジューラーに登録すれば、自動実行できます。

  • プリグラム/スクリプト: %SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe
  • 引数の追加(オプション): -ExecutionPolicy RemoteSigned -File スクリプトのパス

ただ、普通に登録するだけではどうしてもウィンドウが表示されてしまうようです。検索するとWSH(VBScriptやJScript)経由でPowerShellを呼び出すことでウィンドウを隠す方法が見つかります。PowerShell単体でできるようにして欲しい(-_-;)

更新履歴

  • 2017/04/08: 不要な記述が残っていたので削除。

*1 : 3e1c1f24-a023-49cf-98ff-90cdabb9930bは[Guid]::NewGuid()で新規取得したGUIDを使っただけ。

Pukiwiki 見出しタイトル化

2017/03/10Programming::PukiWiki

Pukiwiki改造: 見出しタイトル化

最初に出現したレベル1見出しをページタイトル(titleとh1)に反映する改造です。

改造前のconvert_htmlにbodycache用の記述も含まれているので、使っていない場合は無視してください。

  • lib/convert_html.php
    • function convert_html
      • 変更前
        function convert_html($lines)
        {
        	global $vars, $digest;
        	global $bodycache_status;
        	static $contents_id = 0;
        
        	if ( $bodycache_status === 'cached' ) {
        		$contents_id += 90 ;
        	}
        
        	// Set digest
        	$digest = md5(join('', get_source($vars['page'])));
        
        	if (! is_array($lines)) $lines = explode("\n", $lines);
        
        	$body = new Body(++$contents_id);
        	$body->parse($lines);
        
        	return $body->toString();
        }
        
      • 変更後: 「global $title, $page;」と「if ( isset( $body->title ) )~」を追記。
        function convert_html($lines)
        {
        	global $vars, $digest;
        	global $title, $page;
        	global $bodycache_status;
        	static $contents_id = 0;
        	
        	if ( $bodycache_status === 'cached' ) {
        		$contents_id += 90 ;
        	}
        
        	// Set digest
        	$digest = md5(join('', get_source($vars['page'])));
        
        	if (! is_array($lines)) $lines = explode("\n", $lines);
        
        	$body = new Body(++$contents_id);
        	$body->parse($lines);
        	
        	if ( isset( $body->title ) ){
        		$page = $title = htmlsc(trim($body->title));
        	}
        
        	return $body->toString();
        }
        
    • Heading::function Heading
      • 変更前
        	function Heading(& $root, $text)
        	{
        		parent::Element();
        
        		$this->level = min(3, strspn($text, '*'));
        		list($text, $this->msg_top, $this->id) = $root->getAnchor($text, $this->level);
        		$this->insert(Factory_Inline($text));
        		$this->level++; // h2,h3,h4
        	}
        
      • 変更後
        	function Heading(& $root, $text)
        	{
        		parent::Element();
        
        		$this->level = min(3, strspn($text, '*'));
        
        		if( $this->level === 1 && !isset( $root->title ) && $root->id === 1 ){
        			$root->title = preg_replace('|\s*\[#.*|', '', $text);
        			$root->title = preg_replace('|^\*\s*|', '', $root->title);
        			$root->title = preg_replace('|\[\[([^>\]]+)(>[^>\]]+)?\]\]|', '$1', $root->title);
        		}
        
        		list($text, $this->msg_top, $this->id) = $root->getAnchor($text, $this->level);
        		$this->insert(Factory_Inline($text));
        		$this->level++; // h2,h3,h4
        	}