Машина без правильной локали и часового пояса - источник тихих, противных багов: логи с временем "не от мира сего", команда sort выдаёт неожиданный порядок, скрипт падает на дробном числе, а в имени файла вместо кириллицы видны кракозябры. В этом уроке разбираемся, как Linux хранит время и язык окружения, чем locale отличается от часового пояса, как чинить кодировки утилитой iconv и почему один и тот же скрипт ведёт себя по-разному в зависимости от переменной LC_ALL.

Как это работает
Сначала про время. Системные часы (RTC и программный таймер ядра) лучше всего держать в UTC - это абсолютная шкала без переходов на летнее время. Локальное время вычисляется на лету из UTC плюс правило часового пояса. Эти правила (когда смещение, когда переходы, исторические сдвиги) лежат в базе tzdata в каталоге /usr/share/zoneinfo - по одному скомпилированному бинарному файлу на зону, например /usr/share/zoneinfo/Europe/Moscow. Файл /etc/localtime - это символьная ссылка или копия нужного файла зоны, именно его читают библиотечные функции при форматировании времени.
База tzdata живёт своей жизнью: правительства меняют пояса и отменяют переход на летнее время, поэтому пакет с зонами регулярно обновляется (релизы вида 2026a, 2026b). Держать его свежим - часть гигиены сервера, иначе после очередной реформы машина покажет неверное локальное время.
Теперь про язык. Локаль - это набор правил "как в данной культуре принято" писать числа, деньги, даты, как сравнивать строки и на каком языке выводить сообщения. Задаётся переменными окружения. LANG - общий рубильник для всех категорий. Категории LC_* перекрывают LANG по частям: LC_COLLATE отвечает за сортировку, LC_CTYPE за классификацию символов и кодировку, LC_NUMERIC за десятичный разделитель, LC_TIME за формат даты, LC_MESSAGES за язык сообщений программ. Переменная LC_ALL - аварийный перекрыватель: если она задана, она бьёт и LANG, и все LC_*. Поэтому в скриптах для предсказуемости часто пишут LC_ALL=C.
Имя локали имеет вид язык_СТРАНА.кодировка, например ru_RU.UTF-8. Особые имена: C (он же POSIX) - минимальная локаль, байтовая сортировка, английские сообщения; C.UTF-8 - то же, но с поддержкой UTF-8 как кодировки. В 2026 рабочая кодировка везде UTF-8, старые однобайтовые (KOI8-R, ISO-8859, CP1251) встречаются только в легаси-данных. UTF-8 хорош тем, что ASCII в нём занимает один байт и совместим, а остальные символы - от двух до четырёх байт.
Важно: locale - это про интерпретацию байтов, а не про их изменение. Если файл реально записан в CP1251, локаль UTF-8 его не "переведёт" - её надо перекодировать утилитой iconv.
Команды и примеры
Часовой пояс через systemd - одинаково на всех современных дистрибутивах:
Код: Выделить всё
timedatectl # текущее время, зона, статус NTP
timedatectl list-timezones # список всех зон
timedatectl set-timezone Europe/Moscow
timedatectl set-ntp true # включить синхронизацию времени
Код: Выделить всё
ls -l /etc/localtime
# /etc/localtime -> ../usr/share/zoneinfo/Europe/Moscow
date # время в локальной зоне
date -u # то же в UTC
Код: Выделить всё
locale # текущие значения всех категорий
locale -a # какие локали собраны в системе
locale -a | grep -i ru # есть ли русская
Код: Выделить всё
# вписать ru_RU.UTF-8 в /etc/locale.gen (или dpkg-reconfigure locales)
sudo sed -i 's/^# *ru_RU.UTF-8/ru_RU.UTF-8/' /etc/locale.gen
sudo locale-gen
sudo update-locale LANG=ru_RU.UTF-8
Код: Выделить всё
sudo dnf install glibc-langpack-ru
localectl set-locale LANG=ru_RU.UTF-8
Код: Выделить всё
LC_ALL=C sort names.txt # детерминированная байтовая сортировка
LANG=ru_RU.UTF-8 date # дата по-русски
Код: Выделить всё
iconv -f CP1251 -t UTF-8 old.txt -o new.txt
iconv -l # список поддерживаемых кодировок
iconv -f UTF-8 -t UTF-8//IGNORE bad.txt -o clean.txt # выкинуть битые байты
Влияние локали наглядно на сортировке. В локали C сортировка идёт по кодам байтов: все заглавные буквы раньше строчных. В ru_RU.UTF-8 действуют человеческие правила, регистр и порядок букв учитываются культурно:
Код: Выделить всё
printf 'b\nA\na\nB\n' | LC_ALL=C sort # A B a b
printf 'b\nA\na\nB\n' | LC_ALL=ru_RU.UTF-8 sort # a A b B
- LC_ALL задана где-то в профиле и молча перекрывает всё - LANG менять бесполезно, пока не снимете LC_ALL. Проверяйте командой locale на пустые и неожиданные значения.
- Скрипт работает у вас и падает на сервере из-за LC_NUMERIC: десятичный разделитель в ru_RU - запятая, и awk или printf неверно парсят "3.14". В скриптах фиксируйте LC_ALL=C.
- Имя зоны чувствительно к регистру и формату: Europe/Moscow, а не europe/moscow или MSK. Аббревиатуры вроде EST в качестве зоны - ловушка, они не учитывают переход на летнее время.
- Файл в CP1251 открыт как UTF-8 - видны кракозябры. Локаль не перекодирует содержимое, нужен iconv. И наоборот, дважды перекодированный UTF-8 уже не починить вслепую.
- Поставили langpack или сгенерировали локаль, но не перелогинились - переменные окружения подхватываются только в новой сессии.
- Часы в UTC, но date показывает не то: дело не в часах, а в /etc/localtime или в устаревшем tzdata после реформы пояса. Обновите пакет с зонами.
- LANGUAGE (с приоритетом списков языков для сообщений gettext) путают с LANG - это разные переменные, LANGUAGE влияет только на язык переводов.
- Выполните timedatectl и запомните текущую зону. Переключитесь командой timedatectl set-timezone на Asia/Tokyo и сравните вывод date и date -u.
- Верните свою зону через tzselect (получите имя) и timedatectl set-timezone. Проверьте ссылку ls -l /etc/localtime.
- Командой locale -a убедитесь, есть ли ru_RU.UTF-8. Если нет - сгенерируйте её способом для вашего дистрибутива (locale-gen или dnf install glibc-langpack-ru).
- Создайте файл со строками b, A, a, B и отсортируйте его дважды: LC_ALL=C sort и LC_ALL=ru_RU.UTF-8 sort. Объясните разницу.
- Сделайте текстовый файл с кириллицей, перекодируйте его в CP1251 утилитой iconv, затем обратно в UTF-8. Проверьте file -i на каждом шаге.
- Задайте LC_NUMERIC=ru_RU.UTF-8 и выполните printf "%.2f\n" 3,14 и затем то же с LC_ALL=C для значения 3.14 - сравните поведение.
- Чем отличается /etc/localtime от каталога /usr/share/zoneinfo и что на что ссылается?
- Какая переменная имеет наивысший приоритет: LANG, LC_TIME или LC_ALL, и почему её используют в скриптах?
- За какую категорию локали отвечает LC_COLLATE и как локаль C меняет результат sort?
- Какой командой systemd сменить часовой пояс и что при этом происходит с /etc/localtime?
- Почему открытие файла в кодировке CP1251 под локалью UTF-8 даёт кракозябры и какой утилитой это лечится?
- Чем имя локали C.UTF-8 отличается от ru_RU.UTF-8 по части сортировки и сообщений?