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