$ 0 sempre incluirá o caminho para o script?

11

Desejo cumprimentar o script atual para poder imprimir informações de ajuda e versão na seção de comentários na parte superior.

Eu estava pensando em algo assim:

grep '^#h ' -- "$0" | sed -e 's/#h //'

Mas então me perguntei o que aconteceria se o script estivesse localizado em um diretório que estava no PATH e chamado sem especificar explicitamente o diretório.

Procurei uma explicação das variáveis ​​especiais e encontrei as seguintes descrições de $0:

  • nome do shell ou programa atual

  • nome do arquivo do script atual

  • nome do próprio script

  • comando como foi executado

Nada disso esclarece se o valor de $0incluiria ou não o diretório se o script fosse chamado sem ele. O último realmente implica para mim que não.

Teste no meu sistema (Bash 4.1)

Criei um arquivo executável em / usr / local / bin chamado scriptname com uma linha echo $0e o invoquei de locais diferentes.

Estes são os meus resultados:

> cd /usr/local/bin/test
> ../scriptname
../scriptname

> cd /usr/local/bin
> ./scriptname
./scriptname

> cd /usr/local
> bin/scriptname
bin/scriptname

> cd /tmp
> /usr/local/bin/scriptname
/usr/local/bin/scriptname

> scriptname
/usr/local/bin/scriptname

Nesses testes, o valor de $0é sempre exatamente como o script foi chamado, exceto se for chamado sem nenhum componente de caminho. Nesse caso, o valor de $0é o caminho absoluto . Portanto, parece que seria seguro passar para outro comando.

Mas então me deparei com um comentário no Stack Overflow que me confundiu. A resposta sugere usar $(dirname $0)para obter o diretório do script atual. O comentário (votado 7 vezes) diz "que não funcionará se o script estiver no seu caminho".

Questões

  • Esse comentário está correto?
  • O comportamento é diferente em outros sistemas?
  • Existem situações em $0que não incluiria o diretório?
toxalote
fonte
As pessoas responderam sobre situações em que $0há algo diferente do script, que responde ao título da pergunta. No entanto, também estou interessado em situações em que $0está o próprio script, mas não inclui o diretório. Em particular, estou tentando entender o comentário feito na resposta SO.
toxalot

Respostas:

17

Nos casos mais comuns, $0conterá um caminho, absoluto ou relativo ao script, portanto

script_path=$(readlink -e -- "$0")

(supondo que exista um readlinkcomando e ele suporte -e) geralmente é uma maneira suficientemente boa de obter o caminho absoluto canônico para o script.

$0 é designado a partir do argumento que especifica o script como passado ao intérprete.

Por exemplo, em:

the-shell -shell-options the/script its args

$0fica the/script.

Quando você executa:

the/script its args

Seu shell fará um:

exec("the/script", ["the/script", "its", "args"])

Se o script contiver um #! /bin/sh -she-bang, por exemplo, o sistema transformará isso em:

exec("/bin/sh", ["/bin/sh" or "the/script", "-", "the/script", "its", "args"])

(se não contiver um she-bang ou, de maneira mais geral, se o sistema retornar um erro ENOEXEC, será o seu shell que fará a mesma coisa)

Há uma exceção para scripts setuid / setgid em alguns sistemas, em que o sistema abrirá o script em alguns fd xe será executado:

exec("/bin/sh", ["/bin/sh" or "the/script", "-", "/dev/fd/x", "its", "args"])

para evitar condições de corrida (caso em que $0conterá /dev/fd/x).

Agora, você pode argumentar que esse /dev/fd/x é o caminho para esse script. Observe, no entanto, que, se você ler $0, interromperá o script ao consumir a entrada.

Agora, há uma diferença se o nome do comando de script chamado não contiver uma barra. No:

the-script its args

Seu shell irá procurar the-scriptno $PATH. $PATHpode conter caminhos absolutos ou relativos (incluindo a cadeia vazia) para alguns diretórios. Por exemplo, se $PATHcontém /bin:/usr/bin:e the-scripté encontrado no diretório atual, o shell fará um:

exec("the-script", ["the-script", "its", "args"])

