1/* $OpenBSD: seq.c,v 1.8 2023/06/13 21:10:41 millert Exp $ */ 2 3/*- 4 * Copyright (c) 2005 The NetBSD Foundation, Inc. 5 * All rights reserved. 6 * 7 * This code is derived from software contributed to The NetBSD Foundation 8 * by Brian Ginsbach. 9 * 10 * Redistribution and use in source and binary forms, with or without 11 * modification, are permitted provided that the following conditions 12 * are met: 13 * 1. Redistributions of source code must retain the above copyright 14 * notice, this list of conditions and the following disclaimer. 15 * 2. Redistributions in binary form must reproduce the above copyright 16 * notice, this list of conditions and the following disclaimer in the 17 * documentation and/or other materials provided with the distribution. 18 * 19 * THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS 20 * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 21 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 22 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS 23 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 24 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 25 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 26 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 27 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 28 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 29 * POSSIBILITY OF SUCH DAMAGE. 30 */ 31 32#include <ctype.h> 33#include <err.h> 34#include <errno.h> 35#include <getopt.h> 36#include <math.h> 37#include <locale.h> 38#include <stdio.h> 39#include <stdlib.h> 40#include <string.h> 41#include <unistd.h> 42 43#define VERSION "1.0" 44#define ZERO '0' 45#define SPACE ' ' 46 47#define MAXIMUM(a, b) (((a) < (b))? (b) : (a)) 48#define ISSIGN(c) ((int)(c) == '-' || (int)(c) == '+') 49#define ISEXP(c) ((int)(c) == 'e' || (int)(c) == 'E') 50#define ISODIGIT(c) ((int)(c) >= '0' && (int)(c) <= '7') 51 52/* Globals */ 53 54static const char *decimal_point = "."; /* default */ 55static char default_format[] = { "%g" }; /* default */ 56 57static const struct option long_opts[] = { 58 {"format", required_argument, NULL, 'f'}, 59 {"help", no_argument, NULL, 'h'}, 60 {"separator", required_argument, NULL, 's'}, 61 {"version", no_argument, NULL, 'v'}, 62 {"equal-width", no_argument, NULL, 'w'}, 63 {NULL, no_argument, NULL, 0} 64}; 65 66/* Prototypes */ 67 68static double e_atof(const char *); 69 70static int decimal_places(const char *); 71static int numeric(const char *); 72static int valid_format(const char *); 73 74static char *generate_format(double, double, double, int, char); 75 76static __dead void usage(int error); 77 78/* 79 * The seq command will print out a numeric sequence from 1, the default, 80 * to a user specified upper limit by 1. The lower bound and increment 81 * maybe indicated by the user on the command line. The sequence can 82 * be either whole, the default, or decimal numbers. 83 */ 84int 85main(int argc, char *argv[]) 86{ 87 int c = 0; 88 int equalize = 0; 89 double first = 1.0; 90 double last = 0.0; 91 double incr = 0.0; 92 double prev = 0.0; 93 double cur, step; 94 struct lconv *locale; 95 char *fmt = NULL; 96 const char *sep = "\n"; 97 const char *term = "\n"; 98 char *cur_print, *last_print, *prev_print; 99 char pad = ZERO; 100 101 if (pledge("stdio", NULL) == -1) 102 err(1, "pledge"); 103 104 /* Determine the locale's decimal point. */ 105 locale = localeconv(); 106 if (locale && locale->decimal_point && locale->decimal_point[0] != '\0') 107 decimal_point = locale->decimal_point; 108 109 /* 110 * Process options, but handle negative numbers separately 111 * least they trip up getopt(3). 112 */ 113 while ((optind < argc) && !numeric(argv[optind]) && 114 (c = getopt_long(argc, argv, "+f:s:w", long_opts, NULL)) != -1) { 115 116 switch (c) { 117 case 'f': /* format (plan9/GNU) */ 118 fmt = optarg; 119 equalize = 0; 120 break; 121 case 's': /* separator (GNU) */ 122 sep = optarg; 123 break; 124 case 'v': /* version (GNU) */ 125 printf("seq version %s\n", VERSION); 126 return 0; 127 case 'w': /* equal width (plan9/GNU) */ 128 if (fmt == NULL) { 129 if (equalize++) 130 pad = SPACE; 131 } 132 break; 133 case 'h': /* help (GNU) */ 134 usage(0); 135 break; 136 default: 137 usage(1); 138 break; 139 } 140 } 141 142 argc -= optind; 143 argv += optind; 144 if (argc < 1 || argc > 3) 145 usage(1); 146 147 last = e_atof(argv[argc - 1]); 148 149 if (argc > 1) 150 first = e_atof(argv[0]); 151 152 if (argc > 2) { 153 incr = e_atof(argv[1]); 154 /* Plan 9/GNU don't do zero */ 155 if (incr == 0.0) 156 errx(1, "zero %screment", (first < last) ? "in" : "de"); 157 } 158 159 /* default is one for Plan 9/GNU work alike */ 160 if (incr == 0.0) 161 incr = (first < last) ? 1.0 : -1.0; 162 163 if (incr <= 0.0 && first < last) 164 errx(1, "needs positive increment"); 165 166 if (incr >= 0.0 && first > last) 167 errx(1, "needs negative decrement"); 168 169 if (fmt != NULL) { 170 if (!valid_format(fmt)) 171 errx(1, "invalid format string: `%s'", fmt); 172 /* 173 * XXX to be bug for bug compatible with Plan 9 add a 174 * newline if none found at the end of the format string. 175 */ 176 } else 177 fmt = generate_format(first, incr, last, equalize, pad); 178 179 for (step = 1, cur = first; incr > 0 ? cur <= last : cur >= last; 180 cur = first + incr * step++) { 181 if (cur != first) 182 fputs(sep, stdout); 183 printf(fmt, cur); 184 prev = cur; 185 } 186 187 /* 188 * Did we miss the last value of the range in the loop above? 189 * 190 * We might have, so check if the printable version of the last 191 * computed value ('cur') and desired 'last' value are equal. If 192 * they are equal after formatting truncation, but 'cur' and 'prev' 193 * are different, it means the exit condition of the loop held true 194 * due to a rounding error and we still need to print 'last'. 195 */ 196 if (asprintf(&cur_print, fmt, cur) == -1 || 197 asprintf(&last_print, fmt, last) == -1 || 198 asprintf(&prev_print, fmt, prev) == -1) 199 err(1, "asprintf"); 200 if (strcmp(cur_print, last_print) == 0 && 201 strcmp(cur_print, prev_print) != 0) { 202 if (cur != first) 203 fputs(sep, stdout); 204 fputs(last_print, stdout); 205 } 206 free(cur_print); 207 free(last_print); 208 free(prev_print); 209 210 fputs(term, stdout); 211 212 return 0; 213} 214 215/* 216 * numeric - verify that string is numeric 217 */ 218static int 219numeric(const char *s) 220{ 221 int seen_decimal_pt, decimal_pt_len; 222 223 /* skip any sign */ 224 if (ISSIGN((unsigned char)*s)) 225 s++; 226 227 seen_decimal_pt = 0; 228 decimal_pt_len = strlen(decimal_point); 229 while (*s) { 230 if (!isdigit((unsigned char)*s)) { 231 if (!seen_decimal_pt && 232 strncmp(s, decimal_point, decimal_pt_len) == 0) { 233 s += decimal_pt_len; 234 seen_decimal_pt = 1; 235 continue; 236 } 237 if (ISEXP((unsigned char)*s)) { 238 s++; 239 if (ISSIGN((unsigned char)*s) || 240 isdigit((unsigned char)*s)) { 241 s++; 242 continue; 243 } 244 } 245 break; 246 } 247 s++; 248 } 249 return *s == '\0'; 250} 251 252/* 253 * valid_format - validate user specified format string 254 */ 255static int 256valid_format(const char *fmt) 257{ 258 unsigned conversions = 0; 259 260 while (*fmt != '\0') { 261 /* scan for conversions */ 262 if (*fmt != '%') { 263 fmt++; 264 continue; 265 } 266 fmt++; 267 268 /* allow %% but not things like %10% */ 269 if (*fmt == '%') { 270 fmt++; 271 continue; 272 } 273 274 /* flags */ 275 while (*fmt != '\0' && strchr("#0- +'", *fmt)) { 276 fmt++; 277 } 278 279 /* field width */ 280 while (*fmt != '\0' && strchr("0123456789", *fmt)) { 281 fmt++; 282 } 283 284 /* precision */ 285 if (*fmt == '.') { 286 fmt++; 287 while (*fmt != '\0' && strchr("0123456789", *fmt)) { 288 fmt++; 289 } 290 } 291 292 /* conversion */ 293 switch (*fmt) { 294 case 'A': 295 case 'a': 296 case 'E': 297 case 'e': 298 case 'F': 299 case 'f': 300 case 'G': 301 case 'g': 302 /* floating point formats are accepted */ 303 conversions++; 304 break; 305 default: 306 /* anything else is not */ 307 return 0; 308 } 309 } 310 311 /* PR 236347 -- user format strings must have a conversion */ 312 return conversions == 1; 313} 314 315/* 316 * e_atof - convert an ASCII string to a double 317 * exit if string is not a valid double, or if converted value would 318 * cause overflow or underflow 319 */ 320static double 321e_atof(const char *num) 322{ 323 char *endp; 324 double dbl; 325 326 errno = 0; 327 dbl = strtod(num, &endp); 328 329 if (errno == ERANGE) 330 /* under or overflow */ 331 err(2, "%s", num); 332 else if (*endp != '\0') 333 /* "junk" left in number */ 334 errx(2, "invalid floating point argument: %s", num); 335 336 /* zero shall have no sign */ 337 if (dbl == -0.0) 338 dbl = 0; 339 return dbl; 340} 341 342/* 343 * decimal_places - count decimal places in a number (string) 344 */ 345static int 346decimal_places(const char *number) 347{ 348 int places = 0; 349 char *dp; 350 351 /* look for a decimal point */ 352 if ((dp = strstr(number, decimal_point))) { 353 dp += strlen(decimal_point); 354 355 while (isdigit((unsigned char)*dp++)) 356 places++; 357 } 358 return places; 359} 360 361/* 362 * generate_format - create a format string 363 * 364 * XXX to be bug for bug compatible with Plan9 and GNU return "%g" 365 * when "%g" prints as "%e" (this way no width adjustments are made) 366 */ 367static char * 368generate_format(double first, double incr, double last, int equalize, char pad) 369{ 370 static char buf[256]; 371 char cc = '\0'; 372 int precision, width1, width2, places; 373 374 if (equalize == 0) 375 return default_format; 376 377 /* figure out "last" value printed */ 378 if (first > last) 379 last = first - incr * floor((first - last) / incr); 380 else 381 last = first + incr * floor((last - first) / incr); 382 383 snprintf(buf, sizeof(buf), "%g", incr); 384 if (strchr(buf, 'e')) 385 cc = 'e'; 386 precision = decimal_places(buf); 387 388 width1 = snprintf(buf, sizeof(buf), "%g", first); 389 if (strchr(buf, 'e')) 390 cc = 'e'; 391 if ((places = decimal_places(buf))) 392 width1 -= (places + strlen(decimal_point)); 393 394 precision = MAXIMUM(places, precision); 395 396 width2 = snprintf(buf, sizeof(buf), "%g", last); 397 if (strchr(buf, 'e')) 398 cc = 'e'; 399 if ((places = decimal_places(buf))) 400 width2 -= (places + strlen(decimal_point)); 401 402 /* XXX if incr is floating point fix the precision */ 403 if (precision) { 404 snprintf(buf, sizeof(buf), "%%%c%d.%d%c", pad, 405 MAXIMUM(width1, width2) + (int)strlen(decimal_point) + 406 precision, precision, (cc) ? cc : 'f'); 407 } else { 408 snprintf(buf, sizeof(buf), "%%%c%d%c", pad, 409 MAXIMUM(width1, width2), (cc) ? cc : 'g'); 410 } 411 412 return buf; 413} 414 415static __dead void 416usage(int error) 417{ 418 fprintf(stderr, 419 "usage: %s [-w] [-f format] [-s string] [first [incr]] last\n", 420 getprogname()); 421 exit(error); 422} 423