Bash: Replace each occurrence of string in file with next value from array - bash

I have an .yml file with three address entries and some other data, and an array containing three new addresses to replace these with
file.yml:
[...]
- address: s1.example.com
user: ubuntu
role: storage
- address: w1.example.com
user: ubuntu
role: worker
- address: w2.example.com
user: ubuntu
role: worker
[...]
array:
addr[0]: storage.domain.com
addr[1]: worker1.domain.com
addr[2]: worker2.domain.com
expected result:
[...]
- address: storage.domain.com
user: ubuntu
role: storage
- address: worker1.domain.com
user: ubuntu
role: worker
- address: worker2.domain.com
user: ubuntu
role: worker
[...]
I'm using sed, as I would like to write the new lines directly to the original file
I have tried a number of times, but the array incrementing always fails
Attempt 1
sed -i "s/- address: .*/- address: ${addr[$i]}/g" file.yml
This seems to exclusively write the first item in the array:
- address: storage.domain.com
[...]
- address: storage.domain.com
[...]
- address: storage.domain.com
[...]
Attempt 2
if (grep -e "- address:" file.yml); then
sed -i "s/- address: .*/- address: ${addr[$i]}/g"
((i++))
fi
This seems to grep all results at the same time, and forwards nothing to sed as I haven't figured that one out yet.
Output:
- address: s1.example.com
- address: w1.example.com
- address: w2.example.com
sed: no input files

Recently I started doing this with similar jobs:
Print all array members with some separator, ex. :. Put this as the first input to sed.
The first line (ie. the array members printed with some separator) are put into hold space
Then for each the - address: found:
I copy the hold space into pattern space, extract the first element of the array and append it with - address :. And print.
And remove the first element from hold space
The script below:
# replicate input
cat <<EOF >file.yml
[...]
- address: s1.example.com
user: ubuntu
role: storage
- address: w1.example.com
user: ubuntu
role: worker
- address: w2.example.com
user: ubuntu
role: worker
[...]
EOF
addr[0]=storage.domain.com
addr[1]=worker1.domain.com
addr[2]=worker2.domain.com
# the sed script
sed -nE '
1{
# hold the first line with array members separated with :
H
# don't print anything
d
}
# if the line is address
/- address: .*/{
g
s/\n//
# remove all except first array member from hold space
s/:.*//
# prepend it with _- address_
s/^/- address: /
# remove the first member from hold space
x
s/[^:]*://
x
}
# print the output
p
' <( IFS=':'; printf "%s\n" "${addr[*]}"; ) file.yml
and the same oneliner:
sed -n '1{;H;d};/- address: .*/{g;s/\n//;s/:.*//;s/^/- address: /;x;s/[^:]*://;x};p' <( IFS=':'; printf "%s\n" "${addr[*]}"; ) file.yml
will output:
[...]
- address: storage.domain.com
user: ubuntu
role: storage
- address: worker1.domain.com
user: ubuntu
role: worker
- address: worker2.domain.com
user: ubuntu
role: worker
[...]

#! /bin/bash
# initialise the array the way you want
addr[0]="storage.domain.com"
addr[1]="worker1.domain.com"
addr[2]="worker2.domain.com"
awk -F: -v addr="${addr[*]}" ' BEGIN{count=0 ; split(addr, addr_array, " ") }
{
if( $1 ~ /address/ ) {
for(i=1;i<=NF-1;++i){
printf "%s:", $i
}
printf "%s\n", addr_array[++count]
}
else
{
print
}
}' file.yml
If you want to overwrite the original file,
addr[0]="storage.domain.com"
addr[1]="worker1.domain.com"
addr[2]="worker2.domain.com"
gawk -i inplace -F: -v addr="${addr[*]}" ' BEGIN{count=0 ; split(addr, addr_array, " ") }
{
if( $1 ~ /address/ ) {
for(i=1;i<=NF-1;++i){
printf "%s:", $i
}
printf "%s\n", addr_array[++count]
}
else
{
print
}
}' file.yml

