+ in the interest of robustness, I added
[oweals/busybox.git] / 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 #if !defined(__GLIBC__) && (__GLIBC__ >= 2) && (__GLIBC_MINOR__ >= 1)
53 # include <linux/types.h>
54 #else
55 # include <sys/types.h>
56 #endif
57 #include <sys/stat.h>
58 #include <stdio.h>
59 #include <unistd.h>
60 #include <dirent.h>
61 #include <errno.h>
62 #include <stdio.h>
63 #ifdef BB_FEATURE_LS_TIMESTAMPS
64 #include <time.h>
65 #endif
66
67 #define TYPEINDEX(mode) (((mode) >> 12) & 0x0f)
68 #define TYPECHAR(mode)  ("0pcCd?bB-?l?s???" [TYPEINDEX(mode)])
69 #ifdef BB_FEATURE_LS_FILETYPES
70 #define APPCHAR(mode)   ("\0|\0\0/\0\0\0\0\0@\0=\0\0\0" [TYPEINDEX(mode)])
71 #endif
72
73 #define FMT_AUTO        0
74 #define FMT_LONG        1                       /* one record per line, extended info */
75 #define FMT_SINGLE      2                       /* one record per line */
76 #define FMT_ROWS        3                       /* print across rows */
77 #define FMT_COLUMNS     3                       /* fill columns (same, since we don't sort) */
78
79 #define TIME_MOD        0
80 #define TIME_CHANGE     1
81 #define TIME_ACCESS     2
82
83 #define DISP_FTYPE      1                       /* show character for file type */
84 #define DISP_EXEC       2                       /* show '*' if regular executable file */
85 #define DISP_HIDDEN     4                       /* show files starting . (except . and ..) */
86 #define DISP_DOT        8                       /* show . and .. */
87 #define DISP_NUMERIC    16              /* numeric uid and gid */
88 #define DISP_FULLTIME   32              /* show extended time display */
89 #define DIR_NOLIST      64                      /* show directory as itself, not contents */
90 #define DISP_DIRNAME    128             /* show directory name (for internal use) */
91
92 #ifndef MAJOR
93 #define MAJOR(dev) (((dev)>>8)&0xff)
94 #define MINOR(dev) ((dev)&0xff)
95 #endif
96
97 static unsigned char display_fmt = FMT_AUTO;
98 static unsigned short opts = 0;
99 static unsigned short column = 0;
100
101 #ifdef BB_FEATURE_AUTOWIDTH
102 static unsigned short terminal_width = 0;
103 static unsigned short column_width = 0;
104 static unsigned short toplevel_column_width = 0;
105 #else
106 #define terminal_width  TERMINAL_WIDTH
107 #define column_width    COLUMN_WIDTH
108 #endif
109
110 #ifdef BB_FEATURE_LS_TIMESTAMPS
111 static unsigned char time_fmt = TIME_MOD;
112 #endif
113
114 #define wr(data,len) fwrite(data, 1, len, stdout)
115
116 static void writenum(long val, short minwidth)
117 {
118         char scratch[128];
119
120         char *p = scratch + sizeof(scratch);
121         short len = 0;
122         short neg = (val < 0);
123
124         if (neg)
125                 val = -val;
126         do
127                 *--p = (val % 10) + '0', len++, val /= 10;
128         while (val);
129         if (neg)
130                 *--p = '-', len++;
131         while (len < minwidth)
132                 *--p = ' ', len++;
133         wr(p, len);
134         column += len;
135 }
136
137 static void newline(void)
138 {
139         if (column > 0) {
140                 wr("\n", 1);
141                 column = 0;
142         }
143 }
144
145 static void tab(short col)
146 {
147         static const char spaces[] = "                ";
148
149 #define nspaces ((sizeof spaces)-1)     /* null terminator! */
150
151         short n = col - column;
152
153         if (n > 0) {
154                 column = col;
155                 while (n > nspaces) {
156                         wr(spaces, nspaces);
157                         n -= nspaces;
158                 }
159                 /* must be 1...(sizeof spaces) left */
160                 wr(spaces, n);
161         }
162 #undef nspaces
163 }
164
165 #ifdef BB_FEATURE_LS_FILETYPES
166 static char append_char(mode_t mode)
167 {
168         if (!(opts & DISP_FTYPE))
169                 return '\0';
170         if ((opts & DISP_EXEC) && S_ISREG(mode)
171                 && (mode & (S_IXUSR | S_IXGRP | S_IXOTH))) return '*';
172         return APPCHAR(mode);
173 }
174 #endif
175
176 /**
177  **
178  ** Display a file or directory as a single item
179  ** (in either long or short format)
180  **
181  **/
182
183 static void list_single(const char *name, struct stat *info,
184                                                 const char *fullname)
185 {
186         char scratch[PATH_MAX + 1];
187         short len = strlen(name);
188
189 #ifdef BB_FEATURE_LS_FILETYPES
190         char append = append_char(info->st_mode);
191 #endif
192
193         if (display_fmt == FMT_LONG) {
194                 mode_t mode = info->st_mode;
195
196                 newline();
197                 wr(modeString(mode), 10);
198                 column = 10;
199                 writenum((long) info->st_nlink, (short) 5);
200                 fputs(" ", stdout);
201 #ifdef BB_FEATURE_LS_USERNAME
202                 if (!(opts & DISP_NUMERIC)) {
203                         memset(scratch, 0, sizeof(scratch));
204                         my_getpwuid(scratch, info->st_uid);
205                         if (*scratch) {
206                                 fputs(scratch, stdout);
207                                 if (strlen(scratch) <= 8)
208                                         wr("          ", 9 - strlen(scratch));
209                         } else {
210                                 writenum((long) info->st_uid, (short) 8);
211                                 fputs(" ", stdout);
212                         }
213                 } else
214 #endif
215                 {
216                         writenum((long) info->st_uid, (short) 8);
217                         fputs(" ", stdout);
218                 }
219 #ifdef BB_FEATURE_LS_USERNAME
220                 if (!(opts & DISP_NUMERIC)) {
221                         memset(scratch, 0, sizeof(scratch));
222                         my_getgrgid(scratch, info->st_gid);
223                         if (*scratch) {
224                                 fputs(scratch, stdout);
225                                 if (strlen(scratch) <= 8)
226                                         wr("         ", 8 - strlen(scratch));
227                         } else
228                                 writenum((long) info->st_gid, (short) 8);
229                 } else
230 #endif
231                         writenum((long) info->st_gid, (short) 8);
232                 //tab(26);
233                 if (S_ISBLK(mode) || S_ISCHR(mode)) {
234                         writenum((long) MAJOR(info->st_rdev), (short) 3);
235                         fputs(", ", stdout);
236                         writenum((long) MINOR(info->st_rdev), (short) 3);
237                 } else
238                         writenum((long) info->st_size, (short) 8);
239                 fputs(" ", stdout);
240                 //tab(32);
241 #ifdef BB_FEATURE_LS_TIMESTAMPS
242                 {
243                         time_t cal;
244                         char *string;
245
246                         switch (time_fmt) {
247                         case TIME_CHANGE:
248                                 cal = info->st_ctime;
249                                 break;
250                         case TIME_ACCESS:
251                                 cal = info->st_atime;
252                                 break;
253                         default:
254                                 cal = info->st_mtime;
255                                 break;
256                         }
257                         string = ctime(&cal);
258                         if (opts & DISP_FULLTIME)
259                                 wr(string, 24);
260                         else {
261                                 time_t age = time(NULL) - cal;
262
263                                 wr(string + 4, 7);      /* mmm_dd_ */
264                                 if (age < 3600L * 24 * 365 / 2 && age > -15 * 60)
265                                         /* hh:mm if less than 6 months old */
266                                         wr(string + 11, 5);
267                                 else
268                                         /* _yyyy otherwise */
269                                         wr(string + 19, 5);
270                         }
271                         wr(" ", 1);
272                 }
273 #else
274                 fputs("--- -- ----- ", stdout);
275 #endif
276                 wr(name, len);
277                 if (S_ISLNK(mode)) {
278                         wr(" -> ", 4);
279                         len = readlink(fullname, scratch, sizeof scratch);
280                         if (len > 0)
281                                 fwrite(scratch, 1, len, stdout);
282 #ifdef BB_FEATURE_LS_FILETYPES
283                         /* show type of destination */
284                         if (opts & DISP_FTYPE) {
285                                 if (!stat(fullname, info)) {
286                                         append = append_char(info->st_mode);
287                                         if (append)
288                                                 fputc(append, stdout);
289                                 }
290                         }
291 #endif
292                 }
293 #ifdef BB_FEATURE_LS_FILETYPES
294                 else if (append)
295                         wr(&append, 1);
296 #endif
297         } else {
298                 static short nexttab = 0;
299
300                 /* sort out column alignment */
301                 if (column == 0);               /* nothing to do */
302                 else if (display_fmt == FMT_SINGLE)
303                         newline();
304                 else {
305                         if (nexttab + column_width > terminal_width
306 #ifndef BB_FEATURE_AUTOWIDTH
307                                 || nexttab + len >= terminal_width
308 #endif
309                                 )
310                                 newline();
311                         else
312                                 tab(nexttab);
313                 }
314                 /* work out where next column starts */
315 #ifdef BB_FEATURE_AUTOWIDTH
316                 /* we know the calculated width is big enough */
317                 nexttab = column + column_width + COLUMN_GAP;
318 #else
319                 /* might cover more than one fixed-width column */
320                 nexttab = column;
321                 do
322                         nexttab += column_width + COLUMN_GAP;
323                 while (nexttab < (column + len + COLUMN_GAP));
324 #endif
325                 /* now write the data */
326                 wr(name, len);
327                 column = column + len;
328 #ifdef BB_FEATURE_LS_FILETYPES
329                 if (append)
330                         wr(&append, 1), column++;
331 #endif
332         }
333 }
334
335 /**
336  **
337  ** List the given file or directory, expanding a directory
338  ** to show its contents if required
339  **
340  **/
341
342 static int list_item(const char *name)
343 {
344         struct stat info;
345         DIR *dir;
346         struct dirent *entry;
347         char fullname[MAXNAMLEN + 1], *fnend;
348
349         if (lstat(name, &info))
350                 goto listerr;
351
352         if (!S_ISDIR(info.st_mode) || (opts & DIR_NOLIST)) {
353 #ifdef BB_FEATURE_AUTOWIDTH
354                 column_width = toplevel_column_width;
355 #endif
356                 list_single(name, &info, name);
357                 return 0;
358         }
359
360         /* Otherwise, it's a directory we want to list the contents of */
361
362         if (opts & DISP_DIRNAME) {      /* identify the directory */
363                 if (column)
364                         wr("\n\n", 2), column = 0;
365                 wr(name, strlen(name));
366                 wr(":\n", 2);
367         }
368
369         dir = opendir(name);
370         if (!dir)
371                 goto listerr;
372 #ifdef BB_FEATURE_AUTOWIDTH
373         column_width = 0;
374         while ((entry = readdir(dir)) != NULL) {
375                 short w = strlen(entry->d_name);
376
377                 if (column_width < w)
378                         column_width = w;
379         }
380 #ifdef HAS_REWINDDIR
381         rewinddir(dir);
382 #else
383         closedir(dir);
384         dir = opendir(name);
385         if (!dir)
386                 goto listerr;
387 #endif
388 #endif
389
390         /* List the contents */
391
392         strcpy(fullname, name);         /* *** ignore '.' by itself */
393         fnend = fullname + strlen(fullname);
394         if (fnend[-1] != '/')
395                 *fnend++ = '/';
396
397         while ((entry = readdir(dir)) != NULL) {
398                 const char *en = entry->d_name;
399
400                 if (en[0] == '.') {
401                         if (!en[1] || (en[1] == '.' && !en[2])) {       /* . or .. */
402                                 if (!(opts & DISP_DOT))
403                                         continue;
404                         } else if (!(opts & DISP_HIDDEN))
405                                 continue;
406                 }
407                 /* FIXME: avoid stat if not required */
408                 strcpy(fnend, entry->d_name);
409                 if (lstat(fullname, &info))
410                         goto direrr;            /* (shouldn't fail) */
411                 list_single(entry->d_name, &info, fullname);
412         }
413         closedir(dir);
414
415         if (opts & DISP_DIRNAME) {      /* separate the directory */
416                 if (column) {
417                         wr("\n", 1);
418                 }
419                 wr("\n", 1);
420                 column = 0;
421         }
422
423         return 0;
424
425   direrr:
426         closedir(dir);
427   listerr:
428         newline();
429         perror(name);
430         return 1;
431 }
432
433 static const char ls_usage[] = "ls [-1a"
434 #ifdef BB_FEATURE_LS_TIMESTAMPS
435         "c"
436 #endif
437         "d"
438 #ifdef BB_FEATURE_LS_TIMESTAMPS
439         "e"
440 #endif
441         "ln"
442 #ifdef BB_FEATURE_LS_FILETYPES
443         "p"
444 #endif
445 #ifdef BB_FEATURE_LS_TIMESTAMPS
446         "u"
447 #endif
448         "xAC"
449 #ifdef BB_FEATURE_LS_FILETYPES
450         "F"
451 #endif
452         "] [filenames...]\n\n"
453         "Options:\n"
454         "\t-a\tdo not hide entries starting with .\n"
455 #ifdef BB_FEATURE_LS_TIMESTAMPS
456         "\t-c\twith -l: show ctime (the time of last\n"
457     "\t\tmodification of file status information)\n"
458 #endif
459         "\t-d\tlist directory entries instead of contents\n"
460 #ifdef BB_FEATURE_LS_TIMESTAMPS
461         "\t-e\tlist both full date and full time\n"
462 #endif
463         "\t-l\tuse a long listing format\n"
464         "\t-n\tlist numeric UIDs and GIDs instead of names\n"
465 #ifdef BB_FEATURE_LS_FILETYPES
466         "\t-p\tappend indicator (one of /=@|) to entries\n"
467 #endif
468 #ifdef BB_FEATURE_LS_TIMESTAMPS
469         "\t-u\twith -l: show access time (the time of last\n"
470         "\t\taccess of the file)\n"
471 #endif
472         "\t-x\tlist entries by lines instead of by columns\n"
473         "\t-A\tdo not list implied . and ..\n"
474         "\t-C\tlist entries by columns\n"
475 #ifdef BB_FEATURE_LS_FILETYPES
476         "\t-F\tappend indicator (one of */=@|) to entries\n"
477 #endif
478         ;
479
480 extern int ls_main(int argc, char **argv)
481 {
482         int argi = 1, i;
483
484         /* process options */
485         while (argi < argc && argv[argi][0] == '-') {
486                 const char *p = &argv[argi][1];
487
488                 if (!*p)
489                         goto print_usage_message;       /* "-" by itself not allowed */
490                 if (*p == '-') {
491                         if (!p[1]) {            /* "--" forces end of options */
492                                 argi++;
493                                 break;
494                         }
495                         /* it's a long option name - we don't support them */
496                         goto print_usage_message;
497                 }
498
499                 while (*p)
500                         switch (*p++) {
501                         case 'l':
502                                 display_fmt = FMT_LONG;
503                                 break;
504                         case '1':
505                                 display_fmt = FMT_SINGLE;
506                                 break;
507                         case 'x':
508                                 display_fmt = FMT_ROWS;
509                                 break;
510                         case 'C':
511                                 display_fmt = FMT_COLUMNS;
512                                 break;
513 #ifdef BB_FEATURE_LS_FILETYPES
514                         case 'p':
515                                 opts |= DISP_FTYPE;
516                                 break;
517                         case 'F':
518                                 opts |= DISP_FTYPE | DISP_EXEC;
519                                 break;
520 #endif
521                         case 'A':
522                                 opts |= DISP_HIDDEN;
523                                 break;
524                         case 'a':
525                                 opts |= DISP_HIDDEN | DISP_DOT;
526                                 break;
527                         case 'n':
528                                 opts |= DISP_NUMERIC;
529                                 break;
530                         case 'd':
531                                 opts |= DIR_NOLIST;
532                                 break;
533 #ifdef BB_FEATURE_LS_TIMESTAMPS
534                         case 'u':
535                                 time_fmt = TIME_ACCESS;
536                                 break;
537                         case 'c':
538                                 time_fmt = TIME_CHANGE;
539                                 break;
540                         case 'e':
541                                 opts |= DISP_FULLTIME;
542                                 break;
543 #endif
544                         default:
545                                 goto print_usage_message;
546                         }
547
548                 argi++;
549         }
550
551         /* choose a display format */
552         if (display_fmt == FMT_AUTO)
553                 display_fmt = isatty(fileno(stdout)) ? FMT_COLUMNS : FMT_SINGLE;
554         if (argi < argc - 1)
555                 opts |= DISP_DIRNAME;   /* 2 or more items? label directories */
556 #ifdef BB_FEATURE_AUTOWIDTH
557         /* could add a -w option and/or TIOCGWINSZ call */
558         if (terminal_width < 1)
559                 terminal_width = TERMINAL_WIDTH;
560
561         for (i = argi; i < argc; i++) {
562                 int len = strlen(argv[i]);
563
564                 if (toplevel_column_width < len)
565                         toplevel_column_width = len;
566         }
567 #endif
568
569         /* process files specified, or current directory if none */
570         i = 0;
571         if (argi == argc)
572                 i = list_item(".");
573         while (argi < argc)
574                 i |= list_item(argv[argi++]);
575         newline();
576         exit(i);
577
578   print_usage_message:
579         usage(ls_usage);
580         exit(FALSE);
581 }