math: Make function parentheses optional (#7877)

* math: Make function parentheses optional

It's a bit annoying to use parentheses here because that requires
quoting or escaping.

This allows the parens to be omitted, so

math sin pi

is the same as

math 'sin(pi)'

Function calls have the lowest precedence, so

math sin 2 + 6

is the same as

math 'sin(2 + 6)'

* Add more tests

* Add a note to the docs

* even moar docs

Moar docca

* moar tests

Call me Nikola Testla
This commit is contained in:
Fabian Homborg 2021-03-30 17:21:28 +02:00 committed by GitHub
parent d5cba5fe12
commit ed9268f99c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 61 additions and 26 deletions

View File

@ -20,8 +20,11 @@ By default, the output is a floating-point number with trailing zeroes trimmed.
Keep in mind that parameter expansion happens before expressions are evaluated. This can be very useful in order to perform calculations involving shell variables or the output of command substitutions, but it also means that parenthesis (``()``) and the asterisk (``*``) glob character have to be escaped or quoted. ``x`` can also be used to denote multiplication, but it needs to be followed by whitespace to distinguish it from hexadecimal numbers.
Parentheses for functions are optional - ``math sin pi`` prints ``0``. However, a comma will bind to the inner function, so ``math pow sin 3, 5`` is an error because it tries to give ``sin`` the arguments ``3`` and ``5``. When in doubt, use parentheses.
``math`` ignores whitespace between arguments and takes its input as multiple arguments (internally joined with a space), so ``math 2 +2`` and ``math "2 + 2"`` work the same. ``math 2 2`` is an error.
The following options are available:
- ``-sN`` or ``--scale=N`` sets the scale of the result. ``N`` must be an integer or the word "max" for the maximum scale. A scale of zero causes results to be rounded down to the nearest integer. So ``3/2`` returns ``1`` rather than ``2`` which ``1.5`` would normally round to. This is for compatibility with ``bc`` which was the basis for this command prior to fish 3.0.0. Scale values greater than zero causes the result to be rounded using the usual rules to the specified number of decimal places.
@ -121,7 +124,7 @@ Examples
``math 0xFF`` outputs 255, ``math 0 x 3`` outputs 0 (because it computes 0 multiplied by 3).
``math "bitand(0xFE, 0x2e)"`` outputs 46.
``math bitand 0xFE, 0x2e`` outputs 46.
``math "bitor(9,2)"`` outputs 11.

View File

@ -396,41 +396,52 @@ static te_expr *base(state *s) {
case TE_FUNCTION1:
case TE_FUNCTION2:
case TE_FUNCTION3:
case TE_FUNCTION3: {
arity = get_arity(s->type);
ret = new_expr(s->type, nullptr);
ret->function = s->function;
next_token(s);
bool have_open = false;
if (s->type == TOK_OPEN) {
int i;
for (i = 0; i < arity; i++) {
next_token(s);
ret->parameters[i] = expr(s);
if (s->type != TOK_SEP) {
break;
}
// If we *have* an opening parenthesis,
// we need to consume it and
// expect a closing one.
have_open = true;
next_token(s);
}
int i;
for (i = 0; i < arity; i++) {
ret->parameters[i] = expr(s);
if (s->type != TOK_SEP) {
break;
}
if (s->type == TOK_CLOSE && i == arity - 1) {
next_token(s);
} else if (s->type != TOK_ERROR || s->error == TE_ERROR_UNEXPECTED_TOKEN) {
// If we had the right number of arguments, we're missing a closing paren.
if (i == arity - 1 && s->type != TOK_ERROR) {
s->error = TE_ERROR_MISSING_CLOSING_PAREN;
} else {
// Otherwise we complain about the number of arguments *first*,
// a closing parenthesis should be more obvious.
s->error = i < arity ? TE_ERROR_TOO_FEW_ARGS : TE_ERROR_TOO_MANY_ARGS;
}
s->type = TOK_ERROR;
next_token(s);
}
if (!have_open && i == arity - 1) {
break;
}
if (have_open && s->type == TOK_CLOSE && i == arity - 1) {
// We have an opening and a closing paren, consume the closing one and done.
next_token(s);
} else if (s->type != TOK_ERROR || s->error == TE_ERROR_UNEXPECTED_TOKEN) {
// If we had the right number of arguments, we're missing a closing paren.
if (have_open && i == arity - 1 && s->type != TOK_ERROR) {
s->error = TE_ERROR_MISSING_CLOSING_PAREN;
} else {
// Otherwise we complain about the number of arguments *first*,
// a closing parenthesis should be more obvious.
s->error = i < arity ? TE_ERROR_TOO_FEW_ARGS : TE_ERROR_TOO_MANY_ARGS;
}
} else if (s->type != TOK_ERROR || s->error == TE_ERROR_UNKNOWN) {
s->type = TOK_ERROR;
s->error = TE_ERROR_MISSING_OPENING_PAREN;
}
break;
}
case TOK_OPEN:
next_token(s);

View File

@ -171,9 +171,7 @@ math "bitor(37 ^ 5, 255)"
# CHECK: 69343999
math 'log 16'
# CHECKERR: math: Error: Missing opening parenthesis
# CHECKERR: 'log 16'
# CHECKERR: ^
# CHECK: 1.20412
math 'log(16'
# CHECKERR: math: Error: Missing closing parenthesis
@ -195,3 +193,26 @@ echo $status
math 'log2(8)'
# CHECK: 3
# same as sin(cos(2 x pi))
math sin cos 2 x pi
# CHECK: 0.841471
# Inner function binds stronger, so this is interpreted as
# pow(sin(3,5))
math pow sin 3, 5
# CHECKERR: math: Error: Too many arguments
# CHECKERR: 'pow sin 3, 5'
# CHECKERR: ^
math sin pow 3, 5
# CHECK: -0.890009
math pow 2, cos -pi
# CHECK: 0.5
# pow(2*cos(-pi), 2)
# i.e. 2^2
# i.e. 4
math pow 2 x cos'(-pi)', 2
# CHECK: 4