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