2020/OCT/10
更新履歴
日付
変更内容 2020/SEP/09
新規作成 2020/SEP/09
実装
追加 2020/SEP/10
2重includeの防止
追加 2020/SEP/11
brush up
追加 2020/SEP/13
ツール
追加
kon_utを使った版
追加
kon_ut風の説明
追加2020/SEP/15
簡単な置換機能
追加 2020/SEP/16
サーチ・ディレクトリ
追加
再帰展開の抑止
追加
デフォルトの正規表現を変更
追加2020/SEP/24
にせファイル(というかバッファ)
追加 2020/SEP/26
バッファ名の自動生成
追加 2020/OCT/09
字下げ
追加
エラーなしオプション
追加2020/OCT/10
行末接続
追加
2重include許容オプション
追加
C言語のプリプロセッサのinclude行の展開のような事がしたく、 簡単なツールを作ってみました。
C言語のプリプロセッサをそのまま使えばそれまでですが、 あーでもない、こーでもないとPythonで処理を考えた時の記録です。
今更ですが、include行を展開する動作の説明です。
テキストデータ中に、指定のファイルをincludeする指示の行が現れると、 その行を指定ファイルの中身に置き換えます。
例えば、メールのテキスト。
head1.txt
kon pageの近藤です。
いつもお世話になっております。
head2.txt
kon pageの近藤でございます。
お日柄もよろしく、貴殿様におかれましてはご機嫌麗しゅう。
を用意しておいて、
mail1.txt
#include "head1.txt"
お電話頂いた件につきまして、
ご確認のほどよろしくお願い致します。
ならば
kon pageの近藤です。 いつもお世話になっております。 お電話頂いた件につきまして、 ご確認のほどよろしくお願い致します。
に展開。
mail2.txt
#include "head2.txt"
お電話頂いた件につきまして、
ご確認のほどよろしくお願い致します。
kon pageの近藤でございます。 お日柄もよろしく、貴殿様におかれましてはご機嫌麗しゅう。 お電話頂いた件につきまして、 ご確認のほどよろしくお願い致します。
に展開する動作になります。
例えば、C言語のhello worldでお馴染みのヘッダファイルstdio.h。
#include <stdio.h>
の行がstdio.hの中身に置き換えられます。
stdio.hの中身からも別のファイルをincludeしてます。
grepしてみると
$ grep include /usr/include/stdio.h | head * This product includes software developed by the University of #include <sys/cdefs.h> #include <Availability.h> #include <_types.h> /* DO NOT REMOVE THIS COMMENT: fixincludes needs to see: * __gnuc_va_list and include <stdarg.h> */ #include <sys/_types/_va_list.h> #include <sys/_types/_size_t.h> #include <sys/_types/_null.h> #include <sys/stdio.h>
ネスト(入れ子)してます。
ファイルの中身への置き換えは、再帰的に処理せねばなりません。
ちょっと実際にプリプロセッサ展開を試してみます。
$ gcc -E -dD /usr/include/stdio.h | head # 1 "/usr/include/stdio.h" # 1 "<built-in>" 1 # 1 "<built-in>" 3 #define __llvm__ 1 #define __clang__ 1 #define __clang_major__ 8 #define __clang_minor__ 0 #define __clang_patchlevel__ 0 #define __clang_version__ "8.0.0 (clang-800.0.38)" #define __GNUC_MINOR__ 2
$ wc -l /usr/include/stdio.h 495 /usr/include/stdio.h
$ gcc -E -dD /usr/include/stdio.h | wc -l 2581
495行のstdio.hが2581行の内容に展開されます。
まずinclude処理について、「ゆるーく」実装を考えてみます。
処理対象のテキストデータは、標準入力から読み込み、 展開結果を標準出力に書き出すことにします。
while True s = sys.stdin.readline() if not s: break if is_inc( s ): s = inc_exp( s ) sys.stdout.write( s )
ちょー簡単。
って、いやいや。
is_inc( s ) はともかく、inc_exp( s )が大変なのです。
inc_exp( s ) の処理について、「ゆるーく」考えてみます。
メインループと同じ処理に戻ってきます。再帰です。
def inc_exp(s): path = get_path( s ) s = get_text( path ) lines = s.split( '\n' ) for s in lines: if is_inc( s ): s = inc_exp( s ) sys.stdout.write( s )
いや。
メインループのsys.stdin.readline()は末尾に改行コードが付いてます。
split()で行にバラすと改行コードはありません。
sys.stdout.write( s ) は print( s ) とすべきか?
ここは、split() したら join() で戻したいところ。
inc_exp( s )は、展開した結果を行のリストで返すことにしてみます。
def inc_exp(s): path = get_path( s ) s = get_text( path ) lines = s.split( '\n' ) ret = [] for s in lines: if is_inc( s ): ret.extend( inc_exp( s ) ) else: ret.append( s ) return ret
てな再帰にしておいて、
メインループ側は
s = sys.stdin.read() lines = s.split( '\n' ) ret = [] for s in lines: if is_inc( s ): ret.extend( inc_exp( s ) ) else: ret.append( s ) s = '\n'.join( ret ) sys.stdout.write( s )
いやいや。
メインループ側とinc_exp()側で、同じ処理書きすぎでしょ。
inc_exp( s ) の引数sは、改行を含む複数行のテキストデータということで。
1行分の展開処理は内部関数 inc_exp_line( s ) に。
def inc_exp(s): def inc_exp_line(s): path = get_path( s ) s = get_text( path ) return inc_exp( s ) lines = s.split( '\n' ) ret = [] for s in lines: if is_inc( s ): ret.extend( inc_exp_line( s ) ) else: ret.append( s ) return ret
メインループ側は
s = sys.stdin.read() lines = inc_exp( s ) s = '\n'.join( lines ) sys.stdout.write( s )
あー。
それなら inc_exp() の返す値も行のリストにしなくてもいいのでは。
def inc_exp(s): def inc_exp_line(s): path = get_path( s ) s = get_text( path ) return inc_exp( s ) lines = s.split( '\n' ) ret = [] for s in lines: if is_inc( s ): ret.append( inc_exp_line( s ) ) else: ret.append( s ) return '\n'.join( ret )
そしてメインループ側は
s = sys.stdin.read() s = inc_exp( s ) sys.stdout.write( s )
1 lineなら
sys.stdout.write( inc_exp( sys.stdin.read() ) )
inc_exp( s )の後半、append() 同じなので、
def inc_exp(s): def inc_exp_line(s): path = get_path( s ) s = get_text( path ) return inc_exp( s ) func = lambda s: inc_exp_line( s ) if is_inc( s ) else s return '\n'.join( map( func, s.split( '\n' ) ) )
1行の処理はinc_exp_line()側に入れるべきで、もうひとこえ。
def inc_exp(s): def inc_exp_line(s): if not is_inc( s ): return s path = get_path( s ) s = get_text( path ) return inc_exp( s ) return '\n'.join( map( inc_exp_line, s.split( '\n' ) ) )
当初より、かなりシンプルになりました。
is_inc( s ) | s がinclude指示行か判定 |
get_path( s ) | include指示行 s からファイルパスを取得 |
get_text( path ) | get_path( s )で取得したpathのファイルの中身のテキストを返す |
こんな感じでした。
指示行の仕様に依存する箇所なので、 1つの関数にまとめて扱っておきます。
inc_text( s ) | s がinclude指示行でなければ、Noneを返す
s がinclude指示行ならば、ファイルの中身のテキストを返す s がinclude指示行だが、ファイルの中身の取得に失敗したらFalseを返す (ファイルが見つからないなど) |
この inc_text() を、呼び出し元から与えるようにしておけば、 include処理そのものは「include指示行の仕様」の影響を受けずに済みます。
def inc_exp(s, inc_text): def inc_exp_line(s): r = inc_text( s ) return s if r in ( None, False ) else inc_exp( r, inc_text ) return '\n'.join( map( inc_exp_line, s.split( '\n' ) ) )
メインループ側は
s = sys.stdin.read() s = inc_exp( s, inc_text ) sys.stdout.write( s )
「include行の仕様」に依存する箇所の実装になります。
とりあえず、簡単な仕様で試してみます。
hello @head1.txt world @/tmp/head2.txt
こんな感じで、使います。
シンプルな仕様なので、実装もとても簡単。
import os def inc_text(s): if not s.startswith( '@' ): return None path = s[ 1: ] r = False if not os.path.exists( path ): return r with open( path, 'rb' ) as f: try: r = f.read().decode() except: pass return r
とりあえず版 inc1.py にまとめて試してみます。
テキストデータはasciiコードの範囲のみで。
smp1.txt
hello
@foo.txt
world
foo.txt
foo
FOO
$ cat smp1.txt | ./inc1.py hello foo FOO world
おっと、意図しない改行が、FOOとworldの間に。
素直に考えると 「@foo.txt + 改行」が 「「foo + 改行 + FOO + 改行」 に展開されて欲しいところです。
美しい対策とは言えませんが...
読み込むファイルの末尾が改行で終っていたら、 join()で追加されるのを見込んで削除しておきます。
差分
--- inc1.py 2020-09-09 22:22:57.000000000 +0900
+++ inc2.py 2020-09-09 22:22:59.000000000 +0900
@@ -15,6 +15,8 @@
with open( path, 'rb' ) as f:
try:
r = f.read().decode()
+ if r[ -1 ] == '\n':
+ r = r[ : -1 ]
except:
pass
return r
$ cat smp1.txt | ./inc2.py hello foo FOO world
OK。
$ cat smp1.txt | python2 inc2.py hello foo FOO world
$ cat smp1.txt | python3 inc2.py hello foo FOO world
python2, python3 OK。
試してみます。
先の
head1.txt
kon pageの近藤です。
いつもお世話になっております。
$ nkf -g head1.txt UTF-8
smp2.txt
hello
@head1.txt
world
展開される側のsmp2.txtはasciiの範囲だけで。
$ cat smp2.txt | python2 inc2.py hello @head1.txt world
う。python2で展開されてない...です。
$ cat smp2.txt | python3 inc2.py Traceback (most recent call last): File "inc2.py", line 35, in <module> sys.stdout.write( s ) UnicodeEncodeError: 'ascii' codec can't encode characters in position 14-19: ordinal not in range(128)
さらにpython3だとエラー。
そう。
Python2とPython3での日本語文字列対応について の通り、ややこしいです。
まずpython2で
r = f.read().decode()
のdecode()が通りません。
とりあえずdecode( 'utf-8' )で。
最後の出力は
sys.stdout.write( s.encode( 'utf-8' ) )
にするとpython2ではOK。
$ cat smp2.txt | python2 inc3.py hello kon pageの近藤です。 いつもお世話になっております。 world
python3では通りません。
$ cat smp2.txt | python3 inc3.py Traceback (most recent call last): File "inc3.py", line 35, in <module> sys.stdout.write( s.encode( 'utf-8' ) ) TypeError: write() argument must be str, not bytes
pythonのユーティリ・ティプログラム 2020冬 nkf.py の導入は大げさかも?
import six
で乗り切れるか試してみます。
差分
--- inc2.py 2020-09-09 22:22:59.000000000 +0900
+++ inc3.py 2020-09-09 22:23:52.000000000 +0900
@@ -2,6 +2,7 @@
import sys
import os
+import six
def inc_text(s):
if not s.startswith( '@' ):
@@ -14,7 +15,7 @@
with open( path, 'rb' ) as f:
try:
- r = f.read().decode()
+ r = f.read().decode( 'utf-8' )
if r[ -1 ] == '\n':
r = r[ : -1 ]
except:
@@ -32,5 +33,7 @@
if __name__ == "__main__":
s = sys.stdin.read()
s = inc_exp( s, inc_text )
- sys.stdout.write( s )
+
+ f = sys.stdout if six.PY2 else sys.stdout.buffer
+ f.write( s.encode( 'utf-8' ) )
# EOF
$ cat smp2.txt | python2 inc3.py hello kon pageの近藤です。 いつもお世話になっております。 world
$ cat smp2.txt | python3 inc3.py hello kon pageの近藤です。 いつもお世話になっております。 world
OK。
展開される側もUTF-8のテキストにして、 標準入力のリードも対応してみます。
差分
--- inc3.py 2020-09-09 22:23:52.000000000 +0900
+++ inc4.py 2020-09-09 22:24:27.000000000 +0900
@@ -31,7 +31,9 @@
return '\n'.join( map( inc_exp_line, s.split( '\n' ) ) )
if __name__ == "__main__":
- s = sys.stdin.read()
+ f = sys.stdin if six.PY2 else sys.stdin.buffer
+ s = f.read().decode( 'utf-8' )
+
s = inc_exp( s, inc_text )
f = sys.stdout if six.PY2 else sys.stdout.buffer
smp3.txt
関係各位
@head1.txt
以上
$ nkf -g smp3.txt UTF-8
$ cat smp3.txt | python2 inc4.py 関係各位 kon pageの近藤です。 いつもお世話になっております。 以上
$ cat smp3.txt | python3 inc4.py 関係各位 kon pageの近藤です。 いつもお世話になっております。 以上
python2, python3 OK。
なんとか乗り切りました。
$ echo @foo.txt | ./inc4.py foo FOO
に対して、例えばファイルが見つからなかった場合
$ echo @bar.txt | ./inc4.py @bar.txt
include指示行が、そのまま残って表示されてます。
何かエラーのアクションが欲しいところ。
差分
--- inc4.py 2020-09-09 22:24:27.000000000 +0900
+++ inc5.py 2020-09-09 22:25:00.000000000 +0900
@@ -22,11 +22,17 @@
pass
return r
+def err(s):
+ sys.stderr.write( 'err ' + s + '\n' )
+ sys.exit( 1 )
+
def inc_exp(s, inc_text):
def inc_exp_line(s):
r = inc_text( s )
- return s if r in ( None, False ) else inc_exp( r, inc_text )
+ if r == False:
+ err( s )
+ return s if r == None else inc_exp( r, inc_text )
return '\n'.join( map( inc_exp_line, s.split( '\n' ) ) )
$ echo @bar.txt | ./inc5.py err @bar.txt
これで、とりあえずエラーが発生してる事は、なんとか分かります。
もうちょっと、処理中のファイル名や行番号の情報も欲しいなと。
差分
--- inc5.py 2020-09-09 22:25:00.000000000 +0900
+++ inc6.py 2020-09-09 22:25:28.000000000 +0900
@@ -18,23 +18,25 @@
r = f.read().decode( 'utf-8' )
if r[ -1 ] == '\n':
r = r[ : -1 ]
+ return ( r, path ) # !
except:
pass
return r
-def err(s):
- sys.stderr.write( 'err ' + s + '\n' )
+def err(s, path, i):
+ sys.stderr.write( 'err {}, {} L{}\n'.format( s, path, i+1 ) )
sys.exit( 1 )
-def inc_exp(s, inc_text):
+def inc_exp(s, inc_text, path='-'):
- def inc_exp_line(s):
+ def inc_exp_line(i_s):
+ (i, s) = i_s
r = inc_text( s )
if r == False:
- err( s )
- return s if r == None else inc_exp( r, inc_text )
+ err( s, path, i )
+ return s if r == None else inc_exp( r[ 0 ], inc_text, r[ 1 ] )
- return '\n'.join( map( inc_exp_line, s.split( '\n' ) ) )
+ return '\n'.join( map( inc_exp_line, enumerate( s.split( '\n' ) ) ) )
if __name__ == "__main__":
f = sys.stdin if six.PY2 else sys.stdin.buffer
$ echo @bar.txt | ./inc6.py err @bar.txt, - L1
'-' (標準入力)の1行目の @bar.txt でエラー。OK。
おっと、ここまでネストの動作確認をしておりませんでしたね。
あと、inc_text()の返す値の仕様が少し変りました。
inc_text( s ) | 変更前
inc5.py |
s がinclude指示行でなければ、Noneを返す
s がinclude指示行ならば、ファイルの中身のテキストを返す s がinclude指示行だが、ファイルの中身の取得に失敗したらFalseを返す (ファイルが見つからないなど) |
変更後
inc6.py |
s がinclude指示行でなければ、Noneを返す
s がinclude指示行ならば、ファイルの中身のテキストと、ファイルパス文字列の組み(タプル)を返す s がinclude指示行だが、ファイルの中身の取得に失敗したらFalseを返す (ファイルが見つからないなど) |
nest.txt
nest test
--> smp1.txt
@smp1.txt
<--
--> foo.txt
@foo.txt
<-- foo.txt
$ cat nest.txt | ./inc6.py nest test --> smp1.txt hello foo FOO world <-- --> foo.txt foo FOO <-- foo.txt
OK。
ng.txt
ng test start
@bar.txt
ng test end
$ cat ng.txt | ./inc6.py err @bar.txt, - L3
$ echo @ng.txt | ./inc6.py err @bar.txt, ng.txt L3
ng.txtの3行目の @bar.txt でエラーと出てますね。
OK。
2つのファイルで相互に参照するinclude指示行があると、 展開し続けて資源を使い尽くしてしまいます。
せっかくなので、実際に試してみましょう。
$ cat loop.txt @loop.txt
$ cat loop.txt | ./inc6.py Traceback (most recent call last): File "./inc6.py", line 45, in <module> s = inc_exp( s, inc_text ) File "./inc6.py", line 39, in inc_exp return '\n'.join( map( inc_exp_line, enumerate( s.split( '\n' ) ) ) ) File "./inc6.py", line 37, in inc_exp_line return s if r == None else inc_exp( r[ 0 ], inc_text, r[ 1 ] ) File "./inc6.py", line 39, in inc_exp : File "./inc6.py", line 23, in inc_text pass RecursionError: maximum recursion depth exceeded while calling a Python object
相互の場合も
$ cat loop_a.txt @loop_b.txt $ cat loop_b.txt @loop_a.txt
$ cat loop_a.txt | ./inc6.py Traceback (most recent call last): File "./inc6.py", line 45, in <module> s = inc_exp( s, inc_text ) File "./inc6.py", line 39, in inc_exp return '\n'.join( map( inc_exp_line, enumerate( s.split( '\n' ) ) ) ) : r = inc_text( s ) File "./inc6.py", line 23, in inc_text pass RecursionError: maximum recursion depth exceeded while calling a Python object
パスの履歴を作って、2重includeを検出したら、対策してみます。
アクションとしては、エラー表示で停止するよりも、 同じファイルの2度目のincludeを「そっと、しないでおく」だけにしておきます。
なんとなく、その方が良い気がして。
差分
--- inc6.py 2020-09-09 22:25:28.000000000 +0900
+++ inc7.py 2020-09-10 12:43:31.000000000 +0900
@@ -23,18 +23,28 @@
pass
return r
-def err(s, path, i):
- sys.stderr.write( 'err {}, {} L{}\n'.format( s, path, i+1 ) )
+paths = [ '-' ]
+
+def err(s, i):
+ sys.stderr.write( 'err {}, {} L{}\n'.format( s, paths[ -1 ], i+1 ) )
sys.exit( 1 )
-def inc_exp(s, inc_text, path='-'):
+def inc_exp(s, inc_text):
def inc_exp_line(i_s):
(i, s) = i_s
r = inc_text( s )
if r == False:
- err( s, path, i )
- return s if r == None else inc_exp( r[ 0 ], inc_text, r[ 1 ] )
+ err( s, i )
+ if r == None:
+ return s
+ (s, path) = r
+ if path in paths:
+ return '' # !
+ paths.append( path )
+ s = inc_exp( s, inc_text )
+ paths.pop()
+ return s
return '\n'.join( map( inc_exp_line, enumerate( s.split( '\n' ) ) ) )
引数pathを廃止して、グローバル変数pathsにリストで保持します。
グローバル変数を使ったこういう対応も、規模の小さいソースでは「あり」かと。
$ cat loop.txt | ./inc7.py $ cat loop_a.txt | ./inc7.py
この場合、include指示行しか無い場合だったので、展開結果も皆無でした。
ちゃんと動作してるのだろうか?
ややこしいデータで試してみます。
loop_c.txt
loop_c start
@loop_d.txt
loop_c ck1
@loop_c.txt
loop_c end
loop_d.txt
loop_d start
@loop_e.txt
loop_d ck1
@loop_d.txt
loop_d ck2
@loop_c.txt
loop_d end
loop_e.txt
loop_e start
@loop_e.txt
loop_e ck1
@loop_d.txt
loop_e ck2
@loop_c.txt
loop_e end
$ cat loop_c.txt | ./inc7.py
実行結果
loop_c start ;;; stdin
loop_d start
loop_e start
;;; ここは @loop_e.txt in loop_e , OK
loop_e ck1
;;; ここは @loop_d.txt in loop_e , OK
loop_e ck2
loop_c start ;;; 最初のloop_c.txtは標準入力からなので、path記録的には初回のloop_c
;;; ここは @loop_d.txt in loop_c , OK
loop_c ck1
;;; ここは @loop_c.txt in loop_c , OK
loop_c end
;;; ここは @loop_c.txt in loop_e , OK
loop_e end
loop_d ck1
;;; ここは @loop_d.txt in loop_d , OK
loop_d ck2
loop_c start
;;; ここは @loop_d.txt in loop_c , OK
loop_c ck1
;;; ここは @loop_c.txt in loop_c , OK
loop_c end
;;; ここは @loop_c.txt in loop_d , OK
loop_d end
loop_c ck1 ;;; stdin
loop_c start
loop_d start
loop_e start
;;; ここは @loop_e.txt in loop_e , OK
loop_e ck1
;;; ここは @loop_d.txt in loop_e , OK
loop_e ck2
;;; ここは @loop_c.txt in loop_e , OK
loop_e end
loop_d ck1
;;; ここは @loop_d.txt in loop_d , OK
loop_d ck2
;;; ここは @loop_c.txt in loop_d , OK
loop_d end
loop_c ck1
;;; ここは @loop_c.txt in loop_c , OK
loop_c end
;;; ここは @loop_c.txt in loop_c (stdin) , OK
loop_c end ;;; stdin
OK。ああ、ややこしい。
$ ls bar.txt ls: bar.txt: No such file or directory
$ echo @bar.txt | ./inc7.py err @bar.txt, - L1
$ cat ng.txt ng test start @bar.txt ng test end
$ echo @ng.txt | ./inc7.py err @bar.txt, ng.txt L3
$ cat smp3.txt 関係各位 @head1.txt 以上
$ cat head1.txt kon pageの近藤です。 いつもお世話になっております。
$ cat smp3.txt | ./inc7.py 関係各位 kon pageの近藤です。 いつもお世話になっております。 以上
OK。
さらに、そぎ落してみます。
差分
--- inc7.py 2020-09-10 12:43:31.000000000 +0900
+++ inc8.py 2020-09-12 02:24:06.000000000 +0900
@@ -4,46 +4,43 @@
import os
import six
-def inc_text(s):
+def inc_text(s, paths):
if not s.startswith( '@' ):
return None
path = s[ 1: ]
r = False
- if not os.path.exists( path ):
- return r
-
- with open( path, 'rb' ) as f:
- try:
- r = f.read().decode( 'utf-8' )
- if r[ -1 ] == '\n':
- r = r[ : -1 ]
- return ( r, path ) # !
- except:
- pass
+ if os.path.exists( path ):
+ with open( path, 'rb' ) as f:
+ try:
+ r = f.read().decode( 'utf-8' )
+ paths.append( path )
+ except:
+ pass
return r
paths = [ '-' ]
+is_dbl_paths = lambda : paths[ -1 ] in paths[ : -1 ]
+
def err(s, i):
sys.stderr.write( 'err {}, {} L{}\n'.format( s, paths[ -1 ], i+1 ) )
sys.exit( 1 )
+cut_tail_nl = lambda s: s[ : -1 ] if s and s[ -1 ] == '\n' else s
+
def inc_exp(s, inc_text):
def inc_exp_line(i_s):
(i, s) = i_s
- r = inc_text( s )
+ r = inc_text( s, paths )
if r == False:
err( s, i )
- if r == None:
- return s
- (s, path) = r
- if path in paths:
- return '' # !
- paths.append( path )
- s = inc_exp( s, inc_text )
- paths.pop()
+ if r != None:
+ s = ''
+ if not is_dbl_paths():
+ s = inc_exp( cut_tail_nl( r ), inc_text )
+ paths.pop()
return s
return '\n'.join( map( inc_exp_line, enumerate( s.split( '\n' ) ) ) )
inc_text()の仕様、ちょっと変えました。
変更前
inc6.py |
inc_text( s ) | s がinclude指示行でなければ、Noneを返す
s がinclude指示行ならば、ファイルの中身のテキストと、ファイルパス文字列の組み(タプル)を返す s がinclude指示行だが、ファイルの中身の取得に失敗したらFalseを返す (ファイルが見つからないなど) |
変更後
inc8.py |
inc_text( s, paths ) | s がinclude指示行でなければ、Noneを返す
s がinclude指示行ならば、ファイルパス文字列をリストpathsの末尾に追加し、ファイルの中身のテキストを返す s がinclude指示行だが、ファイルの中身の取得に失敗したらFalseを返す (ファイルが見つからないなど) |
inc_text(s, paths)だけは、include指示行の仕様依存箇所なので、 あとで外部ファイルに出せるようにしたく。
グローバル変数pathsを、直接使わないようにしてます。
$ cat smp3.txt | ./inc8.py 関係各位 kon pageの近藤です。 いつもお世話になっております。 以上
$ echo @ng.txt | ./inc8.py err @bar.txt, ng.txt L3
$ cat loop_c.txt | ./inc7.py > nest_res.txt
$ cat loop_c.txt | ./inc8.py | diff -u nest_res.txt - $
ツールの形に仕立ててみます。
ここまで、include指定行の仕様によって、 inc_text(s, paths) 関数を差し替えれば、 対応できる作りにしてきました。
よく使いそうなinclude指定行の仕様の inc_text(s, paths) をいくつか用意して、 そこそこ使えるツールを目指してみます。
さらに、コマンドライン行や設定ファイルから、あるいは環境変数からでも、 細やかにinclude指定行の仕様を追加できれば、 なお良しです。
ここまで試したのは、超シンプルな仕様でした。
基本的に、1行で1つのファイルを指定する事は、ゆるぎ無い「しばり」です。
例えば、C言語のプリプロセッサの場合
のような感じでしょうか。
そして、一部の「ファイルパスを含むような形式のファイル名」は、 複数のincludeパスとして指定されたディレクトリ以下で、 合致するファイルを探します。
includeパスは、コンパイル時に "-I ディレクトリ" の形式で指定する「あれ」です。
別に「C言語のヘッダファイルを展開する事」が目的じゃないので、 もっとシンプルでも十分なのですが...
の基本は変わりません。
前半の2つが何とかなれば、後半の2つはこれまでの処理通りです。
行頭からの形式が、指定のパターンに合致してるかどうか?
結局、そういう事に落とし込まれる気がします。
「正規表現」でしょうか。
となると「いにしえ」からのgrep, sedコマンド。
pythonなら「re」モジュールでしょうか。
これまでほとんど使った事が無かったですが、この機会に試してみます。
$ python >>> import re >>> p = re.compile( r'^@' ) >>> p.match( 'foo' ) >>> p.match( '@foo' ) <_sre.SRE_Match object; span=(0, 1), match='@'>
>>> p2 = re.compile( r'^# *include +' ) >>> p2.match( 'foo' ) >>> p2.match( '#include <stdio.h>' ) <_sre.SRE_Match object; span=(0, 9), match='#include '> >>> p2.match( '# include "stdio.h"' ) <_sre.SRE_Match object; span=(0, 12), match='# include '> >>> p2.match( ' #include <stdio.h>' ) >>>
ふーむ。
sedならば
$ echo foo | sed -n -e 's/^@\(.*\)$/\1/p' $ $ echo @foo.txt | sed -n -e 's/^@\(.*\)$/\1/p' foo.txt $ echo @/tmp/foo.txt | sed -n -e 's/^@\(.*\)$/\1/p' /tmp/foo.txt $ pre $ echo "#include <stdio.h>" | sed -n -e 's/^# *include *<\(.*\)>$/\1/p' stdio.h $ echo '# include "stdio.h"' | sed -n -e 's/^# *include *"\(.*\)"$/\1/p' stdio.h
python reモジュールなら
$ python >>> import re >>> p = re.compile( r'^@(.+)$' ) >>> p.sub( r'\1', '@foo.txt' ) 'foo.txt' >>> p.sub( r'\1', '@/tmp/foo.txt' ) '/tmp/foo.txt' >>> p2 = re.compile( r'^# *include +<(.*)>$' ) >>> p2.sub( r'\1', '#include <stdio.h>' ) 'stdio.h' >>> p2.sub( r'\1', '# include <stdio.h>' ) 'stdio.h' >>> p3 = re.compile( r'^# *include +"(.*)"' ) >>> p3.sub( r'\1', '#include "stdio.h"' ) 'stdio.h' >>> p3.sub( r'\1', '#include "sys/time.h"' ) 'sys/time.h'
うーむ、なるほど。
reモジュールを使うのであれば、正規表現の文字列を1つ与えるだけで良さそうです。
マッチするかどうかで、include指定行かどうかを判定。
マッチするなら、r'\1' でファイルパスを取り出せるような正規表現にしておきます。
inc8.py を下敷きにして、第一弾。
ここまでinc_exp()ではグローバル変数pathsを直接参照せずに、 引数でpathsを与えるようにしてきましたが...
include指定行の仕様依存箇所は、正規表現の文字列として切り出してしまったので、 inc_exp()を別ファイルで与えるような計画そのものがナンセンスになってしまいました。
もうグローバル変数pathsの参照ありで、書いてみます。
$ cat smp3.txt | ./inc.py 関係各位 kon pageの近藤です。 いつもお世話になっております。 以上
$ pwd /Users/kondoh/kon_page/inc $ ( cd /tmp ; echo "# include <head1.txt>" | /Users/kondoh/kon_page/inc/inc.py ) kon pageの近藤です。 いつもお世話になっております。
$ echo @ng.txt | ./inc.py err @bar.txt, ng.txt L3
$ cat loop_c.txt | ./inc7.py > nest_res.txt
$ cat loop_c.txt | ./inc.py | diff -u nest_res.txt - $
日本語がUTF-8限定なのもアレなので、 pythonのユーティリティ・プログラム 2020冬 nkf.py を導入します。
他にも、使えるところは出来るだけ kon_ut から引っぱってきてみます。
そして、 kon_ut に、 inc_ut.py を部品として追加しておきます。
$ cat smp3.txt | ./inc_ut.py 関係各位 kon pageの近藤です。 いつもお世話になっております。 以上
$ pwd /Users/kondoh/kon_page/inc $ ( cd /tmp ; echo "# include <head1.txt>" | /Users/kondoh/kon_page/inc/inc_ut.py ) kon pageの近藤です。 いつもお世話になっております。
$ nkf -j smp3.txt | nkf -g ISO-2022-JP $ nkf -j smp3.txt | ./inc_ut.py | nkf -g ISO-2022-JP $ nkf -j smp3.txt | ./inc_ut.py | nkf -u 関係各位 kon pageの近藤です。 いつもお世話になっております。 以上
$ echo @ng.txt | ./inc_ut.py err @bar.txt, ng.txt L3
$ cat loop_c.txt | ./inc7.py > nest_res.txt
$ cat loop_c.txt | ./inc_ut.py | diff -u nest_res.txt - $
少し追加してみました。
v2.patch
diff -ur v1/inc_ut.py v2/inc_ut.py
--- v1/inc_ut.py 2020-09-13 22:47:44.000000000 +0900
+++ v2/inc_ut.py 2020-09-15 04:10:46.000000000 +0900
@@ -28,23 +28,35 @@
def new( lst=[] ):
lst += [
- r'^# *include +(.*)$',
- r'^@ *include +(.*)$',
- r'^@ *inc +(.*)$',
- r'^@(.*)$',
+ r'^# *include +(\S*)',
+ r'^@ *include +(\S*)',
+ r'^@ *inc +(\S*)',
+ r'^@(\S*)',
]
pat_lst = list( map( re.compile, lst ) )
def match_path(s):
for p in pat_lst:
- if p.match( s ):
- return cut_quote( p.sub( r'\1', s ) )
- return None
+ m = p.match( s )
+ if m and m.groups():
+ path = cut_quote( m.group( 1 ) )
+ opts = p.sub( '', s ).split()
+ return ( path, opts )
+ return ( None, '' )
paths = [ '-' ]
+ def cvt_opts(r, opts):
+ for opt in opts:
+ delim = '='
+ if delim in opt:
+ i = opt.index( delim )
+ (k, v) = ( opt[ : i ], opt[ i + len( delim ) : ] )
+ r = r.replace( k, v )
+ return r
+
def inc_text(s):
- path = match_path( s )
+ (path, opts) = match_path( s )
if path == None:
return None
@@ -60,6 +72,7 @@
if r == False:
return False
paths.append( path )
+ r = cvt_opts( r, opts )
return cut_tail_nl( r )
return False
@@ -83,11 +96,10 @@
inc = new()
b = nkf.get_stdin()
- opt = nkf.guess( b )
- s = nkf.dec( nkf.cvt( b, '-u' ) )
+ (s, opt) = nkf.to_str( b )
s = inc.exp( s )
- b = nkf.cvt( nkf.enc( s ), opt )
+ b = nkf.str_to( s, opt )
nkf.put_stdout( b )
# EOF
include指示行の後続に、文字列の置換の指定を追加できるようにしてみました。
文字列の置換は、次の形式で指定します。
置換前の文字列=置換後の文字列
空白で区切って、複数指定できます。
簡単なパース処理にしてるので、 置換前の文字列や置換後の文字列に空白を含めることはできません。
引用符でくくってもダメです。
$ echo "@foo.txt" | ./inc_ut.py foo FOO $ echo "@foo.txt FOO=BAR o=0" | ./inc_ut.py f00 BAR
$ cat smp3.txt | ./inc_ut.py 関係各位 kon pageの近藤です。 いつもお世話になっております。 以上
$ pwd /Users/kondoh/kon_page/inc $ ( cd /tmp ; echo "# include <head1.txt>" | /Users/kondoh/kon_page/inc/inc_ut.py ) kon pageの近藤です。 いつもお世話になっております。
$ nkf -j smp3.txt | nkf -g ISO-2022-JP $ nkf -j smp3.txt | ./inc_ut.py | nkf -g ISO-2022-JP $ nkf -j smp3.txt | ./inc_ut.py | nkf -u 関係各位 kon pageの近藤です。 いつもお世話になっております。 以上
$ echo @ng.txt | ./inc_ut.py err @bar.txt, ng.txt L3
$ cat loop_c.txt | ./inc7.py > nest_res.txt
$ cat loop_c.txt | ./inc_ut.py | diff -u nest_res.txt - $
これまで、includeするファイルのサーチ・ディレクトリは、 次の仕様で固定してました。
相対ファイルパスは、次のディレクトリの順で探す。
import先のソースのあるディレクトリなども追加したく、 更新しました。
v3.patch
diff -ur v2/inc_ut.py v3/inc_ut.py
--- v2/inc_ut.py 2020-09-15 04:10:46.000000000 +0900
+++ v3/inc_ut.py 2020-09-16 04:13:58.000000000 +0900
@@ -55,6 +55,8 @@
r = r.replace( k, v )
return r
+ search_dirs = [ os.path.dirname( __file__ ) ]
+
def inc_text(s):
(path, opts) = match_path( s )
if path == None:
@@ -62,7 +64,7 @@
path_lst = [ path ]
if not path.startswith( '/' ):
- path_lst.append( os.path.join( os.path.split( __file__ )[ 0 ], path ) )
+ path_lst += list( map( lambda dir_: os.path.join( dir_, path ), search_dirs ) )
for path in path_lst:
if os.path.exists( path ):
オブジェクトのアトリビュート.search_dirsとしてリストが用意されています。
exp()実行時に、指定の相対パスのファイルが、 カレントディレクトリにファイルが見つからなかった場合、 search_dirs のリストの先頭から順に、ファイルを探します。
初期値は
search_dirs = [ os.path.dirname( __file__ ) ]
として、inc_ut.py の存在するディレクトリだけが入ってます。
$ cd /tmp $ ls -l foo.txt ls: cannot access 'foo.txt': No such file or directory
$ cat /home/kondoh/kon_page/kon_ut/foo.txt foo FOO
$ cat /tmp/bar/foo.txt foo_foo
$ python >>> import inc_ut >>> inc = inc_ut.new() >>> inc.search_dirs ['/home/kondoh/kon_page/kon_ut'] >>> print( inc.exp( '@foo.txt' ) ) foo FOO >>> inc.search_dirs.insert( 0, '/tmp/bar' ) >>> inc.search_dirs ['/tmp/bar', '/home/kondoh/kon_page/kon_ut'] >>> print( inc.exp( '@foo.txt' ) ) foo_foo
最初に安易に
などと掲げた仕様のために、例えば差分ファイルをinclude展開しようとすると
diff -ur v2/inc_ut.py v3/inc_ut.py --- v2/inc_ut.py 2020-09-15 04:10:46.000000000 +0900 +++ v3/inc_ut.py 2020-09-16 04:13:58.000000000 +0900 @@ -55,6 +55,8 @@ r = r.replace( k, v ) return r + search_dirs = [ os.path.dirname( __file__ ) ] + :
4行目の
@@ -55,6 +55,8 @@
の部分が、正規表現
r'^@(\S*)'
にマッチしてしまいます。
"@ -55,6 +55,8 @@" というファイルを探して見つからずにエラー表示で停止 orz
再帰的な展開を抑止するオプション指定を追加します。
-nr
include指示行にを追加すると、再帰的な展開をしません。
no recursive です。
v4.patch
diff -ur v3/inc_ut.py v4/inc_ut.py
--- v3/inc_ut.py 2020-09-16 04:13:58.000000000 +0900
+++ v4/inc_ut.py 2020-09-16 05:06:21.000000000 +0900
@@ -74,8 +74,7 @@
if r == False:
return False
paths.append( path )
- r = cvt_opts( r, opts )
- return cut_tail_nl( r )
+ return empty.new( s=cut_tail_nl( r ), opts=opts )
return False
def exp_line(i, s):
@@ -86,7 +85,9 @@
return s
if r == True:
return ''
- s = exp( r )
+ s = cvt_opts( r.s, r.opts )
+ if '-nr' not in r.opts:
+ s = exp( s )
paths.pop()
return s
$ echo "@nest.txt" | ./inc_ut.py nest test --> smp1.txt hello foo FOO world <-- --> foo.txt foo FOO <-- foo.txt
$ echo "@nest.txt -nr" | ./inc_ut.py nest test --> smp1.txt @smp1.txt <-- --> foo.txt @foo.txt <-- foo.txt
再帰展開の抑止 だけでは、おさまりませんでした。
例えば、
$ cat head1.txt kon pageの近藤です。 いつもお世話になっております。 $ ./inc_ut.py <<EOF > @head1.txt > ソースコード中の > #include <stdio.h> > の件について > : > EOF
などとしようものなら、 "#include <stdio.h>"の行もマッチして、 ファイルstdio.hを展開しようとして、探しても見つからずエラー停止。
先の、diff出力の行頭から'@@ 'で始まる行の問題もあるので、 デフォルトの正規表現のリストを修正します。
def new( lst=[] ): lst += [ r'^@([^@\s]+)', ] pat_lst = list( map( re.compile, lst ) ) :
これだけにします。
行頭から@で始まり、続いて「@以外または空白文字以外」が1つ以上続く パターンにマッチします。
「@以外または空白文字以外」が1つ以上続く部分が、ファイルの指定になります。
v5.patch
diff -ur v4/inc_ut.py v5/inc_ut.py
--- v4/inc_ut.py 2020-09-16 05:06:21.000000000 +0900
+++ v5/inc_ut.py 2020-09-16 08:36:04.000000000 +0900
@@ -28,10 +28,7 @@
def new( lst=[] ):
lst += [
- r'^# *include +(\S*)',
- r'^@ *include +(\S*)',
- r'^@ *inc +(\S*)',
- r'^@(\S*)',
+ r'^@([^@\s]+)',
]
pat_lst = list( map( re.compile, lst ) )
$ ./inc_ut.py <<EOF > @head1.txt > ソースコード中の > #include <stdio.h> > の件について > : > EOF kon pageの近藤です。 いつもお世話になっております。 ソースコード中の #include <stdio.h> の件について :
$ echo "@head1.txt" | ./inc_ut.py kon pageの近藤です。 いつもお世話になっております。 $ echo "@@head1.txt" | ./inc_ut.py @@head1.txt $ echo "@@ -55,6 +55,8 @@" | ./inc_ut.py @@ -55,6 +55,8 @@
$ cat smp3.txt | ./inc_ut.py 関係各位 kon pageの近藤です。 いつもお世話になっております。 以上
$ pwd /Users/kondoh/kon_page/inc $ ( cd /tmp ; echo "@<head1.txt>" | /Users/kondoh/kon_page/inc/inc_ut.py ) kon pageの近藤です。 いつもお世話になっております。
$ nkf -j smp3.txt | nkf -g ISO-2022-JP $ nkf -j smp3.txt | ./inc_ut.py | nkf -g ISO-2022-JP $ nkf -j smp3.txt | ./inc_ut.py | nkf -u 関係各位 kon pageの近藤です。 いつもお世話になっております。 以上
$ echo @ng.txt | ./inc_ut.py err @bar.txt, ng.txt L3
$ cat loop_c.txt | ./inc7.py > nest_res.txt
$ cat loop_c.txt | ./inc_ut.py | diff -u nest_res.txt - $
$ cd /tmp $ ls -l foo.txt ls: cannot access 'foo.txt': No such file or directory
$ cat /home/kondoh/kon_page/kon_ut/foo.txt foo FOO
$ cat /tmp/bar/foo.txt foo_foo
$ python >>> import inc_ut >>> inc = inc_ut.new() >>> inc.search_dirs ['/home/kondoh/kon_page/kon_ut'] >>> print( inc.exp( '@foo.txt' ) ) foo FOO >>> inc.search_dirs.insert( 0, '/tmp/bar' ) >>> inc.search_dirs ['/tmp/bar', '/home/kondoh/kon_page/kon_ut'] >>> print( inc.exp( '@foo.txt' ) ) foo_foo
$ echo "@nest.txt" | ./inc_ut.py nest test --> smp1.txt hello foo FOO world <-- --> foo.txt foo FOO <-- foo.txt
$ echo "@nest.txt -nr" | ./inc_ut.py nest test --> smp1.txt @smp1.txt <-- --> foo.txt @foo.txt <-- foo.txt
本来のinclude展開処理から外れますが、 バッファにテキストを貯めておいて、 そのテキストをinclude展開する仕組みを入れてみました。
v6.patch
diff -ur v5/inc_ut.py v6/inc_ut.py
--- v5/inc_ut.py 2020-09-16 08:36:04.000000000 +0900
+++ v6/inc_ut.py 2020-09-24 21:32:53.000000000 +0900
@@ -7,6 +7,38 @@
import nkf
import dbg
+def buf_new():
+ dic = {}
+ ks = []
+
+ def set(s):
+ get_k = lambda : s[ 2 : ]
+
+ if s.startswith( '@>' ):
+ ks.append( get_k() )
+ return True
+ if s.startswith( '@<' ):
+ if ks:
+ ks.pop()
+ return True
+ if s.startswith( '@!' ):
+ k = get_k()
+ if k in dic:
+ dic.pop( k )
+ return True
+ if ks:
+ k = ks[ -1 ]
+ if k not in dic:
+ dic[ k ] = ''
+ dic[ k ] += s + '\n'
+ return True
+
+ return False
+
+ get = dic.get
+
+ return empty.new( locals() )
+
def read_str(path):
r = False
with open( path, 'rb' ) as f:
@@ -32,6 +64,8 @@
]
pat_lst = list( map( re.compile, lst ) )
+ buf = buf_new()
+
def match_path(s):
for p in pat_lst:
m = p.match( s )
@@ -55,10 +89,18 @@
search_dirs = [ os.path.dirname( __file__ ) ]
def inc_text(s):
+ if buf.set( s ):
+ return True
+
(path, opts) = match_path( s )
if path == None:
return None
+ r = buf.get( path )
+ if r != None:
+ paths.append( path )
+ return empty.new( s=cut_tail_nl( r ), opts=opts )
+
path_lst = [ path ]
if not path.startswith( '/' ):
path_lst += list( map( lambda dir_: os.path.join( dir_, path ), search_dirs ) )
@@ -81,14 +123,17 @@
if r == None:
return s
if r == True:
- return ''
+ return True
s = cvt_opts( r.s, r.opts )
if '-nr' not in r.opts:
s = exp( s )
paths.pop()
return s
- exp = lambda s: '\n'.join( map( lambda i_s: exp_line( *i_s ), enumerate( s.split( '\n' ) ) ) )
+ def exp(s):
+ f_map = lambda i_s: exp_line( *i_s )
+ f_filter = lambda s: s != True
+ return '\n'.join( filter( f_filter, map( f_map, enumerate( s.split( '\n' ) ) ) ) )
return empty.new( locals() )
include指示行は
行頭から@で始まり、続いて「@以外または空白文字以外」が1つ以上続くパターン
でした。
さらに、次の3つのパターンを特別扱いします。
'@>'で始まる行 | その行の'@>'以降の名前のバッファへの登録開始 |
'@<'で始まる行 | 現在のバッファへの登録終了 |
'@!'で始まる行 | その行の'@!'以降の名前のバッファを削除 |
例えば入力テキスト中に
: @>foo hello world @< :
の記述があると、名前fooのバッファにhello worldの2行が登録されます。
'@>xxx' から '@<' までの間の行は、出力されません。
以降の箇所で
: @foo :
のinclude指示行があると、ファイルfooを探さずに、 名前fooのバッファの内容が展開されます。
名前fooのバッファが未登録なら、従来通りファイルfooを探します。
buf.txt
にせファイル(というかバッファ)の使用例
@>hoge
みなさまお聴きの放送は
FREQ kHz CS NAME です。
@<
@hoge FREQ=1179 CS=MBS NAME=毎日放送
@hoge FREQ=1008 CS=ABC NAME=朝日放送
@hoge FREQ=1314 CS=OBC NAME=ラジオ大阪
の場合
$ ./inc_ut.py < buf.txt にせファイル(というかバッファ)の使用例 みなさまお聴きの放送は 1179 kHz MBS 毎日放送 です。 みなさまお聴きの放送は 1008 kHz ABC 朝日放送 です。 みなさまお聴きの放送は 1314 kHz OBC ラジオ大阪 です。
などと展開されます。
バッファへの登録中の出力抑制のために、従来の動作を少しだけ変更しています。
2重includeを検出した時に、そのinclude指示行をしれっと無視する際、 指示行は空行として1行分出力されていました。
2重includeの指示行は行削除扱いとして出力しないようにしました。
v7.patch
diff -ur v6/inc_ut.py v7/inc_ut.py
--- v6/inc_ut.py 2020-09-24 21:32:53.000000000 +0900
+++ v7/inc_ut.py 2020-09-26 02:54:32.000000000 +0900
@@ -11,20 +11,34 @@
dic = {}
ks = []
+ e = empty.new()
+ e.last = ''
+
+ def anon_k():
+ id = 0
+ get = lambda : 'tMp_{}'.format( id )
+ while get() in dic:
+ id += 1
+ return get()
+
def set(s):
- get_k = lambda : s[ 2 : ]
+ def get_k():
+ k = s[ 2 : ]
+ return k if k else anon_k()
if s.startswith( '@>' ):
ks.append( get_k() )
return True
if s.startswith( '@<' ):
if ks:
- ks.pop()
+ e.last = ks.pop()
return True
if s.startswith( '@!' ):
k = get_k()
if k in dic:
dic.pop( k )
+ if k == e.last:
+ e.last = ''
return True
if ks:
k = ks[ -1 ]
@@ -37,7 +51,9 @@
get = dic.get
- return empty.new( locals() )
+ to_last_key = lambda s, k='$$': s.replace( k, e.last )
+
+ return empty.add( e, locals() )
def read_str(path):
r = False
@@ -96,6 +112,9 @@
if path == None:
return None
+ path = buf.to_last_key( path )
+ opts = list( map( buf.to_last_key, opts ) )
+
r = buf.get( path )
if r != None:
paths.append( path )
'@>'で始まる行 | その行の'@>'以降の名前のバッファへの登録開始 |
'@<'で始まる行 | 現在のバッファへの登録終了 |
'@!'で始まる行 | その行の'@!'以降の名前のバッファを削除 |
'@>'の登録開始で名前を指定しないときに、 重複しない適当な名前を自動生成するように変更してみました。
'@>'で始まる行 | その行の'@>'以降の名前のバッファへの登録開始
名前の指定が無いと重複しない名前が自動生成される |
'@<'で始まる行 | 現在のバッファへの登録終了 |
'@!'で始まる行 | その行の'@!'以降の名前のバッファを削除 |
最後に'@<'で登録終了したバッファ名を、 '$$'のキーワードで参照できるようにしました。
'$$'が使えるのはinclude指示行だけです。 path文字列か、以降のオプション文字列の中で使用します。
v8.patch
diff -ur v7/inc_ut.py v8/inc_ut.py
--- v7/inc_ut.py 2020-09-26 02:54:32.000000000 +0900
+++ v8/inc_ut.py 2020-10-09 23:21:17.000000000 +0900
@@ -74,6 +74,23 @@
return s[ 1 : -1 ]
return s
+def indent(s, opts):
+ def get_i():
+ k = '-s'
+ for opt in opts:
+ if opt.startswith( k ):
+ r = opt[ len( k ) : ]
+ if not r:
+ return 1
+ elif r.isdigit():
+ return int( r )
+ return 0
+
+ i = get_i()
+ if i > 0:
+ s = '\n'.join( map( lambda s: ' ' * i + s, s.split( '\n' ) ) )
+ return s
+
def new( lst=[] ):
lst += [
r'^@([^@\s]+)',
@@ -146,6 +163,7 @@
s = cvt_opts( r.s, r.opts )
if '-nr' not in r.opts:
s = exp( s )
+ s = indent( s, r.opts )
paths.pop()
return s
include展開するテキストに「字下げ」を追加するためのオプションを、 追加してみました。
include指示行に '-s' を追加すると、 展開するテキストの行頭に空白を1つ追加します。
空白を2つ追加したいときは '-s2' のように、-sの直後に数字を指定します。
v9.patch
diff -ur v8/inc_ut.py v9/inc_ut.py
--- v8/inc_ut.py 2020-10-09 23:21:17.000000000 +0900
+++ v9/inc_ut.py 2020-10-09 23:21:53.000000000 +0900
@@ -147,9 +147,13 @@
return True
r = read_str( path )
if r == False:
- return False
+ break
paths.append( path )
return empty.new( s=cut_tail_nl( r ), opts=opts )
+
+ if '-q' in opts:
+ return True
+
return False
def exp_line(i, s):
従来動作では、includeすべきファイルが見つからなかったり、 ファイルのオープンに失敗すると、 エラー表示で終了します。
include指示行に '-q' (quiet)を追加すると、 エラー発生時でもしれっと無視し、 include指示行を削除扱いして、後続の処理を進めるようになります。
v10.patch
diff -ur v9/inc_ut.py v10/inc_ut.py
--- v9/inc_ut.py 2020-10-09 23:21:53.000000000 +0900
+++ v10/inc_ut.py 2020-10-10 18:42:36.000000000 +0900
@@ -87,8 +87,11 @@
return 0
i = get_i()
- if i > 0:
- s = '\n'.join( map( lambda s: ' ' * i + s, s.split( '\n' ) ) )
+
+ tc = ' \\' if '-tc' in opts else ''
+
+ if i > 0 or tc:
+ s = '\n'.join( map( lambda s: ' ' * i + s + tc, s.split( '\n' ) ) )
return s
def new( lst=[] ):
include指示行に '-tc' (tail continued) を追加すると、 展開するテキストの行末に空白1つとバックスラッシュを追加します。
v11.patch
diff -ur v10/inc_ut.py v11/inc_ut.py
--- v10/inc_ut.py 2020-10-10 18:42:36.000000000 +0900
+++ v11/inc_ut.py 2020-10-10 21:24:30.000000000 +0900
@@ -146,7 +146,7 @@
for path in path_lst:
if os.path.exists( path ):
- if path in paths:
+ if path in paths and not '-ad' in opts:
return True
r = read_str( path )
if r == False:
2重includeの防止 で、 検出すると「そっと、しないでおく」対処になっています。
ですが、 簡単な置換機能 によって、無事に終端する場合もあります。
あえて、2重includeを許容するオプションとして '-ad' (allow double include) を追加します。
include指示行を含むテキストデータの展開に使用します。
include指示行用の正規表現のリスト lst を与え、 include展開処理用のオブジェクトを返します。
デフォルトとして次の正規表現は組み込み済みで、 引数指定のlstは、その前方に挿入されます。
オブジェクトの主なメソッド
メソッド | 内容 |
---|---|
exp(s) | 改行を含む文字列sを指定すると、include指示行を展開した文字列を返します。 |
$ cat foo.txt foo FOO
$ python >>> import inc_ut >>> inc = inc_ut.new() >>> s = '''bar ... hoge ... #include "foo.txt" ... fuga''' >>> s 'bar\nhoge\n#include "foo.txt"\nfuga' >>> inc.exp( s ) 'bar\nhoge\nfoo\nFOO\nfuga' >>> print( inc.exp( s ) ) bar hoge foo FOO fuga
include指示行の後続に、次の形式で文字列の置換の指定を追加できます。
置換前の文字列=置換後の文字列
空白で区切って、複数指定できます。
簡単なパース処理にしてるので、 置換前の文字列や置換後の文字列に空白を含めることはできません。
$ cat foo.txt foo FOO $ python >>> import inc_ut >>> inc = inc_ut.new() >>> print( inc.exp( '@foo.txt FOO=BAR o=0' ) ) f00 BAR
オブジェクトのアトリビュート.search_dirsとしてリストが用意されています。
exp()実行時に、指定の相対パスのファイルが、 カレントディレクトリにファイルが見つからなかった場合、 search_dirs のリストの先頭から順に、ファイルを探します。
初期値は
search_dirs = [ os.path.dirname( __file__ ) ]
として、inc_ut.py の存在するディレクトリだけが入ってます。
include指示行に '-nr' を追加すると、再帰的な展開をしません。
'@>'で始まる行 | その行の'@>'以降の名前のバッファへの登録開始
名前の指定が無いと重複しない名前が自動生成される |
'@<'で始まる行 | 現在のバッファへの登録終了 |
'@!'で始まる行 | その行の'@!'以降の名前のバッファを削除 |
include指示行でpathと同じ名前のバッファが存在すると、 ファイルを探さずに、バッファの内容を展開します。
include指示行では、キーワード'$$'が、 最後に登録されたバッファ名に展開されます。
include指示行に '-s' を追加すると、 展開するテキストの行頭に空白を1つ追加します。
空白を2つ追加したいときは '-s2' のように、-sの直後に数字を指定します。
include指示行に '-tc' (tail continued) を追加すると、 展開するテキストの行末に空白1つとバックスラッシュを追加します。
include指示行に '-q' (quiet)を追加すると、 エラー発生時でもしれっと無視し、 include指示行を削除扱いして、後続の処理を進めるようになります。
通常は、2重includeを検出すると、2つ目のinclude指示行は無視されます。
include指示行に '-ad' (allow double include)を追加すると、 2重includeを検出しても、処理を続行します。