Parsing .ini file in bash
Asked Answered
D

7

7

I have a below properties file and would like to parse it as mentioned below. Please help in doing this.

.ini file which I created :

[Machine1]

app=version1


[Machine2]

app=version1

app=version2

[Machine3]

app=version1
app=version3

I am looking for a solution in which ini file should be parsed like

[Machine1]app = version1
[Machine2]app = version1
[Machine2]app = version2
[Machine3]app = version1
[Machine3]app = version3

Thanks.

Distend answered 21/3, 2018 at 6:53 Comment(0)
U
13

Try:

$ awk '/\[/{prefix=$0; next} $1{print prefix $0}' file.ini
[Machine1]app=version1
[Machine2]app=version1
[Machine2]app=version2
[Machine3]app=version1
[Machine3]app=version3

How it works

  • /\[/{prefix=$0; next}

    If any line begins with [, we save the line in the variable prefix and then we skip the rest of the commands and jump to the next line.

  • $1{print prefix $0}

    If the current line is not empty, we print the prefix followed by the current line.

Adding spaces

To add spaces around any occurrence of =:

$ awk -F= '/\[/{prefix=$0; next} $1{$1=$1; print prefix $0}' OFS=' = ' file.ini
[Machine1]app = version1
[Machine2]app = version1
[Machine2]app = version2
[Machine3]app = version1
[Machine3]app = version3

This works by using = as the field separator on input and = as the field separator on output.

Urba answered 21/3, 2018 at 7:17 Comment(0)
C
9

I love John1024's answer. I was looking for exactly that. I have created a bash function that allows me to lookup sections or specific keys based on his idea:

function iniget() {
  if [[ $# -lt 2 || ! -f $1 ]]; then
    echo "usage: iniget <file> [--list|<section> [key]]"
    return 1
  fi
  local inifile=$1

  if [ "$2" == "--list" ]; then
    for section in $(cat $inifile | grep "\[" | sed -e "s#\[##g" | sed -e "s#\]##g"); do
      echo $section
    done
    return 0
  fi

  local section=$2
  local key
  [ $# -eq 3 ] && key=$3

  # https://mcmap.net/q/1393573/-parsing-ini-file-in-bash
  # This awk line turns ini sections => [section-name]key=value
  local lines=$(awk '/\[/{prefix=$0; next} $1{print prefix $0}' $inifile)
  for line in $lines; do
    if [[ "$line" = \[$section\]* ]]; then
      local keyval=$(echo $line | sed -e "s/^\[$section\]//")
      if [[ -z "$key" ]]; then
        echo $keyval
      else          
        if [[ "$keyval" = $key=* ]]; then
          echo $(echo $keyval | sed -e "s/^$key=//")
        fi
      fi
    fi
  done
}

So given this as file.ini

[Machine1]
app=version1
[Machine2]
app=version1
app=version2
[Machine3]
app=version1
app=version3

then the following results are produced

$ iniget file.ini --list
Machine1
Machine2
Machine3

$ iniget file.ini Machine3
app=version1
app=version3

$ iniget file.ini Machine1 app
version1

$ iniget file.ini Machine2 app
version2
version3

Again, thanks to @John1024 for his answer, I was pulling my hair out trying to create a simple bash ini parser that supported sections.

Tested on Mac using GNU bash, version 5.0.0(1)-release (x86_64-apple-darwin18.2.0)

Clyve answered 8/2, 2019 at 17:32 Comment(4)
Under what license is the printed code? Can I use it in my GPLv3 code?Outandout
@Outandout - consider my code snippet MIT licensed, as in free beer. EnjoyClyve
When you signed up for Stack Overflow, you agreed to publish your contributions under the Creative Commons BY-SA license (which version depends on when this was posted. The timeline link next to each post - the clock icon below the votes - provides this detail; in this case, currently, CC BY-SA 4.0).Stalinsk
Okay, what tripleee saidClyve
D
3

You can try using awk:

 awk '/\[[^]]*\]/{          # Match pattern like [...]
        a=$1;next           # store the pattern in a
      } 
      NF{                   # Match non empty line
        gsub("=", " = ")    # Add space around the = character
        print a $0          # print the line
     }' file
Demasculinize answered 21/3, 2018 at 7:18 Comment(0)
A
3

Excellent answers here. I made some modifications to @davfive's function to fit it better to my use case. This version is largely the same except it allows for whitespace before and after = characters, and allows values to have spaces in them.

# Get values from a .ini file
function iniget() {
    if [[ $# -lt 2 || ! -f $1 ]]; then
        echo "usage: iniget <file> [--list|<section> [key]]"
        return 1
    fi
    local inifile=$1
    
    if [ "$2" == "--list" ]; then
        for section in $(cat $inifile | grep "^\\s*\[" | sed -e "s#\[##g" | sed -e "s#\]##g"); do
            echo $section
        done
        return 0
    fi
    
    local section=$2
    local key
    [ $# -eq 3 ] && key=$3
    
    # This awk line turns ini sections => [section-name]key=value
    local lines=$(awk '/\[/{prefix=$0; next} $1{print prefix $0}' $inifile)
    lines=$(echo "$lines" | sed -e 's/[[:blank:]]*=[[:blank:]]*/=/g')
    while read -r line ; do
        if [[ "$line" = \[$section\]* ]]; then
            local keyval=$(echo "$line" | sed -e "s/^\[$section\]//")
            if [[ -z "$key" ]]; then
                echo $keyval
            else          
                if [[ "$keyval" = $key=* ]]; then
                    echo $(echo $keyval | sed -e "s/^$key=//")
                fi
            fi
        fi
    done <<<"$lines"
}
Attenuator answered 6/2, 2021 at 22:21 Comment(0)
U
0

For taking disparate sectional and tacking the section name (including 'no-section'/Default together) to each of its related keyword (along with = and its keyvalue), this one-liner AWK will do the trick coupled with a few clean-up regex.

ini_buffer="$(echo "$raw_buffer" | awk '/^\[.*\]$/{obj=$0}/=/{print obj $0}')"  

Will take your lines and output them like you wanted:

+++ awk '/^\[.*\]$/{obj=$0}/=/{print obj $0}'
++ ini_buffer='[Machine1]app=version1
[Machine2]app=version1
[Machine2]app=version2
[Machine3]app=version1
[Machine3]app=version3'

A complete solution to the INI-format File

As Clonato, INI-format expert said that for the latest INI version 1.4 (2009-10-23), there are several other tricky aspects to the INI file:

  1. character set constraint for section name
  2. character set constraint for keyword
  3. And lastly is for the keyvalue to be able to handle pretty much anthing that is not used in the section and keyword name; that includes nesting of quotes inside a pair of same single/double-quote.

Except for the nesting of quotes, a INI-format Github complete solution to parsing INI-format file with default section:

# syntax: ini_file_read <raw_buffer>
# outputs: formatted bracket-nested "[section]keyword=keyvalue"
ini_file_read()
{
  local ini_buffer raw_buffer hidden_default
  raw_buffer="$1"

  # somebody has to remove the 'inline' comment
  # there is a most complex SED solution to nested 
  #    quotes inline comment coming ... TBA
  raw_buffer="$(echo "$raw_buffer" | sed '
  s|[[:blank:]]*//.*||; # remove //comments
  s|[[:blank:]]*#.*||; # remove #comments
  t prune
  b
  :prune
  /./!d; # remove empty lines, but only those that
         # become empty as a result of comment stripping'
 )"

  # awk does the removal of leading and trailing spaces
  ini_buffer="$(echo "$raw_buffer" | awk '/^\[.*\]$/{obj=$0}/=/{print obj $0}')"  # original

  ini_buffer="$(echo "$ini_buffer" | sed  's/^\s*\[\s*/\[/')"
  ini_buffer="$(echo "$ini_buffer" | sed  's/\s*\]\s*/\]/')"

  # finds all 'no-section' and inserts '[Default]'
  hidden_default="$(echo "$ini_buffer" \
              | egrep '^[-0-9A-Za-z_\$\.]+=' | sed 's/^/[Default]/')"
  if [ -n "$hidden_default" ]; then
    echo "$hidden_default"
  fi
  # finds sectional and outputs as-is
  echo "$(echo "$ini_buffer" | egrep '^\[\s*[-0-9A-Za-z_\$\.]+\s*\]')"
}

The unit test for this StackOverflow post is included in this file:

Source:

Uttermost answered 5/3, 2022 at 12:59 Comment(0)
C
0

I tried to improve on davfive's answer and make it more robust.

It now handles spaces in filenames as well as input such as:

[Machine1]
app = version1 # comment
#unusedApp = versionX

The rest of a line after a # is ignored, and calling it with just a filename lists sections by default.

Also made it a script rather than a function:

#!/usr/bin/env bash
# get property value in ini file
# original author: davfive
# https://stackoverflow.com/a/54597545
# edited by: cherrynoize

if [ "$1" == "-h" ] || [ "$1" == "--help" ]; then
cat<<EOF
get property value in ini file
usage: iniget <file> [-l|--list|<section> [property]]
EOF
exit
fi

inifile="$1"

if [ ! -f "$1" ]; then
  echo "error: no file '$1'"
  exit 4
fi

if [ -z "$2" ] || [ "$2" = "-l" ] || [ "$2" == "--list" ]; then
  for section in $(cat "$inifile" | grep "\[" | sed -e "s#\[##g" | sed -e "s#\]##g"); do
    echo "$section"
  done
  exit
fi

section="$2"
[ $# -eq 3 ] && property="$3"

remove_section_header () {
  sed "s/^\[$section\]//" <<< "$1"
}

strip_comments () {
  sed -r -e 's/^#.*//' -e 's/[[:space:]]+#.*//' <<< "$1"
}

strip_spaces () {
  tr -d " " <<< "$1"
}

# https://mcmap.net/q/1393573/-parsing-ini-file-in-bash
# turn ini sections => [section-name]property=value
mapfile lines <<< "$(awk '/\[/{prefix=$0; next} $1{print prefix $0}' "$inifile")"

for line in "${lines[@]}"; do
  # verify property belongs to requested section
  if [[ "$line" != \[$section\]* ]]; then
    continue
  fi

  # strip property line
  line="$(remove_section_header "$line")"
  line="$(strip_comments "$line")"
  line="$(strip_spaces "$line")"

  if [ "$line" = "" ]; then
    continue
  fi

  if [ -z "$property" ]; then
    echo "$line"
    continue
  fi

  if [[ $line = $property=* ]]; then
    sed -e "s/^$property=//" <<< "$line"
    exit
  fi
done

Also, this is a simpler oneliner for doing the same thing:

grep -oP "(?<=^$key = ).*" "$filename" | cut -d' ' -f2-

Though it only supports a single form:

key = value

But actually with just sed:

sed -n "s/^$key[[:space:]]=[[:space:]]//p" "$filename"

You can handle any amount of whitespace/tab separators.


By the way, here's a script to edit .ini file values as well:

#!/usr/bin/env bash
# set property value in ini file
# author: cherrynoize
# https://github.com/cherrynoize/dotfiles

# initialize current value to empty
cur_value=

if [ "$1" == "-h" ] || [ "$1" == "--help" ]; then
cat<<EOF
set property value in ini file
usage: iniset <file> <section> <property>
EOF
exit
fi

if [ "$#" -lt 4 ]; then
  echo "error: not enough arguments"
  exit 1
fi

inifile="$1"
section="$2"
property="$3"
new_value="$4"

if [ ! -f "$1" ]; then
  echo "error: no file '$1'"
  exit 4
fi

remove_section_header () {
  sed "s/^\[$section\]//" <<< "$1"
}

strip_comments () {
  sed -r -e 's/^#.*//' -e 's/[[:space:]]+#.*//' <<< "$1"
}

strip_spaces () {
  tr -d " " <<< "$1"
}

# https://mcmap.net/q/1393573/-parsing-ini-file-in-bash
# turn ini sections => [section-name]property=value
# maintain header and empty lines track line numbers correctly
mapfile lines <<< "$(awk '/\[/{section=$0; print; next} !$1{print ""; next} {print section $0}' "$inifile")"

# keep track of line number to update the correct line
line_number=0

for line in "${lines[@]}"; do
  (( line_number++ ))

  # verify property belongs to requested section
  if [[ "$line" != \[$section\]* ]]; then
    continue
  fi

  # strip property line
  line="$(remove_section_header "$line")"
  line="$(strip_comments "$line")"
  line="$(strip_spaces "$line")"

  if [ "$line" = "" ]; then
    continue
  fi

  if [[ $line = $property=* ]]; then
    cur_value="$(sed -e "s/^$property=//" <<< "$line")"
    sed -i "${line_number}s/$cur_value"'$'"/$new_value/" "$inifile"
    exit
  fi
done

if [ -z "$cur_value" ]; then
  echo "error: property '$property' not found in '$inifile'"
  exit 255
fi

You can use it like so:

iniset <file> <section> <property>
Coblenz answered 25/2 at 13:17 Comment(0)
E
0

If you want an easy way to parse the contents of an .ini file you can use jc command. It will convert the input to json which you can then query/transform with jq

The specific solution is more robust than awk/sed-based parsing and it also respects comments.

The following command:

cat test.ini | jc -p --ini-dup | jq -r \
'. | to_entries[] | .key as $root | (.value|to_entries[]) as $child | ($child.value|to_entries[]|.value) as $val | ("["+$root+"]"+$child.key+" = "+$val)'

will print:

[Machine1]app = version1
[Machine2]app = version1
[Machine2]app = version2
[Machine3]app = version1
[Machine3]app = version3
[Machine3]kap = version1

even when the input file has comments starting with ;

Note: To get the latest version on ubuntu I had to install with pip3 instead of apt.

Eneidaenema answered 27/2 at 17:2 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.