BUGS (Ошибки в sed).

Вступление (Эта программа работает криво! Что делать?)

Ну скорее всего криво работает не программа, а ваш sed-скрипт. "Криво" - значит не так, как вы того хотите. Это вполне нормальное явление, и например у меня так бывает постоянно. Мало того, значительно чаще, чем скажем если я использую например C++. И тем не менее намного быстрее написать быстренько sed(bash/perl/...) скрипт, который с третьего раза, но выполнит задачу, чем мучится с составлением всяких ООПшных конструкций.

Для начала, авторы рекомендуют сначала изучить список известных "ошибок", которые из разряда "это не баг, а фича". Ну а я попытаюсь пояснить, почему-же это так сделано. Список представляет собой перевод файла BUGS из исходных текстов Gnu-sed version 4.2.

Куда слать баг-репорты?

Замечание

Bugs and comments may be sent to bonzini@gnu.org; please include in the Subject: header the first line of the output of ``sed --version''.

Please do not send a bug report like this:

[while building frobme-1.3.4]

$ configure

sed: file sedscr line 1: Unknown option to 's'

If sed doesn't configure your favorite package, take a few extra minutes to identify the specific problem and make a stand-alone test case.

A stand-alone test case includes all the data necessary to perform the test, and the specific invocation of sed that causes the problem. The smaller a stand-alone test case is, the better. A test case should not involve something as far removed from sed as ``try to configure frobme-1.3.4''. Yes, that is in principle enough information to look for the bug, but that is not a very practical prospect.

Команда N и последняя строка.

Многие версии sed завершают работу скрипта, если выполняется команда N для последней строки. Однако, GNU sed в данном случае распечатает значение буфера, если это не запрещено (например опцией -n). И так-же прервёт выполнение скрипта.

Замечание

Данное поведение не очень логично, но очень удобно. На самом деле это поведение при ошибке, ведь мы читаем строку, которой нет. Если нам это не нужно, мы можем проверить, является-ли данная строка последней, и если это не так, то выполнить N.

См. также info sed, и Пример типичного использования команд D и N.

Синтаксис регулярных выражений (проблемы с обратным слешем).

sed использует базовые POSIX регулярные выражения. Кроме стандартных, GNU sed использует несколько своих спец-символов:

`\|', `\+', `\?', `\`', `\'', `\<', `\>', `\b', `\B', `\w', `\W'

Таким образом, RE /x\+/ совпадает в GNU-sed с одним или боле символов «x», а в некоторых других sed это RE совпадает только с «x+».

Замечание

