mirror of
https://github.com/SerenityOS/serenity.git
synced 2025-01-23 09:51:57 -05:00
touch: Support custom timestamps
Previously, `touch` remained limited to creating files and updating their current access and modifications time to the current time. It's now capable of accepting two different timestamp formats with flags `-d` and `-t` and referencing timestamps of other files with flag `-r`. `touch` can also update only the last access time with `-a`, only the last access time with `-m`, or update both as usual. `touch` updates both left unspecified. With `-c`, `touch` does not create a file if it doesn't already exist.
This commit is contained in:
parent
244357472a
commit
a2c34554cd
1 changed files with 216 additions and 16 deletions
|
@ -1,51 +1,251 @@
|
|||
/*
|
||||
* Copyright (c) 2018-2021, Andreas Kling <kling@serenityos.org>
|
||||
* Copyright (c) 2022, Ariel Don <ariel@arieldon.com>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <AK/CheckedFormatString.h>
|
||||
#include <AK/GenericLexer.h>
|
||||
#include <AK/Time.h>
|
||||
#include <LibCore/ArgsParser.h>
|
||||
#include <LibCore/Stream.h>
|
||||
#include <LibCore/System.h>
|
||||
#include <LibMain/Main.h>
|
||||
#include <LibTimeZone/TimeZone.h>
|
||||
#include <ctype.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <sys/stat.h>
|
||||
#include <time.h>
|
||||
|
||||
static bool file_exists(String const& path)
|
||||
static String program_name;
|
||||
|
||||
template<typename... Parameters>
|
||||
[[noreturn]] static void err(CheckedFormatString<Parameters...>&& fmtstr, Parameters const&... parameters)
|
||||
{
|
||||
struct stat st;
|
||||
int rc = stat(path.characters(), &st);
|
||||
if (rc < 0) {
|
||||
if (errno == ENOENT)
|
||||
return false;
|
||||
}
|
||||
if (rc == 0) {
|
||||
return true;
|
||||
}
|
||||
perror("stat");
|
||||
warn("{}: ", program_name);
|
||||
warnln(move(fmtstr), parameters...);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
inline static bool validate_timestamp(unsigned year, unsigned month, unsigned day, unsigned hour, unsigned minute, unsigned second)
|
||||
{
|
||||
return (year >= 1970) && (month >= 1 && month <= 12) && (day >= 1 && day <= static_cast<unsigned>(days_in_month(year, month))) && (hour <= 23) && (minute <= 59) && (second <= 59);
|
||||
}
|
||||
|
||||
static void parse_time(StringView input_time, timespec& atime, timespec& mtime)
|
||||
{
|
||||
// Parse [[CC]YY]MMDDhhmm[.SS] format, where brackets signify optional
|
||||
// parameters.
|
||||
if (input_time.length() < 8)
|
||||
err("invalid time format '{}' -- too short", input_time);
|
||||
else if (input_time.length() > 15)
|
||||
err("invalid time format '{}' -- too long", input_time);
|
||||
|
||||
Vector<u8> parameters;
|
||||
GenericLexer lexer(input_time);
|
||||
unsigned year, month, day, hour, minute, second;
|
||||
|
||||
auto lex_number = [&]() {
|
||||
auto literal = lexer.consume(2);
|
||||
if (literal.length() < 2)
|
||||
err("invalid time format '{}' -- expected 2 digits per parameter", input_time);
|
||||
auto maybe_parameter = literal.to_uint();
|
||||
if (maybe_parameter.has_value())
|
||||
parameters.append(maybe_parameter.value());
|
||||
else
|
||||
err("invalid time format '{}'", input_time);
|
||||
};
|
||||
|
||||
while (!lexer.is_eof() && lexer.next_is(isdigit))
|
||||
lex_number();
|
||||
if (parameters.size() > 6)
|
||||
err("invalid time format '{}' -- too many parameters", input_time);
|
||||
|
||||
if (lexer.consume_specific('.')) {
|
||||
lex_number();
|
||||
second = parameters.take_last();
|
||||
} else {
|
||||
second = 0;
|
||||
}
|
||||
|
||||
auto current_year = seconds_since_epoch_to_year(time(nullptr));
|
||||
auto current_century = current_year / 100;
|
||||
if (parameters.size() == 6)
|
||||
year = parameters.take_first() * 100 + parameters.take_first();
|
||||
else if (parameters.size() == 5)
|
||||
year = current_century * 100 + parameters.take_first();
|
||||
else
|
||||
year = current_year;
|
||||
|
||||
minute = parameters.take_last();
|
||||
hour = parameters.take_last();
|
||||
day = parameters.take_last();
|
||||
month = parameters.take_last();
|
||||
|
||||
if (validate_timestamp(year, month, day, hour, minute, second))
|
||||
atime = mtime = AK::Time::from_timestamp(year, month, day, hour, minute, second, 0).to_timespec();
|
||||
else
|
||||
err("invalid time format '{}'", input_time);
|
||||
}
|
||||
|
||||
static void parse_datetime(StringView input_datetime, timespec& atime, timespec& mtime)
|
||||
{
|
||||
// Parse YYYY-MM-DDThh:mm:SS[.frac][tz] or YYYY-MM-DDThh:mm:SS[,frac][tz]
|
||||
// formats, where brackets signify optional parameters.
|
||||
GenericLexer lexer(input_datetime);
|
||||
unsigned year, month, day, hour, minute, second, millisecond;
|
||||
StringView time_zone;
|
||||
|
||||
auto lex_number = [&](unsigned& value, size_t n) {
|
||||
auto maybe_value = lexer.consume(n).to_uint();
|
||||
if (!maybe_value.has_value())
|
||||
err("invalid datetime format '{}' -- expected number at index {}", input_datetime, lexer.tell());
|
||||
else
|
||||
value = maybe_value.value();
|
||||
};
|
||||
|
||||
lex_number(year, 4);
|
||||
if (!lexer.consume_specific('-'))
|
||||
err("invalid datetime format '{}' -- expected '-' after year", input_datetime);
|
||||
lex_number(month, 2);
|
||||
if (!lexer.consume_specific('-'))
|
||||
err("invalid datetime format '{}' -- expected '-' after month", input_datetime);
|
||||
lex_number(day, 2);
|
||||
|
||||
// Parse the time designator -- a single 'T' or ' ' according to POSIX.
|
||||
if (!lexer.consume_specific('T') && !lexer.consume_specific(' '))
|
||||
err("invalid datetime format '{}' -- expected 'T' or ' ' for time designator", input_datetime);
|
||||
|
||||
lex_number(hour, 2);
|
||||
if (!lexer.consume_specific(':'))
|
||||
err("invalid datetime format '{}' -- expected ':' after hour", input_datetime);
|
||||
lex_number(minute, 2);
|
||||
if (!lexer.consume_specific(':'))
|
||||
err("invalid datetime format '{}' -- expected ':' after minute", input_datetime);
|
||||
lex_number(second, 2);
|
||||
|
||||
millisecond = 0;
|
||||
if (!lexer.is_eof()) {
|
||||
if (lexer.consume_specific(',') || lexer.consume_specific('.')) {
|
||||
auto fractional_second = lexer.consume_while(isdigit);
|
||||
if (fractional_second.is_empty())
|
||||
err("invalid datetime format '{}' -- expected floating seconds", input_datetime);
|
||||
for (u8 i = 0; i < 3 && i < fractional_second.length(); ++i) {
|
||||
unsigned n = fractional_second[i] - '0';
|
||||
switch (i) {
|
||||
case 0:
|
||||
millisecond += 100 * n;
|
||||
break;
|
||||
case 1:
|
||||
millisecond += 10 * n;
|
||||
break;
|
||||
case 2:
|
||||
millisecond += n;
|
||||
break;
|
||||
default:
|
||||
VERIFY_NOT_REACHED();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
time_zone = lexer.consume_all();
|
||||
if (!time_zone.is_empty() && time_zone != "Z")
|
||||
err("invalid datetime format '{}' -- failed to parse time zone", input_datetime);
|
||||
}
|
||||
|
||||
if (validate_timestamp(year, month, day, hour, minute, second)) {
|
||||
auto timestamp = AK::Time::from_timestamp(year, month, day, hour, minute, second, millisecond);
|
||||
auto time = timestamp.to_timespec();
|
||||
if (time_zone.is_empty() && TimeZone::system_time_zone() != "UTC") {
|
||||
auto offset = TimeZone::get_time_zone_offset(TimeZone::system_time_zone(), timestamp);
|
||||
if (offset.has_value())
|
||||
time.tv_sec -= offset.value().seconds;
|
||||
else
|
||||
err("failed to get the system time zone");
|
||||
}
|
||||
atime = mtime = time;
|
||||
} else {
|
||||
err("invalid datetime format '{}'", input_datetime);
|
||||
}
|
||||
}
|
||||
|
||||
static void reference_time(StringView reference_path, timespec& atime, timespec& mtime)
|
||||
{
|
||||
auto maybe_buffer = Core::System::stat(reference_path);
|
||||
if (maybe_buffer.is_error())
|
||||
err("failed to reference times of '{}': {}", reference_path, maybe_buffer.release_error());
|
||||
auto buffer = maybe_buffer.release_value();
|
||||
atime.tv_sec = buffer.st_atime;
|
||||
atime.tv_nsec = buffer.st_atim.tv_nsec;
|
||||
mtime.tv_sec = buffer.st_mtime;
|
||||
mtime.tv_nsec = buffer.st_mtim.tv_nsec;
|
||||
}
|
||||
|
||||
ErrorOr<int> serenity_main(Main::Arguments arguments)
|
||||
{
|
||||
TRY(Core::System::pledge("stdio rpath cpath fattr"));
|
||||
|
||||
program_name = arguments.strings[0];
|
||||
|
||||
Vector<String> paths;
|
||||
|
||||
timespec times[2];
|
||||
auto& atime = times[0];
|
||||
auto& mtime = times[1];
|
||||
|
||||
bool update_atime = false;
|
||||
bool update_mtime = false;
|
||||
bool no_create_file = false;
|
||||
|
||||
String input_datetime = "";
|
||||
String input_time = "";
|
||||
String reference_path = "";
|
||||
|
||||
Core::ArgsParser args_parser;
|
||||
args_parser.set_general_help("Create a file, or update its mtime (time of last modification).");
|
||||
args_parser.set_general_help("Create a file or update file access time and/or modification time.");
|
||||
args_parser.add_ignored(nullptr, 'f');
|
||||
args_parser.add_option(update_atime, "Change access time of file", nullptr, 'a');
|
||||
args_parser.add_option(no_create_file, "Do not create a file if it does not exist", nullptr, 'c');
|
||||
args_parser.add_option(update_mtime, "Change modification time of file", nullptr, 'm');
|
||||
args_parser.add_option(input_datetime, "Use specified datetime instead of current time", nullptr, 'd', "datetime");
|
||||
args_parser.add_option(input_time, "Use specified time instead of current time", nullptr, 't', "time");
|
||||
args_parser.add_option(reference_path, "Use time of file specified by reference path instead of current time", nullptr, 'r', "reference");
|
||||
args_parser.add_positional_argument(paths, "Files to touch", "path", Core::ArgsParser::Required::Yes);
|
||||
args_parser.parse(arguments);
|
||||
|
||||
if (input_datetime.is_empty() + input_time.is_empty() + reference_path.is_empty() < 2)
|
||||
err("cannot specify a time with more than one option");
|
||||
|
||||
if (!input_datetime.is_empty())
|
||||
parse_datetime(input_datetime, atime, mtime);
|
||||
else if (!input_time.is_empty())
|
||||
parse_time(input_time, atime, mtime);
|
||||
else if (!reference_path.is_empty())
|
||||
reference_time(reference_path, atime, mtime);
|
||||
else
|
||||
atime.tv_nsec = mtime.tv_nsec = UTIME_NOW;
|
||||
|
||||
// According to POSIX, if neither -a nor -m are specified, then the program
|
||||
// should behave as if both are specified.
|
||||
if (!update_atime && !update_mtime)
|
||||
update_atime = update_mtime = true;
|
||||
if (update_atime && !update_mtime)
|
||||
mtime.tv_nsec = UTIME_OMIT;
|
||||
if (update_mtime && !update_atime)
|
||||
atime.tv_nsec = UTIME_OMIT;
|
||||
|
||||
for (auto path : paths) {
|
||||
if (file_exists(path)) {
|
||||
if (auto result = Core::System::utime(path, {}); result.is_error())
|
||||
warnln("{}", result.release_error());
|
||||
} else {
|
||||
if (Core::Stream::File::exists(path)) {
|
||||
if (utimensat(AT_FDCWD, path.characters(), times, 0) == -1)
|
||||
err("failed to touch '{}': {}", path, strerror(errno));
|
||||
} else if (!no_create_file) {
|
||||
int fd = TRY(Core::System::open(path, O_CREAT, 0100644));
|
||||
if (futimens(fd, times) == -1)
|
||||
err("failed to touch '{}': {}", path, strerror(errno));
|
||||
TRY(Core::System::close(fd));
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue