1/* GNU's pinky.
2   Copyright (C) 1992-1997, 1999-2006, 2008-2010 Free Software Foundation, Inc.
3
4   This program is free software: you can redistribute it and/or modify
5   it under the terms of the GNU General Public License as published by
6   the Free Software Foundation, either version 3 of the License, or
7   (at your option) any later version.
8
9   This program is distributed in the hope that it will be useful,
10   but WITHOUT ANY WARRANTY; without even the implied warranty of
11   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12   GNU General Public License for more details.
13
14   You should have received a copy of the GNU General Public License
15   along with this program.  If not, see <http://www.gnu.org/licenses/>.  */
16
17/* Created by hacking who.c by Kaveh Ghazi ghazi@caip.rutgers.edu */
18
19#include <config.h>
20#include <getopt.h>
21#include <pwd.h>
22#include <stdio.h>
23
24#include <sys/types.h>
25#include "system.h"
26
27#include "canon-host.h"
28#include "error.h"
29#include "hard-locale.h"
30#include "readutmp.h"
31
32/* The official name of this program (e.g., no `g' prefix).  */
33#define PROGRAM_NAME "pinky"
34
35#define AUTHORS \
36  proper_name ("Joseph Arceneaux"), \
37  proper_name ("David MacKenzie"), \
38  proper_name ("Kaveh Ghazi")
39
40#ifndef MAXHOSTNAMELEN
41# define MAXHOSTNAMELEN 64
42#endif
43
44char *ttyname (int);
45
46/* If true, display the hours:minutes since each user has touched
47   the keyboard, or blank if within the last minute, or days followed
48   by a 'd' if not within the last day. */
49static bool include_idle = true;
50
51/* If true, display a line at the top describing each field. */
52static bool include_heading = true;
53
54/* if true, display the user's full name from pw_gecos. */
55static bool include_fullname = true;
56
57/* if true, display the user's ~/.project file when doing long format. */
58static bool include_project = true;
59
60/* if true, display the user's ~/.plan file when doing long format. */
61static bool include_plan = true;
62
63/* if true, display the user's home directory and shell
64   when doing long format. */
65static bool include_home_and_shell = true;
66
67/* if true, use the "short" output format. */
68static bool do_short_format = true;
69
70/* if true, display the ut_host field. */
71#ifdef HAVE_UT_HOST
72static bool include_where = true;
73#endif
74
75/* The strftime format to use for login times, and its expected
76   output width.  */
77static char const *time_format;
78static int time_format_width;
79
80static struct option const longopts[] =
81{
82  {GETOPT_HELP_OPTION_DECL},
83  {GETOPT_VERSION_OPTION_DECL},
84  {NULL, 0, NULL, 0}
85};
86
87/* Count and return the number of ampersands in STR.  */
88
89static size_t
90count_ampersands (const char *str)
91{
92  size_t count = 0;
93  do
94    {
95      if (*str == '&')
96        count++;
97    } while (*str++);
98  return count;
99}
100
101/* Create a string (via xmalloc) which contains a full name by substituting
102   for each ampersand in GECOS_NAME the USER_NAME string with its first
103   character capitalized.  The caller must ensure that GECOS_NAME contains
104   no `,'s.  The caller also is responsible for free'ing the return value of
105   this function.  */
106
107static char *
108create_fullname (const char *gecos_name, const char *user_name)
109{
110  size_t rsize = strlen (gecos_name) + 1;
111  char *result;
112  char *r;
113  size_t ampersands = count_ampersands (gecos_name);
114
115  if (ampersands != 0)
116    {
117      size_t ulen = strlen (user_name);
118      size_t product = ampersands * ulen;
119      rsize += product - ampersands;
120      if (xalloc_oversized (ulen, ampersands) || rsize < product)
121        xalloc_die ();
122    }
123
124  r = result = xmalloc (rsize);
125
126  while (*gecos_name)
127    {
128      if (*gecos_name == '&')
129        {
130          const char *uname = user_name;
131          if (islower (to_uchar (*uname)))
132            *r++ = toupper (to_uchar (*uname++));
133          while (*uname)
134            *r++ = *uname++;
135        }
136      else
137        {
138          *r++ = *gecos_name;
139        }
140
141      gecos_name++;
142    }
143  *r = 0;
144
145  return result;
146}
147
148/* Return a string representing the time between WHEN and the time
149   that this function is first run. */
150
151static const char *
152idle_string (time_t when)
153{
154  static time_t now = 0;
155  static char buf[INT_STRLEN_BOUND (long int) + 2];
156  time_t seconds_idle;
157
158  if (now == 0)
159    time (&now);
160
161  seconds_idle = now - when;
162  if (seconds_idle < 60)	/* One minute. */
163    return "     ";
164  if (seconds_idle < (24 * 60 * 60))	/* One day. */
165    {
166      int hours = seconds_idle / (60 * 60);
167      int minutes = (seconds_idle % (60 * 60)) / 60;
168      sprintf (buf, "%02d:%02d", hours, minutes);
169    }
170  else
171    {
172      unsigned long int days = seconds_idle / (24 * 60 * 60);
173      sprintf (buf, "%lud", days);
174    }
175  return buf;
176}
177
178/* Return a time string.  */
179static const char *
180time_string (const STRUCT_UTMP *utmp_ent)
181{
182  static char buf[INT_STRLEN_BOUND (intmax_t) + sizeof "-%m-%d %H:%M"];
183
184  /* Don't take the address of UT_TIME_MEMBER directly.
185     Ulrich Drepper wrote:
186     ``... GNU libc (and perhaps other libcs as well) have extended
187     utmp file formats which do not use a simple time_t ut_time field.
188     In glibc, ut_time is a macro which selects for backward compatibility
189     the tv_sec member of a struct timeval value.''  */
190  time_t t = UT_TIME_MEMBER (utmp_ent);
191  struct tm *tmp = localtime (&t);
192
193  if (tmp)
194    {
195      strftime (buf, sizeof buf, time_format, tmp);
196      return buf;
197    }
198  else
199    return timetostr (t, buf);
200}
201
202/* Display a line of information about UTMP_ENT. */
203
204static void
205print_entry (const STRUCT_UTMP *utmp_ent)
206{
207  struct stat stats;
208  time_t last_change;
209  char mesg;
210
211#define DEV_DIR_WITH_TRAILING_SLASH "/dev/"
212#define DEV_DIR_LEN (sizeof (DEV_DIR_WITH_TRAILING_SLASH) - 1)
213
214  char line[sizeof (utmp_ent->ut_line) + DEV_DIR_LEN + 1];
215
216  /* Copy ut_line into LINE, prepending `/dev/' if ut_line is not
217     already an absolute file name.  Some system may put the full,
218     absolute file name in ut_line.  */
219  if (utmp_ent->ut_line[0] == '/')
220    {
221      strncpy (line, utmp_ent->ut_line, sizeof (utmp_ent->ut_line));
222      line[sizeof (utmp_ent->ut_line)] = '\0';
223    }
224  else
225    {
226      strcpy (line, DEV_DIR_WITH_TRAILING_SLASH);
227      strncpy (line + DEV_DIR_LEN, utmp_ent->ut_line, sizeof (utmp_ent->ut_line));
228      line[DEV_DIR_LEN + sizeof (utmp_ent->ut_line)] = '\0';
229    }
230
231  if (stat (line, &stats) == 0)
232    {
233      mesg = (stats.st_mode & S_IWGRP) ? ' ' : '*';
234      last_change = stats.st_atime;
235    }
236  else
237    {
238      mesg = '?';
239      last_change = 0;
240    }
241
242  printf ("%-8.*s", UT_USER_SIZE, UT_USER (utmp_ent));
243
244  if (include_fullname)
245    {
246      struct passwd *pw;
247      char name[UT_USER_SIZE + 1];
248
249      strncpy (name, UT_USER (utmp_ent), UT_USER_SIZE);
250      name[UT_USER_SIZE] = '\0';
251      pw = getpwnam (name);
252      if (pw == NULL)
253        /* TRANSLATORS: Real name is unknown; at most 19 characters. */
254        printf (" %19s", _("        ???"));
255      else
256        {
257          char *const comma = strchr (pw->pw_gecos, ',');
258          char *result;
259
260          if (comma)
261            *comma = '\0';
262
263          result = create_fullname (pw->pw_gecos, pw->pw_name);
264          printf (" %-19.19s", result);
265          free (result);
266        }
267    }
268
269  printf (" %c%-8.*s",
270          mesg, (int) sizeof (utmp_ent->ut_line), utmp_ent->ut_line);
271
272  if (include_idle)
273    {
274      if (last_change)
275        printf (" %-6s", idle_string (last_change));
276      else
277        /* TRANSLATORS: Idle time is unknown; at most 5 characters. */
278        printf (" %-6s", _("?????"));
279    }
280
281  printf (" %s", time_string (utmp_ent));
282
283#ifdef HAVE_UT_HOST
284  if (include_where && utmp_ent->ut_host[0])
285    {
286      char ut_host[sizeof (utmp_ent->ut_host) + 1];
287      char *host = NULL;
288      char *display = NULL;
289
290      /* Copy the host name into UT_HOST, and ensure it's nul terminated. */
291      strncpy (ut_host, utmp_ent->ut_host, (int) sizeof (utmp_ent->ut_host));
292      ut_host[sizeof (utmp_ent->ut_host)] = '\0';
293
294      /* Look for an X display.  */
295      display = strchr (ut_host, ':');
296      if (display)
297        *display++ = '\0';
298
299      if (*ut_host)
300        /* See if we can canonicalize it.  */
301        host = canon_host (ut_host);
302      if ( ! host)
303        host = ut_host;
304
305      if (display)
306        printf (" %s:%s", host, display);
307      else
308        printf (" %s", host);
309
310      if (host != ut_host)
311        free (host);
312    }
313#endif
314
315  putchar ('\n');
316}
317
318/* Display a verbose line of information about UTMP_ENT. */
319
320static void
321print_long_entry (const char name[])
322{
323  struct passwd *pw;
324
325  pw = getpwnam (name);
326
327  printf (_("Login name: "));
328  printf ("%-28s", name);
329
330  printf (_("In real life: "));
331  if (pw == NULL)
332    {
333      /* TRANSLATORS: Real name is unknown; no hard limit. */
334      printf (" %s", _("???\n"));
335      return;
336    }
337  else
338    {
339      char *const comma = strchr (pw->pw_gecos, ',');
340      char *result;
341
342      if (comma)
343        *comma = '\0';
344
345      result = create_fullname (pw->pw_gecos, pw->pw_name);
346      printf (" %s", result);
347      free (result);
348    }
349
350  putchar ('\n');
351
352  if (include_home_and_shell)
353    {
354      printf (_("Directory: "));
355      printf ("%-29s", pw->pw_dir);
356      printf (_("Shell: "));
357      printf (" %s", pw->pw_shell);
358      putchar ('\n');
359    }
360
361  if (include_project)
362    {
363      FILE *stream;
364      char buf[1024];
365      const char *const baseproject = "/.project";
366      char *const project =
367        xmalloc (strlen (pw->pw_dir) + strlen (baseproject) + 1);
368      stpcpy (stpcpy (project, pw->pw_dir), baseproject);
369
370      stream = fopen (project, "r");
371      if (stream)
372        {
373          size_t bytes;
374
375          printf (_("Project: "));
376
377          while ((bytes = fread (buf, 1, sizeof (buf), stream)) > 0)
378            fwrite (buf, 1, bytes, stdout);
379          fclose (stream);
380        }
381
382      free (project);
383    }
384
385  if (include_plan)
386    {
387      FILE *stream;
388      char buf[1024];
389      const char *const baseplan = "/.plan";
390      char *const plan =
391        xmalloc (strlen (pw->pw_dir) + strlen (baseplan) + 1);
392      stpcpy (stpcpy (plan, pw->pw_dir), baseplan);
393
394      stream = fopen (plan, "r");
395      if (stream)
396        {
397          size_t bytes;
398
399          printf (_("Plan:\n"));
400
401          while ((bytes = fread (buf, 1, sizeof (buf), stream)) > 0)
402            fwrite (buf, 1, bytes, stdout);
403          fclose (stream);
404        }
405
406      free (plan);
407    }
408
409  putchar ('\n');
410}
411
412/* Print the username of each valid entry and the number of valid entries
413   in UTMP_BUF, which should have N elements. */
414
415static void
416print_heading (void)
417{
418  printf ("%-8s", _("Login"));
419  if (include_fullname)
420    printf (" %-19s", _("Name"));
421  printf (" %-9s", _(" TTY"));
422  if (include_idle)
423    printf (" %-6s", _("Idle"));
424  printf (" %-*s", time_format_width, _("When"));
425#ifdef HAVE_UT_HOST
426  if (include_where)
427    printf (" %s", _("Where"));
428#endif
429  putchar ('\n');
430}
431
432/* Display UTMP_BUF, which should have N entries. */
433
434static void
435scan_entries (size_t n, const STRUCT_UTMP *utmp_buf,
436              const int argc_names, char *const argv_names[])
437{
438  if (hard_locale (LC_TIME))
439    {
440      time_format = "%Y-%m-%d %H:%M";
441      time_format_width = 4 + 1 + 2 + 1 + 2 + 1 + 2 + 1 + 2;
442    }
443  else
444    {
445      time_format = "%b %e %H:%M";
446      time_format_width = 3 + 1 + 2 + 1 + 2 + 1 + 2;
447    }
448
449  if (include_heading)
450    print_heading ();
451
452  while (n--)
453    {
454      if (IS_USER_PROCESS (utmp_buf))
455        {
456          if (argc_names)
457            {
458              int i;
459
460              for (i = 0; i < argc_names; i++)
461                if (strncmp (UT_USER (utmp_buf), argv_names[i], UT_USER_SIZE)
462                    == 0)
463                  {
464                    print_entry (utmp_buf);
465                    break;
466                  }
467            }
468          else
469            print_entry (utmp_buf);
470        }
471      utmp_buf++;
472    }
473}
474
475/* Display a list of who is on the system, according to utmp file FILENAME. */
476
477static void
478short_pinky (const char *filename,
479             const int argc_names, char *const argv_names[])
480{
481  size_t n_users;
482  STRUCT_UTMP *utmp_buf;
483
484  if (read_utmp (filename, &n_users, &utmp_buf, 0) != 0)
485    error (EXIT_FAILURE, errno, "%s", filename);
486
487  scan_entries (n_users, utmp_buf, argc_names, argv_names);
488}
489
490static void
491long_pinky (const int argc_names, char *const argv_names[])
492{
493  int i;
494
495  for (i = 0; i < argc_names; i++)
496    print_long_entry (argv_names[i]);
497}
498
499void
500usage (int status)
501{
502  if (status != EXIT_SUCCESS)
503    fprintf (stderr, _("Try `%s --help' for more information.\n"),
504             program_name);
505  else
506    {
507      printf (_("Usage: %s [OPTION]... [USER]...\n"), program_name);
508      fputs (_("\
509\n\
510  -l              produce long format output for the specified USERs\n\
511  -b              omit the user's home directory and shell in long format\n\
512  -h              omit the user's project file in long format\n\
513  -p              omit the user's plan file in long format\n\
514  -s              do short format output, this is the default\n\
515"), stdout);
516      fputs (_("\
517  -f              omit the line of column headings in short format\n\
518  -w              omit the user's full name in short format\n\
519  -i              omit the user's full name and remote host in short format\n\
520  -q              omit the user's full name, remote host and idle time\n\
521                  in short format\n\
522"), stdout);
523      fputs (HELP_OPTION_DESCRIPTION, stdout);
524      fputs (VERSION_OPTION_DESCRIPTION, stdout);
525      printf (_("\
526\n\
527A lightweight `finger' program;  print user information.\n\
528The utmp file will be %s.\n\
529"), UTMP_FILE);
530      emit_ancillary_info ();
531    }
532  exit (status);
533}
534
535int
536main (int argc, char **argv)
537{
538  int optc;
539  int n_users;
540
541  initialize_main (&argc, &argv);
542  set_program_name (argv[0]);
543  setlocale (LC_ALL, "");
544  bindtextdomain (PACKAGE, LOCALEDIR);
545  textdomain (PACKAGE);
546
547  atexit (close_stdout);
548
549  while ((optc = getopt_long (argc, argv, "sfwiqbhlp", longopts, NULL)) != -1)
550    {
551      switch (optc)
552        {
553        case 's':
554          do_short_format = true;
555          break;
556
557        case 'l':
558          do_short_format = false;
559          break;
560
561        case 'f':
562          include_heading = false;
563          break;
564
565        case 'w':
566          include_fullname = false;
567          break;
568
569        case 'i':
570          include_fullname = false;
571#ifdef HAVE_UT_HOST
572          include_where = false;
573#endif
574          break;
575
576        case 'q':
577          include_fullname = false;
578#ifdef HAVE_UT_HOST
579          include_where = false;
580#endif
581          include_idle = false;
582          break;
583
584        case 'h':
585          include_project = false;
586          break;
587
588        case 'p':
589          include_plan = false;
590          break;
591
592        case 'b':
593          include_home_and_shell = false;
594          break;
595
596        case_GETOPT_HELP_CHAR;
597
598        case_GETOPT_VERSION_CHAR (PROGRAM_NAME, AUTHORS);
599
600        default:
601          usage (EXIT_FAILURE);
602        }
603    }
604
605  n_users = argc - optind;
606
607  if (!do_short_format && n_users == 0)
608    {
609      error (0, 0, _("no username specified; at least one must be\
610 specified when using -l"));
611      usage (EXIT_FAILURE);
612    }
613
614  if (do_short_format)
615    short_pinky (UTMP_FILE, n_users, argv + optind);
616  else
617    long_pinky (n_users, argv + optind);
618
619  exit (EXIT_SUCCESS);
620}
621