diff --git a/doc_src/cmds/path.rst b/doc_src/cmds/path.rst index 4a5da7cb1..52bacdac0 100644 --- a/doc_src/cmds/path.rst +++ b/doc_src/cmds/path.rst @@ -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 `. + +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 ----------------------- diff --git a/share/completions/path.fish b/share/completions/path.fish index 79ea5ebbc..1d34bfc39 100644 --- a/share/completions/path.fish +++ b/share/completions/path.fish @@ -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' diff --git a/share/functions/__fish_apropos.fish b/share/functions/__fish_apropos.fish index 16c90bb36..c93827237 100644 --- a/share/functions/__fish_apropos.fish +++ b/share/functions/__fish_apropos.fish @@ -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" diff --git a/share/functions/__fish_print_eopkg_packages.fish b/share/functions/__fish_print_eopkg_packages.fish index 19ff348d2..1fe9c5f32 100644 --- a/share/functions/__fish_print_eopkg_packages.fish +++ b/share/functions/__fish_print_eopkg_packages.fish @@ -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 diff --git a/share/functions/__fish_print_pacman_packages.fish b/share/functions/__fish_print_pacman_packages.fish index 194e375e1..85c0772fd 100644 --- a/share/functions/__fish_print_pacman_packages.fish +++ b/share/functions/__fish_print_pacman_packages.fish @@ -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 diff --git a/share/functions/__fish_print_port_packages.fish b/share/functions/__fish_print_port_packages.fish index ea3ed4b42..d42c70595 100644 --- a/share/functions/__fish_print_port_packages.fish +++ b/share/functions/__fish_print_port_packages.fish @@ -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 & diff --git a/share/functions/__fish_print_rpm_packages.fish b/share/functions/__fish_print_rpm_packages.fish index e10194361..b802a8277 100644 --- a/share/functions/__fish_print_rpm_packages.fish +++ b/share/functions/__fish_print_rpm_packages.fish @@ -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 diff --git a/share/functions/__fish_print_xbps_packages.fish b/share/functions/__fish_print_xbps_packages.fish index 82af7ae88..16234d252 100644 --- a/share/functions/__fish_print_xbps_packages.fish +++ b/share/functions/__fish_print_xbps_packages.fish @@ -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 diff --git a/src/builtins/path.cpp b/src/builtins/path.cpp index e9482560c..b4b3c58ae 100644 --- a/src/builtins/path.cpp +++ b/src/builtins/path.cpp @@ -7,6 +7,7 @@ #include #include +#include #include #include #include @@ -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 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}, diff --git a/tests/checks/path.fish b/tests/checks/path.fish index 410e12d0b..79c1a5dd2 100644 --- a/tests/checks/path.fish +++ b/tests/checks/path.fish @@ -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