Оборачивание `time` (и похожих ключевых слов) в вызове из другого скрипта
У меня есть скрипт Bash (назовем его clock
) который должен работать как оболочка, похожая на time
ключевое слово в Bash, например clock ls
должен что-то сделать, а затем запустить ls
, Вот пример этого скрипта:
#!/bin/bash
echo "do something"
$@
Обратите внимание, что он не использует exec
, чтобы разрешить оборачивать встроенные модули.
Тем не менее, когда аргумент для переноса является time
ключевое слово, он не работает должным образом: вывод показывает, что он запускает /usr/bin/time
команда, а не ключевое слово оболочки.
Как я могу заставить мой скрипт-обертку обрабатывать ключевые слова (такие как time
) точно так, как если бы они были набраны прямо в оболочке?
Примечание: в моем связанном вопросе я узнал, как заставить это работать, когда clock
была функция Bash в том же сценарии, но в моем реальном случае использования, clock
на самом деле сам скрипт Bash, поэтому предыдущее решение не работает. Кроме того, решения, упомянутые в связанном вопросе (с использованием $@
непосредственно или работает exec bash -c ""$@""
) не работают в этом случае.
Одно частичное решение, которое я нашел, состояло в том, чтобы использовать eval $@
, но это очень ненадежно. Это работает в этом простом случае с time
, но не удается во многих ситуациях, например, в clock ls '~$Document1'
,
1 ответ
Анализ
Проблема в
time
который вы хотите использовать, предназначен для обработки целых конвейеров в Bash. Было бы невозможно, если бы
time
была распознана и "выполнена" как любая обычная команда (например, внешняя
time
исполняемый файл) или даже встроенный. Это должно быть ключевое слово. Оболочка должна распознать это очень рано, примерно в то время, когда она распознает конвейеры.
Вы не можете вводить рабочие
|
,
&&
, или же
;
в код оболочки, заставляя их выталкиваться из переменной (или параметра) во время расширения переменной. В момент раскрытия переменной оболочка уже знает, какова логика линии. Точно так же уже слишком поздно для
time
всплывать и интерпретироваться как ключевое слово.
Это означает единственный способ пройти
time
через переменную (или параметр) и интерпретировать ее как ключевое слово для ее оценки (вместе со всей будущей командой) с самого начала после раскрытия переменной. Это что
eval
или же
bash -c
сможет сделать. Вы не можете этого избежать.
Базовое решение
Самый простой способ - потребовать
clock
(ваш сценарий) принимать только один аргумент. Вы бы использовали это так:
clock ls
clock 'ls -l'
clock 'time ls -l'
clock 'time ls -l | wc -l'
Внутри скрипта важная команда должна быть:
eval "$1"
# or
exec bash -c "$1" "$0"
(Если вам интересно об этом
"$0"
тогда прочтите это. Дело в том, чтобы сделать
$0
в новой оболочке так же, как в текущей оболочке. Его значение, скорее всего, будет
clock
.)
Я думаю, вы хотели бы иметь возможность удобно запускать
clock time ls -l
вместо
clock 'time ls -l'
. Если это так, ключевыми командами в сценарии должны быть:
eval "$@"
# or
IFS=$' \t\n'; eval "$*"
# or
IFS=$' \t\n'; exec bash -c "$*" "$0"
На вашем месте я бы предпочел
eval
потому что это не начинается
bash
с самого начала (производительность) и сохраняет доступными неэкспортированные переменные (они могут быть актуальны, если вместо / помимо
echo "do something"
ваш скрипт устанавливает некоторые переменные).
я бы предпочел
eval "$@"
над
eval "$*"
потому что первое не зависит от
IFS
. При получении нескольких аргументов (что может быть в случае
"$@"
) eval
объединяет их вместе, разделяя пробелами, прежде чем оценивать результат. Это эквивалентно передаче
"$*"
(который всегда является единственным аргументом), если только
IFS
переменная начинается с пробела. Где бы я ни использовал
"$*"
Я убедился
IFS
начинается с пробела на случай, если ваш скрипт по какой-либо причине изменил переменную ранее. Пробел + табуляция + новая строка - значение по умолчанию.
Мой выбор:
#!/bin/bash
echo "do something"
eval "$@"
Цитирование
Что бы вы ни выбрали, двойные кавычки
$@
,
$*
или же
$1
в сценарии. Обратите внимание на три стадии расширения:
Когда вы пройдете
clock whatever
в оболочку оболочка анализирует команду, как всегда: распознавание токенов, раскрытие скобок, раскрытие тильды и т. д. Вы можете избежать (в контексте этого списка: возможно, отложить) различных расширений путем цитирования и / или экранирования.Когда ваш сценарий дойдет до
"$@"
,"$*"
или же"$1"
, происходит расширение параметра. Если параметр не заключен в двойные кавычки, результат будет разделен на слова и имя файла расширено. Скорее всего, они вам не нужны на данном этапе, если вы используетеeval
; и они вам определенно не нужны, если вы используетеbash -c
.Наконец, когда
eval
или жеbash -c
выполняет свою работу, он анализирует строку, переданную в качестве аргумента (ов), с самого начала. Опять же, вы можете избежать различных расширений путем правильного цитирования или экранирования. Обратите внимание на кавычки и / или обратную косую черту, которые должны подавлять некоторые расширения, или такие символы, как*
или фрагменты вроде{a,b,c}
или же$foo
которые должны быть расширены на этом этапе - они должны быть изначально процитированы или исключены, чтобы они пережили первую стадию, а не были "использованы" слишком рано.
Вам следует тщательно процитировать и / или уйти на первом этапе, зная и планируя, как команда будет выглядеть на последнем этапе.
Если вы выберете решение с
"$@"
(или же
"$*"
), а не с
"$1"
, следующие две команды будут эквивалентны:
clock 'ls -l'
clock ls -l
(если пользовательская часть вашего скрипта не различает их). Но не эти двое:
clock 'ls -l | wc -l'
clock ls -l | wc -l
Обратите внимание, что это очень похоже на то, как команды вроде
watch 'ls -l'
или же
ssh user@host 'ls -l'
вести себя. Вы можете опустить цитаты и получить тот же результат. Все еще
watch 'ls -l | wc -l'
и
watch ls -l | wc -l
не эквивалентны; ни то, ни другое
ssh user@host 'ls -l > foo.txt'
и
ssh user@host ls -l > foo.txt
.
Ваши попытки
с помощью
$@
прямо
Единственный
$@
не дает дополнительной оценки после раскрытия переменной. когда
time
всплывает слишком поздно, чтобы интерпретировать его как ключевое слово.
Если
time
тогда не было проблемой
$@
или же
exec
$@might be a good idea, but think twice if you want
$@`без кавычек в таком случае.
Бег
exec bash -c ""$@""
Это неправильно, и я уведомил автора об ответе, от которого вы его получили (ответ был улучшен). Эти соседние двойные кавычки нейтрализуют друг друга. В результате
$@
не заключен в кавычки и подвержен разделению слов и генерации имени файла, как упоминалось выше. Но даже
"$@"
было бы неправильно здесь, потому что
bash -c
принимает в качестве кода ровно один аргумент. Следующие аргументы (если есть) определяют позиционные параметры (от 0, для этого есть причина). Если ваш скрипт использует этот некорректный код, то, например,
clock ls -l
будет работать
ls
не
ls -l
; Четный
clock
ls -lwill run
ls` без аргумента из-за разделения слов.
Одно частичное решение, которое я нашел, заключалось в использовании
eval $@
, но это очень ненадежно. Это работает в этом простом случае сtime
, но не работает во многих ситуациях, например вclock ls '~$Document1'
.
Одинарными кавычками вы защитили
$Document
от расширения (как переменной) на первом этапе, но не на последнем этапе. С немного другой струной
~
тоже может быть проблематично. Без кавычек
$@
представил возможность возникновения промежуточных проблем, хотя и не в этом конкретном случае. Вам нужно защитить
$
дважды:
clock ls '~\$Document1'
Мое основное решение требует защиты
$
дважды и в этом случае. Сделать
time
Работайте так, как хотите, вам нужен этот дополнительный этап расширения, так что вам просто нужно с этим разобраться.
Сравнить
watch ls '~$Document1'
и
watch ls '~\$Document1'
. Такая же ситуация.
Есть уловка. Смотри ниже.
Трюк
Возможность выбрать, на каком этапе будет расширяться какая-либо подстрока, полезна в случае
watch
или же
ssh
.
Например, вы можете отслеживать размеры уже существующих
*.tmp
файлы, не обращая внимания на новые файлы. В этом случае вам понадобится
*
быть расширенным один раз:
watch ls -l *.tmp
. Или вы можете захотеть включить новые файлы, соответствующие шаблону. В этом случае вам понадобится
*
многократно расширяться:
watch 'ls -l *.tmp'
. С участием
ssh
вы можете захотеть, чтобы переменная была расширена локально или на удаленном сервере. Для обоих инструментов иногда бывает полезно отложить расширение.
Однако ваш сценарий должен работать аналогично
time
ключевое слово. Ключевое слово не вводит дополнительный этап расширения, и ваш пример с
~$Document1
показывает, что вы не хотите его вводить. Тем не менее, согласно моему анализу, он вам нужен, но только для интерпретации таких слов, как
time
(передаются как аргументы) как ключевые слова.
Есть способ подавить эти нежелательные расширения на последнем этапе. Вы можете использовать
Q
оператор ранее:
${parameter@operator}
Расширение - это либо преобразование значения
parameter
или информация оparameter
само по себе, в зависимости от стоимостиoperator
. Каждыйoperator
это одна буква:
Q
Расширение - это строка, которая является значениемparameter
цитируется в формате, который можно повторно использовать в качестве входных данных.
( источник)
Это добавляет один уровень одиночных кавычек / экранирования к расширенной строке. Теперь идея состоит в том, чтобы использовать его на нашем этапе 2, поэтому на этапе 3 эти дополнительные кавычки предотвратят различные расширения (и будут удалены).
Просто меняя
eval "$@"
к
eval "${@@Q}"
приведет к:
- способность бегать
clock ls '~$Document1'
вот так (красиво!); - неспособность бежать
clock 'time ls -l | wc -l'
(Ну что ж); - неспособность распознать
time
вclock time ls -l
как ключевое слово (упс!); на этапе 3time
будет в одинарных кавычках и'time'
не является ключевым словом.
Решение - не использовать
Q
для первого аргумента командной строки:
#!/bin/bash
echo "do something"
cmnd="$1"
shift
eval "$cmnd" "${@@Q}"
Первый аргумент командной строки для
clock
не защищен от раскрытия на этапе 3, но другие аргументы защищены. В результате:
- Вы можете запустить
clock ls '~$Document1'
вот так (красиво!); - Вы можете запустить
clock 'time ls -l | wc -l'
(хорошо), хотя вам нужно помнить цитаты для этапа 1 и этапа 3 ( этот вопрос может помочь в некоторых случаях); time
вclock time …
или жеclock 'time …'
этоtime
вы хотите (ура!).
Если вы беспокоитесь о первом аргументе командной строки для
clock
не защищены от расширения на этапе 3? На самом деле, нет. Это будет либо полная длинная команда (например, конвейер), цитируемая как единое целое, тогда вы должны рассматривать ее как длинную команду, переданную в
watch
или же
ssh
как один аргумент; или это будет ключевое слово / builtin / команда, которая не может вызвать нежелательное расширение на этапе 3, потому что имена команд намеренно просты и безопасны (нет
$
,
~
или такой). Было бы иначе, если бы вы хотели запустить
clock '*' …
или же
clock './~$Document1' …
. Я считаю, что у вас нет причин для этого.
Насколько я помню, Bash имеет builtin
команда, которая заставляет его работать, как вы уже догадались, встроенная команда, даже если в PATH
с тем же именем.
Я проверил, сделав этот скрипт в /usr/bin
:
#!/bin/bash
echo "This is /usr/bin/cd, and it does nothing"
И вот результаты:
jarmund@jarmint/etc$ /usr/bin/cd ~
This is /usr/bin/cd, and it does nothing
jarmund@jarmint/etc$ builtin cd ~
jarmund@jarmint~$
Вывод: префикс вашей команды builtin
должен устранить любую двусмысленность, которую может испытывать оболочка.