1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

KuinAdvent Calendar 2018

Day 4

スクリプト言語Kuin

Last updated at Posted at 2018-12-03

Kuin Advent Calender 2018 4日目ということで、おはようございます(ネタは11/26に思いついたのでそれを書きます)。

:snowflake: :snowflake: :snowflake:

Kuin言語にはKuinエディタというエディタが付属されています。ところでKuinのライブラリも.knファイルで記述されているのですが、ご存知でしたでしょうか?
001.png
例えば、cuiライブラリはsys\cui.knで記述されているのですが、サクラエディタで開くとこんな感じです。
002.png
そんでもってこいつをKuinエディタで開いてやります。
003.png
これを違うフォルダのファイルに保存して、もう一度サクラエディタを開きます。
004.png
はい、Kuinエディタで開いて保存すると微妙にファイルのインデントが変わるのが分かるかと思います。

さて、Kuinのissueを開いてくいなちゃんにこのことを報告してKuinエディタを直してもらってもいいのですが、少し大人げないですね。他に方法はないのでしょうか。プログラマがよくやるやり方としては、PerlやRubyなどのスクリプト言語を書いてインデントを直したりします。でもWindowsにスクリプト言語をインストールするのは面倒。とぼとぼと職場から帰宅する途中で思いつきました。

「そうだ!Kuinでスクリプトを書けばいいじゃないか!」

step1. ファイルパスの取得

さて、まず何から始めればよいのでしょうか。ファイル名を1つ指定するとそのファイルパスを表示するプログラムを作成してみます。

test1.kn
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

こいつをデスクトップでコンパイルして実行すると正しい結果が得られることが分かります。
005.png

分かる人もいるかと思いますが一応解説を。

lib@cmdLine()というのはプログラムで指定したコマンドライン引数を文字列の配列で取得します。例えば、a.exe a b cなら、lib@cmdLine()の戻り値は["a", "b", "c"]です。a.exeを引数なしで実行した場合はlib@cmdLine()の戻り値は[]になります。このとき配列から要素を取り出そうとするとエラーになるのでif文を書いてエラーメッセージを出力するようにしています。

file@getCurDir()関数はカレントディレクトリを取得します。これはコマンドプロンプトの">"の前に書いてあるフォルダパスのことです。カレントディレクトリという概念があるおかげで、コマンドプロンプトのユーザーはコマンド実行時に長いファイルパスを書かなくて済むようになっています。

"~"は配列を連結する演算子です。Kuinでは文字列は文字の配列となっているのでこの演算子を使うことができます。

step2. ファイルの内容を表示するプログラムを作成する

では、ファイルを読み込んでその内容を出力するプログラムを作成しましょう。

cat.kn
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

このプログラムをコンパイルして実行すると正しくファイルを読み込んで、コンソールに出力できていることが分かります。
006.png

#step3. 正規表現を書く

さて、ここではコマンドラインのプログラムのことは一時置いておいて、正規表現のプログラムを書いてみます。

match1.kn
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.*は空白でない文字で始まる文字列にマッチします。

さて、先ほどの正規表現はいくつかの括弧で区切られていました。これはどうやってつかうのでしょうか?

match2.kn
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

このプログラムの出力結果は次のとおりです。
007.png

つまり、正規表現のオブジェクトからmatchメソッドを呼び出すと、その0番目に文字列全体、1番目以降に括弧でくくった部分文字列がでてきます。これを使ってインデントし直せばプログラムの中心部分の完成です。

match3.kn
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

008.png

#step4. プログラムをつなげてみる

では、step2とstep3をつなげてプログラムを完成させましょう。

indent.kn
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

009.png

最初のほうは正しく動作していますが、後ろのほうはそのまま出力されていますね。どうやら関数定義内の配列型の記述が邪魔をしているようです。こいつをどうにかするために、m[2]から[と]を排除してやります。

indent.kn
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

010.png

正規表現の記述だけ変えています。

  • \\[は[にマッチします。\\]は]にマッチします。
  • [\\[\\]]は[または]にマッチします。
  • [^\\\\[\\\\]]は[と]以外にマッチします。
  • [^\\\\[\\\\]]*は[と]が入っていない文字列にマッチします。

従ってプログラムが正しく動作するというわけです。

まとめ

どうですか?分からなかったですか?もし、この記事の内容についてご質問がございましたら、若草春男のTwitterアカウント(@HaruoWakakusa)のほうにツイートしていただければと思います。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?