全角文字が大量に含まれているとなぜか例外が出る

PowerShell 7 で日誌をgrepするとエラーで落ちることがあって、何でだろうと思い調べたら、どうもバグっぽい。

全角文字が画面バッファーの幅以上に含まれていると例外が出る。

$Host.UI.RawUI.BufferSize.Width
# 160

"`u{5b57}" * 159 | Out-File foo.txt
Select-String "`u{5b57}" foo.txt
# foo.txt:1:字字字字字字字字字字字字字字 ...

"`u{5b57}" * 160 | Out-File bar.txt
Select-String "`u{5b57}" bar.txt
# out-lineoutput: startIndex cannot be larger than length of string. (Parameter 'startIndex')

f:id:itasuke:20210723144734p:plain

ASCIIならいくら含まれていても問題なし。一行に含まれている全角文字の数だけが問題。

"xxx `u{5b57} xxx " * 159 | Out-File foo.txt
Select-String "`u{5b57}" foo.txt
# foo.txt:1:xxx 字 xxx xxx 字 xxx xxx 字 xxx ...

"xxx `u{5b57} xxx " * 160 | Out-File bar.txt
Select-String "`u{5b57}" bar.txt
# out-lineoutput: startIndex cannot be larger than length of string. (Parameter 'startIndex')

f:id:itasuke:20210723144732p:plain

回避法

手動で文字列化してください。こんな感じ。

"`u{5b57}" * 160 | Out-File bar.txt
Select-String "`u{5b57}" bar.txt | Out-String
# bar.txt:1:字字字字字字字字字字字字字字 ...

f:id:itasuke:20210723144729p:plain

おわり🌱

去年の時点で似ような報告が上がってるから、ま、すぐに直ると思うけどね。
github.com

デフォルトのエイリアスとかぶってるコマンド

コマンドプロンプトで使っていたコマンドを PowerShell で実行しようとすると、時々エイリアスとかぶってるやつがあることに気づく。そういう時は後ろに .exe を付ければ実行できる。PowerShellに専用のコマンドレットもあるのだが、その場でタイプして実行する用途としてはそれほど使いやすくはなかったりする。

sc.exe

サービスを制御するためのコマンドです(TechNet)。

Set-Contentエイリアスとかぶってます。

サービスの制御なら *-Service 系のコマンドで代替できます。

# サービスの状態を確認
sc.exe query sysmain
↓
# PowerShell
Get-Service sysmain | Format-List
gsv sysmain | fl
# サービスの停止
sc.exe stop spooler
↓
# PowerShell
Stop-Service spooler
spsv spooler

where.exe

ファイルの検索コマンドです(TechNet)。

Where-Objectエイリアスとかぶってます。

ファイルを探すなら Get-CommandGet-ChildItem で代替できます。

# パスの通っている場所からコマンドを検索
where.exe notepad
↓
# PowerShell
Get-Command notepad -all
gcm notepad -all
# 特定のディレクトリ以下からファイルを検索
where.exe /r . *.txt
↓
# PowerShell
Get-ChildItem -Recurse *.txt | Select-Object FullName
gci -re *.txt | select fullname

fc.exe

ファイルの中身を比較するコマンドです(TechNet)。

Format-Customエイリアスとかぶってます。

Compare-Object でも一応ファイルの比較はできますが、fc.exe ほど出力される情報は多くありません。

Compare-Object (Get-Content foo.txt) (Get-Content bar.txt)
compare (gc foo.txt) (gc bar.txt)

そもそも実際にやりたいことは、ファイルの中身が同じか否か確認したいだけでしょうから、その場合はファイルのハッシュ値を計算させた方が手っ取り早いでしょう。

Get-FileHash foo.txt, bar.txt

winreg でレジストリを読み書きするサンプルコード

標準ライブラリに入ってる winreg を使うとレジストリの読み書きができるけど、こいつは Windows API を薄く包んでいるだけなのでリファレンスを見ても使い方がさっぱりわからないという。

いろいろ調べて、おそらくこうするのが正しいと思われるサンプルコードを書いた。

