>From a71492f0bc75d0324dd69983a4f85a650e4f56a4 Mon Sep 17 00:00:00 2001 From: Bernhard Voelker Date: Sun, 1 Sep 2013 23:19:36 +0200 Subject: [PATCH] groups,id: add -0, --null option * src/group-list.h (print_group_list): Add a parameter for the separator of type char. * src/group-list.c (print_group_list): Likewise, and use it instead of a white space character to separate the group entries. * src/groups.c (longopts): Add an array element for the new long option. (usage): Document the new options. (main): Define the bool flag opt_null indicating the use of the new options. In the getopt_long loop, handle it. In the case without a given USERNAME argument, pass either a white space or a NUL character to print_group_list() - depending on the above flag. Only output the final newline character unless the --null option has been specified. Likewise in the case with a given USERNAME and when the --null option has been specified, plus output 2 NULs separating consecutive users. * src/id.c (longopts): Add array element for the new long option. (usage): Document the new options. While at it, fix the alignment of the descriptions to match that of HELP_OPTION_DESCRIPTION. (main): Define the bool flag opt_null indicating the use of the new options. In the getopt_long loop, handle it. Output an error diagnostic in the case the --null 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. Only output the final newline character unless the --null option has been specified. * doc/coreutils.texi (optNullEntry): Add new macro describing the new options. (id invocation): Use the above new macro and mention that the new options are not permitted when using the default format. Move the @exitstatus macro down after @primaryAndSupplementaryGroups in order to be consistent with other info pages. (groups invocation): Instead of saying that the --help and --version options are the only ones, add a @ref to these common options and use the above new macro to explain the new options. Document the resulting output syntax for the cases without, with one and with more user names specified. Move @exitstatus down after @primaryAndSupplementaryGroups here, too. * tests/misc/groups-id-null.sh: Add new test exercising the new options. * tests/local.mk (all_tests): Reference it. * NEWS (New features): Mention the new options. Fixes http://bugs.gnu.org/9987 --- NEWS | 3 + doc/coreutils.texi | 58 +++++++++++++++++-- src/group-list.c | 6 +- src/group-list.h | 2 +- src/groups.c | 41 +++++++++++--- src/id.c | 38 +++++++++---- tests/local.mk | 1 + tests/misc/groups-id-null.sh | 129 +++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 250 insertions(+), 28 deletions(-) create mode 100755 tests/misc/groups-id-null.sh diff --git a/NEWS b/NEWS index 4a78617..ddf4a6e 100644 --- a/NEWS +++ b/NEWS @@ -40,6 +40,9 @@ GNU coreutils NEWS -*- outline -*- du accepts a new option: --inodes to show the number of inodes instead of the blocks used. + groups and id accept a new option: --null (-0) to separate 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..d330772 100644 --- a/doc/coreutils.texi +++ b/doc/coreutils.texi @@ -14473,7 +14473,28 @@ followed by the corresponding user or group name in parentheses. The options cause @command{id} to print only part of the above information. Also see @ref{Common options}. +@macro optNullEntry{cmd} +@item -0 +@opindex -0 +@itemx --null +@opindex --null +@cindex output NUL-byte-terminated entries +Output a zero byte (ASCII NUL) to separate the entries, rather than a whitespace +character. Also elide the newline character at the end of the output. +This option enables other programs to parse the output of @command{\cmd\} +even when that output would contain data with embedded whitespaces. +@end macro + @table @samp +@optNullEntry{id} +This option is not permitted when using the default format. + +Example: +@example +$ id -Gn --null +users devs +@end example + @item -g @itemx --group @opindex -g @@ -14518,8 +14539,6 @@ set the exit status to 1. @end table -@exitstatus - @macro primaryAndSupplementaryGroups{cmd,arg} Primary and supplementary groups for a process are normally inherited from its parent and are usually unchanged since login. This means @@ -14530,6 +14549,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 +14608,38 @@ groups [@var{username}]@dots{} The group lists are equivalent to the output of the command @samp{id -Gn}. -@primaryAndSupplementaryGroups{groups,list of users} +The program accepts the following options. Also see @ref{Common options}. -The only options are @option{--help} and @option{--version}. @xref{Common -options}. +@table @samp +@optNullEntry{id} -@exitstatus +Example: +@example +$ groups -0 +users devs +@end example + +When a @var{username} is specified, @command{groups} outputs a NUL byte after +the @var{username} instead of the colon (@samp{" : "}). + +@example +$ groups --null matt +matt users +@end example + +When further @var{username}s are specified, then @command{groups} outputs +2 NUL bytes before continuing with the next user: +@example +$ groups john matt -0 +john users devs +matt users +@end example +@end table + +@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..a76ee00 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 separator) { bool ok = true; struct passwd *pwd = NULL; @@ -52,7 +52,7 @@ print_group_list (const char *username, if (egid != rgid) { - putchar (' '); + putchar (separator); 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 (separator); 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 . */ 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..268b0d8 100644 --- a/src/groups.c +++ b/src/groups.c @@ -38,6 +38,7 @@ static struct option const longopts[] = { + {"null", no_argument, NULL, '0'}, {GETOPT_HELP_OPTION_DECL}, {GETOPT_VERSION_OPTION_DECL}, {NULL, 0, NULL, 0} @@ -56,6 +57,9 @@ Print group memberships for each USERNAME or, if no USERNAME is specified, for\ \n\ the current process (which may differ if the groups database has changed).\n"), stdout); + fputs (_("\ + -0, --null separate entries by a NUL, not whitespace;\n\ + only permitted without USERNAME\n"), stdout); fputs (HELP_OPTION_DESCRIPTION, stdout); fputs (VERSION_OPTION_DESCRIPTION, stdout); emit_ancillary_info (); @@ -68,9 +72,11 @@ main (int argc, char **argv) { int optc; bool ok = true; + bool opt_null = false; gid_t rgid, egid; uid_t ruid; + initialize_main (&argc, &argv); set_program_name (argv[0]); setlocale (LC_ALL, ""); @@ -82,10 +88,14 @@ main (int argc, char **argv) /* Processing the arguments this way makes groups.c behave differently to * groups.sh if one of the arguments is "--". */ - while ((optc = getopt_long (argc, argv, "", longopts, NULL)) != -1) + while ((optc = getopt_long (argc, argv, "0", longopts, NULL)) != -1) { switch (optc) { + case '0': + opt_null = true; + break; + case_GETOPT_HELP_CHAR; case_GETOPT_VERSION_CHAR (PROGRAM_NAME, AUTHORS); default: @@ -114,13 +124,17 @@ 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, + opt_null ? '\0' : ' ')) ok = false; - putchar ('\n'); + + if (!opt_null) + 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. */ + int nusers = 0; while (optind < argc) { struct passwd *pwd = getpwnam (argv[optind]); @@ -129,10 +143,23 @@ main (int argc, char **argv) ruid = pwd->pw_uid; rgid = egid = pwd->pw_gid; - printf ("%s : ", argv[optind]); - if (!print_group_list (argv[optind++], ruid, rgid, egid, true)) + if (opt_null) + { + /* Separate consecutive user output by two NULs. */ + if (nusers++) + printf ("%c%c", '\0', '\0'); + printf ("%s%c", argv[optind], '\0'); + } + else + { + printf ("%s : ", argv[optind]); + } + if (!print_group_list (argv[optind++], ruid, rgid, egid, true, + opt_null ? '\0' : ' ')) ok = false; - putchar ('\n'); + + if (!opt_null) + putchar ('\n'); } } diff --git a/src/id.c b/src/id.c index 3e7016f..46b792c 100644 --- a/src/id.c +++ b/src/id.c @@ -61,6 +61,7 @@ static security_context_t context = NULL; static struct option const longopts[] = { + {"null", no_argument, NULL, '0'}, {"context", no_argument, NULL, 'Z'}, {"group", no_argument, NULL, 'g'}, {"groups", no_argument, NULL, 'G'}, @@ -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 (_("\ + -0, --null separate entries by a NUL, not whitespace;\n\ + not permitted in default format\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\ "), 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_null = false; /* If true, output the list of all group IDs. -G */ bool just_group_list = false; @@ -127,10 +133,14 @@ main (int argc, char **argv) atexit (close_stdout); - while ((optc = getopt_long (argc, argv, "agnruGZ", longopts, NULL)) != -1) + while ((optc = getopt_long (argc, argv, "0agnruGZ", longopts, NULL)) != -1) { switch (optc) { + case '0': + opt_null = true; + break; + case 'a': /* Ignore -a, for compatibility with SVR4. */ break; @@ -193,6 +203,10 @@ main (int argc, char **argv) error (EXIT_FAILURE, 0, _("cannot print only names or real IDs in default format")); + if (default_format && opt_null) + error (EXIT_FAILURE, 0, + _("option --null 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 +283,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_null ? '\0' : ' ')) ok = false; } else if (just_context) @@ -280,7 +295,8 @@ main (int argc, char **argv) { print_full_info (argv[optind]); } - putchar ('\n'); + if (default_format || !opt_null) + putchar ('\n'); exit (ok ? EXIT_SUCCESS : EXIT_FAILURE); } diff --git a/tests/local.mk b/tests/local.mk index b00ff59..32aa3e3 100644 --- a/tests/local.mk +++ b/tests/local.mk @@ -271,6 +271,7 @@ all_tests = \ tests/misc/false-status.sh \ tests/misc/fold.pl \ tests/misc/groups-dash.sh \ + tests/misc/groups-id-null.sh \ tests/misc/groups-version.sh \ tests/misc/head-c.sh \ tests/misc/head-pos.sh \ diff --git a/tests/misc/groups-id-null.sh b/tests/misc/groups-id-null.sh new file mode 100755 index 0000000..a28b889 --- /dev/null +++ b/tests/misc/groups-id-null.sh @@ -0,0 +1,129 @@ +#!/bin/sh +# Exercise "groups --null" and "id --null". + +# 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 . + +. "${srcdir=.}/tests/init.sh"; path_prepend_ ./src +print_ver_ groups id +require_perl_ + +# +# Converting back null-separated output of id(1) / groups(1) without a USERNAME. +# Format: group1 group2 group3 +# Result: group1 group2 group3 +# Just replace all NULs by whitespace (SP) and terminate with a newline (NL). +# +cat <<\EOF > conv-basic || framework_failure_ +$_ = <>; +s/\0/ /g; +print $_ . "\n"; +EOF + +# +# Converting back null-separated output of groups(1) with USERNAME(s). +# Format: +# user1 group1 user2 group2 group3 +# Result: +# user1 : group1 user2 : group2 group3 +# Split up records by double-NUL, then +# split up the line by a single NUL into USERNAME and the list of group IDs, +# and then print each group - separated by a whitespace; +# Terminate a record with a newline. +# +cat <<\EOF > conv-w-user || framework_failure_ +foreach (split ("\0\0", <>, -1)) + { + my ($user, @groups) = split ("\0"); + print $user . " :"; + for (@groups) + { + print " " . $_; + } + print "\n"; + } +EOF + +u="$( id -nu )" +groups "$u" > exp || fail=1 +# Verify that xargs works. +echo "$u" | xargs groups >out 2>err || skip_ "xargs required" +compare exp out || skip_ "xargs required" +compare /dev/null err || skip_ "xargs required" + +# Create a list of users. +# FIXME: Maybe use "getent passwd"? Is it portable? +# FIXME: Maybe use "head -n100" to limit # of users? +sed 's/:.*$//' /etc/passwd > users || framework_failure_ +[ $(wc -l < users) -gt 0 ] || framework_failure_ + + +# Exercise "group -0", i.e. without a USERNAME. +groups > exp || fail=1 +groups --null > out || fail=1 +$PERL conv-basic out > out2 || framework_failure_ +compare exp out2 || fail=1 + +# Exercise "group -0 user1". +groups "$u" > exp || fail=1 +groups --null "$u" > out || fail=1 +$PERL conv-w-user out > out2 || framework_failure_ +compare exp out2 || fail=1 + +if [ "$u" != "root" ] && groups "root" >/dev/null +then + # Exercise another user name ... + groups "root" > exp || fail=1 + groups --null "root" > out || fail=1 + $PERL conv-w-user out > out2 || framework_failure_ + compare exp out2 || fail=1 + + # ... and both together. + groups "$u" "root" > exp || fail=1 + groups --null "$u" "root" > out || fail=1 + $PERL conv-w-user out > out2 || framework_failure_ + compare exp out2 || fail=1 +else + # Fall back to using the same user name twice. + groups "$u" "$u" > exp || fail=1 + groups --null "$u" "$u" > out || fail=1 + $PERL conv-w-user out > out2 || framework_failure_ + compare exp out2 || fail=1 +fi + +# Exercise "group -0 user1 user2 ...". +xargs groups exp || fail=1 +xargs groups -0 out || fail=1 +$PERL conv-w-user out > out2 || framework_failure_ +compare exp out2 || fail=1 + + +# id(1) should refuse --null in default format. +id --null > exp 2>err && fail=1 +grep 'option --null not permitted in default format' err || fail=1 + +# Exercise "id -0" with various options. +for o in g gr G Gr u ur ; do + for n in "" n ; do + for a in "" $u root ; do + id -${o}${n} $a > exp || fail=1 + id -${o}${n}0 $a > out || fail=1 + $PERL conv-basic out > out2 || framework_failure_ + compare exp out2 || fail=1 + done + done +done + +Exit $fail -- 1.8.3.1