我经常想要获取与用户 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 关联的登录名?
答案1
一种常见的方法是测试您想要的程序是否存在并且可以从您的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
}
由于 POSIXid
不支持 UID 参数,因此 elif
for 子句id
不仅要测试是否id
在 PATH 中,还要测试它是否能够正常运行。这意味着它可能会运行id
两次,幸运的是这不会对性能产生明显的影响。也有可能同时运行id
和,而性能影响同样可以忽略不计。awk
顺便说一句,使用这种方法,不需要存储输出。仅运行其中之一,因此只有其中之一会打印函数返回的输出。
答案2
POSIX 指定getpwuid
作为标准 C 函数,用于在用户数据库中搜索用户 ID,从而允许将 ID 转换为登录名。我下载了GNU的源代码核心工具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
POSIX 中除了id
.尝试id
并返回解析/etc/passwd
可能在实践中是可移植的。
BusyBoxid
不接受用户 ID,但带有 BusyBox 的系统通常是自治嵌入式系统,其中解析/etc/passwd
就足够了。
如果您遇到id
不接受用户 ID 的非 GNU 系统,您也可以尝试调用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
答案4
一般来说,我建议不要这样做。从用户名到 uids 的映射不是一对一的,并且您可以转换的编码假设从...回来获取用户名的 uid 会破坏一切。例如,我经常通过使容器中的passwd
和文件将所有用户名和组名映射到 id 0 来运行完全无根的用户命名空间容器;group
这使得安装包能够正常工作而不会chown
失败。但是如果某些东西试图将 0 转换回 uid 并且没有得到它所期望的结果,它就会无缘无故地崩溃。因此,在此示例中,您应该转换为 uid 并在该空间中进行比较,而不是转换回并比较用户名。
如果您确实需要执行此操作,如果您是 root,则可以通过创建一个临时文件,chown
将其写入 uid,然后使用ls
读回并解析所有者的名称来进行半可移植。但我只会使用一种众所周知的方法,该方法不是标准化的,但“在实践中可移植”,就像您已经找到的方法之一一样。
但再次强调,不要这样做。有时,很难做的事情就是向您发送消息。