Почему MacOS всегда добавляет в файл, перенаправленный дескриптор, даже когда указано перезаписать? Ubuntu добавляет только когда строго указано добавить.

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

Дан следующий код:

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.

Пример

В предоставленном вами коде есть несколько явных примеров таких несоответствий.

  1. Простой редирект:

    • На 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 добавляет данные.

  2. Эксплицитное дополнение:

    • Результат для 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$

В случае явного указания дополнения (>>), обе операционные системы ведут себя одинаково, что демонстрирует консистентность работы данных команд на обоих ОС.

  1. Случай с дополнительным файловым дескриптором:
    • На MacOS:
      0$
      1$
      2$
      3$
      4$
    • На Ubuntu:
      4$

Этот пример демонстрирует различия в поведении, когда файловые дескрипторы открываются для дополнения в MacOS, а Ubuntu, похоже, перезаписывает, что ведет к потере данных.

Приложение

Чтобы избежать проблем с несовместимостью в ваших скриптах на разных платформах, разработчики должны принимать меры для обеспечения предсказуемости. Включение таких элементов как:

  • Проверка документации: Убедитесь, что вы понимаете и учитываете поведение редиректов в используемой вами оболочке и системе.
  • Явное указание флагов: Где возможно, используйте явное указание методов ввода-вывода, как, например, работа с noclobber или использование инструментов с флагами для контроля поведения.
  • Платформенно-специфичный код: При необходимости, замените части кода с использованием условных конструкций, чтобы подстроиться под особенности каждой платформы.
  • Тестирование и автоматизация: Проведите тщательное тестирование ваших скриптов на целевых системах, использующих непрерывную интеграцию и автоматические проверки.

Вывод

Данная проблема показывает важность мелочей при программировании в разных системах, особенно когда речь идет о POSIX-совместимых системах, каждая из которых может иметь свои собственные нюансы. Осознание этих различий и тщательное проектирование скриптов помогут гарантировать, что ваши программы будут функционировать, как задумано, на любой платформе.

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

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