writeout: add %time{}

Output the current UTC time using strftime format. %f is an extra curl
specific flag to output the microsecond fraction of the current second.

Verified by test 1981

Closes #18119
This commit is contained in:
Daniel Stenberg 2025-07-31 16:41:36 +02:00
parent 5b80b4c012
commit fadc487567
No known key found for this signature in database
GPG key ID: 5CC908FDB71E12C2
5 changed files with 340 additions and 3 deletions

View file

@ -220,6 +220,10 @@ From this point on, the --write-out output is written to standard output.
This is the default, but can be used to switch back after switching to stderr. This is the default, but can be used to switch back after switching to stderr.
(Added in 7.63.0) (Added in 7.63.0)
## `time{format}`
Output the current UTC time using `strftime()` format. See TIME OUTPUT FORMAT
below for details. (Added in 8.16.0)
## `time_appconnect` ## `time_appconnect`
The time, in seconds, it took from the start until the SSL/SSH/etc The time, in seconds, it took from the start until the SSL/SSH/etc
connect/handshake to the remote host was completed. (Added in 7.19.0) connect/handshake to the remote host was completed. (Added in 7.19.0)
@ -347,3 +351,185 @@ The numerical identifier of the last transfer done. -1 if no transfer has been
started yet for the handle. The transfer id is unique among all transfers started yet for the handle. The transfer id is unique among all transfers
performed using the same connection cache. performed using the same connection cache.
(Added in 8.2.0) (Added in 8.2.0)
##
TIME OUTPUT FORMAT
When showing time with `%time{}`, the following output qualifiers are
available:
## `%a`
The abbreviated name of the day of the week according to the current locale.
## `%A`
The full name of the day of the week according to the current locale.
## `%b`
The abbreviated month name according to the current locale.
## `%B`
The full month name according to the current locale.
## `%c`
The preferred date and time representation for the current locale. (In the
POSIX locale this is equivalent to %a %b %e %H:%M:%S %Y.)
## `%C`
The century number (year/100) as a 2-digit integer.
## `%d`
The day of the month as a decimal number (range 01 to 31).
## `%D`
Equivalent to %m/%d/%y. In international contexts, this format is ambiguous
and should be avoided.)
## `%e`
Like %d, the day of the month as a decimal number, but a leading zero is
replaced by a space.
## `%f`
The number of microseconds elapsed of the current second. (This a curl special
code and not a standard one.)
## `%F`
Equivalent to %Y-%m-%d (the ISO 8601 date format).
## `%G`
The ISO 8601 week-based year with century as a decimal number. The 4-digit
year corresponding to the ISO week number (see %V). This has the same format
and value as %Y, except that if the ISO week number belongs to the previous or
next year, that year is used instead.
## `%g`
Like `%G`, but without century, that is, with a 2-digit year (00-99).
## `%h`
Equivalent to `%b`.
## `%H`
The hour as a decimal number using a 24-hour clock (range 00 to 23).
## `%I`
The hour as a decimal number using a 12-hour clock (range 01 to 12).
## `%j`
The day of the year as a decimal number (range 001 to 366).
## `%k`
The hour (24-hour clock) as a decimal number (range 0 to 23); single digits
are preceded by a blank.
## `%l`
The hour (12-hour clock) as a decimal number (range 1 to 12); single digits
are preceded by a blank.
## `%m`
The month as a decimal number (range 01 to 12).
## `%M`
The minute as a decimal number (range 00 to 59).
## `%p`
Either "AM" or "PM" according to the given time value, or the corresponding
strings for the current locale. Noon is treated as "PM" and midnight as "AM".
## `%P`
Like %p but in lowercase: "am" or "pm" or a corresponding string for the
current locale.
## `%r`
The time in am or pm notation.
## `%R`
The time in 24-hour notation (%H:%M). For a version including the seconds, see
`%T` below.
## `%s`
The number of seconds since the Epoch, 1970-01-01 00:00:00 +0000 (UTC).
## `%S`
The second as a decimal number (range 00 to 60). (The range is up to 60 to
allow for occasional leap seconds.)
## `%T`
The time in 24-hour notation (%H:%M:%S).
## `%u`
The day of the week as a decimal, range 1 to 7, Monday being 1.
## `%U`
The week number of the current year as a decimal number, range 00 to 53,
starting with the first Sunday as the first day of week 01. See also `%V` and
`%W`.
## `%V`
The ISO 8601 week number (see NOTES) of the current year as a decimal number,
range 01 to 53, where week 1 is the first week that has at least 4 days in the
new year. See also `%U` and `%W`.
## `%w`
The day of the week as a decimal, range 0 to 6, Sunday being 0. See also `%u`.
## `%W`
The week number of the current year as a decimal number, range 00 to 53,
starting with the first Monday as the first day of week 01.
## `%x`
The preferred date representation for the current locale without the time.
## `%X`
The preferred time representation for the current locale without the date.
## `%y`
The year as a decimal number without a century (range 00 to 99).
## `%Y`
The year as a decimal number including the century.
## `%z`
The `+hhmm` or `-hhmm` numeric timezone (that is, the hour and minute offset
from UTC). As time is always UTC, this outputs `+0000`.
## `%Z`
The timezone name. For some reason `GMT`.

View file

@ -539,6 +539,81 @@ matchvar(const void *m1, const void *m2)
#define MAX_WRITEOUT_NAME_LENGTH 24 #define MAX_WRITEOUT_NAME_LENGTH 24
/* return the position after %time{} */
static const char *outtime(const char *ptr, /* %time{ ... */
FILE *stream)
{
const char *end;
ptr += 6;
end = strchr(ptr, '}');
if(end) {
struct tm *utc;
struct dynbuf format;
char output[256]; /* max output time length */
#ifdef HAVE_GETTIMEOFDAY
struct timeval cnow;
#else
struct curltime cnow;
#endif
time_t secs;
unsigned int usecs;
size_t i;
size_t vlen;
CURLcode result = CURLE_OK;
#ifdef HAVE_GETTIMEOFDAY
gettimeofday(&cnow, NULL);
#else
cnow.tv_sec = time(NULL);
cnow.tv_usec = 0;
#endif
secs = cnow.tv_sec;
usecs = (unsigned int)cnow.tv_usec;
#ifdef DEBUGBUILD
{
const char *timestr = getenv("CURL_TIME");
if(timestr) {
curl_off_t val;
curlx_str_number(&timestr, &val, TIME_T_MAX);
secs = (time_t)val;
usecs = (unsigned int)(val % 1000000);
}
}
#endif
vlen = end - ptr;
curlx_dyn_init(&format, 1024);
/* insert sub-seconds for %f */
/* insert +0000 for %z because it is otherwise not portable */
/* insert UTC for %Z because it is otherwise not portable */
for(i = 0; !result && i < vlen; i++) {
if((i < vlen - 1) && ptr[i] == '%' &&
((ptr[i + 1] == 'f') || ((ptr[i + 1] | 0x20) == 'z'))) {
if(ptr[i + 1] == 'f')
result = curlx_dyn_addf(&format, "%06u", usecs);
else if(ptr[i + 1] == 'Z')
result = curlx_dyn_addn(&format, "UTC", 3);
else
result = curlx_dyn_addn(&format, "+0000", 5);
i++;
}
else
result = curlx_dyn_addn(&format, &ptr[i], 1);
}
if(!result) {
/* !checksrc! disable BANNEDFUNC 1 */
utc = gmtime(&secs);
strftime(output, sizeof(output), curlx_dyn_ptr(&format), utc);
fputs(output, stream);
curlx_dyn_free(&format);
}
ptr = end + 1;
}
else
fputs("%time{", stream);
return ptr;
}
void ourWriteOut(struct OperationConfig *config, struct per_transfer *per, void ourWriteOut(struct OperationConfig *config, struct per_transfer *per,
CURLcode per_result) CURLcode per_result)
{ {
@ -640,6 +715,9 @@ void ourWriteOut(struct OperationConfig *config, struct per_transfer *per,
else else
fputs("%header{", stream); fputs("%header{", stream);
} }
else if(!strncmp("time{", &ptr[1], 5)) {
ptr = outtime(ptr, stream);
}
else if(!strncmp("output{", &ptr[1], 7)) { else if(!strncmp("output{", &ptr[1], 7)) {
bool append = FALSE; bool append = FALSE;
ptr += 8; ptr += 8;

View file

@ -238,7 +238,7 @@ test1933 test1934 test1935 test1936 test1937 test1938 test1939 test1940 \
test1941 test1942 test1943 test1944 test1945 test1946 test1947 test1948 \ test1941 test1942 test1943 test1944 test1945 test1946 test1947 test1948 \
test1955 test1956 test1957 test1958 test1959 test1960 test1964 \ test1955 test1956 test1957 test1958 test1959 test1960 test1964 \
test1970 test1971 test1972 test1973 test1974 test1975 test1976 test1977 \ test1970 test1971 test1972 test1973 test1974 test1975 test1976 test1977 \
test1978 test1979 test1980 \ test1978 test1979 test1980 test1981 \
\ \
test2000 test2001 test2002 test2003 test2004 test2005 \ test2000 test2001 test2002 test2003 test2004 test2005 \
\ \

62
tests/data/test1981 Normal file
View file

@ -0,0 +1,62 @@
<testcase>
<info>
<keywords>
HTTP
HTTP GET
</keywords>
</info>
#
# Server-side
<reply>
<data crlf="yes" nocheck="yes">
HTTP/1.1 200 OK
Date: Tue, 09 Nov 2010 14:49:00 GMT
Server: test-server/fake
Last-Modified: Tue, 13 Jun 2000 12:10:00 GMT
ETag: "21025-dc7-39462498"
Accept-Ranges: bytes
Content-Length: 6
Connection: close
Content-Type: text/html
Funny-head: yesyes
-foo-
</data>
</reply>
#
# Client-side
<client>
<server>
http
</server>
<name>
%time output with --write-out
</name>
<features>
Debug
</features>
<setenv>
CURL_TIME=1754037103
</setenv>
<command>
http://%HOSTIP:%HTTPPORT/%TESTNUMBER --write-out='Time: %time{%d/%b/%Y %H:%M:%S.%f %z %Z}\n' -s -o %LOGDIR/dump
</command>
</client>
#
# Verify data after the test has been "shot"
<verify>
<protocol crlf="yes">
GET /%TESTNUMBER HTTP/1.1
Host: %HOSTIP:%HTTPPORT
User-Agent: curl/%VERSION
Accept: */*
</protocol>
<stdout mode="text">
Time: 01/Aug/2025 08:31:43.037103 +0000 UTC
</stdout>
</verify>
</testcase>

View file

@ -54,11 +54,22 @@ sub getsrcvars {
close($f); close($f);
} }
my %special = (
'header{name}' => 1,
'output{filename}' => 1,
'time{format}' => 1,
);
sub getdocsvars { sub getdocsvars {
open(my $f, "<", "$root/../docs/cmdline-opts/write-out.md"); open(my $f, "<", "$root/../docs/cmdline-opts/write-out.md");
while(<$f>) { while(<$f>) {
if($_ =~ /^\#\# \`([^\`]*)\`/) { chomp;
if($1 ne "header{name}" && $1 ne "output{filename}") { $_ =~ s/[\r\n]//g;
if($_ =~ /^\#\# *\z/) {
last;
}
elsif($_ =~ /^\#\# \`([^\`]*)\`/) {
if(!$special{$1}) {
$indocs{$1} = 1; $indocs{$1} = 1;
} }
} }