45

I am trying to perform a mathematical operation with sed, but it continues to treat my variables as strings. The input is of this kind:

$ echo 12 | sed 's/[0-9]*/&+3/'
$ 12+3

I'd like to have 15 as output. I need to do the operation and replace its mathematical result in only one passage, because I am running the program as a Python daemon, and I want to avoid passages like redirecting stdout on files, open those files, perform operations, extract the result, do the replacement. To me, sed seems the best to perform all in one line.

I've tried to cast both input and output in various ways like

$ echo 12 | sed 's/[0-9]*/int(&+3)/'
$ echo 12 | sed 's/[0-9]*/\int(&+3)/'
$ echo 12 | sed 's/[0-9]*/\int(&+3)/'

but the result was always a printing of the second field.

4
  • 14
    It's treating your "variables" as strings because that's all sed does - string manipulation. It has no concept of "integer."
    – Kevin
    Apr 20, 2012 at 16:10
  • 4
    I'm very curious why you want to use sed to do math Apr 21, 2012 at 13:43
  • I just thought it could easily cast variables, didn't realize was so complex! Apr 22, 2012 at 16:56
  • 1
    I'm a little late, but vim users have maybe done math in a search and replace command before and since sed is very similar to vim search and replace, one would assume you could do the same type of stuff with it @DavidOneill
    – Shanteva
    Dec 27, 2019 at 18:04

13 Answers 13

94

If you honestly want to use sed, then this is the way to go:

s/[0-9]/<&/g
s/0//g; s/1/|/g; s/2/||/g; s/3/|||/g; s/4/||||/g; s/5/|||||/g; s/6/||||||/g
s/7/|||||||/g; s/8/||||||||/g; s/9/|||||||||/g
: tens
s/|</<||||||||||/g
t tens
s/<//g
s/+//g
: minus
s/|-|/-/g
t minus
s/-$//
: back
s/||||||||||/</g
s/<\([0-9]*\)$/<0\1/
s/|||||||||/9/; s/||||||||/8/; s/|||||||/7/; s/||||||/6/; s/|||||/5/; s/||||/4/
s/|||/3/; s/||/2/; s/|/1/
s/</|/g
t back

Input:

1+2
100+250
100-250

Output:

3
350
-150

Your mission, should you choose to accept it, is to implement multiplication.

9
  • 5
    +1 for the challenge, love it! Maybe that'd be something for Code Golf ;-p Apr 20, 2012 at 14:15
  • 6
    And some people say that programming is not mathematics. This little gem refutes them all. Best use of Base 1 ever.
    – user732
    Apr 20, 2012 at 15:42
  • 1
    Nice one! - @Simon: I challenge you to implement tetration :P
    – A T
    Apr 20, 2012 at 17:45
  • 17
    +1 This is a beautiful example of what a misconception paired with creativity can breed. Apr 20, 2012 at 19:28
  • 1
    Multiplication be done with sed - and it scales to huge numbers quite well, too! Apr 13, 2016 at 20:07
26

sed isn't the best option here, it doesn't do arithmetics natively (see Increment a number for how you could possibly do it though). You could do that with awk:

$ echo 12 | awk '{print $0+3}'
15

The best piece of code to use will depend on the exact format of your input and what you want/need to do if it is not numeric, or contains more than one number, etc.

You could also do this only with bash:

$ echo $(( $(echo 12) + 3 ))

or using expr in a similar fashion.

20

I tried to accept your challenge @Richter, this is what I did using part of your code:

