6

How to separate strings and numbers from one line using a bash command.

Example: I have a string containing

string123anotherstr456thenanotherstr789

The output should be:

string
123
anotherstr
456
thenanotherstr
789
4
  • 1
    is the string in a variable already? or is in a file? is it output from a process?
    – Jeff Schaller
    Jan 8, 2018 at 14:31
  • Thank you for asking @Jeff Schaller. Let consider it is in a file
    – HUY
    Jan 8, 2018 at 15:25
  • 1
    What do you want to happen if there are characters other than letters and digits in the string? Does “number” mean “positive integer” (i.e., a sequence of numeric digits only), or do 3.14 and -1 qualify as numbers? Jan 9, 2018 at 4:13
  • @G-Man Thank you to point out the shortcoming of the question.
    – HUY
    Jan 9, 2018 at 13:11

9 Answers 9

21

GNU grep or compatible solution:

s="string123anotherstr456thenanotherstr789"
grep -Eo '[[:alpha:]]+|[0-9]+' <<<"$s"
  • [[:alpha:]]+|[0-9]+ - regex alternation group, matches either alphabetic character(s) or number(s); both will be considered as separate entries on output

The output:

string
123
anotherstr
456
thenanotherstr
789
5

awk

Input contains only letters and numerals

Add a newline character after every [[:alpha:]]+ (sequence of letters) and after every [[:digit:]]+ (sequence of numerals):

awk '{ gsub(/([[:alpha:]]+|[[:digit:]]+)/,"&\n",$0) ; printf $0 }' filename

(The & is awk shorthand for the matched sequence.)


Input contains other characters (eg, punctuation)

As before, but now also dealing with substrings of [^[:alnum:]]+ (non-letter, non-numeral) characters:

awk '{ gsub(/([[:alpha:]]+|[[:digit:]]+|[^[:alnum:]]+)/,"&\n",$0) ; printf $0 }' filename

Negative numbers and decimal fractions

Treat - (hyphen) and . (period) as numbers:

awk '{ gsub(/([[:alpha:]]+|[[:digit:].-]+|[^[:alnum:].-]+)/,"&\n",$0) ; printf $0 }' filename

Those characters must appear in both the [[:digit:].-]+ and [^[:alnum:].-]+ expressions. Also, to be interpreted as a literal hyphen, the - must be last character before the final right square bracket of each expression; otherwise, it indicates a range of characters.

Example:

[test]$ cat file.txt 
string123another!!str456.001thenanotherstr-789

[test]$ awk '{ gsub(/([[:alpha:]]+|[[:digit:].-]+|[^[:alnum:].-]+)/,"&\n",$0) ; printf $0 }' file.txt 
string
123
another
!!
str
456.001
thenanotherstr
-789

An exercise for the reader

If the input file requires it, you could modify the awk command to:

  • Ensure that - only counts as part of a number if it occurs at the start of a numeral sequence.
  • Allow numbers that are expressed in scientific notation.
2
  • The first argument of printf is the format, you should use $0 there. Use printf "%s", $0 Jan 9, 2018 at 11:20
  • Or set ORS="" and then use just print or print $0
    – ilkkachu
    Jan 9, 2018 at 14:07
5

POSIXly:

string=string123anotherstr456thenanotherstr789
sed '
  s/[^[:alnum:]]//g; # remove anything other than letters and numbers
  s/[[:alpha:]]\{1,\}/&\
/g; # insert a newline after each sequence of letters
  s/[0-9]\{1,\}/&\
/g; # same for digits
  s/\n$//; # remove a trailing newline if any' << EOF
$string
EOF
3
  • (1) Gotcha!  (2) Why do you believe that the OP wants non-alphanumeric characters to be ignored? Jan 9, 2018 at 4:18
  • @G-Man, no good reason other than that's what the accepted answer does. It still differs from the accepted answer in that it will print an empty line for each line of input that contains neither letters nor digits. Jan 9, 2018 at 11:18
  • 1
    An additional (IMHO, more significant) difference: the accepted answer treats non-alphanumeric characters as word separators, and then discards them, so 3.14 becomes two output lines (3 and 14) and G-Man@stack becomes three (G, Man and stack). Your answer ignores them, so those inputs yield one output each (314 and GManstack). Jan 9, 2018 at 19:51
3

GNU sed (or compatible) solution:

s="string123anotherstr456thenanotherstr789"
sed 's/[a-zA-Z]*\|[0-9]*/&\n/g; s/\n$//' <<<"$s"

The output:

string
123
anotherstr
456
thenanotherstr
789
3

python3

python3 -c '
from itertools import groupby
s = ("".join(g) for k, g in 
    groupby("string123anotherstr456thenanotherstr789", lambda x: x.isalpha()))
print(*s, sep="\n")
'

string
123
anotherstr
456
thenanotherstr
789
2

Used below one liner to achieve the same. As tested its worked fine

sed "s/[0-9]\{3\}/\n&/g" filename | sed "s/[0-9]\{3\}/&\n/g"| sed '/^$/d'

output

string
123
anotherstr
456
thenanotherstr
789
3
  • @wjandrea:  The OP says (in a comment) “consider it is in a file”. Jan 9, 2018 at 4:11
  • 1
    It seems to me that you might as well say that the answer is printf 'string\n123\nanotherstr\n456\nthenanotherstr\n789\n'. I admit that the question is unclear, in the sense that it is incomplete, but the string presented in the question is clearly labeled as an example. I see no reason to assume that every number that will ever be in the input will be a three-digit number. Jan 9, 2018 at 4:14
  • 1
    I think you could combine the first two seds and add both newlines at one go s/.../\n&\n/g. I also don't see why insist on the numbers being exactly three digits long, since it's trivial to make a more general solution.
    – ilkkachu
    Jan 9, 2018 at 14:10
1

I didn't see a Perl solution yet, so here:

$ cat s
string123anotherstr456thenanotherstr789
$ perl -lne 'print $& while /[[:alpha:]]+|[[:digit:]]+/g' < s
string
123
anotherstr
...

Of course, for wider definitions of "numbers", we might want to use [-+]?[0-9]+ (leading sign), [-+]?[0-9]+(.[0-9]+)? (plus optional fractional part), or even [-+]?[0-9]+(\.[0-9]+)?([eE][-+]?[0-9]+)? (plus optional exponent). The latter two ones require at least one digit before and after the decimal point, if it is present.

1

Pure Bash

This is relatively inefficient because it makes several (shorter) copies of the original string:

declare s=string123anotherstr456thenanotherstr789
while [[ "$s" =~ ^([a-z]+)([0-9]+) ]]; do
  echo ${BASH_REMATCH[1]}
  echo ${BASH_REMATCH[2]}
  s="${s:${#BASH_REMATCH[0]}}"
done

How many letters-digits pairs per line are you dealing with?

1
gawk '{ $1 = $1; print }' FPAT='[a-z]+|[0-9]+' OFS='\n' input.txt

Testing

gawk '{ $1 = $1; print }' FPAT='[a-z]+|[0-9]+' OFS='\n' <<< 'string123anotherstr456thenanotherstr789'

Output

string
123
anotherstr
456
thenanotherstr
789

You must log in to answer this question.

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