In shell (not Bash) I have a string like this that I want to parse:


After parsing, I want to keep the 99 at the end, and throw away the rest of the string.

The substring that needs to be kept will not always be two characters long. It will be one or more digits, from the ! to either the end of string, or a ,.

Example input/output:

In: stringhere/morestring!99
Out: 99

In: string/more!99,string/more!98,string/more!97
Out: 99

cut sounds like the obvious thing to use, except for the ! in the middle of the string.

Is there an easy way to do this? Would awk be better?


4 Answers 4


If that strings are in FILE, and you always only want the first numbers after ! and before the first ,, if it exists this should work

awk -F'[!,]' '{print$2}' FILE

It takes either ! or , as delimiter and shows the second field, which will be the first numbers between ! and , or are just after ! if there is no , in that line, or before it.

If there is , before first ! upper awk example is not applicable.

You could also pipe one cut command to another, in first you specify ! as delimiter and take stuff after the first ! and in second you specify , as delimiter and take the stuff before first , if it exists

cut -d'!' -f2 FILE | cut -d',' -f1
  • 6
    The awk will also take the 77 from data like one,77,two!99 or three,77!99. The order of elements in the FS pattern is not significant. Mar 16, 2021 at 10:28

You can use cut, but it will need two passes. The first will get what comes after the first ! and the second will remove anything after a ,:

$ echo 'string/more!99,string/more!98,string/more!97' | 
    cut -d'!' -f2- | cut -d, -f1

Similarly for the case where you don't have a , (the second cut isn't needed here, I am including it only to show that you can use the exact same command):

$ echo 'string/more!99' | cut -d'!' -f2- | cut -d, -f1

Another option is sed:

$ echo 'string/more!99,string/more!98,string/more!97' | 
    sed -E 's/^[^!]+!([0-9]+).*/\1/'
$ echo 'string/more!99' | sed -E 's/^[^!]+!([0-9]+).*/\1/'

Or perl:

$ echo 'string/more!99,string/more!98,string/more!97' | 
    perl -pe 's/.+?!(\d+).*/\1/'
$ echo 'string/more!99' | perl -pe 's/.+?!(\d+).*/\1/'

Or GNU grep

$ echo 'string/more!99,string/more!98,string/more!97' | 
    grep -oP '^[^!]+!\K\d+'
$ echo 'string/more!99' | grep -oP '^[^!]+!\K\d+'

All you need is the shell's parameter expansion syntax: this was done in

$ input='stringhere/morestring!99'
$ echo "${input#*!}"

The # is followed by a pattern: the shortest prefix matching that pattern is removed.

$ input='string/more!99,string/more!98,string/more!97'
$ first=${input%%,*}
$ echo "${first#*!}"

The %% is followed by a pattern: the longest suffix matching the pattern is removed.

${var#pattern) -- shortest matching prefix removed.
${var##pattern) -- longest matching prefix removed.
${var%pattern) -- shortest matching suffix removed.
${var%%pattern) -- longest matching suffix removed.


You can also use sed. Using GNU sed with extended regular expressions turned on:

sed -E 's/^[^!]*!([0-9]+).*$/\1/'

or - more portable:

sed 's/^[^!]*!\([0-9]\{1,\}\).*$/\1/p'

This will match a pattern "anything up to the first !, followed by one or more digits, and possibly any kind of characters after that until end-of-line", and replace the entire line with only the "one or more digits" part.

~> echo 'string/more!99,string/more!98,string/more!97' | sed -E 's/^[^!]*!([0-9]+).*$/\1/'

If there can be lines that do not match, you can suppress them using

sed -nE 's/^[^!]*!([0-9]+).*$/\1/p'

instead. This will not output anything by default, and only print output if the matching pattern was found.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.