builtin test: Implement -ot, -nt, -ef

These are non-POSIX extensions other test(1) utilities implement,
which compares the modification time of two files as proposed for
fish in #3589: testing if one file is newer than another file.

-ef is a common extension to test(1) which checks if two paths refer
to the same file, by comparing the dev and inode numbers.
This commit is contained in:
Aaron Gyes 2022-07-07 17:07:32 -07:00
parent 7bdc712615
commit 8f91ee7f6b
3 changed files with 40 additions and 3 deletions

View File

@ -65,6 +65,10 @@ enum token_t {
test_string_equal, // "=", true if strings are identical
test_string_not_equal, // "!=", true if strings are not identical
test_file_newer, // f1 -nt f2, true if f1 exists and is newer than f2, or there is no f2
test_file_older, // f1 -ot f2, true if f2 exists and f1 does not, or f1 is older than f2
test_file_same, // f1 -ef f2, true if f1 and f2 exist and refer to same file
test_number_equal, // "-eq", true if numbers are equal
test_number_not_equal, // "-ne", true if numbers are not equal
test_number_greater, // "-gt", true if first number is larger than second
@ -152,6 +156,9 @@ static const token_info_t *token_for_string(const wcstring &str) {
{L"-z", {test_string_z, UNARY_PRIMARY}},
{L"=", {test_string_equal, BINARY_PRIMARY}},
{L"!=", {test_string_not_equal, BINARY_PRIMARY}},
{L"-nt", {test_file_newer, BINARY_PRIMARY}},
{L"-ot", {test_file_older, BINARY_PRIMARY}},
{L"-ef", {test_file_same, BINARY_PRIMARY}},
{L"-eq", {test_number_equal, BINARY_PRIMARY}},
{L"-ne", {test_number_not_equal, BINARY_PRIMARY}},
{L"-gt", {test_number_greater, BINARY_PRIMARY}},
@ -742,6 +749,15 @@ static bool binary_primary_evaluate(test_expressions::token_t token, const wcstr
case test_string_not_equal: {
return left != right;
}
case test_file_newer: {
return file_id_for_path(right).older_than(file_id_for_path(left));
}
case test_file_older: {
return file_id_for_path(left).older_than(file_id_for_path(right));
}
case test_file_same: {
return file_id_for_path(left) == file_id_for_path(right);
}
case test_number_equal: {
return parse_number(left, &ln, errors) && parse_number(right, &rn, errors) &&
ln.compare(rn) == 0;

View File

@ -868,6 +868,22 @@ static int compare(T a, T b) {
return 0;
}
/// \return true if \param rhs has higher mtime seconds than this file_id_t.
/// If identical, nanoseconds are compared.
bool file_id_t::older_than(const file_id_t &rhs) const {
int ret = compare(mod_seconds, rhs.mod_seconds);
if (!ret) ret = compare(mod_nanoseconds, rhs.mod_nanoseconds);
switch (ret) {
case -1:
return true;
case 1:
case 0:
return false;
default:
DIE("unreachable");
}
}
int file_id_t::compare_file_id(const file_id_t &rhs) const {
// Compare each field, stopping when we get to a non-equal field.
int ret = 0;

View File

@ -145,9 +145,14 @@ struct file_id_t {
dev_t device{static_cast<dev_t>(-1LL)};
ino_t inode{static_cast<ino_t>(-1LL)};
uint64_t size{static_cast<uint64_t>(-1LL)};
time_t change_seconds{-1};
/* some platforms handle negative time_t
values to represent WW1-era dates, initialize ancient
for the sake of comparisons.
tv_nsec's meaningful values are REALLY [0, 999999999]
this nanosecond component we'll initialize at 0. */
time_t change_seconds{std::numeric_limits<time_t>::min()};
long change_nanoseconds{-1};
time_t mod_seconds{-1};
time_t mod_seconds{std::numeric_limits<time_t>::min()};
long mod_nanoseconds{-1};
constexpr file_id_t() = default;
@ -159,7 +164,7 @@ struct file_id_t {
bool operator<(const file_id_t &rhs) const;
static file_id_t from_stat(const struct stat &buf);
bool older_than(const file_id_t &rhs) const;
wcstring dump() const;
private: