From 99b9de835ca00df3794fe3bf20d6d502d54b603d Mon Sep 17 00:00:00 2001 From: millert Date: Tue, 22 Feb 2022 15:15:34 +0000 Subject: [PATCH] Add a seq(1) command, similar to what is present in GNU and Plan9. Adapted from the NetBSD version with some changes from FreeBSD. OK gnezdo@ --- usr.bin/Makefile | 4 +- usr.bin/seq/Makefile | 8 + usr.bin/seq/seq.1 | 197 ++++++++++++++++++++ usr.bin/seq/seq.c | 417 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 624 insertions(+), 2 deletions(-) create mode 100644 usr.bin/seq/Makefile create mode 100644 usr.bin/seq/seq.1 create mode 100644 usr.bin/seq/seq.c diff --git a/usr.bin/Makefile b/usr.bin/Makefile index 22027a4f753..a2173f9c55c 100644 --- a/usr.bin/Makefile +++ b/usr.bin/Makefile @@ -1,4 +1,4 @@ -# $OpenBSD: Makefile,v 1.165 2021/10/13 15:04:53 kn Exp $ +# $OpenBSD: Makefile,v 1.166 2022/02/22 15:15:34 millert Exp $ .include @@ -21,7 +21,7 @@ SUBDIR= apply arch at aucat audioctl awk banner \ pkg-config pkill \ pr printenv printf quota radioctl rcs rdist rdistd \ readlink realpath renice rev rpcgen rpcinfo rs rsync rup rusers rwall \ - sdiff script sed sendbug shar showmount signify skey \ + sdiff seq script sed sendbug shar showmount signify skey \ skeyaudit skeyinfo skeyinit sndioctl sndiod snmp \ sort spell split ssh stat su systat \ tail talk tcpbench tee telnet tftp tic time timeout \ diff --git a/usr.bin/seq/Makefile b/usr.bin/seq/Makefile new file mode 100644 index 00000000000..96ad1236c7e --- /dev/null +++ b/usr.bin/seq/Makefile @@ -0,0 +1,8 @@ +# $OpenBSD: Makefile,v 1.1 2022/02/22 15:15:34 millert Exp $ + +PROG= seq +CFLAGS+= -Wall +LDADD+= -lm +DPADD+= ${LIBM} + +.include diff --git a/usr.bin/seq/seq.1 b/usr.bin/seq/seq.1 new file mode 100644 index 00000000000..4996b97b38e --- /dev/null +++ b/usr.bin/seq/seq.1 @@ -0,0 +1,197 @@ +.\" $OpenBSD: seq.1,v 1.1 2022/02/22 15:15:34 millert Exp $ +.\" +.\" Copyright (c) 2005 The NetBSD Foundation, Inc. +.\" All rights reserved. +.\" +.\" This code is derived from software contributed to The NetBSD Foundation +.\" by Brian Ginsbach. +.\" +.\" 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 NETBSD FOUNDATION, INC. 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 FOUNDATION 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 $Mdocdate: February 22 2022 $ +.Dt SEQ 1 +.Os +.Sh NAME +.Nm seq +.Nd print sequences of numbers +.Sh SYNOPSIS +.Nm +.Op Fl w +.Op Fl f Ar format +.Op Fl s Ar string +.Op Ar first Op Ar incr +.Ar last +.Sh DESCRIPTION +The +.Nm +utility prints a sequence of numbers, one per line +.Pq default , +from +.Ar first +.Pq default 1 , +to near +.Ar last +as possible, in increments of +.Ar incr +.Pq default 1 . +When +.Ar first +is larger than +.Ar last , +the default +.Ar incr +is -1. +.Pp +All numbers are interpreted as floating point. +.Pp +Normally, integer values are printed as decimal integers. +.Pp +The +.Nm +utility accepts the following options: +.Bl -tag -width Ar +.It Fl f Ar format , Fl -format Ar format +Use a +.Xr printf 3 +style +.Ar format +to print each number. +Only the +.Cm A , +.Cm a , +.Cm E , +.Cm e , +.Cm F , +.Cm f , +.Cm G , +.Cm g , +and +.Cm % +conversion characters are valid, along with any optional +flags and an optional numeric minimum field width or precision. +The +.Ar format +can contain character escape sequences in backslash notation as +defined in +.St -ansiC . +The default is +.Cm %g . +.It Fl s Ar string , Fl -separator Ar string +Use +.Ar string +to separate numbers. +The +.Ar string +can contain character escape sequences in backslash notation as +defined in +.St -ansiC . +The default is +.Cm \en . +.It Fl w , Fl -fixed-width +Equalize the widths of all numbers by padding with zeros as necessary. +This option has no effect with the +.Fl f +option. +If any sequence numbers will be printed in exponential notation, +the default conversion is changed to +.Cm %e . +.It Fl -help +Display the program usage and exit. +.It Fl -version +Display the verion number and exit. +.El +.Sh EXIT STATUS +.Ex -std +.Sh EXAMPLES +Generate a sequence from 1 to 3 (inclusive) with a default increment of 1: +.Bd -literal -offset indent +# seq 1 3 +1 +2 +3 +.Ed +.Pp +Generate a sequence from 3 to 1 (inclusive) with a default increment of -1: +.Bd -literal -offset indent +# seq 3 1 +3 +2 +1 +.Ed +.Pp +Generate a sequence from 0 to 0.1 (inclusive) with an increment of 0.05 and padding +with leading zeroes. +.Bd -literal -offset indent +# seq -w 0 .05 .1 +0.00 +0.05 +0.10 +.Ed +.Pp +Generate a sequence from 1 to 3 (inclusive) with a default increment of 1, +and a custom separator string: +.Bd -literal -offset indent +# seq -s " " 1 3 +1 2 3 +.Ed +.Pp +Generate a sequence from 1 to 2 (inclusive) with an increment of 0.2 and +print the results with two digits after the decimal point (using a +.Xr printf 3 +style format): +.Bd -literal -offset indent +# seq -f %.2f 1 0.2 2 +1.00 +1.20 +1.40 +1.60 +1.80 +2.00 +.Ed +.Sh SEE ALSO +.Xr jot 1 , +.Xr printf 1 , +.Xr printf 3 +.Sh HISTORY +A +.Nm +command appeared in +.At v8 . +This version of +.Nm +appeared in +.Nx 3.0 +and was ported to +.Ox 7.1 . +It was based on the command of the same name in +Plan 9 from Bell Labs and the GNU core utilities. +The GNU +.Nm +command first appeared in the 1.13 shell utilities release. +.Sh BUGS +The +.Fl w +option does not handle the transition from pure floating point +to exponent representation very well. +The +.Nm +utility is not bug for bug compatible with other implementations. diff --git a/usr.bin/seq/seq.c b/usr.bin/seq/seq.c new file mode 100644 index 00000000000..5ca075f7d80 --- /dev/null +++ b/usr.bin/seq/seq.c @@ -0,0 +1,417 @@ +/* $OpenBSD: seq.c,v 1.1 2022/02/22 15:15:34 millert Exp $ */ + +/*- + * Copyright (c) 2005 The NetBSD Foundation, Inc. + * All rights reserved. + * + * This code is derived from software contributed to The NetBSD Foundation + * by Brian Ginsbach. + * + * 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 NETBSD FOUNDATION, INC. 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 FOUNDATION 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define VERSION "1.0" +#define ZERO '0' +#define SPACE ' ' + +#define MAXIMUM(a, b) (((a) < (b))? (b) : (a)) +#define ISSIGN(c) ((int)(c) == '-' || (int)(c) == '+') +#define ISEXP(c) ((int)(c) == 'e' || (int)(c) == 'E') +#define ISODIGIT(c) ((int)(c) >= '0' && (int)(c) <= '7') + +/* Globals */ + +static const char *decimal_point = "."; /* default */ +static char default_format[] = { "%g" }; /* default */ + +static const struct option long_opts[] = +{ + {"format", required_argument, NULL, 'f'}, + {"help", no_argument, NULL, 'h'}, + {"separator", required_argument, NULL, 's'}, + {"version", no_argument, NULL, 'v'}, + {"equal-width", no_argument, NULL, 'w'}, + {NULL, no_argument, NULL, 0} +}; + +/* Prototypes */ + +static double e_atof(const char *); + +static int decimal_places(const char *); +static int numeric(const char *); +static int valid_format(const char *); + +static char *generate_format(double, double, double, int, char); + +static __dead void usage(int error); + +/* + * The seq command will print out a numeric sequence from 1, the default, + * to a user specified upper limit by 1. The lower bound and increment + * maybe indicated by the user on the command line. The sequence can + * be either whole, the default, or decimal numbers. + */ +int +main(int argc, char *argv[]) +{ + int c = 0; + int equalize = 0; + double first = 1.0; + double last = 0.0; + double incr = 0.0; + double last_shown_value = 0.0; + double cur, step; + struct lconv *locale; + char *fmt = NULL; + const char *sep = "\n"; + const char *term = "\n"; + char *cur_print, *last_print; + char pad = ZERO; + + /* Determine the locale's decimal point. */ + locale = localeconv(); + if (locale && locale->decimal_point && locale->decimal_point[0] != '\0') + decimal_point = locale->decimal_point; + + /* + * Process options, but handle negative numbers separately + * least they trip up getopt(3). + */ + while ((optind < argc) && !numeric(argv[optind]) && + (c = getopt_long(argc, argv, "+f:s:w", long_opts, NULL)) != -1) { + + switch (c) { + case 'f': /* format (plan9/GNU) */ + fmt = optarg; + equalize = 0; + break; + case 's': /* separator (GNU) */ + sep = optarg; + break; + case 'v': /* version (GNU) */ + printf("seq version %s\n", VERSION); + return 0; + case 'w': /* equal width (plan9/GNU) */ + if (fmt == NULL) { + if (equalize++) + pad = SPACE; + } + break; + case 'h': /* help (GNU) */ + usage(0); + break; + default: + usage(1); + break; + } + } + + argc -= optind; + argv += optind; + if (argc < 1 || argc > 3) + usage(1); + + last = e_atof(argv[argc - 1]); + + if (argc > 1) + first = e_atof(argv[0]); + + if (argc > 2) { + incr = e_atof(argv[1]); + /* Plan 9/GNU don't do zero */ + if (incr == 0.0) + errx(1, "zero %screment", (first < last)? "in" : "de"); + } + + /* default is one for Plan 9/GNU work alike */ + if (incr == 0.0) + incr = (first < last) ? 1.0 : -1.0; + + if (incr <= 0.0 && first < last) + errx(1, "needs positive increment"); + + if (incr >= 0.0 && first > last) + errx(1, "needs negative decrement"); + + if (fmt != NULL) { + if (!valid_format(fmt)) + errx(1, "invalid format string: `%s'", fmt); + /* + * XXX to be bug for bug compatible with Plan 9 add a + * newline if none found at the end of the format string. + */ + } else + fmt = generate_format(first, incr, last, equalize, pad); + + for (step = 1, cur = first; incr > 0 ? cur <= last : cur >= last; + cur = first + incr * step++) { + if (cur != first) + fputs(sep, stdout); + printf(fmt, cur); + last_shown_value = cur; + } + + /* + * Did we miss the last value of the range in the loop above? + * + * We might have, so check if the printable version of the last + * computed value ('cur') and desired 'last' value are equal. If they + * are equal after formatting truncation, but 'cur' and + * 'last_shown_value' are not equal, it means the exit condition of the + * loop held true due to a rounding error and we still need to print + * 'last'. + */ + asprintf(&cur_print, fmt, cur); + asprintf(&last_print, fmt, last); + if (strcmp(cur_print, last_print) == 0 && cur != last_shown_value) { + if (cur != first) + fputs(sep, stdout); + fputs(last_print, stdout); + } + free(cur_print); + free(last_print); + + fputs(term, stdout); + + return 0; +} + +/* + * numeric - verify that string is numeric + */ +static int +numeric(const char *s) +{ + int seen_decimal_pt, decimal_pt_len; + + /* skip any sign */ + if (ISSIGN((unsigned char)*s)) + s++; + + seen_decimal_pt = 0; + decimal_pt_len = strlen(decimal_point); + while (*s) { + if (!isdigit((unsigned char)*s)) { + if (!seen_decimal_pt && + strncmp(s, decimal_point, decimal_pt_len) == 0) { + s += decimal_pt_len; + seen_decimal_pt = 1; + continue; + } + if (ISEXP((unsigned char)*s)) { + s++; + if (ISSIGN((unsigned char)*s) || + isdigit((unsigned char)*s)) { + s++; + continue; + } + } + break; + } + s++; + } + return *s == '\0'; +} + +/* + * valid_format - validate user specified format string + */ +static int +valid_format(const char *fmt) +{ + unsigned conversions = 0; + + while (*fmt != '\0') { + /* scan for conversions */ + if (*fmt != '%') { + fmt++; + continue; + } + fmt++; + + /* allow %% but not things like %10% */ + if (*fmt == '%') { + fmt++; + continue; + } + + /* flags */ + while (*fmt != '\0' && strchr("#0- +'", *fmt)) { + fmt++; + } + + /* field width */ + while (*fmt != '\0' && strchr("0123456789", *fmt)) { + fmt++; + } + + /* precision */ + if (*fmt == '.') { + fmt++; + while (*fmt != '\0' && strchr("0123456789", *fmt)) { + fmt++; + } + } + + /* conversion */ + switch (*fmt) { + case 'A': + case 'a': + case 'E': + case 'e': + case 'F': + case 'f': + case 'G': + case 'g': + /* floating point formats are accepted */ + conversions++; + break; + default: + /* anything else is not */ + return 0; + } + } + + /* PR 236347 -- user format strings must have a conversion */ + return conversions == 1; +} + +/* + * e_atof - convert an ASCII string to a double + * exit if string is not a valid double, or if converted value would + * cause overflow or underflow + */ +static double +e_atof(const char *num) +{ + char *endp; + double dbl; + + errno = 0; + dbl = strtod(num, &endp); + + if (errno == ERANGE) + /* under or overflow */ + err(2, "%s", num); + else if (*endp != '\0') + /* "junk" left in number */ + errx(2, "invalid floating point argument: %s", num); + + /* zero shall have no sign */ + if (dbl == -0.0) + dbl = 0; + return dbl; +} + +/* + * decimal_places - count decimal places in a number (string) + */ +static int +decimal_places(const char *number) +{ + int places = 0; + char *dp; + + /* look for a decimal point */ + if ((dp = strstr(number, decimal_point))) { + dp += strlen(decimal_point); + + while (isdigit((unsigned char)*dp++)) + places++; + } + return places; +} + +/* + * generate_format - create a format string + * + * XXX to be bug for bug compatible with Plan9 and GNU return "%g" + * when "%g" prints as "%e" (this way no width adjustments are made) + */ +static char * +generate_format(double first, double incr, double last, int equalize, char pad) +{ + static char buf[256]; + char cc = '\0'; + int precision, width1, width2, places; + + if (equalize == 0) + return default_format; + + /* figure out "last" value printed */ + if (first > last) + last = first - incr * floor((first - last) / incr); + else + last = first + incr * floor((last - first) / incr); + + snprintf(buf, sizeof(buf), "%g", incr); + if (strchr(buf, 'e')) + cc = 'e'; + precision = decimal_places(buf); + + width1 = snprintf(buf, sizeof(buf), "%g", first); + if (strchr(buf, 'e')) + cc = 'e'; + if ((places = decimal_places(buf))) + width1 -= (places + strlen(decimal_point)); + + precision = MAXIMUM(places, precision); + + width2 = snprintf(buf, sizeof(buf), "%g", last); + if (strchr(buf, 'e')) + cc = 'e'; + if ((places = decimal_places(buf))) + width2 -= (places + strlen(decimal_point)); + + /* XXX if incr is floating point fix the precision */ + if (precision) { + snprintf(buf, sizeof(buf), "%%%c%d.%d%c", pad, + MAXIMUM(width1, width2) + (int) strlen(decimal_point) + + precision, precision, (cc) ? cc : 'f'); + } else { + snprintf(buf, sizeof(buf), "%%%c%d%c", pad, + MAXIMUM(width1, width2), (cc) ? cc : 'g'); + } + + return buf; +} + +static __dead void +usage(int error) +{ + fprintf(stderr, + "usage: %s [-vw] [-f format] [-s string] [first [incr]] last\n", + getprogname()); + exit(error); +} -- 2.20.1