Вопрос или проблема
У меня есть переменная x2
, содержащая
08PKj00000YdniC
09:59:04.53 (130409269)|SYSTEM_METHOD_EXIT|[25]|System.debug(ANY)
... много других строк из журналов ниже
Я пытаюсь взять первую строку и отбросить все остальное.
Когда я выполняю
echo $x2 | head -n 1
в терминале,
Я получаю правильный вывод
08PKj00000YdlM5
Однако, когда у меня есть sh-скрипт и я запускаю его из терминала как
./unwrap2.sh verbose
Я вижу странный результат
08PKj00000YdniC 09:59:04.53 (130409269)|SYSTEM_METHOD_EXIT|[25]|System.debug(ANY) ... много других строк из журналов ниже
, что выглядит так, как будто эти строки соединены вместе, хотя я этого не ожидал.
Что я делаю не так внутри скрипта и как это исправить?
Листинг shell-скрипта unwrap2.sh
verbose=$1
# развернуть Data Kit
sf project deploy start -d dc
# развернуть компоненты Data Kit
x=$(sf apex run -f scripts/unwrap.apex --json | jq '.result.logs' -r)
x1=${x#*EXECUTION_STARTED}
x2=${x1#*Result is \(success): }
if [[ "$verbose" = "verbose" ]]; then
echo "x2: $x2"
fi
x3=$(echo "$x2" | head -n 1)
if [[ "$verbose" = "verbose" ]]; then
echo "x3: $x3"
fi
status=$(sf data query -q "select Id,Status FROM BackgroundOperation WHERE Id = '$x3'" --json | jq '.result.records[0].Status' -r)
if [[ "$verbose" = "verbose" ]]; then
echo "select Id,Status FROM BackgroundOperation WHERE Id = '$x3'"
fi
echo "Status: $status"
while [[ "$status" != "Complete" ]]; do
sleep 10
status=$(sf data query -q "select Id,Status FROM BackgroundOperation WHERE Id = '$x3'" --json | jq '.result.records[0].Status' -r)
echo "Status: $status"
done
Когда я выполняю
x3=$(echo $x2 | head -n 1)
в терминале, это работает нормально.
Код в вашем вопросе в основном написан в синтаксисе Korn shell. Korn shell изначально был ответвлением от Bourne shell, в которое добавлено много функций, включая ${var#pattern}
, $(cmd substitution)
и [[ test-expression ]]
.
Стандарт POSIX определил некоторые из этих расширений для стандартного языка sh
, включая ${var#pattern}
и $(cmd substitution)
, но не включая [[ test-expression ]]
.
И Korn shell, и POSIX sh
сохранили ту неправильную функцию Bourne shell, согласно которой параметрическое расширение и замена команд, когда они не заключены в кавычки и находятся в контексте списка, подлежат разделению и расширению wildcards (что можно рассматривать как своего рода неявный оператор, вызываемый при расширении, который многие путают с токенизацией, которую оболочка выполняет в своем синтаксисе¹).
Несколько оболочек скопировали (в более или менее степени) многие из расширений Korn shell. Это относится к bash (и /bin/sh
в macos, который, возможно, вы используете, является древней версией bash) и zsh (оболочка по умолчанию в macos), по крайней мере.
zsh исправил эту неправильную функцию оболочек Bourne/Korn, где расширения $param
подлежат разделению и расширению wildcards, предоставив два отдельных явных оператора для частей разделения ($=param
) и расширения wildcards ($~param
). Разделение все еще выполняется при подстановке команд (где это обычно полезно), и zsh все еще выполняет разделение и расширение wildcards, когда запускается как sh
/ksh
или после emulate sh
/ksh
, чтобы иметь возможность интерпретировать код, написанный для sh
/ksh
.
bash
этого не сделал, и в bash
, будь он запущен как sh
или bash
, разделение и расширение wildcards выполняются при не заключенном в кавычки расширении параметра.
В zsh
:
cmd $var
Где $var
является скаляром (не массивом и не ассоциативным массивом), вызывает cmd
с содержимым $var
как единственным аргументом².
Но в ksh, sh или bash вам нужно:
cmd "$var"
Для эквивалента.
выполнит разделение и расширение wildcards на расширении. С новой строкой в значении по умолчанию echo $x2
$IFS
(который участвует в части разделения), в вашем случае echo
получит отдельные аргументы для каждой из строк в переменной, и так как echo
соединяет свои аргументы пробелами, вывод, вероятно³, будет только одной строкой.
Если в вашем скрипте нет строки-шебанг или если он начинается с #! /bin/sh -
, он будет запускаться через sh
(который в macos является древней версией bash). Поскольку этот код не является допустимым стандартным синтаксисом sh
, это неправильно (хотя sh
в macos все же сможет сделать из него некоторый смысл). Здесь вам нужно #! /bin/zsh -
, если вы хотите, чтобы он интерпретировался через zsh (ту же оболочку, которую вы используете интерактивно), или перевести его в синтаксис POSIX sh (и добавить строку шебанг #! /bin/sh -
).
Даже в zsh
, как подсказано в примечании 3, использование echo
таким образом неправильно, так как echo
не может быть использовано для вывода произвольных данных.
должно быть либо:echo $x2
echo -E - $x2 # специфично для zsh
print -r - $x2 # специфично для zsh
print -r - "$x2" # только оболочки типа Korn
print -r -- "$x2" # вариант Korn, возможно, немного более портативный среди оболочек типа Korn.
printf '%s\n' "$x2" # стандартный, но не все оболочки (в частности ksh88 и некоторые производные pdksh) имеют встроенный printf
cat <
Но здесь, если это для того, чтобы взять первую строку, вы можете использовать стандартный оператор ${var%%pattern}
(также из Korn shell):
first_line=${x2%%$'\n'*}
Или (более портативный, так как $'...'
был только добавлен в издании POSIX 2024 года)
first_line=${x2%%'
'*}
Так что:
#! /bin/zsh -
verbose=$1
if [[ $verbose = verbose ]]; then
verbose() print -ru2 -- "$@"
else
verbose() true
fi
x=$(
sf apex run -f scripts/unwrap.apex --json |
jq -r .result.logs
)
x1=${x#*'EXECUTION_STARTED'}
x2=${x1#*'Result is (success): '}
verbose "x2: $x2"
x3=${x2%%$'\n'*}
verbose "x3: $x3"
(или используйте одну из более продвинутых функций извлечения текста zsh)
Или:
#! /bin/sh -
verbose=$1
if [ "$verbose" = verbose ]; then
verbose() (
IFS=' '
printf>&2 '%s\n' "$*"
)
else
verbose() { true; }
fi
x=$(
sf apex run -f scripts/unwrap.apex --json |
jq -r .result.logs
)
x1=${x#*'EXECUTION_STARTED'}
x2=${x1#*'Result is (success): '}
verbose "x2: $x2"
x3=${x2%%$'\n'*}
verbose "x3: $x3"
¹ Bourne shell, вероятно, ввел эту функцию для сохранения некоторого уровня обратной совместимости с ее предшественником, где результат параметрического расширения (замены команд не существовало) снова вводился и анализировался как shell-код.
² За исключением случаев, когда $var
пусто или не установлено, в этом случае никакой аргумент не передается, что может быть одной из причин, почему даже в zsh вам все еще следует использовать cmd "$var"
, чтобы передавался пустой аргумент.
³ Поскольку echo
расширяет \n
до символа новой строки, у вас все еще может быть несколько строк вывода.
Похоже, я понял.
Мне нужно поставить двойные кавычки внутри файла скрипта.
Из-за особенностей оболочки zsh
, это работает нормально внутри zsh
, но не при выполнении файла скрипта
x3=$(echo "$x2" | head -n 1)
.
Ответ или решение
Теория
Поведение команды head
внутри сценария shell (sh
-скрипта) может отличаться от выполнения в интерактивной оболочке, например, zsh
, из-за способов обработки текстовых данных и интерпретации переменных в этих контекстах. В shell-скриптах, каждая команда обрабатывается отдельно, и когда вы используете текстовые данные без явных кавычек, shell может выполнять операции, такие как разделение по пробелам и символам из переменной окружения IFS
(Internal Field Separator), а также применение глобальных шаблонов. Это может привести к неожиданным результатам, когда строки объединяются или обрезаются иначе, чем ожидалось.
Пример
В вашем сценарии вы пытаетесь извлечь первую строку из переменной x2
, используя команду:
x3=$(echo $x2 | head -n 1)
При таком подходе, если x2
содержит переносы строк, shell будет сначала разбирать переменную x2
на отдельные слова или строки на основе значения IFS
, затем передавать их как отдельные аргументы в команду echo
. echo
соединяет эти аргументы пробелами, что приводит к потере исходных переносов строк, и, следовательно, команде head
не удается правильно идентифицировать первую "строку".
Применение
Чтобы избежать этой проблемы и убедиться, что ваш shell-скрипт работает так же, как интерактивная команда в терминале, вам необходимо использовать кавычки для хранения переноса строк как части одного аргумента. Воспользуйтесь следующей конструкцией:
x3=$(echo "$x2" | head -n 1)
Кавычки вокруг $x2
предотвращают разбиение содержимого переменной на основании символов IFS
, передавая все содержимое в команду echo
как единый аргумент. Это гарантирует, что команда head
видит правильный форматированный ввод и успешно извлекает первую строку, как ожидается.
Заключение
Правильное использование кавычек в shell-скриптах помогает избежать многих распространенных ошибок, связанных с неправильной обработкой текстовых данных. Будьте внимательны к либерализации использования кавычек, чтобы сохранить всю значимую информацию ввода без непреднамеренных модификаций. Это позволит вашему коду быть более предсказуемым и стабильным вне зависимости от среды выполнения, будь то интерактивная сессия оболочки или сценарий.