テキストプロセッサとしての Python

[Perl正規表現] [Python more basics]
[2014/07/07新規] [2014/09/12更新]

Contents

主な形式

python -c 'script' [file...]
python script_file [file...]

sed, Awk 風な説明

Python は sed, Awk, Perl からの影響を受けていないので、Ruby のようにそれらの流れを汲むシンタックスシュガーは持ち合わせていない。

よって、コマンドライン引数をファイル指定と看做したり、コマンドライン引数が無ければ標準入力から行を簡便に読み込むなどのオプションは存在しない。そこで、敢えてそうした処理をする場合には如何にコードを書くべきか、簡単にまとめておく。

`sed -n`, `perl -nl`, `ruby -nl`

#!/usr/bin/python
import fileinput
for line in fileinput.input():
    line = line.rstrip("\r\n")
    :

fileinput ライブラリはファイル名のリスト、規定値は sys.argv[1:] を一行ずつ読み込む。ファイル名に '-' が指定された場合やリストが空の場合は標準入力から一行ずつ読み込む。

`sed -p`, `perl -pl`, `ruby -pl`

#!/usr/bin/python
import fileinput
for line in fileinput.input():
    line = line.rstrip("\r\n")
    :
    print line

Python はパターンスペースを自動的に出力するようなことはしない為、明示的に出力する必要がある。また、改行を切り取るようなオプションもないので、明示的に rstrip("\r\n") する必要がある。

`perl -p`, `ruby -p`

#!/usr/bin/python
import fileinput
for line in fileinput.input():
    :
    print line,

Python にはパターンスペースを自動的に出力するようなことはしない為、明示的に出力する必要がある。また、'print' 文は ',' で終っていれば、改行を出力しない。

`awk -F ':'`, `perl -F':' -na`, `ruby -F ':' -na`

#!/usr/bin/python
import fileinput
for line in fileinput.input():
    F = line.split(":")
    :

Python は Awk のようにフィールドセパレータでレコードとなる行を切り分けるようなことはしないので、明示的に split(FS) する必要がある。

sed, Awk 風な Python スクリプトの概要

Python には Awk, Perl, Ruby のように 'BEGIN', 'END' のような特殊ブロックなどのようなものは存在しない。しかし、これは上述の Perl, Ruby における '-n', '-p' オプションが存在しないので不要である。

一方、sed, Awk のマッチの範囲「式, 式」に処理されるブロック, Perl, Ruby のような範囲演算子 '..', '...' もサポートされない。よって、明示的にそれらに同等なコードを書く必要がある。

例えば以下は、Awk では awk '/^<pre>/,/<\/pre>$/' となる、HTML の 'pre' タグを含むそれに囲まれた行を表示する Python スクリプトである。

	python -c '
import fileinput
import re
flag = False
for line in fileinput.input():
	if not flag:
		m = re.match(r"^<pre>", line)
		if m: flag = True; print line,
	else:
		print line,
		m = re.match(r"^</pre>$", line)
		if m: flag = False
'

このように Python スクリプトは、sed, Awk 風な省略した Perl, Ruby コードのようなものは書けないようになっている。さらには、複文はセミコロン ';' で一行で書くことは出来るが、制御構文には改行とインデントとその深さが必須となっている。よって、Python では同じ目的なら誰が書いても似たようなコードになるという根拠にもなっている。

例えば、sed では sed -e 's/&/\&amp;/g;s/</\&lt;/g;s/>/\&gt;/g' となる、ソースコードを HTML にペーストできるように「<」から「&lt;」への変換等を行なう Python スクリプトは以下のように書かなければならない。

	python -c '
import fileinput
import re
for line in fileinput.input():
	line = re.sub(r"&", "&amp;", line)
	line = re.sub(r"<", "&lt;", line)
	line = re.sub(r">", "&gt;", line)
	print line,
'

簡単な説明

先の perl -p, ruby -p に対応する python スクリプトは、以下とほぼ等価なスクリプトとなる。

	python -c '
import sys
argv = sys.argv[1:]
if not argv: argv.insert(0, "-")
for filename in argv:
    argf = stdin if filename == "-" else open(filename, "r")
    for line in argf:
        print line,
'

ここで for line in ファイルオブジェクト は一行をあたかもリストの一要素かのように次々と辿るイテレータによるループとなる。しかし、残念ながらこのループ内で argf.tell() による「現在のファイル読み込みの位置」を取得しても意図した動作にならない。これは、実は効率化の為に既に多くを読み込んでしまっているからである。ちなみに、先の fileinput ライブラリによる方法でも argf.tell() は実現不可能なようである。

これはさらに以下とほぼ等価なスクリプトとなる。

	python -c '
import sys
argv = sys.argv[1:]
for filename in argv:
    argf = stdin if filename == "-" else open(filename, "r")
    line = argf.readline()
    while line:
        print line,
        line = argf.readline()
'

これならこの while ループ内で argf.tell() による「現在のファイル読み込みの位置」を取得しても意図した動作になる。

ところで、Python ではこの while ループは以下のようには書けない。

    while line = argf.readline():
        print line,

代入文は Python では「式」ではないので値を返さないからである。あまりに杓子定規だが、初心者にはわかりやすいのだろう。

Python スクリプトの概要

例えば先の、ソースコードを HTML にペーストできるように「<」から「&lt;」への変換等を行なう Python スクリプトは、なるべく少ない行数で書けば、推奨はされないが以下のようになる。

	python -c '
import fileinput; import re;
for line in fileinput.input(): line = re.sub(r"&", "&amp;", line); line = re.sub(r"<", "&lt;", line); line = re.sub(r">", "&gt;", line); print line,
'

このように、'for' のような制御構文の前には必ず改行が必要となるが、他の文はセミコロン ';' で継続してもよい。

例えば先の、HTML の 'pre' タグを含むそれに囲まれた行を表示する Python スクリプトは、なるべく少ない行数で書けば、推奨はされないが以下のようになる。

	python -c '
import fileinput; import re; import sys; flag = False
for line in fileinput.input(): flag, rv = ((True, sys.stdout.write(line)) if re.match(r"^<pre>", line) else (False, None)) if not flag else (False if re.match(r"^</pre>", line) else True, sys.stdout.write(line))
'

ここで、'print' ではなく 'sys.write' を使っているのは、'print' は文なので右辺値にはなれないからである。そして、'sys.write' は返り値はないが関数なので、変数 'rv' に代入している。あとは三項演算子 'c ? a : b' に対応する Python の 'a if c else b' 演算子の組み合わせの代入式を 'for' ループで回している。

また、Python では未代入の変数の使用は禁止されており、Perl の 'undef'、Ruby の 'nil' のように Python の 'None' が変数の値になっているわけではない。よって、'flag = False' は必須である。

スクリプトは、さまざまなリテラルからなる式および、それらからなる文(単純文、複合文)からなる。複合文は後に続く文のブロック構造を知らしめる為に、必ず先立って改行が必要となる。ブロック構造は空白によるインデントレベルによって表される。

識別子、リテラル、式

以下の識別子、リテラル、式がある。

さて、Python には Perl における「リファレンス」などは無い、が、オブジェクトのインスタンス変数へのアクセスによって、それが不要となっている。

詳しくは、Python 言語リファレンス「リテラル」, 「」を参照のこと。

真偽値

Python における「偽」は、'None', 'False', 数値 '0', 空のコンテナ(文字列、タプル、リスト、辞書、集合) である。他に、'__len__(self)', '__nonzero__(self)' を持つオブジェクトで偽をかえすものもある。他のオブジェクトはすべて「真」となる。Perl のように文字列 "0" が偽にはならないので安心である。

変数展開、式展開

Python には言語仕様としては変数展開はなく、文字列型の '%' 演算子により、C の sprintf や C++ の boost::format に似た操作を行う。

#!/usr/bin/python
a = 'foo'
b = "bar"
print "%s: %s" % (a, b)	#=> "foo: bar\n"

ここで、第二項はタプルだが要素は式である。よって、以下のような式展開もできてしまう。

#!/usr/bin/python
a = [ 0, 1, 2, 3, ]
print '%s' % (",".join(map(str, a)))	#=> "0,1,2,3\n"

詳しくは、Python 言語リファレンス「文字列フォーマット操作」を参照のこと。

演算子式

以下の演算子式がある。

() [] {} : ``式結合またはタプル、リスト、辞書、文字列変換
[] () .配列または辞書添字またはスライス、関数呼び出し、属性参照
**二項累乗
+ - ~単項正、単項負、ビット否定
* / // %二項乗、除、除(切り捨て)、法
+ -二項加、減
<< >>ビット左、右シフト
&ビットAND
^ビットXOR
|ビットOR
> >= < <= <> == != in, not in, is, is not関係等号不等号、メンバシップ検査
not論理否定
and or論理AND、論理OR
if else三項条件
lambdaラムダ式

特に、Python の演算子は Perl よりもむしろ C++ に近く、文字列の連結は Perl のように '.' ではなく '+' 二項加算演算子である。

しかし、残念ながら代入は Python では式ではないので、これが言語に慣れているものにとっては非常に面倒になる。さらに、多くのプログラミング言語にある ++, -- の前置・後置ともに、Python には存在しない。これは設計思想によるもので、慣れれば Python では不要であることがわかってくる。とは言え、すべての代入や、++, -- の前置・後置は「返値」のあるメソッドによるオブジェクトの破壊的操作と捉えれば導入可能に思えるのだが… すると +=, -= に加えて =+, =- のような新たな演算子も導入可能であろう。

一方、優れているのは関係等号不等号と論理ANDの組み合わせで、よくある 'x < y and y < z''x < y < z' と直感的に書けることである。これは他の言語でも採用すべきだ。

また、優先順位の高い '&&', '||', '!' の論理演算が敢えて存在しないのは、これもまた設計思想なのだろう。

詳しくは、Python 言語リファレンス「」を参照のこと。

代入(タプル代入)

以下の代入文がある。文なので、値を返さない。

= += -= *= /= //= %= **= &= |= ^= <<= >>=代入、累算代入

Python には C/C++ などにはない「タプル代入」なる式がある。Perl のリスト代入 '($a, $b, $c) = (1, 2, 3)' ともまた異なり、Python では 'a, b, c = 1, 2, 3' というスタイルとなる。

詳しくは、Python 言語リファレンス「代入文」を参照のこと。

制御構文

以下の複合文がある。

ブロック構造を表す改行とインデントレベルは省略できないので注意。

特に、'for (i=0; i<10; i++)' のような繰り返し構文がないので、代わりに 'for i in range(10): …' のようにリスト生成による繰り返し文で実現する。しかしこれだと、'for (i=10; i>0; i--)' のような降順の繰り返しができないので、降順の場合は 'for i in range(10, 0, -1):' のようにリスト生成による繰り返し文で実現する。

詳しくは、Python 言語リファレンス「複合文」を参照のこと。

式文と代入文の他に、以下の単純文がある。

詳しくは、Python 言語リファレンス「単純文」を参照のこと。

正規表現

Python の正規表現は Perl とほとんど同じだ。

但し、Perl における '%-' に対応する機能や POSIX 文字クラスがサポートされていないようだ。

定数と特殊変数

Python における定数と特殊変数は、それぞれのライブラリ内で定義されており、以下の組み込み定数、および、対話モードで最後に表示された結果である '_' 以外のグローバルスコープ変数は存在しないようだ。

よく使う 'sys', 'os', 'fileinput' ライブラリの定数、変数、メソッドを以下に示す。それぞれ 'import' 文を要する。

すべての変数は何もしなければローカルスコープとなる。しかし、'import' したモジュール名と同一の変数名は実質使えないので、'sys', 'os' などの変数名は避ける必要がある。

エスケープ文字

Python におけるエスケープ文字(バックスラッシュ記法と正規表現におけるメタ文字)は以下の通りである。

このように Python 1.5 以降では非常に多くのエスケープ文字がサポートされる。

組み込み型と組み込みライブラリ

Python には組み込み型、組み込み関数、組み込みオブジェクト、組み込みモジュールにより、標準ライブラリがサポートされる。

ここで主な組み込み型、標準ライブラリのリファレンスを列挙しておく。

リンク先のメソッドや定数をよく参考にすること。

組み込み関数

組み込み関数は以下の通りである。

	len(obj)	-> int
	cmp(o, p)	-> int
	chr(i)		-> str
	unichr(i)	-> str
	bin(i)		-> str
	hex(i)		-> str
	str([obj])	-> str
	repr(obj)	-> str
	ord(c)		-> int
	bool([o=False])		-> bool
	int([v=0[, base=10]])	-> int
	long([v=0[, base=10]])	-> long
	float([v=0])		-> float
	complex([rv=0[, iv=0]])	-> complex
	abs(v)			-> numbers
	oct(i)			-> str
	pow(xv, yv[, zv])	-> xv**yv|xv**yv%zv
	round(v[, i=0])		-> float
	divmod(v, w)		-> (v//w, v%w)
	print(obj[,...][,sep=''][,end=''][,file=sys.stdout])	# print文とは別なので注意!
	unicode([obj[, encoding[, errors]]])	-> unicode

	all(it) 		-> bool
	any(it) 		-> bool
	max(it[…])		-> obj
	min(it[…])		-> obj
	next(it[, def])		-> obj
	sum(it[, start=0])	-> obj

	open(name[, mode[, bufsize]])	-> file
	format(v[, fs])			-> fmt

	iter(o[, sentinel])			-> iterator
	list([it])				-> list, []
	set([it])      				-> set, {}
	frozenset([it])				-> frozenset
	dict([arg])				-> dictionary, { …: …, ... }
	memoryview(obj)				-> memoryview
	tuple([it])				-> tuple, ()
	bytearray([so[, encoding[, errors]]])	-> sequence
	xrange([startv, ]stopv[, stepv])	-> xrange
	slice([startv, ]stopv[, stepv])		-> slice
	range([startv, ]stopv[, stepv])		-> []
	enumerate(s[, i])			-> ((i, s[i]), ...)
	filter(f, it)				-> [it[], ...]
	map(f, it, ...)				-> []
	reduce(f, it[, init])			-> []
	reversed(it)				-> iterator
	sorted(it[, cmpf[, keyf[, reverse]])	-> []
	zip([it, ...])				-> [(), ...]

	file		
	basestring	
	classmethod	
	staticmethod	

	property(getter[, setter[, deleter[, doc]]])	-> property
	callable(obj)			-> bool
	isinstance(obj, classinfo)	-> bool	# classinfo: class | (class1, ...) 
	issubclass(obj, classinfo)	-> bool
	hasattr(obj, name)		-> bool
	getattr(obj, name[, def])	-> obj|def
	setattr(obj, name, v)		
	delattr(obj, name)		
	dir([obj])			-> [ name, ... ]
	hash(obj)			-> int
	id(obj)				-> int
	object()			-> obj
	super([type, [obj|type]])	
	type(obj)			-> type
	type(name, bases, dict)		-> type
	globals()			-> {}
	locals()			-> {}
	vars([obj])			-> {}|property
	__import__(name[, globals[, locals[, fromlist[, level]]]]))	-> obj
	reload(module)							-> obj
	compile(so, name, mode[, flags[, dont_inherit]])		-> obj
	eval(expr[, globals[, locals]])		-> obj
	execfile(name[, globals[, locals]])	-> obj
	help([obj])		
	input([prompt])		-> obj
	raw_input([prompt])	-> str

例題

代表的な Unix コマンドに相当する Python スクリプトを以下にあげる。

`cat`

	python -c '
import fileinput
for line in fileinput.input():
	print line,
'

このように、cat と同じ python スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`head -n 1`

	python -c '
import fileinput
for line in fileinput.input():
	if (fileinput.lineno() == 1): print line,
'
	python -c '
import fileinput
for line in fileinput.input():
	if (fileinput.lineno() == 1): print line,; break
'

このように、head -n 1 と同じ python スクリプトは以上のようになるが、後者の方が効率がよいだろう。

[sed] [Awk] [Perl] [Ruby] [Python]

`tail -n 1`

	python -c '
import fileinput
for line in fileinput.input():
	b = line
print b,
'

このように、tail -n 1 と同じ python スクリプト以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]
	python -c '
import fileinput
for line in fileinput.input():
	if (not (fileinput.lineno() > 8)): print line,
'
	python -c '
import fileinput
for line in fileinput.input():
	if (not (fileinput.lineno() > 8)): print line,
	else: break
'

このように、head -n 8 と同じ python スクリプトは以上のようになるが、後者の方が効率がよいだろう。

[sed] [Awk] [Perl] [Ruby] [Python]

`tail -n 8`

さて、`head -n 1`, `tail -n 1`, `head -n n` は以上のように大変簡単であるが、`tail -n n` は少し工夫が必要であり、以下のようになる。

	python -c '
import fileinput
n = 8
a = [""]*n
for line in fileinput.input():
	a[fileinput.lineno() % n] = line
for i in range(0, n):
	print a[(fileinput.lineno()+i+1) % n],
'

ここでは、配列にラウンドロビン的に行を格納していき、最後にそれらを出力している。

このように、tail -n 8 と同じ python スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`wc -l`

	python -c '
import fileinput
for line in fileinput.input():
	pass
print fileinput.lineno()
'

このように、wc -l と同じ python スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`wc -c`

sed では大変面倒になる `wc -c`python だと算術演算があるので簡単である。

	python -c '
import fileinput
l = 0
for line in fileinput.input(): l += len(line)
print l
'

ここで、'l = 0' は Python だと変数の型を決定する為に必要になる。

このように、wc -c と同じ python スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`wc -w`

sed では大変面倒になる `wc -w`python だと算術演算があるので簡単であるが、少々工夫が必要であり、以下のようになる。

	python -c '
import fileinput
import re
w = 0
def match_count(m):
	global w
	w += 1
	return m.group(0)
for line in fileinput.input():
	re.sub(r"[^\t\n ]+", match_count, line)
print w
'

ここで、're.sub' に関数を渡してマッチ全体をそのまま返しつつ、'global' 宣言した変数 'w' でマッチ回数をカウントしている。しかし、're.subn' を使えば、置換結果とマッチ回数をタプルで返してくれるので、より簡単に書けて以下のようになる。

	python -c '
import fileinput
import re
w = 0
for line in fileinput.input():
	w += re.subn(r"[^\t\n ]+", r"\g<0>", line)[1]
print w
'

このように、wc -w と同じ python スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`grep '^$'`

	python -c '
import fileinput
import re
for line in fileinput.input():
	if re.search(r"^$", line): print line,
'

このように、基本正規表現の grep '^$' と同じ python スクリプトは以上のようになるが、Python では基本正規表現はサポートされないので、他のパターンではPython正規表現に書き直す必要がある。

[sed] [Awk] [Perl] [Ruby] [Python]

`grep -v '^$'`

	python -c '
import fileinput
import re
for line in fileinput.input():
	if not re.search(r"^$", line): print line,
'

このように、マッチの否定、基本正規表現の grep -v '^$' と同じ python スクリプトは以上のようになるが、Python では基本正規表現はサポートされないので、他のパターンではPython正規表現に書き直す必要がある。

[sed] [Awk] [Perl] [Ruby] [Python]

`grep -E '^.+'`

	python -c '
import fileinput
import re
for line in fileinput.input():
	if re.search(r"^.+", line): print line,
'

このように、拡張正規表現の grep -E '^.+' と同じ python スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`grep -E -v '^.+'`

	python -c '
import fileinput
import re
for line in fileinput.input():
	if not re.search(r"^.+", line): print line,
'

このように、拡張正規表現のマッチの否定、grep -E -v '^.+' と同じ python スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`cut -d ':' -f 1,6`

	python -c '
import fileinput
for line in fileinput.input():
	F = line.split(":")
	if len(F) >= 6:
		print "%s:%s" % (F[0], F[5])
	else:
		print line,
'

このように、区切り ':' のフィールド切り取り、cut -d ':' -f 1,6 但しフィールド数不足の行はそのまま出力と同じ python スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`cut -d ':' -f 1,6 -s`

	python -c '
import fileinput
for line in fileinput.input():
	F = line.split(":")
	if len(F) >= 6: print "%s:%s" % (F[0], F[5])
'

このように、区切り ':' のフィールド切り取り、cut -d ':' -f 1,6 但しフィールド数不足の行は出力しないと同じ python スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`fold -b`

この例、1行あたりの文字数(既定値は80)を越えたら改行を挿入する python スクリプトを示そう。まずは正規表現を使わない方法:

	python -c '
import fileinput
w = 80
for line in fileinput.input():
	h = line.rstrip("\r\n")
	while True:
		if len(h) > w:
			print h[0:w]
			h = h[w:]
		else:
			print h
			break
'

もしくは、正規表現を使った方法:

	python -c '
import fileinput
import re
w = 80
for line in fileinput.input():
	h = line.rstrip("\r\n")
	while not re.search(r"^.{%d}$" % w, h):
		m = re.search(r"^.{%d}" % w, h)
		if not m: break
		print m.group(0)
		h = h[m.end(0):]
	print h
'

ここで、二つの正規表現は同一ではないので注意。Python では 'while' の条件式に代入が行えないので、このように Perl や Ruby などに比べて冗長になってしまう。

このように、fold -b と同じ python スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`tee filename`

	python -c '
import fileinput
f = open("filename.out", "w")
for line in fileinput.input():
	print line,
	f.write(line)
f.close()
'

このように、標準入力を標準出力とファイルに書き出し、tee と同じ python スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`tr 'A-Za-z' 'N-ZA-Mn-za-m'`

この例、ROT13(と呼ばれる暗号化と言うより難読化)は tr コマンドを使うと表題のように簡単に実現できる。そして、python には tr コマンドに似た 'translate' メソッドがあるのだが、2つの引数の長さが等しくないといけない仕様なので、以下のように冗長になる。

	python -c '
import fileinput
import string
for line in fileinput.input():
	print line.translate(string.maketrans("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", "NOPQRSTUVWXYZABCDEFGHIJKLMnopqrstuvwxyzabcdefghijklm")),
'

このように、文字置換、tr 'A-Za-z' 'N-ZA-Mn-za-m' と同じ python スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`cat -n`

sed ではいささか面倒になる `cat -n`python だと極めて簡単である。

	python -c '
import fileinput
for line in fileinput.input():
	print "%6d\t%s" % (fileinput.lineno(), line),
'

このように、cat -n と同じ python スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`cat -b`

sed では大変面倒になる `cat -n`python だと極めて簡単である。しかし、変数の型を決める為の初期化を要するので Perl よりも繁雑になる。

	python -c '
import fileinput
import re
i = 0
for line in fileinput.input():
	if not re.search(r"^$", line):
		i += 1
		print "%6d\t%s" % (i, line),
	else:
		print line,
'

このように、cat -b と同じ python スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`uniq`, `uniq -d`, `uniq -u`

ところで、これらは GNU sedGNU Awk で示される好例となっている。

  1. `uniq` - 重複する行を一行にする in GNU sed
  2. `uniq -d` - 重複する行のみを一行にして表示する in GNU sed
  3. `uniq -u` - 重複しない行のみを表示する in GNU sed
  4. `uniq [-du]` - 以上すべてに対応 in GNU Awk

sed ではいずれも 'N' コマンドで入力行をパターンスペースの末尾に '\n' に続いて追加し、正規表現にて '^\(.*\)\n\1$' のように後方参照 '\1' を利用していることにある。awk においては、基本的には以下に示す例と同等である。

これらを python で実現すると、単純に文字列の比較と制御の組み合わせとなり、順に以下のようになる。

	python -c '
import fileinput
for line in fileinput.input():
	if fileinput.lineno() == 1:
		h = line
		print line,
		continue
	if h != line:
		h = line
		print line,
'
	python -c '
import fileinput
d = False
for line in fileinput.input():
	if fileinput.lineno() == 1:
		h = line
		continue
	if h != line:
		h = line
		d = False
	else:
		if not d: print line,
		d = True
'
	python -c '
import fileinput
d = False
for line in fileinput.input():
	if fileinput.lineno() == 1:
		h = line
		continue
	if h != line:
		if not d: print h,
		h = line
		d = False
	else:
		d = True
if not d: print h,
'

このように、uniq, uniq -d, uniq -u と同じ python スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`expand`

このタブを複数の空白に置換するコマンドを python スクリプトで示そう。この例では expand.py という実行権のついたファイルに記述するものとする。まずは正規表現を使った方法:

#!/usr/bin/python
import fileinput
import re
import sys
n = 8
for line in fileinput.input():
    h = line.rstrip("\r\n")
    m = re.match(r"^([^\t]{0,%d}\t|[^\t]{%d})" % (n-1, n), h)
    while m:
        u = m.group(0)
        h = h[m.end(0):]
        p = u.find("\t")
        if (p < 0): p = len(u)
        u = u[0:p]
        for i in range(n-p): u += " "
        sys.stdout.write(u)
        m = re.match(r"^([^\t]{0,%d}\t|[^\t]{%d})" % (n-1, n), h)
    print h

やはり、Python では 'while' の条件式に代入が行えないので、このように Perl や Ruby などに比べて冗長になってしまう。

もしくは、正規表現を使わない方法:

#!/usr/bin/python
import fileinput
import sys
n = 8
for line in fileinput.input():
    line = line.rstrip("\r\n")
    l = 0
    for i in range(len(line)):
        c = line[i:i+1]
        d = n - (l % n) if (c == "\t") else 1
        if c == "\t":
            c = ""
            for j in range(d): c += " "
        sys.stdout.write(c)
        l += d
    print

両者の効率と柔軟性におけるメリット・デメリットを考えてみるのも興味深い。個人的には、前者の方が判り易くてよいと思うが、例えば、'\b' が一文字戻るとみなす処理を加えるには後者の方が簡単である。

このように、expand と同じ python スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`unexpand -a`

この複数の空白をタブに置換するコマンドを python スクリプトを示そう。この例では unexpand-a.py という実行権のついたファイルに記述するものとする。まずは正規表現を使った方法:

#!/usr/bin/python
import fileinput
import re
import sys
n = 8
for line in fileinput.input():
    h = line.rstrip("\r\n")
    m = re.match(r"^([^\t]{0,%d}\t|[^\t]{%d})" % (n-1, n), h)
    while m:
        u = m.group(0)
        h = h[m.end(0):]
        if re.search(r"^ ", h):
            m = re.search(r" {1,}$", u)
            if m: u = u[0:m.start(0)] + "\t"
        else:
            m = re.search(r"^([^\t]{%d})\t$" % (n-1), u)
            if m: u = m.group(1) + " "
            m = re.search(r" {2,}$", u)
            if m: u = u[0:m.start(0)] + "\t"
        sys.stdout.write(u)
        m = re.match(r"^([^\t]{0,%d}\t|[^\t]{%d})" % (n-1, n), h)
    print h

