Как я могу получить имя команды, вызываемой для подсказок об использовании в Ruby?

83

Некоторое время назад я написал симпатичный небольшой скрипт на Ruby, который мне очень нравится. Я хотел бы улучшить его надежность, проверив правильное количество аргументов:

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

Конечно, это псевдокод. В любом случае, в C или C ++ я мог бы использовать, argv[0]чтобы получить имя, которое пользователь использовал для получения моей команды, независимо от того, называли ли они это как ./myScript.rbили myScript.rbили /usr/local/bin/myScript.rb. В Ruby я знаю, что ARGV[0]это первый истинный аргумент, и ARGVон не содержит имени команды. Есть ли способ получить это?

adam_0
источник

Ответы:

149

У Ruby есть три способа дать нам имя вызываемого скрипта:

#!/usr/bin/env ruby

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

Сохранение этого кода как «test.rb» и его вызов несколькими способами показывает, что сценарий получает имя в том виде, в каком оно было передано ему ОС. Сценарий знает только то, что ему сообщает ОС:

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

Вызов его с помощью ~ярлыка для $ HOME во втором примере показывает, что ОС заменяет его расширенным путем, совпадающим с тем, что находится в третьем примере. Во всех случаях это то, что передала ОС.

Связывание с файлом с использованием как жестких, так и программных ссылок показывает последовательное поведение. Я создал жесткую ссылку для test1.rb и мягкую ссылку для 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

Запуск ruby test.rbс любым из вариантов имени скрипта возвращает согласованные результаты.

Если вам нужно только названное имя файла, вы можете использовать basenameметод File с одной из переменных или разделить его по разделителю и взять последний элемент.

$0и __FILE__имеют некоторые незначительные отличия, но для отдельных скриптов они эквивалентны.

puts File.basename($0)

Есть некоторые преимущества использования File.basename, File.extnameи File.dirnameнабора методов. basenameпринимает необязательный параметр, который является расширением для удаления, поэтому, если вам нужно только базовое имя без расширения

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

делает это без изобретения колеса или необходимости иметь дело с переменной длиной или отсутствующими расширениями или возможностью неправильного обрезания цепочек удлинителей " .rb.txt", например:

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" 
Железный Человек
источник
Спасибо за очень полный ответ!
adam_0
2
Обратите внимание, что существует другое поведение, если вы используете предлагаемые параметры во включенном скрипте с помощью 'require' или 'require_relative. Использование $ 0 и $ PROGRAM_NAME возвращает имя вызывающего скрипта. Использование параметра ФАЙЛ с окружающими двойными чертами подчеркивания (как вы отформатируете это в комментарии?) Возвращает имя включенного скрипта.
mike663
18

этот ответ может быть немного запоздалым, но у меня была такая же проблема, и принятый ответ не казался мне вполне удовлетворительным, поэтому я исследовал немного дальше.

Что меня беспокоило, так это то, что на самом деле $0или $PROGRAM_NAMEне было правильной информации о том, что пользователь ввел . Если мой сценарий Ruby находится в папке PATH и пользователь вводит имя исполняемого файла (без каких-либо определений пути, таких как ./scriptили /bin/script), он всегда будет расширяться до общего пути.

Я думал, что это недостаток Ruby, поэтому я попробовал то же самое с Python, и, к моему огорчению, ничего не изменилось.

Друг предложил мне взломать поиск real thingвнутри /proc/self/cmdline, и результат был: [ruby, /home/danyel/bin/myscript, arg1, arg2...](разделенный нулевым символом). Здесь злодей execve(1)расширяет путь до общего пути, передавая его интерпретатору.

Пример программы на 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);
}

Вывод: `Использование: / home / danyel / bin / myscript ФАЙЛ ...

Чтобы доказать, что это действительно так execve, а не из bash, мы можем создать фиктивный интерпретатор, который ничего не делает, кроме как распечатывает переданные ему аргументы:

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

Мы компилируем его и помещаем в папку пути (или помещаем полный путь после shebang) и создаем фиктивный скрипт в ~/bin/myscript/

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

Теперь в нашем 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);
}

Компиляция и запуск ./main: интерпретатор / home / danyel / bin / myscript -v /var/log/apache2.log

Причина этого, скорее всего, заключается в том, что если сценарий находится в вашем PATH, а полный путь не был указан, интерпретатор распознал бы это как No such fileошибку, что и произойдет, если вы это сделаете: ruby myrubyscript --options arg1и вы не в папке с этим сценарием .

Дэниел
источник
3

Используйте $0или, $PROGRAM_NAMEчтобы получить имя файла, который в данный момент выполняется.

Pierrotlefou
источник
Это дает мне полный путь. Я хотел бы знать, что ввел пользователь. Например, если у меня есть /usr/local/bin/myScriptи /usr/local/binв моем $PATH, и я просто набрать myScript, я получаю /usr/local/bin/myScriptот$0
adam_0
2
Как насчет $0.split("/").last?
pierrotlefou
7
Я не прошу ТОЛЬКО имя программы, я имею в виду, что мне нужно именно то, что пользователь ввел для запуска программы. Если они напечатали ./myScript, мне нужна переменная, которая дает мне ./myScript. Если они напечатали /usr/bin/local/myScript, я хочу именно это. и т. д.
adam_0
3

Это не совсем ответ на ваш вопрос, но похоже, что вы изобретаете колесо. Посмотрите библиотеку optparse . Он позволяет вам определять параметры командной строки, аргументы и т.д., и он сделает всю тяжелую работу за вас.

Крис Хилд
источник
2
Спасибо, но мне не нужно ничего сложного. Я имею в виду, что у меня всего 2 аргумента, так что это слишком простой случай, чтобы все это подтверждать.
adam_0