sed: change values of properties of an environment in a .yml file
I have an .yml file that configures environment properties of an application, such like this:
env1:
:prop1: "value1"
:prop2: "value2"
...
:propn: "valuen"
env2:
:prop1: "value1"
:prop2: "value2"
:prop3: "value3"
...
:propn: "valuen"
...
envn:
:prop1: "value1"
:prop2: "value2"
...
:propn: "valuen"
I would like to produce a bash script with the following interface:
$ change_env.sh <environment> <property> <new value> <file.yml>
Example:
$ change_env.sh env2 prop3 "this value was changed" file.yml
The output will be:
env1:
:prop1开发者_C百科: "value1"
:prop2: "value2"
...
:propn: "valuen"
env2:
:prop1: "value1"
:prop2: "value2"
:prop3: "this value was changed"
...
:propn: "valuen"
...
envn:
:prop1: "value1"
:prop2: "value2"
...
:propn: "valuen"
I found this post, however I could not do it work for my case. Replace an XML element's value? Sed regular expression?
I also tried this: (it fails because alters all properties)
sed 's/\(:pro3:\).*/\1 "new value"/'
Thanks in advance! -- Lourenco.
(very nice first post!)
Try this
cat change_env.sh
#!/bin/bash
# spec : change_env.sh <environment> <property> <new value> <file.yml>
case ${#} in [!4] )
echo "usage: change_env.sh <environment> <property> <new value> <file.yml>" 1>&2
exit 1
;;
esac
env="$1" prop="$2" new="$3" file="$4"
bakFile="${file}".bak
mv "$file" "$bakFile"
sed '/^'"${env}"'/,/^[ ]*$/{ # [ spaceChar tabChar ]
/'"${prop}"'/s/\('"${prop}"'\)\(.*$\)/\1'"${new}"'/
}' "$bakFile" > "$file"
edit
Note, if you expect input to contain white-space in the values you'll want to modify script to quote all variables ("$1","$2"...). (I have now done this, as it is a shell-scripting best practice).
The /env/,/^[{space,tab}]*$/
is a range address for sed. It reads a block of text that contains your environment settings. I'm assuming your sample input is correct and that each env is separated by a blank line. Hmm... this would include the last one in the file.
** edit**
Thanks to @posdef for pointing some problems with this answer. The code is updated to solve the particular case.
Even after the fix, I did notice that given an input like
change_env.sh env2 prop2 "new value" file.yml
The relevant output was
:prop2new value
So, without hardcoding extra :
and space chars into the substitution, this means you'll need to be very verbose in how you call the <property>
value AND the <new value>
, i.e.
change_env.sh env2 ":prop2: " "\"new value\"" file.yml
# note extra cruft-^^-----^^^--^^---------^^--------------
relevant output
env2:
:prop1: "value1"
:prop2: "new value"
:prop3: "value3"
...
:propn: "valuen"
IHTH
I'd use awk:
#!/bin/sh
if [ $# -ne 4 ]; then
echo "usage: $0 env prop value file" >&2
exit 1
fi
awk -F : -v env="$1" -v prop="$2" -v val=" \"$3\"" '
BEGIN {OFS = FS}
$1 == env {in_env = 1}
NF == 0 {in_env = 0}
in_env && $2 == prop {$3 = val}
{print}
' "$4"
This answer is based on Glenn Jackman's AWK script, which in my testing fails due to indentation issues inherent in the type of input OP (and yours truly) has.
Specifically, the condition of checking whether or not we are in the desired environment will happen on a different iteration than checking whether or not we have the desired property since these will typically be on different lines. Thus in_env && $2 == prop
will never return true, considering that property : value
pair will be read as $1 : $2
on a separate line.
Additionally, the comparison $2 == prop
will suffer from the leading whitespace, which needs to be trimmed. I've added a couple of nice one-liners described here, for making the script more human-readable.
Lastly, the original script hard-coded double quotes around the new value, which is a problem if you are inserting numerical values.
I have modified the script in the following way which works well in my test case. I am providing here in case it is of use to others.
#!/bin/sh
if [ $# -ne 4 ]; then
echo "usage: $0 env prop value file" >&2
exit 1
fi
awk -F : -v env="$1" -v prop="$2" -v val=" $3" '
function ltrim(s) { sub(/^[ \t\r\n]+/, "", s); return s }
function rtrim(s) { sub(/[ \t\r\n]+$/, "", s); return s }
function trim(s) { return rtrim(ltrim(s)); }
BEGIN {OFS = FS}
$1 == env {in_env = 1}
NF == 0 {in_env = 0}
in_env && trim($1) == prop {$2 = val}
{print}
' "$4"
Bash script
#!/bin/bash
# tested with bash 4
if [ $# -ne 4 ];then
echo "Usage: .... "
exit
fi
env=$1
prop=$2
text="$3"
file=$4
while read -r line
do
case "$line" in
"$env"* )
toggle=1
;;
esac
if [ "$toggle" = 1 ];then
if [[ $line =~ "$prop" ]] ;then
line="${line%%\"*}\"$text\""
toggle=0
fi
fi
echo "$line"
done < $file > t
mv t $file
Your going to need at least one lookahead assertion. There are many ways to do it, but you should use something that knows how to parse this.
s/(\s*$env:\s*(?:(?!\s*[^\W_]+:)[^\n]*\s*)*\s*:$prop:[^\S\n]*")[^\n]*(")/$1$replacement$2/g
Mildly tested in perl:
use strict;
use warnings;
my $str = '
env1:
:prop1: "value1"
:prop2: "value2"
...
:propn: "valuen"
env2:
:prop1: "value1"
...
:prop2: "value2"
:prop3: "value3"
:propn: "valuen"
...
envn:
:prop1: "value1"
...
:prop3: "value3"
:propn: "valuen"
';
my ($env, $prop, $replacement) = ('(?:env2|envn)', 'prop1', 'this changed');
if ( $str =~ s/
(
\s*$env:\s*
(?: (?!\s*[^\W_]+:) [^\n]*\s* )*
\s*:$prop:[^\S\n]*
"
) [^\n]*
( " )
/$1$replacement$2/xg )
{
print "Found it!\n";
print $str;
}
Output:
Found it!
env1:
:prop1: "value1"
:prop2: "value2"
...
:propn: "valuen"
env2:
:prop1: "this changed"
...
:prop2: "value2"
:prop3: "value3"
:propn: "valuen"
...
envn:
:prop1: "this changed"
...
:prop3: "value3"
:propn: "valuen"
精彩评论