ちなみに、最初の 'if' 文は、完全に `unexpand -a` の挙動を再現するためのものである。やはり、Python では 'while' の条件式に代入が行えないので、このように Perl や Ruby などに比べて冗長になってしまう。しかも、ここでは後述の 're.finditer' は使えないので注意。

もしくは、一文字ずつ調べる方法:

#!/usr/bin/python
import fileinput
import re
import sys
n = 8
for line in fileinput.input():
    line = line.rstrip("\r\n")
    buf = ""
    l = 0
    for i in range(len(line)):
        c = line[i:i+1]
        d = n - (l % n) if (c == "\t") else 1
        l += d
        buf += c
        if l % n == 0:
            if (line[i+1:i+2] == " "):
                m = re.search(r" +$", buf)
                if m: buf = buf[0:m.start(0)] + "\t"
            else:
                m = re.search(r"^([^\t]{%d})\t$" % (n-1), buf)
                if m: buf = m.group(1) + " "
                m = re.search(r"  +$", buf)
                if m: buf = buf[0:m.start(0)] + "\t"
            sys.stdout.write(buf)
            buf = ""
    print buf

両者の効率と柔軟性におけるメリット・デメリットを考えてみるのも興味深い。個人的には、前者の方が判り易くてよいと思うが、例えば、'\b' が一文字戻るとみなす処理を加えるには後者の方が簡単である。

このように、unexpand -a と同じ python スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`rev`

行毎に文字列を反転する BSD rev(1) コマンド。実用したことはないが、sed で実現するには秀逸な技法が必要であった。しかし、python では極めて単純に、文字列を逆順に取り出して出力すればよい。この例では rev.py という実行権のついたファイルに記述するものとする。

#!/usr/bin/python
import fileinput
import sys
for line in fileinput.input():
    line = line.rstrip("\r\n")
    for i in range(len(line)-1, -1, -1):
        sys.stdout.write(line[i:i+1])
    print

このように、BSD rev(1) と同じ python スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`tac`, `tail -r`

最終行から先頭行まで逆順で出力する GNU tac コマンド、`tail -r` に同じ。これも実用したことはないが、GNU sed における、好ましくない実現例のように、python における好ましくない実装は以下の通りである。この例では tac.py という実行権のついたファイルに記述するものとする。

#!/usr/bin/python
import fileinput
import sys
b = ''
for line in fileinput.input():
    b = line + b
sys.stdout.write(b)

変数に行を逆順に連結して最後にそれを出力するという処理なので、メモリが足りなくなるか、仮想メモリで非常に遅くなるだろう。しかし、Python には 'tell', 'seek' とそれらのアクセサ 'pos' がある。より安全な実装は以下のようになる。

#!/usr/bin/python
import sys
f = open(sys.argv[1])
p = []
p.append(f.tell())
line = f.readline()
while line:
    p.append(f.tell())
    line = f.readline()
p.pop
for i in range(len(p) - 1, -1, -1):
    f.seek(p[i])
    sys.stdout.write(f.readline())

ここではファイルを入力しながらすべての改行の次のファイルハンドルの位置を配列に覚えておき、改めてその配列の逆順にファイルハンドルを移動する処理を行なっている。ファイル全体をオンメモリで処理しないので、先の例よりは安全であるが、改行が膨大に存在する長大なファイルの場合、配列のためのメモリ不足になるかもしれない。よって、以下のような安全な実装も考えられる。

#!/usr/bin/python
import sys
import os
f = open(sys.argv[1])
f.seek(0, os.SEEK_END)
p = f.tell() - 2
while p >= 0:
    f.seek(p)
    p -= 1
    if not p < 0: c = f.read(1)
    if p < 0 or c == "\n":
        sys.stdout.write(f.readline())

ここでは始めにファイルハンドルの位置をファイルのの末尾に移動し、1バイト読み込みつつ、そのファイルハンドルの位置を1バイトずつ戻す処理を行なっている。ファイル全体をオンメモリで処理しないので、先の例よりは安全である。

[sed] [Awk] [Perl] [Ruby] [Python]

`fold`

この例、1行あたりの制御コードを考慮した文字数(既定値は80)を越えたら改行を挿入する python スクリプトを示そう。これはカウントが必要になるので sed では困難な好例となっている。この例では fold.py という実行権のついたファイルに記述するものとする。

#!/usr/bin/python
import sys
import fileinput
n = 8
w = 80
argv = sys.argv[1:]
while argv:
    if argv[0] == "--": argv.pop(0); break
    elif argv[0] == "-n" and 1 < len(argv): argv.pop(0); n = int(argv[0])
    elif argv[0] == "-w" and 1 < len(argv): argv.pop(0); w = int(argv[0])
    else: break
    argv.pop(0)
for line in fileinput.input(argv):
    line = line.rstrip("\r\n")
    l = 0
    for i in range(len(line)):
        c = line[i:i+1]
        d = (-1 if (l > 0) else 0) if (c == "\b") else -l if (c == "\r") else n - (l % n) if (c == "\t") else 1
        if l+d > w:
            print
            l = d
        else:
            l += d
        sys.stdout.write(c)
    print

ここで、'sys.argv' のスライスを 'argv' にコピーして、自前でコマンドラインオプションを解析しているが、これは Python では Perl や Ruby のように '-s' オプションでコマンドラインオプションによる大域変数の設定などはないからである。とは言え、本質的ではないので行数を少なめに示していることに留意。その場合、'fileinput.input' の引数はコマンドラインオプションが消費された 'argv' となる。

このように、fold と同じ python スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`fold -s`

この例、1行あたりの制御コードを考慮した文字数(既定値は80)を越えようとするブランクに変わり改行を挿入する python スクリプトを示そう。これもカウントが必要になるので sed では困難な好例となっているだけでなく、python でもかなり複雑にならざるを得ない。この例では fold-s.py という実行権のついたファイルに記述するものとする。

#!/usr/bin/python
import sys
import fileinput
import re
n = 8
w = 80
def increment(l, c):
    return (-1 if (l > 0) else 0) if (c == "\b") else -l if (c == "\r") else n - (l % n) if (c == "\t") else 1
argv = sys.argv[1:]
while argv:
    if argv[0] == "--": argv.pop(0); break
    elif argv[0] == "-n" and 1 < len(argv): argv.pop(0); n = int(argv[0])
    elif argv[0] == "-w" and 1 < len(argv): argv.pop(0); w = int(argv[0])
    else: break
    argv.pop(0)
for line in fileinput.input(argv):
    line = line.rstrip("\r\n")
    buf = ""
    l = siz = 0
    if line == "":
        print ""
        continue
    for i in range(len(line)):
        c = line[i:i+1]
        if l + increment(l, c) > w:
            j = siz - 1
            while j >= 0 and not re.search(r"[\t-\r ]", buf[j:j+1]):
                j -= 1
            space = j
            if space != -1:
                space += 1
                print "%.*s" % (space, buf)
                buf = buf[space:siz]
                siz -= space
                l = 0
                for j in range(siz): l += increment(l, buf[j:j+1])
            else:
                print "%.*s" % (siz, buf)
                l = siz = 0
        l += increment(l, c)
        buf = buf[0:siz] + c
        siz += 1
    if siz != 0:
        print "%.*s" % (siz, buf)

ここでユーザ定義関数を効果的に使用していることに注目したい。しかし、もっと簡単になるような気もする…

このように、fold -s と同じ python スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`strings -a [-n 4] [-t d|o|x]`

この例、ファイルの連続した印字可能な4文字以上の文字列を表示する python スクリプトを示す。この例では strings.py という実行権のついたファイルに記述するものとする。

#!/usr/bin/python
import sys
import fileinput
import re
n = 4
t = None
argv = sys.argv[1:]
while argv:
    if argv[0] == "--": argv.pop(0); break
    elif argv[0] == "-n" and 1 < len(argv): argv.pop(0); n = int(argv[0])
    elif argv[0] == "-t" and 1 < len(argv): argv.pop(0); t = argv[0]
    else: break
    argv.pop(0)
if t: fmt = "%x %s" if (t == "x") else "%o %s" if (t == "o") else "%d %s"
for line in fileinput.input(argv):
    if fileinput.isfirstline(): p = 0
    p += len(line)
    line = line.rstrip("\r\n")
    for m in re.finditer(r"([\f !-\/0-9:-\@A-Z[-`a-z{-~]{%d,})" % (n), line):
        if not t:
            print m.group(1)
        else:
            print fmt % ((p - (len(line) + 1) + m.start(1)), m.group(0))

この strings.py では、'-n 4', '-t d|o|x' オプションが指定できるように自前でしている。ちなみに、Python では 'getopt', 'argparse' のようなコマンドラインオプション解析ライブラリがサポートされるので、それらの利用も検討しよう。