que se tornará:

exec("/bin/sh", ["/bin/sh" or "the-script", "-", "the-script", "its", "args"]

Ou se for encontrado em /usr/bin:

exec("/usr/bin/the-script", ["the-script", "its", "args"])
exec("/bin/sh", ["/bin/sh" or "the-script" or "/usr/bin/the-script",
     "-", "/usr/bin/the-script", "its", "args")

Em todos os casos acima, exceto no caso do canto setuid, $0haverá um caminho (absoluto ou relativo) para o script.

Agora, um script também pode ser chamado como:

the-interpreter the-script its args

Quando, the-scriptcomo acima, não contém caracteres de barra, o comportamento varia ligeiramente de shell para shell.

As kshimplementações antigas da AT&T estavam realmente pesquisando o script incondicionalmente $PATH(o que na verdade era um bug e uma brecha na segurança dos scripts setuid); portanto, $0na verdade, não havia um caminho para o script, a menos que a $PATHpesquisa realmente acontecesse the-scriptno diretório atual.

AT&T mais recente kshtentaria interpretar the-scriptno diretório atual, se for legível. Se não seria lookup para um legível e executável the-script em $PATH.

Para bash, verifica se the-scriptestá no diretório atual (e não é um link simbólico quebrado) e, se não, procura uma legível (não necessariamente executável) the-scriptem $PATH.

zshem shemulação faria como bashexceto que se the-scripté um link quebrado no diretório atual, não seria procurar um the-scriptno $PATHe, ao invés, relatar um erro.

Todas as outras conchas semelhantes a Bourne não olham the-scriptpara dentro $PATH.

De todas as formas, se você achar que $0não contém um /e não é legível, provavelmente já foi pesquisado $PATH. Então, como os arquivos $PATHprovavelmente são executáveis, é provavelmente uma aproximação segura a ser usada command -v -- "$0"para encontrar o caminho (embora isso não funcione se $0também for o nome de um shell incorporado ou de uma palavra-chave (na maioria dos shells)).

Portanto, se você realmente deseja cobrir esse caso, escreva-o:

progname=$0
[ -r "$progname" ] || progname=$(
    IFS=:; set -f
    for i in ${PATH-$(getconf PATH)}""; do
      case $i in
        "") p=$progname;;
        */) p=$i$progname;;
        *) p=$i/$progname
      esac
      [ -r "$p" ] && exec printf '%s\n' "$p"
    done
    exit 1
  ) && progname=$(readlink -e -- "$progname") ||
  progname=unknown

(o ""anexo a $PATHé preservar um elemento vazio à direita com conchas cujos $IFSatos sejam delimitadores em vez de separadores ).

Agora, existem maneiras mais esotéricas de invocar um script. Alguém poderia fazer:

the-shell < the-script

Ou:

cat the-script | the-shell

Nesse caso, $0será o primeiro argumento ( argv[0]) que o intérprete recebeu (acima the-shell, mas isso pode ser qualquer coisa, embora geralmente seja o nome da base ou um caminho para esse intérprete).

Detectar que você está nessa situação com base no valor de $0não é confiável. Você pode olhar para a saída de ps -o args= -p "$$"para obter uma pista. No caso de pipe, não existe uma maneira real de voltar ao caminho do script.

Pode-se também fazer:

the-shell -c '. the-script' blah blih

Então, exceto em zsh(e alguma implementação antiga do shell Bourne), $0seria blah. Novamente, é difícil chegar ao caminho do script nessas conchas.

Ou:

the-shell -c "$(cat the-script)" blah blih

etc.

Para garantir que você tenha o direito $progname, pesquise uma string específica, como:

progname=$0
[ -r "$progname" ] || progname=$(
    IFS=:; set -f
    for i in ${PATH-$(getconf PATH)}:; do
      case $i in
        "") p=$progname;;
        */) p=$i$progname;;
        *) p=$i/$progname
      esac
      [ -r "$p" ] && exec printf '%s\n' "$p"
    done
    exit 1
  ) && progname=$(readlink -e -- "$progname") ||
  progname=unknown

