Implement bare minimum builtin math command

This is the second baby step in resolving #3157. Implement a bare minimum
builtin `math` command. This is solely to ensure that fish can be built
and run in the Travis build environments. This is okay since anyone running
`builtin math` today is already getting an error response.

Also, more work is needed to support bare var references, multiple result
values, etc.
This commit is contained in:
Kurtis Rader 2017-08-22 19:57:30 -07:00
parent d247c121a2
commit 41a7b9457c
4 changed files with 193 additions and 7 deletions

View file

@ -7,23 +7,23 @@ math [-sN | --scale=N] [--] EXPRESSION
\subsection math-description Description
`math` is used to perform mathematical calculations. It is a very thin wrapper for the bc program, which makes it possible to specify an expression from the command line without using non-standard extensions or a pipeline.
`math` is used to perform mathematical calculations. It is based on the MuParser library which is documented <a href="http://beltoforion.de/article.php?a=muparser&hl=en&p=features&s=idPageTop#idPageTop">here</a>. You can use bare variable names (i.e., without the dollar-sign). The stock MuParser does not support the modulo, `%` operator but fish implements it using integer semantics.
For a description of the syntax supported by math, see the manual for the bc program. Keep in mind that parameter expansion takes place on any expressions before they 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 have to be escaped.
Keep in mind that parameter expansion takes place on any expressions before they 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.
The following options are available:
- `-sN` or `--scale=N` sets the scale of the result. `N` must be an integer and defaults to zero. This simply sets bc's `scale` variable to the provided value.
- `-sN` or `--scale=N` sets the scale of the result. `N` must be an integer and defaults to zero (rounded to the nearest integer).
\subsection return-values Return Values
If invalid options or no expression is provided the return `status` is two. If the expression is invalid the return `status` is three. If bc returns a result of `0` (literally, not `0.0` or similar variants) the return `status` is one otherwise it's zero.
If the expression is successfully evaluated the return `status` is zero (success) else one.
\subsection math-example Examples
`math 1+1` outputs 2.
`math $status-128` outputs the numerical exit status of the last command minus 128.
`math status - 128` outputs the numerical exit status of the last command minus 128.
`math 10 / 6` outputs `1`.
@ -33,6 +33,10 @@ If invalid options or no expression is provided the return `status` is two. If t
\subsection math-cautions Cautions
You should always place a `--` flag separator before the expression. 99.99% of the time you'll get the desired result without the separator. Something like `math -10.0 / 2` will fail because the negative floating point value gets treated as an invalid flag. But `math -10 / 2` will work because negative integers are special-cased.
You don't need to use `--` before the expression even if it begins with a minus sign which might otherwise be interpreted as an invalid option.
Note that the modulo operator (`x % y`) is not well defined for floating point arithmetic. The `bc` command produces a nonsensical result rather than emit an error and fail in that case. It doesn't matter if the arguments are integers; e.g., `10 % 4`. You'll still get an incorrect result. Do not use the `-sN` flag with N greater than zero if you want sensible answers when using the modulo operator.
Note that the modulo operator (`x % y`) is not well defined for floating point arithmetic. Fish rounds down all floating point values to nearest int before performing the modulo operation. So `10.5 % 6.1` is `4`.
\subsection math-notes Compatibility notes
Fish 1.x and 2.x releases relied on the `bc` command for handling math expressions via the `math` command. Starting with fish 3.0.0 fish uses the MuParser library.

View file

