diff options
| author | Mehmet Samet Duman <yongdohyun@projecttick.org> | 2026-04-02 18:24:20 +0300 |
|---|---|---|
| committer | Mehmet Samet Duman <yongdohyun@projecttick.org> | 2026-04-02 18:24:20 +0300 |
| commit | c800ffec456be2f8f346a3a3f50e1e5fa6ee2f0e (patch) | |
| tree | e9754c08c193d5706b9ce8b2f7d2aee2db98dad6 /corebinutils/date | |
| parent | d12e80797cf0ae7a0bd3cd0cd7948f532f3181ac (diff) | |
| parent | ae4c58645a13317bb8540d47f8f7cfa768f17eb2 (diff) | |
| download | Project-Tick-c800ffec456be2f8f346a3a3f50e1e5fa6ee2f0e.tar.gz Project-Tick-c800ffec456be2f8f346a3a3f50e1e5fa6ee2f0e.zip | |
Add 'corebinutils/date/' from commit 'ae4c58645a13317bb8540d47f8f7cfa768f17eb2'
git-subtree-dir: corebinutils/date
git-subtree-mainline: d12e80797cf0ae7a0bd3cd0cd7948f532f3181ac
git-subtree-split: ae4c58645a13317bb8540d47f8f7cfa768f17eb2
Diffstat (limited to 'corebinutils/date')
| -rw-r--r-- | corebinutils/date/.gitignore | 25 | ||||
| -rw-r--r-- | corebinutils/date/GNUmakefile | 38 | ||||
| -rw-r--r-- | corebinutils/date/LICENSE | 26 | ||||
| -rw-r--r-- | corebinutils/date/LICENSES/BSD-2-Clause.txt | 9 | ||||
| -rw-r--r-- | corebinutils/date/LICENSES/BSD-3-Clause.txt | 11 | ||||
| -rw-r--r-- | corebinutils/date/README.md | 27 | ||||
| -rw-r--r-- | corebinutils/date/date.1 | 660 | ||||
| -rw-r--r-- | corebinutils/date/date.c | 879 | ||||
| -rw-r--r-- | corebinutils/date/tests/test.sh | 94 | ||||
| -rw-r--r-- | corebinutils/date/vary.c | 560 | ||||
| -rw-r--r-- | corebinutils/date/vary.h | 39 |
11 files changed, 2368 insertions, 0 deletions
diff --git a/corebinutils/date/.gitignore b/corebinutils/date/.gitignore new file mode 100644 index 0000000000..a74d30b48c --- /dev/null +++ b/corebinutils/date/.gitignore @@ -0,0 +1,25 @@ +*.a +*.core +*.lo +*.nossppico +*.o +*.orig +*.pico +*.pieo +*.po +*.rej +*.so +*.so.[0-9]* +*.sw[nop] +*~ +.*DS_Store +.cache +.clangd +.ccls-cache +.depend* +compile_commands.json +compile_commands.events.json +tags +build/ +out/ +.linux-obj/ diff --git a/corebinutils/date/GNUmakefile b/corebinutils/date/GNUmakefile new file mode 100644 index 0000000000..1bc7285d14 --- /dev/null +++ b/corebinutils/date/GNUmakefile @@ -0,0 +1,38 @@ +.DEFAULT_GOAL := all + +CC ?= cc +CPPFLAGS += -D_POSIX_C_SOURCE=200809L -D_XOPEN_SOURCE=700 +CFLAGS ?= -O2 +CFLAGS += -std=c17 -g -Wall -Wextra -Wno-unused-parameter +LDFLAGS ?= +LDLIBS ?= + +OBJDIR := $(CURDIR)/build +OUTDIR := $(CURDIR)/out +TARGET := $(OUTDIR)/date +OBJS := $(OBJDIR)/date.o $(OBJDIR)/vary.o + +.PHONY: all clean dirs test status + +all: $(TARGET) + +dirs: + @mkdir -p "$(OBJDIR)" "$(OUTDIR)" + +$(TARGET): $(OBJS) | dirs + $(CC) $(LDFLAGS) -o "$@" $(OBJS) $(LDLIBS) + +$(OBJDIR)/date.o: $(CURDIR)/date.c $(CURDIR)/vary.h | dirs + $(CC) $(CPPFLAGS) $(CFLAGS) -c "$(CURDIR)/date.c" -o "$@" + +$(OBJDIR)/vary.o: $(CURDIR)/vary.c $(CURDIR)/vary.h | dirs + $(CC) $(CPPFLAGS) $(CFLAGS) -c "$(CURDIR)/vary.c" -o "$@" + +test: $(TARGET) + DATE_BIN="$(TARGET)" sh "$(CURDIR)/tests/test.sh" + +status: + @printf '%s\n' "$(TARGET)" + +clean: + @rm -rf "$(CURDIR)/build" "$(CURDIR)/out" diff --git a/corebinutils/date/LICENSE b/corebinutils/date/LICENSE new file mode 100644 index 0000000000..69717bf550 --- /dev/null +++ b/corebinutils/date/LICENSE @@ -0,0 +1,26 @@ +Copyright (c) 1985, 1987, 1988, 1993 + The Regents of the University of California. All rights reserved. + +Copyright (c) 2026 + Project Tick. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. diff --git a/corebinutils/date/LICENSES/BSD-2-Clause.txt b/corebinutils/date/LICENSES/BSD-2-Clause.txt new file mode 100644 index 0000000000..5f662b354c --- /dev/null +++ b/corebinutils/date/LICENSES/BSD-2-Clause.txt @@ -0,0 +1,9 @@ +Copyright (c) <year> <owner> + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/corebinutils/date/LICENSES/BSD-3-Clause.txt b/corebinutils/date/LICENSES/BSD-3-Clause.txt new file mode 100644 index 0000000000..ea890afbc7 --- /dev/null +++ b/corebinutils/date/LICENSES/BSD-3-Clause.txt @@ -0,0 +1,11 @@ +Copyright (c) <year> <owner>. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/corebinutils/date/README.md b/corebinutils/date/README.md new file mode 100644 index 0000000000..2a60b313e7 --- /dev/null +++ b/corebinutils/date/README.md @@ -0,0 +1,27 @@ +# date + +Standalone musl-libc-based Linux port of FreeBSD `date` for Project Tick BSD/Linux Distribution. + +## Build + +```sh +gmake -f GNUmakefile +gmake -f GNUmakefile CC=musl-gcc +``` + +## Test + +```sh +gmake -f GNUmakefile test +gmake -f GNUmakefile test CC=musl-gcc +``` + +## Notes + +- Port strategy is Linux-native syscall/API mapping, not a FreeBSD userland ABI shim. +- Time reads and writes use `clock_gettime(2)`, `clock_getres(2)`, and `clock_settime(2)`. +- `-r file` uses Linux `stat(2)` nanosecond timestamps via `st_mtim`. +- Time zone selection uses the libc `TZ` mechanism (`setenv("TZ", ...)` + `tzset()`), so named zones depend on installed tzdata and POSIX `TZ` strings work without glibc-specific behavior. +- `%N` formatting, ISO-8601 rendering, and FreeBSD `-v` adjustments are implemented in local project code so the port stays musl-clean. +- Unsupported semantics are explicit: FreeBSD `-n` is rejected on Linux because there is no equivalent timed/network-set path here. +- Setting the real-time clock still requires Linux `CAP_SYS_TIME`; the port does not emulate FreeBSD `utmpx`/syslog side effects when the clock changes. diff --git a/corebinutils/date/date.1 b/corebinutils/date/date.1 new file mode 100644 index 0000000000..21ec516718 --- /dev/null +++ b/corebinutils/date/date.1 @@ -0,0 +1,660 @@ +.\"- +.\" Copyright (c) 1980, 1990, 1993 +.\" The Regents of the University of California. All rights reserved. +.\" +.\" Copyright (c) 2026 +.\" Project Tick. All rights reserved. +.\" +.\" This code is derived from software contributed to Berkeley by +.\" the Institute of Electrical and Electronics Engineers, Inc. +.\" +.\" Redistribution and use in source and binary forms, with or without +.\" modification, are permitted provided that the following conditions +.\" are met: +.\" 1. Redistributions of source code must retain the above copyright +.\" notice, this list of conditions and the following disclaimer. +.\" 2. Redistributions in binary form must reproduce the above copyright +.\" notice, this list of conditions and the following disclaimer in the +.\" documentation and/or other materials provided with the distribution. +.\" 3. Neither the name of the University nor the names of its contributors +.\" may be used to endorse or promote products derived from this software +.\" without specific prior written permission. +.\" +.\" THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND +.\" ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +.\" IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +.\" ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE +.\" FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +.\" DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +.\" OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +.\" HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +.\" LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +.\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +.\" SUCH DAMAGE. +.\" +.Dd November 10, 2025 +.Dt DATE 1 +.Os +.Sh NAME +.Nm date +.Nd display or set date and time +.Sh SYNOPSIS +.\" Display time. +.Nm +.Op Fl nRu +.Op Fl z Ar output_zone +.Op Fl I Ns Op Ar FMT +.Op Fl r Ar filename +.Op Fl r Ar seconds +.Oo +.Sm off +.Fl v +.Op Cm + | - +.Ar val Op Cm y | m | w | d | H | M | S +.Sm on +.Oc +.Op Cm + Ns Ar output_fmt +.\" Set time with the default input format. +.Nm +.Op Fl jnRu +.Op Fl z Ar output_zone +.Op Fl I Ns Op Ar FMT +.Oo +.Sm off +.Fl v +.Op Cm + | - +.Ar val Op Cm y | m | w | d | H | M | S +.Sm on +.Oc +.Sm off +.Oo Oo Oo Oo Oo +.Ar cc Oc +.Ar yy Oc +.Ar mm Oc +.Ar dd Oc +.Ar HH +.Oc Ar MM Op Cm \&. Ar SS +.Sm on +.Op Cm + Ns Ar output_fmt +.\" Set time with the user-provided input format. +.Nm +.Op Fl jnRu +.Op Fl z Ar output_zone +.Op Fl I Ns Op Ar FMT +.Oo +.Sm off +.Fl v +.Op Cm + | - +.Ar val Op Cm y | m | w | d | H | M | S +.Sm on +.Oc +.Fl f Ar input_fmt +.Ar new_date +.Op Cm + Ns Ar output_fmt +.Sh DESCRIPTION +When invoked without arguments, the +.Nm +utility displays the current date and time. +Otherwise, depending on the options specified, +.Nm +will set the date and time or print it in a user-defined way. +.Pp +The +.Nm +utility displays the date and time read from the kernel clock. +When used to set the date and time, +both the kernel clock and the hardware clock are updated. +.Pp +Only the superuser may set the date, +and if the system securelevel (see +.Xr securelevel 7 ) +is greater than 1, +the time may not be changed by more than 1 second. +.Pp +The options are as follows: +.Bl -tag -width Ds +.It Fl f Ar input_fmt +Use +.Ar input_fmt +as the format string to parse the +.Ar new_date +provided rather than using the default +.Sm off +.Oo Oo Oo Oo Oo +.Ar cc Oc +.Ar yy Oc +.Ar mm Oc +.Ar dd Oc +.Ar HH +.Oc Ar MM Op Cm \&. Ar SS +.Sm on +format. +Parsing is done using +.Xr strptime 3 . +.It Fl I Ns Op Ar FMT +Use extended +.St -iso8601 +output format. +.Ar FMT +may be omitted, in which case the default is +.Cm date . +Valid +.Ar FMT +values are +.Cm date , +.Cm hours , +.Cm minutes , +.Cm seconds , +and +.Cm ns +.Pq for nanoseconds . +The date and time is formatted to the specified precision. +When +.Ar FMT +is +.Cm hours +.Po or the more precise +.Cm minutes , +.Cm seconds , +or +.Cm ns Pc , +the extended +.St -iso8601 +format includes the timezone offset. +.It Fl j +Do not try to set the date. +This allows you to use the +.Fl f +flag in addition to the +.Cm + +option to convert one date format to another. +Note that any date or time components unspecified by the +.Fl f +format string take their values from the current time. +.It Fl n +Obsolete flag, accepted and ignored for compatibility. +.It Fl R +Use RFC 2822 date and time output format. +This is equivalent to using +.Ql %a, %d %b %Y \&%T %z +as +.Ar output_fmt +while +.Ev LC_TIME +is set to the +.Dq C +locale . +.It Fl r Ar seconds +Print the date and time represented by +.Ar seconds , +where +.Ar seconds +is the number of seconds since the Unix Epoch +(00:00:00 UTC, January 1, 1970; +see +.Xr time 3 ) , +and can be specified in decimal, octal, or hex. +.It Fl r Ar filename +Print the date and time of the last modification of +.Ar filename . +.It Fl u +Display or set the date in UTC (Coordinated Universal) time. +By default +.Nm +displays the time in the time zone described by +.Pa /etc/localtime +or the +.Ev TZ +environment variable. +.It Fl z Ar output_zone +Just before printing the time, change to the specified timezone; +see the description of +.Ev TZ +below. +This can be used with +.Fl j +to easily convert time specifications from one zone to another. +.It Xo +.Fl v +.Sm off +.Op Cm + | - +.Ar val Op Cm y | m | w | d | H | M | S +.Sm on +.Xc +Adjust (i.e., take the current date and display the result of the +adjustment; not actually set the date) the second, minute, hour, month +day, week day, month or year according to +.Ar val . +If +.Ar val +is preceded by a plus or minus sign, +the date is adjusted forward or backward according to the remaining string, +otherwise the relevant part of the date is set. +The date can be adjusted as many times as required using these flags. +Flags are processed in the order given. +.Pp +When setting values +(rather than adjusting them), +seconds are in the range 0-59, minutes are in the range 0-59, hours are +in the range 0-23, month days are in the range 1-31, week days are in the +range 0-6 (Sun-Sat), +months are in the range 1-12 (Jan-Dec) +and years are in a limited range depending on the platform. +.Pp +On i386, years are in the range 69-38 representing 1969-2038. +On every other platform, years 0-68 are accepted and represent 2000-2068, and +69-99 are accepted and represent 1969-1999. +In both cases, years between 100 and 1900 (both included) are accepted and +interpreted as relative to 1900 of the Gregorian calendar with a limit of 138 on +i386 and a much higher limit on every other platform. +Years starting at 1901 are also accepted, and are interpreted as absolute years. +.Pp +If +.Ar val +is numeric, one of either +.Cm y , +.Cm m , +.Cm w , +.Cm d , +.Cm H , +.Cm M +or +.Cm S +must be used to specify which part of the date is to be adjusted. +.Pp +The week day or month may be specified using a name rather than a +number. +If a name is used with the plus +(or minus) +sign, the date will be put forwards +(or backwards) +to the next +(previous) +date that matches the given week day or month. +This will not adjust the date, +if the given week day or month is the same as the current one. +.Pp +When a date is adjusted to a specific value or in units greater than hours, +daylight savings time considerations are ignored. +Adjustments in units of hours or less honor daylight saving time. +So, assuming the current date is March 26, 0:30 and that the DST adjustment +means that the clock goes forward at 01:00 to 02:00, using +.Fl v No +1H +will adjust the date to March 26, 2:30. +Likewise, if the date is October 29, 0:30 and the DST adjustment means that +the clock goes back at 02:00 to 01:00, using +.Fl v No +3H +will be necessary to reach October 29, 2:30. +.Pp +When the date is adjusted to a specific value that does not actually exist +(for example March 26, 1:30 BST 2000 in the Europe/London timezone), +the date will be silently adjusted forward in units of one hour until it +reaches a valid time. +When the date is adjusted to a specific value that occurs twice +(for example October 29, 1:30 2000), +the resulting timezone will be set so that the date matches the earlier of +the two times. +.Pp +It is not possible to adjust a date to an invalid absolute day, so using +the switches +.Fl v No 31d Fl v No 12m +will simply fail five months of the year. +It is therefore usual to set the month before setting the day; using +.Fl v No 12m Fl v No 31d +always works. +.Pp +Adjusting the date by months is inherently ambiguous because +a month is a unit of variable length depending on the current date. +This kind of date adjustment is applied in the most intuitive way. +First of all, +.Nm +tries to preserve the day of the month. +If it is impossible because the target month is shorter than the present one, +the last day of the target month will be the result. +For example, using +.Fl v No +1m +on May 31 will adjust the date to June 30, while using the same option +on January 30 will result in the date adjusted to the last day of February. +This approach is also believed to make the most sense for shell scripting. +Nevertheless, be aware that going forth and back by the same number of +months may take you to a different date. +.Pp +Refer to the examples below for further details. +.El +.Pp +An operand with a leading plus +.Pq Sq + +sign specifies a user-defined format string +which specifies the format in which to display the date and time. +The format string may contain any of the conversion specifications +described in the +.Xr strftime 3 +manual page, as well as any arbitrary text. +.Pp +The following extensions to the regular +.Xr strftime 3 +syntax are supported: +.Bl -tag -width "xxxx" +.It Cm \&% Ns Ar n Ns Cm N +Replaced by the +.Ar n Ns +-digit fractional part of the number of seconds since the Unix Epoch. +If +.Ar n +is omitted or zero, a default value of 9 is used, resulting in a +number with nanosecond resolution (hence the choice of the letter +.Sq N +for this conversion). +Note that the underlying clock may not necessarily support nanosecond +resolution. +.It Cm \&%-N +As above, but automatically choose the precision based on the reported +resolution of the underlying clock. +If the +.Fl r +option was specified, the default precision of 9 digits is used. +.El +.Pp +A newline +.Pq Ql \en +character is always output after the characters specified by +the format string. +The format string for the default display is +.Dq %+ . +.Pp +If an operand does not have a leading plus sign, it is interpreted as +a value for setting the system's notion of the current date and time. +The canonical representation for setting the date and time is: +.Pp +.Bl -tag -width Ds -compact -offset indent +.It Ar cc +Century +(either 19 or 20) +prepended to the abbreviated year. +.It Ar yy +Year in abbreviated form +(e.g., 89 for 1989, 06 for 2006). +.It Ar mm +Numeric month, a number from 1 to 12. +.It Ar dd +Day, a number from 1 to 31. +.It Ar HH +Hour, a number from 0 to 23. +.It Ar MM +Minutes, a number from 0 to 59. +.It Ar SS +Seconds, a number from 0 to 60 +(59 plus a potential leap second). +.El +.Pp +Everything but the minutes is optional. +.Pp +.Nm +understands the time zone definitions from the IANA Time Zone Database, +.Sy tzdata , +located in +.Pa /usr/share/zoneinfo . +Time changes for Daylight Saving Time, standard time, leap seconds +and leap years are handled automatically. +.Pp +There are two ways to specify the time zone: +.Pp +If the file or symlink +.Pa /etc/localtime +exists, it is interpreted as a time zone definition file, usually in +the directory hierarchy +.Pa /usr/share/zoneinfo , +which contains the time zone definitions from +.Sy tzdata . +.Pp +If the environment variable +.Ev TZ +is set, its value is interpreted as the name of a time zone definition +file, either an absolute path or a relative path to a time zone +definition in +.Pa /usr/share/zoneinfo . +The +.Ev TZ +variable overrides +.Pa /etc/localtime . +.Pp +If the time zone definition file is invalid, +.Nm +silently reverts to UTC. +.Pp +Previous versions of +.Nm +included the +.Fl d +(set daylight saving time flag) and +.Fl t +(set negative time zone offset) options, but these details are now +handled automatically by +.Sy tzdata . +Modern offsets are positive for time zones ahead of UTC and negative +for time zones behind UTC, but like the obsolete +.Fl t +option, the +.Sy tzdata +files in the subdirectory +.Pa /usr/share/zoneinfo/Etc +still use an older convention where times ahead of UTC are considered +negative. +.Sh ENVIRONMENT +The following environment variable affects the execution of +.Nm : +.Bl -tag -width Ds +.It Ev TZ +The timezone to use when displaying dates. +The normal format is a pathname relative to +.Pa /usr/share/zoneinfo . +For example, the command +.Dq TZ=America/Los_Angeles date +displays the current time in California. +The variable can also specify an absolute path. +See +.Xr environ 7 +for more information. +.El +.Sh FILES +.Bl -tag -width /var/log/messages -compact +.It Pa /etc/localtime +Time zone information file for default system time zone. +May be omitted, in which case the default time zone is UTC. +.It Pa /usr/share/zoneinfo +Directory containing time zone information files. +.It Pa /var/log/messages +Record of the user setting the time. +.It Pa /var/log/utx.log +Record of date resets and time changes. +.El +.Sh EXIT STATUS +The +.Nm +utility exits 0 on success, 1 if unable to set the date, and 2 +if able to set the local date, but unable to set it globally. +.Sh EXAMPLES +The command +.Pp +.Dl "date +%s.%3N" +.Pp +will print the time elapsed since the Unix Epoch with millisecond +precision. +.Pp +The command: +.Pp +.Dl "date ""+DATE: %Y-%m-%d%nTIME: %H:%M:%S""" +.Pp +will display: +.Bd -literal -offset indent +DATE: 1987-11-21 +TIME: 13:36:16 +.Ed +.Pp +In the Europe/London timezone, the command: +.Pp +.Dl "date -v1m -v+1y" +.Pp +will display: +.Pp +.Dl "Sun Jan 4 04:15:24 GMT 1998" +.Pp +where it is currently +.Ql "Mon Aug 4 04:15:24 BST 1997" . +.Pp +The command: +.Pp +.Dl "date -v1d -v3m -v0y -v-1d" +.Pp +will display the last day of February in the year 2000: +.Pp +.Dl "Tue Feb 29 03:18:00 GMT 2000" +.Pp +So will the command: +.Pp +.Dl "date -v3m -v30d -v0y -v-1m" +.Pp +because there is no such date as the 30th of February. +.Pp +The command: +.Pp +.Dl "date -v1d -v+1m -v-1d -v-fri" +.Pp +will display the last Friday of the month: +.Pp +.Dl "Fri Aug 29 04:31:11 BST 1997" +.Pp +where it is currently +.Ql "Mon Aug 4 04:31:11 BST 1997" . +.Pp +The command: +.Pp +.Dl "date 8506131627" +.Pp +sets the date to +.Ql "June 13, 1985, 4:27 PM" . +.Pp +.Dl "date ""+%Y%m%d%H%M.%S""" +.Pp +may be used on one machine to print out the date +suitable for setting on another. +.Po Use +.Ql "+%m%d%H%M%Y.%S" +with GNU date on +Linux . +.Pc +.Pp +The command: +.Pp +.Dl "date 1432" +.Pp +sets the time to +.Ql "2:32 PM" , +without modifying the date. +.Pp +The command +.Pp +.Dl "TZ=America/Los_Angeles date -Iseconds -r 1533415339" +.Pp +will display +.Pp +.Dl "2018-08-04T13:42:19-07:00" +.Pp +The command: +.Pp +.Dl "env LC_ALL=C date -j -f ""%a %b %d %T %Z %Y"" ""`env LC_ALL=C date`"" ""+%s""" +.Pp +can be used to parse the output from +.Nm +and express it in Epoch time. +.Pp +Finally the command +.Pp +.Dl "TZ=America/Los_Angeles date -z Europe/Paris -j 0900" +.Pp +will print the time in the +.Dq Europe/Paris +timezone when it is 9:00 in the +.Dq America/Los_Angeles +timezone. +.Sh DIAGNOSTICS +It is invalid to combine the +.Fl I +flag with either +.Fl R +or an output format +.Dq ( + Ns ... ) +operand. +If this occurs, +.Nm +prints: +.Ql multiple output formats specified +and exits with status 1. +.Sh SEE ALSO +.Xr locale 1 , +.Xr clock_gettime 2 , +.Xr gettimeofday 2 , +.Xr getutxent 3 , +.Xr strftime 3 , +.Xr strptime 3 , +.Xr tzset 3 , +.Xr adjkerntz 8 , +.Xr ntpd 8 , +.Xr tzsetup 8 +.Rs +.%T "TSP: The Time Synchronization Protocol for UNIX 4.3BSD" +.%A R. Gusella +.%A S. Zatti +.Re +.Rs +.%U https://iana.org/time-zones +.%T Time Zone Database +.Re +.Sh STANDARDS +The +.Nm +utility is expected to be compatible with +.St -p1003.2 . +With the exception of the +.Fl u +option, all options are extensions to the standard. +.Pp +The format selected by the +.Fl I +flag is compatible with +.St -iso8601 . +.Pp +The +.Ql \&%N +conversion specification for nanoseconds is a non-standard extension. +It is compatible with GNU date's +.Ql \&%N . +.Sh HISTORY +A +.Nm +command appeared in +.At v1 . +.Pp +A number of options were added and then removed again, including the +.Fl d +(set DST flag) and +.Fl t +(set negative time zone offset). +Time zones are now handled by code bundled with +.Sy tzdata . +.Pp +The +.Fl I +flag was added in +.Fx 12.0 . +.Pp +The +.Ql \&%N +conversion specification was added in +.Fx 14.1 . +Support for the +.Ql \&% Ns Ar n Ns Cm N +and +.Ql \&%-N +variants was added in +.Fx 15.1 . diff --git a/corebinutils/date/date.c b/corebinutils/date/date.c new file mode 100644 index 0000000000..86c6896199 --- /dev/null +++ b/corebinutils/date/date.c @@ -0,0 +1,879 @@ +/*- + * SPDX-License-Identifier: BSD-3-Clause + * + * Copyright (c) 1985, 1987, 1988, 1993 + * The Regents of the University of California. All rights reserved. + * + * Copyright (c) 2026 + * Project Tick. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the University nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +#include <sys/stat.h> +#include <sys/types.h> + +#include <ctype.h> +#include <errno.h> +#include <inttypes.h> +#include <limits.h> +#include <locale.h> +#include <stdarg.h> +#include <stdbool.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> +#include <unistd.h> + +#include "vary.h" + +#ifndef TM_YEAR_BASE +#define TM_YEAR_BASE 1900 +#endif + +#define DEFAULT_PLUS_FORMAT "%a %b %e %T %Z %Y" + +struct iso8601_fmt { + const char *refname; + const char *format_string; + bool include_zone; +}; + +struct strbuf { + char *data; + size_t len; + size_t cap; +}; + +struct options { + const char *input_format; + const char *output_zone; + const char *reference_arg; + const char *time_operand; + const char *format_operand; + struct vary *vary_chain; + const struct iso8601_fmt *iso8601_selected; + bool no_set; + bool rfc2822; + bool use_utc; +}; + +static const struct iso8601_fmt iso8601_fmts[] = { + { "date", "%Y-%m-%d", false }, + { "hours", "%Y-%m-%dT%H", true }, + { "minutes", "%Y-%m-%dT%H:%M", true }, + { "seconds", "%Y-%m-%dT%H:%M:%S", true }, + { "ns", "%Y-%m-%dT%H:%M:%S,%N", true }, +}; + +static const char *progname = "date"; +static const char *rfc2822_format = "%a, %d %b %Y %T %z"; + +static void usage(void) __attribute__((__noreturn__)); +static void die(const char *fmt, ...) __attribute__((__noreturn__, format(printf, 1, 2))); +static void die_errno(const char *fmt, ...) __attribute__((__noreturn__, format(printf, 1, 2))); + +static void strbuf_init(struct strbuf *buf); +static void strbuf_reserve(struct strbuf *buf, size_t extra); +static void strbuf_append_mem(struct strbuf *buf, const char *data, size_t len); +static void strbuf_append_char(struct strbuf *buf, char ch); +static void strbuf_append_str(struct strbuf *buf, const char *text); +static char *strbuf_finish(struct strbuf *buf); + +static void set_progname(const char *argv0); +static void parse_args(int argc, char **argv, struct options *options); +static const char *require_option_arg(int argc, char **argv, int *index, + const char *arg, const char *option_name); +static void parse_iso8601_arg(struct options *options, const char *arg); +static void parse_operands(int argc, char **argv, int start_index, + struct options *options); +static void validate_options(const struct options *options); +static void set_timezone_or_die(const char *tz_value, const char *what); +static time_t int64_to_time_t(int64_t value, const char *what); +static void read_reference_time(const char *arg, struct timespec *ts); +static void read_current_time(struct timespec *ts, struct timespec *resolution); +static void localtime_or_die(time_t value, struct tm *tm); +static void parse_legacy_time(const char *text, const struct timespec *base, + struct timespec *ts); +static void parse_formatted_time(const char *format, const char *text, + const struct timespec *base, struct timespec *ts); +static void parse_time_operand(const struct options *options, + const struct timespec *base, struct timespec *ts); +static void set_system_time(const struct timespec *ts); +static void apply_variations(const struct options *options, struct tm *tm); +static char *expand_format_string(const char *format, long nsec, long resolution); +static void append_nsec_digits(struct strbuf *buf, const char *pending, size_t len, + long nsec, long resolution); +static char *render_format(const char *format, const struct tm *tm, long nsec, + long resolution); +static char *render_iso8601(const struct iso8601_fmt *selection, + const struct tm *tm, long nsec, long resolution); +static char *render_numeric_timezone(const struct tm *tm); +static void print_line_and_exit(const char *text) __attribute__((__noreturn__)); + +int +main(int argc, char **argv) +{ + struct options options; + struct timespec ts; + struct timespec resolution = { 0, 1 }; + struct tm tm; + const char *format; + char *output; + + set_progname(argv[0]); + parse_args(argc, argv, &options); + validate_options(&options); + + setlocale(LC_TIME, ""); + + if (options.use_utc) + set_timezone_or_die("UTC0", "TZ=UTC0"); + + if (options.reference_arg != NULL) + read_reference_time(options.reference_arg, &ts); + else + read_current_time(&ts, &resolution); + + if (options.time_operand != NULL) { + parse_time_operand(&options, &ts, &ts); + if (!options.no_set) + set_system_time(&ts); + } else if (options.input_format != NULL) { + die("option -f requires an input date operand"); + } + + if (options.output_zone != NULL) + set_timezone_or_die(options.output_zone, "TZ"); + + localtime_or_die(ts.tv_sec, &tm); + apply_variations(&options, &tm); + + if (options.iso8601_selected != NULL) { + output = render_iso8601(options.iso8601_selected, &tm, ts.tv_nsec, + resolution.tv_nsec); + print_line_and_exit(output); + } + + if (options.rfc2822) { + if (setlocale(LC_TIME, "C") == NULL) + die("failed to activate C locale for -R"); + format = rfc2822_format; + } else if (options.format_operand != NULL) { + format = options.format_operand; + } else { + format = "%+"; + } + + output = render_format(format, &tm, ts.tv_nsec, resolution.tv_nsec); + print_line_and_exit(output); +} + +static void +strbuf_init(struct strbuf *buf) +{ + buf->data = NULL; + buf->len = 0; + buf->cap = 0; +} + +static void +strbuf_reserve(struct strbuf *buf, size_t extra) +{ + size_t needed; + size_t newcap; + char *newdata; + + needed = buf->len + extra + 1; + if (needed <= buf->cap) + return; + + newcap = buf->cap == 0 ? 64 : buf->cap; + while (newcap < needed) { + if (newcap > SIZE_MAX / 2) + die("buffer too large"); + newcap *= 2; + } + + newdata = realloc(buf->data, newcap); + if (newdata == NULL) + die_errno("realloc"); + + buf->data = newdata; + buf->cap = newcap; +} + +static void +strbuf_append_mem(struct strbuf *buf, const char *data, size_t len) +{ + strbuf_reserve(buf, len); + memcpy(buf->data + buf->len, data, len); + buf->len += len; + buf->data[buf->len] = '\0'; +} + +static void +strbuf_append_char(struct strbuf *buf, char ch) +{ + strbuf_reserve(buf, 1); + buf->data[buf->len++] = ch; + buf->data[buf->len] = '\0'; +} + +static void +strbuf_append_str(struct strbuf *buf, const char *text) +{ + strbuf_append_mem(buf, text, strlen(text)); +} + +static char * +strbuf_finish(struct strbuf *buf) +{ + char *result; + + if (buf->data == NULL) { + result = strdup(""); + if (result == NULL) + die_errno("strdup"); + return (result); + } + + result = buf->data; + buf->data = NULL; + buf->len = 0; + buf->cap = 0; + return (result); +} + +static void +set_progname(const char *argv0) +{ + const char *base; + + if (argv0 == NULL || argv0[0] == '\0') + return; + + base = strrchr(argv0, '/'); + progname = base != NULL ? base + 1 : argv0; +} + +static void +parse_args(int argc, char **argv, struct options *options) +{ + int i; + int j; + + memset(options, 0, sizeof(*options)); + + for (i = 1; i < argc; i++) { + const char *arg; + + arg = argv[i]; + if (arg[0] != '-' || arg[1] == '\0') + break; + if (strcmp(arg, "--") == 0) { + i++; + break; + } + + if (strncmp(arg, "-I", 2) == 0) { + parse_iso8601_arg(options, arg[2] == '\0' ? NULL : arg + 2); + continue; + } + if (strncmp(arg, "-f", 2) == 0) { + options->input_format = require_option_arg(argc, argv, &i, + arg + 2, "-f"); + continue; + } + if (strncmp(arg, "-r", 2) == 0) { + options->reference_arg = require_option_arg(argc, argv, &i, + arg + 2, "-r"); + continue; + } + if (strncmp(arg, "-v", 2) == 0) { + options->vary_chain = vary_append(options->vary_chain, + require_option_arg(argc, argv, &i, arg + 2, "-v")); + continue; + } + if (strncmp(arg, "-z", 2) == 0) { + options->output_zone = require_option_arg(argc, argv, &i, + arg + 2, "-z"); + continue; + } + + for (j = 1; arg[j] != '\0'; j++) { + switch (arg[j]) { + case 'j': + options->no_set = true; + break; + case 'n': + die("option -n is not supported on Linux"); + case 'R': + options->rfc2822 = true; + break; + case 'u': + options->use_utc = true; + break; + default: + usage(); + } + } + } + + parse_operands(argc, argv, i, options); +} + +static const char * +require_option_arg(int argc, char **argv, int *index, const char *arg, + const char *option_name) +{ + if (arg != NULL && arg[0] != '\0') + return (arg); + if (*index + 1 >= argc) + die("option %s requires an argument", option_name); + (*index)++; + return (argv[*index]); +} + +static void +parse_iso8601_arg(struct options *options, const char *arg) +{ + size_t i; + + options->iso8601_selected = &iso8601_fmts[0]; + if (arg == NULL) + return; + + for (i = 0; i < sizeof(iso8601_fmts) / sizeof(iso8601_fmts[0]); i++) { + if (strcmp(arg, iso8601_fmts[i].refname) == 0) { + options->iso8601_selected = &iso8601_fmts[i]; + return; + } + } + + die("invalid argument '%s' for -I", arg); +} + +static void +parse_operands(int argc, char **argv, int start_index, struct options *options) +{ + int i; + + for (i = start_index; i < argc; i++) { + if (argv[i][0] == '+') { + if (options->format_operand != NULL) + usage(); + options->format_operand = argv[i] + 1; + continue; + } + if (options->time_operand != NULL) + usage(); + options->time_operand = argv[i]; + } +} + +static void +validate_options(const struct options *options) +{ + if (options->iso8601_selected != NULL && options->rfc2822) + die("multiple output formats specified"); + if (options->iso8601_selected != NULL && options->format_operand != NULL) + die("multiple output formats specified"); + if (options->rfc2822 && options->format_operand != NULL) + die("multiple output formats specified"); +} + +static void +set_timezone_or_die(const char *tz_value, const char *what) +{ + if (setenv("TZ", tz_value, 1) != 0) + die_errno("setenv(%s)", what); + tzset(); +} + +static time_t +int64_to_time_t(int64_t value, const char *what) +{ + time_t converted; + + converted = (time_t)value; + if ((int64_t)converted != value) + die("%s out of range: %" PRId64, what, value); + return (converted); +} + +static void +read_reference_time(const char *arg, struct timespec *ts) +{ + struct stat sb; + char *end; + long long seconds; + + errno = 0; + seconds = strtoll(arg, &end, 10); + if (errno == 0 && arg[0] != '\0' && *end == '\0') { + ts->tv_sec = int64_to_time_t((int64_t)seconds, "seconds"); + ts->tv_nsec = 0; + return; + } + + if (stat(arg, &sb) != 0) + die_errno("%s", arg); + + ts->tv_sec = sb.st_mtim.tv_sec; + ts->tv_nsec = sb.st_mtim.tv_nsec; +} + +static void +read_current_time(struct timespec *ts, struct timespec *resolution) +{ + if (clock_gettime(CLOCK_REALTIME, ts) != 0) + die_errno("clock_gettime"); + if (clock_getres(CLOCK_REALTIME, resolution) != 0) + die_errno("clock_getres"); +} + +static void +localtime_or_die(time_t value, struct tm *tm) +{ + if (localtime_r(&value, tm) == NULL) + die("invalid time"); +} + +static int +parse_two_digits(const char *text, const char *what) +{ + if (!isdigit((unsigned char)text[0]) || !isdigit((unsigned char)text[1])) + die("illegal time format"); + return ((text[0] - '0') * 10 + (text[1] - '0')); +} + +static void +convert_tm_or_die(struct tm *tm, struct timespec *ts) +{ + time_t converted; + + tm->tm_yday = -1; + converted = mktime(tm); + if (tm->tm_yday == -1) + die("nonexistent time"); + ts->tv_sec = converted; + ts->tv_nsec = 0; +} + +static void +parse_legacy_time(const char *text, const struct timespec *base, struct timespec *ts) +{ + struct tm tm; + const char *dot; + size_t digits_len; + int second; + + localtime_or_die(base->tv_sec, &tm); + tm.tm_isdst = -1; + ts->tv_nsec = 0; + + dot = strchr(text, '.'); + if (dot != NULL) { + if (strchr(dot + 1, '.') != NULL || strlen(dot + 1) != 2) + die("illegal time format"); + second = parse_two_digits(dot + 1, "seconds"); + if (second > 61) + die("illegal time format"); + tm.tm_sec = second; + digits_len = (size_t)(dot - text); + } else { + tm.tm_sec = 0; + digits_len = strlen(text); + } + + if (digits_len == 0) + die("illegal time format"); + for (size_t i = 0; i < digits_len; i++) { + if (!isdigit((unsigned char)text[i])) + die("illegal time format"); + } + + switch (digits_len) { + case 12: + tm.tm_year = parse_two_digits(text, "century") * 100 - TM_YEAR_BASE; + tm.tm_year += parse_two_digits(text + 2, "year"); + tm.tm_mon = parse_two_digits(text + 4, "month") - 1; + tm.tm_mday = parse_two_digits(text + 6, "day"); + tm.tm_hour = parse_two_digits(text + 8, "hour"); + tm.tm_min = parse_two_digits(text + 10, "minute"); + break; + case 10: { + int year; + + year = parse_two_digits(text, "year"); + tm.tm_year = year < 69 ? year + 100 : year; + tm.tm_mon = parse_two_digits(text + 2, "month") - 1; + tm.tm_mday = parse_two_digits(text + 4, "day"); + tm.tm_hour = parse_two_digits(text + 6, "hour"); + tm.tm_min = parse_two_digits(text + 8, "minute"); + break; + } + case 8: + tm.tm_mon = parse_two_digits(text, "month") - 1; + tm.tm_mday = parse_two_digits(text + 2, "day"); + tm.tm_hour = parse_two_digits(text + 4, "hour"); + tm.tm_min = parse_two_digits(text + 6, "minute"); + break; + case 6: + tm.tm_mday = parse_two_digits(text, "day"); + tm.tm_hour = parse_two_digits(text + 2, "hour"); + tm.tm_min = parse_two_digits(text + 4, "minute"); + break; + case 4: + tm.tm_hour = parse_two_digits(text, "hour"); + tm.tm_min = parse_two_digits(text + 2, "minute"); + break; + case 2: + tm.tm_min = parse_two_digits(text, "minute"); + break; + default: + die("illegal time format"); + } + + if (tm.tm_mon < 0 || tm.tm_mon > 11 || tm.tm_mday < 1 || tm.tm_mday > 31 || + tm.tm_hour < 0 || tm.tm_hour > 23 || tm.tm_min < 0 || tm.tm_min > 59) + die("illegal time format"); + + convert_tm_or_die(&tm, ts); +} + +static void +parse_formatted_time(const char *format, const char *text, const struct timespec *base, + struct timespec *ts) +{ + struct tm tm; + char *end; + + localtime_or_die(base->tv_sec, &tm); + tm.tm_isdst = -1; + end = strptime(text, format, &tm); + if (end == NULL) + die("failed conversion of '%s' using format '%s'", text, format); + if (*end != '\0') + die("input did not fully match format '%s': %s", format, text); + + convert_tm_or_die(&tm, ts); +} + +static void +parse_time_operand(const struct options *options, const struct timespec *base, + struct timespec *ts) +{ + if (options->input_format != NULL) + parse_formatted_time(options->input_format, options->time_operand, base, ts); + else + parse_legacy_time(options->time_operand, base, ts); +} + +static void +set_system_time(const struct timespec *ts) +{ + if (clock_settime(CLOCK_REALTIME, ts) != 0) + die_errno("clock_settime"); +} + +static void +apply_variations(const struct options *options, struct tm *tm) +{ + const struct vary *failed; + + failed = vary_apply(options->vary_chain, tm); + if (failed != NULL) + die("cannot apply date adjustment: %s", failed->arg); +} + +static bool +pending_is_nsec_prefix(const char *pending, size_t len) +{ + size_t i; + + if (len == 0 || pending[0] != '%') + return (false); + for (i = 1; i < len; i++) { + if (pending[i] == '-' && i == 1) + continue; + if (!isdigit((unsigned char)pending[i])) + return (false); + } + return (true); +} + +static void +append_nsec_digits(struct strbuf *buf, const char *pending, size_t len, long nsec, + long resolution) +{ + size_t i; + int width; + int zeroes; + long value; + char digits[32]; + int printed; + + width = 0; + zeroes = 0; + + if (len == 2 && pending[1] == '-') { + long number; + + for (width = 9, number = resolution; width > 0 && number > 0; + width--, number /= 10) + ; + } else if (len > 1) { + for (i = 1; i < len; i++) { + if (width > (INT_MAX - (pending[i] - '0')) / 10) + die("nanosecond width is too large"); + width = width * 10 + (pending[i] - '0'); + } + } + + if (width == 0) { + width = 9; + } else if (width > 9) { + zeroes = width - 9; + width = 9; + } + + value = nsec; + for (i = 0; i < (size_t)(9 - width); i++) + value /= 10; + + printed = snprintf(digits, sizeof(digits), "%0*ld", width, value); + if (printed < 0 || (size_t)printed >= sizeof(digits)) + die("failed to render nanoseconds"); + + strbuf_append_mem(buf, digits, (size_t)printed); + while (zeroes-- > 0) + strbuf_append_char(buf, '0'); +} + +static char * +expand_format_string(const char *format, long nsec, long resolution) +{ + struct strbuf result; + char pending[64]; + size_t pending_len; + size_t i; + bool in_percent; + + strbuf_init(&result); + pending_len = 0; + in_percent = false; + + for (i = 0; format[i] != '\0';) { + if (!in_percent) { + if (format[i] == '%') { + pending[0] = '%'; + pending_len = 1; + in_percent = true; + } else { + strbuf_append_char(&result, format[i]); + } + i++; + continue; + } + + if (format[i] == 'N' && pending_is_nsec_prefix(pending, pending_len)) { + append_nsec_digits(&result, pending, pending_len, nsec, resolution); + in_percent = false; + i++; + continue; + } + if (format[i] == '+' && pending_len == 1) { + strbuf_append_str(&result, DEFAULT_PLUS_FORMAT); + in_percent = false; + i++; + continue; + } + if (format[i] == '%' ) { + strbuf_append_mem(&result, pending, pending_len); + strbuf_append_char(&result, '%'); + in_percent = false; + i++; + continue; + } + if (format[i] == '-' && pending_len == 1) { + pending[pending_len++] = '-'; + i++; + continue; + } + if (isdigit((unsigned char)format[i]) && + pending_is_nsec_prefix(pending, pending_len) && + !(pending_len == 2 && pending[1] == '-')) { + if (pending_len >= sizeof(pending) - 1) + die("format specifier too long"); + pending[pending_len++] = format[i]; + i++; + continue; + } + + strbuf_append_mem(&result, pending, pending_len); + in_percent = false; + } + + if (in_percent) + strbuf_append_mem(&result, pending, pending_len); + + return (strbuf_finish(&result)); +} + +static char * +render_format(const char *format, const struct tm *tm, long nsec, long resolution) +{ + char *expanded; + char *result; + size_t size; + size_t produced; + + if (format[0] == '\0') { + result = strdup(""); + if (result == NULL) + die_errno("strdup"); + return (result); + } + + expanded = expand_format_string(format, nsec, resolution); + size = 128; + for (;;) { + result = malloc(size); + if (result == NULL) + die_errno("malloc"); + + produced = strftime(result, size, expanded, tm); + if (produced != 0) { + free(expanded); + return (result); + } + + free(result); + if (size > 1 << 20) + die("formatted output is too large"); + size *= 2; + } +} + +static char * +render_numeric_timezone(const struct tm *tm) +{ + char raw[16]; + struct strbuf buf; + size_t len; + + len = strftime(raw, sizeof(raw), "%z", tm); + if (len == 0) + die("failed to render timezone offset"); + + if (len != 5 || (raw[0] != '+' && raw[0] != '-')) { + char *copy; + + copy = strdup(raw); + if (copy == NULL) + die_errno("strdup"); + return (copy); + } + + strbuf_init(&buf); + strbuf_append_mem(&buf, raw, 3); + strbuf_append_char(&buf, ':'); + strbuf_append_mem(&buf, raw + 3, 2); + return (strbuf_finish(&buf)); +} + +static char * +render_iso8601(const struct iso8601_fmt *selection, const struct tm *tm, long nsec, + long resolution) +{ + char *timestamp; + char *zone; + struct strbuf result; + + timestamp = render_format(selection->format_string, tm, nsec, resolution); + if (!selection->include_zone) + return (timestamp); + + zone = render_numeric_timezone(tm); + strbuf_init(&result); + strbuf_append_str(&result, timestamp); + strbuf_append_str(&result, zone); + free(timestamp); + free(zone); + return (strbuf_finish(&result)); +} + +static void +print_line_and_exit(const char *text) +{ + if (printf("%s\n", text) < 0 || fflush(stdout) != 0) + die_errno("stdout"); + free((void *)text); + exit(EXIT_SUCCESS); +} + +static void +usage(void) +{ + fprintf(stderr, "%s\n%s\n%s\n", + "usage: date [-jRu] [-I[date|hours|minutes|seconds|ns]] [-f input_fmt]", + " " + "[ -z output_zone ] [-r filename|seconds] [-v[+|-]val[y|m|w|d|H|M|S]]", + " " + "[[[[[[cc]yy]mm]dd]HH]MM[.SS] | new_date] [+output_fmt]"); + exit(EXIT_FAILURE); +} + +static void +die(const char *fmt, ...) +{ + va_list ap; + + fprintf(stderr, "%s: ", progname); + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); + fputc('\n', stderr); + exit(EXIT_FAILURE); +} + +static void +die_errno(const char *fmt, ...) +{ + int saved_errno; + va_list ap; + + saved_errno = errno; + fprintf(stderr, "%s: ", progname); + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); + fprintf(stderr, ": %s\n", strerror(saved_errno)); + exit(EXIT_FAILURE); +} diff --git a/corebinutils/date/tests/test.sh b/corebinutils/date/tests/test.sh new file mode 100644 index 0000000000..1c8fff9356 --- /dev/null +++ b/corebinutils/date/tests/test.sh @@ -0,0 +1,94 @@ +#!/bin/sh +set -eu + +ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +DATE_BIN=${DATE_BIN:-"$ROOT/out/date"} +TMPDIR=${TMPDIR:-/tmp} +WORKDIR=$(mktemp -d "$TMPDIR/date-test.XXXXXX") +trap 'rm -rf "$WORKDIR"' EXIT INT TERM + +fail() { + printf '%s\n' "FAIL: $1" >&2 + exit 1 +} + +assert_eq() { + expected=$1 + actual=$2 + message=$3 + [ "$actual" = "$expected" ] || fail "$message: expected [$expected] got [$actual]" +} + +assert_match() { + pattern=$1 + text=$2 + message=$3 + printf '%s\n' "$text" | grep -Eq "$pattern" || fail "$message" +} + +run_ok() { + "$DATE_BIN" "$@" +} + +run_err() { + "$DATE_BIN" "$@" 2>&1 || true +} + +[ -x "$DATE_BIN" ] || fail "missing binary: $DATE_BIN" + +export LC_ALL=C +export TZ=UTC0 + +usage_output=$(run_err -x) +assert_match '^usage: date ' "$usage_output" "usage output missing" + +unsupported_output=$(run_err -n) +assert_eq "date: option -n is not supported on Linux" "$unsupported_output" "unsupported -n check failed" + +default_output=$(run_ok -u -r 3222243) +assert_eq "Sat Feb 7 07:04:03 UTC 1970" "$default_output" "default %+ format mismatch" + +rfc_output=$(run_ok -u -R -r 3222243) +assert_eq "Sat, 07 Feb 1970 07:04:03 +0000" "$rfc_output" "RFC 2822 output mismatch" + +iso_seconds=$(run_ok -u -r 1005600000 -Iseconds) +assert_eq "2001-11-12T21:20:00+00:00" "$iso_seconds" "ISO-8601 seconds output mismatch" + +iso_ns=$(run_ok -u -r 3222243 -Ins) +assert_eq "1970-02-07T07:04:03,000000000+00:00" "$iso_ns" "ISO-8601 nanosecond output mismatch" + +format_ns=$(run_ok -u -r 3222243 +%s.%N) +assert_eq "3222243.000000000" "$format_ns" "nanosecond format mismatch" + +format_width=$(run_ok -u -r 3222243 +%3N) +assert_eq "000" "$format_width" "width-limited %N mismatch" + +strict_parse=$(run_ok -j -u -f %Y-%m-%d 2001-11-12 +%F) +assert_eq "2001-11-12" "$strict_parse" "formatted parse mismatch" + +legacy_parse=$(run_ok -j -u 200111122120.59 "+%F %T") +assert_eq "2001-11-12 21:20:59" "$legacy_parse" "legacy numeric parse mismatch" + +vary_month=$(run_ok -j -u -r 1612051200 -v+1m +%F) +assert_eq "2021-02-28" "$vary_month" "month clamp adjustment mismatch" + +zone_output=$(run_ok -r 0 -z UTC-2 +%Y-%m-%dT%H:%M:%S%z) +assert_eq "1970-01-01T02:00:00+0200" "$zone_output" "output zone mismatch" + +touch -t 202001010102.03 "$WORKDIR/reference" +file_output=$(run_ok -u -r "$WORKDIR/reference" +%s) +assert_eq "1577840523" "$file_output" "file reference timestamp mismatch" + +bad_format=$(run_err -j -f %Y-%m-%d 2001-11-12junk +%F) +assert_eq "date: input did not fully match format '%Y-%m-%d': 2001-11-12junk" "$bad_format" "strict format rejection missing" + +bad_iso=$(run_err -Ibogus) +assert_eq "date: invalid argument 'bogus' for -I" "$bad_iso" "invalid -I rejection missing" + +bad_reference=$(run_err -r "$WORKDIR/missing") +assert_match "^date: $WORKDIR/missing: " "$bad_reference" "missing reference error missing" + +bad_operands=$(run_err 200101010101 200201010101) +assert_match '^usage: date ' "$bad_operands" "too many operands not rejected" + +printf '%s\n' "PASS" diff --git a/corebinutils/date/vary.c b/corebinutils/date/vary.c new file mode 100644 index 0000000000..65440f5f64 --- /dev/null +++ b/corebinutils/date/vary.c @@ -0,0 +1,560 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 1997 Brian Somers <brian@Awfulhak.org> + * All rights reserved. + * + * Copyright (c) 2026 + * Project Tick. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +#include <ctype.h> +#include <err.h> +#include <stdbool.h> +#include <stdint.h> +#include <stdlib.h> +#include <string.h> +#include <strings.h> +#include <time.h> + +#include "vary.h" + +struct trans { + int64_t value; + const char *name; +}; + +static const struct trans trans_mon[] = { + { 1, "january" }, { 2, "february" }, { 3, "march" }, { 4, "april" }, + { 5, "may" }, { 6, "june" }, { 7, "july" }, { 8, "august" }, + { 9, "september" }, { 10, "october" }, { 11, "november" }, + { 12, "december" }, { -1, NULL } +}; + +static const struct trans trans_wday[] = { + { 0, "sunday" }, { 1, "monday" }, { 2, "tuesday" }, { 3, "wednesday" }, + { 4, "thursday" }, { 5, "friday" }, { 6, "saturday" }, { -1, NULL } +}; + +static int mdays[12] = { 31, 0, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; + +static int adjyear(struct tm *tm, char type, int64_t value, bool normalize); +static int adjmon(struct tm *tm, char type, int64_t value, bool is_text, + bool normalize); +static int adjday(struct tm *tm, char type, int64_t value, bool normalize); +static int adjwday(struct tm *tm, char type, int64_t value, bool is_text, + bool normalize); +static int adjhour(struct tm *tm, char type, int64_t value, bool normalize); +static int adjmin(struct tm *tm, char type, int64_t value, bool normalize); +static int adjsec(struct tm *tm, char type, int64_t value, bool normalize); + +static bool normalize_tm(struct tm *tm, char type); +static int translate_token(const struct trans *table, const char *arg); +static bool parse_decimal_component(const char *arg, size_t len, int64_t *value); +static int daysinmonth(const struct tm *tm); + +static bool +normalize_tm(struct tm *tm, char type) +{ + time_t converted; + + tm->tm_yday = -1; + while ((converted = mktime(tm)) == (time_t)-1 && + tm->tm_year > 68 && tm->tm_year < 138) { + if (!adjhour(tm, type == '-' ? '-' : '+', 1, false)) + return (false); + } + + return (tm->tm_yday != -1); +} + +static int +translate_token(const struct trans *table, const char *arg) +{ + size_t i; + + for (i = 0; table[i].value != -1; i++) { + if (strncasecmp(table[i].name, arg, 3) == 0 || + strcasecmp(table[i].name, arg) == 0) + return ((int)table[i].value); + } + + return (-1); +} + +struct vary * +vary_append(struct vary *chain, const char *arg) +{ + struct vary *node; + struct vary **nextp; + + if (chain == NULL) { + nextp = &chain; + } else { + nextp = &chain->next; + while (*nextp != NULL) + nextp = &(*nextp)->next; + } + + node = malloc(sizeof(*node)); + if (node == NULL) + err(1, "malloc"); + + node->arg = arg; + node->next = NULL; + *nextp = node; + return (chain); +} + +static bool +parse_decimal_component(const char *arg, size_t len, int64_t *value) +{ + size_t i; + int64_t current; + int digit; + + if (len == 0) + return (false); + + current = 0; + for (i = 0; i < len; i++) { + if (!isdigit((unsigned char)arg[i])) + return (false); + digit = arg[i] - '0'; + if (current > (INT64_MAX - digit) / 10) + return (false); + current = current * 10 + digit; + } + + *value = current; + return (true); +} + +static int +daysinmonth(const struct tm *tm) +{ + int year; + + year = tm->tm_year + 1900; + if (tm->tm_mon == 1) { + if (year % 400 == 0) + return (29); + if (year % 100 == 0) + return (28); + if (year % 4 == 0) + return (29); + return (28); + } + if (tm->tm_mon >= 0 && tm->tm_mon < 12) + return (mdays[tm->tm_mon]); + return (0); +} + +static int +adjyear(struct tm *tm, char type, int64_t value, bool normalize) +{ + switch (type) { + case '+': + tm->tm_year += value; + break; + case '-': + tm->tm_year -= value; + break; + default: + tm->tm_year = value; + if (tm->tm_year < 69) + tm->tm_year += 100; + else if (tm->tm_year > 1900) + tm->tm_year -= 1900; + break; + } + + return (!normalize || normalize_tm(tm, type)); +} + +static int +adjmon(struct tm *tm, char type, int64_t value, bool is_text, bool normalize) +{ + int last_month_days; + + if (value < 0) + return (0); + + switch (type) { + case '+': + if (is_text) { + if (value <= tm->tm_mon) + value += 11 - tm->tm_mon; + else + value -= tm->tm_mon + 1; + } + if (value != 0) { + if (!adjyear(tm, '+', (tm->tm_mon + value) / 12, false)) + return (0); + value %= 12; + tm->tm_mon += value; + if (tm->tm_mon > 11) + tm->tm_mon -= 12; + } + break; + case '-': + if (is_text) { + if (value - 1 > tm->tm_mon) + value = 13 - value + tm->tm_mon; + else + value = tm->tm_mon - value + 1; + } + if (value != 0) { + if (!adjyear(tm, '-', value / 12, false)) + return (0); + value %= 12; + if (value > tm->tm_mon) { + if (!adjyear(tm, '-', 1, false)) + return (0); + value -= 12; + } + tm->tm_mon -= value; + } + break; + default: + if (value < 1 || value > 12) + return (0); + tm->tm_mon = (int)value - 1; + break; + } + + last_month_days = daysinmonth(tm); + if (tm->tm_mday > last_month_days) + tm->tm_mday = last_month_days; + + return (!normalize || normalize_tm(tm, type)); +} + +static int +adjday(struct tm *tm, char type, int64_t value, bool normalize) +{ + int last_month_days; + + switch (type) { + case '+': + while (value != 0) { + last_month_days = daysinmonth(tm); + if (value > last_month_days - tm->tm_mday) { + value -= last_month_days - tm->tm_mday + 1; + tm->tm_mday = 1; + if (!adjmon(tm, '+', 1, false, false)) + return (0); + } else { + tm->tm_mday += value; + value = 0; + } + } + break; + case '-': + while (value != 0) { + if (value >= tm->tm_mday) { + value -= tm->tm_mday; + tm->tm_mday = 1; + if (!adjmon(tm, '-', 1, false, false)) + return (0); + tm->tm_mday = daysinmonth(tm); + } else { + tm->tm_mday -= value; + value = 0; + } + } + break; + default: + if (value > 0 && value <= daysinmonth(tm)) + tm->tm_mday = (int)value; + else + return (0); + break; + } + + return (!normalize || normalize_tm(tm, type)); +} + +static int +adjwday(struct tm *tm, char type, int64_t value, bool is_text, bool normalize) +{ + if (value < 0) + return (0); + + switch (type) { + case '+': + if (is_text) { + if (value < tm->tm_wday) + value = 7 - tm->tm_wday + value; + else + value -= tm->tm_wday; + } else { + value *= 7; + } + return (!value || adjday(tm, '+', value, normalize)); + case '-': + if (is_text) { + if (value > tm->tm_wday) + value = 7 - value + tm->tm_wday; + else + value = tm->tm_wday - value; + } else { + value *= 7; + } + return (!value || adjday(tm, '-', value, normalize)); + default: + if (value < tm->tm_wday) + return (adjday(tm, '-', tm->tm_wday - value, normalize)); + if (value > 6) + return (0); + if (value > tm->tm_wday) + return (adjday(tm, '+', value - tm->tm_wday, normalize)); + break; + } + + return (1); +} + +static int +adjhour(struct tm *tm, char type, int64_t value, bool normalize) +{ + if (value < 0) + return (0); + + switch (type) { + case '+': + if (value != 0) { + int days; + + days = (int)((tm->tm_hour + value) / 24); + value %= 24; + tm->tm_hour += value; + tm->tm_hour %= 24; + if (!adjday(tm, '+', days, false)) + return (0); + } + break; + case '-': + if (value != 0) { + int days; + + days = (int)(value / 24); + value %= 24; + if (value > tm->tm_hour) { + days++; + value -= 24; + } + tm->tm_hour -= value; + if (!adjday(tm, '-', days, false)) + return (0); + } + break; + default: + if (value > 23) + return (0); + tm->tm_hour = (int)value; + break; + } + + return (!normalize || normalize_tm(tm, type)); +} + +static int +adjmin(struct tm *tm, char type, int64_t value, bool normalize) +{ + if (value < 0) + return (0); + + switch (type) { + case '+': + if (value != 0) { + if (!adjhour(tm, '+', (tm->tm_min + value) / 60, false)) + return (0); + value %= 60; + tm->tm_min += value; + if (tm->tm_min > 59) + tm->tm_min -= 60; + } + break; + case '-': + if (value != 0) { + if (!adjhour(tm, '-', value / 60, false)) + return (0); + value %= 60; + if (value > tm->tm_min) { + if (!adjhour(tm, '-', 1, false)) + return (0); + value -= 60; + } + tm->tm_min -= value; + } + break; + default: + if (value > 59) + return (0); + tm->tm_min = (int)value; + break; + } + + return (!normalize || normalize_tm(tm, type)); +} + +static int +adjsec(struct tm *tm, char type, int64_t value, bool normalize) +{ + if (value < 0) + return (0); + + switch (type) { + case '+': + if (value != 0) { + if (!adjmin(tm, '+', (tm->tm_sec + value) / 60, false)) + return (0); + value %= 60; + tm->tm_sec += value; + if (tm->tm_sec > 59) + tm->tm_sec -= 60; + } + break; + case '-': + if (value != 0) { + if (!adjmin(tm, '-', value / 60, false)) + return (0); + value %= 60; + if (value > tm->tm_sec) { + if (!adjmin(tm, '-', 1, false)) + return (0); + value -= 60; + } + tm->tm_sec -= value; + } + break; + default: + if (value > 59) + return (0); + tm->tm_sec = (int)value; + break; + } + + return (!normalize || normalize_tm(tm, type)); +} + +const struct vary * +vary_apply(const struct vary *chain, struct tm *tm) +{ + const struct vary *node; + + for (node = chain; node != NULL; node = node->next) { + const char *arg; + char type; + char unit; + int64_t value; + size_t len; + int translated; + + type = node->arg[0]; + arg = node->arg; + if (type == '+' || type == '-') + arg++; + else + type = '\0'; + + len = strlen(arg); + if (len < 2) + return (node); + + if (type == '\0') + tm->tm_isdst = -1; + + if (!parse_decimal_component(arg, len - 1, &value)) { + translated = translate_token(trans_wday, arg); + if (translated != -1) { + if (!adjwday(tm, type, translated, true, true)) + return (node); + continue; + } + + translated = translate_token(trans_mon, arg); + if (translated != -1) { + if (!adjmon(tm, type, translated, true, true)) + return (node); + continue; + } + + return (node); + } + + unit = arg[len - 1]; + switch (unit) { + case 'S': + if (!adjsec(tm, type, value, true)) + return (node); + break; + case 'M': + if (!adjmin(tm, type, value, true)) + return (node); + break; + case 'H': + if (!adjhour(tm, type, value, true)) + return (node); + break; + case 'd': + tm->tm_isdst = -1; + if (!adjday(tm, type, value, true)) + return (node); + break; + case 'w': + tm->tm_isdst = -1; + if (!adjwday(tm, type, value, false, true)) + return (node); + break; + case 'm': + tm->tm_isdst = -1; + if (!adjmon(tm, type, value, false, true)) + return (node); + break; + case 'y': + tm->tm_isdst = -1; + if (!adjyear(tm, type, value, true)) + return (node); + break; + default: + return (node); + } + } + + return (NULL); +} + +void +vary_destroy(struct vary *chain) +{ + struct vary *next; + + while (chain != NULL) { + next = chain->next; + free(chain); + chain = next; + } +} diff --git a/corebinutils/date/vary.h b/corebinutils/date/vary.h new file mode 100644 index 0000000000..eee6d42a5c --- /dev/null +++ b/corebinutils/date/vary.h @@ -0,0 +1,39 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 1997 Brian Somers <brian@Awfulhak.org> + * All rights reserved. + * + * Copyright (c) 2026 + * Project Tick. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +struct vary { + const char *arg; + struct vary *next; +}; + +extern struct vary *vary_append(struct vary *v, const char *arg); +extern const struct vary *vary_apply(const struct vary *v, struct tm *t); +extern void vary_destroy(struct vary *v); |