One possible solution is as follows:
addr=(storage.domain.com worker1.domain.com worker2.domain.com)
i=0
line_count=1
while IFS= read -r line; do
if [[ $line == *"- address:"* ]]; then
sed -i "${line_count}s/- address: .*/- address: ${addr[i]}/" file.yml
i=$((i+1))
fi
line_count=$((line_count+1))
done < file.yml
Above script iterates through the file, line by line and then replace the matching line with the content of the array.

This might work for you (GNU sed):
printf ": %s\n" "${addr[#]}" |
sed '/address:/R /dev/stdin' ymlFile |
sed '/address:/{N;s/:.*:/:/}'
Print the addr array out to stdout so that each address is on a separate line.
In the first invocation of sed, insert each address on a separate line following the regexp address:.
In the second invocation of sed remove the old addresses.

Related

bash search and replace a line after a certain line

I have a big yaml file containing multiple declaration blocks, related to different services.
The structure is similar to the following (but repeated for multiple applications):
- name: commerce-api
type: helm
version: 0.0.5
I would like to find the block of code that is containing commerce-api and replace the version property value with something else.
The thing is, I wrote this script:
bumpConfig() {
LINE=$(awk "/- name: $1$/{print NR + $2}" "$CONFIG_YML")
sed -i "" -E "${LINE}s/version: $3.*$/\version: $4/" "$CONFIG_YML"
}
bumpConfig "commerce-api" 2 "$OLD_APP_VERSION" "$NEW_APP_VERSION"
Which is kind of allowing me to do what I want, but the only problem is that, the property version is not always on the third line.
How can I make my script to look for the first occurrence of version given the service name to be commerce-api?
Is this even possible using awk?
Adding some variation to the input file:
$ cat config.yml
- name: commerce-api-skip
type: helm
version: 0.0.5
- name: commerce-api
type: helm
bogus line1: bogus value1
version: 0.0.5
bogus line2: bogus value2
- name: commerce-api-skip-too
type: helm
version: 0.0.5
One awk idea:
bumpConfig() {
awk -v name="$1" -v old="$2" -v new="$3" '
/- name: / { replace=0
if ($NF == name)
replace=1
}
replace && $1=="version:" { if ($NF == old)
$0=substr($0,1,index($0,old)-1) new
}
1
' "${CONFIG_YML}"
}
Taking for a test drive:
CONFIG_YML='config.yml'
name='commerce-api'
OLD_APP_VERSION='0.0.5'
NEW_APP_VERSION='0.0.7'
bumpConfig "${name}" "${OLD_APP_VERSION}" "${NEW_APP_VERSION}"
This generates:
- name: commerce-api-skip
type: helm
version: 0.0.5
- name: commerce-api
type: helm
bogus line1: bogus value1
version: 0.0.7
bogus line2: bogus value2
- name: commerce-api-skip-too
type: helm
version: 0.0.5
Once OP is satisfied with the result:
if running GNU awk the file can be updated 'in place' via: awk -i inplace -v name="$1" ...
otherwise the output can be saved to a temp file and then copy the temp file over the original: awk -v name="$1" ... > tmpfile; mv tmpfile "${CONFIG_YML}"
Entirely in sed
sed -i '' "s/^version: $3/version: $4/' "$CONFIG_YML"
/^- name: $1\$/,/^- name:/ restricts the s command to just the lines between the requested name and the next - name: line.
#!/bin/bash
OLD_APP_VERSION=0.0.5
NEW_APP_VERSION=0.0.7
CONFIG_YML=config.yml
bumpConfig() {
gawk -i inplace -v name="$1" -v old="$2" -v new="$3" '
1
/^- name: / && $3 == name {
while (getline > 0) {
if (/^ version: / && $2 == old)
$0 = " version: " new
print
if (!NF || /^-/ || /^ version: /)
break
}
}
' "${CONFIG_YML}"
}
bumpConfig commerce-api "${OLD_APP_VERSION}" "${NEW_APP_VERSION}"

