Como posso obter o nome do comando chamado para prompts de uso em Ruby?

83

Escrevi um pequeno script Ruby há um tempo que gosto bastante. Eu gostaria de melhorar sua robustez verificando o número adequado de argumentos:

if ARGV.length != 2 then
  puts "Usage: <command> arg1 arg2"
end

Claro que isso é pseudocódigo. De qualquer forma, em C ou C ++ eu poderia usar argv[0]para obter o nome que o usuário usou para obter o meu comando, se eles o chamaram como ./myScript.rbou myScript.rbou /usr/local/bin/myScript.rb. Em Ruby, sei que esse ARGV[0]é o primeiro argumento verdadeiro e ARGVnão contém o nome do comando. Existe alguma maneira de eu conseguir isso?

adam_0
fonte

Respostas:

149

Ruby tem três maneiras de nos fornecer o nome do script chamado:

#!/usr/bin/env ruby

puts "$0            : #{$0}"
puts "__FILE__      : #{__FILE__}"
puts "$PROGRAM_NAME : #{$PROGRAM_NAME}"

Salvar esse código como "test.rb" e chamá-lo de algumas maneiras mostra que o script recebe o nome conforme foi transmitido a ele pelo sistema operacional. Um script sabe apenas o que o sistema operacional diz:

$ ./test.rb 
$0            : ./test.rb
__FILE__      : ./test.rb
$PROGRAM_NAME : ./test.rb

$ ~/Desktop/test.rb 
$0            : /Users/ttm/Desktop/test.rb
__FILE__      : /Users/ttm/Desktop/test.rb
$PROGRAM_NAME : /Users/ttm/Desktop/test.rb

$ /Users/ttm/Desktop/test.rb 
$0            : /Users/ttm/Desktop/test.rb
__FILE__      : /Users/ttm/Desktop/test.rb
$PROGRAM_NAME : /Users/ttm/Desktop/test.rb

Chamá-lo usando o ~atalho para $ HOME no segundo exemplo mostra o SO substituindo-o pelo caminho expandido, correspondendo ao que está no terceiro exemplo. Em todos os casos, é o que o sistema operacional passou.

Vincular ao arquivo usando links físicos e virtuais mostra um comportamento consistente. Criei um link físico para test1.rb e um link simbólico para test2.rb:

$ ./test1.rb 
$0            : ./test1.rb
__FILE__      : ./test1.rb
$PROGRAM_NAME : ./test1.rb

$ ./test2.rb 
$0            : ./test2.rb
__FILE__      : ./test2.rb
$PROGRAM_NAME : ./test2.rb

Iniciar ruby test.rbcom qualquer uma das variações no nome do script retorna resultados consistentes.

Se você quiser apenas o nome do arquivo chamado, você pode usar o basenamemétodo de File com uma das variáveis ​​ou dividir no delimitador e pegar o último elemento.

$0e __FILE__têm algumas pequenas diferenças, mas para scripts únicos são equivalentes.

puts File.basename($0)

Existem algumas vantagens de se utilizar o File.basename, File.extnamee File.dirnameconjunto de métodos. basenamerecebe um parâmetro opcional, que é a extensão para strip, então se você precisar apenas do nome de base sem a extensão

File.basename($0, File.extname($0)) 

faz isso sem reinventar a roda ou ter que lidar com extensões de comprimento variável ou ausentes ou a possibilidade de truncar incorretamente cadeias de extensão " .rb.txt" por exemplo:

ruby-1.9.2-p136 :004 > filename = '/path/to/file/name.ext'
 => "/path/to/file/name.ext" 
ruby-1.9.2-p136 :005 > File.basename(filename, File.extname(filename))
 => "name" 
ruby-1.9.2-p136 :006 > filename = '/path/to/file/name.ext' << '.txt'
 => "/path/to/file/name.ext.txt" 
ruby-1.9.2-p136 :007 > File.basename(filename, File.extname(filename))
 => "name.ext" 
