TORIPIYO DIARY

recent events, IoT, programming, security topics

perlのワンライナーで文字列を置換する(初心者向け)

たまに、趣味や仕事でperlワンライナーで文字列置換をするのですが、そのたびに置換方法を忘れているので、ワンライナーで文字列を置換する時に知っておきたいことをまとめました。

目次

perlワンライナーで置換表現のサンプル

サーバの管理でよく遭遇する例です。

1. /etc/passwdの設定ファイルを修正する
/etc/passwdのバックアップファイル(ファイル名の末尾に.backupの文字列を付与)を作成して、ユーザのデフォルトshellをbashからzshに変更する

perl -pi.backup -e 's;/bin/bash;/bin/zsh;g' /etc/passwd

diff -U0 /etc/passwd.backup /etc/passwd
@@ -1 +1 @@
-root:x:0:0:root:/root:/bin/bash
+root:x:0:0:root:/root:/bin/zsh


2. apacheの設定ファイルを修正する
apacheの設定ファイルで、バックアップファイル(ファイル名の末尾に日付を付与)を作成して、MinSpareThreadsを75から125に変更する。
(^\s*MinSpareThreads\s+)の箇所は、(行の先頭からの空白文字列)MinSpareThreads(空白文字列)の文字列と一致して、一致した箇所が${1}に挿入されています。

perl -pi.$(date +%Y%m%d) -e 's;(^\s*MinSpareThreads\s+)\d+;${1}125;g' httpd-mpm.conf

diff -U0 httpd-mpm.conf.20180131 httpd-mpm.conf
@@ -46 +46 @@
-    MinSpareThreads         75
+    MinSpareThreads         125
@@ -63 +63 @@
-    MinSpareThreads         75
+    MinSpareThreads         125
@@ -83 +83 @@
-    MinSpareThreads         25
+    MinSpareThreads         125
@@ -97 +97 @@
-    MinSpareThreads          5
+    MinSpareThreads          125

ちなみに、正直にいうと$1を${1}にしないといけないことがわからなくて、stackoverflowに質問したところ、数分で回答が来ました。早い。
How to avoid wrong variable name interpretation in perl one-liner - Stack Overflow

ここから先は、上記例を理解するための解説です。

perlでのファイル文字列置換方法を知っていたほうがいい理由

sed
下記のリンク先の通り、sedは同じコマンドでもOSによって挙動の変わることがあります。複数のサーバを管理するのであれば、perlでの置換の方がより広い対象に適用できるので、学習コストを小さく出来ます。
環境に依存しないワンライナーを書くならsedよりperlの方がいい - Qiita

ruby
rubyは、そもそもデフォルトではインストールされていないサーバが多いです。ファイル文字列を置換する方法を覚えるのであれば、perlの方がより多くのサーバで利用出来るので効率的です。

文字列を置換するperlワンライナーの書き方

file1.txtという文字列をバックアップファイルの作成なしで置換する場合は、以下の形式で記述します。(perlコマンドのオプションについては後述)

perl -pi -e 'XXXXXXX' file1.txt

'XXXXXXX'の箇所には、どのように文字列の置換をするのかPerlのプログラムで記述します。's;A;B;g' とすると、Aという文字列をBという文字列で置換させるということになります。sは置換用の演算子を表していて、gが有ると置換対象文字列の該当する文字列を全て置換。gが無いと置換対象文字列の最初に該当する文字列のみ置換となります。また、iをgの位置に一緒に付与すると、大文字と小文字を区別せずに置換するようになります。

gが無い場合は、各行で該当する最初の文字列のみ置換される。

#  echo -e "aabbaabb\naabbaabbaabb" | perl -pe 's;aa;bb;'
bbbbaabb
bbbbaabbaabb

gが有る場合は、各行で該当する文字列全てに対して置換される。

#  echo -e "aabbaabb\naabbaabbaabb" | perl -pe 's;aa;bb;g'
bbbbbbbb
bbbbbbbbbbbb

iが有る場合は、大文字・小文字を区別せずに該当する文字列を置換する。(gが無いので各行の最初に該当する文字列のみ置換)

#  echo -e "AAbbaabb\naabbAAbbaabb" | perl -pe 's;aa;bb;i'
bbbbaabb
bbbbAAbbaabb
最低限覚えておきたいperlコマンドのオプション

以下のオプションは、perlワンライナーで置換するときに知っていた方がよいものです。

-e: 引数に指定した文字列をperlのプログラムと認識する

perl -e "print \"Hello World\n\""
Hello World

-p: ファイルやパイプの入力データに対して、行単位で処理・出力する
各入力行は、$_という変数で表現される。ちなみに、$.は現在行。下記の例では、パイプからの入力データに対して、各行の3番目の文字列をZに置き換えている。

echo -e "aaa\nbbb\nccc\nddd" | perl -pe 'substr($_,2,1,Z)'
aaZ
bbZ
ccZ
ddZ

-i: 入力ファイルの内容を更新して上書きする
このオプションは、-i <ファイル名> とすれば、指定したファイルをperlの処理後の結果で上書きする。-i<拡張子> <ファイル名> とすれば、指定された拡張子つきのバックアップファイルの作成と元ファイルの上書きをする。-iと拡張子の文字列の間にスペースは必要ない。

バックアップファイルを作成しない場合

$ cat sample.txt
aaa
$ perl -pe 's;aaa;bbb;g' -i sample.txt
$ cat sample.txt
bbb

バックアップファイルを作成する場合

$ cat sample2.txt
ccc
$ perl -i.backup -pe 's;ccc;ddd;g' sample2.txt
$ cat sample2.txt
ddd
$ ls -l sample2*
-rw-rw-r--. 1 vagrant vagrant 4 Jan 22 12:33 sample2.txt
-rw-rw-r--. 1 vagrant vagrant 4 Jan 22 12:33 sample2.txt.backup
色々な正規表現

正規表現には種類があります。これを知らないと、perl正規表現を利用した置換をする際、意図した置換にたどり着くのに苦労するかもしれません。

知っておいたほうがいい正規表現の種類は、

です。正規表現で表せる表現幅は、BRE < ERE < Perlの順番で広がっていきます。Perl正規表現は他のソフトウェアでも利用できるように、Perl正規表現を取り入れたPCRE(Perl Compatible Regular Expressions)という汎用ライブラリが作られており、Apache HTTPやPostfixなどで利用されています。

正規表現の違いをgrepコマンドで見ていきます。grep コマンドは、オプションによって利用する正規表現を変更できます。基本正規表現は、-G。拡張正規表現は、-E。Perl正規表現は、-P(Macではこのオプションは無し)です。(オプションの指定なしのデフォルトは、-Gオプションの基本正規表現です。)

実際に、"aという文字列が2個以上3個以下で連続して繋がっている文字列"を正規表現で表すと、BREでは、 a\{2,3\}, EREでは、 a{2,3}となり、BREとEREで正規表現の書き方が異なります。

BREモードでのgrepの動作

echo aa | grep "a\{2,3\}"
aa
echo aa | grep "a{2,3}"
(aaはひっかからない!)

EREモードでのgrepの動作

echo aa | grep -E "a\{2,3\}"
(aaはひっかからない!)
echo aa | grep -E "a{2,3}"
aa

このように、正規表現を書くときにはどの正規表現が利用されるのか意識していないと、意図する文字列が引っかからない可能性があります。

BRE(basic regular expression: 基本正規表現)、ERE(extended regular expression: 拡張正規表現)、Perl正規表現を簡単にまとめると次のようになります。

BRE ERE Perl正規表現 意味
^ ^ ^ 文字列の先頭(行の場合は行の先頭)に一致する。
$ $ $ 文字列の末尾(行の場合は行の行末)に一致する。
∗記号の直前の文字に対する、0回以上の繰り返しに一致する。
\+ + + +記号の直前の文字に対する、1回以上の繰り返しに一致する。
\? ? ? ?記号の直前の文字に対する、0回、もしくは1回の繰り返しに一致する。
\( \) ( ) ( ) ()に囲まれたパターンに一致した文字列を記憶する。記憶した一致文字列には、$0...$9の記号でアクセスできる。
\{m,n\} {m,n} {m,n} {}記号の直前の文字に対する、m回以上・n回以下の繰り返しに一致する。
\{m\} {m} {m} {}記号の直前の文字に対する、m回の繰り返しに一致する。
\{m,\} {m,} {m,} {}記号の直前の文字に対する、m回以上の繰り返しに一致する。

このサイトで、記述した正規表現が意図した通りのパターンか確認できます。pcre(php)というのがPerl正規表現の場合です。
Online regex tester and debugger: PHP, PCRE, Python, Golang and JavaScript


シングルクウォートで囲むかダブルクウォートで囲むか

perlのプログラムをシングルクウォートで囲んだ場合と、ダブルクウォートで囲んだ場合では動作は変わります。
シングルクウォートで囲まれた文字列は、その文字列のまま解釈されてperlプログラムが実行されます。一方、ダブルクウォートで囲まれた文字列の場合は、変換の必要な文字列を変換してperlプログラムが実行されます。

例えば、以下の例では、"s;$a;$b;g"の箇所が、"s;apple;banana;g"と変換されてからperlのプログラムが実行されています。

a=apple
b=banana
echo "my favorite is apple" | perl -pe "s;$a;$b;g"
test


こちらの例では、"s;$(logname);testuser;g"の箇所が、"s;vagrant;testuser;g"と変換されてからperlのプログラムが実行されています。(lognameコマンドは、現在のユーザのログイン名を表示します。)"s;`logname`;testuser;g"の場合も、"s;vagrant;testuser;g"と変換されています。

id | perl -pe "s;$(logname);testuser;g"
uid=1000(testuser) gid=1000(testuser) groups=1000(testuser) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023

id | perl -pe "s;`logname`;testuser;g"
uid=1000(testuser) gid=1000(testuser) groups=1000(testuser) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023

このように、ダブルクウォートの内部では、$, `, \ が特別な文字と解釈されます。特別な文字と解釈されないようにするためには、\$, $`, \\というように、特別な文字の前に\を付与します。
ちなみに、シングルクウォートとダブルクウォートで文字列の解釈が異なるのは、perlだからではなくて、bashをシェルとして利用しているからです。このシングルクウォートとダブルクウォートの変換の違いは他のコマンドを利用した時も現れます。このstackoverflowのページには、シングルクウォートとダブルクウォートでの比較がわかりやすくテーブル形式で記載した返答があります。
shell - Difference between single and double quotes in Bash - Stack Overflow