28

I often want to get the login name associated with a user ID and because it’s proven to be a common use case, I decided to write a shell function to do this. While I primarily use GNU/Linux distributions, I try to write my scripts to be as portable as possible and to check that what I’m doing is POSIX-compatible.

Parse /etc/passwd

The first approach I tried was to parse /etc/passwd (using awk).

awk -v uid="$uid" -F: '$3 == uid {print $1}' /etc/passwd

However, the problem with this approach is that the logins may not be local, e.g., user authentication could be via NIS or LDAP.

Use the getent command

Using getent passwd is more portable than parsing /etc/passwd as this also queries non-local NIS or LDAP databases.

getent passwd "$uid" | cut -d: -f1

Unfortunately, the getent utility does not seem to be specified by POSIX.

Use the id command

id is the POSIX-standardised utility for getting data about a user’s identity.

BSD and GNU implementations accept a User ID as an operand:

This means it can be used to print the login name associated with a User ID:

id -nu "$uid"

However, providing User IDs as the operand is not specified in POSIX; it only describes the use of a login name as the operand.

Combining all of the above

I considered combining the above three approaches into something like the following:

get_username(){
    uid="$1"
    # First try using getent
    getent passwd "$uid" | cut -d: -f1 ||
        # Next try using the UID as an operand to id.
        id -nu "$uid" ||
        # As a last resort, parse `/etc/passwd`.
        awk -v uid="$uid" -F: '$3 == uid {print $1}' /etc/passwd
}

However, this is clunky, inelegant and – more importantly – not robust; it exits with a non-zero status if the User ID is invalid or does not exist. Before I write a longer and clunkier shell script that analyses and stores the exit status of each command invocation, I thought I’d ask here:

Is there a more elegant and portable (POSIX-compatible) way of getting the login name associated with a user ID?

9
  • 10
    For added fun, consider that multiple usernames can map to the same id... Sep 12, 2019 at 14:40
  • The issue with multiple usernames mapped to the same id in the context of this question is that neither getent nor id will return anything past the first match; the only way to find them all is to enumerate all users, if the user database allows that. (Looking in /etc/passwd works for users defined there, obviously.) Sep 12, 2019 at 15:39
  • 1
    Thanks @StephenKitt I created such an entry in my /etc/passwd and /etc/shadow to test this scenario and verified that both id and getent passwd behave as you describe. If, at some stage, I end up using a system where a user has multiple names, I'll do the same as these system utilities and simply treat the first occurrence as the canonical name for that user. Sep 12, 2019 at 17:10
  • 1
    Does POSIX require a user id to be associated with a user name at all? Any program running as root can call setuid(some_id), and there's no requirement that some_id may be part of any user database. With such things as user namespaces on Linux this may turn to be a crippling assumption for your scripts.
    – user313992
    Sep 13, 2019 at 5:04
  • 1
    @Philippos that seems like an expensive way of calling the getpwuid() function which ls uses to translate UIDs to login names. Gilles’ answer is a more direct and efficient way of accomplishing this. Sep 13, 2019 at 12:07

4 Answers 4

15

One common way to do this is to test if the program you want exists and is available from your PATH. For example:

get_username(){
  uid="$1"

  # First try using getent
  if command -v getent > /dev/null 2>&1; then 
    getent passwd "$uid" | cut -d: -f1

  # Next try using the UID as an operand to id.
  elif command -v id > /dev/null 2>&1 && \
       id -nu "$uid" > /dev/null 2>&1; then
    id -nu "$uid"

  # Next try perl - perl's getpwuid just calls the system's C library getpwuid
  elif command -v perl >/dev/null 2>&1; then
    perl -e '@u=getpwuid($ARGV[0]);
             if ($u[0]) {print $u[0]} else {exit 2}' "$uid"

  # As a last resort, parse `/etc/passwd`.
  else
      awk -v uid="$uid" -F: '
         BEGIN {ec=2};
         $3 == uid {print $1; ec=0; exit 0};
         END {exit ec}' /etc/passwd
  fi
}

Because POSIX id doesn't support UID arguments, the elif clause for id has to test not only whether id is in the PATH, but also whether it will run without error. This means it may run id twice, which fortunately will not have a noticeable impact on performance. It is also possible that both id and awk will be run, with the same negligible performance hit.

BTW, with this method, there's no need to store the output. Only one of them will be run, so only one will print output for the function to return.

