bash ini parser

In some situations i like to use INI files as configuration files, as python do. But bash do not provide a parser for these files, obviously you can use a awk code or a couple of sed calls, but if you are bash-priest and do not want to use nothing more, then you can try the following obscure code:

Update:Added support spaces between keys and values, described in comment #364

Update:Added a writer function, described in comment #370 with examples.

Update:Fix bug when values or keys contains a ‘#’ sign, as described in comment #397. Now only comments at the beginning of line are allowed.

Update:Fix bug in section evaluation when a file which match section expression exists in cwd, as described in comment #436. Fixed also a bug related with
parser when brackets are escaped (read #437.

cfg_parser ()
{
	ini="$(<$1)"                # read the file
	ini="${ini//[/\[}"          # escape [
	ini="${ini//]/\]}"          # escape ]
	IFS=$'\n' && ini=( ${ini} ) # convert to line-array
	ini=( ${ini[*]//;*/} )      # remove comments with ;
	ini=( ${ini[*]/\	=/=} )  # remove tabs before =
	ini=( ${ini[*]/=\	/=} )   # remove tabs be =
	ini=( ${ini[*]/\ =\ /=} )   # remove anything with a space around =
	ini=( ${ini[*]/#\\[/\}$'\n'cfg.section.} ) # set section prefix
	ini=( ${ini[*]/%\\]/ \(} )    # convert text2function (1)
	ini=( ${ini[*]/=/=\( } )    # convert item to array
	ini=( ${ini[*]/%/ \)} )     # close array parenthesis
	ini=( ${ini[*]/%\\ \)/ \\} ) # the multiline trick
	ini=( ${ini[*]/%\( \)/\(\) \{} ) # convert text2function (2)
	ini=( ${ini[*]/%\} \)/\}} ) # remove extra parenthesis
	ini[0]="" # remove first element
	ini[${#ini[*]} + 1]='}'    # add the last brace
	eval "$(echo "${ini[*]}")" # eval the result
}

cfg_writer ()
{
    IFS=' '$'\n'
    fun="$(declare -F)"
    fun="${fun//declare -f/}"
    for f in $fun; do
        [ "${f#cfg.section}" == "${f}" ] && continue
        item="$(declare -f ${f})"
        item="${item##*\{}"
        item="${item%\}}"
        item="${item//=*;/}"
        vars="${item//=*/}"
        eval $f
        echo "[${f#cfg.section.}]"
        for var in $vars; do
            echo $var=\"${!var}\"
        done
    done
}

And then you can parse your ini files as following:

# parse the config file called 'myfile.ini', with the following
# contents::
#   [sec2]
#   var2='something'
cfg.parser 'myfile.ini'

# enable section called 'sec2' (in the file [sec2]) for reading
cfg.section.sec2

# read the content of the variable called 'var2' (in the file
# var2=XXX). If your var2 is an array, then you can use
# ${var[index]}
echo "$var2"

Unfortunately, the cfg.parser() function do no support embedded spaces
in section names… yet

79 comments

  1. Uh-oh:
    eval: line 2: syntax error near unexpected token `}’
    eval: line 2: `}’

    Hopefully this is just a typo in the code above as I’d be super happy to get this to work and I’m just not quite good enough to figure out what is wrong.

    Thanks for posting this!

  2. Mmm, the code works for me :D

    There are my ‘myfile.ini’:

    [~]$ cat myfile.ini
    [sec2]
    var2=XXX

    And the result:
    [~]$ ./ini.sh
    XXX

    Maybe you need bash >= 3.0?, I’m using:

    [~]$ echo ${BASH_VERSION}
    3.2.17(1)-release

    If you want, send me the result of the command:

    $ bash -x ini.sh

    And I try to help you :)

    Thanks for your comments!! :D

  3. This script also doesn’t work if the ini file is not fully standard, like samba’s smb.conf. In that, there are some white spaces on both sides of the equation sign.

  4. Sure, the script do not work fine with non stardard files, becaus the script parse file variables as bash variables and spaces are not allowed in bash variables. But, you are free to replace spaces with another character. :)

  5. This works great!

    Do you have any functionality for updating the functions array dynamically without re-parsing the ini-file?

    Example: Add new section or add/update field within a section.

    I’m considering trying to code this, but it would of course save me lots of time if it already exists :-)

  6. Sorry, I don’t code any functions to update the array. Really this hack do not use an array. We read the ini and create a couple of variables with the same name as variable in the file, and change the section creating a function called the section name. So if you need to add a new section, you might do:

    cfg.section.MYNEWSECTION ()
    {
    MYNEWVAR1=”MYNEWVALUE1″
    # any other variable
    }

    Now I’m working in a dump function to write the ini from specified functions.

  7. Hi,
    I’ve only copy the function in my bash script and I receive the following error:
    ‘cfg.parser’:not a valid identifier

    someone know why?

    BASH_VERSION -> 3.1.17(1)

  8. Yes, probably your bash is running with POSIX option (check the value of $HELLOPTS), in this case the dot (.) is not a valid character for function name.

    If you really require a POSIX shell compatibility, you can replace cfg.parser and cfg.sections by cfg_parser and cfg_sections, the underline is always a valid character :D

  9. The source listing is somehow garbled. There’s something wrong with quotes (backticks?). The show up as a single character (&#8221 resp. &#8220=

  10. The following will parse out a file that has spaces between the ‘=’ as well as both ; and # comments. Just thought I would share it.

    function cfg.parser ()
    {
    IFS=$’\n’ && ini=( $(<$1) ) # convert to line-array
    ini=( ${ini[*]//;*/} ) # remove comments ‘;’
    ini=( ${ini[*]//\#*/} ) # remove comments ‘#’
    ini=( ${ini[*]/\ =\ /=} ) # remove anything with a space around ‘ = ‘
    ini=( ${ini[*]/#[/\}$'\n'cfg.section.} ) # set section prefix
    ini=( ${ini[*]/%]/ \(} ) # convert text2function (1)
    ini=( ${ini[*]/=/=\( } ) # convert item to array
    ini=( ${ini[*]/%/ \)} ) # close array parenthesis
    ini=( ${ini[*]/%\( \)/\(\) \{} ) # convert text2function (2)
    ini=( ${ini[*]/%\} \)/\}} ) # remove extra parenthesis
    ini=( ${ini[*]/#\ */} ) # remove blank lines
    ini=( ${ini[*]/#\ */} ) # remove blank lines with tabs
    ini[0]=” # remove first element
    ini[${#ini[*]} + 1]=’}’ # add the last brace
    #printf “%s\n” ${ini[*]}
    eval “$(echo “${ini[*]}”)” # eval the result
    }

  11. Thanks for your script!

    Is there any chance to get the available variables of a section?

    For example: if I know that there’s a section [sec2], how can I print out the content of this section?

    I think I need something like this:

    for var in cfg.section.sec2; do
    print “variable: %s\n” $var;
    print “value: %s\n” $var[*];
    done

    but that doesn’t work for me.

  12. Just a quick question… At one point I had this working, however, now all but the first section has been replaced by the number ’1′. It’s replace right after this line:

    IFS=$’\n’ && ini=( $(<$1) )

    I’m using bash 3.0. Any thoughts?

    Thanks for the help.

  13. One more fix: script changes $IFS and doesn’t restore previous value. Took me quote a moment of debugging, why the hell after reading config word splitting doesn’t work as they used to (e.g. LS=”ls -a”;$LS doesn’t work, because bash tries to find and execute “ls -a” command, including space and parameter). Fixed version:

    function cfg.parser ()
    {
    _old_IFS=”$IFS” # save $IFS
    IFS=$’\n’ && ini=( $(<$1) ) # convert to line-array
    ini=( ${ini[*]//;*/} ) # remove comments `;’
    ini=( ${ini[*]//\#*/} ) # remove comments `#’
    ini=( ${ini[*]/\ =\ /=} ) # remove anything with a space around `=`
    ini=( ${ini[*]/#[/\}$'\n'cfg.section.} ) # set section prefix
    ini=( ${ini[*]/%]/ \(} ) # convert text2function (1)
    ini=( ${ini[*]/=/=\( } ) # convert item to array
    ini=( ${ini[*]/%/ \)} ) # close array parenthesis
    ini=( ${ini[*]/%\( \)/\(\) \{} ) # convert text2function (2)
    ini=( ${ini[*]/%\} \)/\}} ) # remove extra parenthesis
    ini=( ${ini[*]/#\ */} ) # remove blank lines
    ini=( ${ini[*]/#\ */} ) # remove blank lines with tabs
    ini[0]=” # remove first element
    ini[${#ini[*]} + 1]=’}’ # add the last brace
    # printf “%s\n” ${ini[*]}
    eval “$(echo “${ini[*]}”)” # eval the result
    if [ -z "$_old_IFS" ] # restore old $IFS
    then unset IFS
    else IFS=”$_old_IFS”
    fi
    }

  14. Yeah, should be

    local IFS instead of just IFS

    the rest of the script seemed a bit magic to me at first, but anyway nice peace of work. (I would suggest not using a dot in name of function).

  15. nice work!

    the error described in first post occurs if a section does not contain values.
    this can be solved by adding any command between the brackets which are evaluated by eval().

    i think, there could be a better solution, but following works;
    replace:
    ni[${#ini[*]} + 1]=’}’ # add the last brace

    with something like:
    ini[${#ini[*]} + 1]=’echo -n; }’

  16. useful post, but the wordpress format makes it a hell to use it.

    what about packaging this in a .tar.gz and give the download link ?

  17. Hello!

    I tried the wonderful script and I’m even starting to understand what it does. I would like to modify the two areas where the text is converted to function and key names to do this: uppercase the names and in the case of the section name replace blank characters with underscore. I don’t know how to do this, especially since the script runs the regex for the whole file in one swing (IE not line by line).

    Best regards

    1. Hi!

      To put the ini contents in uppercase is easy, just change the line:
      IFS=$'\n' && ini=( $(<$1) )
      by the line:
      IFS=$'\n' && ini=( $(tr a-z A-Z <$1) )

      This will be work ok to you. About the spaces in sections, there are no a easy way to do this AFAIK.

      I hope to help you a bit.

      Regards,
      Andres

  18. Hi!
    I want to put enter character in a parameter of my ini file, but the cfg.parser is replacing the \n to )

    #######################################

    Contents of my config.ini:

    [FTP]
    FTP_COMMANDS=”bin
    prompt
    mkdir abc
    cd abc
    mput *
    bye”

    Commands executed to parse the above file:

    #cfg.parser config.ini
    #cfg.parser.section.FTP

    Result of parser:

    #echo $FTP_COMMANDS
    bin ) prompt ) mkdir abc ) cd abc ) mput * ) bye

  19. Galindro,

    Unfortunately this is a minimal INI parse and do not support split lines, including \n and \r. Maybe you can change your config for something like this

    [FTP]
    FTP_COMMANDS="bin\nprompt\nmkdir abc\ncd abc\nmput * \nbye"
    

    Then you can do:

    $ cfg.parser config.ini
    $ cfg.section.FTP
    $ echo -e $FTP_COMMANDS
    bin
    prompt
    mkdir abc
    cd abc
    mput \* 
    bye
    

    Note the -e flag to interpret the \n.

    I hope that can help you, but I known that the config file will not more readable :/

  20. Andrés,

    Thank you very much for anwser! I verify this issue doesn´t affect the ftp shell.

    $ ftp localhost
    Connected to localhost.
    220 Microsoft FTP Service
    Name (localhost:Administrator): Administrator
    331 Password required for Administrator.
    Password:
    230 User Administrator logged in.
    Remote system type is Windows_NT.
    ftp> bin )
    200 Type set to I.
    ftp> prompt )
    Interactive mode off.

  21. How do I remove any occurring tabs from a ini file in the function?

    If there is a line like this:

    foo[TAB]=[TAB]value

    the script fails.

    1. Some comments ago, Stephen (#323) post a variation which allows spaces and tabs.

      I post a new version of bash ini parser in pastebin: http://pastebin.com/m4fe6bdaf, which adds Stephen’s changes and a single multiline feature, like this example:

      [section]
      item1 = value1 value 2 \
      value3

      Enjoy!

      1. This worked for me in Bash 3. I only had to change dots to underscores in the function name(s).

        bash -version
        GNU bash, version 3.2.25(1)-release (x86_64-redhat-linux-gnu)

  22. Hello,

    I’ve implemented such a parser as an one-liner:
    cat $INIFILE | sed -n /^\[$SECTION\]/,/^\[.*\]/p | grep “^[:space:]*$ITEM[:space:]*=” | sed s/.*=[:space:]*//

    The variables INIFILE, SECTION, ITEM are usually read from command prompt $1, $1, $3
    The full listing of the script with some description is here:
    http://heinitz-it.de/archives/64

    Regards,
    Valentin Heinitz

  23. Some people ask me for a cfg.writer function, that is, a function which read the ini configuration loaded from a file and return this configuration in a ini format again. cfg.writer is the inverse of cfg.parser.

    And here is the code:

    cfg.writer ()
    {
    IFS=' '$'\n'
    fun="$(declare -F)"
    fun="${fun//declare -f/}"
    for f in $fun; do
    [ "${f#cfg.section}" == "${f}" ] && continue
    item="$(declare -f ${f})"
    item="${item##*\{}"
    item="${item%\}}"
    item="${item//=*;/}"
    vars="${item//=*/}"
    eval $f
    echo "[${f#cfg.section.}]"
    for var in $vars; do
    echo $var=\"${!var}\"
    done
    done
    }

    And the example of use:

    cfg.parser sample.ini
    cfg.write > sample_copy.ini

    When calling cfg.write, it’s parse the environment looking for cfg variables, and output an ini file with results to stdout.

    There are a limitation with this function, it do not maintain the types on the file and all values are converted to strings (which is not really a problem for bash).

    Enjoy!

    P.S: To avoid problems with wordpress formatting, I publish the code here: http://pastebin.com/vHrNN5jw

    1. this one doesn’t work for me… It never outputs more than two values per section and i’ve tried a number of different formats as well as some of the examples in this post

      1. This worked for me:

        cfg.writer ()
        {
        IFS=’ ‘$’\n’
        fun=”$(declare -F)”
        fun=”${fun//declare -f/}”
        for f in $fun; do
        [ "${f#cfg.section}" == "${f}" ] && continue
        item=”$(declare -f ${f})”
        item=”${item##*\{}”
        item=”${item%\}}”
        vars=”$item”
        echo “[${f#cfg.section.}]”
        for var in $item; do
        var=”${var%=*}”
        echo $var=\”${!var}\”
        done
        echo
        done
        }

        Thanks a lot for the parser!!

  24. multiple sections with empty lines dont work for me (bash 4.0.x):

    [section1]
    var1=value

    [section2]
    var2=value

    [section1]
    var1=value
    [section2]
    var2=value

    is okay, but with huge ini files it looks very ugly

  25. Andrés,
    Is there any chance to make the cfg.parser function works with bash 2? when I try to parse a ini file, it doesn´t create the functions based on sections of the ini file.

    This is my cfg.parser (I´ve only altered the name of the original function cfg.parser to conf_parser):

    conf_parser () {

    local IFS=$’\n’ && CONF=( $(> /dev/null 2>&1
    CONF=( ${CONF[*]//\#*/} ) >> /dev/null 2>&1
    CONF=( ${CONF[*]/\ =\ /=} ) >> /dev/null 2>&1
    CONF=( ${CONF[*]/#[/\}$'\n'conf_parser.section.} ) >> /dev/null 2>&1
    CONF=( ${CONF[*]/=/=\( } ) >> /dev/null 2>&1
    CONF=( ${CONF[*]/%/ \)} ) >> /dev/null 2>&1
    CONF=( ${CONF[*]/%\( \)/\(\) \{} ) >> /dev/null 2>&1
    CONF=( ${CONF[*]/%\} \)/\}} ) >> /dev/null 2>&1
    CONF=( ${CONF[*]/#\ */} ) >> /dev/null 2>&1
    CONF=( ${CONF[*]/#\ */} ) >> /dev/null 2>&1
    CONF[0]=” >> /dev/null 2>&1
    CONF[${#CONF[*]} + 1]=’}’ >> /dev/null 2>&1
    eval “$(echo “${CONF[*]}”)” >> /dev/null 2>&1

    }

    This is the result of execution (with bash -x):

    conf_parser my_ini.ini
    + conf_parser my_ini.ini
    + local ‘IFS=

    + CONF=($(<$1))
    <$1
    + CONF=(${CONF[*]//\#*/})
    + CONF=(${CONF[*]/\ =\ /=})
    + CONF=(${CONF[*]/#[/\}'
    'conf_parser.section.})
    + CONF=(${CONF[*]/=/=\( })
    + CONF=(${CONF[*]/%/ \)})
    + CONF=(${CONF[*]/%\( \)/\(\) \{})
    + CONF=(${CONF[*]/%\} \)/\}})
    + CONF=(${CONF[*]/#\ */})
    + CONF=(${CONF[*]/#\ */})
    + CONF[0]=
    + CONF[${#CONF[*]} + 1]=}
    echo "${CONF[*]}"
    ++ echo '
    }'
    + eval '
    }'

    Do you see the echo "nothing" above?

    Here are the content of my_ini.ini:
    [GLOBAL]
    DATE_FORMAT="%Y-%m-%d"
    TIME_FORMAT="%H:%M:%S"
    STORAGE_VENDOR="EMC"

    [LOADER]
    LOADER_DIR="/opt/loader/data"
    LOADER_FIELDS_SYSTEM="HOST,CPU,MEM"

    Here are the options set for bash:

    # echo $SHELLOPTS
    braceexpand:emacs:hashall:histexpand:history:interactive-comments:monitor

    Here are the bash version:

    # bash –version
    GNU bash, version 2.05b.0(1)-release (i386-redhat-linux-gnu)
    Copyright (C) 2002 Free Software Foundation, Inc.

    1. In theory it’s possible, but I do not have any machine to install bash2 to test (really I don’t have a lot of time too ;)

      I will try to replace the $(<$1) redirection for a classic $(cat $1) in first place, and then, probably, we need to replace the pattern replacement for a sed, I do not like to use sed in this snippet, because it's pretend to be "bash only", but maybe we need to do this to work fine in bash2.

      If you success to run the snippet in bash2, please feedback to me :)

  26. Andrés,
    Unfortunaly, I doesn´t have modified the bash ini parser for work with bash 2.0 because I don´t have lot of time to do that too. But, I have another problem situation, but in bash 3.0 (GNU bash, version 3.00.15(1)-release (i386-redhat-linux-gnu))

    The parser can´t handle a session with a number 1 written on it.
    e.g.:
    [VGCON01]
    IP_DEST=”10.121.18.154″

    But this error only occurs if the script is executed in crontab!! If I execute it manually, no error occurs. The interpreter of my crontab is bash.

    ##### Here are the debug of the error: ######

    conf_parser my.ini
    + conf_parser my.ini
    + local ‘IFS=

    + CONF=($(<$1))
    <$1
    + CONF=(${CONF[*]//\#*/})
    + CONF=(${CONF[*]/\ =\ /=})
    + CONF=(${CONF[*]/#[/\}'
    'conf_parser.section.})
    + CONF=(${CONF[*]/%]/ \(})
    + CONF=(${CONF[*]/=/=\( })
    + CONF=(${CONF[*]/%/ \)})
    + CONF=(${CONF[*]/%\( \)/\(\) \{})
    + CONF=(${CONF[*]/%\} \)/\}})
    + CONF=(${CONF[*]/#\ */})
    + CONF=(${CONF[*]/#\ */})
    + CONF[0]=
    + CONF[${#CONF[*]} + 1]='}'
    echo "${CONF[*]}"
    ++ echo '
    conf_parser.section.GLOBAL () {
    IP_HOST=( "10.121.18.148" )
    HOSTS=( "VGCON02;VGCON_01" )
    }
    conf_parser.section.VGCON02 () {
    IP_DEST=( "10.121.18.154" )
    1 ) <—- HERE ARE THE ERROR. The config.parser replace VGDEC01 for 1
    IP_DEST=( "10.121.18.154" )
    }

    ##### Here are the debug of correct execution: #####

    conf_parser my.ini
    + conf_parser my.ini
    + local 'IFS=
    '
    + CONF=($(<$1))
    <$1
    + CONF=(${CONF[*]//\#*/})
    + CONF=(${CONF[*]/\ =\ /=})
    + CONF=(${CONF[*]/#[/\}'
    'conf_parser.section.})
    + CONF=(${CONF[*]/%]/ \(})
    + CONF=(${CONF[*]/=/=\( })
    + CONF=(${CONF[*]/%/ \)})
    + CONF=(${CONF[*]/%\( \)/\(\) \{})
    + CONF=(${CONF[*]/%\} \)/\}})
    + CONF=(${CONF[*]/#\ */})
    + CONF=(${CONF[*]/#\ */})
    + CONF[0]=
    + CONF[${#CONF[*]} + 1]='}'
    echo "${CONF[*]}"
    ++ echo '
    conf_parser.section.GLOBAL () {
    IP_HOST=( "10.121.18.148" )
    HOSTS=( "VGCON02;VGCON_01" )
    }
    conf_parser.section.VGCON02 () {
    IP_DEST=( "10.121.18.154" )
    conf_parser.section.VGCON01 () )
    IP_DEST=( "10.121.18.154" )
    }

    If you see, the section was processed normaly, without any errors. Have you ever had this problem before?

  27. Andrés,

    I´ve found the BUG associated with the error that I´ve posted before. Try to do a simple test:

    * Write a file with this simple content:

    [SERVER01]
    IP=”0.0.0.0″

    * Execute the following command:
    echo $( GNU bash, version 3.00.15(1)-release (i386-redhat-linux-gnu)

    With bash -> GNU bash, version 3.1.17(1)-release (i686-redhat-linux-gnu) the problem doesn´t occurs.

    Is there any way to replace the line CONF=( $(<$1) ) for another to convert to line-array?

  28. Andrés,

    I´ve found the BUG associated with the error that I´ve posted before. Try to do a simple test:

    * Write a file with this simple content:

    [SERVER01]
    IP=”0.0.0.0″

    * Execute the following command:
    echo $( GNU bash, version 3.00.15(1)-release (i386-redhat-linux-gnu)

    With bash -> GNU bash, version 3.1.17(1)-release (i686-redhat-linux-gnu) the problem doesn´t occurs.

    Is there any way to replace the line CONF=( $(<$1) ) for another to convert to line-array?

  29. Sorry Andrés, I could not post what I wanted before…

    I´ve found the BUG associated with the error that I´ve posted before. Try to do a simple test:

    * Write a file with this simple content:

    [SERVER01]
    IP=”0.0.0.0″

    * Execute the following command:
    # echo $(<file.ini)

    * You will give the following return:
    # 1 IP="0.0.0.0"

    See?? Bash replaced the string SERVER01 for the number 1.

    This error occours with bash GNU bash, version 3.00.15(1)-release (i386-redhat-linux-gnu)

    With bash GNU bash, version 3.1.17(1)-release (i686-redhat-linux-gnu) the problem doesn´t occurs.

    Is there any way to replace the line CONF=( $(<$1) ) for another to convert to line-array? With CONF=( $(cat $1) ) the same error ocurs…

  30. couldn’t make it run under Bash 4. I am using: GNU bash, Version 4.0.33(1)-release (i486-pc-linux-gnu)
    wich is shipped out with K/ubuntu 9.10 by default.

    Error message:
    library.sh: 17: Syntax error: Bad function name

    And the line 17 is:
    cfg.parser () {

    It would be very nice if you could set up a fully demo fo r download, wich includes the ini file as well. Such a demo may prevent some headaches. And it would be nice if you could mention for what bash version this script is coded for.

    thx a lot

  31. You can delete previous posts – found out there issues with Bash 4. Here is an updated one (works with Bash 3 and 4):

    function trim_left()
    { # removes whitespace from beginning of string from $1 and outputs the result (without \n in the end).
       local trim_left_regex='^([[:blank:]]*)(.*)$'
       local line=$1
       if [[ "$line" =~ $trim_left_regex ]]
       then
          line=${BASH_REMATCH[2]}
       fi
    
       echo -n "$line"
    }
    
    # ini_parser usage:
    #
    # to parse the config file called 'somefile.ini', with the following contents (whitespace will be handled correctly):
    #
    # [sec2]
    #   var2='something'
    #
    # use: ini_parser some_parser somefile.ini
    # Notes:
    # 1. some_parser - is just some unique name, to enable the usage of multiple ini files in the same script.
    # 2. To read section called 'sec2' (in the file [sec2]) for reading call: some_parser_section_sec2 
    # 3. To read the content of the key called 'var2' (in the file var2=zzz) just use $some_parser_key_var2
    # 4. If key contains dots '.' - substitute them with '_' when accessing the variable.
    #    for example for keyname1.subkeyname1 use: $some_parser_key_keyname1_subkeyname1
    # 5. If your var2 is an array, then you can use ${some_parser_key_var2[index]}
    function ini_parser()
    {
        local parser_name=$1
        local ini_file=$2
        local empty_section_name='__3mpt4__s3ct10n___'
        local ini_key_regex='^([^=[:blank:]]*)[[:blank:]]*=[[:blank:]]*(.*)$'
        local i
        local OLD_IFS=$IFS
    
        # convert to line-array with fake 'empty' section added for keys with no section (which appear before first section).
        IFS=$'\n' && ini=( "[${empty_section_name}]" $(<${ini_file}) )
    
        # remove prefixing whitespace
        for (( i=0; i<${#ini[*]}; i++ )); do ini[$i]=`trim_left ${ini[$i]}`; done
    
        ini=( ${ini[*]//\#*/} ) # remove comments (# style)
        ini=( ${ini[*]//;*/} )  # remove comments (; style)
    
        # removing whitespace around '='
        # and renaming all keys  to ${parser_name}_key_ to avoid collisions.
        for (( i=0; i<${#ini[*]}; i++ ))
        do
           if [[ ${ini[$i]} =~ $ini_key_regex ]]
           then
             ini[$i]="${parser_name}_key_${BASH_REMATCH[1]//./_}=${BASH_REMATCH[2]}"
           fi
        done
    
        ini=( ${ini[*]/#[/return; \}$'\n'${parser_name}_section_} ) # set section prefix
        ini=( ${ini[*]/%]/ \(} )                 # convert text2function (1)
        ini=( ${ini[*]/=/=\( } )                 # convert item to array
        ini=( ${ini[*]/%/ \)} )                  # close array parenthesis
        ini=( ${ini[*]/%\( \)/\(\) \{} )         # convert text2function (2)
        ini=( ${ini[*]/%\} \)/\}} )              # remove extra parenthesis
        ini[0]=''                                # clear the first element which should be extra 'return; }'
        ini[${#ini[*]} + 1]='return; }'          # add the last return and brace
        eval "$(echo "${ini[*]}")"               # eval the result
        ${parser_name}_section_${empty_section_name} # set global keys
        IFS=$OLD_IFS
    }
    

  32. Hi!

    I have a problem with this portion of code:

    ini=( ${ini[*]//\#*/} ) # remove comments (# style)
    ini=( ${ini[*]//;*/} ) # remove comments (; style)

    My ini file

    [section]
    pass=’234df#dRR’

    return an error if pass contain a #

    help me, please. Sorry for my english ;)

  33. Andrew,
    Indeed it’s a bug in the code, I remove the ‘#’ even if it is not a comment. I suggest you to remove the line:

    ini=( ${ini[*]//\#*/} ) # remove comments (# style)
    

    Removing this line have a collateral effect, you cannot use comments at the end of a line, for example:

    [MYINI]
    # this comment still works fine
    key = value # this one is not allowed
    

    I hope this trick can help you.

    Br,
    Andres

  34. Nice script, however, it doesn’t work for empty values which are pretty common, such as the following example :

    [section]
    key=

    Add only one value like this to your .ini file and the whole script is messed up.

    Could it be possible to fix this ?

    Thanks for any help and keep up the good work :)

    Regards

    1. Hi Gabriel,

      I check the code and it works fine with empty keys also…, I use the code in the top-post with the following file:

      [section]
      key

      And then:

      $ cfg_parser test.ini
      $ cfg.section.section
      $ echo ${key:-yep its empty}
      yep its empty

      I cannot find any problem here, can u post the code and the example that you use?

      Thanks for the issue anyway :)

    1. Gabriel,

      Sorry for delay, I tried the sample and works for me :/ I used the same file that you (equal sign too) and still working.

      Are you using the last version? (at the post)

      I’m, using bash 4.1.9, with no special patches, but the following shopts enabled/disabled:

      nocaseglob     	 off
      nocasematch   off
      nullglob       	 off
      progcomp       	on
      promptvars     	on
      compat31       	off
      compat32       	off
      compat40       	off
      globstar       	off
      

      I hope that this info can help you. Please feedback me with any news in that topic.

      Br,
      Andres

  35. Hello Andres

    Thanks a lot for this wonderful script, very useful !!

    I just discovered a small problem … It look that when there is a directory or file ‘a’ or ‘b’ … ‘z’ in the run directory, then the call to cfg_parser fail
    ie:
    $ cfg_parser somefile.ini
    $ touch a
    $ cfg_parser somefile.ini
    bash: syntax error near unexpected token `}’

    I did tried with
    GNU bash, version 3.00.15(1)-release (x86_64-redhat-linux-gnu)
    And
    GNU bash, version 4.1.0(1)-release (x86_64-unknown-linux-gnu)

    But same “behaviour” … Did I missed something or did I made something wrong ? I admit I am a dummy in ksh/bash :$

    Nob

    1. Hi Nob

      Thank you very much for your report. I was busy past weeks, so please apologize me for delay.

      Really is a very weird behaviour. The problem is not in the file itself, but in the INI file. The brackets (“[" and "]“), are expressions in bash (if you type, for example echo /dev/sda[1], you can get /dev/sda1), so when parse the INI file, and load it into “ini” array, bash also interpret the brackets and you got the problem.

      If the section name contains an “a”, and a file called “a” exists, then [a] is evaluated, and everything fails, if not, [a] is not evaluated and parser works fine.

      I fix the parser in the post, escaping brackets before load file content into “ini” variable.

      I hope this solve the issue.

      Br,
      Andrés

  36. Hi,

    Because you escape the [ and ] chars, i need to change this lines:
    ini=( ${ini[*]/#[/\}$'\n'cfg.section.} ) # set section prefix
    ini=( ${ini[*]/%]/ \(} ) # convert text2function (1)

    To (added the escape \ char to replaced string too):
    ini=( ${ini[*]/#\\[/\}$'\n'cfg.section.} ) # set section prefix
    ini=( ${ini[*]/%\\]/ \(} ) # convert text2function (1)

    Elbandi

    1. Oops, it’s really an epic bug.

      The problem is that the trick of the parser is the conversion of INI file in a bash script, of course if you put evil code in the ini, you will get evil results.

      I’m not pretty sure if its really a bug, or just a bad use, anyway I would like to find a solution.

      My attempts works in the idea to prepend a “declare -a” before any item in the section, so on evil code an bash error is raised instead of executing the code. The problem with it is that declare using in a function is local, so the value of the variables never come back to the parent shell.

      I try to find a solution, without promise. In the meanwhile be careful with what application runs the script…

      Thank you very much for the report.

      Regards,
      Andrés

      1. Sure, the first file isn’t even a proper INI file, so checking if it’s a real INI file will do. The second is a proper INI file, so it’ll still leak through. You can use declare -a and then export the finished result. Or you can try to make it work without executing stuff from a user-provided file ;-)

    1. Sure, I never though in a license for snippet, but a couple of people asked me about it, so let’s say that it’s MIT license, you can use for any purpose without warranty, of course.

      1. Thanks. Now then I just have a question and it may not necessarily be implemented in your code as I actually intended to use this to manage a configuration file on a script. The script would need to be able to create, modify, and save the file.

        1.) Is it possible to initialize new variable for this code to write back to the ini file?

        For now (before I got your response) I had decided to do a very crude method based on a debian post from back in 2004 but it’s very insecure due to the way the bash source command works so I’m hoping to implement your solution instead soon.

  37. Hi,

    If you might interested in this…
    About one year ago I’d spent some days to fix and improve another ini parser I’d found on the net. It’s very comfortable and fast (speaking about bash) and does stable error handling of broken ini files.

    You might check that out:
    https://github.com/rudimeier/bash_ini_parser

    Note it’s a reader only so far.

    Current Version 0.4.1 is using prefixed variables as the interface to get config vars. In “next” branch I’ve changed the interface by using bash-4.0’s hash tables instead. It is able to parse much more existing ini files even with very strange section and variable names.

    greetings from Berlin,
    Rudi

  38. Hi Andrés
    I had a problem with the line

    ini=”${ini//[/\[}" # escape [

    when the ini file had several tags the parsing was realy slow I solved it by doing the [ replacement while reading the file using sed

    changing
    ini="$(<$1)" # read the file
    by
    ini="$(sed -e "s/^\[/\}\ncfg.section./" $1)" # read the file and set section prefix

    and removing the lines
    ini="${ini//[/\[}" # escape [
    ... and ...
    ini=( ${ini[*]/#\\[/\}$'\n'cfg.section.} ) # set section prefix

  39. Great script.
    I have a problem with escaped quotes in a string–
    They read fine but when trying to write they aren’t escaped properly again.

    eg.
    [section]
    var=”The Project \”Pretty Title\””

    when writing it out it looks like…

    [section]
    var=”The Project “Pretty Title””

    which won’t work… :(

  40. Thanks for creating this useful script!

    On my Ubuntu machine (BASH_VERSION : 4.1.5(1)-release) I’m don’t seem to be running bash in POSIX mode but still the dot and underscores in the cfg.parser and cfg.section.test command were bugging me.

    Your suggestion to change cfg.parser into cfg_parser works for me.
    But when changing cfg.section.test into cfg_section_test caused the following error :
    cfg_section_test: command not found
    But somehow leaving cfg.section.test untouched works fine.

    I’m really no expert at this but I hope this suggestion is useful for others who ran into the same problem. :)

  41. Appreciate your wonderful piece of work, implemented this for my project and works like a charm, until I had this new requirement of reading multiple ini files and creating a single xml file, example: filename_section_variable Value, currently the sections are repeating for all the files and variables which is causing a problem. Can you suggest any fix so that, the section name gets reset for each file.

  42. cfg_writer ()
    {
    IFS=’ ‘$’\n’
    fun=”$(echo “${ini[*]}”)”
    #fun=”$(declare -F)”
    #fun=”${fun//declare -f/}”

    Below change did the trick, now the code works like magic again :)
    Thanks Andrés

  43. For interaction with other functions its necessary backup and restore original IFS.

    cfg_parser ()
    {
    OLD_IFS=$IFS
    ini=”$(<$1)" # read the file
    ini="${ini//[/\[}" # escape [
    ini="${ini//]/\]}" # escape ]
    IFS=$'\n' && ini=( ${ini} ) # convert to line-array
    ini=( ${ini[*]//;*/} ) # remove comments with ;
    ini=( ${ini[*]/\ =/=} ) # remove tabs before =
    ini=( ${ini[*]/=\ /=} ) # remove tabs be =
    ini=( ${ini[*]/\ =\ /=} ) # remove anything with a space around =
    ini=( ${ini[*]/#\\[/\}$'\n'cfg.section.} ) # set section prefix
    ini=( ${ini[*]/%\\]/ \(} ) # convert text2function (1)
    ini=( ${ini[*]/=/=\( } ) # convert item to array
    ini=( ${ini[*]/%/ \)} ) # close array parenthesis
    ini=( ${ini[*]/%\\ \)/ \\} ) # the multiline trick
    ini=( ${ini[*]/%\( \)/\(\) \{} ) # convert text2function (2)
    ini=( ${ini[*]/%\} \)/\}} ) # remove extra parenthesis
    ini[0]="" # remove first element
    ini[${#ini[*]} + 1]='}' # add the last brace
    eval "$(echo "${ini[*]}")" # eval the result
    IFS=$OLD_IFS
    }

  44. I’m having problems reading an ini file that contains hyphens
    # filename.ini
    [DBSET-Compile]
    Active=NO
    DB1-ConnectionType=Local

    I thought I’d change “-” to “_” in the section name and left side of “=” but I cant figure out the syntax. Can anyone help?

  45. This is excellent, thank you so much. You’ve really helped me out tremendously with this script.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s