Лично я практически всегда использую ERE, потому у меня символ «+» как раз и надо экранировать слешем, если я хочу, что-бы он воспринимался как обычный символ. Конечно приходится помнить про метасимволы вроде «\`», которые приходится экранировать даже в ERE, впрочем такое встречается редко.

см. также info sed

sed -i портит файлы "только для чтения"!

Короче:

sed d -i

удаляет содержимое файла. Даже если этот файл защищён от модификации. Это не ошибка. Это непонимание, как работают UNIX ФС.

Права на файл говорят о содержимом файла, а права на директорию - о списке файлов в этой директории. sed -i никогда не открывает файл для записи/модификации. Вместо этого она создаёт временную копию изменённого файла, который после редактирования переименовывает как старый, потому кажется, что этот файл был изменён. На самом деле, это новый файл, который был создан на основе старого.

Замечание

Кстати странно, а как ещё должен работать потоковый редактор? Да, он просто фильтрует поток, а -i просто задаёт особое поведение для исходящего потока (после его сохранения в файле, этот файл переименовывается как исходный). Кстати, можно и не удалять источник, есть ещё возможность переименования источника по шаблону. Подробнее см. info sed.

Внимание

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

Выражение /[А-Я]/ не совпадает с буквами русского алфавита!, а команда s/.*// не стирает буфер!

Да. Я знаю... И разработчики sed тоже в курсе... Вот только проблема тут вовсе и не в sed, и даже не в GNU Linux... Данная проблема проявляется исключительно при использовании национальных локалей, которые не совпадают с POSIX, или что тоже самое с C. Или, говоря по иному, при использовании любых алфавитных символов не из диапазона /[A-Za-z]/. С первой проблемой справится довольно просто: можно записать не /А-Я/, а

/[АБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЬЫЪЭЮЯЁ]/

Это конечно довольно криво, но что делать? За то это выражение отлично работает, и всегда совпадает с русскими большими буквами. При этом конечно подразумевается, что sed-скрипт, и обрабатываемый текст в одной и той-же кодировке (при использовании UTF-8 текст и скрипт может быть на любом языке мира, причём одновременно на разных языках). Проблема тут в том, что русские буквы вовсе не обязательно идут по алфавиту, потому сравнивая их мы получаем неопределённый результат (на разных компьютерах результат может быть разным).

Вторая проблема намного серьёзнее.

Внимание

Используя тот факт, что RE /.*/ не всегда совпадает с чем угодно, злоумышленник может протащить свой вредоносный код через любой фильтр основанный на регулярных выражениях! Т.е. практически куда угодно!

Проблема тут в том, что хотя метасимвол точка «.» безусловно совпадает с любым символом, он тем не менее вовсе не обязан совпадать с НЕСИМВОЛОМ! Алфавит UNICODE "дырявый", он содержит дыры, некоторые допустимые числа, которые не сопоставлены ни с каким символом. Поведение точки на таких несимволах не определено стандартом, и на практике точка НЕ совпадает с несимволом.

Подсказка

Отключить юникод можно так.

Вот простой пример: нам надо отсечь всё, что лежит после «|»

$ echo -e "тест|вредоносный код" | sed 's/|.*//'
тест

Как видите, всё прекрасно работает... Однако:

$ echo -e "тест|\xD1вредоносный код" | sed 's/|.*//'
тест?вредоносный код

Ну вот... Вредоносный код успешно проник сквозь фильтр :-(

Подсказка

Кроме отключения юникода есть ещё одно решение.

Внимание

Это вовсе не проблема sed! Это проблема юникода. И единственный возможный метод избежать этой проблемы - отключить юникод.

Проблема sed - в лёгкости, мощности, и чрезвычайной простоте: можно написать изящный, и простой пример уязвимости... Конечно код на языке perl, и тем более C будет намного более громоздким. Беда в том, что если вы пишете на C или PERL'е, то у вас уже включён юникод, и ваша программа так-же подвержена этой уязвимости. Попробуйте например вот эту программу на C:

#include <locale.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <regex.h>

#define ERRBUF_SIZE 1024
#define NMATCH 10

void error(int err_code, regex_t *rs)1
{
        char errbuf[ERRBUF_SIZE];
        if(!err_code)   return;
        regerror(err_code, rs, errbuf, ERRBUF_SIZE);
        fprintf(stderr, "regex error: %s\n", errbuf);
        regfree(rs);
        _exit(err_code);
}

int main(void)
{
        const char str[] = "тестов\xD1\xD1ая строчка. ABCDEFG";2
        const char regex[] = ".*";3
        regex_t rs;
        regmatch_t pmatch[NMATCH];
        char *lc = setlocale(LC_ALL, "ru_RU.UTF-8");4
        if(!lc)	return 77;
        printf("test string: `%s' (%s)\n", str, lc);
        int err = regcomp(&rs, regex, REG_EXTENDED);
        error(err, &rs);
        const char *ps = str;
        while(*ps)5
        {
                err = regexec(&rs, ps, NMATCH, pmatch, 0);
                error(err, &rs);
                int j;
                const char *ps2 = NULL;
                for(j = 0; j < NMATCH; j++)
                {
                        if(pmatch[j].rm_so < 0)
                                continue;
                        printf("Совпадение %d: %d...%d\t`", j, pmatch[j].rm_so, pmatch[j].rm_eo);
                        int i;
                        for(i = pmatch[j].rm_so; i < pmatch[j].rm_eo; i++)
                                printf("%c", ps[i]);
                        printf("'\n");
                        if(!ps2)
                                ps2 = ps + i;6
                }
                ps = ps2;
                if(!ps)	break;7
                printf("остаток: `%s'\n", ps);
        }
        regfree(&rs);
        return 0;
}

1

Функция для проверки ошибок, не производит никаких действий если код ошибки равен нулю, иначе выдаёт сообщение об ошибке и прерывает программу.

2

Именно эта строчка предполагается введённой злоумышленником. В ней содержится НЕСИМВОЛЫ.

3

Это регулярное выражение, предполагается, что оно совпадает с любой строкой.

4

Тут мы устанавливаем локаль UTF-8.

5

Тут мы входим в цикл разбора строки, мы ищем совпадения, пока в строке есть хоть 1 символ.

6

Если это нулевое совпадение(в смысле, совпадение со всем шаблоном, а НЕ с под-шаблонами), то мы сохраняем позицию символа за концом совпадения. Если там больше ничего нет, то сохраняется позиция завершающего нулевого байта.

7

Здесь мы выходим из основного цикла, потому-что совпадений не было.

Ага... Зависнет эта программа в бесконечном цикле... А всё потому, что выражение /.*/ не совпадает со всей строкой - строка некорректная. Ну и поведение программы тоже не предсказуемое. К сожалению, такое будет работать на любом языке, лишь-бы он поддерживал юникод.

Программа зациклилась, потому-что она нашла совпадение, и это совпадение - пустая строка. Это произошло на второй итерации, на первой мы нашли всё что до не символа, а во второй - от несимвола и до несимвола. В итоге и в строке что-то есть, и что-то находится, но продвижения вперёд нет.

Вы можете обсудить этот документ на форуме. Текст предоставляется по лицензии GNU Free Documentation License (Перевод лицензии GFDL).

Вы можете пожертвовать небольшую сумму яндекс-денег на счёт 41001666004238 для оплаты хостинга, интернета, и прочего. Это конечно добровольно, однако это намного улучшит данный документ (у меня будет больше времени для его улучшения). На самом деле, проект часто находится на грани закрытия, ибо никаких денег никогда не приносил, и приносить не будет. Вы можете мне помочь. Спасибо.