[ -f "$progname" ] && grep -q 7YQLVVD3UIUDTA32LSE8U9UOHH < "$progname" ||
  progname=unknown

Mas, novamente, acho que não vale a pena o esforço.

Stéphane Chazelas
fonte
Stéphane, não entendo seu uso "-"nos exemplos acima. Na minha experiência, exec("the-script", ["the-script", "its", "args"])torna-se exec("/the/interpreter", ["/the/interpreter", "the-script", "its", "args"]), é claro, a possibilidade de uma opção de intérprete.
Jrw32982 suporta Monica
@ jrw32982, #! /bin/sh -é o adágio de boas práticas "sempre use cmd -- somethingse você não pode garantir que somethingnão comece com -" aplicado aqui /bin/sh(onde -o marcador de fim de opção é mais portátil que --) por somethingser o caminho / nome do roteiro. Se você não usar isso para scripts setuid (em sistemas que os suportam, mas não com o método / dev / fd / x mencionado na resposta), é possível obter um shell raiz criando um link simbólico para o seu script chamado -iou -spara instância.
Stéphane Chazelas
Obrigado, Stéphane. Eu tinha perdido o hífen único à direita na sua linha de exemplo. Eu tive que procurar onde está documentado que um único hífen é equivalente a um duplo hífen pubs.opengroup.org/onlinepubs/9699919799/utilities/sh.html .
Jrw32982 suporta Monica
É muito fácil esquecer o hífen simples / duplo à direita na linha shebang para um script setuid; de alguma forma, o sistema deve cuidar disso para você. /bin/shDesabilite totalmente os scripts setuid ou, de alguma forma , desabilite o próprio processamento de opções, se detectar que está executando o setuid. Não vejo como o uso de / dev / fd / x por si só corrige isso. Você ainda precisa do hífen único / duplo, eu acho.
Jrw32982 suporta Monica
@ jrw32982, /dev/fd/xcomeça com /, não -. O objetivo principal é remover a condição de corrida entre os dois execve()segundos (entre o execve("the-script")que eleva os privilégios e o subsequente execve("interpreter", "thescript")onde interpreterabre o script mais tarde (que pode muito bem ter sido substituído por um link simbólico para outra coisa nesse meio tempo)). scripts de suid fazer corretamente uma execve("interpreter", "/dev/fd/n")vez que n foi aberto como parte da primeira execve ().
Stéphane Chazelas
6

Aqui estão duas situações em que o diretório não seria incluído:

> bash scriptname
scriptname

> bash <scriptname
bash

Nos dois casos, o diretório atual teria que ser o diretório em que o scriptname estava localizado.

No primeiro caso, o valor de $0ainda pode ser passado para, grepuma vez que assume que o argumento FILE é relativo ao diretório atual.

No segundo caso, se as informações de ajuda e versão forem impressas apenas em resposta a uma opção específica da linha de comando, isso não deve ser um problema. Não sei por que alguém chamaria um script dessa maneira para imprimir as informações de ajuda ou versão.

Ressalvas

  • Se o script alterar o diretório atual, você não desejaria usar caminhos relativos.

  • Se o script for originado, o valor de $0geralmente será o script de chamada em vez do script de origem.

toxalote
fonte
3

Um argumento arbitrário zero pode ser especificado ao usar a -copção para a maioria dos shells (todos?). Por exemplo:

sh -c 'echo $0' argv0

De man bash(escolhido puramente porque tem uma descrição melhor que minha man sh- o uso é o mesmo, independentemente):

-c

Se a opção -c estiver presente, os comandos serão lidos a partir do primeiro argumento sem opção command_string. Se houver argumentos após a command_string, eles serão atribuídos aos parâmetros posicionais, começando com $ 0.

Graeme
fonte
Eu acho que isso funciona porque -c 'command' são operandos e argv0é seu primeiro argumento de linha de comando não operacional.
mikeserv
@ Mike correto, eu atualizei com um trecho de homem.
Graeme
3

OBSERVAÇÃO: Outros já explicaram a mecânica, por $0isso vou pular tudo isso.

