1 /****************************************************************************
2 * Copyright 2020,2021 Thomas E. Dickey *
3 * Copyright 2008-2016,2017 Free Software Foundation, Inc. *
4 * *
5 * Permission is hereby granted, free of charge, to any person obtaining a *
6 * copy of this software and associated documentation files (the *
7 * "Software"), to deal in the Software without restriction, including *
8 * without limitation the rights to use, copy, modify, merge, publish, *
9 * distribute, distribute with modifications, sublicense, and/or sell *
10 * copies of the Software, and to permit persons to whom the Software is *
11 * furnished to do so, subject to the following conditions: *
12 * *
13 * The above copyright notice and this permission notice shall be included *
14 * in all copies or substantial portions of the Software. *
15 * *
16 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS *
17 * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF *
18 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. *
19 * IN NO EVENT SHALL THE ABOVE COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, *
20 * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR *
21 * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR *
22 * THE USE OR OTHER DEALINGS IN THE SOFTWARE. *
23 * *
24 * Except as contained in this notice, the name(s) of the above copyright *
25 * holders shall not be used in advertising or otherwise to promote the *
26 * sale, use or other dealings in this Software without prior written *
27 * authorization. *
28 ****************************************************************************/
29
30 /****************************************************************************
31 * Author: Thomas E. Dickey 2008 *
32 ****************************************************************************/
33
34 /*
35 * tabs.c -- set terminal hard-tabstops
36 */
37
38 #define USE_LIBTINFO
39 #include <progs.priv.h>
40 #include <tty_settings.h>
41
42 MODULE_ID("$Id: tabs.c,v 1.50 2021/10/10 00:54:41 tom Exp $")
43
44 static GCC_NORETURN void usage(void);
45
46 const char *_nc_progname;
47 static int max_cols;
48
49 static void
failed(const char * s)50 failed(const char *s)
51 {
52 perror(s);
53 ExitProgram(EXIT_FAILURE);
54 }
55
56 static int
putch(int c)57 putch(int c)
58 {
59 return putchar(c);
60 }
61
62 static char *
skip_csi(char * value)63 skip_csi(char *value)
64 {
65 if (UChar(*value) == 0x9b)
66 ++value;
67 else if (!strncmp(value, "\033[", 2))
68 value += 2;
69 return value;
70 }
71
72 /*
73 * If the terminal uses ANSI clear_all_tabs, then it is not necessary to first
74 * move to the left margin before clearing tabs.
75 */
76 static bool
ansi_clear_tabs(void)77 ansi_clear_tabs(void)
78 {
79 bool result = FALSE;
80 if (VALID_STRING(clear_all_tabs)) {
81 char *param = skip_csi(clear_all_tabs);
82 if (!strcmp(param, "3g"))
83 result = TRUE;
84 }
85 return result;
86 }
87
88 static void
do_tabs(int * tab_list)89 do_tabs(int *tab_list)
90 {
91 int last = 1;
92 int stop;
93 bool first = TRUE;
94
95 while ((stop = *tab_list++) > 0) {
96 if (first) {
97 first = FALSE;
98 putchar('\r');
99 }
100 if (last < stop) {
101 while (last++ < stop) {
102 if (last > max_cols)
103 break;
104 putchar(' ');
105 }
106 }
107 if (stop <= max_cols) {
108 tputs(set_tab, 1, putch);
109 last = stop;
110 } else {
111 break;
112 }
113 }
114 putchar('\r');
115 }
116
117 /*
118 * Decode a list of tab-stops from a string, returning an array of integers.
119 * If the margin is positive (because the terminal does not support margins),
120 * work around this by adding the margin to the decoded values.
121 */
122 static int *
decode_tabs(const char * tab_list,int margin)123 decode_tabs(const char *tab_list, int margin)
124 {
125 int *result = typeCalloc(int, strlen(tab_list) + (unsigned) max_cols);
126 int n = 0;
127 int value = 0;
128 int prior = 0;
129 int ch;
130
131 if (result == 0)
132 failed("decode_tabs");
133
134 if (margin < 0)
135 margin = 0;
136
137 while ((ch = *tab_list++) != '\0') {
138 if (isdigit(UChar(ch))) {
139 value *= 10;
140 value += (ch - '0');
141 } else if (ch == ',') {
142 result[n] = value + prior + margin;
143 if (n > 0 && result[n] <= result[n - 1]) {
144 fprintf(stderr,
145 "%s: tab-stops are not in increasing order: %d %d\n",
146 _nc_progname, value, result[n - 1]);
147 free(result);
148 result = 0;
149 break;
150 }
151 ++n;
152 value = 0;
153 prior = 0;
154 } else if (ch == '+') {
155 if (n)
156 prior = result[n - 1];
157 }
158 }
159
160 if (result != 0) {
161 /*
162 * If there is only one value, then it is an option such as "-8".
163 */
164 if ((n == 0) && (value > 0)) {
165 int step = value;
166 value = 1;
167 while (n < max_cols - 1) {
168 result[n++] = value + margin;
169 value += step;
170 }
171 }
172
173 /*
174 * Add the last value, if any.
175 */
176 result[n++] = value + prior + margin;
177 result[n] = 0;
178 }
179
180 return result;
181 }
182
183 static void
print_ruler(int * tab_list,const char * new_line)184 print_ruler(int *tab_list, const char *new_line)
185 {
186 int last = 0;
187 int n;
188
189 /* first print a readable ruler */
190 for (n = 0; n < max_cols; n += 10) {
191 int ch = 1 + (n / 10);
192 char buffer[20];
193 _nc_SPRINTF(buffer, _nc_SLIMIT(sizeof(buffer))
194 "----+----%c",
195 ((ch < 10)
196 ? (ch + '0')
197 : (ch + 'A' - 10)));
198 printf("%.*s", ((max_cols - n) > 10) ? 10 : (max_cols - n), buffer);
199 }
200 printf("%s", new_line);
201
202 /* now, print '*' for each stop */
203 for (n = 0, last = 0; (tab_list[n] > 0) && (last < max_cols); ++n) {
204 int stop = tab_list[n];
205
206 while (++last < stop) {
207 if (last <= max_cols) {
208 putchar('-');
209 } else {
210 break;
211 }
212 }
213 if (last <= max_cols) {
214 putchar('*');
215 last = stop;
216 } else {
217 break;
218 }
219 }
220 while (++last <= max_cols)
221 putchar('-');
222 printf("%s", new_line);
223 }
224
225 /*
226 * Write an '*' on each tabstop, to demonstrate whether it lines up with the
227 * ruler.
228 */
229 static void
write_tabs(int * tab_list,const char * new_line)230 write_tabs(int *tab_list, const char *new_line)
231 {
232 int stop;
233
234 while ((stop = *tab_list++) > 0 && stop <= max_cols) {
235 fputs((stop == 1) ? "*" : "\t*", stdout);
236 };
237 /* also show a tab _past_ the stops */
238 if (stop < max_cols)
239 fputs("\t+", stdout);
240 fputs(new_line, stdout);
241 }
242
243 /*
244 * Trim leading/trailing blanks, as well as blanks after a comma.
245 * Convert embedded blanks to commas.
246 */
247 static char *
trimmed_tab_list(const char * source)248 trimmed_tab_list(const char *source)
249 {
250 char *result = strdup(source);
251 if (result != 0) {
252 int j, k, last;
253
254 for (j = k = last = 0; result[j] != 0; ++j) {
255 int ch = UChar(result[j]);
256 if (isspace(ch)) {
257 if (last == '\0') {
258 continue;
259 } else if (isdigit(last) || last == ',') {
260 ch = ',';
261 }
262 } else if (ch == ',') {
263 ;
264 } else {
265 if (last == ',')
266 result[k++] = (char) last;
267 result[k++] = (char) ch;
268 }
269 last = ch;
270 }
271 result[k] = '\0';
272 }
273 return result;
274 }
275
276 static bool
comma_is_needed(const char * source)277 comma_is_needed(const char *source)
278 {
279 bool result = FALSE;
280
281 if (source != 0) {
282 size_t len = strlen(source);
283 if (len != 0)
284 result = (source[len - 1] != ',');
285 } else {
286 result = FALSE;
287 }
288 return result;
289 }
290
291 /*
292 * Add a command-line parameter to the tab-list. It can be blank- or comma-
293 * separated (or a mixture). For simplicity, empty tabs are ignored, e.g.,
294 * tabs 1,,6,11
295 * tabs 1,6,11
296 * are treated the same.
297 */
298 static const char *
add_to_tab_list(char ** append,const char * value)299 add_to_tab_list(char **append, const char *value)
300 {
301 char *result = *append;
302 char *copied = trimmed_tab_list(value);
303
304 if (copied != 0 && *copied != '\0') {
305 const char *comma = ",";
306 size_t need = 1 + strlen(copied);
307
308 if (*copied == ',')
309 comma = "";
310 else if (!comma_is_needed(*append))
311 comma = "";
312
313 need += strlen(comma);
314 if (*append != 0)
315 need += strlen(*append);
316
317 result = malloc(need);
318 if (result == 0)
319 failed("add_to_tab_list");
320
321 *result = '\0';
322 if (*append != 0) {
323 _nc_STRCPY(result, *append, need);
324 free(*append);
325 }
326 _nc_STRCAT(result, comma, need);
327 _nc_STRCAT(result, copied, need);
328
329 *append = result;
330 }
331 free(copied);
332 return result;
333 }
334
335 /*
336 * If the terminal supports it, (re)set the left margin and return true.
337 * Otherwise, return false.
338 */
339 static bool
do_set_margin(int margin,bool no_op)340 do_set_margin(int margin, bool no_op)
341 {
342 bool result = FALSE;
343
344 if (margin == 0) { /* 0 is special case for resetting */
345 if (VALID_STRING(clear_margins)) {
346 result = TRUE;
347 if (!no_op)
348 tputs(clear_margins, 1, putch);
349 }
350 } else if (margin-- < 0) { /* margin will be 0-based from here on */
351 result = TRUE;
352 } else if (VALID_STRING(set_left_margin)) {
353 result = TRUE;
354 if (!no_op) {
355 /*
356 * assuming we're on the first column of the line, move the cursor
357 * to the column at which we will set a margin.
358 */
359 if (VALID_STRING(column_address)) {
360 tputs(TIPARM_1(column_address, margin), 1, putch);
361 } else if (margin >= 1) {
362 if (VALID_STRING(parm_right_cursor)) {
363 tputs(TIPARM_1(parm_right_cursor, margin), 1, putch);
364 } else {
365 while (margin-- > 0)
366 putch(' ');
367 }
368 }
369 tputs(set_left_margin, 1, putch);
370 }
371 } else if (VALID_STRING(set_left_margin_parm)) {
372 result = TRUE;
373 if (!no_op) {
374 if (VALID_STRING(set_right_margin_parm)) {
375 tputs(TIPARM_1(set_left_margin_parm, margin), 1, putch);
376 } else {
377 tputs(TIPARM_2(set_left_margin_parm, margin, max_cols), 1, putch);
378 }
379 }
380 } else if (VALID_STRING(set_lr_margin)) {
381 result = TRUE;
382 if (!no_op) {
383 tputs(TIPARM_2(set_lr_margin, margin, max_cols), 1, putch);
384 }
385 }
386 return result;
387 }
388
389 /*
390 * Check for illegal characters in the tab-list.
391 */
392 static bool
legal_tab_list(const char * tab_list)393 legal_tab_list(const char *tab_list)
394 {
395 bool result = TRUE;
396
397 if (tab_list != 0 && *tab_list != '\0') {
398 if (comma_is_needed(tab_list)) {
399 int n;
400
401 for (n = 0; tab_list[n] != '\0'; ++n) {
402 int ch = UChar(tab_list[n]);
403
404 if (!(isdigit(ch) || ch == ',' || ch == '+')) {
405 fprintf(stderr,
406 "%s: unexpected character found '%c'\n",
407 _nc_progname, ch);
408 result = FALSE;
409 break;
410 }
411 }
412 } else {
413 fprintf(stderr, "%s: trailing comma found '%s'\n", _nc_progname, tab_list);
414 result = FALSE;
415 }
416 } else {
417 /* if no list given, default to "tabs -8" */
418 }
419 return result;
420 }
421
422 static char *
skip_list(char * value)423 skip_list(char *value)
424 {
425 while (*value != '\0' &&
426 (isdigit(UChar(*value)) ||
427 isspace(UChar(*value)) ||
428 strchr("+,", UChar(*value)) != 0)) {
429 ++value;
430 }
431 return value;
432 }
433
434 static void
usage(void)435 usage(void)
436 {
437 #define DATA(s) s "\n"
438 static const char msg[] =
439 {
440 DATA("Usage: tabs [options] [tabstop-list]")
441 DATA("")
442 DATA("Options:")
443 DATA(" -0 reset tabs")
444 DATA(" -8 set tabs to standard interval")
445 DATA(" -a Assembler, IBM S/370, first format")
446 DATA(" -a2 Assembler, IBM S/370, second format")
447 DATA(" -c COBOL, normal format")
448 DATA(" -c2 COBOL compact format")
449 DATA(" -c3 COBOL compact format extended")
450 DATA(" -d debug (show ruler with expected/actual tab positions)")
451 DATA(" -f FORTRAN")
452 DATA(" -n no-op (do not modify terminal settings)")
453 DATA(" -p PL/I")
454 DATA(" -s SNOBOL")
455 DATA(" -u UNIVAC 1100 Assembler")
456 DATA(" -T name use terminal type 'name'")
457 DATA(" -V print version")
458 DATA("")
459 DATA("A tabstop-list is an ordered list of column numbers, e.g., 1,11,21")
460 DATA("or 1,+10,+10 which is the same.")
461 };
462 #undef DATA
463
464 fflush(stdout);
465 fputs(msg, stderr);
466 ExitProgram(EXIT_FAILURE);
467 }
468
469 int
main(int argc,char * argv[])470 main(int argc, char *argv[])
471 {
472 int rc = EXIT_FAILURE;
473 bool debug = FALSE;
474 bool no_op = FALSE;
475 bool change_tty = FALSE;
476 int n, ch;
477 NCURSES_CONST char *term_name = 0;
478 char *append = 0;
479 const char *tab_list = 0;
480 const char *new_line = "\n";
481 int margin = -1;
482 TTY tty_settings;
483 int fd;
484
485 _nc_progname = _nc_rootname(argv[0]);
486
487 if ((term_name = getenv("TERM")) == 0)
488 term_name = "ansi+tabs";
489
490 /* cannot use getopt, since some options are two-character */
491 for (n = 1; n < argc; ++n) {
492 char *option = argv[n];
493 switch (option[0]) {
494 case '-':
495 while ((ch = *++option) != '\0') {
496 switch (ch) {
497 case 'a':
498 switch (*++option) {
499 default:
500 case '\0':
501 tab_list = "1,10,16,36,72";
502 option--;
503 /* Assembler, IBM S/370, first format */
504 break;
505 case '2':
506 tab_list = "1,10,16,40,72";
507 /* Assembler, IBM S/370, second format */
508 break;
509 }
510 break;
511 case 'c':
512 switch (*++option) {
513 default:
514 case '\0':
515 tab_list = "1,8,12,16,20,55";
516 option--;
517 /* COBOL, normal format */
518 break;
519 case '2':
520 tab_list = "1,6,10,14,49";
521 /* COBOL compact format */
522 break;
523 case '3':
524 tab_list = "1,6,10,14,18,22,26,30,34,38,42,46,50,54,58,62,67";
525 /* COBOL compact format extended */
526 break;
527 }
528 break;
529 case 'd': /* ncurses extension */
530 debug = TRUE;
531 break;
532 case 'f':
533 tab_list = "1,7,11,15,19,23";
534 /* FORTRAN */
535 break;
536 case 'n': /* ncurses extension */
537 no_op = TRUE;
538 break;
539 case 'p':
540 tab_list = "1,5,9,13,17,21,25,29,33,37,41,45,49,53,57,61";
541 /* PL/I */
542 break;
543 case 's':
544 tab_list = "1,10,55";
545 /* SNOBOL */
546 break;
547 case 'u':
548 tab_list = "1,12,20,44";
549 /* UNIVAC 1100 Assembler */
550 break;
551 case 'T':
552 ++n;
553 if (*++option != '\0') {
554 term_name = option;
555 } else {
556 term_name = argv[n];
557 option--;
558 }
559 option += ((int) strlen(option)) - 1;
560 continue;
561 case 'V':
562 puts(curses_version());
563 ExitProgram(EXIT_SUCCESS);
564 default:
565 if (isdigit(UChar(*option))) {
566 char *copy = strdup(option);
567 *skip_list(copy) = '\0';
568 tab_list = copy;
569 option = skip_list(option) - 1;
570 } else {
571 usage();
572 }
573 break;
574 }
575 }
576 break;
577 case '+':
578 if ((ch = *++option) != '\0') {
579 int digits = 0;
580 int number = 0;
581
582 switch (ch) {
583 case 'm':
584 /*
585 * The "+mXXX" option is unimplemented because only the long-obsolete
586 * att510d implements smgl, which is needed to support
587 * this option.
588 */
589 while ((ch = *++option) != '\0') {
590 if (isdigit(UChar(ch))) {
591 ++digits;
592 number = number * 10 + (ch - '0');
593 } else {
594 usage();
595 }
596 }
597 if (digits == 0)
598 number = 10;
599 margin = number;
600 break;
601 default:
602 /* special case of relative stops separated by spaces? */
603 if (option == argv[n] + 1) {
604 tab_list = add_to_tab_list(&append, argv[n]);
605 }
606 break;
607 }
608 }
609 break;
610 default:
611 if (append != 0) {
612 if (tab_list != (const char *) append) {
613 /* one of the predefined options was used */
614 free(append);
615 append = 0;
616 }
617 }
618 tab_list = add_to_tab_list(&append, option);
619 break;
620 }
621 }
622
623 fd = save_tty_settings(&tty_settings, FALSE);
624
625 setupterm(term_name, fd, (int *) 0);
626
627 max_cols = (columns > 0) ? columns : 80;
628 if (margin > 0)
629 max_cols -= margin;
630
631 if (!VALID_STRING(clear_all_tabs)) {
632 fprintf(stderr,
633 "%s: terminal type '%s' cannot reset tabs\n",
634 _nc_progname, term_name);
635 } else if (!VALID_STRING(set_tab)) {
636 fprintf(stderr,
637 "%s: terminal type '%s' cannot set tabs\n",
638 _nc_progname, term_name);
639 } else if (legal_tab_list(tab_list)) {
640 int *list;
641
642 if (tab_list == NULL)
643 tab_list = add_to_tab_list(&append, "8");
644
645 if (!no_op) {
646 #if defined(TERMIOS) && defined(OCRNL)
647 /* set tty modes to -ocrnl to allow \r */
648 if (isatty(STDOUT_FILENO)) {
649 TTY new_settings = tty_settings;
650 new_settings.c_oflag &= (unsigned)~OCRNL;
651 update_tty_settings(&tty_settings, &new_settings);
652 change_tty = TRUE;
653 new_line = "\r\n";
654 }
655 #endif
656
657 if (!ansi_clear_tabs())
658 putch('\r');
659 tputs(clear_all_tabs, 1, putch);
660 }
661
662 if (margin >= 0) {
663 putch('\r');
664 if (margin > 0) {
665 /* reset existing margin before setting margin, to reduce
666 * problems moving left of the current margin.
667 */
668 if (do_set_margin(0, no_op))
669 putch('\r');
670 }
671 if (do_set_margin(margin, no_op))
672 margin = -1;
673 }
674
675 list = decode_tabs(tab_list, margin);
676
677 if (list != 0) {
678 if (!no_op)
679 do_tabs(list);
680 if (debug) {
681 fflush(stderr);
682 printf("tabs %s%s", tab_list, new_line);
683 print_ruler(list, new_line);
684 write_tabs(list, new_line);
685 }
686 free(list);
687 } else if (debug) {
688 fflush(stderr);
689 printf("tabs %s%s", tab_list, new_line);
690 }
691 if (!no_op) {
692 if (change_tty) {
693 restore_tty_settings();
694 }
695 }
696 rc = EXIT_SUCCESS;
697 }
698 if (append != 0)
699 free(append);
700 ExitProgram(rc);
701 }
702