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?

  • 10
    For added fun, consider that multiple usernames can map to the same id... – Stephen Kitt Sep 12 '19 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.) – Stephen Kitt Sep 12 '19 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. – Anthony Geoghegan Sep 12 '19 at 17:10
  • That’s probably the only sensible approach... (And in any case, as mentioned by Gilles in his answer to the question you linked above, having multiple usernames for the same id isn’t a good idea. I use it to provide static shells for root, but that’s increasingly irrelevant anyway.) – Stephen Kitt Sep 12 '19 at 17:13
  • @StephenKitt While trying to prevent comment clutter from building up, I deleted the comment which contains the link to Gilles' answer to https://unix.stackexchange.com/q/124968/22812 just before you posted your comment. – Anthony Geoghegan Sep 12 '19 at 17:20
  • 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. –  Sep 13 '19 at 05:04
  • Maybe a little slow, but this is supposed to work on any system and only relies on a public viewable file of that user: find / -user 42 2>/dev/null -exec ls -do {} \; |cut -d" " -f 3|head -n 1 – Philippos Sep 13 '19 at 07:07
  • @Philippos that breaks for user account names containing a space. (Hello Active Directory, I'm looking at you...) On the other hand, POSIX states that conforming usernames must not contain a space, so for POSIX conformance you're fine. – Chris Davies Sep 13 '19 at 10:02
  • 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. – Anthony Geoghegan Sep 13 '19 at 12:07

4 Answers4

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.

cas
  • 78,579
  • 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 '19 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: https://www.gnu.org/software/coreutils/manual/html_node/Disambiguating-names-and-IDs.html#Disambiguating-names-and-IDs – Anthony Geoghegan Sep 12 '19 at 15:01
  • i didn't consider that.....but a) that case will fall through to awk anyway, and b) the awk code there will always have an exit code of 0 whether the uid was matched or not. – cas Sep 12 '19 at 15:05
  • fixed the awk script so that it exits with 2 if not found, same as getent. – cas Sep 12 '19 at 15:07
  • btw, the awk command will fail if the user data is stored in nis or ldap or active directory or something other than /etc/passwd. obvious, but worth mentioning. it's probably the only reason why it's worthwhile running id. – cas Sep 12 '19 at 15:12
  • 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. – Anthony Geoghegan Sep 12 '19 at 15:12
  • is command -v posix? i started with type -p and then realised that -p isn't posix. – cas Sep 12 '19 at 15:14
  • 3
    Yes. I regularly use command -v for this purpose: https://pubs.opengroup.org/onlinepubs/9699919799/utilities/command.html (though I've only ever tested it with the dash shell builtin). – Anthony Geoghegan Sep 12 '19 at 15:16
  • neither getent, id, nor the awk script will ever return more than one username....so the {...} | head -n1 and $rc is no longer needed. i'll delete all that. – cas Sep 12 '19 at 17:32
  • 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. – Gilles 'SO- stop being evil' Sep 12 '19 at 21:02
  • BTW $uid really should be a local variable. but that's difficult to do in a portable, posix-compliant way. See List of shells that support local keyword for defining local variables – cas Sep 13 '19 at 02:48
  • Also, the perl variation I added doesn't output a LF after the username. That shouldn't matter, but if your calling code relies on it for some reason, change the print to print $u[0], "\n" – cas Sep 13 '19 at 02:54
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
  • 3
    Parsing /etc/passwd is not portable at all, and will not work for non-passwd-file backends like LDAP. – R.. GitHub STOP HELPING ICE Sep 12 '19 at 23:38
  • I like that, I'll steal it – cas Sep 13 '19 at 02:39
  • 1
    @R.. The asker is aware of that, this answer doesn't claim otherwise, so what's the point of your comment? – Gilles 'SO- stop being evil' Sep 13 '19 at 06: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). – Anthony Geoghegan Sep 13 '19 at 12:06
  • 2
    As a last fallback, check if there is a c compiler on the system, then compile a supplied getpwuid() wrapper ... – rackandboneman Sep 13 '19 at 12:22
  • POSIX XCU specifies a c99 command. :-) – R.. GitHub STOP HELPING ICE Sep 13 '19 at 12:59
  • @Gilles: I was speaking to "probably as portable as it gets in practice". For example it wouldn't work on most Alpine Linux systems (BusyBox id by default) with non-passwd-file-backend user db. – R.. GitHub STOP HELPING ICE Sep 13 '19 at 13:00
  • cc is POSIX as is getpwuid; build a program and execute it. – Joshua Sep 14 '19 at 01:33
  • @Joshua Not quite: it's part of the “C-Language Development Utilities” extension, which in practice is not present on lots of POSIX-compliant systems, usually because it's part of an optional package which is not installed. Also, cc has not been POSIX in a long time, and even when it was there was no guarantee that it would compile anything correctly (it was an unspecified variant of the C language, which could have been K&R C or might not link requisite libraries). POSIX specifies c99 (and in earlier times c89). – Gilles 'SO- stop being evil' Sep 14 '19 at 06:58
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.