¿Cómo puedo obtener el nombre del comando llamado para indicaciones de uso en Ruby?

83

Escribí un pequeño y agradable guión de Ruby hace un tiempo que me gusta bastante. Me gustaría mejorar su robustez comprobando el número adecuado de argumentos:

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

Por supuesto que eso es pseudocódigo. De todos modos, en C o C ++ podría usar argv[0]para obtener el nombre que el usuario usó para llegar a mi comando, ya sea que lo llamaran como ./myScript.rbo myScript.rbo /usr/local/bin/myScript.rb. En Ruby, sé que ARGV[0]es el primer argumento verdadero y ARGVno contiene el nombre del comando. ¿Hay alguna forma de que pueda conseguir esto?

Adam_0
fuente

Respuestas:

149

Ruby tiene tres formas de darnos el nombre del script llamado:

#!/usr/bin/env ruby

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

Guardar ese código como "test.rb" y llamarlo de dos maneras muestra que el script recibe el nombre tal como se lo pasó por el sistema operativo. Un script solo sabe lo que le dice el sistema operativo:

$ ./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

Llamarlo usando el ~acceso directo para $ HOME en el segundo ejemplo muestra que el sistema operativo lo reemplaza con la ruta expandida, que coincide con lo que está en el tercer ejemplo. En todos los casos es lo que pasó el sistema operativo.

La vinculación al archivo mediante enlaces físicos y blandos muestra un comportamiento coherente. Creé un enlace duro para test1.rb y un enlace suave 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

El lanzamiento ruby test.rbcon cualquiera de las variaciones del nombre del script arroja resultados consistentes.

Si solo desea el nombre de archivo llamado, puede usar el basenamemétodo de File con una de las variables o dividir en el delimitador y tomar el último elemento.

$0y __FILE__tienen algunas diferencias menores, pero para scripts únicos son equivalentes.

puts File.basename($0)

Hay algunas ventajas a usar el File.basename, File.extnamey File.dirnameconjunto de métodos. basenametoma un parámetro opcional, que es la extensión para eliminar, por lo que si solo necesita el nombre de base sin la extensión

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

lo hace sin reinventar la rueda o tener que lidiar con extensiones de longitud variable o faltantes o la posibilidad de truncar incorrectamente cadenas de extensión " .rb.txt" por ejemplo:

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" 
el hombre de hojalata
fuente
¡Gracias por la respuesta tan completa!
adam_0
2
Tenga en cuenta que hay un comportamiento diferente si usa las opciones sugeridas en un script incluido usando 'require' o 'require_relative. El uso de $ 0 y $ PROGRAM_NAME devuelve el nombre del script de llamada. El uso de la opción ARCHIVO con barras subyacentes dobles circundantes (¿Cómo formatear eso en un comentario?) Devuelve el nombre del script incluido.
mike663
18

esta respuesta puede llegar un poco tarde, pero he tenido el mismo problema y la respuesta aceptada no me pareció muy satisfactoria, así que investigué un poco más.

Lo que me molestó fue el hecho de que $0, o $PROGRAM_NAMEen realidad no tiene la información correcta sobre lo que el usuario hubiera tecleado . Si mi script Ruby estaba en una carpeta PATH y el usuario ingresó el nombre del ejecutable (sin ninguna definición de ruta como ./scripto /bin/script), siempre se expandiría a la ruta total.

Pensé que esto era un déficit de Ruby, así que intenté lo mismo con Python y, para mi disgusto, no fue diferente.

Un amigo me sugirió un truco para buscar la real thingen /proc/self/cmdline, y el resultado fue: [ruby, /home/danyel/bin/myscript, arg1, arg2...](separados por la nula-char). El villano aquí es el execve(1)que expande el camino al camino total cuando se lo pasa a un intérprete.

Programa de ejemplo 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);
}

Salida: `Uso: / home / danyel / bin / myscript ARCHIVO ...

Para demostrar que esto es realmente una execvecosa y no de bash, podemos crear un intérprete ficticio que no hace más que imprimir los argumentos que se le pasan:

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

Lo compilamos y lo colocamos en una carpeta de ruta (o colocamos la ruta completa después del shebang) y creamos un script ficticio en ~/bin/myscript/

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

Ahora, en nuestro 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 y ejecutando ./main: interpreter / home / danyel / bin / myscript -v /var/log/apache2.log

Lo más probable es que la razón detrás de esto sea que si el script está en su PATH y no se proporcionó la ruta completa , el intérprete reconocería esto como un No such fileerror, lo que hace si lo hace: ruby myrubyscript --options arg1y no está en la carpeta con ese script .

Danyel
fuente
3

Utilice $0o $PROGRAM_NAMEpara obtener el nombre del archivo que se está ejecutando actualmente.

pierrotlefou
fuente
Esto me da el camino completo. Me gustaría saber qué ingresó el usuario. Por ejemplo, si tengo /usr/local/bin/myScripty /usr/local/binestá en mi $PATH, y solo myScript/usr/local/bin/myScript$0
escribo
2
¿Qué tal $0.split("/").last?
pierrotlefou
7
No estoy pidiendo SOLO el nombre del programa, lo que quiero decir es que quiero exactamente lo que el usuario escribió para ejecutar el programa. Si escribieron ./myScript, quiero una variable que me dé ./myScript. Si escribieron /usr/bin/local/myScript, quiero exactamente eso. etc.
adam_0
3

Esta no es una respuesta a tu pregunta, pero parece que estás reinventando una rueda. Mire la biblioteca optparse . Le permite definir interruptores de línea de comando, argumentos, etc., y hará todo el trabajo pesado por usted.

Chris Heald
fuente
2
Gracias, pero no necesito nada tan complicado. Quiero decir, solo tengo 2 argumentos, por lo que es un caso demasiado simple para justificar todo eso.
adam_0