etsuxのブログ

自分がハマったことなどを記録しています。

サクラエディタで行指向のgrep置換マクロ

サクラエディタで行指向の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とかがわからず、かなりはまった。

以下を修正。

  • 改行コードは元のファイル(の先頭行)に合わせるようにして、改行コードを変えないようにした。(改行コードが差分で出て困るし、そもそも改行コードは変えるべきでない)
  • 最初に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行程度までの縛りで。

基本的な処理は以下になる。

  1. 選択範囲を切り取り
  2. クリップボードの内容をソート
  3. 貼り付け

クリップボードの内容をソートする処理は以下になる。

  1. クリップボードを文字列で取得
  2. 文字列を改行で分割して配列化
  3. 配列をソート
  4. 改行で配列要素を結合し文字列化
  5. クリップボードに戻す

クリップボード周りはサクラエディタのマクロ関数にあるので問題なし。

文字列の分割と配列要素の結合は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のダイアログを表示するための設定です。

レジストリの変更は自己責任でお願いします。

  1. レジストリエディタを起動します。
  2. 以下のキー(SakuraGrepとcommand)を作成します。
    HKEY_CLASSES_ROOT\Folder\shell
    └─SakuraGrep ← 任意の名前でよいです。
      └─command
    ※管理者権限がない場合はHKEY_CURRENT_USER\SOFTWARE\Classes\Folder\shellに作成します。Folderとshellがない場合は作成します。
  3. SakuraGrepのキーの(既定)にコンテキストメニューに表示する名前を指定します。
    SAKURAでgrep(&G)
  4. 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の設定画面例

f:id:etsux:20170515223525p:plain

commandの設定画面例

f:id:etsux:20170515223433p:plain

 

エクスプローラでフォルダを右クリックすると[SAKURAでgrep]のメニューが表示されるようになります。そのメニューを選ぶと、サクラエディタの[Grep条件入力]画面が開き、grep対象のフォルダはエクスプローラで選択したフォルダになります。

f:id:etsux:20170515223935p:plain

 

レジストリファイルを使用する場合は以下の拡張子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が利用可能とのこと。

JavaScript のバージョン情報

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を使用するようにした箇所を変更するのがベストですね。