2

In vim it seems possible to do like this :

:s/^\t\+/\=repeat('    ',len(submatch(0)))

I didn't find out how this translates into sed. I tried the following :

sed -ri "s/^\t\+/\=repeat('    ',len(submatch(0)))/g" test.txt

The command seems to have no effect.

Jeff Schaller
  • 67,283
  • 35
  • 116
  • 255
Sybuser
  • 121
  • You're using the -r option, which enables extended regular expressions: that means you need to use + not \+ for the quantifier. This code looks for one leading tab followed by a plus character. – glenn jackman Jun 05 '21 at 13:16

2 Answers2

7

This is what expand is for, e.g. expand -t 4 file.txt.

expand won't edit the original file, but you can redirect to a new file and mv it over the original, or use sponge if you have it installed, e.g.

    expand -t 4 file.txt > file.new && mv file.new file.txt
or 
    expand -t 4 file.txt | sponge file.txt

FYI, see unexpand to do the reverse, convert multiple spaces to tabs.

Both expand and unexpand support using fixed-width tabs (e.g. -t 4 or -t 8, etc) or a list of tab positions (e.g. -t 4,12,32). They also have options to only convert initial, leading spaces/tabs. See the man pages for details.


For more info about sponge, see man sponge and Is there a standard alternative to sponge to pipe a file into itself?. In short, it does the redirect and mv for you, using a temporary file. BTW, never try to redirect stdout to the same file that's currently being used as stdin. The shell will overwrite it before it even gets read.

sponge is from the moreutils package.


PS: See also GNU indent or one of many other similar source-code reformatting tools.

cas
  • 78,579
  • 1
    expand has at least the -i for the leading tabs, which is what I want, but it has not the -i from sed to replace in the file directly, it would be good if I could do something like expand -i -t 4 */**. So if I want to replace in , say, all java files, I would do something like : find . -type f -name '*.java' -print0 | xargs -0 -Ifile sh -c 'expand -i -t 4 file > file.new && mv file.new file' ? – Sybuser Jun 05 '21 at 08:22
  • I'd use find ... -exec and not bother with xargs. Something like find . -type f -name '*.java' -exec sh -c 'for f in "$@"; do t=$(mktemp); expand -i -t 4 "$f" > "$t" && mv "$t" "$f" || rm "$t"; done' sh {} +. Or, with sponge: find . -type f -name '*.java' -exec sh -c 'for f in "$@"; do expand -i -t 4 "$f" | sponge "$f" ; done' sh {} +` – cas Jun 05 '21 at 09:27
0
$ cat -T /tmp/test.txt
^I^I^I3^I<= this final tab should remain
^I^I2
^I1

Using sed:

sed ':loop;s/^\( *\)\t/\1    /;tloop' /tmp/test.txt | cat -T
            3^I<= this final tab should remain
        2
    1

The above sets up a loop (labeled, ironically enough, loop) that captures any leading spaces and a leading tab; replaces the spaces and converts the tab to 4 spaces; if successful (t) it branches back to the loop. See https://stackoverflow.com/a/35017207/493161