Subject: bug#9987: [PATCH] groups,id: add -0, --null option

From: Bernhard Voelker <[email-masked]>
Date: Sat, 21 Sep 2013 12:15:11 +0200

On 09/19/2013 11:53 AM, Pádraig Brady wrote:
> So I'd avoid getent entirely and do:

Good idea.

> # Add $USER to ensure we have at least one explicit entry
> users="$u"
> # Add 'root' to test multiple groups (as is usually the case)
> id root >/dev/null 2>/dev/null && users="$USERS root"
> # Add 'nobody' to test single group (as is usually the case)
> id nobody >/dev/null 2>/dev/null && users="$USERS nobody"
> # Add $users and '' (implicit $USER) to list to process
> printf '%s\n' $users '' >> users || framework_failure_

Interestingly, the situation is different here (openSUSE):
root is only in group root, and nobody is in the groups
nobody and nogroup.
However, I've added some users which seem to be typical
for being member of one or more groups.

  for u in root man postfix sshd nobody ; do
    id $u >/dev/null 2>&1 && users="$users $u"
  done

New patch attached.  Thanks!

Have a nice day,
Berny
>From f8906040e42ca762be498fd58f474fad1f68f145 Mon Sep 17 00:00:00 2001
From: Bernhard Voelker <[email-masked]>
Date: Sat, 21 Sep 2013 12:08:55 +0200
Subject: [PATCH] id: add -z, --zero option

* src/group-list.h (print_group_list): Add a parameter for the
delimiter of type char.
* src/group-list.c (print_group_list): Likewise, and use it instead
of a white space character to delimit the group entries.
* src/groups.c (main): Pass white space character to print_group_list().
* src/id.c (longopts):  Add array element for the new long option.
(usage): Document the new option.  While at it, fix the alignment
of the descriptions to match that of HELP_OPTION_DESCRIPTION.
(main): Define the bool flag opt_zero indicating the use of the
new option.  In the getopt_long loop, handle it.
Output an error diagnostic in the case the --zero option has been
specified together with the default format.
In the case of -gG, pass either a NUL or a white space character to
print_group_list() - depending on the above new flag.
Likewise change the printing of the final newline character: output
a NUL instead if the --zero option has been specified.
* doc/coreutils.texi (id invocation): Document the new option.
While at it, move the @exitstatus macro down after the macro
@primaryAndSupplementaryGroups in order to be consistent with
other texinfo documents.
(groups invocation): Move @exitstatus down after the macro
@primaryAndSupplementaryGroups here, too.
* tests/misc/id-zero.sh: Add new test exercising the new option.
* tests/local.mk (all_tests): Reference it.
* NEWS (New features): Mention the new option.
Fixes http://bugs.gnu.org/9987
---
 NEWS                  |  3 +++
 doc/coreutils.texi    | 22 ++++++++++++++----
 src/group-list.c      |  6 ++---
 src/group-list.h      |  2 +-
 src/groups.c          |  6 ++---
 src/id.c              | 36 ++++++++++++++++++++---------
 tests/local.mk        |  1 +
 tests/misc/id-zero.sh | 63 +++++++++++++++++++++++++++++++++++++++++++++++++++
 8 files changed, 116 insertions(+), 23 deletions(-)
 create mode 100755 tests/misc/id-zero.sh

diff --git a/NEWS b/NEWS
index d26722d..23c7b54 100644
--- a/NEWS
+++ b/NEWS
@@ -44,6 +44,9 @@ GNU coreutils NEWS                                    -*- outline -*-
   du accepts a new option: --inodes to show the number of inodes instead
   of the blocks used.
 
+  id accepts a new option: --zero (-z) to delimit the output entries by
+  a NUL instead of a white space character.
+
   id and ls with -Z report the SMACK security context where available.
   mkdir, mkfifo and mknod with -Z set the SMACK context where available.
 
diff --git a/doc/coreutils.texi b/doc/coreutils.texi
index 21216b4..d022c45 100644
--- a/doc/coreutils.texi
+++ b/doc/coreutils.texi
@@ -14516,9 +14516,20 @@ Print only the security context of the current user.
 If SELinux is disabled then print a warning and
 set the exit status to 1.
 
-@end table
+@item -z
+@itemx --zero
+@opindex -z
+@opindex --zero
+Delimit output items with NUL characters.
+This option is not permitted when using the default format.
 
-@exitstatus
+Example:
+@example
+$ id -Gn --zero
+users <NUL> devs <NUL>
+@end example
+
+@end table
 
 @macro primaryAndSupplementaryGroups{cmd,arg}
 Primary and supplementary groups for a process are normally inherited
@@ -14530,6 +14541,8 @@ database to be consulted afresh, and so will give a different result.
 @end macro
 @primaryAndSupplementaryGroups{id,user argument}
 
+@exitstatus
+
 @node logname invocation
 @section @command{logname}: Print current login name
 