このように、strings -a と同じ python スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`printenv`

さて、ここまでは入力ファイル駆動型のプログラムばかりであったが、そうではなく自律型のプログラムでは 'fileinput' は不要となる。

この例、環境変数をすべて表示する python スクリプトを示す。この例では printenv.py という実行権のついたファイルに記述するものとする。

#!/usr/bin/python
import os
for k, v in os.environ.iteritems():
    print "%s=%s" % (k, v)

辞書(ハッシュ)のキー値をすべて取り出すには、このように 'iteritems' メソッドを使う。

無論、以下のようにキー値をすべて取り出すやり方でも構わない。

	python -c '
import os
for k in iter(os.environ): print "%s=%s" % (k, os.environ[k])
'

このように、printenv と同じ python スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`yes [expletive]`

この例、プロセス停止まで永遠に "yes^J" を印字し続ける python スクリプトを示す。但しこのコマンド、第一引数を指定した場合には "yes" の代わりにそれを印字する仕様となっている。この例では yes.py という実行権のついたファイルに記述するものとする。

#!/usr/bin/python
import sys
y = sys.argv[1] if sys.argv[1:] else "yes"
while True: print y

このように、yes と同じ python スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`cmp [-l|-s] file1 file2`

この例、二つのファイルのバイトの差異を返す python スクリプトを示す。バイナリファイルとしての扱いが可能な Python での好例となる。

#!/usr/bin/python
import sys
s, l = False, False
argv = sys.argv[1:]
while argv:
    if argv[0] == '--': argv.pop(0); break
    elif argv[0] == '-s': s = True
    elif argv[0] == '-l': l = True
    else: break
    argv.pop(0)
blksize = 4096
bc, lc, rv = 0, 0, 0
if argv[0] == argv[1]: sys.exit(0)
f = (open(argv[0], "rb"), open(argv[1], "rb"))
while True:
    b = (f[0].read(blksize), f[1].read(blksize))
    sz = len(b[0]) if (len(b[0]) < len(b[1])) else len(b[1])
    if not l:
        for i in range(sz):
            if b[0][i:i+1] == "\n": lc += 1
            if b[0][i] != b[1][i]:
                if not s: print argv[0] + " " + argv[1] + " differ: char " + (bc + i + 1) + ", line " + (lc + 1)
                rv = 1
                break
        bc += sz
    else:
        for i in range(sz):
            if b[0][i] != b[1][i]:
                print "%4d %3o %3o" % (bc + i + 1, b[0][i], b[1][i])
                rv = 1
        bc += sz
    if not (len(b[0]) == blksize and len(b[1]) == blksize):
        sys.stderr.write("cmp: EOF on " + (argv[0] if (len(b[0]) < len(b[1])) else argv[1]) + "\n")
        rv = 1
    if not (len(b[0]) == blksize and len(b[1]) == blksize): break
f[0].close()
f[1].close()
sys.exit(rv)

このように cmp と同じ python スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`paste [-d delimiter] [-s] file ...`

この例、複数のファイルの内容を行毎に結合する python スクリプトを示す。但しこのコマンド、'-s' オプションを指定した場合はファイル毎に改行を除去して結合する仕様となっており、特に難しくはない。この例では paste.py という実行権のついたファイルに記述するものとする。

Python ではファイル操作関数が揃っているので、以下のように sed では困難な、複数のファイルの平行な入力に対応できる。

#!/usr/bin/python
import sys
import fileinput
def getlinepos(f, p):
    if p < 0: return None, -1
    fh = open(f, "r")
    fh.seek(0, 2)
    e = fh.tell()
    fh.seek(p, 0)
    b = fh.readline()
    p = fh.tell()
    p = p if p != e else -1
    fh.close()
    return b, p
s = False
d = "\t"
argv = sys.argv[1:]
while argv:
    if argv[0] == "--": argv.pop(0); break
    elif argv[0] == "-s": s = True
    elif argv[0] == "-d" and 1 < len(argv): argv.pop(0); d = argv[0]
    else: break
    argv.pop(0)
if not s:
    po = [ 0 ]*len(argv)
    c = len(argv)
    while c:
        l = ""
        for a in range(len(argv)):
            if a != 0: l += "%s" % (d)
            if po[a] < 0: continue
            b, po[a] = getlinepos(argv[a], po[a])
            b = b.rstrip("\r\n")
            l += "%s" % (b)
            if po[a] < 0: c -= 1
        print l
else:
    previous_nr = 0
    for line in fileinput.input(argv):
        if fileinput.isfirstline():
            previous_nr = fileinput.lineno() - 1
        fnr = fileinput.lineno() - previous_nr
        line = line.rstrip("\r\n")
        if fnr == 1 and previous_nr > 0:
            sys.stdout.write("%s" % ("\n"))
        elif fnr != 1:
            sys.stdout.write("%s" % (d))
        sys.stdout.write("%s" % (line))
    if fileinput.lineno() > 0:
        sys.stdout.write("%s" % ("\n"))

このように、paste と同じ python スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`comm [-1] [-2] [-3] file1 file2`

この例、複数のファイルの内容を行毎に比較する python スクリプトを示す。この例では comm.py という実行権のついたファイルに記述するものとする。

入力ファイルは、行でソートされていることが前提となっているので、複数のファイルをキーの文字列の大小で並行して読み進めていけば良い。

#!/usr/bin/python
import sys
def getlinepos(f, p):
    if p < 0: return None, -1
    fh = open(f, "r")
    fh.seek(0, 2)
    e = fh.tell()
    fh.seek(p, 0)
    b = fh.readline()
    p = fh.tell()
    p = p if p != e else -1
    fh.close()
    return b, p

s1 = False
s2 = False
s3 = False
argv = sys.argv[1:]
while argv:
    if argv[0] == '--': argv.pop(0); break
    elif argv[0] == '-1': s1 = True
    elif argv[0] == '-2': s2 = True
    elif argv[0] == '-3': s3 = True
    else: break
    argv.pop(0)

cf, s, sc, po = [ -1 ]*len(argv), [ False ]*(len(argv)+1), [ 0 ]*(len(argv)+1), [ 0 ]*len(argv)
s[0] = s1
s[1] = s2
s[2] = s3
for a in range(len(argv)+1):
    for j in range(a+1):
        if s[j]: sc[a] += 1
km = None
c = len(argv)
last_a = -1
ceq = 0
while c:
    for a in range(len(argv)):
        cf[a] = -1
        while not po[a] < 0 and cf[a] == -1:
            b, po[a] = getlinepos(argv[a], po[a])
            b = b.rstrip("\r\n")
            if not km:
                km = b
                cf[a] = None
                last_a = a
                ceq = 0
            else:
                if km < b:
                    if ceq + 1 != len(argv):
                        if not s[last_a]: print "%s%s" % ("\t"*(last_a-sc[last_a]), km)
                    km = b
                    cf[a] = 1
                    last_a = a
                    ceq = 0
                elif km == b:
                    if ceq + 1 != len(argv):
                        if not s[len(argv)]: print "%s%s" % ("\t"*(len(argv)-sc[len(argv)]), b)
                        ceq += 1
                    else:
                        ceq = 0
                    cf[a] = 0
                    last_a = a
                else:
                    if not s[a]: print "%s%s" % ("\t"*(a-sc[a]), b)
                    cf[a] = -1
                    ceq = 0
            if po[a] < 0:
                c -= 1
                if c == 0 and km:
                    if ceq + 1 != len(argv):
                        if not s[last_a]: print "%s%s" % ("\t"*(last_a-sc[last_a]), km)
                continue

このように、comm と同じ python スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`join [-a file_number|-v file_number] ... [-t char] [-1 field] [-2 field] file1 file2`

この例、複数のファイルの内容を行のキー毎に結合する python スクリプトを示す。この例では join.py という実行権のついたファイルに記述するものとする。

入力ファイルは、キーとなるフィールドでソートされていることが前提となっているので、複数のファイルをキーの文字列の大小で並行して読み進めていけば良い。とは言え、以下のように多少繁雑になるだろう。

#!/usr/bin/python
import sys
def getlinepos(f, p):
    if p < 0: return None, -1
    fh = open(f, "r")
    fh.seek(0, 2)
    e = fh.tell()
    fh.seek(p, 0)
    b = fh.readline()
    p = fh.tell()
    p = p if p != e else -1
    fh.close()
    return b, p

def printout():
    global argv
    global t, va, na, km
    n, m, o = 0, 0, None
    for i in range(len(argv)):
        if not va[i]: continue
        n = i + 1
        m += 1
        if not o:
            o = va[i]
        else:
            o += t + va[i]
        va[i] = None
    if o and (m == len(argv) or n in na): print km + t + o

