Вопрос или проблема
Дан следующий код:
out="$(mktemp)"
rm -f "$out"
clear
printf '%s\n' 0 >"$out"
{
printf '%s\n' '1' >/dev/stdout
printf '%s\n' '2' >/dev/stdout
} >"$out"
cat -e -- "$out"
rm -f "$out"
На Ubuntu это выводит:
2$
На MacOS это выводит:
1$
2$
При явном добавлении, они ведут себя согласованно:
out="$(mktemp)"
rm -f "$out"
clear
printf '%s\n' 0 >"$out"
{
printf '%s\n' '1' >/dev/stdout
printf '%s\n' '2' >>/dev/stdout
} >"$out"
cat -e -- "$out"
rm -f "$out"
На MacOS и Ubuntu это выводит:
1$
2$
Самый запутывающий пример для меня — этот:
out="$(mktemp)"
rm -f "$out"
clear
printf '%s\n' 0 >"$out"
exec 3>>"$out"
{
printf '%s\n' '1' >/dev/stdout
printf '%s\n' '2' >/dev/stdout
} >&3
{
printf '%s\n' '3' >/dev/stdout
printf '%s\n' '4' >/dev/stdout
} >&3
cat -e -- "$out"
rm -f "$out"
exec 3>&-
Который на MacOS выводит:
0$
1$
2$
3$
4$
Который на Ubuntu выводит:
4$
Я ожидал это на Ubuntu:
0$
2$
4$
Я абсолютно запутался, почему такое поведение возникает в этом примере и во всех других примерах, которые я разработал, чтобы иллюстрировать это несоответствие.
Мои вопросы:
- Что это за несоответствие? Что происходит? Является ли это несоответствие преднамеренным?
- Где еще это несоответствие встречается? Каковы его истоки?
- Если это несоответствие преднамеренное, почему оно было оправдано? Какое поведение должно быть правильным?
- Что можно сделать, чтобы смягчить эти различия при написании кросс-ОС скриптов?
- Является ли
shopt -o noclobber
подходящим ответом? Является ли это истинной необходимостьюnoclobber
?
Ответ или решение
Объяснение проблемы перезаписи и управления файловыми дескрипторами в MacOS и Ubuntu
Теория
Когда мы говорим о работе с файловыми дескрипторами в Unix-подобных системах, таких как MacOS и Ubuntu, мы имеем дело с тонкостями, которые могут вызвать разногласия в поведении при переадресации вывода. В частности, мы рассматриваем различия в поведении команд оболочки при редиректе вывода в файлы, а именно: перезаписывается или дополняется содержимое файла при использовании разных вариантов редиректа.
В основе управления редиректами в Unix-образных системах лежит механизм открытия файла, который контролируется флагами, переданными системному вызову open()
. Операции > и
>>влияют на эти флаги:
>переадресовывает вывод, перезаписывая файл, тогда как
>>` всегда добавляет информацию в конец файла.
На уровне системы различие может быть обусловлено применением буферизации, разным поведением интерпретаторов команд, а также различиями в реализации промежуточных данных в ядре MacOS и Ubuntu.
Пример
В предоставленном вами коде есть несколько явных примеров таких несоответствий.
-
Простой редирект:
-
На Ubuntu:
out="$(mktemp)" rm -f "$out" printf '%s\n' 0 >"$out" { printf '%s\n' '1' >/dev/stdout printf '%s\n' '2' >/dev/stdout } >"$out"
Вывод:
2$
-
На MacOS:
Вывод:1$ 2$
В этом сценарии, похоже, Ubuntu перезаписывает файл при каждом редирект-операции, тогда как MacOS добавляет данные.
-
-
Эксплицитное дополнение:
- Результат для Ubuntu и MacOS:
out="$(mktemp)" rm -f "$out" printf '%s\n' 0 >"$out" { printf '%s\n' '1' >/dev/stdout printf '%s\n' '2' >>/dev/stdout } >"$out"
Вывод:
1$ 2$
- Результат для Ubuntu и MacOS:
В случае явного указания дополнения (>>
), обе операционные системы ведут себя одинаково, что демонстрирует консистентность работы данных команд на обоих ОС.
- Случай с дополнительным файловым дескриптором:
- На MacOS:
0$ 1$ 2$ 3$ 4$
- На Ubuntu:
4$
- На MacOS:
Этот пример демонстрирует различия в поведении, когда файловые дескрипторы открываются для дополнения в MacOS, а Ubuntu, похоже, перезаписывает, что ведет к потере данных.
Приложение
Чтобы избежать проблем с несовместимостью в ваших скриптах на разных платформах, разработчики должны принимать меры для обеспечения предсказуемости. Включение таких элементов как:
- Проверка документации: Убедитесь, что вы понимаете и учитываете поведение редиректов в используемой вами оболочке и системе.
- Явное указание флагов: Где возможно, используйте явное указание методов ввода-вывода, как, например, работа с
noclobber
или использование инструментов с флагами для контроля поведения. - Платформенно-специфичный код: При необходимости, замените части кода с использованием условных конструкций, чтобы подстроиться под особенности каждой платформы.
- Тестирование и автоматизация: Проведите тщательное тестирование ваших скриптов на целевых системах, использующих непрерывную интеграцию и автоматические проверки.
Вывод
Данная проблема показывает важность мелочей при программировании в разных системах, особенно когда речь идет о POSIX-совместимых системах, каждая из которых может иметь свои собственные нюансы. Осознание этих различий и тщательное проектирование скриптов помогут гарантировать, что ваши программы будут функционировать, как задумано, на любой платформе.