目次

値の読み込み

# Python 3.6
import winreg
path = r'Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders'
key = winreg.OpenKeyEx(winreg.HKEY_CURRENT_USER, path)
data, regtype = winreg.QueryValueEx(key, 'Personal')
print('種類:', regtype)
print('データ:', data)
winreg.CloseKey(key)  # key.Close() と書いても同じ

OpenKey と OpenKeyEx はまったく同じです。好きな方を使ってください。両方とも内部では Windows APIRegOpenKeyEx を使っています。

QueryValue と QueryValueEx は中身が違います。Ex がつかない方は16bit時代の遺物なので使いません。

値の書き込み

import winreg
path = r'Software\7-Zip\FM'
key = winreg.OpenKeyEx(winreg.HKEY_CURRENT_USER, path, access=winreg.KEY_WRITE)
winreg.SetValueEx(key, 'PanelPath0', 0, winreg.REG_SZ, 'C:\Program Files')
winreg.CloseKey(key)

値をいじる場合は、access=winreg.KEY_WRITE をつけてください。デフォルトでは KEY_READ なので書き込めません。OpenKeyEx の代わりに後述のCreateKeyExを使ってもいいです。

Ex の付かない SetValue は16bit時代の遺物なので使いません。SetValueEx を使ってください。

コンテキスト・マネージャ

import winreg
with winreg.OpenKeyEx(winreg.HKEY_CURRENT_USER, r'Software') as key:
    pass  # key を使った処理をここで行う

PyHKEY 型は with 文と一緒に使うと自動で CloseKey してくれます。

キーの作成

import winreg
newkey = winreg.CreateKeyEx(winreg.HKEY_CURRENT_USER, r'Software\__test__')
winreg.CloseKey(newkey)

CreateKeyEx は作ったキーを開いて返すので、閉じるのを忘れないように。

Ex の付かない CreateKey は16bit時代の遺物なので使いません。

OpenKeyEx と CreateKeyEx の違い

OpenKeyEx → これから開こうとするキーは既に存在しているはず。もし存在しなければエラー。値の閲覧用として使いやすい。
CreateKeyEx → これから開こうとするキーは存在しないかもしれない。もし存在しなければ自動で作成する。値の書き込み用として使いやすい。

import winreg

path = r'Software\__test2__'

with winreg.CreateKeyEx(winreg.HKEY_CURRENT_USER, path) as key:
    winreg.SetValueEx(key, 'name1', 0, winreg.REG_SZ, 'data1')
    print('キーを開いて値を書き込んだよ')

with winreg.OpenKeyEx(winreg.HKEY_CURRENT_USER, path) as key:
    data, _ = winreg.QueryValueEx(key, 'name1')
    print('読み取ったデータ:', data)

値の削除

import winreg
path = r'Software\__test2__'
with winreg.OpenKeyEx(winreg.HKEY_CURRENT_USER, path, access=winreg.KEY_SET_VALUE) as key:
    winreg.DeleteValue(key, 'name1')

キーの削除

import winreg
winreg.DeleteKey(winreg.HKEY_CURRENT_USER, r'Software\__test2__')

DeleteKey と DeleteKeyEx は中身が違いますが、オプション機能を使わなければ同じはずです。

参考:32bit プロセス(WOW64 プロセス) から、64bit プロセス用のレジストリキーの参照

値の列挙

2つのやり方があるので、どちらか好きな方を選んでください。

1つ目は、予めキーの数を調べてから取得する方法です。

import winreg
path = r'Control Panel\Cursors'
with winreg.OpenKey(winreg.HKEY_CURRENT_USER, path) as key:
    n_subkeys, n_subvalues, timestamp = winreg.QueryInfoKey(key)
    for i in range(n_subvalues):
        name, data, regtype = winreg.EnumValue(key, i)
        print('{:>25s} → {}'.format(name, data))

2つ目は、エラーになるまで取得を繰り返す方法です。

import winreg
import itertools

