ファイル内の文字列を置き換えるにはどうすればよいですか?


752

特定の検索条件に基づいてファイル内の文字列を置き換えることは非常に一般的なタスクです。どうやって

  • 現在のディレクトリ内のすべてのファイルで文字列fooを置き換えますbarか?
  • サブディレクトリにも同じことを繰り返しますか?
  • ファイル名が別の文字列と一致する場合にのみ置き換えますか?
  • 特定のコンテキストで文字列が見つかった場合にのみ置き換えますか?
  • 文字列が特定の行番号にある場合は置き換えますか?
  • 複数の文字列を同じ置換で置き換えます
  • 複数の文字列を異なる置換に置き換えます

2
これは、この主題に関する正規のQ&Aを目的としています(このメタディスカッションを参照)。以下の回答を編集するか、独自の回答を追加してください。
テルドン

回答:


1010

1.現在のディレクトリ内のすべてのファイルで、ある文字列のすべての出現を別の文字列に置き換えます。

これらは、ディレクトリに通常のファイルのみが含まれおり、すべての非表示でないファイルを処理したいことがわかっている場合に 使用します。そうでない場合は、2のアプローチを使用します。

sedこの回答のソリューションはすべてGNUを前提としていますsed。FreeBSDのか、OS / Xを使用している場合は、交換してください-i-i ''。また-i、のバージョンでスイッチを使用すると、sedファイルシステムのセキュリティに特定の意味があり、何らかの方法で配布する予定のスクリプトではお勧めできません。

  • 非再帰的、このディレクトリ内のファイルのみ:

    sed -i -- 's/foo/bar/g' *
    perl -i -pe 's/foo/bar/g' ./* 
    

    perlファイル名の末尾が|空白またはスペースの場合は失敗します)。

  • このサブディレクトリおよびすべてのサブディレクトリ内の再帰的な通常ファイル(隠しファイルを含む

    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)場合に選択されます。

2.ファイル名が別の文字列と一致する場合/特定の拡張子を持つ/特定のタイプなどである場合のみ置換

  • このディレクトリ内の非再帰的なファイルのみ:

    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*)

3.特定のコンテキストで文字列が見つかった場合にのみ置換

  • 同じ行に後である場合にのみ置換fooします。barbaz

    sed -i 's/foo\(.*baz\)/bar\1/' file

    sed、を使用\( \)すると、括弧内にあるものがすべて保存され、でアクセスできます\1。このテーマには多くのバリエーションがあります。そのような正規表現の詳細については、こちらをご覧ください

  • 置き換えるfoobarする場合にのみfoo、入力ファイルの3D列(フィールド)に見出される(空白で区切られたフィールドを想定)。

    gawk -i inplace '{gsub(/foo/,"baz",$3); print}' file

    gawk4.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 

    注:両方awkperlソリューション(先頭と末尾の空白を削除し、一致してこれらの行の1つのスペース文字に空白のシーケンスを変換)をファイルで間隔に影響します。別のフィールドの$F[N-1]場合Nは、希望するフィールド番号をwhere に使用し、別のフィールドセパレーターには($"=":"出力フィールドセパレーターをに設定します:)を使用します。

    perl -i -F':' -ane '$F[2]=~s/foo/baz/g; $"=":";print "@F"' foo 
  • 置き換えfoobarのみ第四行に:

    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
    

4.複数の置換操作:異なる文字列で置換

  • 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 として指定することにより、そのストリームをスクリプトとしてソースできます。sedsedsed-

  • 同様の方法で、複数のスクリプトを結合および連結できます。

    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
    

5.複数の置換操作:複数のパターンを同じ文字列で置換します

  • いずれかを交換するfoobarまたはbazfoobar

    sed -Ei 's/foo|bar|baz/foobar/g' file
  • または

    perl -i -pe 's/foo|bar|baz/foobar/g' file

2
@StéphaneChazelas編集に感謝します。確かにいくつかの問題を修正しました。ただし、bashに関連する情報は削除しないでください。誰もが使用するわけではありませんzsh。どうしてもzsh情報を追加しますが、bashのものを削除する理由はありません。また、テキスト処理にシェルを使用することは理想的ではないことを知っていますが、必要な場合があります。sed実際に解析するためにシェルループを使用する代わりに、スクリプトを作成する元のスクリプトのより良いバージョンで編集しました。これは、たとえば数百組のパターンがある場合に役立ちます。
テルドン

2
@terdon、あなたのbashは間違っています。4.3より前のbashは、降順のときにシンボリックリンクに従います。また、bashには(.)グロビング修飾子に相当するものがないため、ここでは使用できません。(いくつか不足しています-同様に)。forループは正しくなく(-rがない)、ファイルに複数のパスを作成することを意味し、sedスクリプトよりも利点はありません。
ステファンシャゼル

7
何@terdon --sed -iおよび代替コマンドの前に示していますか?
オタク

5
@GeekそれはPOSIXのことです。オプションの終わりを意味し、で始まる引数を渡すことができます-。これを使用すると、コマンドがなどの名前のファイルで機能することが保証されます-foo。それなしでは、-fオプションとして解析されます。
テルドン

1
gitリポジトリで再帰的なコマンドのいくつかを実行する際には非常に注意してください。たとえば、この回答のセクション1で提供されるソリューションは、.gitディレクトリ内の内部gitファイルを実際に変更し、実際にチェックアウトを台無しにします。名前で特定のディレクトリ内/上で操作する方が適切です。
ピスト

75

優れた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ます。

出力には、置換されたファイル/文字列の数と検索の種類(大文字と小文字の区別/全体、部分的な単語)が表示されますが、-qquiet mode)オプション、またはさらに詳細な、-v詳細モード)オプションを使用した各ファイルとディレクトリの一致。

覚えておく価値がある他のオプションは、-e(名誉Eの許可スケープ)regular expressionsので、あなたも、タブ(検索することができます\t)、新しい行(\nなど)を、。許可-f強制して(もちろん、ユーザーが書き込み許可を持っている場合のみ)-d、変更時間を保持するために使用できます`)。

最後に、どちらが正確に作成されるかわからない場合は、-sシミュレーションモード)を使用します


2
sedよりもフィードバックとシンプルさがはるかに優れています。ファイル名に基づいて動作できるようにすれば、そのままで完璧になります。
クズカイ16

1
-s(シミュレーションモード)が好きです:-)
erm3nda

25

複数のファイルで検索と置換を行う方法を提案します

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' )

13

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

13

私はこれを使用しました:

grep -r "old_string" -l | tr '\n' ' ' | xargs sed -i 's/old_string/new_string/g'
  1. を含むすべてのファイルをリストしますold_string

  2. 結果の改行をスペースに置き換えます(ファイルのリストをsed

  3. 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'と、任意のファイル名が処理されます。
テルドン

みんなありがとう。更新を追加し、古いコードを残したため、この動作を知らない人にとって有用な可能性がある興味深い警告です。
o_o_o--

6

ユーザーの観点から見ると、この仕事を完璧にこなす素敵でシンプルなUnixツールはqsubstです。例えば、

% qsubst foo bar *.c *.h

すべてのCファイルで置き換えfooられbarます。良い機能はqsubstquery-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で完全にコンパイルおよび実行されます。


2

ドライランオプションを提供し、グロブで再帰的に動作するものが必要でした。それを試してみた後awksed私は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 は、検索用語と置換を異なる色で強調表示するスクリプトの更新バージョンです。


1
なぜこんなに複雑なものにするのか理解できません。再帰の場合、bash(またはシェルの同等)globstarオプションと**globまたはのいずれかを使用しますfind。ドライランの場合は、を使用しますsed。この-iオプションを使用しない限り、変更は行われません。バックアップ用sed -i.bak(またはperl -i .bak); 一致しないファイルには、を使用しますgrep PATTERN file || echo file。そして、なぜ世界では、Pythonにシェルにそれをさせるのではなく、グロブを拡張させるのですか?なぜscript.py --glob=foo*ただの代わりにscript.py foo*
テルドン

1
私の理由は非常に簡単です:(1)とりわけ、デバッグの容易さ。(2)支援コミュニティで単一の十分に文書化されたツールのみを使用する(3)それらを習得するために余分な時間を費やすことを知らずsedawkよくしない、(4)読みやすさ、(5)このソリューションは非POSIXシステムでも機能する(私がそれを必要としているのではなく、誰かがそうするかもしれない)。
ccpizza

1

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つの利点はrgsed

$ # 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
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.