A documentação para -i
afirma:
The
-i
(simulate initial login) option runs the shell specified by the password database entry of the target user as a login shell. This means that login-specific resource files such as .profile or .login will be read by the shell. If a command is specified, it is passed to the shell for execution via the shell's -c option.
Ou seja, ele executa genuinamente o shell de login do usuário e, em seguida, passa o comando que você forneceu sudo
para o usando -c
- diferente de , que sudo cmd arg arg
normalmente faz sem -i
opção. Normalmente, sudo
usa apenas uma das as funções exec*
diretamente para iniciar o processo em si , sem shell intermediário e todos os argumentos passados exatamente como estão.
Com -i
, ele configura o ambiente, executa o shell do usuário como um shell de login e reconstrói o comando que você solicitou para ser executado como um argumento para bash -c
. No seu caso, ele é executado (digamos, aproximadamente) /bin/bash -c "bash -c ' ... '"
(imagine citando que funciona).
O problema está em como sudo
transforma o comando que você escreveu em algo que -c
pode lidar com , explicado na próxima seção. A última seção tem algumas soluções possíveis, e entre algumas técnicas de depuração e verificação,
Por que isso acontece?
Ao passar o comando para -c
, ele precisa de algum pré-processamento para fazer a coisa certa, o que sudo
faz antes de executar o shell. Por exemplo, se seu comando for:
sudo -iu yy echo 'three spaces'
então esses espaços precisam ser escapados para que o comando signifique a mesma coisa (ou seja, para o argumento único não ser dividido em duas palavras). O que acaba sendo executado é:
/bin/bash -c 'echo three\ \ \ spaces'
Vamos começar com o seu comando simplificado:
sudo bash -c 'echo 1
echo 2'
Nesse caso, sudo
altera o usuário e, em seguida, executa execvp("bash", \["bash", "-c", "echo 1\necho 2"\])
(por uma sintaxe literal de array inventada).
com -i
:
sudo -i bash -c 'echo 1
echo 2'
em vez disso, ele altera o usuário e executa execv("/bin/bash", ["-bash", "-c", "bash -c echo\ 1\\necho\ 2"])
, em que \
equivale a um literal \
e \n
é um quebra de linha. Ele escapou dos espaços e da nova linha em seu comando principal, precedendo-os com barras invertidas.
Ou seja, há um shell de login externo, que possui dois argumentos: -c
e todo o seu comando, reconstruídos em um formulário que se espera que o shell entenda corretamente. Infelizmente, isso não acontece. O comando interno bash
tenta finalmente executar:
echo 1\
echo 2
em que a primeira linha física termina com uma continuação de linha (barra invertida seguida de nova linha), que é totalmente excluída. A linha lógica é apenas echo 1echo 2
, que não faz o que você queria.
Há um argumento de que essa é uma falha no escape de sudo
, dado o padrão comportamento de pares de barra invertida-nova linha . Eu acho que deveria ser seguro deixá-los sem escape aqui.
O mesmo acontece para o seu comando com um documento aqui. Corre como, aproximadamente:
/bin/bash -c 'bash -c cat\ \<\<\ \"EOF\"\012script-content\012EOF\012'
em que 2
representa uma nova linha real - sudo
inseriu uma barra invertida antes de cada uma delas, assim como os espaços. Observe o escape duplo na \012
: que é a versão ps
de uma barra invertida real seguida de nova linha, que estou usando aqui (veja abaixo). O que eventualmente é executado é:
bash -c 'cat << "EOF"\
script-content\
EOF\
'
com continuações de linha \
+ nova linha em todos os lugares, que são apenas removido. Isso faz com que seja uma linha longa, sem novas linhas reais, e um heredoc inválido:
bash -c 'cat << "EOF"script-contentEOF'
Então, esse é o seu problema: o processo bash
interno recebe apenas uma linha de comando e, portanto, o documento aqui nunca tem a chance de terminar (ou iniciar). Eu tenho algumas soluções imperfeitas na parte inferior, mas esta é a causa subjacente.
Como você pode verificar o que está acontecendo?
Para obter esses comandos corretamente citados e validar o que estava acontecendo, modifiquei o arquivo de perfil do shell de login ( .profile
, .bash_profile
, .zprofile
, etc) para dizer apenas:
ps awx|grep $$
Isso mostra a linha de comando do shell em execução no momento e me fornece um par extra de linhas de saída antes do aviso.
hexdump -C /proc/$$/cmdline
também será útil no Linux.
O que você pode fazer sobre isso?
Eu não vejo uma maneira óbvia e confiável de conseguir o que você quer com isso. Você não quer que sudo
toque em seu comando, se possível. Uma opção que funcionará em grande parte para um caso simples é canalizar os comandos para o shell, em vez de especificá-los na linha de comando:
printf 'cat > ... << ... \n ...' | sudo -iu yy
Isso requer cuidadoso escape interno ainda.
Provavelmente, é melhor colocá-los em um arquivo de script temporário e executá-los dessa maneira:
f='mktemp'
printf 'command' > "$f"
chmod +r "$f"
sudo -iu yy "$f"
rm "$f"
Um nome próprio criado por você também funcionará. Dependendo de suas configurações de sudoers, você poderá manter um descritor de arquivo aberto e fazer com que ele seja lido como um arquivo ( /dev/fd/3
), se você realmente não o quiser no disco, mas um arquivo real será mais fácil.