C++17 STL Стандартная библиотека шаблонов — страница 115 из 119


Про этот класс можно сказать лишь следующее:

□ он проверяет каждый элемент один раз;

□ порядок, в котором выполняется перебор, не определен;

□ элементы каталога

.
и
..
уже отфильтрованы.


Однако можно заметить, что итератор

directory_iterator
ведет себя как итератор и как итерабельный диапазон одновременно. Почему? В небольшом примере с циклом
for
мы видели, как он используется в качестве итерабельного диапазона. В самом коде примера мы применили его как итератор:


transform(directory_iterator{dir}, {},

          back_inserter(items), file_info);


Правда заключается в том, что это всего лишь класс итератора, но функции

std::begin
и
std::end
предоставляют перегруженные версии данного типа. Данное обстоятельство позволяет вызвать функции
begin
и
end
для подобного итератора, и они снова будут возвращать итераторы. Это может показаться странным на первый взгляд, но делает наш класс более полезным. 

Инструмент текстового поиска в стиле grep

Большая часть операционных систем оснащена неким локальным инструментом поиска. Пользователи могут запустить его нажатием сочетания клавиш, а затем ввести имя локального файла, который хотели бы найти.

Прежде чем появилась подобная возможность, пользователи командной строки выполняли поиск файлов с помощью таких инструментов, как

grep
или
awk
. Пользователь мог просто ввести команду наподобие
"grep -r foobar ."
, и инструмент рекурсивно прошел бы по текущему каталогу и нашел бы все файлы, содержащие строку
"foobar"
.

В этом примере мы реализуем точно такое же приложение. Наш небольшой клон

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


Как это делается

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

grep
, но для простоты будет не таким зрелым и эффективным.


1. Сначала включим все необходимые заголовочные файлы и объявим об использовании пространств имен

std
и
filesystem
:


#include 

#include 

#include 

#include 

#include 

#include 


using namespace std;

using namespace filesystem;


2. Потом реализуем вспомогательную функцию. Она принимает путь к файлу и объект, содержащий регулярное выражение, описывающее искомый шаблон. Затем создаем экземпляр вектора, который будет включать пары, состоящие из номера строк и их содержимого. Кроме того, создадим экземпляр объекта файлового потока ввода, из которого будем считывать содержимое и сравнивать его с шаблоном строка за строкой:


static vector>

matches(const path &p, const regex &re)

{

  vector> d;

  ifstream is {p.c_str()};


3. Пройдем по файлу строка за строкой с помощью функции

getline
. Функция
regex_search
возвращает значение
true
при условии, что строка содержит наш шаблон. Если это именно так, то поместим в вектор номер строки и саму строку. Наконец, вернем все найденные совпадения:


  string s;

  for (size_t line {1}; getline(is, s); ++line) {

    if (regex_search(begin(s), end(s), re)) {

      d.emplace_back(line, move(s));

    }

  }

  return d;

}


4. В функции

main
сначала проверим, предоставил ли пользователь аргумент командной строки, который можно задействовать как шаблон. Если нет, то сгенерируем ошибку:


int main(int argc, char *argv[])

{

  if (argc != 2) {

    cout << "Usage: " << argv[0] << " \n";

    return 1;

}


5. Далее создадим объект регулярного выражения из входного шаблона. Невозможность создать такое регулярное выражение приведет к генерации исключения. При генерации исключения поймаем его и сгенерируем ошибку:


  regex pattern;

  try { pattern = regex{argv[1]}; }

  catch (const regex_error &e) {

    cout << "Invalid regular expression provided.n";

    return 1;

  }


6. Наконец, можно проитерировать по файловой системе и поискать совпадения с шаблоном. Воспользуемся итератором

recursive_directory_iterator
для итерации по всем файлам рабочего каталога. Он работает точно так же, как и итератор
directory_iterator
из предыдущего примера, но заходит еще и в подкаталоги. Таким образом, не нужно управлять рекурсией. Для каждой записи вызываем вспомогательную функцию matches:


  for (const auto &entry :

       recursive_directory_iterator{current_path()}) {

    auto ms (matches(entry.path(), pattern));


7. Для каждого совпадения (если они есть) выводим путь к файлу, номер строки и содержимое строки, содержащей совпадение:


    for (const auto &[number, content] : ms) {

      cout << entry.path().c_str() << ":" << number

<< " - " << content << '\n';

    }

  }

}


8. Подготовим файл с именем

"foobar.txt"
, содержащий тестовые строки, по которым можно выполнить поиск:


foo

bar

baz


9. Компиляция и запуск программы дадут следующий результат. Я запустил приложение в каталоге

/Users/tfc/testdir
моего ноутбука и сначала передал ему шаблон
"bar"
. Внутри этого каталога приложение нашло вторую строку в нашем файле
"foobar.txt"
и другом файле
"text1.txt"
, который находится в каталоге
testdir/dir1
:


$ ./grepper bar

/Users/tfc/testdir/dir1/text1.txt:1 - foo bar bla blubb

/Users/tfc/testdir/foobar.txt:2 - bar


10. При повторном запуске приложения с шаблоном

"baz"
оно находит третью строку в нашем примере текстового файла:


$ ./grepper baz

/Users/tfc/testdir/foobar.txt:3 - baz


Как это работает

Создание и использование регулярного выражения с целью фильтрации содержимого файлов — основная цель данного примера. Однако рассмотрим итератор

recursive_directory_iterator
, поскольку этот особый класс итераторов мы применяли для фильтрации файлов, по которым итерируем рекурсивно.

Как и

directory_iterator
,
recursive_directory_iterator
итерирует по элементам каталога. Он делает это рекурсивно, согласно своему названию. При встрече с элементом файловой системы, который является каталогом, он вернет экземпляр типа
directory_entry
для данного пути, а затем зайдет в него, чтобы проитерировать по его потомкам.