From 41a7b9457c2e6b8a9a644f4a821d6ab9572b2cc6 Mon Sep 17 00:00:00 2001 From: Kurtis Rader Date: Tue, 22 Aug 2017 19:57:30 -0700 Subject: [PATCH] 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. --- doc_src/math.txt | 18 +++-- src/builtin.cpp | 2 + src/builtin_math.cpp | 171 +++++++++++++++++++++++++++++++++++++++++++ src/builtin_math.h | 9 +++ 4 files changed, 193 insertions(+), 7 deletions(-) create mode 100644 src/builtin_math.cpp create mode 100644 src/builtin_math.h diff --git a/doc_src/math.txt b/doc_src/math.txt index 32535d7cb..2377939dd 100644 --- a/doc_src/math.txt +++ b/doc_src/math.txt @@ -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 here. 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. diff --git a/src/builtin.cpp b/src/builtin.cpp index de96b9ea6..7871d90ef 100644 --- a/src/builtin.cpp +++ b/src/builtin.cpp @@ -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")}, diff --git a/src/builtin_math.cpp b/src/builtin_math.cpp new file mode 100644 index 000000000..b5584e7e6 --- /dev/null +++ b/src/builtin_math.cpp @@ -0,0 +1,171 @@ +// Implementation of the math builtin. +#include "config.h" // IWYU pragma: keep + +#include +#include +#include +#include + +#include +#include +#include +#include + +#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); +} diff --git a/src/builtin_math.h b/src/builtin_math.h new file mode 100644 index 000000000..76d5a5da8 --- /dev/null +++ b/src/builtin_math.h @@ -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