ERROR_NO_MORE_ITEMS = 259
path = r'Control Panel\Cursors'

with winreg.OpenKey(winreg.HKEY_CURRENT_USER, path) as key:
    for i in itertools.count():
        try:
            name, data, regtype = winreg.EnumValue(key, i)
        except WindowsError as err:
            if err.winerror == ERROR_NO_MORE_ITEMS:
                break
            raise
        print('{:>25s} → {}'.format(name, data))

キーの列挙

これも2つのやり方があるので、どちらか好きな方を選んでください。

import winreg
path = r'Software\Microsoft\Windows\CurrentVersion\Uninstall'
with winreg.OpenKey(winreg.HKEY_CURRENT_USER, path) as key:
    n_subkeys, n_subvalues, timestamp = winreg.QueryInfoKey(key)
    for i in range(n_subkeys):
        subkey = winreg.EnumKey(key, i)
        print('-', subkey)
import winreg
import itertools

ERROR_NO_MORE_ITEMS = 259
path = r'Software\Microsoft\Windows\CurrentVersion\Uninstall'

with winreg.OpenKey(winreg.HKEY_CURRENT_USER, path) as key:
    for i in itertools.count():
        try:
            subkey = winreg.EnumKey(key, i)
        except WindowsError as err:
            if err.winerror == ERROR_NO_MORE_ITEMS:
                break
            raise
        print('-', subkey)

ちなみに、キーや値を列挙しながら削除していく時は、インデックスを逆順に回します。前からやるとインデックス番号がずれて困ります。

既定値

import winreg

# 読み
with winreg.OpenKeyEx(winreg.HKEY_CLASSES_ROOT, r'Python.File') as key:
    name = ''  # Noneでも大丈夫
    data, regtype = winreg.QueryValueEx(key, name)
    print(data)

# 書き
with winreg.OpenKeyEx(winreg.HKEY_CLASSES_ROOT, r'Python.File', access=winreg.KEY_SET_VALUE) as key:
    name = ''  # Noneでも大丈夫
    winreg.SetValueEx(key, name, 0, winreg.REG_SZ, 'ぱいそん ふぁいる')

名前欄を空にすれば既定値の読み書きができます。

後ろに Ex の付かない QueryValue と SetValue を使っても既定値は操作できますが、この2つは中のAPIが古いので使いません。

中の人のブログによれば、レジストリに既定値(もしくは既定の値)が存在するのは歴史的な理由によるものなんだそうだ。大昔の Windows では、キーと値は1対1に対応していた。しかし32ビットの Windows では1つのキーで複数の値を持てるようにした。それで値同士を識別するのに「値の名前」が作られ、さらに従来のキーだけでアクセスしていた値は「既定値」になったということらしい。PowerShellレジストリの値がプロパティ扱いされるのも、この辺りに理由がありそうな気がする。

REG_EXPAND_SZ

import winreg
with winreg.OpenKey(winreg.HKEY_CURRENT_USER, r'Environment') as key:
    data, regtype = winreg.QueryValueEx(key, 'temp')
    print('展開前:', data)
    if regtype == winreg.REG_EXPAND_SZ:
        data_exp = winreg.ExpandEnvironmentStrings(data)
        print('展開後:', data_exp)

REG_EXPAND_SZ は、環境変数を含むかもしれない文字列です。環境変数の展開は自分でやります。

REG_BINARY

import winreg
path = r'Software\__test__'

# 書き込み
with winreg.CreateKeyEx(winreg.HKEY_CURRENT_USER, path) as key:
    winreg.SetValueEx(key, 'name1', 0, winreg.REG_BINARY, b'binary data here')

# 読み込み
with winreg.OpenKeyEx(winreg.HKEY_CURRENT_USER, path) as key:
    data, _ = winreg.QueryValueEx(key, 'name1')
    print('読み取ったデータ:', data)

bytes型です。

REG_MULTI_SZ

import winreg
path = r'Software\__test__'

