Differences between revisions 4 and 32 (spanning 28 versions)
Revision 4 as of 2007-09-02 23:28:23
Size: 2905
Editor: c-24-19-17-243
Comment: -i is not unique to GNU sed.
Revision 32 as of 2011-02-18 14:05:02
Size: 4681
Editor: GreyCat
Comment: much clean-up
Deletions are marked like this. Additions are marked like this.
Line 1: Line 1:
[[Anchor(faq21)]] <<Anchor(faq21)>>
Line 3: Line 3:
{{{sed}}} is a good command to replace strings, e.g. `ed` is the standard UNIX command-based editor. Here are some commonly-used syntaxes for replacing the string `olddomain.com` by the string `newdomain.com` in a file named `file`. All four commands do the same thing, with varying degrees of portability and efficiency:
Line 6: Line 6:
    sed 's/olddomain\.com/newdomain.com/g' input > output     # Bash
    ed -s file <<< $',s/olddomain\.com/newdomain.com/g\nw\nQ'

    # Bourne (with printf)
    printf '%s\n' ',s/olddomain\.com/newdomain.com/g' w Q | ed -s file

    printf ',s/olddomain\.com/newdomain.com/g\nw\nQ' | ed -s file

    # Bourne (without printf)
    ed -s file <<!
    ,s/olddomain\.com/newdomain.com/g
    w
    Q
    !
Line 12: Line 25:
    for i in *; do
        sed 's/old/new/g' "$i" > atempfile && mv atempfile "$i"
    for file in ./*; do
        [[ -f $file ]] && ed -s "$file" <<< $',s/old/new/g\nw\nQ'
Line 17: Line 30:
GNU sed 4.x (and sed on FreeBSD and Mac OS X) has a special {{{-i}}} flag which makes the loop and temp file unnecessary: To do this recursively, the easy way would be to enable globstar in bash 4 (`shopt -s globstar`, a good idea to put this in your `~/.bashrc`) and use:
Line 20: Line 33:
      sed -i 's/old/new/g' *     for file in ./**/*; do
        [[ -f $file ]] && ed -s "$file" <<< $',s/old/new/g\nw\nQ'
    done
}}}

If you don't have bash 4, you can use [[UsingFind|find]]. Unfortunately, it's a bit tedious to feed `ed` stdin for each file hit:

{{{
    find . -type f -exec bash -c 'printf "%s\n" ",s/old/new/g" w Q | ed -s "$1"' _ {} \;
}}}

`sed` is a '''Stream EDitor''', not a '''file''' editor. Nevertheless, people everywhere tend to abuse it for trying to edit files. It doesn't edit files. GNU `sed` (and some BSD `sed`s) have a `-i` option that makes a copy and replaces the original file with the copy. An expensive operation, but if you enjoy unportable code, I/O overhead and bad side effects (such as destroying symlinks), this would be an option:

{{{
    sed -i 's/old/new/g' ./* # GNU
    sed -i '' 's/old/new/g' ./* # BSD
    for file in ./* # Other
    do
        [ -f "$file" ] &&
            sed 's/old/new/g' "$file" > "$file~" &&
            mv "$file~" "$file"
    done
Line 26: Line 60:
    perl -pi -e 's/old/new/g' *     perl -pi -e 's/old/new/g' ./*
Line 29: Line 63:
Recursively (requires GNU or BSD {{{find}}}): Recursively using `find`:
Line 32: Line 66:
    find . -type f -print0 | xargs -0 perl -pi -e 's/old/new/g'     find . -type f -exec perl -pi -e 's/old/new/g' {} \;
}}}

If you want to delete lines instead of making substitutions:

{{{
    # Deletes any line containing the perl regex foo
    perl -ni -e 'print unless /foo/' ./*
Line 38: Line 79:
    find . -type f -print0 | xargs -0 perl -i.bak -pne \
 's/\bunsigned\b(?!\s+(int|short|long|char))/unsigned long/g'
    find . -type f -exec perl -i.bak -pne \
 's/\bunsigned\b(?!\s+(int|short|long|char))/unsigned long/g' {} \;
Line 42: Line 83:

Finally, for those of you with ''none'' of the useful things above, here's a script that may be useful:
Finally, for those of you who shun `ed` and have ''none'' of the useful things above, here's a script that may be useful:
Line 54: Line 94:
    [ $# -lt 1 ] && set -- *     [ $# -lt 1 ] && set -- ./*
Line 61: Line 101:
        sed "s|$old|$new|g" "$file" > "$file"-new || exit         sed "s|$old|$new|g" -- "$file" > "$file"-new || exit
Line 63: Line 103:
        if cmp "$file" "$file"-new >/dev/null 2>&1
        then rm "$file"-new # file has not changed
        else mv "$file"-new "$file" # file has changed: overwrite original file
        if cmp -- "$file" "$file"-new >/dev/null 2>&1
        then rm -- "$file"-new # file has not changed
        else mv -- "$file"-new "$file" # file has changed: overwrite original file
Line 70: Line 110:
If the code above is put into a script file (e.g. {{{chtext}}}), the resulting script can be used to change a text e.g. in all HTML files of the current and all subdirectories: If the code above is put into a script file (e.g. `chtext`), the resulting script can be used to change a text e.g. in all HTML files of the current and all subdirectories:
Line 77: Line 117:
 * use another {{{sed}}} separator character than '|', e.g. ^A (ASCII 1)
 * the {{{find}}} command above could use either {{{xargs}}} or the built-in {{{xargs}}} of POSIX find
 * use another `sed` separator character than '|', e.g. ^A (ASCII 0x01)
 * the [[UsingFind|find]] command above could use `+` instead of `\;` if available
 * the `chte
xt` script could be replaced with an `ed` command
Line 80: Line 121:
Note: {{{set -- *}}} in the code above is safe with respect to files whose names contain spaces. The expansion of * by {{{set}}} is the same as the expansion done by {{{for}}}, and filenames will be preserved properly as individual parameters, and not broken into words on whitespace. Note: `set -- ./*` in the code above is safe with respect to files whose names contain spaces. The expansion of `./*` by `set` is the same as the expansion done by `for`, and filenames will be preserved properly as individual parameters, and not broken into words on whitespace.
Line 82: Line 123:
A more sophisticated example of {{{chtext}}} is here: http://www.shelldorado.com/scripts/cmds/chtext A more sophisticated example of `chtext` is here: http://www.shelldorado.com/scripts/cmds/chtext

----
CategoryShell

How can I replace a string with another string in all files?

ed is the standard UNIX command-based editor. Here are some commonly-used syntaxes for replacing the string olddomain.com by the string newdomain.com in a file named file. All four commands do the same thing, with varying degrees of portability and efficiency:

    # Bash
    ed -s file <<< $',s/olddomain\.com/newdomain.com/g\nw\nQ'

    # Bourne (with printf)
    printf '%s\n' ',s/olddomain\.com/newdomain.com/g' w Q | ed -s file

    printf ',s/olddomain\.com/newdomain.com/g\nw\nQ' | ed -s file

    # Bourne (without printf)
    ed -s file <<!
    ,s/olddomain\.com/newdomain.com/g
    w
    Q
    !

To replace a string in all files of the current directory:

    for file in ./*; do
        [[ -f $file ]] && ed -s "$file" <<< $',s/old/new/g\nw\nQ'
    done

To do this recursively, the easy way would be to enable globstar in bash 4 (shopt -s globstar, a good idea to put this in your ~/.bashrc) and use:

    for file in ./**/*; do
        [[ -f $file ]] && ed -s "$file" <<< $',s/old/new/g\nw\nQ'
    done

If you don't have bash 4, you can use find. Unfortunately, it's a bit tedious to feed ed stdin for each file hit:

    find . -type f -exec bash -c 'printf "%s\n" ",s/old/new/g" w Q | ed -s "$1"' _ {} \;

sed is a Stream EDitor, not a file editor. Nevertheless, people everywhere tend to abuse it for trying to edit files. It doesn't edit files. GNU sed (and some BSD seds) have a -i option that makes a copy and replaces the original file with the copy. An expensive operation, but if you enjoy unportable code, I/O overhead and bad side effects (such as destroying symlinks), this would be an option:

    sed -i    's/old/new/g' ./*  # GNU
    sed -i '' 's/old/new/g' ./*  # BSD
    for file in ./*              # Other
    do
        [ -f "$file" ] &&
            sed 's/old/new/g' "$file" > "$file~" &&
            mv "$file~" "$file"
    done

Those of you who have perl 5 can accomplish the same thing using this code:

    perl -pi -e 's/old/new/g' ./*

Recursively using find:

    find . -type f -exec perl -pi -e 's/old/new/g' {} \;

If you want to delete lines instead of making substitutions:

    # Deletes any line containing the perl regex foo
    perl -ni -e 'print unless /foo/' ./*

To replace for example all "unsigned" with "unsigned long", if it is not "unsigned int" or "unsigned long" ...:

    find . -type f -exec perl -i.bak -pne \
        's/\bunsigned\b(?!\s+(int|short|long|char))/unsigned long/g' {} \;

Finally, for those of you who shun ed and have none of the useful things above, here's a script that may be useful:

    #!/bin/sh
    # chtext - change text in several files

    # neither string may contain '|' unquoted
    old='olddomain\.com'
    new='newdomain\.com'

    # if no files were specified on the command line, use all files:
    [ $# -lt 1 ] && set -- ./*

    for file
    do
        [ -f "$file" ] || continue # do not process e.g. directories
        [ -r "$file" ] || continue # cannot read file - ignore it
        # Replace string, write output to temporary file. Terminate script in case of errors
        sed "s|$old|$new|g" -- "$file" > "$file"-new || exit
        # If the file has changed, overwrite original file. Otherwise remove copy
        if cmp -- "$file" "$file"-new >/dev/null 2>&1
        then rm -- "$file"-new              # file has not changed
        else mv -- "$file"-new "$file"      # file has changed: overwrite original file
        fi
    done

If the code above is put into a script file (e.g. chtext), the resulting script can be used to change a text e.g. in all HTML files of the current and all subdirectories:

    find . -type f -name '*.html' -exec chtext {} \;

Many optimizations are possible:

  • use another sed separator character than '|', e.g. ^A (ASCII 0x01)

  • the find command above could use + instead of \; if available

  • the chtext script could be replaced with an ed command

Note: set -- ./* in the code above is safe with respect to files whose names contain spaces. The expansion of ./* by set is the same as the expansion done by for, and filenames will be preserved properly as individual parameters, and not broken into words on whitespace.

A more sophisticated example of chtext is here: http://www.shelldorado.com/scripts/cmds/chtext


CategoryShell

BashFAQ/021 (last edited 2022-11-03 23:42:27 by GreyCat)