sed 's/[0-9]/<&/g
s/0//g; s/1/|/g; s/2/||/g; s/3/|||/g; s/4/||||/g; s/5/|||||/g; s/6/||||||/g
s/7/|||||||/g; s/8/||||||||/g; s/9/|||||||||/g
: tens
s/|</<||||||||||/g
t tens
s/<//g
s/.*\*$/0/
s/^\*.*/0/
s/*|/*/
: mult
s/\(|*\)\*|/\1<\1*/ 
t mult
s/*//g
s/<//g
: back
s/||||||||||/</g
s/<\([0-9]*\)$/<0\1/
s/|||||||||/9/; s/||||||||/8/; s/|||||||/7/; s/||||||/6/; s/|||||/5/; s/||||/4/
s/|||/3/; s/||/2/; s/|/1/
s/</|/g
t back'

Input:

04*3
4*3
40*3
42*32
150*20
1*3
3*1
0*3
3*0

Output: all the correct results

3
16

perl allows for a very similar construct to sed's ... one difference is that perl can do more complex things... sed is very good for simple text substitions

 echo 'a12' | perl -pe 's/([0-9]+)/($1+3)/e'  # the trailing /e means evaluate

output

a15
1
  • 3
    can also do this without the capturing parentheses: perl -pe 's/[0-9]+/$&+3/e' Apr 20, 2012 at 13:12
10

just feed the string into a calculator

 echo 12 | sed 's/[0-9]*/&+3/' | bc
1
  • 1
    this won't work if there's text amongst the numbers. Apr 21, 2012 at 12:25
9

I really don't get why the extreme complexity of the accepted answer, either of the below do what you want:

echo 12 | sed 's/[0-9]*/echo \$(( & + 3 ))/e'

or

echo 12 | sed 's/[0-9]*/expr & + 3/e'

I think it might require GNU sed, but I am not sure.

4
  • 1
    It is a gnu extension.
    – Kevin
    Mar 30, 2013 at 17:02
  • Ok you are right but the answer goes beyond, it implements the general addition not a particular one, you can feed any two numbers and you will get the result Apr 1, 2013 at 18:56
  • 1
    @LuigiTiburzi Its fairly straightforward to generalize this to "x+y" style input: echo 12+3 | sed -r 's/([0-9]*) *\+ *([0-9]*)/expr \1 + \2/e' Oct 15, 2014 at 18:28
  • Try that, but send it the string ; reboot.
    – Kusalananda
    May 13, 2022 at 17:30
5

If you definitely have to combine regular expressions and arithmetic operations, choose a language where the regular expression's replacement parameter can be a callback function.

Perl, Ruby, JavaScript and Python are such languages:

bash-4.2$ echo 12 | perl -pe 's/\d+/$&+3/e'
15

bash-4.2$ echo 12 | ruby -pe '$_.sub!(/\d+/){|s|s.to_i+3}'
15

bash-4.2$ echo 12 | js -e 'print(readline().replace(/\d+/,function(s){return parseInt(s)+3}))'
15

bash-4.2$ echo 12 | python -c 'import re;print re.sub("\d+",lambda s:str(int(s.group(0))+3),raw_input())'
15
1

Another simple bash solution, that actually works in a pipe:

 echo 12 | { read num; echo $(( num + 3)); }
1

If you mix in some bashism:

echo $(($(echo 12 | sed 's/[0-9]*/&+3/')))

To extract the number from a text:

echo $(($(echo "foo12bar" | sed -r 's/[^0-9]*([0-9]*).*/\1+3/')))

Without sed, just bash:

var="foo12bar"
echo $((${var//[^0-9]/}+3))

replaces every non-digit ${var//[^0-9]/} and does arithmetic in double round parens: $((x+3))

1
  • 2
    There's no bashism in there. $((...)) was introduce by POSIX (the bashism is $[...]). ${var//xxx/x} is a kshism also copied by zsh and bash. sed -r is a GNUism Sep 18, 2012 at 22:06
0

Here's a Perl solution:

echo 12 | perl -wlpe '$_ += 3'
# Output:  15

If you prefer to change the first set of digits encountered in a string, you can use:

echo I am 12 years old. | perl -wlpe 's/(\d+)/$1 + 3/e'
# Output:  I am 15 years old.

If you prefer to change all the sets of digits in a string, you can use the /g modifier, like this:

echo They are 11, 12, and 13 years old. | perl -wlpe 's/(\d+)/$1 + 3/eg'
# Output:  They are 14, 15, and 16 years old.
0

Although using sed expression is great it has its limitations. For example following fails:

$ echo "1000000000000000000000000000000+1" | sed -e 's/\([0-9]*\)+\([0-9]*\)/expr \1 + \2/e'
expr: 1000000000000000000000000000000: Numerical result out of range

To overcome this limitation one my simply turn to the built in power of pure sed and implement following arbitrary length decimal adder:

#!/bin/sed -f

s/+/\n/g
s/$/\n\n0/

:LOOP
s/^\(.*\)\(.\)\n\(.*\)\(.\)\n\(.*\)\n\(.\)$/0\1\n0\3\n\5\n\6\2\4/
h
s/^.*\n.*\n.*\n\(...\)$/\1/

# decimal full adder module
# INPUT:  3digits (Carry in, A, B,)
# OUTPUT: 2bits (Carry, Sum)
s/$/;000=00001=01002=02003=03004=04005=05006=06007=07008=08009=09010=01011=02012=03013=04014=05015=06016=07017=08018=09019=10020=02021=03022=04023=05024=06025=07026=08027=09028=10029=11030=03031=04032=05033=06034=07035=08036=09037=10038=11039=12040=04041=05042=06043=07044=08045=09046=10047=11048=12049=13050=05051=06052=07053=08054=09055=10056=11057=12058=13059=14060=06061=07062=08063=09064=10065=11066=12067=13068=14069=15070=07071=08072=09073=10074=11075=12076=13077=14078=15079=16080=08081=09082=10083=11084=12085=13086=14087=15088=16089=17090=09091=10092=11093=12094=13095=14096=15097=16098=17099=18100=01101=02102=03103=04104=05105=06106=07107=08108=09109=10110=02111=03112=04113=05114=06115=07116=08117=09118=10119=11120=03121=04122=05123=06124=07125=08126=09127=10128=11129=12130=04131=05132=06133=07134=08135=09136=10137=11138=12139=13140=05141=06142=07143=08144=09145=10146=11147=12148=13149=14150=06151=07152=08153=09154=10155=11156=12157=13158=14159=15160=07161=08162=09163=10164=11165=12166=13167=14168=15169=16170=08171=09172=10173=11174=12175=13176=14177=15178=16179=17180=09181=10182=11183=12184=13185=14186=15187=16188=17189=18190=10191=11192=12193=13194=14195=15196=16197=17198=18199=19/
s/^\(...\)[^;]*;[^;]*\1=\(..\).*/\2/
H
g
s/^\(.*\)\n\(.*\)\n\(.*\)\n...\n\(.\)\(.\)$/\1\n\2\n\5\3\n\4/
/^\([0]*\)\n\([0]*\)\n/ {
        s/^.*\n.*\n\(.*\)\n\(.\)/\2\1/
        s/^0\(.*\)/\1/
        q
}
b LOOP

The way it works is by implementing decimal adder module that adds two input digits (A and B) as well as Carry Bit and produces a Sum and Carry bit. The idea is borrowed from electronic where binary adder does the same for binary numbers. All we have to do is loop the adder over all digits and we can add arbitrary length numbers (limited by memory). Below is the adder in action:

./decAdder.sed
666666666666666666666666666666999999999999991111111112222+1100000000000000000000011111111111111111111111111111111111
1766666666666666666666677777778111111111111102222222223333

In exactly the same way one can implement binary (or any other base) adder. All you have to do is replace the line that starts with s/$/;000=00001... with proper substitution pattern for given base. For example: s/$/;000=00001=01010=01011=10100=01101=10110=10111=11/ is substitution pattern for arbitrary length binary adder.

You can fit the code documented on my github.

0

Not sure if the 'e' flag for the 's' command has been discussed yet but here's how I'd solve this problem

$ echo 12 | sed -E 's/[0-9]*/expr & + 3/e'
15

the 'e' flag takes the current pattern line and runs it as a command before replacing the pattern space with the output. In this case using the 'expr' command to do our math. source: The S Command

A little more complicated, but converts multiple digits that end in a space from bytes->kilobytes (avoids numbers with a percent sign)

$ df | sed -E '1!{s/(\b)([0-9]+)(\s)/\1$(expr \2 \/ 1024)kb\3/g};s/.*/printf "%s" "&"/e'
Filesystem    512-blocks      Used Available Capacity iused      ifree %iused  Mounted on
/dev/disk1s5   478144kb  21563kb 141089kb    14%  477kb 2390246kb    0%   /
devfs                0kb       0kb         0kb   100%     0kb          0kb  100%   /dev
/dev/disk1s1   478144kb 297544kb 141089kb    68% 1727kb 2388995kb    0%   /System/Volumes/Data
/dev/disk1s4   478144kb  16384kb 141089kb    11%       0kb 2390723kb    0%   /private/var/vm
map auto_home          0kb         0kb         0kb   100%       0kb          0kb  100%   /System/Volumes/Data/home
/dev/disk2s1     1188kb   987kb    201kb    84%       0kb 4194303kb    0%   /Volumes/RecoveryHDMeta
/dev/disk3s1     3822kb   2528kb   1294kb    67%   46kb 4194257kb    0%   /Volumes/macOS Base System
  1. wraps each number in '$(' & ')' the '1!{' & '}' makes sure that this ignores the first line
1!{s/(\b)([0-9]+)(\s)/\1$(expr \2 \/ 1024)kb\3/g};
  1. matches whole line & prefixes line with the printf command, the e flag will then execute each line
;s/.*/printf "%s" "&"/e'
1
0

Using Raku (formerly known as Perl_6)

~$ echo 'a12' | raku -pe 's:g/ (\d+) /{$0 + 3}/'
a15

OR

~$ echo 'a12' | raku -pe 's:g/ \d+ /{$/ + 3}/'
a15

OR

~$ echo 'a12' | raku -pe 's:g/ (<[0..9]>+) /{$0 + 3}/'
a15

OR

~$ echo 'a12' | raku -pe 's:g/ <[0..9]>+ /{$/ + 3}/'
a15

The answers above will add 3 to runs of one-or-more digits in an input string--using curly braces in the replacement to execute code (in other words, curly braces denote a code block). Change the s:g/// to s/// (i.e. omit the :global adverb) to restrict substitution to the first match.

Note: Raku is 'Unicode-aware' so even non-0123456789 digits will match using the \d shorthand. Use <[0..9]> or [ <:ASCII> & [\d] ] or (even better) [ <:ASCII> & <:N> ] to restrict to ASCII digits. Below we see the truly-remarkable power of regexes in Raku to separate characters based upon Unicode properties, such as <:ASCII> or <:Script<Bengali>>:

~$ echo '0a1b2c3d4e5f6g7h8i9j' | raku -ne 'put $/ if m:g/ <[0..9]>+ /;'
0 1 2 3 4 5 6 7 8 9
~$ echo '0a1b2c3d4e5f6g7h8i9j' | raku -ne 'put $/ if m:g/ <[a..j]>+ /;'
a b c d e f g h i j

~$ echo '0a1b2c3d4e5f6g7h8i9j' | raku -ne 'put $/ if m:g/ [ <:ASCII> & [\d] ]+ /;'
0 1 2 3 4 5 6 7 8 9
~$ echo '0a1b2c3d4e5f6g7h8i9j' | raku -ne 'put $/ if m:g/ [ <:ASCII> & <:N> ]+ /;'
0 1 2 3 4 5 6 7 8 9
~$ echo '0a1b2c3d4e5f6g7h8i9j' | raku -ne 'put $/ if m:g/ [ <:ASCII> & [\D] ]+ /;'
a b c d e f g h i j
~$ echo '0a1b2c3d4e5f6g7h8i9j' | raku -ne 'put $/ if m:g/ [ <:ASCII> & <:L> ]+ /;'
a b c d e f g h i j

~$ echo '0123456789০১২৩৪৫৬৭৮৯'  | raku -ne 'put $/ if m:g/ [ <:ASCII> & <:N> ]+ /;'
0123456789
~$ echo '0123456789০১২৩৪৫৬৭৮৯'  | raku -ne 'put $/ if m:g/ [ <:Script<Bengali>> & <:N> ]+ /;'
০১২৩৪৫৬৭৮৯

ADDENDUM: At the moment, Raku's $/ "regex-match" variable is aliased to $<>. Some people may prefer to use $<> instead of $/ to reduce "backslash-itis" in their code.

https://docs.raku.org/language/regexes
https://www.codesections.com/blog/raku-unicode/
https://raku.org

You must log in to answer this question.

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