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 printf
s 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.