Реализация утилиты cat на C
Вступление
Программисты часто используют встроенные команды unix для решения задач. Давайте реализуем cat. cat — утилита которая объединяет файлы и выводит их в стандартный вывод.
Цель
Идея довольно проста — принять файл в качестве аргумента, открыть его, занести в буфер по частям и вывести на стандартный вывод. Утилита cat также оснащена различными флагами опций, которые позволяют пользователю манипулировать буфером перед его отправкой на стандартный вывод.
Примечание
Каждая команда Unix снабжена собственной справочной страницей. Страница руководства документирует различные варианты использования, флаги опций, примеры и другие данные, важные для поведения программы. В новом окне терминала запустите:
man cat
Здесь у нас есть все, что нам нужно, чтобы понять, как работает команда cat. Давайте пройдемся по тому, что мы узнали.
Cat — это сокращение от «объединить и распечатать файлы», и его использование выглядит следующим образом:
cat [-benstuv] [FILE ...]
Первый аргумент показывает различные флаги опций, которые можно применять для управления буфером перед его выводом на стандартный вывод. В разделе описания мы можем увидеть предполагаемое поведение каждого флага в буфере. Попробуем добавить пару таких флагов в нашу собственную реализацию. Все дополнительные аргументы после любых примененных флажков параметров считаются файлами, которые следует записывать в стандартный вывод. Команда cat способна читать несколько файлов и выводить их содержимое последовательно в том порядке, в котором они были предоставлены. Если в качестве аргументов не указаны файлы, команда cat будет читать со стандартного ввода.
Давайте начнем
Теперь, когда у нас есть хорошее представление о том, как должна вести себя команда cat, давайте поэтапно реализуем некоторые ключевые функции. В новом окне терминала создадим каталог для нашего проекта, создадим новый файл C и дадим ему базовую основную функцию, способную принимать аргументы командной строки.
int main(int argc, char **argv) {
return 0;
}
Для начала напишем функцию которая просто откроет и выведет файл.
void print_file(char *name) {
FILE *f = fopen(name, "rt");
if (f != NULL) {
int c = fgetc(f);
while (c != EOF) {
putc(c);
c = fgetc(f);
}
fclose(f);
}
}
Реализовать ввод нескольких файлов можно следующим образом: вызывать нашу функцию print_file в цикле для каждого аргумента.
int main(int argc, char **argv) {
for (int i = 1; i < argc, i++) { // цикл начинается с argv[1]
print_file(argv[i]); // в argv[0] находится "./s21_cat"
}
return 0;
}
./s21_cat 1.txt 2.txt 3.txt
Такой вызов программы выведет содержимое файлов 1.txt, 2.txt, 3.txt на стандартный вывод.
Теперь можно реализовать манипулирование выводом. Для этого придется распарсить аргументы, достать из них флаги опции, и где-нибудь их сохранить.
Для парсинга аргументов можно воспользоваться готовыми решениями, такими как getopt, но для интереса реализуем парсер самостоятельно. Хранить распарсенные флаги я решил в строке, в массиве символов.
Алгоритм такой: проходимся по аргументам, если это флаги, проходимся по каждому символу этого флага и соответствующие ему простые флаги добавляем в массив символов. Например, флагу 't' соответствуют 2 простых «vT», а флагу 'T' соответствует один «T». Флаг и соотвествующие ему простые флаги хранятся в массиве структур. Также парсер принимает переменную index по указателю и записывает в неё индекс последнего аргумента — опции.
int main(int argc, char **argv) {
char flags[7] = "\0"; // массив флагов
int index_end_flags = 1;
flags_parser(flags, argc, argv, &index_end_flags);
return 0;
}
void flags_parser(char *flags, int argc, char **argv, int *index) {
// пройдемся по всем аргументам, кроме 1
for (int i = 1; i < argc; i++) {
// если аргумент начинается не на '-'
// или является строкой "-" или "--" ("--" сигнал что опции кончились)
// считаем что флаги опций кончились, и начались имена файлов
if (argv[i][0] != '-' || strcmp(argv[i], "--") == 0 ||
strcmp(argv[i], "-") == 0 ) {
break;
} else {
*index = i;
// флаги могут быть написаны слитно, например: -bE
// поэтому проходимся по каждому символу аргумента кроме 1
for (size_t j = 1; j < strlen(argv[i]); j++) {
append_flags(flags, argv[i][j]);
}
}
}
}
struct s_avi_flags {
char flag; // флаг
char *equivalent_flags; // эквивалентные простые флаги
};
void append_flags(char *flags, char flag) {
// доступные флаги
int err_code = 1;
struct s_avi_flags avi_flags[8] = {{'b', "b"}, {'E', "E"}, {'e', "Ev"},
{'n', "n"}, {'s', "s"}, {'T', "T"},
{'t', "Tv"}, {'v', "v"}};
for (int i = 0; i < 8; i++) {
if (avi_flags[i].flag == flag) {
for (size_t j = 0; j < strlen(avi_flags[i].equivalent_flags); j++) {
append_flag(flags, avi_flags[i].equivalent_flags[j]);
}
err_code = 0;
break;
}
}
}
// добавить 1 флаг в массив флагов
void append_flag(char *flags, char flag) {
if (strchr(flags, flag) == NULL) { // если такого флага нет
char temp[2] = "-"; // создаем временную строку
temp[0] = flag; // например ['v', '\0']
strcat(flags, temp); // объеденяем её со строкой флагов
}
}
Этот парсер можно усовершенствовать, добавить поддержку длинных GNU флагов (»--number»), добавить обработки ошибок, начать подавать информацию о доступных флагах в аргументе функции.
Теперь, когда у нас есть флаги опций, можно реализовать манипулирование выводом. Реализуем 6 флагов: b E n s T v.
s — сжимает несколько пустых строк.
n — нумерует каждую строку
b — нумерует каждую не пустую строку (аннулирует действие флага n)
E — выводит '$' перед символом '\n'
T — выводит специальный символ '\t' в виде »^» нотации
v — выводит все специальные символы, кроме '\n' и '\t' в виде »^» нотации.
Специальный символ в виде »^» нотации выглядит как: символ '^' и сразу за ним соответствующий символ из таблицы ascii со сдвигом на 64. Например символ '\t' будет выглядеть как »^I»
Для обработки флага s добавим буфер для предыдущего символа и флаг означающий была ли до этого выведена пустая строка. Буфер prev изначально равен '\n', чтобы обрабатывать ситуацию когда файл начинается с пустых строк. Алгоритм такой: если включена опция s и пустая строка уже была выведена и текущая строка — пустая, то пропускаем символ. Далее ставим значение флагу была ли выведена пустая строка. Далее если включен флаг 'n', увеличиваем счетчик в начале пустой строки и сразу его выводим. Если флаг b, то делаем тоже самое, но только если текущая строка не пустая. Считаем что началась новая строка когда предыдущий символ — '\n', считаем что текущая строка пустая когда предыдущий и текущий символы — '\n'. Для флагов E, T, v просто выводим нужный символ и изменяем текущий.
typedef int bool; // псевдоним для типа int, для удобства
int print_file(char *name, char *flags) {
int err_code = 0;
FILE *f = fopen(name, "rt");
if (f != NULL) {
int index = 0;
bool eline_printed = 0;
int c = fgetc(f), prev = '\n';
while (c != EOF) {
print_symb(c, &prev, flags, &index, &eline_printed);
c = fgetc(f);
}
fclose(f);
} else {
err_code = 1;
}
return err_code;
}
void print_symb(int c, int *prev, char *flags, int *index, bool *eline_printed) {
// если s и это пустая строка и пустая строка уже была выведена, пропустим, сюда не зайдем
if (!(strchr(flags, 's') != NULL && *prev == '\n' && c == '\n' && *eline_printed)) {
if (*prev == '\n' && c == '\n') *eline_printed = 1;
else *eline_printed = 0;
// если ( (n без b) или (b и текущий символ не '\n') ) и пред символ '\n'
if (((strchr(flags, 'n') != NULL && strchr(flags, 'b') == NULL) || (strchr(flags, 'b') != NULL && c != '\n')) && *prev == '\n') {
*index += 1;
printf("%6d\t", *index);
}
if (strchr(flags, 'E') != NULL && c == '\n') printf("$");
if (strchr(flags, 'T') != NULL && c == '\t') {
printf("^");
c = '\t' + 64;
}
if (strchr(flags, 'v') != NULL) {
printf("^");
c = c + 64;
}
fputc(c, stdout);
}
*prev = c;
}
Алгоритм обработки флага v можно усовершенствовать, добавив поддержку «M-» нотации для символов с кодами [128,159].
if (strchr(flags, 'v') != NULL) {
if (c > 127 && c < 160) printf("M-^");
if ((c < 32 && c != '\n' && c != '\t') || c == 127) printf("^");
if ((c < 32 || (c > 126 && c < 160)) && c != '\n' && c != '\t') c = c > 126 ? c - 128 + 64 : c + 64;
}
Так же утилита cat может считывать содержимое стандартного ввода, для этого среди имен файлов ей нужно подать »-» или не подать ни одного файла.
cat 1.txt - 2.txt # выводит содержимое 1.txt, стандартный ввод, 2.txt
cat # выводит содержимое стандартного ввода
Реализовать это можно следующим образом: в функции print_file переменной f присваивать не файл, а stdin. Если индекс последнего аргумента опции совпадает с индексом последнего аргумента — не был подан не один файл, в таком случае вызовем print_line с аргументом имя файла »-».
int main(int argc, char **argv) {
char flags[7] = "\0";
int index_end_flags = 1;
flags_parser(flags, argc, argv, &index_end_flags);
// если не было подано файла
if (index_end_flags == arg_count - 1) print_file("-", flags);
for (int i = index_end_flags + 1; i < argc, i++) {
if (strcmp(args[i], "--") == 0) continue;
print_file(argv[i], flags);
}
return 0;
}
int print_file(char *name, char *flags) {
int err_code = 0;
FILE *f;
// если имя файла "-" работаем с stdin
if (strcmp("-", name) == 0) f = stdin;
else f = fopen(name, "rt");
if (f != NULL) {
int index = 0;
bool eline_printed = 0;
int c = fgetc(f), prev = '\n';
while (c != EOF) {
print_symb(c, &prev, flags, &index, &eline_printed);
c = fgetc(f);
}
if (f != stdin) fclose(f); // закрываем файл только если он не stdin
} else {
err_code = 1;
}
return err_code;
}
Заключение
Данный проект я реализовал являясь учеником «School 21».
Автор
Соавтор