na = []
nv = []
t = ' '
n1 = 1
n2 = 1
argv = sys.argv[1:]
while argv:
    if argv[0] == '--': argv.pop(0); break
    elif argv[0] == '-a' and 1 < len(argv): argv.pop(0); na.append(int(argv[0]))
    elif argv[0] == '-v' and 1 < len(argv): argv.pop(0); nv.append(int(argv[0]))
    elif argv[0] == '-t' and 1 < len(argv): argv.pop(0); t = argv[0]
    elif argv[0] == '-1' and 1 < len(argv): argv.pop(0); n1 = int(argv[0])
    elif argv[0] == '-2' and 1 < len(argv): argv.pop(0); n2 = int(argv[0])
    else: break
    argv.pop(0)

cf, kn, va, po = [ -1 ]*len(argv), [ 1 ]*len(argv), [ None ]*len(argv), [ 0 ]*len(argv)
kn[0] = n1
kn[1] = n2
km = None
c = len(argv)
last_a = -1
while c:
    for a in range(len(argv)):
        cf[a] = -1
        while not po[a] < 0 and cf[a] == -1:
            b, po[a] = getlinepos(argv[a], po[a])
            b = b.rstrip("\r\n")
            f = b.split(t)
            k = f[kn[a]-1]
            v = None
            for i in range(len(f)):
                if i+1 == kn[a]: continue
                if not v:
                    v = f[i]
                else:
                    v += t + f[i]
            if not km:
                km = k
                va[a] = v
                cf[a] = 0
                last_a = a
            else:
                if km < k:
                    if last_a+1 in nv:
                        if va[last_a]: print km + t + va[last_a]
                        va[last_a] = None
                    if a+1 in nv and last_a == a: print k + t + v
                    if len(nv) == 0: printout()
                    km = k
                    if len(nv) == 0: va[a] = v
                    cf[a] = 1
                    last_a = a
                elif km == k:
                    if len(nv) == 0: va[a] = v
                    cf[a] = 0
                    last_a = a
                else:
                    if a+1 in na or a+1 in nv: print k + t + v
                    va[a] = None
                    cf[a] = -1
            if po[a] < 0:
                c -= 1
                if c == 0: printout()
                continue

このように、join と同じ python スクリプトは以上のようになる。但し、正確には単一ファイル内の重複するキーにおける挙動には対応していない。

[sed] [Awk] [Perl] [Ruby] [Python]

`split [-l line_count|-b number[k|m]] [-a suffix_length] [file [name]]`

この例、単一のファイルの内容を行数で複数のファイルに分割する python スクリプトを示す。この例では split.py という実行権のついたファイルに記述するものとする。

Python では、`split` コマンドにおける '-b' オプションによるバイナリファイルとしての分割にも対応可能である。

#!/usr/bin/python
import sys
import re
def outputfilename_digit(nf):
    global pre, n, suf
    return '%s%0*d%s' % (pre, n, nf, suf)
def outputfilename_lower(nf):
    global pre, n, suf
    b = d = 26
    while int(nf / d) > 0:
        d *= b
    d /= b
    xxxxxx = ''
    while True:
        r = int(nf / d)
        nf -= d * r
        xxxxxx = xxxxxx + '%c' % (r + 97)
        d = int(d / b)
        if not d > 0: break
    while len(xxxxxx) < n:
        xxxxxx = '%c' % (0 + 97) + xxxxxx
    return '%s%s%s' % (pre, xxxxxx, suf)
def outputfilename(nf):
    global d
    return outputfilename_digit(nf) if d else outputfilename_lower(nf)
units = {
  'b':             512, # blocks
  'KB':           1000, # KiloBytes
  'K':            1024, # KibiBytes
  'k':            1024, # KibiBytes
  'MB':      1000*1000, # MegaBytes
  'M':       1024*1024, # MebiBytes
  'm':       1024*1024, # MebiBytes
  'GB': 1000*1000*1000, # GigaBytes
  'G':  1024*1024*1024, # GibiBytes
  'g':  1024*1024*1024, # GibiBytes
}
k = None
s = True
d = None
pre = ''
suf = ''
n = 2
lc = 1000
bc = 0
f = ''
argv = sys.argv[1:]
while argv:
  if argv[0] == '--': argv.pop(0); break
  elif argv[0] == '-k': k = True
  elif argv[0] == '--verbose': s = None
  elif argv[0] == '-d': d = True
  elif argv[0] == '-f' and 1 < len(argv): argv.pop(0); pre = argv[0]
  elif argv[0] == '-x' and 1 < len(argv): argv.pop(0); suf = argv[0]
  elif argv[0] == '-a' and 1 < len(argv): argv.pop(0); n = int(argv[0])
  elif argv[0] == '-l' and 1 < len(argv): argv.pop(0); lc = int(argv[0])
  elif argv[0] == '-b':
      argv.pop(0)
      m = re.match(r"^(\d+)(KB|MB|GB|[KMGbkmg])?$", argv[0])
      if m:
          bc = int(m.group(0))
          if m.group(2): bc *= units[m.group(2)]
  else:
      if not f:
          f = argv[0]
      else:
          break
  argv.pop(0)
if argv: pre = argv[0]
if bc == 0:
    l = 0
    nr = 0
    nf = 0; of = open(outputfilename(nf), 'w')
    fh = open(f)
    for line in fh:
        nr += 1
        of.write(line)
        if nr % lc == 0:
            if not s: sys.stdout.write(l)
            l = 0
            of.close(); nf += 1; of = open(outputfilename(nf), 'w')
        l += len(line)
    if not s: sys.stdout.write(l)
else:
    buf = ' ' * bc
    nf = 0
    fh = open(f, "rb")
    of = None
    buf = fh.read(bc)
    while buf:
        if of: of.close()
        of = open(outputfilename(nf), 'wb'); nf += 1
        of.write(buf)
        buf = fh.read(bc)

このように、split と同じ python スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`csplit [-k] [-s] [-f prefix] [-n number] file arg1 ...argn`

この例、単一のファイルの内容を行のパターンや行番号で複数のファイルに分割する python スクリプトを示す。この例では csplit.py という実行権のついたファイルに記述するものとする。

Python では、'seek' 等のファイル操作関数が揃っているので、なんら問題はない。

#!/usr/bin/python
import sys
import re
def outputfilename(nf):
    global pre, n, suf
    return '%s%0*d%s' % (pre, n, nf, suf)
def nextsplit():
    global argv
    global p, op
    global previous_stl, stl, ope, rep, ln
    global reg, ofs
    global np, c, oc
    previous_stl = stl
    ope, rep, stl, ln = argv.pop(0) if argv else None, 0, False, 0
    if ope:
        m = re.match(r"^\{(\d+)\}$", argv[0])
        if m:
            rep = int(m.group(1))
            argv.pop(0)
    else:
        ope = 0
        return
    m = re.match(r"^[/%](.*)[/%]([-+]?\d+)?$", ope)
    if m:
        reg, ofs = m.group(1), int(m.group(2)) if m.group(2) else 0
        np, c, oc = -ofs+1, 0, 0
        p = [ 0 ]*(np+1)
        op = [ 0 ]*(np+1)
        stl = True if re.match(r"^%(.*)%([-+]?\d+)?$", ope) else False
        ope = 1
    else:
        m = re.match(r"^(\d+)$", ope)
        if m:
            ln = int(m.group(1))
            ope = 2

k = None
s = None
d = None
pre = 'xx'
suf = ''
n = 2
f = '-'
argv = sys.argv[1:]
while argv:
    if argv[0] == '--': argv.pop(0); break
    elif argv[0] == '-k': k = True
    elif argv[0] == '-s': s = True
    elif argv[0] == '-d': d = True
    elif argv[0] == '-f' and 1 < len(sys.argv): argv.pop(0); pre = argv[0]
    elif argv[0] == '-x' and 1 < len(sys.argv): argv.pop(0); suf = argv[0]
    elif argv[0] == '-n' and 1 < len(sys.argv): argv.pop(0); n = int(argv[0])
    else: f = argv[0]; argv.pop(0); break
    argv.pop(0)

stl = False
ofs = 0
c, oc = 0, 0
ol = 0

fh = open(f, "r")
nf = 0
of = open(outputfilename(nf), 'w'); nf += 1
nextsplit()
if ope == 1 and ofs < 0:
    p[c % (np+1)] = fh.tell()
    c += 1
if ope == 1 and ofs < 0 and not stl:
    op[oc % (np+1)] = of.tell()
    oc += 1
lineno = 0
for line in fh:
    lineno += 1
    if ofs < 0:
        p[c % (np+1)] = fh.tell()
        c += 1
    if ope == 1:
        if re.search(reg, line):
            if not ofs < 0:
                for k in range(ofs):
                    if not stl:
                        of.write(line)
                        ol += len(line)
                    for line in fh: break
                    if not line: break
                    lineno += 1
            if not stl:
                if ofs < 0:
                    of.truncate(op[(oc-1-(np-1)) % (np+1)]) # or warn("cannot truncate: #{$!}")
                    ol -= of.tell() - op[(oc-1-(np-1)) % (np+1)]
                if not s: print ol
                of.close(); of = open(outputfilename(nf), 'w'); nf += 1
                ol = 0
            if ofs < 0:
                for i in range(np-1):
                    fh.seek(p[(c-1-(np-i)) % (np+1)], 0) # == 0 or warn("cannot seek")
                    for line in fh: break
                    of.write(line)
                    ol += len(line)
                fh.seek(p[(c-1-(np-np)) % (np+1)], 0) # == 0 or warn("cannot seek:")
                if not stl:
                    tf = open(outputfilename(nf-2), 'w+')
                    tf.truncate(op[(oc-1-(np-1)) % (np+1)]) # or warn("cannot truncate: #{$!}")
                    tf.close()
            if rep == 0:
                nextsplit()
            else:
                rep -= 1
    elif ope == 2:
        if lineno % ln == 0:
            if not s: print ol
            of.close(); of = open(outputfilename(nf), 'w'); nf += 1
            ol = 0
            if rep == 0:
                nextsplit()
            else:
                rep -= 1
    if previous_stl or not stl:
        of.write(line)
        ol += len(line)
    if ope == 1 and ofs < 0 and not stl:
        op[oc % (np+1)] = of.tell()
        oc += 1
if not s: print ol
fh.close()
of.close()

このように、csplit と同じ python スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`seq [-f format] [-s separator] [low [increment]] hi`

この例、GNU coreutils seq コマンドのように、単調増加する数列を生成する python スクリプト seq.py を示す。

#!/usr/bin/python
import sys
import re
f = None
s = "\n"
rep, beg, dlt, end = None, 1, 1, None
argv = sys.argv[1:]
while argv:
    if argv[0] == '--': argv.pop(0); break
    elif argv[0] == '-f' and 1 < len(argv): argv.pop(0); f = argv[0]
    elif argv[0] == '-s' and 1 < len(argv): argv.pop(0); s = argv[0]
    else: break
    argv.pop(0)
if len(argv) == 1:
    end = float(argv[0])
elif len(argv) == 2:
    beg, end = float(argv[0]), float(argv[1])
elif len(argv) == 3:
    beg, dlt, end = float(argv[0]), float(argv[1]), float(argv[2])
else:
    sys.exit(1)
if not f:
    f = "%d"
    if (isinstance(beg, str) and re.search(r"\.", beg)) or\
       (isinstance(end, str) and re.search(r"\.", end)): f = "%g"
rep = long((end - beg)/dlt)
if rep > 0: sys.stdout.write(f % (beg))
for i in range(1, rep+1):
    sys.stdout.write(s + f % (dlt*i + beg))
if rep > 0: sys.stdout.write("\n")

オブジェクトへの検査は Perl, Ruby に比べてシビアだ。よって、'isinstance' が必要で冗長になる。

このように、GNU coreutils seq と同じ python スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`jot [-c|-n|-r] [-b word] [-w word] [-s string] [reps [begin [end [s]]]]`

この例、BSD jot コマンドのように、単調増減する数列、乱数列、定数列を生成する python スクリプト jot.py を示す。

#!/usr/bin/python
import sys
import re
import random
f = None
s = "\n"
rep, beg, dlt, end = None, 1, 1, None
w, b = None, None
r = False
argv = sys.argv[1:]
while argv:
    if argv[0] == '--': argv.pop(0); break
    elif argv[0] == '-w' and 1 < len(argv): argv.pop(0); w = argv[0]
    elif argv[0] == '-c': f = "%c"
    elif argv[0] == '-b' and 1 < len(argv): argv.pop(0); b = argv[0]
    elif argv[0] == '-s' and 1 < len(argv): argv.pop(0); s = argv[0]
    elif argv[0] == '-r': r = True; dlt = None
    else: break
    argv.pop(0)
if len(argv) == 1:
    rep = long(argv[0])-1
elif len(argv) == 2:
    rep, beg = long(argv[0])-1, argv[1]
elif len(argv) == 3:
    rep, beg, end = None if (argv[0] == "-") else long(argv[0])-1,\
                    None if (argv[1] == "-") else argv[1],\
                    None if (argv[2] == "-") else argv[2]
elif len(argv) == 4:
    rep, beg, end, dlt = None if (argv[0] == "-") else long(argv[0])-1,\
                         None if (argv[1] == "-") else argv[1],\
                         None if (argv[2] == "-") else argv[2],\
                         None if (argv[3] == "-") else float(argv[3])
else:
    sys.exit(1)
integer_flag = True
if f == None:
    f = "%d"
    if (isinstance(beg, str) and re.search(r"\.", beg)) or\
       (isinstance(end, str) and re.search(r"\.", end)):
        f = "%g"
        integer_flag = False
if w != None:
    if re.search(r"%", w):
        f = w
    else:
        f = w + f
if beg != None:
    if re.search(r"^\D$", str(beg)):
        beg = ord(beg[0])
    else:
        beg = float(beg)
if end != None:
    if re.search(r"^\D$", str(end)):
        end = ord(end[0])
    else:
        end = float(end)
if r:
    random.seed(dlt)
    dlt = None
if rep == -1:
    pass
elif rep != None:
    if beg == None: beg = end - dlt*rep
    if end == None: end = beg + dlt*rep
    dlt = (end - beg)/rep
else:
    rep = long((end - beg)/dlt)
if b != None:
    if rep == -1:
        while True:
            sys.stdout.write(b + s)
    else:
        if rep > 0: sys.stdout.write(b)
        for i in range(1, rep+1):
            sys.stdout.write(s + b)
        if rep > 0: sys.stdout.write("\n")
elif r:
    dlt = (end - beg)
    if rep == -1:
        i = 0
        while True:
            sys.stdout.write(f + s % (random.randint(0, dlt-0) + beg))
    else:
        if rep > 0: sys.stdout.write(f % (random.randint(0, dlt-0) + beg))
        for i in range(1, rep+1):
            sys.stdout.write(s + f % (random.randint(0, dlt-0) + beg))
        if rep > 0: sys.stdout.write("\n")
else:
    if rep == -1:
        i = 0
        while True:
            sys.stdout.write(f + s % (dlt*i + beg))
            i += 1
    else:
        if rep > 0: sys.stdout.write(f % (long(beg) if integer_flag else beg))
        for i in range(1, rep+1):
            sys.stdout.write(s + f % (long(dlt*i + beg) if integer_flag else dlt*i + beg))
        if rep > 0: sys.stdout.write("\n")

オリジナルと微妙に既定の書式の扱いが異なるが、ほぼ等価な実装となっている。

このように、BSD jot と同じ python スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`shuf [-r [-n times]] -e arg...`, `shuf [-r [-n times]] -i 1-6`, `shuf [-r [-n times]] [filename]`

この例、GNU coreutils shuf コマンドはオプションの有無によって以下の機能を持つ。

  1. `shuf [-r [-n times]] -e arg...` - コマンドライン引数の配列をシャッフル。 '-r' のとき無限回数または '-n times' で指定回数で生成。
  2. `shuf [-r [-n times]] -i 1-6` - 1から6までの数列(数値は任意)をシャッフル。 '-r' のとき無限回数または '-n times' で指定回数で生成。
  3. `shuf [-r [-n times]] [filename]` - ファイルの行をシャッフル。 '-r' のとき無限回数または '-n times' で指定回数で生成。

ここでは、shuf.py という実行権のついたファイルに記述するものとし、以下のように使用するものとする。

  1. `shuf.py [-r [-n times]] -e arg...`
  2. `shuf.py [-r [-n times]] -i 1-6`
  3. `shuf.py [-r [-n times]] [filename]`
#!/usr/bin/python
import sys
import re
import random
r, n, e, i = False, 0, False, None
argv = sys.argv[1:]
while argv:
    if argv[0] == '--': argv.pop(0); break
    elif argv[0] == '-r': r = True
    elif argv[0] == '-n' and 1 < len(argv): argv.pop(0); n = int(argv[0])
    elif argv[0] == '-e': e = True
    elif argv[0] == '-i' and 1 < len(argv): argv.pop(0); i = argv[0]
    else: break
    argv.pop(0)
if not r:
    if e:
        random.seed()
        while argv:
            sys.stdout.write(argv.pop(random.randint(0, len(argv)-1)) + "\n")
    elif i:
        lo, hi = 1, 6
        m = re.match(r"(\d+)-(\d+)", i)
        lo, hi = int(m.group(1)), int(m.group(2))
        if lo > hi: sys.exit(1)
        a = []
        for it in range(lo, hi+1):
            a.append(it)
        random.seed()
        while a:
            sys.stdout.write(str(a.pop(random.randint(0, len(a)-1))) + "\n")
    else:
        if not argv: argv.insert(0, "-")
        f = sys.stdin if (argv[0] == "-") else open(argv[0], "r")
        a = []
        a.append(f.tell())
        line = f.readline()
        while line:
            a.append(f.tell())
            line = f.readline()
        a.pop()
        random.seed()
        while a:
            f.seek(a.pop(random.randint(0, len(a)-1)), 0)
            sys.stdout.write(f.readline())
        if f != sys.stdin: f.close()