Eu passo geralmente lado toda esta questão e simplesmente usar o comando readlink -f $0. Isso sempre lhe dará de volta o caminho completo do que você der como argumento.

Exemplos

Digamos que estou aqui para começar:

$ pwd
/home/saml/tst/119929/adir

Crie um diretório + arquivo:

$ mkdir adir
$ touch afile
$ cd adir/

Agora comece a exibir readlink:

$ readlink -f ../adir
/home/saml/tst/119929/adir

$ readlink -f ../
/home/saml/tst/119929

$ readlink -f ../afile 
/home/saml/tst/119929/afile

$ readlink -f .
/home/saml/tst/119929/adir

Truques adicionais

Agora, com um resultado consistente sendo retornado quando interrogamos $0via readlink, podemos usar um simplesmente dirname $(readlink -f $0)para obter o caminho absoluto para o script - ou - basename $(readlink -f $0)para obter o nome real do script.

slm
fonte
0

Minha manpágina diz:

$0: expande para o namede shellou shell script.

Parece que isso se traduz argv[0]no shell atual - ou no primeiro argumento de linha de comando não operacional e no qual o shell atualmente em interpretação é alimentado quando invocado. I declarou anteriormente que sh ./somescriptcaminho seria a sua variável de mas esta foi incorreta porque o é um novo processo de sua própria e invocado com um novo .$0 $ENV shshell$ENV

Dessa maneira, sh ./somescript.shdifere do . ./somescript.shque é executado no ambiente atual e $0já está definido.

Você pode verificar isso comparando $0com /proc/$$/status.

echo 'script="/proc/$$/status"
    echo $0
    cat "$script"' \
    > ./script.sh
sh ./script.sh ; . ./script.sh

Obrigado pela correção, @toxalot. Eu aprendi alguma coisa

mikeserv
fonte
No meu sistema, se for de origem ( . ./myscript.shou source ./myscript.sh), $0é o shell. Mas se for passado como argumento para o shell ( sh ./myscript.sh), então $0é o caminho para o script. Claro, shno meu sistema é o Bash. Então, eu não sei se isso faz diferença ou não.
toxalot
Tem um hashbang? Penso que a diferença importa - e posso acrescentar - sem ela não deveria exece, em vez disso, procurá-la, mas com ela, deveria exec.
mikeserv
Com ou sem hashbang, obtenho os mesmos resultados. Com ou sem ser executável, obtenho os mesmos resultados.
toxalot
Eu também! Eu acho que é porque é um shell embutido. Estou verificando ...
mikeserv
As linhas de estrondo são interpretadas pelo kernel. Se você executar ./somescriptcom o bangline #!/bin/sh, seria equivalente a executar /bin/sh ./somescript. Caso contrário, eles não fazem diferença para o casco.
Graeme
0

Desejo cumprimentar o script atual para poder imprimir informações de ajuda e versão na seção de comentários na parte superior.

Embora $0contenha o nome do script, ele pode conter um caminho prefixado com base na maneira como o script é chamado, sempre usei ${0##*/}para imprimir o nome do script na saída da ajuda que remove qualquer caminho principal $0.

Retirado do guia Advanced Bash Scripting - Seção 10.2, substituição de parâmetros

${var#Pattern}

Remova da $varparte mais curta da $Patternque corresponde ao front end de $var.

${var##Pattern}

Remova da $varparte mais longa $Patternque corresponde ao front end de $var.

Portanto, a parte mais longa $0dessas correspondências */será o prefixo do caminho inteiro, retornando apenas o nome do script.

sambler
fonte
Sim, eu também faço isso pelo nome do script usado na mensagem de ajuda. Mas estou falando de grepping o script, então preciso ter pelo menos um caminho relativo. Quero colocar a mensagem de ajuda na parte superior dos comentários e não preciso repetir a mensagem de ajuda mais tarde ao imprimi-la.
toxalot
0

Para algo semelhante, eu uso:

rPath="$(dirname $(realpath $0))"
echo $rPath 

rPath=$(dirname $(readlink -e -- "$0"))
echo $rPath 

rPath sempre tem o mesmo valor.

Anónimo
fonte