12
  • to cope with the possibility of multiple usernames having the same uid, wrap everything from if to fi in { ... } | head -n 1. i.e. drop all but the first uid match. but that will mean you'll have to capture the exit code of whatever program got run.
    – cas
    Sep 12, 2019 at 14:50
  • Thanks for the answer. I was hoping that there might be some other utility that I hadn’t come across but this is helpful. Because I don’t have access to an implementation of id that doesn’t accept an ID as an operand, I figured testing its exit status could be problematic – how to tell the difference between a login name that doesn’t exist or a UID that doesn’t exist. It’s possible for a login name to consist of only numerical characters: gnu.org/software/coreutils/manual/html_node/… Sep 12, 2019 at 15:01
  • 1
    With every edit, the function is becoming more robust. :) On that note, I’d probably use if command -v getent >/dev/null; instead of if [ -x /usr/bin/getent ] ; on the off-chance that these utilities have a different path. Sep 12, 2019 at 15:12
  • 3
    Yes. I regularly use command -v for this purpose: pubs.opengroup.org/onlinepubs/9699919799/utilities/command.html (though I've only ever tested it with the dash shell builtin). Sep 12, 2019 at 15:16
  • 1
    @AnthonyGeoghegan If you ever have to work on ancient systems, type foo >/dev/null 2>/dev/null works on every sh that I've ever seen. command is comparatively modern. Sep 12, 2019 at 21:02
8

POSIX specifies getpwuid as a standard C function to search the user database for a user ID allowing the ID to be translated to a login name. I downloaded the source code for GNU coreutils and can see this function being used in their implementation of utilities such as id and ls.

As a learning exercise, I wrote this quick-and-dirty C program to simply act as a wrapper for this function. Bear in mind that I haven’t programmed in C since college (many years ago) and I don’t intend to use this in production but I thought I’d post it here as a proof of concept (if anyone wants to edit it, feel free):

#include <stdio.h>
#include <stdlib.h>  /* atoi */
#include <pwd.h>

int main( int argc, char *argv[] ) {
    uid_t uid;
    if ( argc >= 2 ) {
        /* NB: atoi returns 0 (super-user ID) if argument is not a number) */
        uid = atoi(argv[1]);
    }
    /* Ignore any other arguments after the first one. */
    else {
        fprintf(stderr, "One numeric argument must be supplied.\n");
        return 1;
    }

    struct passwd *pwd;
    pwd = getpwuid(uid);
    if (pwd) {
        printf("The login name for %d is: %s\n", uid, pwd->pw_name);
        return 0;
    }
    else {
        fprintf(stderr, "Invalid user ID: %d\n", uid);
        return 1;
    }
}

I didn’t have the chance to test it with NIS/LDAP but I noticed that if there are multiple entries for the same user in /etc/passwd, it ignores all but the first.

Example usage:

$ ./get_user ""
The login name for 0 is: root

$ ./get_user 99
Invalid user ID: 99
7

There's nothing in POSIX that would help other than id. Trying id and falling back to parsing /etc/passwd is probably as portable as it gets in practice.

BusyBox's id doesn't accept user IDs, but systems with BusyBox are usually autonomous embedded systems where parsing /etc/passwd is enough.

In case you encounter a non-GNU system where id doesn't accept user IDs, you could also try calling getpwuid via Perl, on the chance that it's available:

username=$(perl -e 'print((getpwuid($ARGV[0]))[0])) 2>/dev/null
if [ -n "$username" ]; then echo "$username"; return; fi

Or Python:

if python -c 'import pwd, sys; print(pwd.getpwuid(int(sys.argv[1]))).pw_name' 2>/dev/null; then return; fi
9
  • 3
    Parsing /etc/passwd is not portable at all, and will not work for non-passwd-file backends like LDAP. Sep 12, 2019 at 23:38
  • I like that, I'll steal it
    – cas
    Sep 13, 2019 at 2:39
  • 1
    @R.. The asker is aware of that, this answer doesn't claim otherwise, so what's the point of your comment? Sep 13, 2019 at 6:57
  • Thanks for this answer. I'm reassured that there isn't some other utility that I wasn't aware of. It looks like POSIX specifies a standard C function to translate UID to a login name but not necessarily a corresponding command (other than id). Sep 13, 2019 at 12:06
  • 2
    As a last fallback, check if there is a c compiler on the system, then compile a supplied getpwuid() wrapper ... Sep 13, 2019 at 12:22
3

Generally, I would recommend against doing this. The mapping from user names to uids is not one-to-one, and encoding assumptions that you can convert back from a uid to get a username is going to break things. For example, I often run completely root-free user-namespace containers by making the passwd and group files in the container map all user and group names to id 0; this allows installing packages to work without chown failing. But if something tries to convert 0 back to a uid and doesn't get what it expects, it will gratuitously break. So in this example, instead of converting back and comparing usernames, you should convert to uids and compare in that space.

If you really do need to do this operation, it might be possible to do semi-portably if you're root, by making a temp file, chowning it to the uid, then using ls to read back and parse the owner's name. But I would just use a well-known approach that's not standardized but "portable in practice", like one of the ones you found already.

But again, don't do this. Sometimes something being hard to do is sending you a message.

1
  • 1
    Comments purged. I'd like to remind both participants that comments need to be civil.
    – terdon
    Sep 14, 2019 at 11:38

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .