Maneira compatível com POSIX para obter o nome do usuário associado a um ID do usuário

23

Muitas vezes, quero obter o nome de login associado a um ID de usuário e, como é um caso de uso comum, decidi escrever uma função de shell para fazer isso. Enquanto eu uso principalmente distribuições GNU / Linux, tento escrever meus scripts para serem o mais portáveis ​​possível e verificar se o que estou fazendo é compatível com POSIX.

Analisar /etc/passwd

A primeira abordagem que tentei foi analisar /etc/passwd(usando awk).

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

No entanto, o problema dessa abordagem é que os logins podem não ser locais, por exemplo, a autenticação do usuário pode ser via NIS ou LDAP.

Use o getentcomando

O uso getent passwdé mais portátil do que a análise, /etc/passwdpois também consulta bancos de dados NIS ou LDAP não locais.

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

Infelizmente, o getentutilitário não parece ser especificado pelo POSIX.

Use o idcomando

id é o utilitário padronizado do POSIX para obter dados sobre a identidade de um usuário.

As implementações BSD e GNU aceitam um ID do usuário como um operando:

Isso significa que ele pode ser usado para imprimir o nome de login associado a um ID do usuário:

id -nu "$uid"

No entanto, fornecer IDs do usuário como operando não é especificado no POSIX; descreve apenas o uso de um nome de login como o operando.

Combinando todas as opções acima

Eu considerei combinar as três abordagens acima em algo como o seguinte:

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
}

No entanto, isso é desajeitado, deselegante e - mais importante - não robusto; sai com um status diferente de zero se o ID do usuário for inválido ou não existir. Antes de escrever um script de shell mais longo e desajeitado que analise e armazene o status de saída de cada chamada de comando, pensei em perguntar aqui:

Existe uma maneira mais elegante e portátil (compatível com POSIX) de obter o nome de login associado a um ID do usuário?

Anthony G - justiça para Monica
fonte
10
Para mais diversão, considere que vários nomes de usuário podem ser mapeados para o mesmo id ...
Stephen Kitt
O problema com vários nomes de usuário mapeados para o mesmo id no contexto desta questão é que nem getentnem idvai retornar nada passado o primeiro jogo; a única maneira de encontrar todos eles é enumerar todos os usuários, se o banco de dados do usuário permitir. (Procurando /etc/passwdtrabalhos para usuários definidos lá, obviamente).
Stephen Kitt
1
Obrigado @StephenKitt Eu criei uma entrada no meu /etc/passwde /etc/shadowpara testar esse cenário e verifiquei que ambos ide getent passwdse comportam como você descreve. Se, em algum momento, eu acabar usando um sistema em que um usuário tem vários nomes, farei o mesmo que esses utilitários de sistema e simplesmente tratarei a primeira ocorrência como o nome canônico desse usuário.
Anthony G - justice for Monica
1
Does POSIX requer um ID de usuário a ser associado com um nome de usuário em tudo ? Qualquer programa em execução como root pode chamar setuid(some_id), e não há requisitos que some_idpossam fazer parte de qualquer banco de dados do usuário. Com coisas como namespaces de usuário no Linux, isso pode se tornar uma suposição incapacitante para seus scripts.
mosvy 13/09
1
@ Philippos, que parece uma maneira cara de chamar a getpwuid()função que lsusa para converter UIDs em nomes de login. A resposta de Gilles é uma maneira mais direta e eficiente de conseguir isso.
Anthony G - justice for Monica

Respostas:

14

Uma maneira comum de fazer isso é testar se o programa que você deseja existe e está disponível no seu site PATH. Por exemplo:

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
}

Como o POSIX idnão suporta argumentos UID, a elifcláusula for iddeve testar não apenas se idestá no PATH, mas também se será executado sem erros. Isso significa que ele pode ser executado idduas vezes, o que felizmente não terá um impacto perceptível no desempenho. Também é possível que ambos ide awksejam executados, com o mesmo desempenho insignificante.

BTW, com este método, não há necessidade de armazenar a saída. Apenas um deles será executado, portanto, apenas um imprimirá a saída para a função retornar.