Running NSLOOKUP against some urls to print out url that is not equals to some IPS i have

I have some Web URLs I am trying to do a nslookup against. All it does is check against the URL and print the ones not equals to some certain IP Address into a file. I am able to do it for one IP Address but i tried adding one more, and I am unable to get it to work.
SUB='.com'
for address in `cat linux.hosts`; do
if [[ "$address" == *"$SUB"* ]]; then
echo "Got [$address]"
nslookup $address \
| awk '!/155.55.66.55/ || !/155.55.66.54/' >> com.txt
fi
done
The linux.hosts file contains info like this
A B B { D E google.com }
A B B { D E twitter.com }
A B B { D E microsoft.com }
A B B { D E facebook.com }
I only want to get the string that has ".com" in it and do a nslookup that doesn't contain a certain IP Address.
The $nslookup address returns
Got [google.com]
Server: BBBB:BBBB:BBBB:BBBB::1
Address: BBBB:BBBB:BBBB:BBBB::1#53
Non-authoritative answer:
Name: google.com
Address: 155.55.66.55
Name: google.com
Address: 155.55.66.54
The | awk '!/155.55.66.55/ || ' >> com.txt works for some of the address that only contains 155.55.66.55 but if contains 155.55.66.54 it does not work, hence i am trying to add another check.
I only want to print the domains with address that doesn't contain
155.55.66.54 and 155.55.66.55.
The $nslookup address should only return
Got [twitter.com]
Server: BBBB:BBBB:BBBB:BBBB::1
Address: BBBB:BBBB:BBBB:BBBB::1#53
Non-authoritative answer:
Name: google.com
Address: 198.168.101.1
Name: google.com
Address: 198.168.101.2
Don't read lines with for.
Also, probably redirect only once, after the loop, to avoid having the shell opening the file and seeking to the end repeatedly inside the loop.
As a minor optimization, use grep to find the matching lines in the file before the main loop.
grep -F '.com' linux.hosts |
while read -r _ _ _ _ _ _ address _; do
nslookup "$address" |
awk '!/155.55.66.55/ || !/155.55.66.54/'
done >>com.txt # or maybe > com.txt if you want to overwrite

Bash Replace Variable IP Address with Network

I have an IP address set in a variable that I'd like to convert into a network address.
This only works for a single digit:
echo '192.168.1.2' | sed 's/.$/0/' => 192.168.1.0
echo '192.168.1.22' | sed 's/.$/0/' => 192.168.1.20
echo '192.168.1.223' | sed 's/.$/0/' => 192.168.1.220
I need a method to return the same network value if the last digit(s) change, i.e:
myip="192.168.1.2" => "192.168.1.0"
myip="192.168.1.22" => "192.168.1.0"
myip="192.168.1.223" => "192.168.1.0"
How can I replace any IP address with it's network address like above?
Pure bash solution without external commands:
echo "${myip%.*}.0"
for example:
$ echo "$myip"
192.168.1.22
$ echo "${myip%.*}.0"
192.168.1.0
Using sed
echo '192.168.1.2' | sed 's/\.[^.]*$/.0/'
sed 's/\.[^.]*$/.0/' <<< 192.168.1.22 # echo + pipe is not needed here
Logic: Replace everything from last . till end with .0
Using awk
awk -F. '{$NF=0}1' OFS=. <<< 192.168.1.22
awk '{$NF=0}1' FS=. OFS=. <<< 192.168.1.22
Logic: Split string with . and set last field to 0.
pure bash:
{ IFS=. read a b c _; echo $a.$b.$c.0; } <<< 192.168.1.22
( IFS=.; read -a ip; ip[3]=0; echo "${ip[*]}"; ) <<< 192.168.1.22
Logic: Read 4 parts of the IP address in 4 variables. Print first 3 and a 0.
Or by using a bash array, if you don't want to clutter environment with too many variables.
You can do this with awk using:
pax> awk -F. '{print $1"."$2"."$3".0"}' <<<12.34.56.78
12.34.56.0
With sed, it's possible to just replace all the digits at the end:
pax sed 's/[0-9]*$/0/' <<<12.34.56.78
12.34.56.0
However, all of those result in an extra process being started up, not something you need to worry about for a few IP addresses but it will make a difference if you're converting many of them.
To do it within bash only (not requiring another process), you can use:
pax> ip=12.34.56.78
pax> echo ${ip%.[0-9]*}.0
12.34.56.0
It is very simple to do with pure bash:
myip="192.168.1.2 "; echo "$myip ==> ${myip%.*}.0"
myip="192.168.1.22 "; echo "$myip ==> ${myip%.*}.0"
myip="192.168.1.223"; echo "$myip ==> ${myip%.*}.0"
Results in:
192.168.1.2 ==> 192.168.1.0
192.168.1.22 ==> 192.168.1.0
192.168.1.223 ==> 192.168.1.0
However, that is assuming the network has a CDIR of 24 (192.168.1.2/24).
If that is not what you will always use, this idea will break.

