mirror of
https://github.com/fish-shell/fish-shell
synced 2025-01-14 05:53:59 +00:00
Teach test to handle floating point values
This commit is contained in:
parent
42c648ab35
commit
40e37c4a87
4 changed files with 97 additions and 20 deletions
|
@ -62,6 +62,7 @@ fish 3.0 is a major release which brings with it both improvements in functional
|
||||||
- Path completions now support expansions, meaning expressions like `python ~/<TAB>` now provides file suggestions just like any other relative or absolute path. (This includes support for other expansions, too.)
|
- Path completions now support expansions, meaning expressions like `python ~/<TAB>` now provides file suggestions just like any other relative or absolute path. (This includes support for other expansions, too.)
|
||||||
- The `string` builtin has new commands `split0` and `join0` for working with NUL-delimited output.
|
- The `string` builtin has new commands `split0` and `join0` for working with NUL-delimited output.
|
||||||
- The `-d` option to `functions` to set the description of an existing function now works; before 3.0 it was documented but unimplemented. Note that the long form `--description` continues to work. (#5105)
|
- The `-d` option to `functions` to set the description of an existing function now works; before 3.0 it was documented but unimplemented. Note that the long form `--description` continues to work. (#5105)
|
||||||
|
- `test` and `[` now support floating point values in numeric comparisons.
|
||||||
|
|
||||||
## Other significant changes
|
## Other significant changes
|
||||||
- Command substitution output is now limited to 10 MB by default (#3822).
|
- Command substitution output is now limited to 10 MB by default (#3822).
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
#include <wchar.h>
|
#include <wchar.h>
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <type_traits>
|
#include <type_traits>
|
||||||
|
@ -74,6 +75,36 @@ enum token_t {
|
||||||
test_paren_close, // ")", close paren
|
test_paren_close, // ")", close paren
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Our number type. We support both doubles and long longs. We have to support these separately
|
||||||
|
/// because some integers are not representable as doubles; these may come up in practice (e.g.
|
||||||
|
/// inodes).
|
||||||
|
class number_t {
|
||||||
|
// A number has an integral base and a floating point delta.
|
||||||
|
// Conceptually the number is base + delta.
|
||||||
|
// We enforce the property that 0 <= delta < 1.
|
||||||
|
long long base;
|
||||||
|
double delta;
|
||||||
|
|
||||||
|
public:
|
||||||
|
number_t(long long base, double delta) : base(base), delta(delta) {
|
||||||
|
assert(0.0 <= delta && delta < 1.0 && "Invalid delta");
|
||||||
|
}
|
||||||
|
number_t() : number_t(0, 0.0) {}
|
||||||
|
|
||||||
|
// Compare two numbers. Returns an integer -1, 0, 1 corresponding to whether we are less than,
|
||||||
|
// equal to, or greater than the rhs.
|
||||||
|
int compare(number_t rhs) const {
|
||||||
|
if (this->base != rhs.base) return (this->base > rhs.base) - (this->base < rhs.base);
|
||||||
|
return (this->delta > rhs.delta) - (this->delta < rhs.delta);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return true if the number is a tty()/
|
||||||
|
bool isatty() const {
|
||||||
|
if (delta != 0.0 || base > INT_MAX || base < INT_MIN) return false;
|
||||||
|
return ::isatty(static_cast<int>(base));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
static bool binary_primary_evaluate(test_expressions::token_t token, const wcstring &left,
|
static bool binary_primary_evaluate(test_expressions::token_t token, const wcstring &left,
|
||||||
const wcstring &right, wcstring_list_t &errors);
|
const wcstring &right, wcstring_list_t &errors);
|
||||||
static bool unary_primary_evaluate(test_expressions::token_t token, const wcstring &arg,
|
static bool unary_primary_evaluate(test_expressions::token_t token, const wcstring &arg,
|
||||||
|
@ -599,22 +630,54 @@ bool parenthetical_expression::evaluate(wcstring_list_t &errors) {
|
||||||
return contents->evaluate(errors);
|
return contents->evaluate(errors);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parse a double from arg. Return true on success, false on failure.
|
||||||
|
static bool parse_double(const wchar_t *arg, double *out_res) {
|
||||||
|
// Consume leading spaces.
|
||||||
|
while (arg && *arg != L'\0' && iswspace(*arg)) arg++;
|
||||||
|
errno = 0;
|
||||||
|
wchar_t *end = NULL;
|
||||||
|
*out_res = wcstod_l(arg, &end, fish_c_locale());
|
||||||
|
// Consume trailing spaces.
|
||||||
|
while (end && *end != L'\0' && iswspace(*end)) end++;
|
||||||
|
return errno == 0 && end > arg && *end == L'\0';
|
||||||
|
}
|
||||||
|
|
||||||
// IEEE 1003.1 says nothing about what it means for two strings to be "algebraically equal". For
|
// IEEE 1003.1 says nothing about what it means for two strings to be "algebraically equal". For
|
||||||
// example, should we interpret 0x10 as 0, 10, or 16? Here we use only base 10 and use wcstoll,
|
// example, should we interpret 0x10 as 0, 10, or 16? Here we use only base 10 and use wcstoll,
|
||||||
// which allows for leading + and -, and whitespace. This is consistent, albeit a bit more lenient
|
// which allows for leading + and -, and whitespace. This is consistent, albeit a bit more lenient
|
||||||
// since we allow trailing whitespace, with other implementations such as bash.
|
// since we allow trailing whitespace, with other implementations such as bash.
|
||||||
static bool parse_number(const wcstring &arg, long long *out, wcstring_list_t &errors) {
|
static bool parse_number(const wcstring &arg, number_t *number, wcstring_list_t &errors) {
|
||||||
*out = fish_wcstoll(arg.c_str());
|
const wchar_t *argcs = arg.c_str();
|
||||||
if (errno) {
|
double floating = 0;
|
||||||
errors.push_back(format_string(_(L"invalid integer '%ls'"), arg.c_str()));
|
bool got_float = parse_double(argcs, &floating);
|
||||||
|
|
||||||
|
errno = 0;
|
||||||
|
long long integral = fish_wcstoll(argcs);
|
||||||
|
bool got_int = (errno == 0);
|
||||||
|
if (got_int) {
|
||||||
|
// Here the value is just an integer; ignore the floating point parse because it may be
|
||||||
|
// invalid (e.g. not a representable integer).
|
||||||
|
*number = number_t{integral, 0.0};
|
||||||
|
return true;
|
||||||
|
} else if (got_float) {
|
||||||
|
// Here we parsed an (in range) floating point value that could not be parsed as an integer.
|
||||||
|
// Break the floating point value into base and delta. Ensure that base is <= the floating
|
||||||
|
// point value.
|
||||||
|
double intpart = std::floor(floating);
|
||||||
|
double delta = floating - intpart;
|
||||||
|
*number = number_t{static_cast<long long>(intpart), delta};
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
// We could not parse a float or an int.
|
||||||
|
errors.push_back(format_string(_(L"invalid number '%ls'"), arg.c_str()));
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
return !errno;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool binary_primary_evaluate(test_expressions::token_t token, const wcstring &left,
|
static bool binary_primary_evaluate(test_expressions::token_t token, const wcstring &left,
|
||||||
const wcstring &right, wcstring_list_t &errors) {
|
const wcstring &right, wcstring_list_t &errors) {
|
||||||
using namespace test_expressions;
|
using namespace test_expressions;
|
||||||
long long left_num, right_num;
|
number_t ln, rn;
|
||||||
switch (token) {
|
switch (token) {
|
||||||
case test_string_equal: {
|
case test_string_equal: {
|
||||||
return left == right;
|
return left == right;
|
||||||
|
@ -623,28 +686,28 @@ static bool binary_primary_evaluate(test_expressions::token_t token, const wcstr
|
||||||
return left != right;
|
return left != right;
|
||||||
}
|
}
|
||||||
case test_number_equal: {
|
case test_number_equal: {
|
||||||
return parse_number(left, &left_num, errors) &&
|
return parse_number(left, &ln, errors) && parse_number(right, &rn, errors) &&
|
||||||
parse_number(right, &right_num, errors) && left_num == right_num;
|
ln.compare(rn) == 0;
|
||||||
}
|
}
|
||||||
case test_number_not_equal: {
|
case test_number_not_equal: {
|
||||||
return parse_number(left, &left_num, errors) &&
|
return parse_number(left, &ln, errors) && parse_number(right, &rn, errors) &&
|
||||||
parse_number(right, &right_num, errors) && left_num != right_num;
|
ln.compare(rn) != 0;
|
||||||
}
|
}
|
||||||
case test_number_greater: {
|
case test_number_greater: {
|
||||||
return parse_number(left, &left_num, errors) &&
|
return parse_number(left, &ln, errors) && parse_number(right, &rn, errors) &&
|
||||||
parse_number(right, &right_num, errors) && left_num > right_num;
|
ln.compare(rn) > 0;
|
||||||
}
|
}
|
||||||
case test_number_greater_equal: {
|
case test_number_greater_equal: {
|
||||||
return parse_number(left, &left_num, errors) &&
|
return parse_number(left, &ln, errors) && parse_number(right, &rn, errors) &&
|
||||||
parse_number(right, &right_num, errors) && left_num >= right_num;
|
ln.compare(rn) >= 0;
|
||||||
}
|
}
|
||||||
case test_number_lesser: {
|
case test_number_lesser: {
|
||||||
return parse_number(left, &left_num, errors) &&
|
return parse_number(left, &ln, errors) && parse_number(right, &rn, errors) &&
|
||||||
parse_number(right, &right_num, errors) && left_num < right_num;
|
ln.compare(rn) < 0;
|
||||||
}
|
}
|
||||||
case test_number_lesser_equal: {
|
case test_number_lesser_equal: {
|
||||||
return parse_number(left, &left_num, errors) &&
|
return parse_number(left, &ln, errors) && parse_number(right, &rn, errors) &&
|
||||||
parse_number(right, &right_num, errors) && left_num <= right_num;
|
ln.compare(rn) <= 0;
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
errors.push_back(format_string(L"Unknown token type in %s", __func__));
|
errors.push_back(format_string(L"Unknown token type in %s", __func__));
|
||||||
|
@ -657,7 +720,6 @@ static bool unary_primary_evaluate(test_expressions::token_t token, const wcstri
|
||||||
wcstring_list_t &errors) {
|
wcstring_list_t &errors) {
|
||||||
using namespace test_expressions;
|
using namespace test_expressions;
|
||||||
struct stat buf;
|
struct stat buf;
|
||||||
long long num;
|
|
||||||
switch (token) {
|
switch (token) {
|
||||||
case test_filetype_b: { // "-b", for block special files
|
case test_filetype_b: { // "-b", for block special files
|
||||||
return !wstat(arg, &buf) && S_ISBLK(buf.st_mode);
|
return !wstat(arg, &buf) && S_ISBLK(buf.st_mode);
|
||||||
|
@ -704,7 +766,8 @@ static bool unary_primary_evaluate(test_expressions::token_t token, const wcstri
|
||||||
return !wstat(arg, &buf) && buf.st_size > 0;
|
return !wstat(arg, &buf) && buf.st_size > 0;
|
||||||
}
|
}
|
||||||
case test_filedesc_t: { // "-t", whether the fd is associated with a terminal
|
case test_filedesc_t: { // "-t", whether the fd is associated with a terminal
|
||||||
return parse_number(arg, &num, errors) && num == (int)num && isatty((int)num);
|
number_t num;
|
||||||
|
return parse_number(arg, &num, errors) && num.isatty();
|
||||||
}
|
}
|
||||||
case test_fileperm_r: { // "-r", read permission
|
case test_fileperm_r: { // "-r", read permission
|
||||||
return !waccess(arg, R_OK);
|
return !waccess(arg, R_OK);
|
||||||
|
|
|
@ -2247,6 +2247,18 @@ static void test_test() {
|
||||||
// https://github.com/fish-shell/fish-shell/issues/601
|
// https://github.com/fish-shell/fish-shell/issues/601
|
||||||
do_test(run_test_test(0, L"-S = -S"));
|
do_test(run_test_test(0, L"-S = -S"));
|
||||||
do_test(run_test_test(1, L"! ! ! A"));
|
do_test(run_test_test(1, L"! ! ! A"));
|
||||||
|
|
||||||
|
// Verify that 1. doubles are treated as doubles, and 2. integers that cannot be represented as
|
||||||
|
// doubles are still treated as integers.
|
||||||
|
do_test(run_test_test(0, L"4611686018427387904 -eq 4611686018427387904"));
|
||||||
|
do_test(run_test_test(0, L"4611686018427387904.0 -eq 4611686018427387904.0"));
|
||||||
|
do_test(run_test_test(0, L"4611686018427387904.00000000000000001 -eq 4611686018427387904.0"));
|
||||||
|
do_test(run_test_test(1, L"4611686018427387904 -eq 4611686018427387905"));
|
||||||
|
do_test(run_test_test(0, L"-4611686018427387904 -ne 4611686018427387904"));
|
||||||
|
do_test(run_test_test(0, L"-4611686018427387904 -le 4611686018427387904"));
|
||||||
|
do_test(run_test_test(1, L"-4611686018427387904 -ge 4611686018427387904"));
|
||||||
|
do_test(run_test_test(1, L"4611686018427387904 -gt 4611686018427387904"));
|
||||||
|
do_test(run_test_test(0, L"4611686018427387904 -ge 4611686018427387904"));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Testing colors.
|
/// Testing colors.
|
||||||
|
|
|
@ -125,6 +125,7 @@ int fish_wcstoi(const wchar_t *str, const wchar_t **endptr = NULL, int base = 10
|
||||||
long fish_wcstol(const wchar_t *str, const wchar_t **endptr = NULL, int base = 10);
|
long fish_wcstol(const wchar_t *str, const wchar_t **endptr = NULL, int base = 10);
|
||||||
long long fish_wcstoll(const wchar_t *str, const wchar_t **endptr = NULL, int base = 10);
|
long long fish_wcstoll(const wchar_t *str, const wchar_t **endptr = NULL, int base = 10);
|
||||||
unsigned long long fish_wcstoull(const wchar_t *str, const wchar_t **endptr = NULL, int base = 10);
|
unsigned long long fish_wcstoull(const wchar_t *str, const wchar_t **endptr = NULL, int base = 10);
|
||||||
|
double fish_wcstod(const wchar_t *str, const wchar_t **endptr);
|
||||||
|
|
||||||
/// Class for representing a file's inode. We use this to detect and avoid symlink loops, among
|
/// Class for representing a file's inode. We use this to detect and avoid symlink loops, among
|
||||||
/// other things. While an inode / dev pair is sufficient to distinguish co-existing files, Linux
|
/// other things. While an inode / dev pair is sufficient to distinguish co-existing files, Linux
|
||||||
|
|
Loading…
Reference in a new issue