else:
    if e:
        random.seed()
        if n:
            for it in range(1, n+1):
                sys.stdout.write(argv[random.randint(0, len(argv)-1)] + "\n")
        else:
            while True:
                sys.stdout.write(argv[random.randint(0, len(argv)-1)] + "\n")
    elif i:
        lo, hi = 1, 6
        m = re.match(r"(\d+)-(\d+)", i)
        lo, hi = int(m.group(1)), int(m.group(2))
        if lo > hi: sys.exit(1)
        a = (hi - lo + 1)
        random.seed()
        if n:
            for it in range(1, int(n)+1):
                sys.stdout.write(str(random.randint(0, a-1) + lo) + "\n")
        else:
            while True:
                sys.stdout.write(str(random.randint(0, a-1) + lo) + "\n")
    else:
        if not argv: argv.insert(0, "-")
        f = sys.stdin if (argv[0] == "-") else open(argv[0], "r")
        a = []
        a.append(f.tell())
        line = f.readline()
        while line:
            a.append(f.tell())
            line = f.readline()
        a.pop()
        random.seed()
        if n:
            for it in range(1, int(n)+1):
                f.seek(a[random.randint(0, len(a)-1)], 0)
                sys.stdout.write(f.readline())
        else:
            while True:
                f.seek(a[random.randint(0, len(a)-1)], 0)
                sys.stdout.write(f.readline())
        if f != sys.stdin: f.close()

このように、shuf と同じ python スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

`sort [-c|-m] [-u] [-r] [-d|-f|-i]`

POSIX sort コマンドは主に以下の3つの機能がある。

  1. `sort -c file` - 一つのファイルがソート済みか否かをチェックする。
  2. `sort -m file[...]` - ソート済みの複数のファイルをソートしてマージする。
  3. `sort file[...]` - 複数のファイルをソートしてマージする。

また、'-u' オプションで重複する行の出力の抑止、'-r' オプションで逆順、'-d|-f|-i' オプションで順に、blank と alnum のみの比較、小文字を大文字と見なして比較、印字可能文字のみの比較でソートする。

さらに、'-k key' オプションでソートのキーフィールド指定、'-t char' オプションでフィールド区切りを指定できる。'-k key', '-t char' オプションの実装は多少複雑になるので、行そのものをキーとするソートの実装を示す。

`sort -c [-u] [-r] [-d|-f|-i]`

#!/usr/bin/python
import sys
import re
r = 1
d, f, i, u = False, False, False, False
argv = sys.argv[1:]
while argv:
    if argv[0] == '--': argv.pop(0); break
    elif argv[0] == '-r': r = -1
    elif argv[0] == '-d': d = True
    elif argv[0] == '-f': f = True
    elif argv[0] == '-i': i = True
    elif argv[0] == '-u': u = True
    else: break
    argv.pop(0)

def normalize_line(b):
    global d, f, i
    b = b.rstrip("\r\n")
    if d: b = re.sub(r"[^\t 0-9A-Za-z]", '', b)
    if f: b = b.upper()
    if i: b = re.sub(r"[^ !-/0-9:-@A-Z[-`a-z{-~]", '', b)
    return b
def compare_lines(a, b):
    global r
    p, q = str(a), str(b)
    p = normalize_line(p)
    q = normalize_line(q)
    return cmp(p, q)*r

if not len(argv) > 0: argv.insert(0, "-") 
fh = sys.stdin if (argv[0] == "-") else open(argv[0], "r")
lineno = 0
for b0 in fh:
    lineno += 1
    break
for b in fh:
    lineno += 1
    if (not u and compare_lines(b0, b) == 1) or (u and compare_lines(b0, b) != -1):
        sys.stderr.write("sort: %s:%d: disorder: %s\n" % (argv[0], lineno, b))
        sys.exit(1)
    b0 = b
fh.close()
sys.exit(0)

行を読み込み、前の行と比較して 1 以外なら正常終了となる。'-u' オプションのときは 0 つまり重複する行も異常終了となる。

[sed] [Awk] [Perl] [Ruby] [Python]

`sort -m [-u] [-r] [-d|-f|-i]`

#!/usr/bin/python
import sys
import re
def getlinepos(f, p):
    if p < 0: return None, -1
    fh = open(f, "r")
    fh.seek(0, 2)
    e = fh.tell()
    fh.seek(p, 0)
    b = fh.readline()
    p = fh.tell()
    p = p if p != e else -1
    fh.close()
    return b, p

ofh = sys.stdout
r = 1
d, f, i, u = False, False, False, False
argv = sys.argv[1:]
while argv:
    if argv[0] == '--': argv.pop(0); break
    elif argv[0] == '-o' and 1 < len(argv): argv.pop(0); ofh = open(argv[0], 'w')
    elif argv[0] == '-r': r = -1
    elif argv[0] == '-d': d = True
    elif argv[0] == '-f': f = True
    elif argv[0] == '-i': i = True
    elif argv[0] == '-u': u = True
    else: break
    argv.pop(0)

def normalize_line(b):
    global d, f, i
    b = b.rstrip("\r\n")
    if d: b = re.sub(r"[^\t 0-9A-Za-z]", '', b)
    if f: b = b.upper()
    if i: b = re.sub(r"[^ !-/0-9:-@A-Z[-`a-z{-~]", '', b)
    return b
def compare_lines(a, b):
    global r
    p, q = str(a), str(b)
    p = normalize_line(p)
    q = normalize_line(q)
    return cmp(p, q)*r
def compare_line_positions(a, b):
    global argv, p
    r, q = getlinepos(argv[a], p[a])
    s, q = getlinepos(argv[b], p[b])
    return compare_lines(r, s)

a, p = [i for i in range(len(argv))], [0]*len(argv)
b0 = None
while len(a) > 1:
    a.sort(cmp=compare_line_positions)
    b, p[a[0]] = getlinepos(argv[a[0]], p[a[0]])
    if not u or (u and (not b0 or compare_lines(b0, b) != 0)):
        b0 = b
        ofh.write(b0)
    for i in range(len(a)):
        if a < len(a) and p[a[i]] == -1:
            a.pop(i)
while p[a[0]] != -1:
    b, p[a[0]] = getlinepos(argv[a[0]], p[a[0]])
    if not u or (u and (not b0 or compare_lines(b0, b) != 0)):
        b0 = b
        ofh.write(b0)

各ファイルはソート済みであることが前提としてあるので、すべてのファイルを並行して読み、それらの行を並び変えて先頭のファイルの行のみを出力して進めばよい。

[sed] [Awk] [Perl] [Ruby] [Python]

`sort [-r] [-u] [-d|-f|-i]`

#!/usr/bin/python
import sys
import re

ofh = sys.stdout
r = 1
d, f, i, u = False, False, False, False
argv = sys.argv[1:]
while argv:
    if argv[0] == '--': argv.pop(0); break
    elif argv[0] == '-o' and 1 < len(argv): argv.pop(0); ofh = open(argv[0], 'w')
    elif argv[0] == '-r': r = -1
    elif argv[0] == '-d': d = True
    elif argv[0] == '-f': f = True
    elif argv[0] == '-i': i = True
    elif argv[0] == '-u': u = True
    else: break
    argv.pop(0)

def normalize_line(b):
    global d, f, i
    b = b.rstrip("\r\n")
    if d: b = re.sub(r"[^\t 0-9A-Za-z]", '', b)
    if f: b = b.upper()
    if i: b = re.sub(r"[^ !-/0-9:-@A-Z[-`a-z{-~]", '', b)
    return b
def compare_lines(a, b):
    global r
    p, q = str(a), str(b)
    p = normalize_line(p)
    q = normalize_line(q)
    return cmp(p, q)*r

class File_Position:
    def __init__(self, name, position):
        self.name = name
        self.position = position

file_position_list = []

def file_position_getline(file_position):
    fh = open(file_position.name, "r")
    fh.seek(file_position.position, 0)
    b = fh.readline()
    fh.close()
    return b
def compare_file_position_getlines(a, b):
    p = file_position_getline(a)
    q = file_position_getline(b)
    return compare_lines(p, q)

for a in range(len(argv)):
    fh = open(argv[a], "r")
    while True:
        file_position_list.append(File_Position(argv[a], fh.tell()))
        b = fh.readline()
        if not b: break
    fh.close()
    file_position_list.pop()
file_position_list.sort(compare_file_position_getlines)
b0 = None
for file_position in file_position_list:
    b = file_position_getline(file_position)
    if not u or (u and (not b0 or compare_lines(b0, b) != 0)):
        b0 = b
        ofh.write(b0)

各ファイルの行頭のファイルポジションをファイル名とのペアで保持しておき、それを行の比較でソートすればよい。

このように、sort (但し、キーフィールド指定なし) と同じ python スクリプトは以上のようになる。

[sed] [Awk] [Perl] [Ruby] [Python]

参考文献

  1. Python 言語リファレンス
  2. Python 標準ライブラリ
  3. Python チュートリアル
Written by Taiji Yamada <taiji@aihara.co.jp>