Reputation: 545
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.
Upvotes: 7
Views: 11713
Reputation: 10828
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
.
Upvotes: 0
Reputation: 13
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://stackoverflow.com/questions/49399984/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://stackoverflow.com/questions/49399984/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>
Upvotes: 0
Reputation: 383
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://stackoverflow.com/questions/49399984/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)
Upvotes: 9
Reputation: 2596
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'
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:
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:
Upvotes: 0
Reputation: 63
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"
}
Upvotes: 3
Reputation: 113884
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.
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.
Upvotes: 13
Reputation: 13259
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
Upvotes: 3