Pythonのf文字列は便利ですが、ちょっとした問題もあります。それを解決するために、Python 3.14ではテンプレート文字列が導入されました。f文字列の問題、t文字列とは、その基本的な使い方を紹介します。
Python 3.14では新たにテンプレート文字列(以下、t文字列)と呼ばれる新たな文字列リテラルが導入された。手短にいえばこれは「f文字列内に何らかの値を埋め込む前に処理を介入させることで、より安全な文字列補間を行える」ようにしたものだ。t文字列はPEP 750で提案されている(ただし、これを説明する公式なドキュメントは「string.templatelib --- Support for template string literals」となる)。
以下ではt文字列について簡単にまとめていく。
以下のコードを見てほしい。
import os
user_input = 'hp_kawasaki' # 実際にはユーザー入力を受け取るものとする
cmd_to_exec = f'echo hello {user_input}'
os.system(cmd_to_exec)
これはuser_inputに入力された名前を受け取り、それを基に「echo <名前>」という文字列を組み立てて、それをos.system関数で実行するコードだ。
どうもHPかわさきです。
ここでは「なぜos.system関数を使うの?」という質問はなしということで。あからさまに危険なコードの例だと思ってください。また、user_inputには'hp_kawasaki'を決め打ちで代入していますが、これも何らかの手段でユーザー入力を受け取るものだと考えてください。
このコードを例えば、macOSで実行すると次のようになる。
コンソールに「hello hp_kawasaki」と表示された。その下の「0」は問題なく実行が終わったことを意味する終了コードだ。このようにf文字列は手軽に、文字列に何らかの値を埋め込む(補間する)ことができる便利な仕組みである。
しかし、user_inputが次のようなものだったらどうだろう。
import os
user_input = 'hp_kawasaki; touch newfile.txt; ls -l newfile.txt'
cmd_to_exec = f'echo hello {user_input}'
os.system(cmd_to_exec)
「hp_kawasaki」に続けて「; touch newfile.txt; ls -l newfile.txt」とある。これを実行すると次のようになる。
「hp_kawasaki」の後ろにあるセミコロン「;」はf文字列にあるechoコマンドを終了させる。この結果、メッセージを表示した後、touchコマンドでnewfile.txtというファイルが新規に作成され、lsコマンドでその情報が表示されている。
ここではファイルの作成とその情報の表示だけだったが、f文字列ではこのようなプログラマーが想定していない値を受け取った場合に、それをf文字列では置換フィールド(波かっこ「{}」に囲まれた部分)に記述された式は、f文字列が評価されるときに即座に評価され、その値がf文字列に埋め込まれる。そのため、危険なコードが含まれていても、それを事前に無害化する処理などを差し込めないのが弱点といえる。
ここでは波かっこ「{}」で囲まれた部分を「置換フィールド」と呼んでいますが、ドキュメントにはこれが置換フィールドという名前だとは明言されてないんですよね。でも、分かりやすいってことで、ここでは置換フィールドと呼ぶことにします。
今述べたf文字列の問題点に対処するために生まれたのがt文字列といえる。t文字列は通常の文字列(f文字列を含む)のような見た目に分かりやすいイミュータブル値ではない。実際にt文字列を作成してみよう。
user_input = 'hp_kawasaki'
cmd_to_exec = t'echo hello {user_input}'
print(cmd_to_exec)
これを実行すると、その結果は次のようになる。
'hello hp_kawasaki'のようなシンプルな文字列リテラルを予想していると、ちょっとビックリするかもしれない。t文字列はstring.templatelibモジュールで定義されているTemplateクラスのインスタンスであり、ちょっと複雑な構造を持ったイミュータブルなオブジェクトである。
Templateオブジェクトには重要な属性が3つある。
上に示した実行結果ではstrings属性とinterpolations属性のみが表示されている点には注意しよう。上のコードで作成したt文字列が実際にはどのような形で上記の属性に格納されているのかを以下に示す。
記述した置換フィールドは1つだけなので、ここではinterpolations属性は要素を1つだけ持つタプルとなっていることには注意されたい。置換フィールドを複数記述すれば、interpolations属性の要素数も複数になる。Interpolationクラスのオブジェクトが持つ実際の値、式、文字列化の方法、書式化文字列はvalue、expression、conversion、format_specの各属性を使っても取得できる。
ここで重要なのはstrings属性に格納されるのは、プログラマーがt文字列内に自ら記述する文字列リテラルなので自分での制御が可能、つまり安全だと見なしてもよさそうなことだ。対して、interpolations属性には外部から入力された危険な値が格納される可能性がある。よって、無害化などの処理は主にこちらを対象に行うことになるだろう。
values属性はinterpolations属性に格納されるInterpolationオブジェクトの値(value属性)を格納している。置換フィールドに書かれた式の値を取得するだけなら、この属性が使えるかもしれない(最後に見るように何らかの前処理を行うときには、values属性に格納されている値は最終的に得られる文字列に埋め込まれる値とは異なる可能性がある点に注意)。
foo = 'FOO'
bar = 'BAR'
tstr = t'foo: {foo}, bar: {bar}'
for val in tstr.values:
print(val) # 'FOO'と'BAR'が出力される
最後にt文字列(Templateオブジェクト)は反復可能オブジェクトでもある。例えば、for文でこれを使うと、strings属性とinterpolations属性に含まれるそれぞれの値がt文字列で登場する順番に反復される。この機構を使って、t文字列に埋め込まれる値を処理しながら、最終的な結果(文字列)を構築するというのがt文字列のよくある使い方となるだろう。
今述べたt文字列の各要素を反復処理していくサンプルとして、先ほど紹介したf文字列には問題があったが、t文字列を使ってこれを無害化してみよう。以下にコードを再掲する。
import os
user_input = 'hp_kawasaki; touch newfile.txt; ls -l newfile.txt'
cmd_to_exec = f'echo hello {user_input}'
os.system(cmd_to_exec)
このf文字列にはセミコロンの後にファイルを新規に作成し、その情報を表示するシェルコマンドが含まれている。単純にこれを無害化するにはuser_input全体をクオートで囲って「echo 'hp_kawasaki; touch newfile.txt; ls -l newfile.txt'」がos.system関数で実行されるようにしてしまえばよい。これにはshlexモジュールのquote関数を使うのが簡単だ。
shlexモジュールはUNIX系統のOS向けに設計されているので、Windowsで使う際には正しくエスケープできるかの保証がないことには注意してください。
まず、cmd_to_execにf文字列ではなくt文字列を代入しよう。
import os
user_input = 'hp_kawasaki; touch newfile.txt; ls -l newfile.txt'
cmd_to_exec = t'echo hello {user_input}'
これをos.system関数に渡しても、これが文字列でないために例外が発生する。そこで、これを無害化して文字列に変換する何らかの処理が必要となる。ここでは以下のような関数を定義する。
from string.templatelib import Interpolation
from shlex import quote
def escape(tstr):
result = []
for part in tstr:
if isinstance(part, Interpolation):
tmp = quote(part.value)
result.append(tmp)
else:
result.append(part)
return ''.join(result)
escape関数では次のような処理を行っている。
for文で各要素を反復し、その中のif文で要素がInterpolationオブジェクトかどうかに応じて処理を切り分け、最後にそれらを結合して文字列を得るというのが王道的なやり方といえる。
後はこの関数を呼び出してt文字列から通常の文字列を作成し、それをos.system関数に渡すだけだ。全てのコードをまとめると次のようになる。
import os
from string.templatelib import Interpolation
from shlex import quote
def escape(tstr):
result = []
for part in tstr:
if isinstance(part, Interpolation):
tmp = quote(part.value)
result.append(tmp)
else:
result.append(part)
return ''.join(result)
user_input = 'hp_kawasaki; touch newfile.txt; ls -l newfile.txt'
cmd_to_exec = t'echo hello {user_input}'
result = escape(cmd_to_exec)
os.system(result)
実行結果を以下に示す。
エスケープによって元のuser_inputにあったセミコロン以降のtouchコマンドとlsコマンドも含めてechoコマンドに渡されるようになり、勝手にファイルが作成されたり、lsコマンドが実行されたりしなくなったのが分かる。
ここではt文字列が生まれた大きな理由であると思われる「文字列への値の安全な埋め込み」の例としたが、値を文字列に埋め込む前に何らかの処理を差し挟みたいというときにはt文字列を使うと何かよいことがあるかもしれない。
「何かよいことが」と書きましたが、ちょっとまだ具体的にこんなことができて便利だよ、というのは思い付いていないんですよね。思い付いたらPythonクイズの問題にでもしたいと思います(笑)。
Copyright© Digital Advantage Corp. All Rights Reserved.