Vários argumentos em shebang

32

Eu estou querendo saber se existe uma maneira geral de passar várias opções para um executável através da linha shebang ( #!).

Eu uso o NixOS, e a primeira parte do shebang em qualquer script que escrevo é normalmente /usr/bin/env. O problema que encontro então é que tudo o que vem depois é interpretado como um único arquivo ou diretório pelo sistema.

Suponha, por exemplo, que eu queira escrever um script para ser executado bashno modo posix. A maneira ingênua de escrever o shebang seria:

#!/usr/bin/env bash --posix

mas tentar executar o script resultante produz o seguinte erro:

/usr/bin/env: ‘bash --posix’: No such file or directory

Estou ciente deste post , mas queria saber se havia uma solução mais geral e mais limpa.


EDIT : Eu sei que para scripts Guile , há uma maneira de conseguir o que eu quero, documentado na Seção 4.3.4 do manual:

 #!/usr/bin/env sh
 exec guile -l fact -e '(@ (fac) main)' -s "$0" "$@"
 !#

O truque, aqui, é que a segunda linha (começando com exec) é interpretada como código por sh, mas, estando no bloco #!... !#, como um comentário e, portanto, ignorada, pelo intérprete Guile.

Não seria possível generalizar esse método para nenhum intérprete?


Segunda edição : depois de brincar um pouco, parece que, para intérpretes que podem ler suas entradas stdin, o seguinte método funcionaria:

#!/usr/bin/env sh
sed '1,2d' "$0" | bash --verbose --posix /dev/stdin; exit;

Provavelmente não é o ideal, pois o shprocesso dura até que o intérprete termine seu trabalho. Qualquer feedback ou sugestão seria apreciada.

Rastapopoulos
fonte

Respostas:

27

Não existe uma solução geral, pelo menos não se você precisar dar suporte ao Linux, porque o kernel do Linux trata tudo após a primeira "palavra" na linha shebang como um único argumento .

Não sei ao certo quais são as restrições do NixOS, mas normalmente eu escreveria seu shebang como

#!/bin/bash --posix

ou, sempre que possível, defina opções no script :

set -o posix

Como alternativa, você pode reiniciar o script com a chamada de shell apropriada:

#!/bin/sh -

if [ "$1" != "--really" ]; then exec bash --posix -- "$0" --really "$@"; fi

shift

# Processing continues

Essa abordagem pode ser generalizada para outros idiomas, desde que você encontre uma maneira de as primeiras linhas (que são interpretadas pelo shell) serem ignoradas pelo idioma de destino.

O GNU coreutils' envfornece uma solução alternativa desde a versão 8.30, veja a resposta de unode para obter detalhes. (Está disponível no Debian 10 e posterior, RHEL 8 e posterior, Ubuntu 19.04 e posterior, etc.)

Stephen Kitt
fonte
18

Embora não seja exatamente portátil, a partir do coreutils 8.30 e de acordo com a documentação, você poderá usar:

#!/usr/bin/env -S command arg1 arg2 ...

Então dado:

$ cat test.sh
#!/usr/bin/env -S showargs here 'is another' long arg -e "this and that " too

você vai ter:

% ./test.sh 
$0 is '/usr/local/bin/showargs'
$1 is 'here'
$2 is 'is another'
$3 is 'long'
$4 is 'arg'
$5 is '-e'
$6 is 'this and that '
$7 is 'too'
$8 is './test.sh'

e caso você esteja curioso showargsé:

#!/usr/bin/env sh
echo "\$0 is '$0'"

i=1
for arg in "$@"; do
    echo "\$$i is '$arg'"
    i=$((i+1))
done
unode
fonte
É muito bom saber para referência futura.
John McGehee
Essa opção foi copiada do FreeBSD, envonde -Sfoi adicionada em 2005. Veja lists.gnu.org/r/coreutils/2018-04/msg00011.html
Stéphane Chazelas
Realiza um tratamento no Fedora 29
Eric
@unode algumas melhorias de showargs: pastebin.com/q9m6xr8H e pastebin.com/gS8AQ5WA (one-liner)
Eric
FYI: a partir do coreutils 8.31, envinclui o seu próprio showargs: a opção -v por exemplo#!/usr/bin/env -vS --option1 --option2 ...
chocolateboy
9

O padrão POSIX é muito conciso ao descrever #!:

Na seção de justificativa da documentação da exec()família de interfaces do sistema :

Outra maneira que algumas implementações históricas lidam com scripts de shell é reconhecer os dois primeiros bytes do arquivo como a cadeia de caracteres #!e usar o restante da primeira linha do arquivo como o nome do interpretador de comando a ser executado.

Na seção Introdução ao Shell :

O shell lê sua entrada de um arquivo (consulte sh), da -copção ou das funções system()e popen()definidas no volume System Interfaces do POSIX.1-2008. Se a primeira linha de um arquivo de comandos shell começar com os caracteres #!, os resultados não serão especificados .

Isso basicamente significa que qualquer implementação (o Unix que você está usando) é livre para fazer as especificações da análise da linha shebang como ela deseja.

Alguns Unices, como o macOS (não podem testar o ATM), dividirão os argumentos dados ao intérprete na linha shebang em argumentos separados, enquanto o Linux e a maioria dos outros Unices fornecerão os argumentos como uma opção única para o intérprete.

Portanto, não é aconselhável confiar na capacidade de a linha shebang assumir mais do que um único argumento.

Veja também a seção Portabilidade do artigo Shebang na Wikipedia .


Uma solução fácil, generalizável para qualquer utilitário ou idioma, é criar um script de wrapper que execute o script real com os argumentos de linha de comando apropriados:

#!/bin/sh
exec /bin/bash --posix /some/path/realscript "$@"

Eu não acho que pessoalmente tentaria fazê-lo se reexecutar , pois isso parece um pouco frágil.

Kusalananda
fonte
7

O shebang é descrito na execve(2) página de manual da seguinte maneira:

#! interpreter [optional-arg]

Dois espaços são aceitos nesta sintaxe:

  1. Um espaço antes do caminho do intérprete , mas esse espaço é opcional.
  2. Um espaço que separa o caminho do interpretador e seu argumento opcional.

Observe que eu não usei o plural ao falar de um argumento opcional, nem a sintaxe acima [optional-arg ...], pois você pode fornecer no máximo um único argumento .

No que diz respeito ao script de shell, você pode usar o setcomando interno próximo ao início do seu script, o que permitirá definir parâmetros de intérpretes, fornecendo o mesmo resultado como se você usasse argumentos de linha de comando.

No seu caso:

set -o posix

Em um prompt do Bash, verifique a saída de help setpara obter todas as opções disponíveis.

WhiteWinterWolf
fonte
1
Você tem permissão para ter mais de dois espaços, eles são apenas considerados parte do argumento opcional.
Stephen Kitt
@StephenKitt: De fato, o espaço em branco aqui deve ser considerado mais como uma categoria do que o caractere de espaço real. Suponho que outros espaços em branco, como tabulações, também sejam amplamente aceitos.
WhiteWinterWolf
3

No Linux, o shebang não é muito flexível; de acordo com várias respostas (resposta de Stephen Kitt e Jörg W Mittag ), não existe uma maneira designada de passar vários argumentos em uma linha shebang.

Não tenho certeza se será útil para alguém, mas escrevi um script curto para implementar o recurso que faltava. Consulte https://gist.github.com/loxaxs/7cbe84aed1c38cf18f70d8427bed1efa .

Também é possível escrever soluções alternativas incorporadas. A seguir, apresento quatro soluções alternativas independentes de idioma aplicadas ao mesmo script de teste e o resultado é impresso. Suponho que o script é executável e é no /tmp/shebang.


Envolvendo seu script em um heredoc bash dentro da substituição do processo

Até onde eu sei, essa é a maneira mais confiável e independente de linguagem. Permite passar argumentos e preserva stdin. A desvantagem é que o intérprete não sabe a localização (real) do arquivo que lê.

#!/bin/bash
exec python3 -O <(cat << 'EOWRAPPER'
print("PYTHON_SCRIPT_BEGINNING")

from sys import argv
try:
    print("input() 0 ::", input())
    print("input() 1 ::", input())
except EOFError:
    print("input() caused EOFError")
print("argv[0]   ::", argv[0])
print("argv[1:]  ::", argv[1:])
print("__debug__ ::", __debug__)
# The -O option changes __debug__ to False

print("PYTHON_SCRIPT_END")
EOWRAPPER
) "$@"

Chamando echo -e 'aa\nbb' | /tmp/shebang 'arg1' 'arg2 contains spaces' 'arg3\ uses\ \\escapes\\'impressões:

PYTHON_SCRIPT_BEGINNING
input() 0 :: aa
input() 1 :: bb
argv[0]   :: /dev/fd/62
argv[1:]  :: ['arg1', 'arg2 contains spaces', 'arg3\\ uses\\ \\\\escapes\\\\']
__debug__ :: False
PYTHON_SCRIPT_END

Observe que a substituição do processo produz um arquivo especial. Isso pode não ser adequado a todos os executáveis. Por exemplo, #!/usr/bin/lessreclama:/dev/fd/63 is not a regular file (use -f to see it)

Não sei se é possível ter o heredoc dentro da substituição do processo no hífen.


Envolvendo seu script em um heredoc simples

Mais curto e mais simples, mas você não poderá acessar stdindo seu script e exige que o intérprete possa ler e executar um script stdin.

#!/bin/sh
exec python3 - "$@" << 'EOWRAPPER'
print("PYTHON_SCRIPT_BEGINNING")

from sys import argv

try:
    print("input() 0 ::", input())
    print("input() 1 ::", input())
except EOFError:
    print("input() caused EOFError")
print("argv[0]   ::", argv[0])
print("argv[1:]  ::", argv[1:])
print("__debug__ ::", __debug__)
# The -O option changes __debug__ to False

print("PYTHON_SCRIPT_END")
EOWRAPPER

Chamando echo -e 'aa\nbb' | /tmp/shebang 'arg1' 'arg2 contains spaces' 'arg3\ uses\ \\escapes\\'impressões:

PYTHON_SCRIPT_BEGINNING
input() caused EOFError
argv[0]   :: -
argv[1:]  :: ['arg1', 'arg2 contains spaces', 'arg3\\ uses\\ \\\\escapes\\\\']
__debug__ :: True
PYTHON_SCRIPT_END

Use system()chamada awk, mas sem argumentos

Passa corretamente o nome do arquivo executado, mas seu script não receberá os argumentos que você fornecer. Observe que o awk é o único idioma que conheço cujo intérprete está instalado no linux por padrão e lê suas instruções na linha de comando por padrão.

#!/usr/bin/gawk BEGIN {system("python3 -O " ARGV[1])}
print("PYTHON_SCRIPT_BEGINNING")

from sys import argv

print("input() 0 ::", input())
print("input() 1 ::", input())
print("argv[0]   ::", argv[0])
print("argv[1:]  ::", argv[1:])
print("__debug__ ::", __debug__)
# The -O option changes __debug__ to False

print("PYTHON_SCRIPT_END")

Chamando echo -e 'aa\nbb' | /tmp/shebang 'arg1' 'arg2 contains spaces' 'arg3\ uses\ \\escapes\\'impressões:

PYTHON_SCRIPT_BEGINNING
input() 0 :: aa
input() 1 :: bb
argv[0]   :: /tmp/shebang
argv[1:]  :: []
__debug__ :: False
PYTHON_SCRIPT_END

Use a system()chamada awk 4.1+ , desde que seus argumentos não contenham espaços

Bom, mas apenas se você tiver certeza de que seu script não será chamado com argumentos que contenham espaços. Como você pode ver, seus argumentos que contêm espaços seriam divididos, a menos que os espaços sejam escapados.

#!/usr/bin/gawk @include "join"; BEGIN {system("python3 -O " join(ARGV, 1, ARGC, " "))}
print("PYTHON_SCRIPT_BEGINNING")

from sys import argv

print("input() 0 ::", input())
print("input() 1 ::", input())
print("argv[0]   ::", argv[0])
print("argv[1:]  ::", argv[1:])
print("__debug__ ::", __debug__)
# The -O option changes __debug__ to False

print("PYTHON_SCRIPT_END")

Chamando echo -e 'aa\nbb' | /tmp/shebang 'arg1' 'arg2 contains spaces' 'arg3\ uses\ \\escapes\\'impressões:

PYTHON_SCRIPT_BEGINNING
input() 0 :: aa
input() 1 :: bb
argv[0]   :: /tmp/shebang
argv[1:]  :: ['arg1', 'arg2', 'contains', 'spaces', 'arg3 uses \\escapes\\']
__debug__ :: False
PYTHON_SCRIPT_END

Para versões awk abaixo de 4.1, você precisará usar a concatenação de strings dentro de um loop for, veja a função de exemplo https://www.gnu.org/software/gawk/manual/html_node/Join-Function.html .

loxaxs
fonte
1
Cite o terminador de documento aqui para inibir $variableou `command`substituir:exec python3 -O <(cat <<'EOWRAPPER'
John McGehee
2

Um truque para usar LD_LIBRARY_PATHcom python na linha #!(shebang) que não depende de nada além do shell e funciona como um deleite:

#!/bin/sh
'''' 2>/dev/null; exec /usr/bin/env LD_LIBRARY_PATH=. python -x "$0" "$@" #'''

__doc__ = 'A great module docstring'

Conforme explicado em outra parte desta página, alguns shells shpodem receber um script em suas entradas padrão.

O script que fornecemos shtenta executar o comando ''''simplificado para ''(a cadeia vazia) she, é claro, falha ao executá-lo, pois não há ''comando; portanto, ele normalmente é exibido line 2: command not foundno descritor de erro padrão, mas redirecionamos essa mensagem usando 2>/dev/nullo comando buraco negro mais próximo, porque seria confuso e confuso para o usuário deixar shexibi-lo.

Em seguida, prosseguimos para o comando de nosso interesse: execque substitui o processo atual do shell pelo seguinte, no nosso caso: /usr/bin/env pythonpelos parâmetros adequados:

  • "$0" para que o python saiba qual script ele deve abrir e interpretar e também defina sys.argv[0]
  • "$@"para definir python sys.argv[1:]com os argumentos passados ​​na linha de comando do script.

E também pedimos envpara definir a LD_LIBRARY_PATHvariável de ambiente, que é o único ponto do hack.

O comando shell termina no comentário começando com, #para que o shell ignore as aspas triplas à direita '''.

shé então substituída por uma nova e brilhante instância do interpretador python que abre e lê o script de origem python fornecido como primeiro argumento (the "$0").

Python abre o arquivo e pula a primeira linha da fonte, graças ao -xargumento. Nota: também funciona sem, -xporque para Python um shebang é apenas um comentário .

O Python interpreta a segunda linha como a docstring do arquivo do módulo atual; portanto, se você precisar de uma docstring válida do módulo, defina a __doc__primeira coisa no seu programa python, como no exemplo acima.

Eric
fonte
Dado que uma string vazia está ... hum ... vazia, você deve descartar seu comando não encontrado: negócio de macacos: ''''exec ...deve fazer o trabalho. Observe que não há espaço antes do exec ou ele fará com que ele procure o comando vazio. Você deseja unir o vazio ao primeiro argumento, assim $0é exec.
Caleb
1

Encontrei uma solução bastante estúpida ao procurar um executável que excede um script como um único argumento:

#!/usr/bin/awk BEGIN{system("bash --posix "ARGV[1])}
até
fonte