GAWK4 関数内グローバル変数を検索する
ユーザー定義関数内のグローバル変数
ご存知の通り、ユーザー定義関数実装部のパラメータはローカル変数として使えます。従来、暗黙の了解として、本当に引数として使うパラメータの後にスペースを複数個入れ、その後に続くパラメータはローカル変数とみなすようになっています。
一方、BEGIN ACTION END セクションで定義使用する変数は、グローバル変数となります。これに関連して、色々なトラブルが発生します。上記関数内で、意図せずグローバル変数を使用して値を変更、関数のパラメータから漏れた変数を気づかずに END セクションで参照、同関数内で単純にタイプミスしてグローバルな変数を定義した等々。
他の言語のように使用変数の宣言等の面倒がなく、自由にどこでも、唐突に変数を使える代償として、ユーザーが変数を完全に管理する必要があるのです。
そこで変数管理の一環として、「ユーザー定義関数内部で意図しないグローバル変数を使っていないか」閲覧/検査する方法を模索しました。
PROCINFO を使用し、ユーザー定義関数内のグローバル変数を閲覧/検査する
はじめに、PROCINFO["identifiers"]
の値(群)は、スクリプトのパース終了直後の識別子情報であり、プログラムの実行中に更新されることはない(User's Guide)、ということを記しておきます。
そして次は、
if (PROCINFO["identifiers"][i] == "untyped")
[i]
は、スカラー或いは配列として使われるであろう型指定されていない変数(名)で、GAWK側が未だ知らない変数(User's Guide) すなわちユーザーがスクリプトで、定義使用しているグローバル変数です。
同じくグローバル変数である ARGC ARGV FS
等は、これに含まれません。
"untyped"
これをユーザー定義関数からすべて取り出せれば、目的は達成されます。
awk_test_func.awk
1 : #awk_test_func.awk
2 : #. BEGIN;ユーザー定義関数のグローバル変数チェック
3 : BEGIN {
4 : test_func();
5 : }
実行するのは下記の test_func() のみです。この関数は、このソース自体で使用されるユーザー定義グローバル変数を使用箇所(行)とともに列挙するものです。test_func() は内部でグローバル変数(組み込み変数除く)を使用せず、引数も取らず、自己完結します。
自身を入力ファイルとして実行すると、下方に配置した検査対象関数 lex_awk() が使用する
ユーザー定義グローバル変数のみを取得し、その使用箇所を走査できる、というからくりです。
6 : #. test_func();関数で使用されるグローバル変数を書き出す
7 : function test_func( a_line, a_i, a_ret, reg, ct, i, j, k) {
8 : while (getline < ARGV[1] > 0) a_line[++ct] = $0; #このファイルを読む
9 : close(ARGV[1]);
10 :
11 : for (i in PROCINFO["identifiers"])
12 : if (PROCINFO["identifiers"][i] == "user") #ユーザー定義関数
13 : if (i ~ /test_func/); #この関数を除外
14 : else print "User-Defined FuncName : " i "()";
15 :
16 : print "\nGlobal Variables";
17 : for (i in PROCINFO["identifiers"])
18 : if (PROCINFO["identifiers"][i] == "untyped") #グローバル変数(user)
19 : a_i[i];
20 :
21 : for (i in a_i) {
22 : for (j = 1; j < ct + 1; j++) {
23 : reg = "\\y" i "\\y"; #Dynamic Regexps 単語境界
24 : if (match(a_line[j], reg))
25 : a_ret[++k] = i " : line " sprintf("%3d", j);
26 : }
27 : }
28 : PROCINFO["sorted_in"] = "@val_str_asc"
29 : for (j in a_ret) print a_ret[j];
30 : }
17-19行でユーザー定義グローバル変数を取得し、21-25行でソースのどの部分で使われているかを照合抽出しています。
下記が検査対象のユーザー定義関数です。本当にただ配置するだけになります。
31 : #. lex_awk();レキシカルアナライザ改(属性付きトークン)
32 : function lex_awk(str, res, mode, len,\
33 : fonetime, fcon, i, ib, ch, dlm,\
34 : frtok, pos, ct, ia, iach, ret) {
35 : ct = i = 1;
36 : dlm = "_fst"; #デリミタを設定
37 : delete res; #2次元配列を初期化
38 : len = length(str);
39 : if (substr(str, len) ~ /\\/) fcon = 1; #行継続
40 : if (!len) { res[1][0] = "e"; res[1][1] = ""; return 0; } #空行
41 :
42 : while (i < len + 1) {
43 : ch = substr(str, i, 1); #一文字切り出す
44 : ib = i; #ib 切り出し開始位置
45 : if (!mode && _cont_ch == "") #mode0 空白無視(非行継続)
46 : if (ch ~ /[ \t]/) { i++; continue; }
47 :
48 : #複数行にまたがる正規表現/リテラル 前行引き継ぎ
49 : if (!fonetime) #whileで最初の1度だけ実行
50 : if (_cont_ch == "") fonetime = 1; #fone 自身を設定
51 : else { fonetime = 1; ch = _cont_ch; } #i=1 ch=" or ch=/ で開始
52 :
53 : switch (ch) {
54 : case /[a-zA-Z_\$]/: #識別子
55 : for (; i < len + 1; i++)
56 : if (substr(str, i, 1) ~ /[^a-zA-Z0-9_\$]/) break;
57 : ret = substr(str, ib, i - ib);
58 : if (ret in _kyw) res[ct][0] = "k" #制御文キーワード
59 : else if (ret in _bltf) res[ct][0] = "f"; #組み込み関数
60 : else if (ret in _bltv) res[ct][0] = "v"; #組み込み変数/定数
61 : else {
62 : for (ia = i; ia < len + 1; ia++) {
63 : iach = substr(str, ia, 1);
64 : if (iach ~ /[^ \t]/ ) break; }
65 : if (iach ~ /\(/ ) res[ct][0] = "u"; #ユーザー定義関数
66 : else res[ct][0] = "i"; #識別子(変数)
67 : }
68 : (ret !~ /^case$/) ? frtok = 1 : frtok = 0; #先行句
69 : break;
70 : case /[0-9]/: #数値
71 : for (; i < len + 1; i++)
72 : if (substr(str, i, 1) ~ /[^0-9\.]/) break;
73 : ret = substr(str, ib, i - ib);
74 : res[ct][0] = "n";
75 : frtok = 1; #先行句
76 : break;
77 : case /"/: #文字列リテラル
78 : pos = litendpos(substr(str, i));
79 : if (pos) { i += pos; _cont_ch = ""; } #両端の””含む
80 : #行継続 大域変数に書き込み 次行は最初からリテラルで開始
81 : else if (fcon) { i = len; _cont_ch = "\""; } #大域変数設定
82 : else { err(NR, i); i++; _cont_ch = ""; } #エラー スルー
83 : ret = substr(str, ib, i - ib);
84 : res[ct][0] = "l";
85 : frtok = 0; #先行句リセット
86 : break;
87 : case /\//: #正規表現か除算演算子(直前のデリミタと先行句で判断)
88 : if (dlm in _dlm && !frtok) {
89 : pos = regendpos(substr(str, i));
90 : if (pos) { i += pos; _cont_ch = ""; } #両端の//含む
91 : #行継続 大域変数に書き込み 次行は最初から正規表現で開始
92 : else if (fcon) { i = len; _cont_ch = "/"; } #大域変数設定
93 : else { err(NR, i); i++; _cont_ch = ""; } #エラー スルー
94 : } else { i++; dlm = "/"; } #除算演算子
95 : ret = substr(str, ib, i - ib);
96 : if (i - ib == 1) res[ct][0] = "o";
97 : else res[ct][0] = "r";
98 : frtok = 0; #先行句リセット
99 : break;
100 : case /#/: #コメント
101 : if (! mode) { i = len + 1; continue; } #mode 0 コメント無視
102 : else {
103 : i = len + 1; #残りの全文字列
104 : ret = substr(str, ib, i - ib);
105 : res[ct][0] = "c";
106 : break;
107 : }
108 : default: #その他の演算子や区切り文字等
109 : if (ch ~ /[ \t]/ && dlm ~ /_fst/) { #行頭の空白をまとめる
110 : for (; i < len + 1; i++)
111 : if (substr(str, i, 1) ~ /[^ \t]/) break;
112 : ret = substr(str, ib, i - ib);
113 : res[ct][0] = "w";
114 : } else {
115 : if (ch ~ /[^ \t]/) {
116 : ret = dlm = ch;
117 : if (ch in _op1) res[ct][0] = "o"; #operator
118 : else res[ct][0] = "p"; #punctuation
119 : frtok = 0; #先行句リセット
120 : tmp = substr(str, i, 2);
121 : i++; #1つ進める (記号空白は次を検索していないため)
122 : if (tmp in _op2) { #2文字演算子?
123 : i++; #もう1つ進める
124 : ret = dlm = tmp; #デリミタ書き換え
125 : res[ct][0] = "o"; #operator
126 : }
127 : #空白はdlm/frtokを書き換えない
128 : } else { ret = ch; res[ct][0] = "w"; i++; }
129 : }
130 : break;
131 : }
132 : res[ct][1] = ret; #トークン格納
133 : ct++;
134 : }
135 : return ct - 1; #切り出したトークン数
136 : }
実行結果 gawk -f awk_test_func.awk awk_test_func.awk (自身のソース)
わざとらしいのですが、24-26行目「tmp」はグローバルであってはいけません。他のグローバル変数名はアンダースコアから始まりますので、一目瞭然です。ユーザー関数内で使用する(意図して)グローバル変数には、先頭にアンダースコアを付けるように、筆者は心掛けています。
ちなみに、配置した lex_awk() 内で未定義のユーザー定義関数を使っていますが、この状態(パースのみ)ではエラーは出ません。
さて、ここまでが前振りです。このソースを踏まえて、サクラエディタのマクロを作りましょう。
お気づきかとは思いますが、配置しただけの関数 lex_awk() を他のユーザー関数とすれば、汎用的に使えます。AWKスクリプトのコーディング中に、ユーザー定義関数内のグローバル変数を確認する、という荒業が可能になります。
マクロの骨子は、動的に上記スクリプトを作成し、それを実行する、です。
サクラエディタマクロを実装する
test_func.vbs
1 : 'サクラマクロ test_func.vbs
2 :
3 : Option Explicit
4 : Const batpath = "y:\test_func\test_func.bat"
5 : Dim scmd, spath, sexte, snz, nline
6 :
7 : With Editor
8 : spath = .GetFilename 'fullpath
9 : sexte = Mid(spath, InStrRev(spath, ".") + 1) 'extention
10 : If LCase(sexte) = "awk" Then
11 : nline = .GetSelectLineFrom() 'headline of selected(layout)
12 : nline = .LayoutToLogicLineNum(nline) 'change logical number
13 : snz = .GetSelectedString(0)
14 : .SetClipboard 0, snz
15 : scmd = batpath & " " & nline 'with arg
16 : .ExecCommand scmd, 1
17 : .ActivateWinOutput
18 : End If
19 : End With
エディタ側の処理の流れ(エディタ上では関数部分を範囲選択中)
- 8 編集中のファイルパスを取得
- 9 ファイルの拡張子を取得
- 10 編集中のファイルが awk ファイルであれば以下を実行する
- 11 選択開始行番号を取得(折り返し単位 レイアウト表示行)
- 12 表示行から論理行へ変換
- 13 選択中の文字列を取得
- 14 選択文字列をクリップボードに送る
- 15 バッチファイルを起動するコマンド文字列 + test_func.bat に引数(選択開始行番号)を渡す
- 16 バッチファイルを起動し、その出力をアウトプットウィンドウに送る
- 17 アウトプットウィンドウをアクティブ(エディタ最前面)にする。
batpath に下記 test_func.bat のバッチファイルパスを設定します。
サクラエディタのマクロ登録方法は こちらのページ で紹介しています
test_func.bat
1 : @echo off
2 : rem AWK ユーザー定義関数のグローバル変数チェック
3 : pushd %0\..
4 : type _test_func.txt > _temp.awk
5 : call mshta_clip_out.cmd >> _temp.awk
6 : gawk421 -v ARG1=%1 -f _temp.awk _temp.awk
7 : del _temp.awk
バッチ処理の流れ
- 3 カレントディレクトリをこのバッチファイルが存在するディレクトリに変更
- 4 後述のテキストファイルを一時ファイルにリダイレクト(コピー) _temp.awkは自動作成。
- 5 クリップボードから文字データを _temp.awk にリダイレクト(追記)
- 6 GAWKは「-v ARG1=%1」でバッチファイルに渡された引数(行番号)を受け取り、
(「ARG1」は筆者が勝手に名付けた変数名でGAWKソース内でそのまま参照できる)
スクリプトとして_temp.awk を実行、自身を引数(読み込みファイル)としている。
実行結果は標準/エラー出力へ
- 7 一時ファイルを削除
下記の2つと合わせて3つのファイルが同じディレクトリにあるようにしてください。
mshta_clip_out.cmd クリップボードから標準出力へ (1行書き 改行不可)
@MSHTA.EXE vbscript:Execute("s=clipboardData.getData(""text""):If Not IsNull(s) Then CreateObject(""Scripting.FileSystemObject"").GetStandardStream(1).Write(s): End If:close()")
こういう使いかたしかしませんが、とても便利な Windows 標準添付の mshta.exe です。1行書きが必須ですが、短いスクリプトなら vbs 本体を普通に書くより楽かもしれません。この1文でクリップボードの文字データを標準出力に吐き出すことができます。コマンドプロンプトには「CLIP」というコマンドがありますが、これはクリップボードへデータを送る機能しかありません。筆者はちょっと前までこの mshta の存在を知らなかったので、C++ で自前のもの作成し使っていました。
_test_func.txt (行番号補正)
1 : BEGIN {
2 : test_func();
3 : }
4 : function test_func( a_line, a_i, a_ret, reg, ct, i, j, k) {
5 : while (getline < ARGV[1] > 0) a_line[++ct] = $0;
6 : close(ARGV[1]);
7 :
8 : for (i in PROCINFO["identifiers"])
9 : if (PROCINFO["identifiers"][i] == "user")
10 : if (i ~ /test_func/);
11 : else print "User-Defined FuncName : " i "()";
12 :
13 : print "\nGlobal Variables";
14 : for (i in PROCINFO["identifiers"])
15 : if (PROCINFO["identifiers"][i] == "untyped")
16 : a_i[i];
17 :
18 : for (i in a_i) {
19 : for (j = 1; j < ct + 1; j++) {
20 : reg = "\\y" i "\\y";
21 : if (match(a_line[j], reg))
22 : a_ret[++k] = i " : line " sprintf("%3d", (j + ARG1 - 29));
23 : }
24 : }
25 :
26 : PROCINFO["sorted_in"] = "@val_str_asc"
27 : for (j in a_ret) print a_ret[j];
28 : }
エディタ側の行番号との整合性の為 22行目末尾 j に「+ ARG1 - 29」を追加しています。
「ARG1」は、エディタからバッチファイルに渡された引数で、エディタの選択開始行番号が入っています。
「 - 29 」は自身のソースが 28 行あることと、エディタの行番号、ソースの「 a_ret[] 」どちらも 1 から数え始めるので、1 を引き、「 - 29 」としています。
ちなみに GAWK の引数「ARG1」は グローバル変数なのですが、
PROCINFO["identifiers"]["ARG1"] == "scalar"
と、パース直後に認識しています。
このテキストファイルをコピーして保存する際には、29行目の先頭が「 EOF 」となるようにしてください。テキストの最後(EOF)からクリップボード経由で送られてきた文字列(ユーザー定義関数)が追記され、目的である AWK ファイルとして完成します。
マクロを使ってみる
マクロの実行手順と結果
コーディングして、一応の動作確認が済んだ頃に
ユーザー定義関数をドラッグして選択後に右クリック
コンテキストメニューより test_func を選択
関数内で使用されているユーザー定義のグローバル変数が、使用行番号と共に表示される
※ 4 行目末尾に「 21 」とありますが、これが引数として渡された選択開始行番号であり
マクロの変数「 nline 」、バッチファイル引数「 %1 」、GAWKソースの「 ARG1 」です
留意事項
ソースとして実行しますので、選択範囲に誤りがあると、エラーが出ます。
同様に、ユーザー定義関数に文法上のエラー等があると普通にエラーが出ます。
同様に BEGIN/END/ACTION を選択すると、実行してしまいますので、おやめ下さい。
ドラッグにより、複数のユーザー定義関数を一度に検査できますが、どの関数がグローバル変数を使用しているのか分からないので、あまりお勧めしません。
使用箇所 「xxx :line nnn」となっていますが、PROCINFO が吐き出した変数名をターゲット文字列として、単にソースをテキストとして検索しているだけですので、コメントやリテラル、正規表現の中に「xxx」が単語(単語境界が存在)としてあれば、残念ながらその単語も検出してしまいます。
・
・
・
・
・
より便利に使うために test_func.vbs (改)
別ページで紹介している select_section.awk
を併用すると、検査前にユーザー定義関数全体を選択する手間が省けて便利です。以下は併用時のマクロ例です。
注)このマクロを使用すると、複数のユーザー定義関数を同時に選ぶことはできなくなります。
1 : 'サクラマクロ test_func.vbs
2 : '編集中のユーザー定義関数に使用されているグローバル変数を簡易検査する
3 : '準備物
4 : 'test_func.bat, test_func.txt, mshta_clip_out.cmd, select_section.awk
5 : '使用法
6 : 'サクラエディタにこのマクロを登録する
7 : 'コンテキストメニューにこのマクロを追加する
8 : 'ユーザー定義関数1つのエリアを、左クリック(カーソルが対象関数にある)
9 : 'コンテキストメニューからこのマクロを選択
10 : '禁忌
11 : 'BEGIN, END, ACTION に使う -> スクリプトが実行される
12 :
13 : Option Explicit
14 : Const gawk = "gawk414 -v ARG1="
15 : Const sel_awk = " -f y:\select_section\select_section.awk "
16 : Const batpath = "y:\test_func\test_func.bat"
17 :
18 : Dim spath, slext, ncurline, objwsh, objexe, scmd
19 : Dim gawk_ret, aselect, fail, erstr, snz, nline
20 :
21 : With Editor
22 : spath = .GetFileName
23 : slext = LCase(Mid(spath, InStrRev(spath,".") + 1))
24 : End With
25 :
26 : If slext <> "awk" Then
27 : msgbox "AWKファイルではありません",vbOKOnly, "select_section"
28 : Else
29 : With Editor
30 : .FileSave
31 : ncurline = .ExpandParameter("$y")
32 : scmd = gawk & ncurline & sel_awk & spath
33 : End With
34 :
35 : Set objwsh = CreateObject("WScript.Shell")
36 : Set objexe = objwsh.exec(scmd)
37 :
38 : If Err.Number = 0 Then
39 : Do While objexe.Status = 0
40 : Editor.Sleep(100)
41 : Loop
42 : gawk_ret = objexe.stdout.readall
43 : erstr = objexe.stderr.readall
44 : fail = objexe.exitcode
45 : Else
46 : msgbox "コマンドが不正です", vbOKOnly, "select_section"
47 : End If
48 :
49 : Set objexe = Nothing
50 : Set objwsh = Nothing
51 :
52 : If fail = 1 Then
53 : msgbox erstr, vbOKOnly, "select_section"
54 : Else
55 : aselect = Split(gawk_ret)
56 : With Editor
57 : .CancelMode 0
58 : .SetDrawSwitch 0
59 : .Jump aselect(0), 1
60 : .BeginSelect 0
61 : .Jump aselect(1), 1
62 : .GoLineEnd
63 : 'from select_section.vbs
64 :
65 : 'from test_func.vbs
66 : nline = .GetSelectLineFrom()
67 : nline = .LayoutToLogicLineNum(nline)
68 : snz = .GetSelectedString(0)
69 : .SetClipboard 0, snz
70 : .CancelMode 0
71 : scmd = batpath & " " & nline
72 : .ExecCommand scmd, 1
73 : .SetDrawSwitch 1
74 : .ActivateWinOutput
75 : End With
76 : End If
77 : End If
関数自動選択の範囲は、「function」上部のコメント部を含みますので、検査結果15-16行にある通り、コメント部の記述を拾うことがあります。