# 書き込み
with winreg.CreateKeyEx(winreg.HKEY_CURRENT_USER, path) as key:
    L = ['foo', 'bar', 'baz']
    winreg.SetValueEx(key, 'name2', 0, winreg.REG_MULTI_SZ, L)

# 読み込み
with winreg.OpenKeyEx(winreg.HKEY_CURRENT_USER, path) as key:
    data, _ = winreg.QueryValueEx(key, 'name2')
    print('読み取ったデータ:', data)

文字列のリストです。空文字列や \0 を含む文字列を混ぜるとバグります。

サブキーを開く

import winreg

k0 = winreg.HKEY_CURRENT_USER
k1 = winreg.OpenKeyEx(k0, 'Software')
k2 = winreg.OpenKeyEx(k1, 'Microsoft')
k3 = winreg.OpenKeyEx(k2, 'Windows')
k4 = winreg.OpenKeyEx(k3, 'CurrentVersion\Run')

for i in k1, k2, k3, k4:
    winreg.CloseKey(i)

このように、あるキーのサブキーを順繰りに開いていくことができます。CreateKeyEx でも同様です。

参考

Out-GridView は選択画面としても使える

Out-GridView を使うとコマンドレットの出力をGUIに表示できるわけだが、PassThru パラメーターを指定することで、オブジェクトの「選択画面」としても機能します。

# 例:カレントディレクトリのファイル一覧を表示して削除するファイルを選ばせる
Get-ChildItem | Out-GridView -PassThru | Remove-Item
# 省略形
gci | ogv -pass | ri

f:id:itasuke:20180103120907p:plain

Ctrl や Shift キーを押しながら行をポチポチ選んでから OK ボタンを押す。すると選んだ項目が Remove-Item に渡されてファイルが削除されるという寸法です。

ちなみに、Windows のリストビューにおいて Ctrl は一行選択、Shift は範囲選択です。

Core にはないよ(追記:2020/01/03)

PowerShell Core 6.x では、なぜか使えなくなっています(コアだから?)。ドキュメントの注意書きによれば PowerShell 7 で復活する予定です。

Firefoxで文字化けするよくある理由

時々Firefoxでページが文字化けするサイトがあるけど、原因を調査すると95%くらいは Content-Type の設定が間違ってました。それもHTMLソースの方じゃなくて、HTTPヘッダーの方が。

普通、文字コードを指定するとしたらHTMLでこう書くと思うけど、

<!-- HTML5 -->
<meta charset="utf-8" />

<!-- HTML4 -->
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">

HTTPのレスポンスヘッダーでも文字コードは指定できます。

GET /foo.html HTTP/1.1
Host: www.example.com
Connection: close

HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8  ←コレ!!

で、もしmetaタグとHTTPヘッダーの文字コード指定が異なる場合は、HTTPヘッダーの方を優先するようです。

HTTPヘッダーによる指定の方が文書内でのmeta指定よりも優先度が高い

https://www.w3.org/International/questions/qa-html-encoding-declarations.ja

WebブラウザがHTML文書の文字コードを判定する際には、このメディアタイプのcharsetパラメータを最優先に参照する

http://www.atmarkit.co.jp/ait/articles/0412/25/news006.html

文字化けに至るまでの、よくありがちな流れはこんな感じです。

  • 以前はShift_JISEUC-JPでHTMLを記述していた。
  • 最近になって新しいコンテンツはUTF-8で書くようにした。
  • サーバーの設定も変えて、charset=utf8 を付けるようにした。
  • 古いコンテンツが文字化けし始めたことに気づかない。

とにかくコンテンツを見れるようにする

閲覧者の方で文字コードを変更する方法ですが、この前Firefoxはメニューの配置を変えたので奥の方に移動されています。探すと「その他」の中に配置されてました。

→ その他 → テキストエンコーディングの中です。

f:id:itasuke:20171218214112p:plain

話は変わるけど、この記号 を「ハンバーガーメニュー」と呼ぶんだが、どう見てもハンバーガーには見えないし、マクドナルドと紛らわしいので、代わりに三つ引き(みつひき)と呼んだらいいと思う。