o homem de lata
fonte
Obrigado pela resposta muito completa!
adam_0
2
Observe que há um comportamento diferente se você usar as opções sugeridas em um script incluído usando 'require' ou 'require_relative. Usar $ 0 e $ PROGRAM_NAME retorna o nome do script de chamada. Usar a opção FILE com as barras inferiores duplas (Como você formata isso em um comentário?) Retorna o nome do script incluído.
mike663 de
18

esta resposta pode vir um pouco tarde, mas eu tive o mesmo problema e a resposta aceita não me pareceu muito satisfatória, então investiguei um pouco mais.

O que me incomodou foi o fato de que $0ou $PROGRAM_NAMErealmente não armazenar as informações corretas sobre o que o usuário tinha digitado . Se meu script Ruby estivesse em uma pasta PATH e o usuário inserisse o nome do executável (sem nenhuma definição de caminho como ./scriptou /bin/script), ele sempre se expandiria para o caminho total.

Achei que era uma deficiência de Ruby, então tentei o mesmo com Python e, para minha tristeza, não foi diferente.

Um amigo me sugeriu um hack para procurar o real thingin /proc/self/cmdline, e o resultado foi: [ruby, /home/danyel/bin/myscript, arg1, arg2...](separado por null-char). O vilão aqui é execve(1)quem expande o caminho até o caminho total quando passa para um intérprete.

Exemplo de programa C:

#include <stdlib.h>
#include <unistd.h>

extern char** environ;
int main() {
  char ** arr = malloc(10 * sizeof(char*));
  arr[0] = "myscript";
  arr[1] = "-h";
  arr[2] = NULL;
  execve("/home/danyel/bin/myscript", arr, environ);
}

Saída: ʻUsage: / home / danyel / bin / myscript FILE ...

Para provar que isso é realmente uma execvecoisa e não do bash, podemos criar um interpretador fictício que não faz nada além de imprimir os argumentos passados ​​para ele:

// interpreter.c
int main(int argc, const char ** argv) {
  while(*argv)
    printf("%s\n", *(argv++));
}

Nós compilamos e colocamos em uma pasta de caminho (ou colocamos o caminho completo após o shebang) e criamos um script fictício em ~/bin/myscript/

#!/usr/bin/env interpreter
Hi there!

Agora, em nosso main.c:

#include <stdlib.h>

extern char** environ;
int main() {
  char ** arr = malloc(10 * sizeof(char*));
  arr[0] = "This will be totally ignored by execve.";
  arr[1] = "-v";
  arr[2] = "/var/log/apache2.log";
  arr[3] = NULL;
  execve("/home/danyel/bin/myscript", arr, environ);
}

Compilando e executando ./main: interpreter / home / danyel / bin / myscript -v /var/log/apache2.log

A razão por trás disso provavelmente é que se o script estiver em seu PATH e o caminho completo não for fornecido, o intérprete reconhecerá isso como um No such fileerro, o que acontecerá se você fizer isso: ruby myrubyscript --options arg1e você não estiver na pasta com esse script .

Danyel
fonte
3

Use $0ou $PROGRAM_NAMEpara obter o nome do arquivo que está sendo executado.

Pierrotlefou
fonte
Isso me dá o caminho completo. Gostaria de saber o que o usuário inseriu. Por exemplo, se eu tenho /usr/local/bin/myScripte /usr/local/binestá no meu $PATHe apenas digito myScript, recebo /usr/local/bin/myScriptde$0
adam_0
2
Que tal $0.split("/").last?
pierrotlefou
7
Não estou pedindo APENAS o nome do programa, o que quero dizer é que quero exatamente o que o usuário digitou para executar o programa. Se eles digitaram ./myScript, quero uma variável que me forneça ./myScript. Se eles digitaram /usr/bin/local/myScript, eu quero exatamente isso. etc.
adam_0
3

Esta não é exatamente uma resposta à sua pergunta, mas parece que você está reinventando uma roda. Veja a biblioteca optparse . Ele permite que você defina opções de linha de comando, argumentos, etc, e fará todo o trabalho pesado para você.

Chris Heald
fonte
2
Obrigado, mas não preciso de nada tão complicado. Quer dizer, eu só tenho 2 argumentos, então é um caso muito simples para garantir tudo isso.
adam_0