# How to subtract 1 from numbers matched by regular expression?

I'm trying to write a script that reduces each number in line by "1", but I'm getting all "0"s instead:

``````awk '{a=gensub(/([0-9]+)/,"\\1","g",\$0);
if(a~/[0-9]+/) {gsub(/[0-9]+/,a-1,\$0);}
print \$0}'
``````

For example, the string:

``````1,2,3,4-7
``````

should result in:

``````0,1,2,3-6
``````

``````0,0,0,0-0
``````

`awk` substitution capabilities are quite limited. `gawk` has `gensub()` that can at least include parts of the matched portion in the replacement, but no operation can be done on those.

It's possible with `awk`, but you need to take a different approach:

``````awk '{
text = \$0
\$0 = ""
while (match(text, /[0-9]+/)) {
\$0 = \$0 substr(text, 1, RSTART-1) \
(substr(text, RSTART, RLENGTH) - 1)
text = substr(text, RSTART+RLENGTH)
}
\$0 = \$0 text
print}'
``````

Or with GNU `awk` as a variation on @jofel's approach:

``````gawk -v 'RS=[0-9]+' '{printf "%s", \$0 (RT==""?"":RT-1)}'
``````

or

``````gawk -v 'RS=[^0-9]+' '{printf "%s",(\$0==""?"":\$0 - 1)RT}'
``````

However, here it's a lot easier with `perl`:

``````perl -pe 's/\d+/\$&-1/ge'
``````

`perl` can use capture groups (as `\$1`, `\$2`... and `\$&` for the whole matched portion) and with the `e` flag can run arbitrary `perl` expressions with those.

Your awk solution matches only the first number and then replaces all other number with the first number reduced by one.

Taking your program, you can use with GNU's awk (`gawk`):

``````awk 'BEGIN { RS="[^0-9]"; OFS=""; ORS=""; } {a=gensub(/([0-9]+)/,"\\1","g",\$0);if(a~/[0-9]+/) {gsub(/[0-9]+/,(a-1),\$0);} print \$0,RT}'
``````

But this can be simplified to

``````awk 'BEGIN { RS="[^0-9]"; OFS=""; ORS=""; } {if(length(\$0)) {print (\$0-1);}print RT}'
``````

``````awk '
BEGIN {
RS="[^0-9]";  # set the record separator to a regexp matching all
OFS="";  # no output field separator
ORS="";  # no output record separator (we use RT)
}
{
if(length(\$0)) { # if number found
print (\$0-1); # print it decreased by one
}
print RT # output current field separator (=non-digit).
}'
``````

Each non-digit is used as record separator and re-inserted with the print statement.

Here a solution in python:

``````python -c 'import re,sys; print re.compile("\d+").sub(lambda i: str(int(i.group())-1),sys.stdin.read()),'
``````
• It's actually worse than that. It's the entire line, interpreted as a number, less one. Commented Apr 22, 2016 at 9:16

## Using generic (non-GNU) "awk":

I would suggest a separation of the input lines into arrays of values and separators. Then, modify the values and recombine them with the separators:

``````awk '{
split("0," \$0 ",0", numbers, "[^0-9]+"); # make sure each line starts and ends with a number
split(\$0, sep, "[0-9]+");
res = ""; j = 1;
for (i = 2; i < length(numbers); i ++) { # ignore the dummy numbers added above
res = res sep[j++] (numbers[i] - 1);
}
print res;
}' file
``````
• Does this work if the line starts with a non-digit? It seems also sometimes to insert some wrong "-1". Commented Apr 22, 2016 at 9:40
• @jofel You're right, this did not handle lines beginning or ending with a delimiter correctly. I've fixed my answer accordingly. Commented Apr 22, 2016 at 10:23