52

I've got a long line that I want to insert a space every 4 characters, on a single lone line of solid text to make it easier to read, what's the simplest way to do this? also I should be able to input the line from a pipe. e.g.

echo "foobarbazblargblurg" | <some command here>

gives

foob arba zbla rgbl urg

8 Answers 8

82

Use sed as follows:

$ echo "foobarbazblargblurg" | sed 's/.\{4\}/& /g'
foob arba zbla rgbl urg
6
  • 1
    cursing that was so close to the sed I tried first I could kick myself. Jan 17, 2011 at 14:29
  • 9
    Just curious, what's the '&' accomplish? Oh, it's a standin for 'the thing what just matched'. Silly me. Aug 27, 2012 at 5:25
  • 3
    it should be noted that this adds a space at the end as well if there is one more character in the string, which might not be desirable
    – Anubis
    Apr 19, 2018 at 17:07
  • 8
    @Anubis 's/.\{4\}/& /g;s/ $//' May 23, 2019 at 15:21
  • 1
    @Owl: Perl regexes are probably opposite of sed, you probably need bare {4} to act as a counted repeat modifier, instead of \{4\}. Same if you use sed -E (extended regexps). Nov 19, 2022 at 11:42
33

You can use the following simple example:

$ echo "foobarbazblargblurg" | fold -w4 | paste -sd' ' -
foob arba zbla rgbl
2
  • Very nice...I think this is even better than the sed answer. I didn't know about fold before.
    – Wildcard
    Jan 28, 2016 at 19:18
  • 4
    Unfortunately, with current versions of GNU fold, it doesn't work with multi-byte characters (as in echo €€€€€€€€ | fold -w4 | paste -sd' ' - in UTF-8). May 5, 2017 at 15:26
8

In bash only, no external commands:

