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