// Implementation of the math builtin. #include "config.h" // IWYU pragma: keep #include "builtin_math.h" #include #include #include #include #include #include #include #include "builtin.h" #include "common.h" #include "fallback.h" // IWYU pragma: keep #include "io.h" #include "tinyexpr.h" #include "wgetopt.h" #include "wutil.h" // IWYU pragma: keep // The maximum number of points after the decimal that we'll print. static constexpr int kDefaultScale = 6; // The end of the range such that every integer is representable as a double. // i.e. this is the first value such that x + 1 == x (or == x + 2, depending on rounding mode). static constexpr double kMaximumContiguousInteger = double(1LLU << std::numeric_limits::digits); struct math_cmd_opts_t { bool print_help = false; bool have_scale = false; int scale = kDefaultScale; int base = 10; }; // 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 *const short_options = L"+:hs:b:"; static const struct woption long_options[] = {{L"scale", required_argument, nullptr, 's'}, {L"base", required_argument, nullptr, 'b'}, {L"help", no_argument, nullptr, 'h'}, {nullptr, 0, nullptr, 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, nullptr)) != -1) { switch (opt) { case 's': { opts.have_scale = true; // "max" is the special value that tells us to pick the maximum scale. if (std::wcscmp(w.woptarg, L"max") == 0) { opts.scale = 15; } else { 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 'b': { if (std::wcscmp(w.woptarg, L"hex") == 0) { opts.base = 16; } else if (std::wcscmp(w.woptarg, L"octal") == 0) { opts.base = 8; } else { opts.base = fish_wcstoi(w.woptarg); if (errno || (opts.base != 8 && opts.base != 16)) { streams.err.append_format(_(L"%ls: '%ls' is not a valid base 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"); } } } if (opts.have_scale && opts.scale != 0 && opts.base != 10) { streams.err.append_format( _(L"%ls: Bases other than 10 can only do scale=0 output currently\n"), cmd, w.woptarg); return STATUS_INVALID_ARGS; } *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) { // error wperror(L"read"); return nullptr; } if (rc == 0) { // EOF if (arg.empty()) return nullptr; 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)++] : nullptr; } /// 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)) { assert(streams.stdin_fd >= 0 && "stdin should not be closed since it is directly redirected"); return math_get_arg_stdin(storage, streams); } return math_get_arg_argv(argidx, argv); } static const wchar_t *math_describe_error(const te_error_t &error) { if (error.position == 0) return L"NO ERROR?!?"; switch (error.type) { case TE_ERROR_NONE: DIE("Error has no position"); case TE_ERROR_UNKNOWN_FUNCTION: return _(L"Unknown function"); case TE_ERROR_MISSING_CLOSING_PAREN: return _(L"Missing closing parenthesis"); case TE_ERROR_MISSING_OPENING_PAREN: return _(L"Missing opening parenthesis"); case TE_ERROR_TOO_FEW_ARGS: return _(L"Too few arguments"); case TE_ERROR_TOO_MANY_ARGS: return _(L"Too many arguments"); case TE_ERROR_MISSING_OPERATOR: return _(L"Missing operator"); case TE_ERROR_UNEXPECTED_TOKEN: return _(L"Unexpected token"); case TE_ERROR_LOGICAL_OPERATOR: return _(L"Logical operations are not supported, use `test` instead"); case TE_ERROR_UNKNOWN: return _(L"Expression is bogus"); default: return L"Unknown error"; } } /// Return a formatted version of the value \p v respecting the given \p opts. static wcstring format_double(double v, const math_cmd_opts_t &opts) { if (opts.base == 16) { v = trunc(v); return format_string(L"0x%x", (long)v); } else if (opts.base == 8) { v = trunc(v); return format_string(L"0%o", (long)v); } // As a special-case, a scale of 0 means to truncate to an integer // instead of rounding. if (opts.scale == 0) { v = trunc(v); return format_string(L"%.*f", opts.scale, v); } wcstring ret = format_string(L"%.*f", opts.scale, v); // If we contain a decimal separator, trim trailing zeros after it, and then the separator // itself if there's nothing after it. Detect a decimal separator as a non-digit. const wchar_t *const digits = L"0123456789"; if (ret.find_first_not_of(digits) != wcstring::npos) { while (ret.back() == L'0') { ret.pop_back(); } if (!std::wcschr(digits, ret.back())) { ret.pop_back(); } } // If we trimmed everything it must have just been zero. if (ret.empty()) { ret.push_back(L'0'); } return ret; } /// Evaluate math expressions. static int evaluate_expression(const wchar_t *cmd, const parser_t &parser, io_streams_t &streams, const math_cmd_opts_t &opts, wcstring &expression) { UNUSED(parser); int retval = STATUS_CMD_OK; te_error_t error; // Switch locale while computing stuff. // This means that the "." is always the radix character, // so numbers work the same across locales. // // TODO: Technically this is only needed for *output* currently, // because we already use wcstod_l while computing, // but we can't have math print numbers that it won't then also read. char *saved_locale = strdup(setlocale(LC_NUMERIC, nullptr)); setlocale(LC_NUMERIC, "C"); double v = te_interp(expression.c_str(), &error); if (error.position == 0) { // Check some runtime errors after the fact. // TODO: Really, this should be done in tinyexpr // (e.g. infinite is the result of "x / 0"), // but that's much more work. const wchar_t *error_message = nullptr; if (std::isinf(v)) { error_message = L"Result is infinite"; } else if (std::isnan(v)) { error_message = L"Result is not a number"; } else if (std::abs(v) >= kMaximumContiguousInteger) { error_message = L"Result magnitude is too large"; } if (error_message) { streams.err.append_format(L"%ls: Error: %ls\n", cmd, error_message); streams.err.append_format(L"'%ls'\n", expression.c_str()); retval = STATUS_CMD_ERROR; } else { streams.out.append(format_double(v, opts)); streams.out.push_back(L'\n'); } } else { streams.err.append_format(L"%ls: Error: %ls\n", cmd, math_describe_error(error)); streams.err.append_format(L"'%ls'\n", expression.c_str()); streams.err.append_format(L"%*ls%ls\n", error.position - 1, L" ", L"^"); retval = STATUS_CMD_ERROR; } setlocale(LC_NUMERIC, saved_locale); free(saved_locale); return retval; } /// The math builtin evaluates math expressions. maybe_t 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); 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); } if (expression.empty()) { streams.err.append_format(BUILTIN_ERR_MIN_ARG_COUNT1, L"math", 1, 0); return STATUS_CMD_ERROR; } return evaluate_expression(cmd, parser, streams, opts, expression); }