11.2.3 Printing Out User Information

The id utility lists a user’s real and effective user ID numbers, real and effective group ID numbers, and the user’s group set, if any. id only prints the effective user ID and group ID if they are different from the real ones. If possible, id also supplies the corresponding user and group names. The output might look like this:

$ id
-| uid=1000(arnold) gid=1000(arnold) groups=1000(arnold),4(adm),7(lp),27(sudo)

This information is part of what is provided by gawk’s PROCINFO array (see Predefined Variables). However, the id utility provides a more palatable output than just individual numbers.

The POSIX version of id takes several options that give you control over the output’s format, such as printing only real ids, or printing only numbers or only names. Additionally, you can print the information for a specific user, instead of that of the current user.

Here is a version of POSIX id written in awk. It uses the getopt() library function (see Processing Command-Line Options), the user database library functions (see Reading the User Database), and the group database library functions (see Reading the Group Database) from A Library of awk Functions.

The program is moderately straightforward. All the work is done in the BEGIN rule. It starts with explanatory comments, a list of options, and then a usage() function:

# id.awk --- implement id in awk
#
# Requires user and group library functions and getopt
# output is:
# uid=12(foo) euid=34(bar) gid=3(baz) \
#             egid=5(blat) groups=9(nine),2(two),1(one)

# Options:
#   -G Output all group ids as space separated numbers (ruid, euid, groups)
#   -g Output only the euid as a number
#   -n Output name instead of the numeric value (with -g/-G/-u)
#   -r Output ruid/rguid instead of effective id
#   -u Output only effective user id, as a number

function usage()
{
    printf("Usage:\n" \
           "\tid [user]\n" \
           "\tid -G [-n] [user]\n" \
           "\tid -g [-nr] [user]\n" \
           "\tid -u [-nr] [user]\n") > "/dev/stderr"

    exit 1
}

The first step is to parse the options using getopt(), and to set various flag variables according to the options given:

BEGIN {
    # parse args
    while ((c = getopt(ARGC, ARGV, "Ggnru")) != -1) {
        if (c == "G")
            groupset_only++
        else if (c == "g")
            egid_only++
        else if (c == "n")
            names_not_groups++
        else if (c == "r")
            real_ids_only++
        else if (c == "u")
            euid_only++
        else
            usage()
    }

The next step is to check that no conflicting options were provided. -G and -r are mutually exclusive. It is also not allowed to provide more than one user name on the command line:

    if (groupset_only && real_ids_only)
        usage()
    else if (ARGC - Optind > 1)
        usage()

The user and group ID numbers are obtained from PROCINFO for the current user, or from the user and password databases for a user supplied on the command line. In the latter case, real_ids_only is set, since it’s not possible to print information about the effective user and group IDs:

    if (ARGC - Optind == 0) {
        # gather info for current user
        uid = PROCINFO["uid"]
        euid = PROCINFO["euid"]
        gid = PROCINFO["gid"]
        egid = PROCINFO["egid"]
        for (i = 1; ("group" i) in PROCINFO; i++)
            groupset[i] = PROCINFO["group" i]
    } else {
        fill_info_for_user(ARGV[ARGC-1])
        real_ids_only++
    }

The test in the for loop is worth noting. Any supplementary groups in the PROCINFO array have the indices "group1" through "groupN" for some N (i.e., the total number of supplementary groups). However, we don’t know in advance how many of these groups there are.

This loop works by starting at one, concatenating the value with "group", and then using in to see if that value is in the array (see Referring to an Array Element). Eventually, i increments past the last group in the array and the loop exits.

The loop is also correct if there are no supplementary groups; then the condition is false the first time it’s tested, and the loop body never executes.

Now, based on the options, we decide what information to print. For -G (print just the group set), we then select whether to print names or numbers. In either case, when done we exit:

    if (groupset_only) {
        if (names_not_groups) {
            for (i = 1; i in groupset; i++) {
                entry = getgrgid(groupset[i])
                name = get_first_field(entry)
                printf("%s", name)
                if ((i + 1) in groupset)
                    printf(" ")
            }
        } else {
            for (i = 1; i in groupset; i++) {
                printf("%u", groupset[i])
                if ((i + 1) in groupset)
                    printf(" ")
            }
        }

        print ""    # final newline
        exit 0
    }

Otherwise, for -g (effective group ID only), we check if -r was also provided, in which case we use the real group ID. Then based on -n, we decide whether to print names or numbers. Here too, when done, we exit:

    else if (egid_only) {
        id = real_ids_only ? gid : egid
        if (names_not_groups) {
            entry = getgrgid(id)
            name = get_first_field(entry)
            printf("%s\n", name)
        } else {
            printf("%u\n", id)
        }

        exit 0
    }

The get_first_field() function extracts the group name from the group database entry for the given group ID.

Similar processing logic applies to -u (effective user ID only), combined with -r and -n:

    else if (euid_only) {
        id = real_ids_only ? uid : euid
        if (names_not_groups) {
            entry = getpwuid(id)
            name = get_first_field(entry)
            printf("%s\n", name)
        } else {
            printf("%u\n", id)
        }

        exit 0
    }

At this point, we haven’t exited yet, so we print the regular, default output, based either on the current user’s information, or that of the user whose name was provided on the command line. We start with the real user ID:

    printf("uid=%d", uid)
    pw = getpwuid(uid)
    print_first_field(pw)

The print_first_field() function prints the user’s login name from the password file entry, surrounded by parentheses. It is shown soon. Printing the effective user ID is next:

    if (euid != uid && ! real_ids_only) {
        printf(" euid=%d", euid)
        pw = getpwuid(euid)
        print_first_field(pw)
    }

Similar logic applies to the real and effective group IDs:

    printf(" gid=%d", gid)
    pw = getgrgid(gid)
    print_first_field(pw)

    if (egid != gid && ! real_ids_only) {
        printf(" egid=%d", egid)
        pw = getgrgid(egid)
        print_first_field(pw)
    }

Finally, we print the group set and the terminating newline:

    for (i = 1; i in groupset; i++) {
        if (i == 1)
            printf(" groups=")
        group = groupset[i]
        printf("%d", group)
        pw = getgrgid(group)
        print_first_field(pw)
        if ((i + 1) in groupset)
            printf(",")
    }

    print ""
}

The get_first_field() function extracts the first field from a password or group file entry for use as a user or group name. Fields are separated by ‘:’ characters:

function get_first_field(str,  a)
{
    if (str != "") {
        split(str, a, ":")
        return a[1]
    }
}

This function is then used by print_first_field() to output the given name surrounded by parentheses:

function print_first_field(str)
{
    first = get_first_field(str)
    printf("(%s)", first)
}

These two functions simply isolate out some code that is used repeatedly, making the whole program shorter and cleaner. In particular, moving the check for the empty string into get_first_field() saves several lines of code.

Finally, fill_info_for_user() fetches user, group, and group set information for the user named on the command. The code is fairly straightforward, merely requiring that we exit if the given user doesn’t exist:

function fill_info_for_user(user,
                            pwent, fields, groupnames, grent, groups, i)
{
    pwent = getpwnam(user)
    if (pwent == "") {
        printf("id: '%s': no such user\n", user) > "/dev/stderr"
        exit 1
    }

    split(pwent, fields, ":")
    uid = fields[3] + 0
    gid = fields[4] + 0

Getting the group set is a little awkward. The library routine getgruser() returns a list of group names. These have to be gone through and turned back into group numbers, so that the rest of the code will work as expected:

    groupnames = getgruser(user)
    split(groupnames, groups, " ")
    for (i = 1; i in groups; i++) {
        grent = getgrnam(groups[i])
        split(grent, fields, ":")
        groupset[i] = fields[3] + 0
    }
}