Пакет coreutils предустановлен во многих дистрибутивах Linux. Он содержит в себе стандартные и такие привычные утилиты, как cat, chmod, date, echo, ls и многие другие. Но даже в таком каноническом пакете встречаются ошибки, которые могут помешать работе пользователя. С одной из них я столкнулся на собственном опыте и хочу рассказать о том, как смог её обойти.
Задача была следующая – преобразовать текстовый файл с длинными строками так, чтобы ни одна строка не была длиннее 80 символов. Длинные строки должны разбиваться на несколько строк по 80 или менее символов. Файл закодирован в UTF-8. Немного погуглив можно узнать, что в Unix-подобных ОС с этой задачей справляется утилита fold. Отлично, значит, будем её использовать. Для начала выполним в терминале пару тестовых команд, чтобы научиться управляться с ней. Я приведу ниже вывод команд, выполненных в системе Debian 7.5 с пакетом coreutils 8.13. Такой же вывод будет и в системе Arch Linux с coreutils 8.22.
При выполнении всех тестовых команд настройки локали следующие:
Если у вас не так, то выполните:
Пусть тестовая команда разобьёт строку «abcdefghij» на строки по 4 символа:
Здорово! Теперь строку «абвгдеёжзи»:
И тут-то нас ждет сюрприз. Видим, что строка «абвгдеёжзи» разбилась на строки по два символа. Дело тут в том, что кириллический символ в кодировке UTF-8 занимает два байта, а символ латиницы один. Утилита fold, считая все символы однобайтовыми, просто разбила данную строку (массив байт) на куски по 4 байта. Как видно, такой алгоритм разбиения верен в кодировке UTF-8 только для латинских символов. В то же время утилита wc верно подсчитает количество символов в строке «абвгдеёжзи»:
Это говорит о том, что поддержка юникода в пакете coreutils реализована частично, и результат работы с юникодом различных утилит может быть непредсказуемым.
На самом деле, об этой ошибке было известно несколько лет назад. Она описана тут и тут, и даже дан ответ от разработчиков, но, к сожалению, она по-прежнему находится в состоянии «это не баг, это фича».
Описанное выше не относится к BSD системам, у них собственная реализация стандартных утилит. Тест в системе FreeBSD 10 показал, что там с юникодом всё в порядке.
Теперь поговорим о том, как обойти эту ошибку. Мне известны две замены coreutils: BusyBox и Heirloom. Первый вариант мне показался более актуальным и простым, поэтому покажу как с его помощью соорудить костыль, который позволит нормально пользоваться утилитой fold в вашей системе. Аналогичным образом можно соорудить костыль и для любой другой стандартной утилиты.
Для начала установим пакет busybox. В системе Debian команда:
В системе Arch Linux, соответственно, такая команда:
Согласно документации, использовать BusyBox можно так:
Т.е. просто передавать имя утилиты как параметр исполняемому файлу busybox. Можно также переименовать исполняемый файл в одну из поддерживаемых им команд, и он будет автоматически действовать так, как будто это и есть эта команда. Переименовывать мы его не будем, но вот символьную ссылку с именем fold на него создадим:
После этого fold можно использовать самым привычным образом: вызывать из терминала или скрипта. Такая заплатка в системе является для меня приемлемой. Буду рад, если кому-то она тоже сможет помочь. А пока остаётся надеяться, что когда-нибудь coreutils будет полностью поддерживать юникод.
Задача была следующая – преобразовать текстовый файл с длинными строками так, чтобы ни одна строка не была длиннее 80 символов. Длинные строки должны разбиваться на несколько строк по 80 или менее символов. Файл закодирован в UTF-8. Немного погуглив можно узнать, что в Unix-подобных ОС с этой задачей справляется утилита fold. Отлично, значит, будем её использовать. Для начала выполним в терминале пару тестовых команд, чтобы научиться управляться с ней. Я приведу ниже вывод команд, выполненных в системе Debian 7.5 с пакетом coreutils 8.13. Такой же вывод будет и в системе Arch Linux с coreutils 8.22.
При выполнении всех тестовых команд настройки локали следующие:
$ locale
LANG=ru_RU.UTF-8
LC_CTYPE="ru_RU.UTF-8"
LC_NUMERIC="ru_RU.UTF-8"
LC_TIME="ru_RU.UTF-8"
LC_COLLATE="ru_RU.UTF-8"
LC_MONETARY="ru_RU.UTF-8"
LC_MESSAGES="ru_RU.UTF-8"
LC_PAPER="ru_RU.UTF-8"
LC_NAME="ru_RU.UTF-8"
LC_ADDRESS="ru_RU.UTF-8"
LC_TELEPHONE="ru_RU.UTF-8"
LC_MEASUREMENT="ru_RU.UTF-8"
LC_IDENTIFICATION="ru_RU.UTF-8"
LC_ALL=ru_RU.UTF-8
Если у вас не так, то выполните:
$ export LC_ALL="ru_RU.UTF-8"
Пусть тестовая команда разобьёт строку «abcdefghij» на строки по 4 символа:
$ echo "abcdefghij" | fold -w 4
abcd
efgh
ij
Здорово! Теперь строку «абвгдеёжзи»:
$ echo "абвгдеёжзи" | fold -w 4
аб
вг
де
ёж
зи
И тут-то нас ждет сюрприз. Видим, что строка «абвгдеёжзи» разбилась на строки по два символа. Дело тут в том, что кириллический символ в кодировке UTF-8 занимает два байта, а символ латиницы один. Утилита fold, считая все символы однобайтовыми, просто разбила данную строку (массив байт) на куски по 4 байта. Как видно, такой алгоритм разбиения верен в кодировке UTF-8 только для латинских символов. В то же время утилита wc верно подсчитает количество символов в строке «абвгдеёжзи»:
$ echo -n "абвгдеёжзи" | wc -m
10
Это говорит о том, что поддержка юникода в пакете coreutils реализована частично, и результат работы с юникодом различных утилит может быть непредсказуемым.
На самом деле, об этой ошибке было известно несколько лет назад. Она описана тут и тут, и даже дан ответ от разработчиков, но, к сожалению, она по-прежнему находится в состоянии «это не баг, это фича».
Описанное выше не относится к BSD системам, у них собственная реализация стандартных утилит. Тест в системе FreeBSD 10 показал, что там с юникодом всё в порядке.
Теперь поговорим о том, как обойти эту ошибку. Мне известны две замены coreutils: BusyBox и Heirloom. Первый вариант мне показался более актуальным и простым, поэтому покажу как с его помощью соорудить костыль, который позволит нормально пользоваться утилитой fold в вашей системе. Аналогичным образом можно соорудить костыль и для любой другой стандартной утилиты.
Для начала установим пакет busybox. В системе Debian команда:
# apt-get install busybox
В системе Arch Linux, соответственно, такая команда:
# pacman -S busybox
Согласно документации, использовать BusyBox можно так:
$ busybox ls -l
$ busybox ps
$ busybox seq 1 5
Т.е. просто передавать имя утилиты как параметр исполняемому файлу busybox. Можно также переименовать исполняемый файл в одну из поддерживаемых им команд, и он будет автоматически действовать так, как будто это и есть эта команда. Переименовывать мы его не будем, но вот символьную ссылку с именем fold на него создадим:
# cd $(dirname $(which fold))
# mv fold fold.orig
# ln -s $(which busybox) fold
После этого fold можно использовать самым привычным образом: вызывать из терминала или скрипта. Такая заплатка в системе является для меня приемлемой. Буду рад, если кому-то она тоже сможет помочь. А пока остаётся надеяться, что когда-нибудь coreutils будет полностью поддерживать юникод.