Kuin Advent Calender 2018 4日目ということで、おはようございます(ネタは11/26に思いついたのでそれを書きます)。
Kuin言語にはKuinエディタというエディタが付属されています。ところでKuinのライブラリも.knファイルで記述されているのですが、ご存知でしたでしょうか?
例えば、cuiライブラリはsys\cui.knで記述されているのですが、サクラエディタで開くとこんな感じです。
そんでもってこいつをKuinエディタで開いてやります。
これを違うフォルダのファイルに保存して、もう一度サクラエディタを開きます。
はい、Kuinエディタで開いて保存すると微妙にファイルのインデントが変わるのが分かるかと思います。
さて、Kuinのissueを開いてくいなちゃんにこのことを報告してKuinエディタを直してもらってもいいのですが、少し大人げないですね。他に方法はないのでしょうか。プログラマがよくやるやり方としては、PerlやRubyなどのスクリプト言語を書いてインデントを直したりします。でもWindowsにスクリプト言語をインストールするのは面倒。とぼとぼと職場から帰宅する途中で思いつきました。
「そうだ!Kuinでスクリプトを書けばいいじゃないか!」
step1. ファイルパスの取得
さて、まず何から始めればよいのでしょうか。ファイル名を1つ指定するとそのファイルパスを表示するプログラムを作成してみます。
func main()
if(^lib@cmdLine() <> 1)
do cui@print("エラー: ファイル名を1つ指定してください。\n")
ret
end if
do cui@print(file@getCurDir() ~ lib@cmdLine()[0] ~ "\n")
end func
こいつをデスクトップでコンパイルして実行すると正しい結果が得られることが分かります。
分かる人もいるかと思いますが一応解説を。
lib@cmdLine()というのはプログラムで指定したコマンドライン引数を文字列の配列で取得します。例えば、a.exe a b cなら、lib@cmdLine()の戻り値は["a", "b", "c"]です。a.exeを引数なしで実行した場合はlib@cmdLine()の戻り値は[]になります。このとき配列から要素を取り出そうとするとエラーになるのでif文を書いてエラーメッセージを出力するようにしています。
file@getCurDir()関数はカレントディレクトリを取得します。これはコマンドプロンプトの">"の前に書いてあるフォルダパスのことです。カレントディレクトリという概念があるおかげで、コマンドプロンプトのユーザーはコマンド実行時に長いファイルパスを書かなくて済むようになっています。
"~"は配列を連結する演算子です。Kuinでは文字列は文字の配列となっているのでこの演算子を使うことができます。
step2. ファイルの内容を表示するプログラムを作成する
では、ファイルを読み込んでその内容を出力するプログラムを作成しましょう。
func main()
; コマンドライン引数が1つでなければエラー
if(^lib@cmdLine() <> 1)
do cui@print("エラー: ファイル名を1つ指定してください。\n")
ret
end if
; ファイルを開く
var rd: file@Reader :: file@makeReader(
|file@getCurDir() ~ lib@cmdLine()[0])
while(!rd.term()) {ファイルの終端でなければ}
do cui@print(rd.readLine() ~ "\n") {1行読んで出力する}
end while
; ファイルを閉じる
do rd.fin()
end func
このプログラムをコンパイルして実行すると正しくファイルを読み込んで、コンソールに出力できていることが分かります。
#step3. 正規表現を書く
さて、ここではコマンドラインのプログラムのことは一時置いておいて、正規表現のプログラムを書いてみます。
func check(rg: regex@Regex, str: []char)
if(rg.match(str) <>& null)
do dbg@print(str ~ ": matched\n")
else
do dbg@print(str ~ ": unmatched\n")
end if
end func
func main()
var rg: regex@Regex
|:: regex@makeRegex("([\\t\\+]*)func(\\[.*\\])(\\S.*)")
do @check(rg, "func") {unmatched}
do @check(rg, "func[]abc(") {matched}
do @check(rg, "func\\[\\]\\(") {unmatched}
do @check(rg, "func[] abc(") {unmatched}
do @check(rg, "\t+func[_abc, _def]ghi(jkl)") {matched}
end func
makeRegexで呪文のようなものを書いています。この呪文のことを正規表現と言います。
- \t はタブ文字にマッチします。
- \+ は+にマッチします。
- [\\t\\+]はタブ文字か+にマッチします。
- [\\t\\+]*は0個以上のタブ文字か+にマッチします。
つまりこれは関数定義の"func"の前にある文字集合を表現しています。"end func"にはマッチしないようになっています。
- \\[は[にマッチします。
- \\]は]にマッチします。
- .*は任意の文字列にマッチします。
- \\[.*\\]は[と]ではさまれた文字列にマッチします。
func [_foo_option] bar(baz)という記述の[_foo_option]は、仕様では公開されていない関数のオプションを指定するためのものです。\\[.*\\]でその部分にマッチさせます。
- \\Sは空白でない文字にマッチします。
- .*は任意の文字列にマッチします。
つまり、\\S.*は空白でない文字で始まる文字列にマッチします。
さて、先ほどの正規表現はいくつかの括弧で区切られていました。これはどうやってつかうのでしょうか?
func main()
var rg: regex@Regex
|:: regex@makeRegex("([\\t\\+]*)func(\\[.*\\])(\\S.*)")
var m: [][]char :: rg.match("\t+func[_abc, _def]ghi(jkl)")
for i(0, ^m - 1)
do dbg@print(i.toStr() ~ ": " ~ m[i] ~ "\n")
end for
end func
つまり、正規表現のオブジェクトからmatchメソッドを呼び出すと、その0番目に文字列全体、1番目以降に括弧でくくった部分文字列がでてきます。これを使ってインデントし直せばプログラムの中心部分の完成です。
func main()
var rg: regex@Regex
|:: regex@makeRegex("([\\t\\+]*)func(\\[.*\\])(\\S.*)")
var input: []char :: "\t+func[_abc, _def]ghi(jkl)"
var m: [][]char :: rg.match(input)
var output: []char :: m[1] ~ "func " ~ m[2] ~ " " ~ m[3]
do dbg@print(output ~ "\n")
end func
#step4. プログラムをつなげてみる
では、step2とstep3をつなげてプログラムを完成させましょう。
func main()
; コマンドライン引数が1つでなければエラー
if(^lib@cmdLine() <> 1)
do cui@print("エラー: ファイル名を1つ指定してください。\n")
ret
end if
; 正規表現オブジェクトの作成
var rg: regex@Regex
|:: regex@makeRegex("([\\t\\+]*)func(\\[.*\\])(\\S.*)")
; ファイルを開く
var rd: file@Reader :: file@makeReader(
|file@getCurDir() ~ lib@cmdLine()[0])
while(!rd.term()) {ファイルの終端でなければ}
var line: []char :: rd.readLine() {1行読む}
var m: [][]char :: rg.match(line) {マッチさせてみる}
if(m <>& null) {マッチした場合}
; インデントする
do cui@print(m[1] ~ "func " ~ m[2] ~ " " ~ m[3] ~ "\n")
else {マッチしなかった場合}
; そのまま出力する
do cui@print(line ~ "\n")
end if
end while
; ファイルを閉じる
do rd.fin()
end func
最初のほうは正しく動作していますが、後ろのほうはそのまま出力されていますね。どうやら関数定義内の配列型の記述が邪魔をしているようです。こいつをどうにかするために、m[2]から[と]を排除してやります。
func main()
; コマンドライン引数が1つでなければエラー
if(^lib@cmdLine() <> 1)
do cui@print("エラー: ファイル名を1つ指定してください。\n")
ret
end if
; 正規表現オブジェクトの作成
var rg: regex@Regex
|:: regex@makeRegex("([\\t\\+]*)func(\\[[^\\[\\]]*\\])(\\S.*)")
; ファイルを開く
var rd: file@Reader :: file@makeReader(
|file@getCurDir() ~ lib@cmdLine()[0])
while(!rd.term()) {ファイルの終端でなければ}
var line: []char :: rd.readLine() {1行読む}
var m: [][]char :: rg.match(line) {マッチさせてみる}
if(m <>& null) {マッチした場合}
; インデントする
do cui@print(m[1] ~ "func " ~ m[2] ~ " " ~ m[3] ~ "\n")
else {マッチしなかった場合}
; そのまま出力する
do cui@print(line ~ "\n")
end if
end while
; ファイルを閉じる
do rd.fin()
end func
正規表現の記述だけ変えています。
- \\[は[にマッチします。\\]は]にマッチします。
- [\\[\\]]は[または]にマッチします。
- [^\\\\[\\\\]]は[と]以外にマッチします。
- [^\\\\[\\\\]]*は[と]が入っていない文字列にマッチします。
従ってプログラムが正しく動作するというわけです。
まとめ
どうですか?分からなかったですか?もし、この記事の内容についてご質問がございましたら、若草春男のTwitterアカウント(@HaruoWakakusa)のほうにツイートしていただければと思います。