cas
fonte
para lidar com a possibilidade de vários nomes de usuário com o mesmo uid, envolva tudo de ifpara fidentro { ... } | head -n 1. ou seja, largue tudo, exceto a primeira partida do uid. mas isso significa que você precisará capturar o código de saída de qualquer programa executado.
cas
Obrigado pela resposta. Eu esperava que houvesse algum outro utilitário que eu não tivesse encontrado, mas isso é útil. Como não tenho acesso a uma implementação idque não aceita um ID como operando, achei que testar seu status de saída poderia ser problemático - como saber a diferença entre um nome de login que não existe ou um UID que não existe. É possível que um nome de login seja composto apenas por caracteres numéricos: gnu.org/software/coreutils/manual/html_node/…
Anthony G - justice para Monica
1
A cada edição, a função está se tornando mais robusta. :) Nessa nota, eu provavelmente usaria em if command -v getent >/dev/null;vez de ter if [ -x /usr/bin/getent ] ;a chance de esses utilitários terem um caminho diferente.
Anthony G - justice for Monica
3
Sim. Uso regularmente command -vpara esse fim: pubs.opengroup.org/onlinepubs/9699919799/utilities/command.html (embora eu só o tenha testado com o dashshell interno).
Anthony G - justice for Monica
1
@AnthonyGeoghegan Se você já trabalhou em sistemas antigos, type foo >/dev/null 2>/dev/nulltrabalha em todos os aspectos que já vi. commandé relativamente moderno.
Gilles 'SO- stop be evil'
6

Não há nada no POSIX que possa ajudar a não ser id. Tentar ide voltar a analisar /etc/passwdé provavelmente o mais portátil possível.

O BusyBox idnão aceita IDs de usuário, mas os sistemas com o BusyBox geralmente são sistemas embarcados autônomos, onde a análise /etc/passwdé suficiente.

Caso encontre um sistema não-GNU em idque não aceite IDs de usuário, você também pode tentar ligar getpwuidvia Perl, com a chance de estar disponível:

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

Ou Python:

if python -c 'import pwd, sys; print(pwd.getpwuid(int(sys.argv[1]))).pw_name' 2>/dev/null; then return; fi
Gilles 'SO- parar de ser mau'
fonte
2
A análise /etc/passwdnão é de todo portátil e não funcionará para back-end de arquivos que não sejam do passwd, como o LDAP.
R ..
Eu gosto disso, vou roubá-lo
cas
1
@R .. O autor da pergunta está ciente disso, esta resposta não afirma o contrário, então qual é o sentido do seu comentário?
Gilles 'SO- stop be evil'
Obrigado por esta resposta. Estou certo de que não há outro utilitário que eu não conhecia. Parece que o POSIX especifica uma função C padrão para converter o UID em um nome de login, mas não necessariamente em um comando correspondente (exceto id).
Anthony G - justice for Monica
2
Como último recurso, verifique se há um compilador de corrente alternada no sistema e compile um wrapper fornecido getpwuid () ...
rackandboneman
5

POSIX especifica getpwuidcomo uma função C padrão para procurar no banco de dados do usuário por um ID do usuário, permitindo que o ID seja convertido em um nome de login. Eu baixei o código fonte do GNU coreutils e posso ver essa função sendo usada na implementação de utilitários como ide ls.

Como exercício de aprendizado, escrevi este programa C rápido e sujo para simplesmente atuar como um invólucro para essa função. Lembre-se de que não tenho programado em C desde a faculdade (há muitos anos) e não pretendo usá-lo na produção, mas pensei em publicá-lo aqui como uma prova de conceito (se alguém quiser editá-lo) , fique à vontade):

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

Não tive a chance de testá-lo com NIS / LDAP, mas notei que, se houver várias entradas para o mesmo usuário /etc/passwd, ele ignorará todas, exceto a primeira.

Exemplo de uso:

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

$ ./get_user 99
Invalid user ID: 99
Anthony G - justiça para Monica
fonte
3

Geralmente, eu recomendaria não fazer isso. O mapeamento de nomes de usuário para uids não é um para um, e as suposições de codificação que você pode converter de volta de um uid para obter um nome de usuário vão quebrar as coisas. Por exemplo, geralmente executo contêineres de espaço para nome de usuário completamente livres de raiz, fazendo com que os arquivos passwde groupno contêiner mapeiem todos os nomes de usuário e grupo para o ID 0; isso permite que a instalação de pacotes funcione sem chownfalhar. Mas se algo tentar converter 0 de volta em um uid e não conseguir o que espera, ele será quebrado gratuitamente. Portanto, neste exemplo, em vez de converter novamente e comparar nomes de usuários, você deve converter em uids e comparar nesse espaço.

Se você realmente precisar fazer essa operação, poderá ser possível executar de maneira semi-portável, se você for root, criando um arquivo temporário, chowninserindo-o no uid e usando-o lspara ler novamente e analisar o nome do proprietário. Mas eu usaria uma abordagem bem conhecida que não é padronizada, mas "portátil na prática", como uma das que você já encontrou.

Mas, novamente, não faça isso. Às vezes, algo difícil de fazer é enviar uma mensagem.

R ..
fonte
1
Comentários eliminados. Gostaria de lembrar aos dois participantes que os comentários precisam ser civis.
terdon