Extract usage information into a separate file.
[oweals/busybox.git] / coreutils / ls.c
1 /* vi: set sw=4 ts=4: */
2 /*
3  * tiny-ls.c version 0.1.0: A minimalist 'ls'
4  * Copyright (C) 1996 Brian Candler <B.Candler@pobox.com>
5  * 
6  *  This program is free software; you can redistribute it and/or modify
7  *  it under the terms of the GNU General Public License as published by
8  *  the Free Software Foundation; either version 2 of the License, or
9  *  (at your option) any later version.
10  *
11  *  This program is distributed in the hope that it will be useful,
12  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
13  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  *  GNU General Public License for more details.
15  *
16  *  You should have received a copy of the GNU General Public License
17  *  along with this program; if not, write to the Free Software
18  *  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
19  */
20
21 /*
22  * To achieve a small memory footprint, this version of 'ls' doesn't do any
23  * file sorting, and only has the most essential command line switches
24  * (i.e. the ones I couldn't live without :-) All features which involve
25  * linking in substantial chunks of libc can be disabled.
26  *
27  * Although I don't really want to add new features to this program to
28  * keep it small, I *am* interested to receive bug fixes and ways to make
29  * it more portable.
30  *
31  * KNOWN BUGS:
32  * 1. ls -l of a directory doesn't give "total <blocks>" header
33  * 2. ls of a symlink to a directory doesn't list directory contents
34  * 3. hidden files can make column width too large
35  *
36  * NON-OPTIMAL BEHAVIOUR:
37  * 1. autowidth reads directories twice
38  * 2. if you do a short directory listing without filetype characters
39  *    appended, there's no need to stat each one
40  * PORTABILITY:
41  * 1. requires lstat (BSD) - how do you do it without?
42  */
43
44 #define TERMINAL_WIDTH  80              /* use 79 if your terminal has linefold bug */
45 #define COLUMN_WIDTH    14              /* default if AUTOWIDTH not defined */
46 #define COLUMN_GAP      2                       /* includes the file type char, if present */
47 #define HAS_REWINDDIR
48
49 /************************************************************************/
50
51 #include "internal.h"
52 # include <sys/types.h>
53 #include <sys/stat.h>
54 #include <stdio.h>
55 #include <unistd.h>
56 #include <dirent.h>
57 #include <errno.h>
58 #include <stdio.h>
59 #ifdef BB_FEATURE_LS_TIMESTAMPS
60 #include <time.h>
61 #endif
62
63 #define TYPEINDEX(mode) (((mode) >> 12) & 0x0f)
64 #define TYPECHAR(mode)  ("0pcCd?bB-?l?s???" [TYPEINDEX(mode)])
65 #ifdef BB_FEATURE_LS_FILETYPES
66 #define APPCHAR(mode)   ("\0|\0\0/\0\0\0\0\0@\0=\0\0\0" [TYPEINDEX(mode)])
67 #endif
68
69 #define FMT_AUTO        0
70 #define FMT_LONG        1                       /* one record per line, extended info */
71 #define FMT_SINGLE      2                       /* one record per line */
72 #define FMT_ROWS        3                       /* print across rows */
73 #define FMT_COLUMNS     3                       /* fill columns (same, since we don't sort) */
74
75 #define TIME_MOD        0
76 #define TIME_CHANGE     1
77 #define TIME_ACCESS     2
78
79 #define DISP_FTYPE      1                       /* show character for file type */
80 #define DISP_EXEC       2                       /* show '*' if regular executable file */
81 #define DISP_HIDDEN     4                       /* show files starting . (except . and ..) */
82 #define DISP_DOT        8                       /* show . and .. */
83 #define DISP_NUMERIC    16              /* numeric uid and gid */
84 #define DISP_FULLTIME   32              /* show extended time display */
85 #define DIR_NOLIST              64              /* show directory as itself, not contents */
86 #define DISP_DIRNAME    128             /* show directory name (for internal use) */
87 #define DISP_RECURSIVE  256             /* Do a recursive listing */
88
89 #ifndef MAJOR
90 #define MAJOR(dev) (((dev)>>8)&0xff)
91 #define MINOR(dev) ((dev)&0xff)
92 #endif
93
94 static unsigned char display_fmt = FMT_AUTO;
95 static unsigned short opts = 0;
96 static unsigned short column = 0;
97
98 #ifdef BB_FEATURE_AUTOWIDTH
99 static unsigned short terminal_width = 0;
100 static unsigned short column_width = 0;
101 static unsigned short toplevel_column_width = 0;
102 #else
103 #define terminal_width  TERMINAL_WIDTH
104 #define column_width    COLUMN_WIDTH
105 #endif
106
107 #ifdef BB_FEATURE_LS_TIMESTAMPS
108 static unsigned char time_fmt = TIME_MOD;
109 #endif
110
111 #define wr(data,len) fwrite(data, 1, len, stdout)
112
113 static void writenum(long val, short minwidth)
114 {
115         char scratch[128];
116
117         char *p = scratch + sizeof(scratch);
118         short len = 0;
119         short neg = (val < 0);
120
121         if (neg)
122                 val = -val;
123         do
124                 *--p = (val % 10) + '0', len++, val /= 10;
125         while (val);
126         if (neg)
127                 *--p = '-', len++;
128         while (len < minwidth)
129                 *--p = ' ', len++;
130         wr(p, len);
131         column += len;
132 }
133
134 static void newline(void)
135 {
136         if (column > 0) {
137                 wr("\n", 1);
138                 column = 0;
139         }
140 }
141
142 static void tab(short col)
143 {
144         static const char spaces[] = "                ";
145
146 #define nspaces ((sizeof spaces)-1)     /* null terminator! */
147
148         short n = col - column;
149
150         if (n > 0) {
151                 column = col;
152                 while (n > nspaces) {
153                         wr(spaces, nspaces);
154                         n -= nspaces;
155                 }
156                 /* must be 1...(sizeof spaces) left */
157                 wr(spaces, n);
158         }
159 #undef nspaces
160 }
161
162 #ifdef BB_FEATURE_LS_FILETYPES
163 static char append_char(mode_t mode)
164 {
165         if (!(opts & DISP_FTYPE))
166                 return '\0';
167         if ((opts & DISP_EXEC) && S_ISREG(mode)
168                 && (mode & (S_IXUSR | S_IXGRP | S_IXOTH))) return '*';
169         return APPCHAR(mode);
170 }
171 #endif
172
173 /**
174  **
175  ** Display a file or directory as a single item
176  ** (in either long or short format)
177  **
178  **/
179
180 static void list_single(const char *name, struct stat *info,
181                                                 const char *fullname)
182 {
183         char scratch[BUFSIZ + 1];
184         short len = strlen(name);
185
186 #ifdef BB_FEATURE_LS_FILETYPES
187         char append = append_char(info->st_mode);
188 #endif
189
190         if (display_fmt == FMT_LONG) {
191                 mode_t mode = info->st_mode;
192
193                 newline();
194                 wr(modeString(mode), 10);
195                 column = 10;
196                 writenum((long) info->st_nlink, (short) 5);
197                 fputs(" ", stdout);
198 #ifdef BB_FEATURE_LS_USERNAME
199                 if (!(opts & DISP_NUMERIC)) {
200                         memset(scratch, 0, sizeof(scratch));
201                         my_getpwuid(scratch, info->st_uid);
202                         if (*scratch) {
203                                 fputs(scratch, stdout);
204                                 if (strlen(scratch) <= 8)
205                                         wr("          ", 9 - strlen(scratch));
206                         } else {
207                                 writenum((long) info->st_uid, (short) 8);
208                                 fputs(" ", stdout);
209                         }
210                 } else
211 #endif
212                 {
213                         writenum((long) info->st_uid, (short) 8);
214                         fputs(" ", stdout);
215                 }
216 #ifdef BB_FEATURE_LS_USERNAME
217                 if (!(opts & DISP_NUMERIC)) {
218                         memset(scratch, 0, sizeof(scratch));
219                         my_getgrgid(scratch, info->st_gid);
220                         if (*scratch) {
221                                 fputs(scratch, stdout);
222                                 if (strlen(scratch) <= 8)
223                                         wr("         ", 8 - strlen(scratch));
224                         } else
225                                 writenum((long) info->st_gid, (short) 8);
226                 } else
227 #endif
228                         writenum((long) info->st_gid, (short) 8);
229                 //tab(26);
230                 if (S_ISBLK(mode) || S_ISCHR(mode)) {
231                         writenum((long) MAJOR(info->st_rdev), (short) 3);
232                         fputs(", ", stdout);
233                         writenum((long) MINOR(info->st_rdev), (short) 3);
234                 } else
235                         writenum((long) info->st_size, (short) 8);
236                 fputs(" ", stdout);
237                 //tab(32);
238 #ifdef BB_FEATURE_LS_TIMESTAMPS
239                 {
240                         time_t cal;
241                         char *string;
242
243                         switch (time_fmt) {
244                         case TIME_CHANGE:
245                                 cal = info->st_ctime;
246                                 break;
247                         case TIME_ACCESS:
248                                 cal = info->st_atime;
249                                 break;
250                         default:
251                                 cal = info->st_mtime;
252                                 break;
253                         }
254                         string = ctime(&cal);
255                         if (opts & DISP_FULLTIME)
256                                 wr(string, 24);
257                         else {
258                                 time_t age = time(NULL) - cal;
259
260                                 wr(string + 4, 7);      /* mmm_dd_ */
261                                 if (age < 3600L * 24 * 365 / 2 && age > -15 * 60)
262                                         /* hh:mm if less than 6 months old */
263                                         wr(string + 11, 5);
264                                 else
265                                         /* _yyyy otherwise */
266                                         wr(string + 19, 5);
267                         }
268                         wr(" ", 1);
269                 }
270 #else
271                 fputs("--- -- ----- ", stdout);
272 #endif
273                 wr(name, len);
274                 if (S_ISLNK(mode)) {
275                         wr(" -> ", 4);
276                         len = readlink(fullname, scratch, sizeof scratch);
277                         if (len > 0)
278                                 fwrite(scratch, 1, len, stdout);
279 #ifdef BB_FEATURE_LS_FILETYPES
280                         /* show type of destination */
281                         if (opts & DISP_FTYPE) {
282                                 if (!stat(fullname, info)) {
283                                         append = append_char(info->st_mode);
284                                         if (append)
285                                                 fputc(append, stdout);
286                                 }
287                         }
288 #endif
289                 }
290 #ifdef BB_FEATURE_LS_FILETYPES
291                 else if (append)
292                         wr(&append, 1);
293 #endif
294         } else {
295                 static short nexttab = 0;
296
297                 /* sort out column alignment */
298                 if (column == 0);               /* nothing to do */
299                 else if (display_fmt == FMT_SINGLE)
300                         newline();
301                 else {
302                         if (nexttab + column_width > terminal_width
303 #ifndef BB_FEATURE_AUTOWIDTH
304                                 || nexttab + len >= terminal_width
305 #endif
306                                 )
307                                 newline();
308                         else
309                                 tab(nexttab);
310                 }
311                 /* work out where next column starts */
312 #ifdef BB_FEATURE_AUTOWIDTH
313                 /* we know the calculated width is big enough */
314                 nexttab = column + column_width + COLUMN_GAP;
315 #else
316                 /* might cover more than one fixed-width column */
317                 nexttab = column;
318                 do
319                         nexttab += column_width + COLUMN_GAP;
320                 while (nexttab < (column + len + COLUMN_GAP));
321 #endif
322                 /* now write the data */
323                 wr(name, len);
324                 column = column + len;
325 #ifdef BB_FEATURE_LS_FILETYPES
326                 if (append)
327                         wr(&append, 1), column++;
328 #endif
329         }
330 }
331
332 /**
333  **
334  ** List the given file or directory, expanding a directory
335  ** to show its contents if required
336  **
337  **/
338
339 static int list_item(const char *name)
340 {
341         struct stat info;
342         DIR *dir;
343         struct dirent *entry;
344         char fullname[BUFSIZ + 1], *fnend;
345
346         if (lstat(name, &info))
347                 goto listerr;
348
349         if (!S_ISDIR(info.st_mode) || (opts & DIR_NOLIST)) {
350 #ifdef BB_FEATURE_AUTOWIDTH
351                 column_width = toplevel_column_width;
352 #endif
353                 list_single(name, &info, name);
354                 return 0;
355         }
356
357         /* Otherwise, it's a directory we want to list the contents of */
358
359         if (opts & DISP_DIRNAME) {      /* identify the directory */
360                 if (column)
361                         wr("\n\n", 2), column = 0;
362                 wr(name, strlen(name));
363                 wr(":\n", 2);
364         }
365
366         dir = opendir(name);
367         if (!dir)
368                 goto listerr;
369 #ifdef BB_FEATURE_AUTOWIDTH
370         column_width = 0;
371         while ((entry = readdir(dir)) != NULL) {
372                 short w = strlen(entry->d_name);
373
374                 if (column_width < w)
375                         column_width = w;
376         }
377 #ifdef HAS_REWINDDIR
378         rewinddir(dir);
379 #else
380         closedir(dir);
381         dir = opendir(name);
382         if (!dir)
383                 goto listerr;
384 #endif
385 #endif
386
387         /* List the contents */
388
389         strcpy(fullname, name);         /* *** ignore '.' by itself */
390         fnend = fullname + strlen(fullname);
391         if (fnend[-1] != '/')
392                 *fnend++ = '/';
393
394         while ((entry = readdir(dir)) != NULL) {
395                 const char *en = entry->d_name;
396
397                 if (en[0] == '.') {
398                         if (!en[1] || (en[1] == '.' && !en[2])) {       /* . or .. */
399                                 if (!(opts & DISP_DOT))
400                                         continue;
401                         } else if (!(opts & DISP_HIDDEN))
402                                 continue;
403                 }
404                 /* FIXME: avoid stat if not required */
405                 strcpy(fnend, entry->d_name);
406                 if (lstat(fullname, &info))
407                         goto direrr;            /* (shouldn't fail) */
408                 list_single(entry->d_name, &info, fullname);
409         }
410         closedir(dir);
411
412         if (opts & DISP_DIRNAME) {      /* separate the directory */
413                 if (column) {
414                         wr("\n", 1);
415                 }
416                 wr("\n", 1);
417                 column = 0;
418         }
419
420         return 0;
421
422   direrr:
423         closedir(dir);
424   listerr:
425         newline();
426         perror(name);
427         return 1;
428 }
429
430 #ifdef BB_FEATURE_LS_RECURSIVE
431 static int dirAction(const char *fileName, struct stat *statbuf, void* junk)
432 {
433         int i;
434         fprintf(stdout, "\n%s:\n", fileName);
435         i = list_item(fileName);
436         newline();
437         return (i);
438 }
439 #endif
440
441 extern int ls_main(int argc, char **argv)
442 {
443         int argi = 1, i;
444
445         /* process options */
446         while (argi < argc && argv[argi][0] == '-') {
447                 const char *p = &argv[argi][1];
448
449                 if (!*p)
450                         goto print_usage_message;       /* "-" by itself not allowed */
451                 if (*p == '-') {
452                         if (!p[1]) {            /* "--" forces end of options */
453                                 argi++;
454                                 break;
455                         }
456                         /* it's a long option name - we don't support them */
457                         goto print_usage_message;
458                 }
459
460                 while (*p)
461                         switch (*p++) {
462                         case 'l':
463                                 display_fmt = FMT_LONG;
464                                 break;
465                         case '1':
466                                 display_fmt = FMT_SINGLE;
467                                 break;
468                         case 'x':
469                                 display_fmt = FMT_ROWS;
470                                 break;
471                         case 'C':
472                                 display_fmt = FMT_COLUMNS;
473                                 break;
474 #ifdef BB_FEATURE_LS_FILETYPES
475                         case 'p':
476                                 opts |= DISP_FTYPE;
477                                 break;
478                         case 'F':
479                                 opts |= DISP_FTYPE | DISP_EXEC;
480                                 break;
481 #endif
482                         case 'A':
483                                 opts |= DISP_HIDDEN;
484                                 break;
485                         case 'a':
486                                 opts |= DISP_HIDDEN | DISP_DOT;
487                                 break;
488                         case 'n':
489                                 opts |= DISP_NUMERIC;
490                                 break;
491                         case 'd':
492                                 opts |= DIR_NOLIST;
493                                 break;
494 #ifdef BB_FEATURE_LS_TIMESTAMPS
495                         case 'u':
496                                 time_fmt = TIME_ACCESS;
497                                 break;
498                         case 'c':
499                                 time_fmt = TIME_CHANGE;
500                                 break;
501                         case 'e':
502                                 opts |= DISP_FULLTIME;
503                                 break;
504 #endif
505 #ifdef BB_FEATURE_LS_RECURSIVE
506                         case 'R':
507                                 opts |= DISP_RECURSIVE;
508                                 break;
509 #endif
510                         case 'g': /* ignore -- for ftp servers */
511                                 break;
512                         default:
513                                 goto print_usage_message;
514                         }
515
516                 argi++;
517         }
518
519         /* choose a display format */
520         if (display_fmt == FMT_AUTO)
521                 display_fmt = isatty(fileno(stdout)) ? FMT_COLUMNS : FMT_SINGLE;
522         if (argi < argc - 1)
523                 opts |= DISP_DIRNAME;   /* 2 or more items? label directories */
524 #ifdef BB_FEATURE_AUTOWIDTH
525         /* could add a -w option and/or TIOCGWINSZ call */
526         if (terminal_width < 1)
527                 terminal_width = TERMINAL_WIDTH;
528
529         for (i = argi; i < argc; i++) {
530                 int len = strlen(argv[i]);
531
532                 if (toplevel_column_width < len)
533                         toplevel_column_width = len;
534         }
535 #endif
536
537         /* process files specified, or current directory if none */
538 #ifdef BB_FEATURE_LS_RECURSIVE
539         if (opts & DISP_RECURSIVE) {
540                 i = 0;
541                 if (argi == argc) {
542                         i = recursiveAction(".", TRUE, FALSE, FALSE, NULL, dirAction, NULL);
543                 }
544                 while (argi < argc) {
545                         i |= recursiveAction(argv[argi++], TRUE, FALSE, FALSE, NULL, dirAction, NULL);
546                 }
547         } else 
548 #endif
549         {
550                 i = 0;
551                 if (argi == argc)
552                         i = list_item(".");
553                 while (argi < argc)
554                         i |= list_item(argv[argi++]);
555                 newline();
556         }
557         exit(i);
558
559   print_usage_message:
560         usage(ls_usage);
561         exit(FALSE);
562 }