5

My question is similar to How do I prevent sed -i from destroying symlinks?, but concerning hardlinks.

Using sed -i to work on a file destroys all the hardlinks the file has, since sed works by writing to a temporary file and then moving this. The --follow-symlinks parameter doesn't help in case of a hard link.

Is there an alternative to using the rather ugly:

sed 's/cat/dog/' pet_link > pet_link
jubilatious1
  • 3,195
  • 8
  • 17
user186430
  • 51
  • 2

4 Answers4

12

For sed 's/cat/dog/' or any other substitution that doesn't change the size of the file, with any Bourne-like shell, you can do:

sed 's/cat/dog/' < file 1<> file

The little-known but over 35 year old¹ standard <> operator is to open a file in read+write mode without truncation. Basically, here that makes sed write its output over its input. It's important to make sure that the output doesn't overwrite sections of the file that sed has not read yet.

For substitutions that cause the file size to decrease, with ksh93:

sed 's/hippopotamus/ant/' < file 1<>; file

<>;, a ksh93 extension is the same as <> except that if the command being redirected succeeds, the file gets truncated where the command finished.

Or with perl:

perl -pe 's/hippopotamus/ant/;
          END{truncate STDOUT, tell STDOUT}' < file 1<> file

For anything else, just use the standard form:

cp -i file file.back &&
  sed 's/dog/horse/g' < file.back > file # && rm -f file.back

¹ Though the initial implementation in the Bourne shell and early versions of the Korn shell was actually broken, fixed in the late 80s. And the Almquist shell initially didn't support it.

2

Please note that sed is a Stream EDitor, not a file editor, therefore people tend to abuse it for trying to edit files. Basically -i option is non-standard FreeBSD extensions (may not be available on other operating systems), secondly it doesn't edit files - it makes a copy and replaces the original file with the copy. BashFAQ

The alternative is to use ed or ex command (part of Vim) which has similar syntax, e.g.

ex +%s/cat/dog/e -scwq pet_link

Or as per @Wildcard recommendation:

printf '%s\n' '%s/cat/dog/' x | ex pet_link

For multiple files, you can use:

ex "+bufdo! %s/foo/bar/ge" -scxa **/*.lnk

If your shell supports a new globbing option (enable by: shopt -s globstar), using ** in this case will work recursively.


For more POSIX syntax, you can try (as per @Wildcard suggestion):

for f in *.txt; do printf '%s\n' '%s/cat/dog/g' x | ex "$f"; done

or:

find . -type f -exec sh -c 'for f; do printf "%s\n" "%s/cat/dog/g" x | ex "$f"; done' sh {} +

Related:

kenorb
  • 20,988
  • 1
    The first command given (1) only runs the substitution on the first line of the file, and (2) fails to exit if the pattern isn't found on the first line, instead dropping silently into interactive mode (but with no prompts). Instead I'd recommend printf '%s\n' '%s/cat/dog/' x | ex pet_link, which runs the substitution on all lines, but exits without changing the file if the pattern is not present. – Wildcard Jan 31 '17 at 22:18
  • 1
    And for multiple files, to do it POSIXly, either for f in *.txt; do printf '%s\n' '%s/cat/dog/g' x | ex "$f"; done or to do it recursively within subdirectories, find . -type f -exec sh -c 'for f; do printf "%s\n" "%s/cat/dog/g" x | ex "$f"; done' sh {} +. Feel free to put these into your answer if you like. And +1 just for using ex. – Wildcard Jan 31 '17 at 22:26
  • -i was added first to GNU sed (2001-09-25) based on the same -i option in perl and shortly after (2002-05-07) and independently but with a different syntax on FreeBSD. And much much later NetBSD (2014) and OpenBSD (2015) added one with the GNU style. – Stéphane Chazelas Nov 30 '17 at 14:17
  • 1
    While vim has another implementation of ex, ex was initially written in the mid 70s and included in 1BSD. ed predates even Unix. – Stéphane Chazelas Nov 30 '17 at 14:21
0

A little bit of ed:

$ ed pet_link <<END_OF_ED
g/cat/s//dog/
w
END_OF_ED

This doesn't break the hard link.

Kusalananda
  • 333,661
0

I find that this works well (preserving both symbolic and hard links):

sed 's/cat/dog/' pet_link > pet_link.tmp
cat pet_link.tmp > pet_link
rm pet_link.tmp
dashohoxha
  • 236
  • 2
  • 6