POSIX兼容的方法来获取与用户ID关联的用户名


23

我经常想获取与用户ID相关联的登录名,并且由于事实证明这是一种常见用例,因此我决定编写一个Shell函数来完成此操作。当我主要使用GNU / Linux发行版时,我尝试编写脚本以使其尽可能具有可移植性,并检查我在做什么与POSIX兼容。

解析 /etc/passwd

我尝试的第一种方法是解析/etc/passwd(使用awk)。

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

但是,这种方法的问题在于登录名可能不是本地的,例如,用户身份验证可以通过NIS或LDAP进行。

使用getent命令

使用getent passwd比解析更具可移植性,/etc/passwd因为它还会查询非本地NIS或LDAP数据库。

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

不幸的是,该getent实用程序似乎未由POSIX指定。

使用id命令

id 是POSIX标准化的实用程序,用于获取有关用户身份的数据。

BSD和GNU实现接受用户ID作为操作数:

这意味着它可用于打印与用户ID关联的登录名:

id -nu "$uid"

但是,未在POSIX中指定提供用户ID作为操作数。它仅描述使用登录名作为操作数。

结合以上所有

我考虑过将以上三种方法合并为以下内容:

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
}

但是,这是笨拙,不雅且更重要的是不可靠。如果用户ID无效或不存在,它将以非零状态退出。在编写一个更长而笨拙的shell脚本来分析并存储每个命令调用的退出状态之前,我想我会在这里问:

是否有一种更优雅,更便携(与POSIX兼容)的方式来获取与用户ID相关联的登录名?


10
为了增加乐趣,请考虑将多个用户名映射到相同的ID ...
Stephen Kitt,

在此问题的上下文中,多个用户名映射到相同的ID的问题是,第一个匹配项都不返回,getentid不会返回任何值。找到所有用户的唯一方法是枚举所有用户,如果用户数据库允许的话。(/etc/passwd显然,正在寻找那里定义的用户的作品。)
Stephen Kitt

1
感谢@StephenKitt,我在自己的数据库中创建了这样的条目,/etc/passwd/etc/shadow测试了这种情况,并验证了两者的行为,idgetent passwd按照您的描述进行操作。如果在某个阶段最终使用了一个用户有多个名称的系统,则将与这些系统实用程序相同,并且将第一个出现的名称视为该用户的规范名称。
安东尼G-莫妮卡

1
确实POSIX需要用户ID与用户名相关联的所有?以root用户身份运行的任何程序都可以调用setuid(some_id),并且没有任何要求some_id可以包含在任何用户数据库中。对于Linux上的用户名称空间之类的东西,这可能会成为脚本的一个残酷假设。
mosvy

1
@Philippos似乎是调用该getpwuid()函数的昂贵方式,该函数ls用于将UID转换为登录名。吉尔斯的答案是实现这一目标的更直接有效的方法。
Anthony G-莫妮卡

Answers:


14

一种执行此操作的常用方法是测试所需的程序是否存在,并且可以从Windows XP中获取该程序PATH。例如:

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
}

因为POSIX id不支持UID参数,所以eliffor 的 子句id不仅必须测试idPATH中是否存在,还必须测试它是否可以正确运行。这意味着它可以运行id两次,所幸不会对性能产生明显影响。这也有可能是两个idawk将运行,用相同的可以忽略不计的性能影响。

顺便说一句,使用这种方法,不需要存储输出。其中只有一个会运行,因此只有一个会打印输出以使函数返回。


适应具有相同的UID多个用户名的可能性,从包裹一切iffi{ ... } | head -n 1。即删除所有除第一个uid匹配。但这将意味着您必须捕获运行任何程序的退出代码。
cas

感谢您的回答。我希望可能还没有遇到其他实用程序,但这很有用。因为我无法访问id不接受ID作为操作数的实现,所以我认为测试其退出状态可能会出现问题-如何区分不存在的登录名或UID不存在。这是可能的登录名只包含数字字符:gnu.org/software/coreutils/manual/html_node/...
正义莫妮卡-安东尼摹

