特定の検索条件に基づいてファイル内の文字列を置き換えることは非常に一般的なタスクです。どうやって
- 現在のディレクトリ内のすべてのファイルで文字列
foo
を置き換えますbar
か? - サブディレクトリにも同じことを繰り返しますか?
- ファイル名が別の文字列と一致する場合にのみ置き換えますか?
- 特定のコンテキストで文字列が見つかった場合にのみ置き換えますか?
- 文字列が特定の行番号にある場合は置き換えますか?
- 複数の文字列を同じ置換で置き換えます
- 複数の文字列を異なる置換に置き換えます
特定の検索条件に基づいてファイル内の文字列を置き換えることは非常に一般的なタスクです。どうやって
foo
を置き換えますbar
か?回答:
これらは、ディレクトリに通常のファイルのみが含まれており、すべての非表示でないファイルを処理したいことがわかっている場合に 使用します。そうでない場合は、2のアプローチを使用します。
sed
この回答のソリューションはすべてGNUを前提としていますsed
。FreeBSDのか、OS / Xを使用している場合は、交換してください-i
と-i ''
。また-i
、のバージョンでスイッチを使用すると、sed
ファイルシステムのセキュリティに特定の意味があり、何らかの方法で配布する予定のスクリプトではお勧めできません。
非再帰的、このディレクトリ内のファイルのみ:
sed -i -- 's/foo/bar/g' *
perl -i -pe 's/foo/bar/g' ./*
このサブディレクトリおよびすべてのサブディレクトリ内の再帰的な通常ファイル(隠しファイルを含む)
find . -type f -exec sed -i 's/foo/bar/g' {} +
zshを使用している場合:
sed -i -- 's/foo/bar/g' **/*(D.)
(リストが大きすぎる場合は失敗する可能性がありzargs
ます。回避策を参照してください)。
Bashは通常のファイルを直接チェックできません。ループが必要です(波括弧はオプションをグローバルに設定することを避けます):
( shopt -s globstar dotglob;
for file in **; do
if [[ -f $file ]] && [[ -w $file ]]; then
sed -i -- 's/foo/bar/g' "$file"
fi
done
)
ファイルは、実際のファイル(-f)で書き込み可能な(-w)場合に選択されます。
このディレクトリ内の非再帰的なファイルのみ:
sed -i -- 's/foo/bar/g' *baz* ## all files whose name contains baz
sed -i -- 's/foo/bar/g' *.baz ## files ending in .baz
このサブディレクトリおよびすべてのサブディレクトリ内の再帰的な通常ファイル
find . -type f -name "*baz*" -exec sed -i 's/foo/bar/g' {} +
bashを使用している場合(波括弧はオプションをグローバルに設定しないでください):
( shopt -s globstar dotglob
sed -i -- 's/foo/bar/g' **baz*
sed -i -- 's/foo/bar/g' **.baz
)
zshを使用している場合:
sed -i -- 's/foo/bar/g' **/*baz*(D.)
sed -i -- 's/foo/bar/g' **/*.baz(D.)
これ--
はsed
、コマンドラインでこれ以上フラグが与えられないことを伝えるのに役立ちます。これは、で始まるファイル名から保護するのに役立ちます-
。
ファイルが特定のタイプ、たとえば実行可能ファイルである場合(man find
その他のオプションについては参照):
find . -type f -executable -exec sed -i 's/foo/bar/g' {} +
zsh
:
sed -i -- 's/foo/bar/g' **/*(D*)
同じ行に後である場合にのみ置換foo
します。bar
baz
sed -i 's/foo\(.*baz\)/bar\1/' file
でsed
、を使用\( \)
すると、括弧内にあるものがすべて保存され、でアクセスできます\1
。このテーマには多くのバリエーションがあります。そのような正規表現の詳細については、こちらをご覧ください。
置き換えるfoo
とbar
する場合にのみfoo
、入力ファイルの3D列(フィールド)に見出される(空白で区切られたフィールドを想定)。
gawk -i inplace '{gsub(/foo/,"baz",$3); print}' file
(gawk
4.1.0以降が必要)。
別のフィールドの$N
場合N
は、対象のフィールドの番号がどこにあるかを使用します。別のフィールド区切り文字(:
この例では)の場合:
gawk -i inplace -F':' '{gsub(/foo/,"baz",$3);print}' file
を使用した別のソリューションperl
:
perl -i -ane '$F[2]=~s/foo/baz/g; $" = " "; print "@F\n"' foo
注:両方awk
とperl
ソリューション(先頭と末尾の空白を削除し、一致してこれらの行の1つのスペース文字に空白のシーケンスを変換)をファイルで間隔に影響します。別のフィールドの$F[N-1]
場合N
は、希望するフィールド番号をwhere に使用し、別のフィールドセパレーターには($"=":"
出力フィールドセパレーターをに設定します:
)を使用します。
perl -i -F':' -ane '$F[2]=~s/foo/baz/g; $"=":";print "@F"' foo
置き換えfoo
でbar
のみ第四行に:
sed -i '4s/foo/bar/g' file
gawk -i inplace 'NR==4{gsub(/foo/,"baz")};1' file
perl -i -pe 's/foo/bar/g if $.==4' file
sed
コマンドを組み合わせることができます:
sed -i 's/foo/bar/g; s/baz/zab/g; s/Alice/Joan/g' file
順序が重要であることに注意してください(sed 's/foo/bar/g; s/bar/baz/g'
に置き換えfoo
られますbaz
)。
またはPerlコマンド
perl -i -pe 's/foo/bar/g; s/baz/zab/g; s/Alice/Joan/g' file
多数のパターンがある場合、パターンとその置換をsed
スクリプトファイルに保存する方が簡単です。
#! /usr/bin/sed -f
s/foo/bar/g
s/baz/zab/g
または、上記のパターンペアが多すぎて実行できない場合は、ファイルからパターンペアを読み取ることができます(スペースごとに区切られた2つのパターン、$ patternと$ replacement、行ごと)。
while read -r pattern replacement; do
sed -i "s/$pattern/$replacement/" file
done < patterns.txt
パターンの長いリストと大きなデータファイルの場合は非常に遅いため、sed
代わりにパターンを読み取ってスクリプトを作成することをお勧めします。以下では、<space>区切り文字が、ファイル内で1行に1つずつ出現するMATCH <space> REPLACEペアのリストを区切ると仮定していますpatterns.txt
。
sed 's| *\([^ ]*\) *\([^ ]*\).*|s/\1/\2/g|' <patterns.txt |
sed -f- ./editfile >outfile
上記の形式はほとんど任意であり、たとえば、MATCHまたはREPLACEのいずれかの<space>を許可していません。メソッドは非常に一般的です。基本的に、スクリプトのように見える出力ストリームを作成できる場合、のスクリプトファイルをstdin として指定することにより、そのストリームをスクリプトとしてソースできます。sed
sed
sed
-
同様の方法で、複数のスクリプトを結合および連結できます。
SOME_PIPELINE |
sed -e'#some expression script' \
-f./script_file -f- \
-e'#more inline expressions' \
./actual_edit_file >./outfile
POSIX sed
は、コマンドラインに表示される順序ですべてのスクリプトを1つに連結します。これらのいずれも、最終的に終了する必要はありません\n
。
grep
同じように機能します:
sed -e'#generate a pattern list' <in |
grep -f- ./grepped_file
固定文字列をパターンとして使用する場合、正規表現のメタキャラクターをエスケープすることをお勧めします。これはかなり簡単にできます:
sed 's/[]$&^*\./[]/\\&/g
s| *\([^ ]*\) *\([^ ]*\).*|s/\1/\2/g|
' <patterns.txt |
sed -f- ./editfile >outfile
いずれかを交換するfoo
、bar
またはbaz
でfoobar
sed -Ei 's/foo|bar|baz/foobar/g' file
または
perl -i -pe 's/foo|bar|baz/foobar/g' file
zsh
。どうしてもzsh
情報を追加しますが、bashのものを削除する理由はありません。また、テキスト処理にシェルを使用することは理想的ではないことを知っていますが、必要な場合があります。sed
実際に解析するためにシェルループを使用する代わりに、スクリプトを作成する元のスクリプトのより良いバージョンで編集しました。これは、たとえば数百組のパターンがある場合に役立ちます。
(.)
グロビング修飾子に相当するものがないため、ここでは使用できません。(いくつか不足しています-同様に)。forループは正しくなく(-rがない)、ファイルに複数のパスを作成することを意味し、sedスクリプトよりも利点はありません。
--
後 sed -i
および代替コマンドの前に示していますか?
-
。これを使用すると、コマンドがなどの名前のファイルで機能することが保証されます-foo
。それなしでは、-f
オプションとして解析されます。
.git
ディレクトリ内の内部gitファイルを実際に変更し、実際にチェックアウトを台無しにします。名前で特定のディレクトリ内/上で操作する方が適切です。
優れたRの電子PL acementのLinuxツールですRPLそれはで利用可能ですので、それは元々 、Debianプロジェクトのために書かれた、apt-get install rpl
任意のDebianの派生ディストリビューションであり、他人のためかもしれないが、そうでなければ、ダウンロードすることができ tar.gz
、ファイルをSourgeForge。
最も簡単な使用例:
$ rpl old_string new_string test.txt
文字列にスペースが含まれる場合は、引用符で囲む必要があります。デフォルトでrpl
は大文字のみを扱いますが、完全な単語は扱いませんが、これらのデフォルトはオプション-i
(大文字と小文字を区別しない)および-w
(単語全体)で変更できます。複数のファイルを指定することもできます:
$ rpl -i -w "old string" "new string" test.txt test2.txt
あるいは指定した拡張子を(-x
検索、あるいは検索する)再帰的に(-R
ディレクトリ):
$ rpl -x .html -x .txt -R old_string new_string test*
(プロンプト)オプションを使用して、対話モードで検索/置換することもでき-p
ます。
出力には、置換されたファイル/文字列の数と検索の種類(大文字と小文字の区別/全体、部分的な単語)が表示されますが、-q
(quiet mode)オプション、またはさらに詳細な、-v
(詳細モード)オプションを使用した各ファイルとディレクトリの一致。
覚えておく価値がある他のオプションは、-e
(名誉Eの許可スケープ)regular expressions
ので、あなたも、タブ(検索することができます\t
)、新しい行(\n
など)を、。許可-f
を強制して(もちろん、ユーザーが書き込み許可を持っている場合のみ)-d
、変更時間を保持するために使用できます`)。
最後に、どちらが正確に作成されるかわからない場合は、-s
(シミュレーションモード)を使用します。
findとsedを使用することもできますが、perlのこの小さな行はうまく機能することがわかります。
perl -pi -w -e 's/search/replace/g;' *.php
- -eは、次のコード行を実行することを意味します。
- -iは、インプレース編集を意味します
- -w警告を書き込む
- -p入力ファイルをループし、スクリプトが適用された後に各行を出力します。
私の最良の結果は、perlとgrepを使用することから得られます(ファイルに検索式が含まれるようにするため)
perl -pi -w -e 's/search/replace/g;' $( grep -rl 'search' )
ExモードでVimを使用できます。
現在のディレクトリ内のすべてのファイルで文字列ALFをBRAに置き換えますか?
for CHA in *
do
ex -sc '%s/ALF/BRA/g' -cx "$CHA"
done
サブディレクトリにも同じことを繰り返しますか?
find -type f -exec ex -sc '%s/ALF/BRA/g' -cx {} ';'
ファイル名が別の文字列と一致する場合にのみ置き換えますか?
for CHA in *.txt
do
ex -sc '%s/ALF/BRA/g' -cx "$CHA"
done
特定のコンテキストで文字列が見つかった場合にのみ置き換えますか?
ex -sc 'g/DEL/s/ALF/BRA/g' -cx file
文字列が特定の行番号にある場合は置き換えますか?
ex -sc '2s/ALF/BRA/g' -cx file
複数の文字列を同じ置換で置き換えます
ex -sc '%s/\vALF|ECH/BRA/g' -cx file
複数の文字列を異なる置換に置き換えます
ex -sc '%s/ALF/BRA/g|%s/FOX/GOL/g' -cx file
私はこれを使用しました:
grep -r "old_string" -l | tr '\n' ' ' | xargs sed -i 's/old_string/new_string/g'
を含むすべてのファイルをリストしますold_string
。
結果の改行をスペースに置き換えます(ファイルのリストをsed
。
sed
それらのファイルを実行して、古い文字列を新しい文字列に置き換えます。
更新:上記の結果は、空白を含むファイル名では失敗します。代わりに、次を使用します。
grep --null -lr "old_string" | xargs --null sed -i 's/old_string/new_string/g'
grep --null -lr "old_string" | xargs --null sed -i 's/old_string/new_string/g'
と、任意のファイル名が処理されます。
ユーザーの観点から見ると、この仕事を完璧にこなす素敵でシンプルなUnixツールはqsubst
です。例えば、
% qsubst foo bar *.c *.h
すべてのCファイルで置き換えfoo
られbar
ます。良い機能はqsubst
、query-replaceを実行することです。つまり、発生するたびに表示さfoo
れ、置換するかどうかを尋ねます。[ -go
オプションで無条件に(質問なしで)置き換えることができます。他のオプションがあります。たとえば、単語全体の-w
場合にのみ置換foo
したい場合。]
入手方法:qsubst
(McGillの)der Mouseによって発明され、1987年8月にcomp.unix.sources 11(7)に投稿されました。更新されたバージョンが存在します。たとえば、NetBSDバージョンqsubst.c,v 1.8 2004/11/01
は、私のMacで完全にコンパイルおよび実行されます。
ドライランオプションを提供し、グロブで再帰的に動作するものが必要でした。それを試してみた後awk
、sed
私はgaveめ、代わりにpythonでそれをしました。
このスクリプトは、globパターン(たとえば、--glob="*.html"
)に一致するすべてのファイルを再帰的に検索して正規表現を検索し、置換正規表現で置き換えます。
find_replace.py [--dir=my_folder] \
--search-regex=<search_regex> \
--replace-regex=<replace_regex> \
--glob=[glob_pattern] \
--dry-run
などのすべての長いオプションに--search-regex
は、対応する短いオプションがあります-s
。with -h
を実行して、すべてのオプションを表示します。
たとえば、これにより、すべての日付がから2017-12-31
に反転します31-12-2017
。
python replace.py --glob=myfile.txt \
--search-regex="(\d{4})-(\d{2})-(\d{2})" \
--replace-regex="\3-\2-\1" \
--dry-run --verbose
import os
import fnmatch
import sys
import shutil
import re
import argparse
def find_replace(cfg):
search_pattern = re.compile(cfg.search_regex)
if cfg.dry_run:
print('THIS IS A DRY RUN -- NO FILES WILL BE CHANGED!')
for path, dirs, files in os.walk(os.path.abspath(cfg.dir)):
for filename in fnmatch.filter(files, cfg.glob):
if cfg.print_parent_folder:
pardir = os.path.normpath(os.path.join(path, '..'))
pardir = os.path.split(pardir)[-1]
print('[%s]' % pardir)
filepath = os.path.join(path, filename)
# backup original file
if cfg.create_backup:
backup_path = filepath + '.bak'
while os.path.exists(backup_path):
backup_path += '.bak'
print('DBG: creating backup', backup_path)
shutil.copyfile(filepath, backup_path)
with open(filepath) as f:
old_text = f.read()
all_matches = search_pattern.findall(old_text)
if all_matches:
print('Found {} matches in file {}'.format(len(all_matches), filename))
new_text = search_pattern.sub(cfg.replace_regex, old_text)
if not cfg.dry_run:
with open(filepath, "w") as f:
print('DBG: replacing in file', filepath)
f.write(new_text)
else:
for idx, matches in enumerate(all_matches):
print("Match #{}: {}".format(idx, matches))
print("NEW TEXT:\n{}".format(new_text))
elif cfg.verbose:
print('File {} does not contain search regex "{}"'.format(filename, cfg.search_regex))
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='''DESCRIPTION:
Find and replace recursively from the given folder using regular expressions''',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog='''USAGE:
{0} -d [my_folder] -s <search_regex> -r <replace_regex> -g [glob_pattern]
'''.format(os.path.basename(sys.argv[0])))
parser.add_argument('--dir', '-d',
help='folder to search in; by default current folder',
default='.')
parser.add_argument('--search-regex', '-s',
help='search regex',
required=True)
parser.add_argument('--replace-regex', '-r',
help='replacement regex',
required=True)
parser.add_argument('--glob', '-g',
help='glob pattern, i.e. *.html',
default="*.*")
parser.add_argument('--dry-run', '-dr',
action='store_true',
help="don't replace anything just show what is going to be done",
default=False)
parser.add_argument('--create-backup', '-b',
action='store_true',
help='Create backup files',
default=False)
parser.add_argument('--verbose', '-v',
action='store_true',
help="Show files which don't match the search regex",
default=False)
parser.add_argument('--print-parent-folder', '-p',
action='store_true',
help="Show the parent info for debug",
default=False)
config = parser.parse_args(sys.argv[1:])
find_replace(config)
Here
は、検索用語と置換を異なる色で強調表示するスクリプトの更新バージョンです。
globstar
オプションと**
globまたはのいずれかを使用しますfind
。ドライランの場合は、を使用しますsed
。この-i
オプションを使用しない限り、変更は行われません。バックアップ用sed -i.bak
(またはperl -i .bak
); 一致しないファイルには、を使用しますgrep PATTERN file || echo file
。そして、なぜ世界では、Pythonにシェルにそれをさせるのではなく、グロブを拡張させるのですか?なぜscript.py --glob=foo*
ただの代わりにscript.py foo*
?
sed
、awk
よくしない、(4)読みやすさ、(5)このソリューションは非POSIXシステムでも機能する(私がそれを必要としているのではなく、誰かがそうするかもしれない)。
ripgrep(コマンド名rg
)はgrep
ツールですが、検索と置換もサポートしています。
$ cat ip.txt
dark blue and light blue
light orange
blue sky
$ # by default, line number is displayed if output destination is stdout
$ # by default, only lines that matched the given pattern is displayed
$ # 'blue' is search pattern and -r 'red' is replacement string
$ rg 'blue' -r 'red' ip.txt
1:dark red and light red
3:red sky
$ # --passthru option is useful to print all lines, whether or not it matched
$ # -N will disable line number prefix
$ # this command is similar to: sed 's/blue/red/g' ip.txt
$ rg --passthru -N 'blue' -r 'red' ip.txt
dark red and light red
light orange
red sky
rg
インプレースオプションをサポートしていないため、自分で行う必要があります。
$ # -N isn't needed here as output destination is a file
$ rg --passthru 'blue' -r 'red' ip.txt > tmp.txt && mv tmp.txt ip.txt
$ cat ip.txt
dark red and light red
light orange
red sky
正規表現の構文と機能
については、Rust regexのドキュメントを参照してください。-P
スイッチが有効になりますPCRE2の味を。rg
デフォルトでUnicodeをサポートします。
$ # non-greedy quantifier is supported
$ echo 'food land bark sand band cue combat' | rg 'foo.*?ba' -r 'X'
Xrk sand band cue combat
$ # unicode support
$ echo 'fox:αλεπού,eagle:αετός' | rg '\p{L}+' -r '($0)'
(fox):(αλεπού),(eagle):(αετός)
$ # set operator example, remove all punctuation characters except . ! and ?
$ para='"Hi", there! How *are* you? All fine here.'
$ echo "$para" | rg '[[:punct:]--[.!?]]+' -r ''
Hi there! How are you? All fine here.
$ # use -P if you need even more advanced features
$ echo 'car bat cod map' | rg -P '(bat|map)(*SKIP)(*F)|\w+' -r '[$0]'
[car] bat [cod] map
のようにgrep
、この-F
オプションは固定文字列の一致を許可します。これsed
は実装する必要があると思う便利なオプションです。
$ printf '2.3/[4]*6\nfoo\n5.3-[4]*9\n' | rg --passthru -F '[4]*' -r '2'
2.3/26
foo
5.3-29
別の便利なオプションは-U
、複数行の一致を有効にすることです
$ # (?s) flag will allow . to match newline characters as well
$ printf '42\nHi there\nHave a Nice Day' | rg --passthru -U '(?s)the.*ice' -r ''
42
Hi Day
rg
DOSスタイルのファイルも処理できます
$ # same as: sed -E 's/\w+(\r?)$/123\1/'
$ printf 'hi there\r\ngood day\r\n' | rg --passthru --crlf '\w+$' -r '123'
hi 123
good 123
のもう1つの利点はrg
、sed
$ # for small files, initial processing time of rg is a large component
$ time echo 'aba' | sed 's/a/b/g' > f1
real 0m0.002s
$ time echo 'aba' | rg --passthru 'a' -r 'b' > f2
real 0m0.007s
$ # for larger files, rg is likely to be faster
$ # 6.2M sample ASCII file
$ wget https://norvig.com/big.txt
$ time LC_ALL=C sed 's/\bcat\b/dog/g' big.txt > f1
real 0m0.060s
$ time rg --passthru '\bcat\b' -r 'dog' big.txt > f2
real 0m0.048s
$ diff -s f1 f2
Files f1 and f2 are identical
$ time LC_ALL=C sed -E 's/\b(\w+)(\s+\1)+\b/\1/g' big.txt > f1
real 0m0.725s
$ time rg --no-pcre2-unicode --passthru -wP '(\w+)(\s+\1)+' -r '$1' big.txt > f2
real 0m0.093s
$ diff -s f1 f2
Files f1 and f2 are identical