Advanced AWK formatting

I am having problem using this awk command . It is not producing the result I want giving this input file. Can someone help me with this please?
I am searching for "Class:" value of "ABC". When I find ABC . I like to assign the values associated with userName/servicelist/hostlist and port number to variables. ( please see output section ) to
awk -v q="\"" '/ABC/{f=1;c++}
f && /userName|serviceList|hostList|portNumber/
{sub(":",c"=",$1);
print $1 q $3 q
}
/port:/{f=0;print ""}' filename
The file contains the following input
Instance: Ths is a test
Class: ABC
Variables:
udpRecvBufSize: Numeric: 8190000
userName: String:test1
pingInterval: Numeric: 2
blockedServiceList: String:
acceptAllServices: Boolean: False
serviceList: String: ABC
hostList: String: 159.220.108.3
protocol: String: JJJJ
portNumber: Numeric: 20001
port: String: RTR_LLLL
Children:
Instance: The First Server in the Loop
Class: Servers
Variables:
pendout: Numeric: 0
overflows: Counter: 0
peakBufferUsage: Numeric: 100
bufferPercentage: Gauge: 1 (0,100)
currentBufferUsage: Numeric: 1
pendingBytesOut: Numeric: 0
pendingBytesIn: Numeric: 1
pingsReceived: Counter: 13597
pingsSent: Counter: 87350
clientToServerPings: Boolean: True
serverToClientPings: Boolean: True
numInputBuffers: Numeric: 10
maxOutputBuffers: Numeric: 100
guaranteedOutputBuffers: Numeric: 100
lastOutageDuration: String: 0:00:00:00
peakDisconnectTime: String:
totalDisconnectTime: String: 0:00:00:00
disconnectTime: String:
disconnectChannel: Boolean: False
enableDacsPermTest: Boolean: False
enableFirewall: Boolean: False
dacsPermDenied: Counter: 0
dacsDomain: String:
compressPercentage: Gauge: 0 (0,100)
uncompBytesSentRate: Gauge: 0 (0,9223372036854775807)
Instance: Ths is a test
Class: ABC
Variables:
udpRecvBufSize: Numeric: 8190000
userName: String:test2
pingInterval: Numeric: 4
blockedServiceList: String:
acceptAllServices: Boolean: False
serviceList: String: DEF
hostList: String: 159.220.111.2
protocol: String: ffff
portNumber: Numeric: 20004
port: String: JJJ_LLLL
Children:
This is the output I am looking for . Assigning variables
userName1="test1"
serviceList1="ABC"
hostList1="159.220.108.3"
portNumber1="2001"
userName2="test2"
serviceList2="DEF"
hostList2="159.220.111.2"
portNumber2="2004"
If your intention is to assign to a series of variables, then rather than parsing the whole file at once, perhaps you could just extract the specific parts that you're interested in one by one. For example:
$ awk -F'\n' -v RS= -v record=1 -v var=userName 'NR == record { for (i=1; i<=NF; ++i) if (sub("^\\s*" var ".*:\\s*", "", $i)) print $i }' file
test1
$ awk -F'\n' -v RS= -v record=1 -v var=serviceList 'NR == record { for (i=1; i<=NF; ++i) if (sub("^\\s*" var ".*:\\s*", "", $i)) print $i }' file
ABC
The awk script could be put inside a shell function and used like this:
parse_file() {
record=$1
var=$2
file=$3
awk -F'\n' -v RS= -v record="$record" -v var="$var" 'NR == record {
for (i=1; i<=NF; ++i) if (sub("^\\s*" var ".*:\\s*", "", $i)) print $i
}' "$file"
}
userName1=$(parse_file 1 userName file)
serviceList1=$(parse_file 1 serviceList file)
# etc.
$ awk -F: -v q="\"" '/Class: ABC/{f=1;c++;print ""} \
f && /userName|serviceList|hostList|portNumber/ \
{gsub(/ /,"",$1); \
gsub(/ /,"",$3); \
print $1 c "=" q $3 q} \
/Children:/{f=0}' vars
userName1="test1"
serviceList1="ABC"
hostList1="159.220.108.3"
portNumber1="20001"
userName2="test2"
serviceList2="DEF"
hostList2="159.220.111.2"
portNumber2="20004"
it will increment the counter for each "Class: ABC" pattern and set a flag. Will format and print the selected entries until the terminal pattern for the block. This limits the context between the two patterns.
Assuming bash 4.0 or newer, there's no need for awk here at all:
flush() {
if (( ${#hostvars[#]} )); then
for varname in userName serviceList hostList portNumber; do
[[ ${hostvars[$varname]} ]] && {
printf '%q=%q\n' "$varname" "${hostvars[$varname]}"
}
done
printf '\n'
fi
hostvars=( )
}
class=
declare -A hostvars=( )
while read -r line; do
[[ $line = *"Class: "* ]] && class=${line#*"Class: "}
[[ $class = ABC ]] || continue
case $line in
*:*:*)
IFS=$': \t' read varName varType value <<<"$line"
hostvars[$varName]=$value
;;
*"Variables:"*)
flush
;;
esac
done
flush
Notable points:
The full set of defined variables are collected in the hostvars associative array (what other languages might call a "map" or "hash"), even though we're only printing the four names defined to be of interest. More interesting logic could thus be defined that combined multiple variables to decide what to output, &c.
The flush function is defined outside the loop so it can be used in multiple places -- both when starting a new block (as detected, here, by seeing Variables:), and when at the end-of-file.
The output varies from what you requested in that it includes quotes only if necessary -- but that quoting is guaranteed to be correct and sufficient for bash to parse without room for security holes even if the strings being emitted would otherwise contain security-relevant content. Think about correctly handling a case where serviceList contains $(rm -rf /*)'$(rm -rf /*)' (the duplication being present to escape single quotes); printf %q makes this easy, whereas awk has no equivalent.
Solution in TXR:
#(collect)
#(skip)Class: ABC
Variables:
# (gather)
userName: String:#user
serviceList: String: #servicelist
hostList: String: #hostlist
portNumber: Numeric: #port
# (until)
Children:
# (end)
#(end)
#(deffilter shell-esc
("\"" "\\\"") ("$" "\\$") ("`" "\\'")
("\\" "\\\\"))
#(output :filter shell-esc)
# (repeat :counter i)
userName#(succ i)="#user"
serviceList#(succ i)="#servicelist"
hostList#(succ i)="#hostlist"
portNumber#(succ i)="#port"
# (end)
#(end)
Run:
$ txr data.txr data
userName1="test1"
serviceList1="ABC"
hostList1="159.220.108.3"
portNumber1="20001"
userName2="test2"
serviceList2="DEF"
hostList2="159.220.111.2"
portNumber2="20004"
Note 1: Escaping is necessary if the data may contain characters which are special between quotes in the target language. The shell-esc filter is based on the assumption that the generated variable assignments are shell syntax. It can easily be replaced.
Note 2: The code assumes that each Class: ABC has all of the required variables present. It will not work right if some are missing, and there are two ways to address it by tweaking the #(gather) line:
failure:
#(gather :vars (user servicelist hostlist port))
Meaning: fail if any of these four variables are not gathered. The consequence is that the entire Class: ABC section with missing variables is skipped.
default missing:
#(gather :vars (user (servicelist "ABC") hostlist port))
Meaning: must gather the four variables user, servicelist, hostlist and port. However, if serviceList is missing, then it gets the default value "ABC" and is treated as if it had been found.

How do I represent a newline character in bash inline string shell replacement?

I can modify a string like the following:
mod=${orig//[xyz]/_}
It will replace all occurances of [xyz] with _
But if I have a string such as eth0 eth1 eth2, how do I replace ' ' space with newline \n. The following does not work.
orig="eth0 eth1 eth2"
mod=${orig// /\n}
This is how I'm planning to use it:
VRRP_INTERFACE="${VRRP_INTERFACE:-ib0}"
VRRP_ADDITIONAL_INTERFACES="${VRRP_ADDITIONAL_INTERFACES// /\\n}"
cat << EOF > /etc/keepalived/keepalived.conf
vrrp_instance VI_1 {
interface ${VRRP_INTERFACE}
state BACKUP
virtual_router_id ${VRRP_ROUTER_ID}
priority ${VRRP_PRIORITY}
advert_int 1
# Monitor these as well
track_interface {
$VRRP_ADDITIONAL_INTERFACES
}
virtual_ipaddress {
${VRRP_VIP} dev ${VRRP_INTERFACE} label ${VRRP_INTERFACE}:1
}
EOF
I note that if I use
echo -e $VRRP_ADDITIONAL_INTERFACES it works when the string contains a "\n" in the string. i.e. replace with \n rather than \n.
But in the case of using cat << EOF > filename format it doesn't work.
Use $'\n':
$ orig="eth0 eth1 eth2"
$ mod=${orig// /$'\n'}
$ echo "$mod"
eth0
eth1
eth2
This approach puts actual newline characters into the string mod.
Using cat << EOF
Consider this shell script which uses the same substitution:
orig="eth0 eth1 eth2"
mod=${orig// /$'\n'}
cat << EOF > test.txt
track_interface {
$mod
}
EOF
cat test.txt
When run, this is the output:
$ bash script.sh
track_interface {
eth0
eth1
eth2
}
Note that this approach requires bash. Thus, on debian-like systems (for which sh is dash), the following will not work:
$ sh script.sh
matt.sh: 2: script.sh: Bad substitution
I figured out a method. It's not as pretty but works.
Assign the cat to a variable FILE.
Then echo -e "$FILE" > filename
i.e.
VRRP_INTERFACE="${VRRP_INTERFACE:-ib0}"
VRRP_ADDITIONAL_INTERFACES="${VRRP_ADDITIONAL_INTERFACES// /'\n'}"
FILE=$(cat << EOF
vrrp_instance VI_1 {
interface ${VRRP_INTERFACE}
state BACKUP
virtual_router_id ${VRRP_ROUTER_ID}
priority ${VRRP_PRIORITY}
advert_int 1
# Monitor these as well
track_interface {
$VRRP_ADDITIONAL_INTERFACES
}
virtual_ipaddress {
${VRRP_VIP} dev ${VRRP_INTERFACE} label ${VRRP_INTERFACE}:1
}
EOF
)
echo -e "$FILE" > /etc/keepalived/keepalived.conf
#John1024 solution, works well
orig="eth0 eth1 eth2"
mod=${orig// /$'\n'}
cat << EOF > test.txt
track_interface {
$mod
}
EOF

Resources