Мне нужно проанализировать строки, возвращаемые utmpdump /var/log/wtmp
. Они в таком формате:
[8] [13420] [ ] [ ] [pts/3 ] [ ] [0.0.0.0 ] [2024-07-22T11:18:29,836564+00:00]
[7] [13611] [ts/3] [john ] [pts/3 ] [192.168.1.38 ] [192.168.1.38 ] [2024-07-22T11:21:30,065856+00:00]
[8] [13611] [ ] [ ] [pts/3 ] [ ] [0.0.0.0 ] [2024-07-22T11:21:41,814051+00:00]
Характеристика заключается в том, что:
[]
,\0
),Как разобрать такую строку на 8 переменных char*
с помощью sscanf
конкретно? Проблема, которую я не могу решить, заключается в том, как обрезать конечные пробелы и в то же время позволить промежуточной строке []
изменять длину. Возможно ли это вообще с sscanf
?
У меня есть рабочее решение без sscanf
, но это нужно сделать специально с sscanf
. Обрезка пробелов после того, как sscanf
выполняет синтаксический анализ, является резервным решением, но я пытаюсь сделать это с помощью sscanf
полного анализа.
Обновлено: Основываясь на предоставленных ответах, я ищу решение sscanf «1 строка». Этот код дает сбой, если есть пустые столбцы, как указано в комментариях:
#include <stdio.h>
#define BUF_LEN (64)
int main (void)
{
const char* const line = "[8] [13420] [ ] [ ] [pts/3 ] [ ] [0.0.0.0 ] [2024-07-22T11:18:29,836564+00:00]";
size_t record_num = 0;
size_t pid = 0;
char session_type[BUF_LEN] = { 0 };
char username[BUF_LEN] = { 0 };
char terminal[BUF_LEN] = { 0 };
char source_ip[BUF_LEN] = { 0 };
char dest_ip[BUF_LEN] = { 0 };
char timestamp[BUF_LEN] = { 0 };
sscanf(line, "[%zu] [%zu] [%64[^] ] ] [%64[^] ] ] [%64[^] ] ] [%64[^] ] ] [%64[^] ] ] [%64[^] ] ]",
&record_num, &pid, session_type, username, terminal, source_ip, dest_ip, timestamp);
printf("Record number: %zu \n", record_num);
printf("Pid: %zu \n", pid);
printf("Session type: %s \n", session_type);
printf("User name: %s \n", username);
printf("Terminal: %s \n", terminal);
printf("Source IP: %s \n", source_ip);
printf("Destination IP: %s \n", dest_ip);
printf("Timestamp: %s \n", timestamp);
return 0;
}
Выход:
Record number: 8
Pid: 13420
Session type:
User name:
Terminal:
Source IP:
Destination IP:
Timestamp:
Есть ли способ исправить формат для учета пустых столбцов без необходимости анализировать каждый столбец с помощью sscanf
индивидуально?
Вы должны показать, что вы пробовали. Затем ответы можно будет сформулировать на уровне, который вам будет лучше всего понятен.
Я бы рекомендовал написать собственную функцию вместо использования sscanf
для чего-то, что не предназначено для простой и полной реализации.
У меня есть рабочее решение путем анализа строки вручную. Однако мне сказали, что это можно сделать только с помощью sscanf. У меня нет работающего решения sscanf, поскольку я не знаю, как отформатировать часть синтаксического анализа, поскольку с моей стороны это кажется невозможным с учетом предоставленных требований. Сейчас я пытаюсь выяснить, действительно ли это невозможно или это действительно возможно.
Конечно, это можно сделать с помощью sscanf
, но это нетривиально. Простой метод «ручного анализа» может быть легче читать, понимать и поддерживать.
Моя лучшая попытка: ideone.com/1Kq6Ae TIL scanf
не любит пустые строки :-)
I do have a working solution by manually parsing the string
Сохраняйте. I need to parse lines
Почему бы не использовать существующую библиотеку для анализа файла utmp.h
? utmpdump
написано на C, зачем анализировать вывод utmpdump
, почему бы не анализировать файл? (где «разобрать» на самом деле просто fread(struct utmp
, вот и все). Глядя на исходники, utmpdump
есть функция undump github.com/util-linux/util-linux/blob/master/login-utils/…, которая может быть вам интересна. Наконец, это не вопрос XY? Вас действительно волнует использование именно sscanf?
Похоже, " [%1023[^]]]"
делает свое дело (где буфер имеет размер 1024). Просто обрежьте пробелы постфактум.
@KamilCuk причина в том, что код построен с использованием другой стандартной библиотеки C, отличной от той, которая используется по умолчанию для целевой системы. Было проще просто проанализировать текст, поскольку упомянутый вами подход я пробовал вначале и не смог прочитать какие-либо данные. Кроме того, что касается XY, мне специально сказали, что это можно сделать с помощью просто sscanf - я просто хочу доказать, что это не так (или, если это так, узнайте, как это сделать).
@WilliamPursell Я знаю, что после вызова sscanf можно обрезать пробелы. Речь идет конкретно о том, чтобы доказать, возможно ли это с помощью одного вызова sscanf или нет...
Формат " [ %N[^]]]"
с N — максимальный размер поля, включая \0
, может читать каждую строку и удалять пробелы в заголовках. Но удалить конечные пробелы невозможно в sscanf()
, необходим еще один шаг по их удалению.
@dalfaB я тоже это узнал. Спасибо.
Если строка не содержит пробелов, " [%1023[^] ] ]"
удалит конечные пробелы. %1023[^] ]
читается до тех пор, пока не встретится пробел или ]
. Тогда следующий ` ]` будет фильтровать любые пробелы и соответствовать последнему ]
.
@WeatherVane, хорошо, я думаю, что в строках не должно быть пробелов. Попробую это, спасибо.
@WeatherVane: предлагаемый вами формат не работает для полей, содержащих только пробелы. %1023[^] ]
завершится ошибкой, а остальная часть строки формата ` ]` будет проигнорирована. Именно по этой причине индивидуальный подход предпочтительнее.
@chqrlie в моем тесте входная строка " [ ] "
выдает на выходе ""
. Сканирование останавливается на первом замкнутом пространстве, затем фильтруется остальное.
@chqrlie Понятно, преобразование не выполняется. Если входные данные — " [ ] [abc ]"
, а строка формата содержит то, что я написал дважды, например sscanf(str, " [%1023[^] ] ] [%1023[^] ] ]"
, то второе значение также не преобразуется.
@dalfaB «Но удаление конечных пробелов невозможно в sscanf()» --> Коду не нужно их удалять, просто просканируйте их.
Сделайте шаг назад и подумайте, как [s]scanf()
обрабатывает свой спецификатор формата для строки. Он (обычно) начинается с отбрасывания начальных пробелов, а затем по одному символу сопоставляется со спецификатором. В вашей проблеме вы хотите, чтобы он прекратил «потреблять» и назначал либо (конечный) SP
, либо ']'
, а затем, при необходимости, потреблял больше SP, пока не будет найден определенный ']'
. Несмотря на свои возможности, функция на это не способна. «Один экземпляр scanf()
» не даст вам желаемых результатов. Тот, кто сказал вам, что это возможно, ошибся (или использовал альтернативное значение слова «один»).
Используйте "%n"
, чтобы определить смещение частей сканирования.
Используйте " "
, чтобы просмотреть необязательные пробелы.
*scanf()
не любит сканировать/формировать строки нулевой длины, поэтому при сканировании столбца рассматривайте [
как часть строки, чтобы гарантировать наличие хотя бы 1 символа в строке. Позже начните поле, которое прошло '['
.
#include <stdio.h>
#define FN 8
char *cut_up_line(char *offset[FN], char * line) {
for (int i = 0; i < FN; i++) {
int field_end;
int column_end = 0;
// If possible for the line to include unusually white-spaces,
// adjust format to include them: "%*[^] \t\r\v\f]%n ] %n"
sscanf(line, "%*[^] \t]%n ] %n", &field_end, &column_end);
if (column_end == 0) {
// debug: printf("-%d <%s>\n", i, line);
return NULL;
}
// Maybe add test here that *line is a '['.
offset[i] = line + 1;
line[field_end] = '\0';
line += column_end;
// debug: printf("+%d <%s>\n", i, offset[i]);
}
return line;
}
void test(char *line) {
printf("<%s>\n", line);
char *field[FN];
if (cut_up_line(field, line) == NULL) {
printf("** Failed **\n");
} else {
for (int i = 0; i < FN; i++) {
printf(" %d:<%s>\n", i, field[i]);
}
}
}
int main() {
char s1[] = "[8] [13420] [ ] [ ] [pts/3 ] [ ] [0.0.0.0 ] [2024-07-22T11:18:29,836564+00:00]";
char s2[] = "[7] [13611] [ts/3] [john ] [pts/3 ] [192.168.1.38 ] [192.168.1.38 ] [2024-07-22T11:21:30,065856+00:00]";
char s3[] = "[8] [13611] [ ] [ ] [pts/3 ] [ ] [0.0.0.0 ] [2024-07-22T11:21:41,814051+00:00]";
test(s1);
test(s2);
test(s3);
}
Выход
<[8] [13420] [ ] [ ] [pts/3 ] [ ] [0.0.0.0 ] [2024-07-22T11:18:29,836564+00:00]>
0:<8>
1:<13420>
2:<>
3:<>
4:<pts/3>
5:<>
6:<0.0.0.0>
7:<2024-07-22T11:18:29,836564+00:00>
<[7] [13611] [ts/3] [john ] [pts/3 ] [192.168.1.38 ] [192.168.1.38 ] [2024-07-22T11:21:30,065856+00:00]>
0:<7>
1:<13611>
2:<ts/3>
3:<john>
4:<pts/3>
5:<192.168.1.38>
6:<192.168.1.38>
7:<2024-07-22T11:21:30,065856+00:00>
<[8] [13611] [ ] [ ] [pts/3 ] [ ] [0.0.0.0 ] [2024-07-22T11:21:41,814051+00:00]>
0:<8>
1:<13611>
2:<>
3:<>
4:<pts/3>
5:<>
6:<0.0.0.0>
7:<2024-07-22T11:21:41,814051+00:00>
#define FMT_ALL_BUT_RBRACKET_WSPACE "%*[^] \t\r\n\f\v]"
...
sscanf(line, FMT_ALL_BUT_RBRACKET_WSPACE "%n ] %n", &field_end, &column_end);
line
, скорее всего, доступен для записи, поэтому токенизированные поля сохраняются в line
путем добавления нулевых символов. Это позволяет избежать проблем с переполнением буфера.UV за смелость написать это, когда strtok()
и некоторые друзья (или одно из нескольких других решений) выполнили бы работу без сложностей с псевдорегулярными выражениями... Ура! :-)
Основываясь на вашем решении, можно ли сделать лайнер sscanf
` для одновременного анализа всех 8 переменных?
@ŁukaszPrzeniosło Да. Повторите "%*[^] \t]%n ] "
8 раз. Хотя это не элегантно.
@chux-ReinstateMonica это анализируется неправильно. Предполагая, что первые два элемента — это size_t
, имели ли вы в виду этот синтаксис? [%zu] [%zu] [%64[^] ] ] [%64[^] ] ] [%64[^] ] ] [%64[^] ] ] [%64[^] ] ] [%64[^] ] ]
@ŁukaszPrzeniosło, формат, который я имею в виду в этом ответе, определяет смещения в строке (int
) начала поля и его конца. Чтобы получить начальное смещение минус 1 и конечное смещение, используйте "%n%*[^] \t]%n ] "
8 раз. Почему вы хотите использовать один вызов sscanf()
, а не цикл?
@chux-ReinstateMonica Для простоты я хотел использовать 1 вызов. Теперь я понял вашу идею, спасибо.
@ŁukaszPrzeniosło "для простоты используйте 1 вызов." --> Цикл становится более понятным, его легче писать правильно и его легче поддерживать. Удачи.
@chux-ReinstateMonica В целом я согласен, но если идти по этому пути, становится еще более очевидным, что несколько лучше анализировать его вручную.
Вот простой пользовательский парсер для этого формата:
#include <stdio.h>
// split the line and populate `fields` with pointers to trimmed contents.
int split_log(char *fields[], int n, char *line) {
char *p = line;
char *tail;
int i;
for (i = 0; i < n; i++) {
while (*p == ' ')
p++;
if (*p != '[')
break;
p++;
fields[i] = p;
for (tail = p; *p && *p != ']'; p++) {
if (*p != ' ')
tail = p + 1;
}
if (*p != ']')
break;
*tail = '\0';
p++;
}
return i;
}
void print_log(char *fields[], int n) {
for (int i = 0; i < n; i++)
printf("field%d = '%s'\n", i + 1, fields[i]);
printf("\n");
}
int test_split(char *line, int n) {
char *fields[8];
int n1 = split_log(fields, n, line);
printf("split_log found %d fields:\n", n1);
print_log(fields, n1);
return n == n1;
}
int main(void) {
char line1[] = "[8] [13420] [ ] [ ] [pts/3 ] [ ] [0.0.0.0 ] [2024-07-22T11:18:29,836564+00:00]\n";
char line2[] = "[7] [13611] [ts/3] [john ] [pts/3 ] [192.168.1.38 ] [192.168.1.38 ] [2024-07-22T11:21:30,065856+00:00]\n";
char line3[] = "[8] [13611] [ ] [ ] [pts/3 ] [ ] [0.0.0.0 ] [2024-07-22T11:21:41,814051+00:00]\n";
test_split(line1, 8);
test_split(line2, 8);
test_split(line3, 8);
return 0;
}
Выход:
split_log found 8 fields:
field1 = '8'
field2 = '13420'
field3 = ''
field4 = ''
field5 = 'pts/3'
field6 = ''
field7 = '0.0.0.0'
field8 = '2024-07-22T11:18:29,836564+00:00'
split_log found 8 fields:
field1 = '7'
field2 = '13611'
field3 = 'ts/3'
field4 = 'john'
field5 = 'pts/3'
field6 = '192.168.1.38'
field7 = '192.168.1.38'
field8 = '2024-07-22T11:21:30,065856+00:00'
split_log found 8 fields:
field1 = '8'
field2 = '13611'
field3 = ''
field4 = ''
field5 = 'pts/3'
field6 = ''
field7 = '0.0.0.0'
field8 = '2024-07-22T11:21:41,814051+00:00'
Как разобрать такую строку на 8 переменных
char *
с помощьюsscanf
конкретно?
Предполагая, что все поля содержат хотя бы один символ, возможно, пробел, вышеуказанную функцию можно реализовать с помощью sscanf
для большей части работы, но я боюсь, что код менее читабелен и его труднее понять:
// split the line and populate `fields` with pointers to trimmed contents.
int split_log(char *fields[], int n, char *line) {
char *p = line;
char open[2], close[2];
int i, begin, end;
for (i = 0; i < n; i++) {
if (sscanf(p, " %1[[]%n%*[^]]%n%1[]]", open, &begin, &end, close) != 2)
break;
fields[i] = p + begin;
while (p[end - 1] == ' ')
end--;
p[end] = '\0';
p += end + 1;
}
return i;
}
Проблема, которую я не могу решить, заключается в том, как обрезать конечные пробелы и в то же время позволить промежуточной строке
[]
изменять длину. Возможно ли это вообще сsscanf
?
Как видно из приведенного выше кода, обрезка по-прежнему выполняется с помощью специального кода. Не существует простого способа обрезать конечные пробелы с помощью sscanf
, однако можно использовать sscanf
запутанным способом, чтобы избежать ручной проверки содержимого буфера и, таким образом, еще больше запутать код:
// while (p[end - 1] == ' ')
// end--;
while (sscanf(p + end - 1, "%1[ ]", open))
end--;
Однако если содержимое поля содержит не более одного слова, вы можете использовать sscanf
для вычисления длины этого слова:
int split_log(char *fields[], int n, char *line) {
char *p = line;
char open[2], close[2];
int i, begin, end, len;
for (i = 0; i < n; i++) {
if (sscanf(p, " %1[[]%n%*[^]]%n%1[]]", open, &begin, &end, close) != 2)
break;
fields[i] = p + begin;
p[end] = '\0';
len = 0;
sscanf(p + begin, "%*s%n", &len);
p[begin + len] = '\0';
p += end + 1;
}
return i;
}
sscanf
— мощный инструмент, но полный особенностей и подводных камней. Его легко использовать неправильно даже для простых задач синтаксического анализа. В таких случаях написание собственного кода — более безопасный способ получить правильные результаты.
Обязательно ли это делать с помощью
sscanf
? Почему? Каково ваше фактическое задание? Каковы его ограничения и требования?