1
每次编辑后,该功能将变得更加强大。:)关于这一点,我可能会使用if command -v getent >/dev/null;,而不是if [ -x /usr/bin/getent ] ;在关闭的机会,这些工具有不同的路径。
安东尼G-莫妮卡

3
是。我经常command -v为此目的使用:pubs.opengroup.org/onlinepubs/9699919799/utilities/command.html(尽管我只用dash内置的shell 测试过)。
安东尼G-莫妮卡

1
@AnthonyGeoghegan如果您必须在古老的系统上type foo >/dev/null 2>/dev/null工作,请在我见过的每一个sh上工作。command比较现代。
吉尔(Gilles)“所以,别再邪恶了”

6

除了以外,POSIX中没有任何其他帮助id。尝试id并退回解析/etc/passwd可能与实际操作一样可移植。

BusyBox id不接受用户ID,但是具有BusyBox的系统通常是自动嵌入式系统,在这些系统中解析/etc/passwd就足够了。

万一遇到非GNU系统id不接受用户ID的情况,您还可以尝试getpwuid通过Perl进行调用,以获取可用的机会:

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

或Python:

if python -c 'import pwd, sys; print(pwd.getpwuid(int(sys.argv[1]))).pw_name' 2>/dev/null; then return; fi

2
解析/etc/passwd根本不是可移植的,并且不适用于LDAP等非passwd文件后端。
R ..

我喜欢,我会偷的
cas

1
@R ..询问者知道这一点,除非另有说明,否则您的评论有什么意义?
吉尔(Gilles)“所以,别再邪恶了”

感谢您的回答。我可以放心,没有其他我不知道的实用程序。看起来POSIX指定了一个标准C函数来将UID转换为登录名,但不一定是相应的命令(除外id)。
Anthony G-莫妮卡

2
作为最后的备用,检查系统上是否有ac编译器,然后编译提供的getpwuid()包装器
rackandboneman

5

POSIX指定getpwuid作为标准C函数在用户数据库中搜索用户ID,以将ID转换为登录名。我下载了GNU coreutils的源代码,并且可以看到此函数在诸如id和的实用程序的实现中正在使用ls

作为一项学习练习,我编写了这个快速而又肮脏的C程序,只是将其用作该函数的包装器。请记住,自大学(很多年前)以来,我还没有使用C进行编程,因此我不打算在生产环境中使用它,但我想我会将其发布在这里作为概念证明(如果有人想对其进行编辑) ,随意):

#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;
    }
}

我没有机会使用NIS / LDAP进行测试,但是我注意到,如果中的同一用户有多个条目/etc/passwd,它将忽略除第一个条目以外的所有条目。

用法示例:

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

$ ./get_user 99
Invalid user ID: 99

3

通常,我建议您不要这样做。从用户名到uid的映射不是一对一的,并且可以 uid 转换以获取用户名的编码假设将破坏事情。例如,我经常通过使容器中的passwdgroup文件将所有用户名和组名映射为id 0来运行完全无根的用户命名空间容器;这样一来,安装软件包即可正常运行chown。但是,如果某些东西试图将0转换回uid却没有得到期望的结果,它将无偿中断。因此,在此示例中,您应该转换为uid并在该空间进行比较,而不是转换回去并比较用户名。

如果确实需要执行此操作,则如果您是root用户,则可以进行半便携式操作,方法是制作一个临时文件,将chown其写入uid,然后使用ls回读并解析所有者的名称。但是,我只是使用一种众所周知的方法,它不是标准化的而是“在实践中可移植的”,就像您已经发现的一种方法一样。

但同样,不要这样做。有时很难做的就是向您发送消息。


1
评论已清除。我想提醒两位与会者,评论必须是民俗的。
terdon
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.