e9ed348eab5c8a92ec559befb6c3706fded62374
[platform/upstream/coreutils.git] / src / unexpand.c
1 /* unexpand - convert spaces to tabs
2    Copyright (C) 89, 91, 1995-1999 Free Software Foundation, Inc.
3
4    This program is free software; you can redistribute it and/or modify
5    it under the terms of the GNU General Public License as published by
6    the Free Software Foundation; either version 2, or (at your option)
7    any later version.
8
9    This program is distributed in the hope that it will be useful,
10    but WITHOUT ANY WARRANTY; without even the implied warranty of
11    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12    GNU General Public License for more details.
13
14    You should have received a copy of the GNU General Public License
15    along with this program; if not, write to the Free Software
16    Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.  */
17
18 /* By default, convert only maximal strings of initial blanks and tabs
19    into tabs.
20    Preserves backspace characters in the output; they decrement the
21    column count for tab calculations.
22    The default action is equivalent to -8.
23
24    Options:
25    --tabs=tab1[,tab2[,...]]
26    -t tab1[,tab2[,...]]
27    -tab1[,tab2[,...]]   If only one tab stop is given, set the tabs tab1
28                         spaces apart instead of the default 8.  Otherwise,
29                         set the tabs at columns tab1, tab2, etc. (numbered from
30                         0); replace any tabs beyond the tabstops given with
31                         single spaces.
32    --all
33    -a                   Use tabs wherever they would replace 2 or more spaces,
34                         not just at the beginnings of lines.
35
36    David MacKenzie <djm@gnu.ai.mit.edu> */
37
38 #include <config.h>
39
40 #include <stdio.h>
41 #include <getopt.h>
42 #include <sys/types.h>
43 #include "system.h"
44
45 #include "long-options.h"
46 #include "error.h"
47
48 /* The official name of this program (e.g., no `g' prefix).  */
49 #define PROGRAM_NAME "unexpand"
50
51 /* The number of bytes added at a time to the amount of memory
52    allocated for the output line. */
53 #define OUTPUT_BLOCK 256
54
55 /* The number of bytes added at a time to the amount of memory
56    allocated for the list of tabstops. */
57 #define TABLIST_BLOCK 256
58
59 /* The name this program was run with. */
60 char *program_name;
61
62 /* If nonzero, convert blanks even after nonblank characters have been
63    read on the line. */
64 static int convert_entire_line;
65
66 /* If nonzero, the size of all tab stops.  If zero, use `tab_list' instead. */
67 static int tab_size;
68
69 /* Array of the explicit column numbers of the tab stops;
70    after `tab_list' is exhausted, the rest of the line is printed
71    unchanged.  The first column is column 0. */
72 static int *tab_list;
73
74 /* The index of the first invalid element of `tab_list',
75    where the next element can be added. */
76 static int first_free_tab;
77
78 /* Null-terminated array of input filenames. */
79 static char **file_list;
80
81 /* Default for `file_list' if no files are given on the command line. */
82 static char *stdin_argv[] =
83 {
84   "-", NULL
85 };
86
87 /* Nonzero if we have ever read standard input. */
88 static int have_read_stdin;
89
90 /* Status to return to the system. */
91 static int exit_status;
92
93 static struct option const longopts[] =
94 {
95   {"tabs", required_argument, NULL, 't'},
96   {"all", no_argument, NULL, 'a'},
97   {NULL, 0, NULL, 0}
98 };
99
100 /* Add tab stop TABVAL to the end of `tab_list', except
101    if TABVAL is -1, do nothing. */
102
103 static void
104 add_tabstop (int tabval)
105 {
106   if (tabval == -1)
107     return;
108   if (first_free_tab % TABLIST_BLOCK == 0)
109     tab_list = (int *) xrealloc ((char *) tab_list,
110                                  first_free_tab + TABLIST_BLOCK);
111   tab_list[first_free_tab++] = tabval;
112 }
113
114 /* Add the comma or blank separated list of tabstops STOPS
115    to the list of tabstops. */
116
117 static void
118 parse_tabstops (const char *stops)
119 {
120   int tabval = -1;
121
122   for (; *stops; stops++)
123     {
124       if (*stops == ',' || ISBLANK (*stops))
125         {
126           add_tabstop (tabval);
127           tabval = -1;
128         }
129       else if (ISDIGIT (*stops))
130         {
131           if (tabval == -1)
132             tabval = 0;
133           tabval = tabval * 10 + *stops - '0';
134         }
135       else
136         error (EXIT_FAILURE, 0, _("tab size contains an invalid character"));
137     }
138
139   add_tabstop (tabval);
140 }
141
142 /* Check that the list of tabstops TABS, with ENTRIES entries,
143    contains only nonzero, ascending values. */
144
145 static void
146 validate_tabstops (const int *tabs, int entries)
147 {
148   int prev_tab = 0;
149   int i;
150
151   for (i = 0; i < entries; i++)
152     {
153       if (tabs[i] == 0)
154         error (EXIT_FAILURE, 0, _("tab size cannot be 0"));
155       if (tabs[i] <= prev_tab)
156         error (EXIT_FAILURE, 0, _("tab sizes must be ascending"));
157       prev_tab = tabs[i];
158     }
159 }
160
161 /* Close the old stream pointer FP if it is non-NULL,
162    and return a new one opened to read the next input file.
163    Open a filename of `-' as the standard input.
164    Return NULL if there are no more input files.  */
165
166 static FILE *
167 next_file (FILE *fp)
168 {
169   static char *prev_file;
170   char *file;
171
172   if (fp)
173     {
174       if (ferror (fp))
175         {
176           error (0, errno, "%s", prev_file);
177           exit_status = 1;
178         }
179       if (fp == stdin)
180         clearerr (fp);          /* Also clear EOF. */
181       else if (fclose (fp) == EOF)
182         {
183           error (0, errno, "%s", prev_file);
184           exit_status = 1;
185         }
186     }
187
188   while ((file = *file_list++) != NULL)
189     {
190       if (file[0] == '-' && file[1] == '\0')
191         {
192           have_read_stdin = 1;
193           prev_file = file;
194           return stdin;
195         }
196       fp = fopen (file, "r");
197       if (fp)
198         {
199           prev_file = file;
200           return fp;
201         }
202       error (0, errno, "%s", file);
203       exit_status = 1;
204     }
205   return NULL;
206 }
207
208 /* Change spaces to tabs, writing to stdout.
209    Read each file in `file_list', in order. */
210
211 static void
212 unexpand (void)
213 {
214   FILE *fp;                     /* Input stream. */
215   int c;                        /* Each input character. */
216   /* Index in `tab_list' of next tabstop: */
217   int tab_index = 0;            /* For calculating width of pending tabs. */
218   int print_tab_index = 0;      /* For printing as many tabs as possible. */
219   int column = 0;               /* Column on screen of next char. */
220   int next_tab_column;          /* Column the next tab stop is on. */
221   int convert = 1;              /* If nonzero, perform translations. */
222   int pending = 0;              /* Pending columns of blanks. */
223
224   fp = next_file ((FILE *) NULL);
225   if (fp == NULL)
226     return;
227
228   /* Binary I/O will preserve the original EOL style (DOS/Unix) of files.  */
229   SET_BINARY2 (fileno (fp), STDOUT_FILENO);
230
231   for (;;)
232     {
233       c = getc (fp);
234
235       if (c == ' ' && convert)
236         {
237           ++pending;
238           ++column;
239         }
240       else if (c == '\t' && convert)
241         {
242           if (tab_size == 0)
243             {
244               /* Do not let tab_index == first_free_tab;
245                  stop when it is 1 less. */
246               while (tab_index < first_free_tab - 1
247                      && column >= tab_list[tab_index])
248                 tab_index++;
249               next_tab_column = tab_list[tab_index];
250               if (tab_index < first_free_tab - 1)
251                 tab_index++;
252               if (column >= next_tab_column)
253                 {
254                   convert = 0;  /* Ran out of tab stops. */
255                   goto flush_pend;
256                 }
257             }
258           else
259             {
260               next_tab_column = column + tab_size - column % tab_size;
261             }
262           pending += next_tab_column - column;
263           column = next_tab_column;
264         }
265       else
266         {
267         flush_pend:
268           /* Flush pending spaces.  Print as many tabs as possible,
269              then print the rest as spaces. */
270           if (pending == 1)
271             {
272               putchar (' ');
273               pending = 0;
274             }
275           column -= pending;
276           while (pending != 0)
277             {
278               if (tab_size == 0)
279                 {
280                   /* Do not let print_tab_index == first_free_tab;
281                      stop when it is 1 less. */
282                   while (print_tab_index < first_free_tab - 1
283                          && column >= tab_list[print_tab_index])
284                     print_tab_index++;
285                   next_tab_column = tab_list[print_tab_index];
286                   if (print_tab_index < first_free_tab - 1)
287                     print_tab_index++;
288                 }
289               else
290                 {
291                   next_tab_column = column + tab_size - column % tab_size;
292                 }
293               if (next_tab_column - column <= pending)
294                 {
295                   putchar ('\t');
296                   pending -= next_tab_column - column;
297                   column = next_tab_column;
298                 }
299               else
300                 {
301                   --print_tab_index;
302                   column += pending;
303                   while (pending != 0)
304                     {
305                       putchar (' ');
306                       pending--;
307                     }
308                 }
309             }
310
311           if (c == EOF)
312             {
313               fp = next_file (fp);
314               if (fp == NULL)
315                 break;          /* No more files. */
316               else
317                 {
318                   SET_BINARY2 (fileno (fp), STDOUT_FILENO);
319                   continue;
320                 }
321             }
322
323           if (convert)
324             {
325               if (c == '\b')
326                 {
327                   if (column > 0)
328                     --column;
329                 }
330               else
331                 {
332                   ++column;
333                   if (convert_entire_line == 0)
334                     convert = 0;
335                 }
336             }
337
338           putchar (c);
339
340           if (c == '\n')
341             {
342               tab_index = print_tab_index = 0;
343               column = pending = 0;
344               convert = 1;
345             }
346         }
347     }
348 }
349
350 void
351 usage (int status)
352 {
353   if (status != 0)
354     fprintf (stderr, _("Try `%s --help' for more information.\n"),
355              program_name);
356   else
357     {
358       printf (_("\
359 Usage: %s [OPTION]... [FILE]...\n\
360 "),
361               program_name);
362       printf (_("\
363 Convert spaces in each FILE to tabs, writing to standard output.\n\
364 With no FILE, or when FILE is -, read standard input.\n\
365 \n\
366   -a, --all           convert all whitespace, instead of initial whitespace\n\
367   -t, --tabs=NUMBER   have tabs NUMBER characters apart instead of 8\n\
368   -t, --tabs=LIST     use comma separated list of explicit tab positions\n\
369       --help          display this help and exit\n\
370       --version       output version information and exit\n\
371 \n\
372 Instead of -t NUMBER or -t LIST, -NUMBER or -LIST may be used.\n\
373 "));
374       puts (_("\nReport bugs to <bug-textutils@gnu.org>."));
375     }
376   exit (status == 0 ? EXIT_SUCCESS : EXIT_FAILURE);
377 }
378
379 int
380 main (int argc, char **argv)
381 {
382   int tabval = -1;              /* Value of tabstop being read, or -1. */
383   int c;                        /* Option character. */
384
385   program_name = argv[0];
386   setlocale (LC_ALL, "");
387   bindtextdomain (PACKAGE, LOCALEDIR);
388   textdomain (PACKAGE);
389
390   parse_long_options (argc, argv, "unexpand", GNU_PACKAGE, VERSION,
391                       "David MacKenzie", usage);
392
393   have_read_stdin = 0;
394   exit_status = 0;
395   convert_entire_line = 0;
396   tab_list = NULL;
397   first_free_tab = 0;
398
399   while ((c = getopt_long (argc, argv, "at:,0123456789", longopts, NULL)) != -1)
400     {
401       switch (c)
402         {
403         case 0:
404           break;
405
406         case '?':
407           usage (1);
408         case 'a':
409           convert_entire_line = 1;
410           break;
411         case 't':
412           convert_entire_line = 1;
413           parse_tabstops (optarg);
414           break;
415         case ',':
416           add_tabstop (tabval);
417           tabval = -1;
418           break;
419         default:
420           if (tabval == -1)
421             tabval = 0;
422           tabval = tabval * 10 + c - '0';
423           break;
424         }
425     }
426
427   add_tabstop (tabval);
428
429   validate_tabstops (tab_list, first_free_tab);
430
431   if (first_free_tab == 0)
432     tab_size = 8;
433   else if (first_free_tab == 1)
434     tab_size = tab_list[0];
435   else
436     {
437       /* Append a sentinel to the list of tab stop indices.  */
438       add_tabstop (INT_MAX);
439       tab_size = 0;
440     }
441
442   if (optind == argc)
443     file_list = stdin_argv;
444   else
445     file_list = &argv[optind];
446
447   unexpand ();
448
449   if (have_read_stdin && fclose (stdin) == EOF)
450     error (EXIT_FAILURE, errno, "-");
451   if (fclose (stdout) == EOF)
452     error (EXIT_FAILURE, errno, _("write error"));
453   exit (exit_status == 0 ? EXIT_SUCCESS : EXIT_FAILURE);
454 }