str="foobarbazblargblurg"
[[ $str =~ ${str//?/(.)} ]]
printf "%s%s%s%s " "${BASH_REMATCH[@]:1}"

or as a one-line pipe version:

echo foobarbazblargblurg | 
  { IFS= read -r str; [[ $str =~ ${str//?/(.)} ]]; \
    printf "%s%s%s%s " "${BASH_REMATCH[@]:1}"; }

The way this works is to convert each character of the string to a "(.)" for regex match and capture with =~, then just output the captured expressions from BASH_REMATCH[] array, grouped as required. Leading/trailing/intermediate spaces are preserved, remove the quotes around "${BASH_REMATCH[@]:1}" to omit them.

Here it is wrapped up in a function, this one will process its arguments or read stdin if there are no arguments:

function fmt4() {
  while IFS= read -r str; do
    [[ $str =~ ${str//?/(.)} ]]
    printf "%s%s%s%s " "${BASH_REMATCH[@]:1}"
  done < <( (( $# )) && printf '%s\n' "$@" || printf '%s\n' $(< /dev/stdin) )
}

$ echo foobarbazblargblurg | fmt4
foob arba zbla rgbl urg 

You can easily parameterise the count to adjust the format string accordingly.

A trailing space is added, use two printfs instead of one if that's a problem:

printf "%s%s%s%s" "${BASH_REMATCH[@]:1:4}"
(( ${#BASH_REMATCH[@]} > 5 )) && printf " %s%s%s%s" "${BASH_REMATCH[@]:5}"

The first printf prints (up to) the first 4 characters, the second conditionally prints all the rest (if any) with a leading space to separate the groups. The test is for 5 elements not 4 to account for the zeroth element.

Notes:

  • shell printf's%c could be used instead of %s, %c (maybe) makes the intent clearer, but it's not multi-byte character safe. If your version of bash is capable, the above is all multi-byte character safe.
  • shell printf reuses its format string until it runs out of arguments, so it just gobbles up 4 arguments at a time, and handles the trailing arguments (so no edge cases needed, unlike some of the other answers here which are arguably wrong)
  • BASH_REMATCH[0] is the entire matched string, so only output starting from index 1
  • use printf -v myvar ... instead to store to a variable myvar (subject to usual read-loop/subshell behaviour)
  • add printf "\n" if required

You can make the above work in zsh if you use the array match[] instead of BASH_REMATCH[], and subtract 1 from all the indexes as zsh doesn't keep a 0 element with the entire match.

2
  • +1 Because this should have been the accepted answer but as always, fear of REGEX keeps people from learning what the shell can actually do.
    – Yokai
    Apr 18, 2021 at 17:28
  • I just learned that bash can do regex without external commands. Jul 18, 2022 at 11:38
5

With zsh only:

str=foobarbazblargblurg

set -o extendedglob
printf '%s\n' ${str//(#m)????/$MATCH }

Or

printf '%s%s%s%s ' ${(s::)str}

with ksh93 only:

printf '%s\n' "${str//????/\0 }"

With any POSIX shell only (also avoiding the trailing space if the input length is a multiple of 4):

out=
while true; do
  case $str in
    (?????*)
      new_str=${str#????}
      out=$out${str%"$new_str"}' '
      str=$new_str
      ;;
    (*)
      out=$out$str
      break
  esac
done
printf '%s\n' "$out"

Now, that's for characters. If you wanted to do it on grapheme clusters (for instance, to break Stéphane, written as $'Ste\u0301phane', as Stép hane and not Ste phan e), with zsh:

set -o rematchpcre
str=$'Ste\u301phane' out=
while [[ $str =~ '(\X{4})(.+)' ]] {
  out+="$match[1] " str=$match[2]
}
out+=$str
printf '%s\n' $out

With ksh93, you could break by display width as well, which would work for that Stéphane above, but could also help when some other sorts of zero-width or double-width characters are involved:

str=$'Ste\u301phane' out=
while
  start=${ printf %L.4s. "$str"; }
  start=${start%.}
  [ "$start" != "$str" ]
do
  out+="$start " str=${str#"$start"}
done
out+=$str
printf '%s\n' "$out"
4

Here is the example using grep and xargs:

$ echo "foobarbazblargblurg" | grep -o .... | xargs
foob arba zbla rgbl
2
  • 2
    xargs runs echo by default, so it won't work with words like -nen or that contain backslashes depending on the echo implementation. You'll see the odd newline character once in a while as well if xargs runs more than one echo. Better to pipe to paste -sd ' ' - instead. Note that -o is not a standard option. May 5, 2017 at 15:36
  • grep -o is an interesting hack to insert newlines (potentially useful as an alternative to fold -w 4 if grep handles UTF-8 multibyte characters better), but xargs is not a robust or efficient way to convert newlines to spaces. Nov 19, 2022 at 11:45
3

I'm going to answer by only inserting spaces as required so a space appears at least after every 4 characters on a line; not sure which way you want to handle this case. For example, given input of "aa bbccdd", you'd get output "aa bbcc dd" rather than "aa b bccd d".

I'm using Perl for lookahead, but I'm not very familiar with Perl in general, so there may be tweaks needed:

$ echo "foobarbazblargblurg" | perl -wp -e 's/[^ ]{4}(?=[^\n ])/$& /g'
foob arba zbla rgbl urg

$ echo 'aa bbccdd' | perl -wp -e 's/[^ ]{4}(?=[^\n ])/$& /g'
aa bbcc dd
# not 'aa b bccd d'!

$ echo 'some input' | perl -wp -e 's/[^ ]{4}(?=[^\n ])/$& /g'
some inpu t
# not 'some  inp ut'!

$ echo $'aabb\nc cddee' | perl -wp -e 's/[^ ]{4}(?=[^\n ])/$& /g' | 
> while read; do echo "${REPLY}x"; done
aabbx
c cdde ex
# no spaces added at the end of the first line (while loop to add to the end of
# the line and show this)
0

I have done this by using python

First i am reading file then i am dividing by 4 characters and adding space

#!/usr/bin/python
import re
b=re.compile(r'[a-z]{4}')

p=open('/root/l.txt','r')
i=p.readlines()
for j in i:
    m=re.findall(b,j)
print " " .join (m) + "  "

/root/l.txt==> Consists of the content which you given in example

output

foob arba zbla rgbl
0

You can also try the split command. While it is commonly used to split files, you can use it for this as well:

split -b 4 --filter='cat;echo -n " "' <<< 'foobarbazblargblurg'

I mean it works, but YMMV.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .