My sed command to insert lines into a file is not working - is special characters the issue? - bash

I am trying to add a couple of lines in a text file with sed.
I think I have special characters that are giving me the issue.
I want to insert lines between
username: system:node:{{EC2PrivateDNSName}}
and
kind: ConfigMap
This is what I want to insert -
- groups:
- eks-role
- system:master
rolearn: arn:aws:iam::xxxxx:role/eks
username: eks
mapUsers: |
- userarn: arn:aws:iam::xxxxx:user/test-ecr
username: test-ecr
groups:
- eks-role
I have also tried using forward slashes around the special characters to no avail.
Here is the sed command I have now that does not work - it seems not to insert anything. I assume it can't find the line "username: system:node:{{EC2PrivateDNSName}}".
sed '/^username\: system:node\:{{EC2PrivateDNSName}}$/r'<(
echo " - groups:"
echo " - eks-role"
echo " - system:master"
echo " rolearn: arn:aws:iam::xxxxx:role/eks"
echo " username: eks"
echo " mapUsers: |"
echo " - userarn: arn:aws:iam::xxxxx:user/test-ecr"
echo " username: ecr"
echo " groups:"
echo " - eks-role"
) -i -- temp-aws-auth.yaml
Here is the contents of the file that I want to insert into -
apiVersion: v1
data:
mapRoles: |
- groups:
- system:bootstrappers
- system:nodes
rolearn: arn:aws:iam::xxxxx:role/eksctl-ops-nodegroup-linux-ng-sys-NodeInstanceRole-763ALQD2ZGXK
username: system:node:{{EC2PrivateDNSName}}
kind: ConfigMap
metadata:
creationTimestamp: "2020-12-09T15:54:56Z"
name: aws-auth
namespace: kube-system
resourceVersion: "1298"

UPDATE: Taking into consideration OPs answer/comment re: missing spaces, and a bit more fiddling, I was able to get the following sed command to work, too:
sed '/^.*username.*EC2PrivateDNSName.*$/r'<(cat replace.txt) temp-aws-auth.yaml
Assumptions:
OP is unable to use a yaml-aware tool to perform the edit
username ... EC2PrivateDNSName only shows up in one place in the file (or, alternatively, it shows up in multiple places and OP wishes to add a new line after each occurrence)
Replacement data:
$ cat replace.txt
- groups:
- eks-role
- system:master
rolearn: arn:aws:iam::xxxxx:role/eks
username: eks
mapUsers: |
- userarn: arn:aws:iam::xxxxx:user/test-ecr
username: test-ecr
groups:
- eks-role
NOTE: If the replacement data is in a variable it can fed into awk as a herestring.
One awk idea:
awk '
FNR==NR { a[FNR]=$0 # store first file (replacement data) into an array
next } # skip to next line in first file
{ print } # print current line of second file
/username.*EC2PrivateDNSName/ { for (i in a) # if we found our match then dump the contents of array a[] to stdout
print a[i]
next
}
' replace.txt temp-aws-auth.yaml
Or as a single-line:
awk 'FNR==NR {a[FNR]=$0; next} {print} /username.*EC2PrivateDNSName/ { for (i in a) print a[i]; next}' replace.txt temp-aws-auth.yaml
This generates:
apiVersion: v1
data:
mapRoles: |
- groups:
- system:bootstrappers
- system:nodes
rolearn: arn:aws:iam::xxxxx:role/eksctl-ops-nodegroup-linux-ng-sys-NodeInstanceRole-763ALQD2ZGXK
username: system:node:{{EC2PrivateDNSName}}
- groups:
- eks-role
- system:master
rolearn: arn:aws:iam::xxxxx:role/eks
username: eks
mapUsers: |
- userarn: arn:aws:iam::xxxxx:user/test-ecr
username: test-ecr
groups:
- eks-role
kind: ConfigMap
metadata:
creationTimestamp: "2020-12-09T15:54:56Z"
name: aws-auth
namespace: kube-system
resourceVersion: "1298"

I found out the issue with my original command - Sed needs the spaces included in the line it is looking for!
Since the line I was looking for has spaces in it :
' username: system:node:{{EC2PrivateDNSName}}'
I had to add the spaces to my sed statement :
sed '/^ username\: system:node\:{{EC2PrivateDNSName}}$/r'<(
Thanks for the feedback!
Happy holidays!!

This might work for you (GNU sed & cat):
cat <<\! |sed ':a;/username: system:node:{{EC2PrivateDNSName}}/{n;/kind: ConfigMap/!ba;h;s/.*/cat -/ep;g}' file
- groups:
- eks-role
- system:master
rolearn: arn:aws:iam::xxxxx:role/eks
username: eks
mapUsers: |
- userarn: arn:aws:iam::xxxxx:user/test-ecr
username: test-ecr
groups:
- eks-role
!
Make a here-document with the lines to be inserted.
Pipe the here-document through to a sed command.
If a line contains username: system:node:{{EC2PrivateDNSName}}, print it and fetch the next line.
If the following line does not contain kind: ConfigMap go to the start of the sed cycle and start again.
Otherwise, copy the current line, replace/print the current line by the lines to inserted from the here-document and then over write the replacement by the copy in the hold space.
N.B. The replacement lines are inserted into the document by way of the substitute command and the e flag, that evaluates what is substituted into the pattern space i.e. the cat - that is the here-document that is passed through via the pipe.

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}"

Trailing newline included in variable assignment after grep, and sent to GitHub actions output

I've got a bash script in a GitHub action that greps for image tags, then replaces them in a docker-compose.yml.
# docker-compose.yml from a JSON payload
docker_compose='"services:\n api:\n proxy: ghcr.io/org/api:main-4095094301\n ports:\n - 0:80\n restart: always\n\n proxy:\n image: ghcr.io/org/proxy:main-4095124301\n depends_on:\n - api\n environment:\n - \"ENVIRONMENT=dev\"\n - \"PORT=8000\"\n ports:\n - 0:3000\n restart: always\n\n ..."'
# Extract the tag of each image, send to GitHub actions output
echo "proxy_tag=$(echo -n $docker_compose | grep -oP 'proxy:\K[-\w]+')" >> $GITHUB_OUTPUT
If I remove the >> operator to echo the output, everything looks OK:
proxy_tag=main-4095094301
But when I feed it into sed later in a different step of the pipeline, an extra newline character seems to come from nowhere:
echo "running s/PROXY_TAG/$proxy_tag/"
sed -i "s/PROXY_TAG/$proxy_tag/" docker-compose.yml
# running s/PROXY_TAG/main-4095094301
# /
# sed: -e expression #1, char 18: unterminated `s' command
# Error: Process completed with exit code 1.
I've tried some common suggestions eg. piping output through tr -d '\n':
echo "proxy_tag=$(echo -n $docker_compose | grep -oP 'proxy:\K[-\w]+' | tr -d '\n')" >> $GITHUB_OUTPUT
Is there something missing I don't understand about bash vars or Github actions?
See below a more complete context of how where these commands are being used.
name: Update Docker Compose
on:
workflow_dispatch:
inputs:
proxy_tag:
description: proxy_tag
type: string
required: false
# api_tag:
# site_tag:
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout#v3
- name: Fetch docker-compose
id: get-tags
run: |
docker_compose=$(curl ...)
echo "proxy_tag=$(echo -n $docker_compose | grep -oP 'proxy:\K[-\w]+')" >> $GITHUB_OUTPUT
echo "api_tag=$(echo $docker_compose | grep -oP 'api:\K[-\w]+')" >> $GITHUB_OUTPUT
echo "site_tag=$(echo $docker_compose | grep -oP 'site:\K[-\w]+')" >> $GITHUB_OUTPUT
- name: Replace tags docker-compose
env:
proxy_tag: >
${{ github.event_name == 'workflow_dispatch'
&& github.event.inputs.proxy_tag
|| steps.get-tags.outputs.proxy_tag
|| 'latest'
}}
# api_tag: >
# site_tag: >
run: |
echo "Setting [$proxy_tag]"
sed -i "s/PROXY_TAG/$proxy_tag/" docker-compose.yml
sed -i "s/API_TAG/$api_tag/" docker-compose.yml
sed -i "s/SITE_TAG/$site_tag/" docker-compose.yml
# Outputs:
# Setting [main-4095094301 # <--- notice the newline
# ]
#
# sed: -e expression #1, char 18: unterminated `s' command
# Error: Process completed with exit code 1.
I suspect that the newline character being added is from the YAML's folding using > while setting your environment variables under env. Try >- to not add (strip) the last newline character.
This (https://yaml-multiline.info/) might be helpful for live experimentation with YAML.
Worst case, if you're using bash,
$: proxy_tag="main-4095094301
" # embedded newline for example
$: echo "[$proxy_tag]" # show with embedded newline
[main-4095094301
]
$: echo "[${proxy_tag%$'\n'}]" # show with embedded newline removed
[main-4095094301]
$: echo PROXY_TAG | sed "s/PROXY_TAG/$proxy_tag/" # syntax error with embedded newline
sed: -e expression #1, char 27: unterminated `s' command
$: echo PROXY_TAG | sed "s/PROXY_TAG/${proxy_tag%$'\n'}/" # removed, syntax ok
main-4095094301
That seems error-prone to me, though, and I prefer to do most of my basic string processing right in the interpreter unless it's huge and I need it faster, so I'd probably do something based on this -
$: docker_compose=$'services:\n api:\n proxy: ghcr.io/org/api:main-4095094301\n ports:\n - 0:80\n restart: always\n\n proxy:\n image: ghcr.io/org/proxy:main-4095124301\n depends_on:\n - api\n environment:\n - "ENVIRONMENT=dev"\n - "PORT=8000"\n ports:\n - 0:3000\n restart: always\n\n ...' # note $'...' for clean quotes and newlines
$: echo "[$docker_compose]"
[services:
api:
proxy: ghcr.io/org/api:main-4095094301
ports:
- 0:80
restart: always
proxy:
image: ghcr.io/org/proxy:main-4095124301
depends_on:
- api
environment:
- "ENVIRONMENT=dev"
- "PORT=8000"
ports:
- 0:3000
restart: always
...]
$: shopt -s extglob # enable extended globbing
$: tmp="${docker_compose##* proxy: *([^:]):}" # trim front - *(...) is "0 or more"
$: proxy_tag="${tmp%%$'\n'*}" # trim after, from 1st newline on
$: echo "[$proxy_tag]" # show that it's clean
[main-4095094301]

How do I embed YAML inside a YAML as text in bash script?

I am assembling a YAML build pipeline using bash as follows.
cat <<EOT >> ${BUILD_SOURCESDIRECTORY}/azure-pipelines.yml
- template: templates/deploy-to-env-ssh.yml#infrastructure
parameters:
dockerHostEndpoint: ${DOCKER_HOST}
jobName: ${BASENAME}
stackName: ${STACK_NAME}
composeFile: ${STACK_NAME}.yml
schedule: ???
$(cat schedule.yml)
tz: ${TZ}
EOT
What I want is to store the following YAML into schedule as a string which I can reuse in a another part of the pipeline.
version: 1.4
jobs:
DockerJob:
cmd: docker ps
time: "*"
notifyOnSuccess:
- type: stdout
data:
- stdout
But it seems it needs to be indented.
You can use the pr utility:
cat <<EOF >> ${BUILD_SOURCESDIRECTORY}/azure-pipelines.yml
- template: templates/deploy-to-env-ssh.yml#infrastructure
parameters:
dockerHostEndpoint: ${DOCKER_HOST}
jobName: ${BASENAME}
stackName: ${STACK_NAME}
composeFile: ${STACK_NAME}.yml
schedule: $(printf "\n" && pr -to 8 schedule.yml)
tz: ${TZ}
EOT
I use printf "\n" because you'd need to place the $(…) at the first column if you want to write $(…) on a new line, since every line including the first one will be offset by the given number of spaces.

kubectl YML : what type of file is this and how to run it

I can see a way to create kubectl command like this where I can pass some parameterize values ad well.
My question is, with what type of file we will save this, Is this a bash script? and how to run and supply the parameter?
export SECRET_NAME="my-app.secret"
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Secret
metadata:
name: $SECRET_NAME
type: Opaque
data:
password: $(echo -n "s33msi4" | base64 -w0)
username: $(echo -n "jane" | base64 -w0)
EOF
Yes, it can be treated as a bash script. As Jetchisel already mentioned in his comment, it contains a structure called Here Document used with cat command but it also contains export command which sets and exports a variable. So as a whole it can be treated as a simple bash script with 2 instructions.
In order to run it and create a new Secret object (which is your ultimate goal), follow these steps:
Fix the indentation which is crucial in yaml files:
export SECRET_NAME="my-app.secret"
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Secret
metadata:
name: $SECRET_NAME
type: Opaque
data:
password: $(echo -n "s33msi4" | base64 -w0)
username: $(echo -n "jane" | base64 -w0)
EOF
Save the above content as a file. You can call it secret.sh.
Source it (source and . are the same command):
. secret.sh
You should see the following message:
secret/my-app.secret created
Alternatvely you can paste it directly into the console. As you can see, it also works:
### don't copy it, this is the example console output
### you will see once you paste the above script in your bash shell
$ export SECRET_NAME="my-app.secret"
$
$ cat <<EOF | kubectl apply -f -
> apiVersion: v1
> kind: Secret
> metadata:
> name: $SECRET_NAME
> type: Opaque
> data:
> password: $(echo -n "s33msi4" | base64 -w0)
> username: $(echo -n "jane" | base64 -w0)
> EOF
secret/my-app.secret created

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

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.

Resources