Функция head ведет себя по-разному внутри скрипта sh и в терминале. Почему?

Вопрос или проблема

У меня есть переменная 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"

Для эквивалента.

echo $x2 выполнит разделение и расширение wildcards на расширении. С новой строкой в значении по умолчанию $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-скриптах помогает избежать многих распространенных ошибок, связанных с неправильной обработкой текстовых данных. Будьте внимательны к либерализации использования кавычек, чтобы сохранить всю значимую информацию ввода без непреднамеренных модификаций. Это позволит вашему коду быть более предсказуемым и стабильным вне зависимости от среды выполнения, будь то интерактивная сессия оболочки или сценарий.

Оцените материал
Добавить комментарий

Капча загружается...