Как разобрать и преобразовать INI-файл в переменные массива Bash?

12

Я пытаюсь преобразовать INI-файл в переменные массива Bash. Пример INI, как показано ниже:

[foobar]
session=foo
path=/some/path

[barfoo]
session=bar
path=/some/path

так они становятся:

session[foobar]=foo
path[foobar]=/some/path
session[barfoo]=bar

и так далее.

Прямо сейчас я мог придумать только эту команду

awk -F'=' '{ if ($1 ~ /^\[/) section=$1; else if ($1 !~ /^$/) print $1 section "=" $2 }'

Кроме того, другая проблема заключается в том, что он не учитывает пробелы =. Я думаю, что sed, вероятно, лучше подходит для этой работы, но я не знаю, как хранить и хранить временную переменную для имени раздела в sed.

Так есть идеи, как это сделать?

кремень
источник
Если есть другой эффективный способ сделать это, не стесняйтесь опубликовать свое решение тоже :)
Флинт
Для простого решения проверьте: Как получить значение INI в сценарии оболочки? в стеке потока
Кенорб

Ответы:

10

Gawk принимает регулярные выражения в качестве разделителей полей. Следующее исключает пробелы вокруг знака равенства, но сохраняет их в остальной части строки. Кавычки добавляются вокруг значения, поэтому эти пробелы, если они есть, сохраняются при выполнении присваивания Bash. Я предполагаю, что имена разделов будут числовыми переменными, но если вы используете Bash 4, было бы легко адаптировать это для использования ассоциативных массивов с именами разделов в качестве индексов.

awk -F ' *= *' '{ if ($1 ~ /^\[/) section=$1; else if ($1 !~ /^$/) print $1 section "=" "\"" $2 "\"" }'

Обратите внимание, что вы можете также захотеть удалить пробел, который показывает Халед (только на $ 1 и разделе), так как имена переменных Bash не могут содержать пробелы.

Кроме того, этот метод не будет работать, если значения содержат знаки равенства.

Другим методом будет использование while readцикла Bash и выполнение назначений при чтении файла, использование declareкоторого защищено от большинства вредоносного содержимого.

foobar=1
barfoo=2  # or you could increment an index variable each time a section is found
while IFS='= ' read var val
do
    if [[ $var == \[*] ]]
    then
        section=$var
    elif [[ $val ]]
    then
        declare "$var$section=$val"
    fi
done < filename

Опять же, ассоциативные массивы можно довольно легко поддерживать.

Приостановлено до дальнейшего уведомления.
источник
1
Очень хорошая информация, и мне особенно нравится второй метод, поскольку он использует встроенную функцию bash, а не полагается на внешние команды.
Флинт
@TonyBarganski: Это можно изменить в один вызов AWK вместо того, чтобы пересылать один в другой.
Приостановлено до дальнейшего уведомления.
10

Я бы использовал простой скрипт на python для этой работы, так как он имеет встроенный парсер INI :

#!/usr/bin/env python

import sys, ConfigParser

config = ConfigParser.ConfigParser()
config.readfp(sys.stdin)

for sec in config.sections():
    print "declare -A %s" % (sec)
    for key, val in config.items(sec):
        print '%s[%s]="%s"' % (sec, key, val)

а затем в bash:

#!/bin/bash

# load the in.ini INI file to current BASH - quoted to preserve line breaks
eval "$(cat in.ini  | ./ini2arr.py)"

# test it:
echo ${barfoo[session]}

Конечно, в awk есть более короткие реализации, но я думаю, что это более читабельно и проще в обслуживании.

Михал Шрайер
источник
3
В версиях bash до 4.2 необходимо объявить ассоциированный массив перед его заполнением, например,print "declare -A %s" % (sec)
Felix Eve
2
Вместо eval:source <(cat in.ini | ./ini2arr.py)
Приостановлено до дальнейшего уведомления.
3

Если вы хотите устранить лишние пробелы, вы можете использовать встроенную функцию gsub. Например, вы можете добавить:

gsub(/ /, "", $1);

Это удалит все пробелы. Если вы хотите удалить пробелы в начале или конце токена, вы можете использовать

gsub(/^ /, "", $1);
gsub(/ $/, "", $1);
Халед
источник
Прикольные трюки. Не знал, что есть такая встроенная функция :)
Flint
0

Вот чистое решение Bash.

Это новая и улучшенная версия того, что отправил chilladx:

https://github.com/albfan/bash-ini-parser

Для действительно простого начального примера: после того, как вы загрузите его, просто скопируйте файлы bash-ini-parserи scripts/file.iniв тот же каталог, затем создайте сценарий тестирования клиента, используя пример, который я привел ниже, для этого же каталога.

source ./bash-ini-parser
cfg_parser "./file.ini"
cfg_section_sec2
echo "var2=$var2"
echo "var5[*]=${var5[*]}"
echo "var5[1]=${var5[1]}"

Вот некоторые дополнительные улучшения, которые я сделал в скрипте bash-ini-parser ...

Если вы хотите иметь возможность читать ini-файлы с окончаниями строк Windows, а также с Unix, добавьте эту строку в функцию cfg_parser, следующую сразу за той, которая читает файл:

ini=$(echo "$ini"|tr -d '\r') # remove carriage returns

Если вы хотите читать файлы с ограниченными правами доступа, добавьте эту необязательную функцию:

# Enable the cfg_parser to read "locked" files
function sudo_cfg_parser {

    # Get the file argument
    file=$1

    # If not "root", enable the "sudo" prefix
    sudoPrefix=
    if [[ $EUID -ne 0 ]]; then sudoPrefix=sudo; fi

    # Save the file permissions, then "unlock" the file
    saved_permissions=$($sudoPrefix stat -c %a $file)
    $sudoPrefix chmod 777 $file

    # Call the standard cfg_parser function
    cfg_parser $file

    # Restore the original permissions
    $sudoPrefix chmod $saved_permissions $file  
}
BuvinJ
источник
Пришлось понизить голос из-за chmod 777. Хотя в лучшем случае это ненадежная практика, нет необходимости делать исполняемый файл INI. Лучшим подходом будет использовать sudoчтение файла, а не связываться с разрешениями.
Richlv
@Richlv Хорошо. Я действительно ценю объяснение по итогам голосования. Но это крошечная часть этого, которая имеет минимальное значение для ответа на вопрос в целом. Ответом является ссылка: github.com/albfan/bash-ini-parser . Вместо того, чтобы голосовать целиком, за то, что уже обозначено как необязательная функция-обертка, вы могли бы предложить редактирование.
BuvinJ
0

Всегда предполагая наличие ConfigParser в Python, можно создать вспомогательную функцию оболочки следующим образом:

get_network_value()
{
    cat <<EOF | python
import ConfigParser
config = ConfigParser.ConfigParser()
config.read('network.ini')
print (config.get('$IFACE','$param'))
EOF
}

$IFACEи $paramявляются соответственно разделом параметра.

Затем этот помощник разрешает такие звонки:

address=`param=address get_network_value` || exit 1
netmask=`param=netmask get_network_value` || exit 1
gateway=`param=gateway get_network_value` || exit 1

Надеюсь это поможет!

Матиас Дитер Валлнофер
источник
0

Если у вас есть доступный Git и вы согласны с ограничением невозможности использовать подчеркивания в именах ключей, вы можете использовать его git configкак синтаксический анализатор / редактор INI общего назначения.

Он будет обрабатывать разбор пары ключ / значение по всему =и отбрасывать незначительные пробелы, плюс вы получите комментарии (как ;и #), так и приведение типов в основном бесплатно. Я включил полный рабочий пример для ввода ОП .iniи желаемого вывода (ассоциативные массивы Bash) ниже.

Тем не менее, учитывая конфигурационный файл, как это

; mytool.ini
[section1]
    inputdir = ~/some/dir
    enablesomefeature = true
    enablesomeotherfeature = yes
    greeting = Bonjour, Monde!

[section2]
    anothersetting = 42

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

eval $(git config -f mytool.ini --list | tr . _)

# or if 'eval' skeeves you out excessively
source <(git config -f mytool.ini --list | tr . _)

который создает переменные среды с именами sectionname_variablenameв текущей среде. Это, конечно, работает, только если вы можете верить, что ни одно из ваших значений никогда не будет содержать точку или пробел (более подробное решение см. Ниже).

Другие простые примеры

Извлечение произвольных значений с использованием функции оболочки для сохранения ввода:

function myini() { git config -f mytool.ini; }

Псевдоним тоже будет в порядке, но он обычно не раскрывается в сценарии оболочки [ 1 ], и в любом случае псевдонимы заменяются функциями оболочки «почти для всех целей» [ 2 ], согласно странице руководства Bash .

myini --list
# result:
# section1.inputdir=~/some/dir
# section1.enablesomefeature=true
# section1.enablesomeotherfeature=yes
# section2.anothersetting=42

myini --get section1.inputdir
# result:
# ~/some/dir

С помощью этой --typeопции вы можете «канонизировать» определенные настройки в виде целых чисел, логических значений или путей (автоматически расширяется ~):

myini --get --type=path section1.inputdir  # value '~/some/dir'
# result:
# /home/myuser/some/dir

myini --get --type=bool section1.enablesomeotherfeature  # value 'yes'
# result:
# true

Чуть более надежный, быстрый и грязный пример

Сделайте все переменные mytool.iniдоступными как SECTIONNAME_VARIABLENAMEв текущей среде, сохранив внутренний пробел в значениях ключа:

source <(
    git config -f mytool.ini --list \
      | sed 's/\([^.]*\)\.\(.*\)=\(.*\)/\U\1_\2\E="\3"/'
)

То, что делает выражение sed, на английском языке, это

  1. найти группу непериодических символов до периода, помня это как \1, затем
  2. найти группу символов до знака равенства, помня, что как \2, и
  3. найти все символы после знака равенства как \3
  4. наконец, в строке замены
    • имя раздела + имя переменной в верхнем регистре, и
    • часть значения заключена в двойные кавычки, если она содержит символы, которые имеют особое значение для оболочки, если она не заключена в кавычки (например, пробел)

Последовательности \Uи \Eв строке замены (верхний регистр этой части строки замены) являются sedрасширением GNU . В macOS и BSD вы бы просто использовали несколько -eвыражений для достижения одного и того же эффекта.

Работа со встроенными кавычками и пробелами в названиях разделов (что git configпозволяет) оставляется в качестве упражнения для читателя.:)

Использование имен разделов в качестве ключей в ассоциативном массиве Bash

Данный:

; foo.ini
[foobar]
session=foo
path=/some/path

[barfoo]
session=bar
path=/some/path

Это даст результат, который запрашивает OP, просто переставив некоторые записи в выражении замены sed, и будет работать без GNU sed:

source <(
    git config -f foo.ini --list \
      | sed 's/\([^.]*\)\.\(.*\)=\(.*\)/declare -A \2["\1"]="\3"/'
)

Я предполагаю, что при цитировании реального .iniфайла могут возникнуть некоторые проблемы , но это работает для приведенного примера. Результат:

declare -p {session,path}
# result:
# declare -A session=([barfoo]="bar" [foobar]="foo" )
# declare -A path=([barfoo]="/some/path" [foobar]="/some/path" )
Чувак пребывает
источник