Add path mtime (#9057)

This can be used to print the modification time, like `stat` with some
options.

The reason is that `stat` has caused us a number of portability
headaches:

1. It's not available everywhere by default
2. The versions are quite different

For instance, with GNU stat it's `stat -c '%Y'`, with macOS it's `stat
-f %m`.

So now checking a cache file can be done just with builtins.
This commit is contained in:
Fabian Boehm 2022-07-18 20:39:01 +02:00 committed by GitHub
parent d9ee5d3863
commit 5dfb64b547
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 115 additions and 26 deletions

View File

@ -17,6 +17,7 @@ Synopsis
path is GENERAL_OPTIONS [(-v | --invert)] [(-t | --type) TYPE]
[-d] [-f] [-l] [-r] [-w] [-x]
[(-p | --perm) PERMISSION] [PATH ...]
path mtime GENERAL_OPTIONS [(-R | --relative)] [PATH ...]
path normalize GENERAL_OPTIONS [PATH ...]
path resolve GENERAL_OPTIONS [PATH ...]
path change-extension GENERAL_OPTIONS EXTENSION [PATH ...]
@ -234,6 +235,40 @@ Examples
>_ path is -fx /bin/sh
# /bin/sh is usually an executable file, so this returns true.
"mtime" subcommand
-----------------------
::
path mtime [-z | --null-in] [-Z | --null-out] [-q | --quiet] [-R | --relative] [PATH ...]
``path mtime`` returns the last modification time ("mtime" in unix jargon) of the given paths, in seconds since the unix epoch (the beginning of the 1st of January 1970).
With ``--relative`` (or ``-R``), it prints the number of seconds since the modification time. It only reads the current time once at start, so in case multiple paths are given the times are all relative to the *start* of ``path mtime -R`` running.
If you want to know if a file is newer or older than another file, consider using ``test -nt`` instead. See :ref:`the test documentation <cmd-test>`.
It returns 0 if reading mtime for any path succeeded.
Examples
^^^^^^^^
::
>_ date +%s
# This prints the current time as seconds since the epoch
1657217847
>_ path mtime /etc/
1657213796
>_ path mtime -R /etc/
4078
# So /etc/ on this system was last modified a little over an hour ago
# This is the same as
>_ math (date +%s) - (path mtime /etc/)
"normalize" subcommand
-----------------------

View File

@ -5,6 +5,7 @@ complete -f -c path -n "test (count (commandline -opc)) -lt 2" -a basename -d 'G
complete -f -c path -n "test (count (commandline -opc)) -lt 2" -a dirname -d 'Give dirname for given paths'
complete -f -c path -n "test (count (commandline -opc)) -lt 2" -a extension -d 'Give extension for given paths'
complete -f -c path -n "test (count (commandline -opc)) -lt 2" -a change-extension -d 'Change extension for given paths'
complete -f -c path -n "test (count (commandline -opc)) -lt 2" -a mtime -d 'Show modification time'
complete -f -c path -n "test (count (commandline -opc)) -lt 2" -a normalize -d 'Normalize given paths (remove ./, resolve ../ against other components..)'
complete -f -c path -n "test (count (commandline -opc)) -lt 2" -a resolve -d 'Normalize given paths and resolve symlinks'
complete -f -c path -n "test (count (commandline -opc)) -lt 2" -a filter -d 'Print paths that match a filter'
@ -22,6 +23,7 @@ complete -f -c path -n "test (count (commandline -opc)) -ge 2; and contains -- (
complete -f -c path -n "test (count (commandline -opc)) -ge 2; and contains -- (commandline -opc)[2] filter is" -s r -d "Filter readable paths"
complete -f -c path -n "test (count (commandline -opc)) -ge 2; and contains -- (commandline -opc)[2] filter is" -s w -d "Filter writable paths"
complete -f -c path -n "test (count (commandline -opc)) -ge 2; and contains -- (commandline -opc)[2] filter is" -s x -d "Filter executable paths"
complete -f -c path -n "test (count (commandline -opc)) -ge 2; and contains -- (commandline -opc)[2] mtime" -s R -l relative -d "Show seconds since the modification time"
complete -f -c path -n "test (count (commandline -opc)) -ge 2; and contains -- (commandline -opc)[2] sort" \
-l key -x -a 'basename\t"Sort only by basename" dirname\t"Sort only by dirname" path\t"Sort by full path"'
complete -f -c path -n "test (count (commandline -opc)) -ge 2; and contains -- (commandline -opc)[2] sort" -s u -l unique -d 'Only leave the first of each run with the same key'

View File

@ -33,10 +33,7 @@ if test $status -eq 0 -a (count $sysver) -eq 3
set -l age $max_age
if test -f "$whatis"
# Some people use GNU tools on macOS, and GNU stat works differently.
# However it's currently guaranteed that the macOS stat is in /usr/bin,
# so we use that explicitly.
set age (math (date +%s) - (/usr/bin/stat -f %m $whatis))
set age (path mtime -R -- $whatis)
end
MANPATH="$dir" apropos "^$argv"

View File

@ -14,7 +14,7 @@ function __fish_print_eopkg_packages
set -l cache_file $xdg_cache_home/.eopkg-installed-cache.$USER
if test -f $cache_file
cat $cache_file
set -l age (math (date +%s) - (stat -c '%Y' $cache_file))
set -l age (path mtime -R -- $cache_file)
set -l max_age 500
if test $age -lt $max_age
return 0
@ -28,7 +28,7 @@ function __fish_print_eopkg_packages
set -l cache_file $xdg_cache_home/.eopkg-available-cache.$USER
if test -f $cache_file
cat $cache_file
set -l age (math (date +%s) - (stat -c '%Y' $cache_file))
set -l age (path mtime -R -- $cache_file)
set -l max_age 500
if test $age -lt $max_age
return 0

View File

@ -12,7 +12,7 @@ function __fish_print_pacman_packages
set -l cache_file $xdg_cache_home/.pac-cache.$USER
if test -f $cache_file
cat $cache_file
set -l age (math (date +%s) - (stat -c '%Y' $cache_file))
set -l age (path mtime -R -- $cache_file)
set -l max_age 250
if test $age -lt $max_age
return

View File

@ -2,21 +2,19 @@ function __fish_print_port_packages
type -q -f port || return 1
# port needs caching, as it tends to be slow
# BSD find is used for determining file age because HFS+ and APFS
# don't save unix time, but the actual date. Also BSD stat is vastly
# different from linux stat and converting its time format is tedious
set -l xdg_cache_home (__fish_make_cache_dir)
or return
set -l cache_file $xdg_cache_home/.port-cache.$USER
if test -e $cache_file
# Delete if cache is older than 15 minutes
find "$cache_file" -ctime +15m | awk '{$1=$1;print}' | xargs rm
if test -f $cache_file
cat $cache_file
if test -f $cache_file
cat $cache_file
set -l age (path mtime -R -- $cache_file)
set -l max_age 250
if test $age -lt $max_age
return
end
end
# Remove trailing whitespace and pipe into cache file
printf "all\ncurrent\nactive\ninactive\ninstalled\nuninstalled\noutdated" >$cache_file
port echo all | awk '{$1=$1};1' >>$cache_file &

View File

@ -8,20 +8,12 @@ function __fish_print_rpm_packages
set -l xdg_cache_home (__fish_make_cache_dir)
or return
set -l fmt_mtime (
if stat --version 2>/dev/null >/dev/null
echo -- -c%Y # GNU
else
echo -- -f%m # BSD
end
)
if type -q -f /usr/share/yum-cli/completion-helper.py
# If the cache is less than six hours old, we do not recalculate it
set -l cache_file $xdg_cache_home/.yum-cache.$USER
if test -f $cache_file
cat $cache_file
set -l age (math (date +%s) - (stat $fmt_mtime $cache_file))
set -l age (path mtime -R -- $cache_file)
set -l max_age 21600
if test $age -lt $max_age
return
@ -40,7 +32,7 @@ function __fish_print_rpm_packages
set -l cache_file $xdg_cache_home/.rpm-cache.$USER
if test -f $cache_file
cat $cache_file
set -l age (math (date +%s) - (stat $fmt_mtime $cache_file))
set -l age (path mtime -R -- $cache_file)
set -l max_age 250
if test $age -lt $max_age
return

View File

@ -11,7 +11,7 @@ function __fish_print_xbps_packages
if not set -q _flag_installed
set -l cache_file $xdg_cache_home/.xbps-cache.$USER
if test -f $cache_file
set -l age (math (date +%s) - (stat -c '%Y' $cache_file))
set -l age (path mtime -R -- $cache_file)
set -l max_age 300
if test $age -lt $max_age
cat $cache_file

View File

@ -7,6 +7,7 @@
#include <sys/types.h>
#include <unistd.h>
#include <ctime>
#include <string>
#include <utility>
#include <vector>
@ -159,6 +160,7 @@ struct options_t { //!OCLINT(too many fields)
bool perm_valid = false;
bool type_valid = false;
bool invert_valid = false;
bool relative_valid = false;
bool reverse_valid = false;
bool key_valid = false;
bool unique_valid = false;
@ -179,6 +181,7 @@ struct options_t { //!OCLINT(too many fields)
path_perm_flags_t perm = 0;
bool invert = false;
bool relative = false;
bool reverse = false;
const wchar_t *arg1 = nullptr;
@ -307,6 +310,16 @@ static int handle_flag_perms(const wchar_t **argv, parser_t &parser, io_streams_
return STATUS_INVALID_ARGS;
}
static int handle_flag_R(const wchar_t **argv, parser_t &parser, io_streams_t &streams,
const wgetopter_t &w, options_t *opts) {
if (opts->relative_valid) {
opts->relative = true;
return STATUS_CMD_OK;
}
path_unknown_option(parser, streams, argv[0], argv[w.woptind - 1]);
return STATUS_INVALID_ARGS;
}
static int handle_flag_r(const wchar_t **argv, parser_t &parser, io_streams_t &streams,
const wgetopter_t &w, options_t *opts) {
if (opts->reverse_valid) {
@ -398,6 +411,7 @@ static wcstring construct_short_opts(options_t *opts) { //!OCLINT(high npath co
short_opts.append(L"fld");
}
if (opts->invert_valid) short_opts.append(L"v");
if (opts->relative_valid) short_opts.append(L"R");
if (opts->reverse_valid) short_opts.append(L"r");
if (opts->unique_valid) short_opts.append(L"u");
return short_opts;
@ -413,6 +427,7 @@ static const struct woption long_options[] = {
{L"perm", required_argument, nullptr, 'p'},
{L"type", required_argument, nullptr, 't'},
{L"invert", no_argument, nullptr, 'v'},
{L"relative", no_argument, nullptr, 'R'},
{L"reverse", no_argument, nullptr, 'r'},
{L"unique", no_argument, nullptr, 'u'},
{L"key", required_argument, nullptr, 1},
@ -427,6 +442,7 @@ static const std::unordered_map<char, decltype(*handle_flag_q)> flag_to_function
{'l', handle_flag_l}, {'d', handle_flag_d},
{'l', handle_flag_l}, {'d', handle_flag_d},
{'u', handle_flag_u}, {1, handle_flag_key},
{'R', handle_flag_R},
};
/// Parse the arguments for flags recognized by a specific string subcommand.
@ -586,6 +602,36 @@ static bool filter_path(options_t opts, const wcstring &path) {
return true;
}
static int path_mtime(parser_t &parser, io_streams_t &streams, int argc, const wchar_t **argv) {
options_t opts;
opts.relative_valid = true;
int optind;
int retval = parse_opts(&opts, &optind, 0, argc, argv, parser, streams);
if (retval != STATUS_CMD_OK) return retval;
int n_transformed = 0;
time_t t = std::time(nullptr);
arg_iterator_t aiter(argv, optind, streams, opts.null_in);
while (const wcstring *arg = aiter.nextstr()) {
auto ret = file_id_for_path(*arg);
if (ret != kInvalidFileID) {
if (opts.quiet) return STATUS_CMD_OK;
n_transformed++;
if (!opts.relative) {
path_out(streams, opts, to_string(ret.change_seconds));
} else {
path_out(streams, opts, to_string(t - ret.change_seconds));
}
}
}
return n_transformed > 0 ? STATUS_CMD_OK : STATUS_CMD_ERROR;
}
static int path_normalize(parser_t &parser, io_streams_t &streams, int argc, const wchar_t **argv) {
return path_transform(parser, streams, argc, argv, normalize_helper);
}
@ -868,6 +914,7 @@ static constexpr const struct path_subcommand {
{L"extension", &path_extension},
{L"filter", &path_filter},
{L"is", &path_is},
{L"mtime", &path_mtime},
{L"normalize", &path_normalize},
{L"resolve", &path_resolve},
{L"sort", &path_sort},

View File

@ -198,3 +198,21 @@ test (path resolve link) = (pwd -P)/link
and echo link resolves to link
# CHECK: link resolves to link
# path mtime
# These tests deal with *time*, so we have to account
# for slow systems (like CI).
# So we should only test with a lot of slack.
echo bananana >> foo
test (math abs (date +%s) - (path mtime foo)) -lt 20
or echo MTIME IS BOGUS
sleep 2
set -l mtime (path mtime --relative foo)
test $mtime -ge 1
or echo mtime is too small
test $mtime -lt 20
or echo mtime is too large