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