全角文字が大量に含まれているとなぜか例外が出る
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')
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')
回避法
手動で文字列化してください。こんな感じ。
"`u{5b57}" * 160 | Out-File bar.txt Select-String "`u{5b57}" bar.txt | Out-String # bar.txt:1:字字字字字字字字字字字字字字 ...
おわり🌱
去年の時点で似ような報告が上がってるから、ま、すぐに直ると思うけどね。
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-Command
と Get-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 を薄く包んでいるだけなのでリファレンスを見ても使い方がさっぱりわからないという。
いろいろ調べて、おそらくこうするのが正しいと思われるサンプルコードを書いた。
目次
- 値の読み込み
- 値の書き込み
- コンテキスト・マネージャ
- キーの作成
- OpenKeyEx と CreateKeyEx の違い
- 値の削除
- キーの削除
- 値の列挙
- キーの列挙
- 既定値
- REG_EXPAND_SZ
- REG_BINARY
- REG_MULTI_SZ
- サブキーを開く
値の読み込み
# 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 API の RegOpenKeyEx を使っています。
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 は中身が違いますが、オプション機能を使わなければ同じはずです。
値の列挙
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_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 でも同様です。
参考
- winreg.c - winregモジュールの本体。
- winreg.c.h - Python側の引数解析。
Out-GridView は選択画面としても使える
Out-GridView を使うとコマンドレットの出力をGUIに表示できるわけだが、PassThru パラメーターを指定することで、オブジェクトの「選択画面」としても機能します。
# 例:カレントディレクトリのファイル一覧を表示して削除するファイルを選ばせる Get-ChildItem | Out-GridView -PassThru | Remove-Item # 省略形 gci | ogv -pass | ri
Ctrl や Shift キーを押しながら行をポチポチ選んでから OK ボタンを押す。すると選んだ項目が Remove-Item に渡されてファイルが削除されるという寸法です。
ちなみに、Windows のリストビューにおいて Ctrl は一行選択、Shift は範囲選択です。
Core にはないよ(追記:2020/01/03)
PowerShell Core 6.x では、なぜか使えなくなっています(コアだから?)。ドキュメントの注意書きによれば PowerShell 7 で復活する予定です。
参考
- Out-GridView - 本記事のネタ元(Example 7 を見よ)
- 「Format-」コマンドで実行結果の出力を整形する
- PowerShell7 (Preview) の Out-GridView
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
文字化けに至るまでの、よくありがちな流れはこんな感じです。