Вопрос или проблема
INI файл – это profiles.ini Firefox.
Я хочу напечатать все под [Profile
n
]
, включая заголовки секций, где n
– это неотрицательные целые числа, с пустой строкой между каждой секцией.
Существует также необязательный ключ Default
под [Install*]
, который отличается от ключей Default
в секциях профилей, и я хочу напечатать его, если он присутствует.
По сути, shell-скрипт был просто grep -E 'Default=[^1]' profiles.ini
и grep -A4 '^\[Profile' profiles.ini
раньше (чтобы напечатать ключи и значения Name
, IsRelative
, Path
и необязательный Default
), но это потерпит неудачу, если ключи будут добавлены или удалены, и опция -A
grep не является переносимой.
Вот мое хакерское решение, которое не является идиоматичным или надежным AWK:
/^[[]Profile[0123456789]{1,}[]]$/ {
print
while ((getline) > 0) {
if ($0 ~ /^$/) { # На самом деле нужно прерывать на новых секциях.
print ""
break
} else {
print
}
}
}
/^Default=/ {
print # Путь по умолчанию для профиля, указанный в разделе Install*.
}
Пример входных данных:
[Profile2]
Name=default-test
IsRelative=0
Path=/home/user/ffprofiles/f9bwn86n.default-test
[Profile1]
Name=default
IsRelative=1
Path=x64qf7nv.default
Default=1
[Profile0]
Name=default-release
IsRelative=1
Path=9hv1fbkk.default-release-3426201712696
[General]
StartWithLastProfile=1
Version=2
[Install22379532B4E49482]
Default=9hv1fbkk.default-release-3426201712696
Locked=1
Пример выходных данных:
[Profile2]
Name=default-test
IsRelative=0
Path=/home/user/ffprofiles/f9bwn86n.default-test
[Profile1]
Name=default
IsRelative=1
Path=x64qf7nv.default
Default=1
[Profile0]
Name=default-release
IsRelative=1
Path=9hv1fbkk.default-release-3426201712696
Default=9hv1fbkk.default-release-3426201712696
Как я могу сделать это более лаконично и правильно?
Решение не обязательно должно быть в AWK, но я считаю, что awk более подходит в этом случае, чем sed или любые другие утилиты Unix.
Тем не менее, решение должно быть переносимым и соответствовать POSIX.
Спасибо заранее.
То, что вы описываете, – это структурированный текстовый файл с контекстом.
awk
может (будет) извлекать конкретный раздел, но ему потребуется делать много предположений, которые не основаны на том, как на самом деле работает формат файла, а на том, как конкретный пример, о котором вы думаете, выглядит (начиная с “легких” вопросов, таких как “имеет ли значение регистр” до более интересных вопросов, таких как “как мне справиться с двумя секциями с одинаковым именем и перекрывающимися ключами?”).
Поэтому просто не используйте awk
, sed
или какой-либо другой в первую очередь свободный от контекста подход для разбора чего-то вроде ini файлов. Используйте парсер с пониманием формата.
Формат здесь – это TOML, кстати.
Вы используете posix, что указывает на то, что вы можете использовать компилятор C99 (это инструмент такой же POSIX, как awk
!). Так что вместо парсера TOML на основе awk
, используйте устоявшуюся и хорошо работающую библиотеку TOML. toml-c
– это библиотека, которую вы можете просто разместить рядом со своим .c файлом. Директория examples/
содержит два примера, которые вы можете напрямую адаптировать под свой случай; просто замените toml_parse(char*,…)
на toml_parse_file(FILE*,…)
, и откройте файл, который вы передали как argv[1]
; легко.
Не собираюсь предоставлять полное решение на C99 только потому, что вы думаете, что утилиты POSIX – это способ переносимости – на самом деле это, к сожалению, не так; разные реализации одной и той же утилиты POSIX, как правило, более несовместимы между платформами, чем, скажем, интерпретаторы Python, и на каждой платформе, для которой у вас установлен awk
, очень вероятно, также есть python
.
Говоря о Python, он поставляется со встроенным парсером toml, вот ваш 9-строчный скрипт, включая помощь по использованию, чтобы напечатать все ключи/значения из заданного раздела. Поскольку он выдаст ошибку с ненулевым значением возвращаемого кода, когда вы дадите имя раздела, которого нет в вашем вводе, простая оболочка может быть использована для обработки Profile0
, Profile1
… до тех пор, пока ProfileN+1
больше не существует. Более элегантным было бы действительно сделать это в самом Python, но это оставлено читателю в качестве примера, поскольку это было бы очень специфично, в то время как этот инструмент более общий и полезный:
#!/usr/bin/env python3
import tomllib
from sys import argv, stdin, stderr, exit
if len(argv != 2):
stderr.print(f"USAGE: {argv[0]} SECTION-NAME < inputfile.ini\n")
stderr.print(f"Выводит все пары ключ/значение из раздела в INI\n")
stderr.print(f"(или вообще, файле TOML), разделенных овцой.\n")
exit(127)
ini = tomllib.load(stdin)
section = ini[argv[1]]
for key, value in section.items():
print(f"{key} 🐑 {value}")
awk способен сохранять состояние между строками без необходимости использования getline(). Если вам нужно решение на основе awk, самый простой способ будет следующим:
cat profiles.ini | awk '
/^\[/ {ok=0}
/^\[Install\]$/ {ok=1; header=$0}
ok && /^Default=/ {print header; print}
'
cat profiles.ini | awk '
/^\[/ {ok=0}
/^\[Profile[0-9]*\]$/ {ok=1}
ok {print}
'
Как единый скрипт awk, это может сработать:
/^\[/ {ok=0}
/^\[Profile[0-9]*\]$/ {ok=1}
/^\[Install\]$/ {ok=2; header=$0}
ok==1 {print}
ok==2 && /^Default=/ {print header; print}
Всегда сохраняя заголовок и делая строковое сравнение, может быть понятнее, но также может иметь обратный эффект. (Я вовсе не тестировал эту версию.)
/^\[/ {hdr=""}
/^\[Profile[0-9]*\]$/ {hdr=$0}
/^\[Install\]$/ {hdr=$0}
hdr ~ /\[Profile/ {print}
hdr ~ /\[Install/ && /Default=/ {print hdr; print}
Используя любой awk:
$ awk '
/^\[/ { sect = substr($0,2) }
(sect ~ /^Profile/) || ( (sect ~ /^Install/) && /^Default=/ )
' profiles.ini
[Profile2]
Name=default-test
IsRelative=0
Path=/home/user/ffprofiles/f9bwn86n.default-test
[Profile1]
Name=default
IsRelative=1
Path=x64qf7nv.default
Default=1
[Profile0]
Name=default-release
IsRelative=1
Path=9hv1fbkk.default-release-3426201712696
Default=9hv1fbkk.default-release-3426201712696
Поскольку вы довольны извлечением одного ключевого блока за раз, вы можете обойтись одним awk-командой. Конец каждого блока удобно обозначен другим [key] или концом файла.
Вот команда для извлечения, скажем, [Profile0]:
$ awk '/^\[Profile0/{p=1; print; next} /^\[/{p=0}; p>0{print}' ini.txt
[Profile0]
Name=default-release
IsRelative=1
Path=9hv1fbkk.default-release-3426201712696
или извлечь блок, который начинается с [General]:
$ awk '/^\[Gen/{p=1; print; next} /^\[/{p=0}; p>0{print}' ini.txt
[General]
StartWithLastProfile=1
Version=2
Я использовал [Gen и [Profile0 в этих примерах. Замените их на достаточное количество символов из вашего целевого [key], чтобы уникально идентифицировать блок.
Файл ini.txt – это ваш ввод, т.е.:
$ cat ini.txt
[Profile2]
Name=default-test
IsRelative=0
Path=/home/user/ffprofiles/f9bwn86n.default-test
[Profile1]
Name=default
IsRelative=1
Path=x64qf7nv.default
Default=1
[Profile0]
Name=default-release
IsRelative=1
Path=9hv1fbkk.default-release-3426201712696
[General]
StartWithLastProfile=1
Version=2
[Install22379532B4E49482]
Default=9hv1fbkk.default-release-3426201712696
Locked=1
Ответ или решение
Чтобы напечатать все пары ключ-значение в секциях INI-файла, например, в файле profiles.ini
браузера Firefox, можно использовать язык программирования awk
, который позволяет эффективно обрабатывать текстовые файлы. В данной статье мы разберем пошаговый подход для достижения этой цели, включая создание универсального решения для извлечения данных.
Анализ задачи
INI-файлы являются простыми текстовыми файлами, которые используют секции для группировки параметров. Каждая секция начинается с заголовка, заключенного в квадратные скобки, и содержит пары ключ-значение. В нашем случае нас интересуют секции [ProfileN]
, где N
является неотрицательным целым числом, а также опциональный ключ Default
в секции, начинающейся с [Install*]
.
Подход с использованием awk
Мы можем использовать awk
для обработки нашего файла, чтобы извлечь нужные секции и ключи. Пример простой команды выглядит следующим образом:
awk '
/^\[/ { sect = substr($0, 2, length($0)-2); print; blank=0 }
sect ~ /^Profile[0-9]+$/ { inProfile = 1 }
inProfile {
print;
if (/^$/) { blank = 1 } # Если строка пустая
}
/^\[Install/ {
install_section = 1; header = $0
}
install_section && /^Default=/ {
print header; print;
}
/^\[/ { install_section = 0; inProfile = 0 } # Сброс флагов
' profiles.ini
Объяснение кода
-
Инициализация условий: Команда начинается с условия, которое определяет, если строка начинается с
[
, что означает начало секции, и сохраняет ее заголовок без квадратных скобок. -
Признак секции профиля: Используется регулярное выражение для определения секции профиля. Если мы находимся в одной из таких секций, мы печатаем все строки, пока не встретим пустую строку или другую секцию.
-
Обработка секции установки: Если обнаруживается секция
[Install]
, то проверяется наличие ключаDefault
и печатаем его значение, если он существует. -
Сброс состояния: Как только встречается другая секция, мы сбрасываем все активные флаги.
Вывод
Данный скрипт позволяет выводить пары ключ-значение из нужных секций INI-файла, структурируя вывод таким образом, чтобы он был легко читаемым. Этот подход гибкий и переносимый на многие Unix-подобные системы благодаря использованию стандартного инструмента awk
.
Пример использования
Предположим, у вас есть файл profiles.ini
, содержащий:
[Profile2]
Name=default-test
IsRelative=0
Path=/home/user/ffprofiles/f9bwn86n.default-test
[Profile1]
Name=default
IsRelative=1
Path=x64qf7nv.default
Default=1
Запустив приведенный выше скрипт, вы получите аккуратный вывод всех секций [ProfileN]
и, если существует, ключа Default
из секции установка.
Заключение
Использование awk
для обработки INI-файлов позволяет создавать мощные и гибкие решения для извлечения данных. Наш метод ориентирован на производительность и простоту, что делает его отличным выбором для администраторов и разработчиков. С помощью данных примеров и подходов вы сможете легко адаптировать код под свои нужды и расширить его функционал.
Если у вас возникли вопросы или нужно больше информации, не стесняйтесь обращаться!