Правка файла скриптом — дело трёх строк: прочитать, заменить, записать. На Windows у каждой из трёх строк есть способ испортить файл так, что заметно это станет не сразу. Мы на всех трёх споткнулись: однажды потеряли файл целиком, однажды сломали сборку фронта на 181 ошибку, однажды не смогли запустить скрипт из-за одного невидимого символа.

Это пятая часть цикла «Мастерская разработчика».

1. open(«w») стирает файл до записи

s = open(p, encoding="utf-8").read()
s = s.replace(old, new)
with open(p, "w", encoding="utf-8") as f:   # в этот момент файл уже нулевой длины
    f.write(s)                                # а если запись упала — так и останется

Режим "w" обрезает файл в момент открытия. Если запись после этого упадёт, файл останется пустым. Падает она чаще, чем кажется:

  • UnicodeEncodeError: surrogates not allowed — в строке, собранной из \uXXXX-последовательностей, осталась половинка суррогатной пары (так бывает с эмодзи);
  • UnicodeEncodeError: 'charmap' из print() внутри блока записи — консоль Windows в кодировке cp1251 не умеет печатать «→».

Исключение видно, но выглядит как «ничего не сделал». На деле содержимое уже уничтожено. Так мы однажды потеряли рабочий файл на 685 строк — собирали заново по журналам работы, где нашлись его последнее полное чтение и все правки после него.

Правильно — писать во временный файл и подменять одной операцией:

import os, tempfile

def write_atomic(path, text):
    d = os.path.dirname(os.path.abspath(path))
    fd, tmp = tempfile.mkstemp(dir=d, suffix=".tmp")
    try:
        with os.fdopen(fd, "w", encoding="utf-8", newline="\n") as f:
            f.write(text)
        os.replace(tmp, path)      # атомарно: либо старый файл, либо новый целиком
    except BaseException:
        os.unlink(tmp)
        raise

Проверки (assert old in s) — до открытия на запись. И после правки посмотреть размер: ноль байт — авария, а не «файл не изменился».

2. Текстовый режим переписал все переводы строк

Python на Windows в текстовом режиме переводит каждый \n в \r\n при записи. Скрипт поменял две строки, а записал заново весь файл — уже с CRLF. В диффе это выглядит как изменение каждой строки, а сборка с prettier в линтере падает:

error  Delete `␍`  prettier/prettier
… и так 181 раз — на каждой строке файла

Хуже всего то, что следует дальше. Watch-сборка пишет «compiled with 1 error» и не пересобирает JS, а CSS при этом собирает. По грепу правка на месте, стили обновились — и полчаса уходит на поиск, почему не работает скрипт.

Лечение — newline="\n" при открытии на запись, как в функции выше, или бинарный режим. Проверка:

tr -cd '\r' < file.js | wc -c          # 0 — переводы строк LF
git show HEAD:path/file.js | tr -cd '\r' | wc -c   # а как в репозитории?

grep -c $'\r$' в Git Bash врёт и считает все строки — пользуйтесь tr. И не нормализуйте файл, который в репозитории хранится с CRLF: получите дифф на весь файл.

3. PowerShell дописал невидимый символ в начало

Windows PowerShell 5.1 по команде Set-Content -Encoding UTF8 или Out-File пишет файл с меткой порядка байтов — BOM, три байта EF BB BF в начале. Python на такой скрипт отвечает:

SyntaxError: invalid non-printable character U+FEFF

Конфиг nginx, файл .env или shell-скрипт с BOM ломаются так же: первая строка начинается с невидимого символа. Обратная грабля: .ps1 с кириллицей без BOM PowerShell 5.1 читает в системной кодировке и не может разобрать вовсе. Правила простые:

  • файлы для Linux и Python не писать средствами PowerShell 5.1;
  • передавать файл на сервер — через base64 или scp, а не через echo в удалённую команду;
  • Set-Content -Encoding ASCII молча меняет кириллицу на ? — не выход.

Короткий список

СимптомПричинаЧто делать
файл пустой после упавшего скриптаopen("w") обрезал до записивременный файл + os.replace
сборка падает на каждой строке, JS не пересобираетсяCRLF после записи в текстовом режимеnewline="\n"; проверка tr -cd '\r'
U+FEFF в первой строкеBOM от PowerShell 5.1писать файлы Python-ом или редактором; на сервер — base64
UnicodeEncodeError в printконсоль cp1251в вывод только ASCII или PYTHONIOENCODING=utf-8

Скрипты и сборка ведут себя «мистически»?

Пришлите, что происходит, — найду, где теряются байты.

· полезно?
АЧ
Алексей Чикун веб-разработчик, команда TRIOHEAD

Делаю сайты и сервисы целиком: от вёрстки до сервера. Пишу о том, что проверил на своих проектах.

Комментарии

// загружаю комментарии…

Комментарии публикуются после проверки. Ссылки и HTML не принимаются.