@ -46,6 +46,7 @@
#include "builtin_functions.h"
#include "builtin_history.h"
#include "builtin_jobs.h"
#include "builtin_math.h"
#include "builtin_printf.h"
#include "builtin_pwd.h"
#include "builtin_random.h"
@ -440,6 +441,7 @@ static const builtin_data_t builtin_datas[] = {
{L"history", &builtin_history, N_(L"History of commands executed by user")},
{L"if", &builtin_generic, N_(L"Evaluate block if condition is true")},
{L"jobs", &builtin_jobs, N_(L"Print currently running jobs")},
{L"math", &builtin_math, N_(L"Evaluate math expressions")},
{L"not", &builtin_generic, N_(L"Negate exit status of job")},
{L"or", &builtin_generic, N_(L"Execute command if previous command failed")},
{L"printf", &builtin_printf, N_(L"Prints formatted text")},

171
src/builtin_math.cpp Normal file
View file

@ -0,0 +1,171 @@
// Implementation of the math builtin.
#include "config.h" // IWYU pragma: keep
#include <errno.h>
#include <stddef.h>
#include <stdint.h>
#include <wchar.h>
#include <algorithm>
#include <iostream>
#include <random>
#include <string>
#include "builtin.h"
#include "builtin_math.h"
#include "common.h"
#include "fallback.h" // IWYU pragma: keep
#include "io.h"
#include "wgetopt.h"
#include "wutil.h" // IWYU pragma: keep
#include "muParser.h"
#include "muParserBase.h"
#include "muParserDef.h"
struct math_cmd_opts_t {
bool print_help = false;
int scale = 0;
};
// This command is atypical in using the "+" (REQUIRE_ORDER) option for flag parsing.
// This is needed because of the minus, `-`, operator in math expressions.
static const wchar_t *short_options = L"+:hs:";
static const struct woption long_options[] = {{L"scale", required_argument, NULL, 's'},
{L"help", no_argument, NULL, 'h'},
{NULL, 0, NULL, 0}};
static int parse_cmd_opts(math_cmd_opts_t &opts, int *optind, //!OCLINT(high ncss method)
int argc, wchar_t **argv, parser_t &parser, io_streams_t &streams) {
const wchar_t *cmd = L"math";
int opt;
wgetopter_t w;
while ((opt = w.wgetopt_long(argc, argv, short_options, long_options, NULL)) != -1) {
switch (opt) {
case 's': {
opts.scale = fish_wcstoi(w.woptarg);
if (errno || opts.scale < 0 || opts.scale > 15) {
streams.err.append_format(_(L"%ls: '%ls' is not a valid scale value\n"), cmd,
w.woptarg);
return STATUS_INVALID_ARGS;
}
break;
}
case 'h': {
opts.print_help = true;
break;
}
case ':': {
builtin_missing_argument(parser, streams, cmd, argv[w.woptind - 1]);
return STATUS_INVALID_ARGS;
}
case '?': {
// For most commands this is an error. We ignore it because a math expression
// can begin with a minus sign.
*optind = w.woptind - 1;
return STATUS_CMD_OK;
}
default: {
DIE("unexpected retval from wgetopt_long");
break;
}
}
}
*optind = w.woptind;
return STATUS_CMD_OK;
}
// We read from stdin if we are the second or later process in a pipeline.
static bool math_args_from_stdin(const io_streams_t &streams) {
return streams.stdin_is_directly_redirected;
}
/// Get the arguments from stdin.
static const wchar_t *math_get_arg_stdin(wcstring *storage, const io_streams_t &streams) {
std::string arg;
for (;;) {
char ch = '\0';
long rc = read_blocked(streams.stdin_fd, &ch, 1);
if (rc < 0) return NULL; // failure
if (rc == 0) { // EOF
if (arg.empty()) return NULL;
break;
}
if (ch == '\n') break; // we're done
arg += ch;
}
*storage = str2wcstring(arg);
return storage->c_str();
}
/// Return the next argument from argv.
static const wchar_t *math_get_arg_argv(int *argidx, wchar_t **argv) {
return argv && argv[*argidx] ? argv[(*argidx)++] : NULL;
}
/// Get the arguments from argv or stdin based on the execution context. This mimics how builtin
/// `string` does it.
static const wchar_t *math_get_arg(int *argidx, wchar_t **argv, wcstring *storage,
const io_streams_t &streams) {
if (math_args_from_stdin(streams)) {
return math_get_arg_stdin(storage, streams);
}
return math_get_arg_argv(argidx, argv);
}
/// Implement integer modulo math operator.
static double moduloOperator(double v, double w) { return (int)v % std::max(1, (int)w); };
static int evaluate_expression(wchar_t *cmd, parser_t &parser, io_streams_t &streams,
math_cmd_opts_t &opts, wcstring &expression) {
UNUSED(parser);
try {
mu::Parser p;
// MuParser doesn't implement the modulo operator so we add it ourselves since there are
// likely users of our old math wrapper around bc that expect it to be available.
p.DefineOprtChars(L"%");
p.DefineOprt(L"%", moduloOperator, mu::prINFIX);
p.SetExpr(expression);
streams.out.append_format(L"%.*lf\n", opts.scale, p.Eval());
return STATUS_CMD_OK;
} catch (mu::Parser::exception_type &e) {
streams.err.append_format(_(L"%ls: Invalid expression: %ls\n"), cmd, e.GetMsg().c_str());
return STATUS_CMD_ERROR;
}
}
/// The math builtin evaluates math expressions.
int builtin_math(parser_t &parser, io_streams_t &streams, wchar_t **argv) {
wchar_t *cmd = argv[0];
int argc = builtin_count_args(argv);
math_cmd_opts_t opts;
int optind;
// Is this really the right way to handle no expression present?
// if (argc == 0) return STATUS_CMD_OK;
int retval = parse_cmd_opts(opts, &optind, argc, argv, parser, streams);
if (retval != STATUS_CMD_OK) return retval;
if (opts.print_help) {
builtin_print_help(parser, streams, cmd, streams.out);
return STATUS_CMD_OK;
}
wcstring expression;
wcstring storage;
while (const wchar_t *arg = math_get_arg(&optind, argv, &storage, streams)) {
if (!expression.empty()) expression.push_back(L' ');
expression.append(arg);
}
return evaluate_expression(cmd, parser, streams, opts, expression);
}

9
src/builtin_math.h Normal file
View file

@ -0,0 +1,9 @@
// Prototypes for executing builtin_math function.
#ifndef FISH_BUILTIN_MATH_H
#define FISH_BUILTIN_MATH_H
class parser_t;
struct io_streams_t;
int builtin_math(parser_t &parser, io_streams_t &streams, wchar_t **argv);
#endif