@@ -14587,13 +14600,12 @@ groups [@var{username}]@dots{}
 
 The group lists are equivalent to the output of the command @samp{id -Gn}.
 
-@primaryAndSupplementaryGroups{groups,list of users}
-
 The only options are @option{--help} and @option{--version}.  @xref{Common
 options}.
 
-@exitstatus
+@primaryAndSupplementaryGroups{groups,list of users}
 
+@exitstatus
 
 @node users invocation
 @section @command{users}: Print login names of users currently logged in
diff --git a/src/group-list.c b/src/group-list.c
index 7d4995b..d54b057 100644
--- a/src/group-list.c
+++ b/src/group-list.c
@@ -35,7 +35,7 @@
 extern bool
 print_group_list (const char *username,
                   uid_t ruid, gid_t rgid, gid_t egid,
-                  bool use_names)
+                  bool use_names, char delim)
 {
   bool ok = true;
   struct passwd *pwd = NULL;
@@ -52,7 +52,7 @@ print_group_list (const char *username,
 
   if (egid != rgid)
     {
-      putchar (' ');
+      putchar (delim);
       if (!print_group (egid, use_names))
         ok = false;
     }
@@ -79,7 +79,7 @@ print_group_list (const char *username,
     for (i = 0; i < n_groups; i++)
       if (groups[i] != rgid && groups[i] != egid)
         {
-          putchar (' ');
+          putchar (delim);
           if (!print_group (groups[i], use_names))
             ok = false;
         }
diff --git a/src/group-list.h b/src/group-list.h
index 3fac887..573de1d 100644
--- a/src/group-list.h
+++ b/src/group-list.h
@@ -16,4 +16,4 @@
    along with this program.  If not, see <http://www.gnu.org/licenses/>.  */
 
 bool print_group (gid_t, bool);
-bool print_group_list (const char *, uid_t, gid_t, gid_t, bool);
+bool print_group_list (const char *, uid_t, gid_t, gid_t, bool, char);
diff --git a/src/groups.c b/src/groups.c
index 53332d5..d30c9fb 100644
--- a/src/groups.c
+++ b/src/groups.c
@@ -114,13 +114,13 @@ main (int argc, char **argv)
       if (rgid == NO_GID && errno)
         error (EXIT_FAILURE, errno, _("cannot get real GID"));
 
-      if (!print_group_list (NULL, ruid, rgid, egid, true))
+      if (!print_group_list (NULL, ruid, rgid, egid, true, ' '))
         ok = false;
       putchar ('\n');
     }
   else
     {
-      /* At least one argument.  Divulge the details of the specified users. */
+      /* At least one argument.  Divulge the details of the specified users.  */
       while (optind < argc)
         {
           struct passwd *pwd = getpwnam (argv[optind]);
@@ -130,7 +130,7 @@ main (int argc, char **argv)
           rgid = egid = pwd->pw_gid;
 
           printf ("%s : ", argv[optind]);
-          if (!print_group_list (argv[optind++], ruid, rgid, egid, true))
+          if (!print_group_list (argv[optind++], ruid, rgid, egid, true, ' '))
             ok = false;
           putchar ('\n');
         }
diff --git a/src/id.c b/src/id.c
index 3e7016f..a0334ba 100644
--- a/src/id.c
+++ b/src/id.c
@@ -67,6 +67,7 @@ static struct option const longopts[] =
   {"name", no_argument, NULL, 'n'},
   {"real", no_argument, NULL, 'r'},
   {"user", no_argument, NULL, 'u'},
+  {"zero", no_argument, NULL, 'z'},
   {GETOPT_HELP_OPTION_DECL},
   {GETOPT_VERSION_OPTION_DECL},
   {NULL, 0, NULL, 0}
@@ -83,14 +84,18 @@ usage (int status)
       fputs (_("\
 Print user and group information for the specified USERNAME,\n\
 or (when USERNAME omitted) for the current user.\n\
-\n\
-  -a              ignore, for compatibility with other versions\n\
-  -Z, --context   print only the security context of the current user\n\
-  -g, --group     print only the effective group ID\n\
-  -G, --groups    print all group IDs\n\
-  -n, --name      print a name instead of a number, for -ugG\n\
-  -r, --real      print the real ID instead of the effective ID, with -ugG\n\
-  -u, --user      print only the effective user ID\n\
+\n"),
+             stdout);
+      fputs (_("\
+  -a             ignore, for compatibility with other versions\n\
+  -Z, --context  print only the security context of the current user\n\
+  -g, --group    print only the effective group ID\n\
+  -G, --groups   print all group IDs\n\
+  -n, --name     print a name instead of a number, for -ugG\n\
+  -r, --real     print the real ID instead of the effective ID, with -ugG\n\
+  -u, --user     print only the effective user ID\n\
+  -z, --zero     delimit entries with NUL characters, not whitespace;\n\
+                   not permitted in default format\n\
 "), stdout);
       fputs (HELP_OPTION_DESCRIPTION, stdout);
       fputs (VERSION_OPTION_DESCRIPTION, stdout);
@@ -109,6 +114,7 @@ main (int argc, char **argv)
   int optc;
   int selinux_enabled = (is_selinux_enabled () > 0);
   bool smack_enabled = is_smack_enabled ();
+  bool opt_zero = false;
 
   /* If true, output the list of all group IDs. -G */
   bool just_group_list = false;
@@ -127,7 +133,7 @@ main (int argc, char **argv)
 
   atexit (close_stdout);
 
-  while ((optc = getopt_long (argc, argv, "agnruGZ", longopts, NULL)) != -1)
+  while ((optc = getopt_long (argc, argv, "agnruzGZ", longopts, NULL)) != -1)
     {
       switch (optc)
         {
@@ -162,6 +168,9 @@ main (int argc, char **argv)
         case 'u':
           just_user = true;
           break;
+        case 'z':
+          opt_zero = true;
+          break;
         case 'G':
           just_group_list = true;
           break;
@@ -193,6 +202,10 @@ main (int argc, char **argv)
     error (EXIT_FAILURE, 0,
            _("cannot print only names or real IDs in default format"));
 
+  if (default_format && opt_zero)
+    error (EXIT_FAILURE, 0,
+           _("option --zero not permitted in default format"));
+
   /* If we are on a SELinux/SMACK-enabled kernel, no user is specified, and
      either --context is specified or none of (-u,-g,-G) is specified,
      and we're not in POSIXLY_CORRECT mode, get our context.  Otherwise,
@@ -269,7 +282,8 @@ main (int argc, char **argv)
     }
   else if (just_group_list)
     {
-      if (!print_group_list (argv[optind], ruid, rgid, egid, use_name))
+      if (!print_group_list (argv[optind], ruid, rgid, egid, use_name,
+                             opt_zero ? '\0' : ' '))
         ok = false;
     }
   else if (just_context)
@@ -280,7 +294,7 @@ main (int argc, char **argv)
     {
       print_full_info (argv[optind]);
     }
-  putchar ('\n');
+  putchar (opt_zero ? '\0' : '\n');
 
   exit (ok ? EXIT_SUCCESS : EXIT_FAILURE);
 }
diff --git a/tests/local.mk b/tests/local.mk
index b00ff59..8f76b23 100644
--- a/tests/local.mk
+++ b/tests/local.mk
@@ -277,6 +277,7 @@ all_tests =					\
   tests/misc/id-context.sh			\
   tests/misc/id-groups.sh			\
   tests/misc/id-setgid.sh			\
+  tests/misc/id-zero.sh			\
   tests/misc/md5sum.pl				\
   tests/misc/md5sum-bsd.sh			\
   tests/misc/md5sum-newline.pl			\
diff --git a/tests/misc/id-zero.sh b/tests/misc/id-zero.sh
new file mode 100755
index 0000000..b0e3887
--- /dev/null
+++ b/tests/misc/id-zero.sh
@@ -0,0 +1,63 @@
+#!/bin/sh
+# Exercise "id --zero".
+
+# Copyright (C) 2013 Free Software Foundation, Inc.
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+. "${srcdir=.}/tests/init.sh"; path_prepend_ ./src
+print_ver_ id
+
+u="$( id -nu )"
+id || fail=1
+id "$u" || fail=1
+id root || fail=1
+
+# id(1) should refuse --zero in default format.
+echo 'id: option --zero not permitted in default format' > err-exp \
+  || framework_failure_
+id --zero > out 2>err && fail=1
+compare /dev/null out || fail=1
+compare err-exp   err || fail=1
+
+# Create a nice list of users.
+# Add $USER to ensure we have at least one explicit entry.
+users="$u"
+# Add a few typical users to test single group and multiple groups.
+for u in root man postfix sshd nobody ; do
+  id $u >/dev/null 2>&1 && users="$users $u"
+done
+# Add $users and '' (implicit $USER) to list to process.
+printf '%s\n' $users '' >> users || framework_failure_
+
+# Exercise "id -z" with various options.
+printf '\n' > exp || framework_failure_
+:> out || framework_failure_
+
+while read u ; do
+  for o in g gr G Gr u ur ; do
+    for n in '' n ; do
+      printf   '%s: ' "id -${o}${n}[z] $u" >> exp || framework_failure_
+      printf '\n%s: ' "id -${o}${n}[z] $u" >> out || framework_failure_
+      id -${o}${n}  $u >> exp || fail=1
+      id -${o}${n}z $u  > tmp || fail=1
+      head -c-1 < tmp >> out || framework_failure_
+    done
+  done
+done < users
+printf '\n' >> out || framework_failure_
+tr '\0' ' ' < out > out2 || framework_failure_
+compare exp out2 || fail=1
+
+Exit $fail
-- 
1.8.3.1