Вывод скрипта оболочки неправильно разделяется при передаче в качестве аргумента скрипту

Вывод скрипта оболочки неправильно разделяется при передаче в качестве аргумента скрипту

Допустим, у меня есть следующие два скрипта оболочки:

#!/bin/sh
#This script is named: args.sh

echo 1 "\"Two words\"" 3

, и:

#!/bin/sh
#This script is named: test.sh

echo "Argument 1: "$1
echo "Argument 2: "$2
echo "Argument 3: "$3

Когда я вызываю скрипты следующим образом:

sh test.sh $(sh args.sh)

, Я получаю:

Argument 1: 1
Argument 2: "Two
Argument 3: words"

Когда я ожидал вместо этого получить:

Argument 1: 1
Argument 2: Two words
Argument 3: 3

Копирование вывода sh args.shи вставка его в качестве ввода sh test.shработает отлично; поэтому я предполагаю, что это не то, что на самом деле делает оболочка. Я могу получить желаемый/ожидаемый вывод, вызвав sh args.sh | xargs sh test.shвместо этого.

Однако мне интересно, есть ли эквивалентный способ сделать это без перенаправления вывода первого скрипта (args.sh) в xargs? Я хочу, чтобы скрипты вызывались в исходном порядке; аргументный скрипт выводил параметры второму. Я также ищу объяснение, почему этот вызов не работает так, как ожидалось?

решение1

Часть проблемы заключается в том, что строка, возвращаемая args.sh, не анализируется так же, как прямая команда, а только по значению $IFS ( $' \t\n'). Попробуйте включить трассировку команд с помощью set -x:

$ sh /tmp/test.sh $(sh /tmp/args.sh)
++ sh /tmp/args.sh
+ sh /tmp/test.sh 1 '"Two' 'words"' 3
Argument 1: 1
Argument 2: "Two
Argument 3: words"
$

Обратите внимание на строку, начинающуюся с одного +: есть четыре аргумента, '"Two'и 'words"'два из них анализируются как отдельные аргументы. Что вам нужно сделать, так это изменить $IFS.

$ set -x
$ IFS='"'
+ IFS='"'
$ sh /tmp/test.sh $(sh /tmp/args.sh)
++ sh /tmp/args.sh
+ sh /tmp/test.sh '1 ' 'Two words' ' 3'
Argument 1: 1
Argument 2: Two words
Argument 3:  3
$

Это не будет работать для каждого вывода. Лучше всего изменить вывод args.sh, чтобы отделить вывод чем-то отличным от пробела, например, запятой или двоеточием:

$ cat /tmp/args.sh
#!/bin/sh
#This script is named: args.sh

echo "1,Two words,3"
$ IFS=,
$ sh /tmp/test.sh $(sh /tmp/args.sh)
+ sh /tmp/args.sh
+ sh /tmp/test.sh 1 Two words 3
Argument 1: 1
Argument 2: Two words
Argument 3: 3
$

решение2

$varЕсли подстановку переменной или команды оставить $(cmd)без кавычек, результат претерпит следующие преобразования:

  1. Разделить полученную строку на слова. Разделение происходит по пробелам (последовательности пробелов, табуляции и новой строки); это можно настроить с помощью параметра IFS(см.1,2).
  2. Каждое полученное слово обрабатывается как шаблон поиска, и если оно соответствует некоторым файлам, слово заменяется списком имен файлов.

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

Общее правило программирования оболочки — всегда заключать подстановки переменных и команд в двойные кавычки, если только вы не знаете, почему их нужно оставлять. Так что в test.sh, напишите echo "Argument 1: $1". Чтобы передать аргументы в test.sh, у вас возникнут проблемы: вам нужно передать список слов из args.shв test.sh, но выбранный вами метод подразумевает подстановку команды, а это дает возможность только простой строки.

Если вы можете гарантировать, что передаваемые аргументы не содержат никаких новых строк, и допустимо немного изменить процесс вызова, вы можете задать, чтобы он IFSсодержал только новую строку. Убедитесь, что args.shвыводится ровно одно имя файла на строку без лишних кавычек.

IFS='
'
test.sh $(args.sh)
unset IFS

Если аргументы могут содержать произвольные символы (предположительно, за исключением нулевых байтов, которые вы не можете передать как аргументы), вам нужно будет выполнить некоторую кодировку. Подойдет любая кодировка; конечно, это не будет то же самое, что передача аргументов напрямую: это невозможно. Например, в args.sh(замените \tна реальный символ табуляции, если ваша оболочка его не поддерживает):

for x; do
  printf '%s_\n' "$x" |
  sed -e 's/q/qq/g' -e 's/ /qs/g' -e 's/\t/qt/g' -e 's/$/qn/'
done

И в test.sh:

for arg; do
  decoded=$(printf '%s\n' "$arg" |
            sed -e 's/qs/ /g' -e 's/qt/\t/g' -e 's/qn/\n/g' -e 's/qq/q/g')
  decoded=${decoded%_qn}
  # process "$decoded"
done

Вы можете предпочесть изменить, test.shчтобы принять список строк в качестве входных данных. Если строки не содержат новых строк, вызовите args.sh | test.shи используйте это в args.sh(объяснение):

while IFS= read -r line; do
  # process "$line"
done

Другой метод, который полностью устраняет необходимость цитирования, — это вызов второго скрипта из первого.


args.sh "$@"

Связанный контент