サクラエディタで行指向のgrep置換マクロ
行指向のgrep置換というのは、grepした結果を行単位で反映する置換の方法。
単純な単語置換や正規表現の置換ではできない置換ができる。
例えば、見出しで番号を振っているテキストファイルの場合、連番が間違っていることは正規表現「^\d」でgrepするとわかりやすい。
test.txt(3,1): 1. 見出し test.txt(5,1): 1.2. 見出し test.txt(7,1): 1.2. 見出し test.txt(10,1): 2. 見出し test.txt(12,1): 2.1. 見出し test.txt(14,1): 2.3. 見出し
この結果を直接編集して正しい番号を振りなおして置換するのが行指向のgrep置換。
test.txt(3,1): 1. 見出し test.txt(5,1): 1.1. 見出し ← gepの結果を直接編集して直す test.txt(7,1): 1.2. 見出し test.txt(10,1): 2. 見出し test.txt(12,1): 2.1. 見出し test.txt(14,1): 2.2. 見出し ← gepの結果を直接編集して直す
grepの結果で置換すると以下のように変更が反映される。
タイトル 1. 見出し 記事 1.1. 見出し ← 直った 記事 1.2. 見出し 記事 2. 見出し 記事 2.1. 見出し 記事 2.2. 見出し ← 直った
いつもどおりVBScriptとかFileSystemObjectとかがわからず、かなりはまった。
- SJISとかUTF-8とかのファイルを扱うのはFileSystemObjectではないのですね。
[VBScript]UTF-8でファイル入出力(ADODB.Streamオブジェクト) - 拝啓、シーシュポス - 正規表現で改行コードが扱えるような書き方になっているけど、実際にはうまく認識しないんですね。
".+\r?\n"とか"[.\r\n]+"だと一致しないから、改行コードは無視するようにした。
Pattern プロパティ (できるはずなんだけど)
- RegExpの結果を受け取るにはMatchesだのSubMatchesだの...
- 正規表現に一致していないのにMatches(0)で要素を見ようとするとエラーになるけど、何が悪いのかわからず。
Microsoft VBScript 実行時エラー: プロシージャの呼び出し、または引数が不正です。 - MatchのオブジェクトはSetでいったん変数に格納しないとエラーになるけど、何が悪いのかわからず。
Microsoft VBScript 実行時エラー: オブジェクトがありません。: 'submatches(...)'
以下を修正。
- 改行コードは元のファイル(の先頭行)に合わせるようにして、改行コードを変えないようにした。(改行コードが差分で出て困るし、そもそも改行コードは変えるべきでない)
- 最初にgrep置換を実行するか確認を出した。(割り付けキーのミスタイプでいきなり置換されるのは危険なので)
- 2017/6/3 バックアップフォルダの親フォルダが作成されるようにした。
- 2017/6/8 UTF-8の場合にBOMを削除するようにした。
マクロファイル:C:\Users\ユーザ名\AppData\Roaming\sakura\greprep.vbs
'###### 大域変数定義 ###### '置換したファイル数/行数 Dim repfcnt, replcnt repfcnt = 0 '置換したファイル数 replcnt = 0 '置換した行数 '処理中のファイル名、行番号、文字コード Dim file, lineno, charset file = "" ' lineno = 0 charset = GetCharset("SJIS") '文字コード 'ファイルオブジェクト/入出力オブジェクト Dim fs, fi, fo Set fs = CreateObject("Scripting.FileSystemObject") Set fi = CreateObject("ADODB.Stream") Set fo = CreateObject("ADODB.Stream") 'カレントディレクトリ、一時ディレクトリ Dim curdir, tmpdir Set curdir = fs.GetFolder(".") Set tmpdir = fs.CreateFolder(fs.GetParentFolderName(curdir.path)+"\"+fs.GetTempName()) '正規表現オブジェクト Dim re_grep, re_code, re_matches, re_match 'grep結果「ファイルパス(行番号,列番号): 置換内容」 Set re_grep = New RegExp re_grep.Pattern = "^(.+?)\((\d+)(,\d+)?\): (.+)" '文字コード「文字コードセット:SJIS」など Set re_code = New RegExp re_code.Pattern = "^ +\(文字コードセット:(.+)\)" '###### 主処理 ###### '最初に実行するか確認してから実行 rc = Editor.OkCancelBox("現在の編集内容でgrep置換を実行しますか?") If rc = 1 Then GrepRepMain End If '###### サブルーチン ###### 'grep置換本体 Sub GrepRepMain 'grep結果に一致する行を置き換える allnum = Editor.GetLineCount(0) For i = 1 To allnum : Do '行を取得し、改行コードを削除する str = Chomp(Editor.GetLineStr(i)) '複数のgrep結果がある場合に、空行を境とみなす If str = "" Then Finalize Exit Do End If '文字コード指定行の場合は文字コードを設定 Set re_matches = re_code.Execute(str) If re_matches.Count > 0 Then Set re_match = re_matches(0) charset = GetCharset(re_match.submatches(0)) End If 'grep結果の行に一致した場合は置き換え Set re_matches = re_grep.Execute(str) If re_matches.Count > 0 Then 'ファイルパス、行番号、対象行で行置換を呼び出し Set re_match = re_matches(0) GrepReplace re_match.submatches(0), CInt(re_match.submatches(1)), Chomp(re_match.submatches(3)) End If Loop Until 1 : Next '残りの行の書き出し Finalize Editor.InfoMsg(""+CStr(repfcnt)+"ファイル、"+CStr(replcnt)+"行を置換しました。"+vbCrLf+tmpdir.path+"にバックアップがあります。") End Sub '対象行を置き換えるサブルーチン Sub GrepReplace(t_file, t_lineno, t_str) '初回 or 前回のファイルと一致しない If t_file <> file Then '残りの書き込み Finalize '対象のファイルを設定、行番号の初期化 file = t_file lineno = 0 '書き込み用ファイルオブジェクトをオープン fo.Open fo.Type = 2 'テキスト fo.charset = charset '文字コード '読み込み対象ファイルをオープン fi.Open fi.Type = 2 'テキスト fi.charset = charset '文字コード fi.LineSeparator = 10 'LF fi.LoadFromFile file '1行読み込み、変換後の改行コードを設定 If Right(fi.ReadText(-2), 1) = vbCr Then fo.LineSeparator = -1 'CR/LF Else fo.LineSeparator = 10 'LF End If fi.Position = 0 End If ReplaceLine t_lineno, t_str End Sub '対象行を置き換える '※t_linenoに0を指定することでファイルの最後まで読み込み/書き込み Sub ReplaceLine(t_lineno, t_str) '1行ずつ読み込み、置換対象の行の場合は指定内容で置換 Do While fi.EOS = False str = Chomp(fi.ReadText(-2)) lineno = lineno + 1 If lineno = t_lineno Then fo.WriteText t_str, 1 replcnt = replcnt + 1 Exit Do Else fo.WriteText str, 1 End If Loop End Sub '残りの行の読み込み/書き込み、バックアップ取得、ファイル入替 Sub Finalize '対象ファイルがオープンしていなければ何もしない If file = "" Then Exit Sub End If '残りの行の読み込み/書き込み ReplaceLine 0, "" 'UTF-8の場合はBOMを削除 If fo.charset = "UTF-8" Then fo.Position = 0 fo.Type = 1 'バイナリ fo.Position = 3 data = fo.Read fo.Close fo.Open fo.Write(data) End If '一時ファイルに書き込みファイルクローズ ftmp = fs.GetTempName() fo.SaveToFile ftmp, 2 fo.Close fi.Close 'ファイル数のカウント repfcnt = repfcnt + 1 '一時ディレクトリに同じパスでバックアップ用のファイル名を生成 fbak = Replace(file, curdir.path, tmpdir.path) If fbak = file Then fbak = tmpdir.path + Replace(file, fs.GetDriveName, "") End If 'バックアップ先のフォルダがなければ作成 CreateParentFolder(fbak) 'ファイルをバックアップして一時ファイルで置き換え fs.MoveFile file, fbak fs.MoveFile ftmp, file file = "" End Sub '親フォルダを再帰的に作成 Sub CreateParentFolder(path) parent = fs.GetParentFolderName(path) If Not fs.FolderExists(parent) Then CreateParentFolder(parent) fs.CreateFolder(parent) End If End Sub '###### 関数 ###### '改行コードを取り除く Function Chomp(str) str = Replace(str, vbCr, "") Chomp = Replace(str, vbLf, "") End Function '文字コードの文字列を変換する Function GetCharset(str) Select Case str Case "SJIS" GetCharset = "Shift_JIS" Case Else GetCharset = str 'SJIS以外はとりあえず同じ文字列を返却 End Select End Function
サクラエディタのソート用マクロとVBScriptのArrayのArrayListへの格納方法
答えは、Alt+A=昇順、Alt+D=降順、Alt+M=連続重複削除、でした。
サクラエディタのマクロで、編集画面上でソートができると便利なので、ソート用マクロを作ってみようと思う。
秀丸エディタのときはVC++で実行ファイルを作ってソートを実現したが、サクラエディタはVBScriptが使えるのでVBScript縛りで実現してみる。 +ソースコードの行数も50行程度までの縛りで。
基本的な処理は以下になる。
- 選択範囲を切り取り
- クリップボードの内容をソート
- 貼り付け
クリップボードの内容をソートする処理は以下になる。
クリップボード周りはサクラエディタのマクロ関数にあるので問題なし。
文字列の分割と配列要素の結合はVBScriptの機能にあるので問題なし。
VBScriptのArrayにはソートがないが、ArrayListを使用するとソートもできるみたい。
実験記録 No.02 : VBSで動的配列(ArrayList)を使う
ただ、基本的にはVBScriptで書くとするとArrayを途中でArrayListに格納したくなるが、1件ずつ格納する以外に方法が見つからない。
ArrayListのメソッドではどれもダメだった。
- AddRange … ICollectionのオブジェクトでないとダメ
- Insert … 値を挿入するメソッドで、配列を挿入することはできない
- コンストラクタ … CreateObjectでは渡せない、など
Arrayを使わない場合にはSystem.String→Split→System.Collections.ArrayListのようなことがあるが、System.StringはCreateOjectには使えないようなので、System.Text.StringBuilder→ToStringかな?
...結局は、Stringに代入する方法が見つからなかったので断念。
Arrayを1件ずつArrayListにAddする方法でやってみた。
あと、いろいろなソート方法を指定できるのもいいかなと思ったけど、以下の2点でこちらも断念。
- VBScriptでソート指定用のダイアログを出してその結果を受け取る必要あり。
- SortメソッドのコールバックにIComparerを用意する必要あり。
試行錯誤の結果、ソート用マクロはこうなった。
- 2017/6/8 重複なしでソートできるようにした。
マクロファイル:C:\Users\ユーザ名\AppData\Roaming\sakura\clipsort.vbs
'ソート方法を指定(選択ダイアログを作れなかった) rc = Editor.YesNoBox("昇順=はい、降順=いいえ を選択してください。") Select Case rc Case 6 'はい order = 0 '昇順 Case 7 'いいえ order = 1 '降順 End Select rc = Editor.YesNoBox("重複あり=はい、重複なし=いいえ を選択してください。") Select Case rc Case 6 'はい unique = 0 '重複あり Case 7 'いいえ unique = 1 '重複なし End Select '選択状態 mode = Editor.IsTextSelected() Select Case mode Case 0 '選択なし=全体をソート Editor.SelectAll(0) Case 1 '選択 mode = 0 '通常貼り付け Case 2 '矩形選択 mode = 1 '矩形貼り付け End Select '選択部分をクリップボードにコピー Editor.Cut(0) 'コピーした文字列を取得 clip = Editor.GetClipboard(0) '改行(LF)で分割(CR/LFはCRを残す) arr = Split(clip, vbLf) 'ソートが可能なArrayListオブジェクトを用意 Dim list Set list = CreateObject("System.Collections.ArrayList") '領域拡張が起こらないよう要素数を事前に設定 list.Capacity = UBound(arr) '最後の空行を含めない行数を算出 n = UBound(arr)-1 If arr(n) = "" or arr(n) = vbCr Then n = n - 1 End If 'Array→ArrayListのコピー(他に方法がなかった) For i=0 to n list.add(arr(i)) Next 'ソート list.Sort() '重複なしの場合は重複を削除する If unique = 1 Then pre = list.Item(n) For i=n-1 to 0 step -1 If list.Item(i) = pre Then list.RemoveAt(i) End If pre = list.Item(i) Next End If '降順の場合は逆順にする If order = 1 Then list.Reverse() End If '改行(LF)で結合して文字列化 clip = Join(list.ToArray(), vbLf) 'クリップボードに設定 rc = Editor.SetClipboard(mode, clip) '貼り付け Editor.Paste(0)
サクラエディタのgrepをエクスプローラから実行
エクスプローラのフォルダを右クリックして、サクラエディタのgrepのダイアログを表示するための設定です。
※レジストリの変更は自己責任でお願いします。
- レジストリエディタを起動します。
- 以下のキー(SakuraGrepとcommand)を作成します。
HKEY_CLASSES_ROOT\Folder\shell
└─SakuraGrep ← 任意の名前でよいです。
└─command
※管理者権限がない場合はHKEY_CURRENT_USER\SOFTWARE\Classes\Folder\shellに作成します。Folderとshellがない場合は作成します。 - SakuraGrepのキーの(既定)にコンテキストメニューに表示する名前を指定します。
SAKURAでgrep(&G) - commandのキーの(既定)に以下のコマンドを指定します。
・Program Files (x86)にインストールしている場合
"C:\Program Files (x86)\sakura\sakura.exe" -GREPMODE -GREPDLG -GOPT=SLP -GFILE="*.*" -GFOLDER="%1"
・Program Filesにインストールしている場合
"C:\Program Files\sakura\sakura.exe" -GREPMODE -GREPDLG -GOPT=SLP -GFILE="*.*" -GFOLDER="%1"
※ コマンド起動の場合、前回の設定をデフォルトひょじしてくれないのですべてオプション指定する必要がある。
※ -GOPT=SLPはサブフォルダ検索、英大小文字、一致行表示。
SakuraGrepの設定画面例
commandの設定画面例
エクスプローラでフォルダを右クリックすると[SAKURAでgrep]のメニューが表示されるようになります。そのメニューを選ぶと、サクラエディタの[Grep条件入力]画面が開き、grep対象のフォルダはエクスプローラで選択したフォルダになります。
レジストリファイルを使用する場合は以下の拡張子regのファイルを作成します。
Windows Registry Editor Version 5.00 [HKEY_CLASSES_ROOT\Folder\shell\SakuraGrep] @="SAKURAでgrep(&G)" [HKEY_CLASSES_ROOT\Folder\shell\SakuraGrep\command] @="\"C:\\Program Files (x86)\\sakura\\sakura.exe\" -GREPMODE -GREPDLG -GOPT=SLP -GFILE=\"*.*\" -GFOLDER=\"%1\""
Windows版のRuby On RailsでTypeError
Windows版のRuby On Railsを使おうとしたら、以下のエラーが出たので原因を調べてみる。
ActionView::Template::Error (TypeError: オブジェクトでサポートされていないプロパティまたはメソッドです。): 7: <%= stylesheet_link_tag 'application', media: 'all' %> 8: <%= javascript_include_tag 'application' %>
以下のコマンド実行でエラーになっていることが確認できた。
cscript //E:jscript //Nologo //U C:/Users/xxx/AppData/Local/Temp/execjsxxxxxxxx-xxxxx-xxxxxxxxx Microsoft JScript 実行時エラー: オブジェクトでサポートされていないプロパティまたはメソッドです。
エラーになっているJavaScriptのコード(execjsxxxx~の中身)は以下。
parse: function parse(input) { var self = this, stack = [0], tstack = [], vstack = [null], lstack = [], table = this.table, yytext = '', yylineno = 0, yyleng = 0, recovering = 0, TERROR = 2, EOF = 1; var args = lstack.slice.call(arguments, 1); var lexer = Object.create(this.lexer); ← ここでエラー var sharedState = { yy: {} };
単体で実行してもエラーになるのでObject.createはJScriptではサポートされていないようだ。
<object_create.jsファイル> Object.create(null);
cscript //E:JScript object_create.js Microsoft JScript 実行時エラー: オブジェクトでサポートされていないプロパティまたはメソッ ドです。
IE9以降でObject.createが利用可能とのこと。
IE9以降のJScriptエンジンのJScript9にしてみたが、エラーが変わっただけで結局サポートされていない。
cscript //E:{16d51579-a30b-4c8b-a276-0ff4dc41e755} object_create.js JavaScript 実行時エラー: オブジェクトは 'create' プロパティまたはメソッドをサポートしていません。
Windows 10環境を使っているので、EdgeのJavaScriptエンジンのChakraで動かしたら正常動作。
cscript //E:{1b7cd997-e5ff-4932-a7a6-2a9e636da385} object_create.js
cscriptのJavaScriptエンジンをChakraに変えてRailsを実行したところ正常動作した。
lib\ruby\gems\x.x.x\gems\execjs-x.x.x\lib\execjs\runtimes.rb JScript = ExternalRuntime.new( name: "JScript", - command: "cscript //E:jscript //Nologo //U", + command: "cscript //E:{1b7cd997-e5ff-4932-a7a6-2a9e636da385} //Nologo //U", runner_path: ExecJS.root + "/support/jscript_runner.js", encoding: 'UTF-16LE' # CScript with //U returns UTF-16LE )
ただし、Windows 7とかにはC:\WINDOWS\System32\Chakra.dllがなく、上記の対処は環境が限定される(Windows 10だけ?)。
IActiveScriptProperty::SetPropertyのSCRIPTPROP_INVOKEVERSIONINGでスクリプトのバージョンを変更できるそうだが、JScript9が初期化された時点で設定するプログラムを作る必要があるので組み込むのは無理。
そもそも、CoffeeScript 1.9.0以降でObject.createを使うようになったことで今回のTypeErrorが発生するようになったらしいので、Object.createを使わないようにしてみる。
他にもObject.createを使用しているコードを探したところ、以下が参考になりそう。
lib\ruby\gems\x.x.x\gems\uglifier-x.x.x\lib\es5.js // https://developer.mozilla.org/en/docs/JavaScript/Reference/Global_Objects/Object/create if (!Object.create) { Object.create = function (o) { if (arguments.length > 1) { throw new Error('Object.create implementation only accepts the first parameter.'); } function F() {} F.prototype = o; return new F(); }; }
CoffeeScriptのObject.createの使用箇所を以下のように変えたところ正常終了。
lib\ruby\gems\x.x.x\gems\coffee-script-source-x.x.x\lib\coffee_script\coffee-script.js parse: function parse(input) { var self = this, stack = [0], tstack = [], vstack = [null], lstack = [], table = this.table, yytext = '', yylineno = 0, yyleng = 0, recovering = 0, TERROR = 2, EOF = 1; var args = lstack.slice.call(arguments, 1); - var lexer = Object.create(this.lexer); + var lexer; + if (Object.create) { + lexer = Object.create(this.lexer); + } + else{ + function F() {} + F.prototype = this.lexer; + lexer = new F(); + } var sharedState = { yy: {} };
結局、CoffeeScriptでObject.createを使用するようにした箇所を変更するのがベストですね。