How to avoid Shell script Sed modifying password variable contents - regex

When i am trying to sed as below
sed -e 's,</Context>,<Resource name="ABC" password="'"$DB_PASS"'"/>\n&,' -i /path
sed is truncating backslash.
For example
DB_PASS='1!2#3#4$5%6^7&8*9(0)[{]}\|'
O/p is
<Resource name="jdbc/KARDB"
password=""1!2#3#4$5%6^7</Context>8*9(0)[{]}|""/> </Context>
if my DB_PASS contains backslash, &, single quote double quote anyother spl characters i dont want sed to change password contents. but substitute as it is.
Thanks,
Kusuma

The specific problem you run into is that your password contains &, which in the replacement part of an s command refers to the matched text.
As has become a litany of mine, it is generally not a good idea to substitute shell variables into sed code precisely for reasons like these: sed cannot differentiate between code and your data. This is one of the more harmless things that could go wrong; imagine what GNU sed would have done if someone had entered a password like rm -Rf /,e #.
A direct replacement with GNU awk (it has to be gawk because RT is GNU-specific) could be
DB_PASS="$DB_PASS" gawk -v RS='</Context>' '{ printf $0; if(RT == RS) { print "<Resource name=\"jdbc/KARDB\" password=\"" ENVIRON["DB_PASS"] "\"/>" } printf RT }'
The usual -v dbpass="$DB_PASS" trick does not work here because you don't even want escape sequences interpreted, so we forcefully add DB_PASS to awk's environment and take it from there. Add -i inplace to awk's options if you want the file to be changed in place and you have GNU awk 4.1.0 or later, although I usually use cp file file~; awk ... file~ > file to have a backup in case things go wrong.
There's a problem, however: how do you handle " in passwords? Might be a good idea to do something like dbpass = ENVIRON["DB_PASS"]; gsub(/"/, """, dbpass); and use dbpass in the output. And the same for all other critical characters, such as < and >.
Since you appear to be parsing XML data, it would be better to use an XML-based tool, so that you don't run into problems when the Context tag is empty and written as <Context/>, and to avoid the above problem (which could be a rather big one). For example, with xmlstarlet you could do something like this:
xmlstarlet ed -s '//Context' -t elem -n Resource -i '//Context/Resource[not(#name)]' -t attr -n name -v 'jdbc/KARDB' -i '//Context/Resource[#name="jdbc/KARDB"]' -t attr -n password -v "$DB_PASS"
This consists of three steps:
-s '//Context' -t elem -n Resource
inserts an empty Resource subnode under every Context node (this is fine if there's only one; otherwise you might want to specify it in more detail)
-i '//Context/Resource[not(#name)]' -t attr -n name -v 'jdbc/KARDB'
inserts an attribute name="jdbc/KARDB" in every Context/Resource node that has no name (which is hopefully only the one we just inserted), and
-i '//Context/Resource[#name="jdbc/KARDB"]' -t attr -n password -v "$DB_PASS"
inserts a password attribute with the value of $DB_PASS (properly quoted) in every Resource node under a Context node whose name attribute has the value jdbc/KARDB (again, this should be only the node we just inserted). For a more perfect solution that inserts the whole node in one go, you'll want to take a closer look at xslt, but if you were happy with the original sed approach, none of the shortcomings of this approach should be a problem.

Related

How to use sed in shell script to replace all environment value occurrences with their current values

I would like to have a shell script to iterate over all the occurrences of environment variable names in a file and replace them with their current values. I am not sure how this can be done by using sed command.
The file content:
values:
value1:
name: "something"
value: "$ENV_VAR1" # this could be any variable name
value2:
name: "something"
value: "$ENV_VAR2"
...
First, I need to find all occurrences of any variable (Using regex "\$(.*?)" ). Then, somehow, I need to replace it with the variable value from the shell. I am not sure how I can use the sed command to achieve the second part as the variable name is specified in the file itself.
Something like the following command:
sed -i "s/\"\$(.*?)\"/${Some_How_Get_Var_Name}/g" file.yaml
This is a problem that comes up often. envsubst is commonly given as a solution, but I find it's easier to just stick with perl and do something like:
perl -pe 'while (my ($k, $v) = each %ENV) { s/\$$k/$v/g }'
This is almost certainly not a robust solution (it will replace $FOO, but it won't do replacements of the form ${FOO}), but I find I'm always disappointed that envsubst doesn't do ${FOO-bar}, and envsubst seems less ubiquitous than perl.
Or, rather than doing the replacement for everything in the environment, you might prefer something like:
perl -pe 's/\$([[:alpha:]_][_[:alnum:]]+)/$ENV{$1}/g'
or
perl -pe 's/\$([[:alpha:]_][\w]+)/$ENV{$1}/g'
These last two will replace '$FOO' with the empty string if FOO is not defined, while the first leaves it unreplaced. Which behavior you desire may drive the decision as to which to use.
I won't claim these are completely correct, but they are a reasonable approximation.
If You are using bash and the envsubst command is avaiable you can do:
envsubst < inputfile
E.g. (creating a temp input for demonstrating it:
$ env | tail -2 | sed 's_^_$_'
$MANPATH=/home/linuxbrew/.linuxbrew/share/man:
$INFOPATH=/home/linuxbrew/.linuxbrew/share/info:
Then running this through envsubst:
$ env | tail -2 | sed 's_^_$_' | envsubst
/home/linuxbrew/.linuxbrew/share/man:=/home/linuxbrew/.linuxbrew/share/man:
/home/linuxbrew/.linuxbrew/share/info:=/home/linuxbrew/.linuxbrew/share/info:
This might work for you (GNU sed):
sed '/value:/{y/"/\n/;s/^.*/printf "&"/e;y/\n/"/}' file
On any line containing the string value: convert any "'s to newlines, use printf to convert the environmental variables to their real values and reconvert the introduced newlines back to "'s.
N.B. If the environmental variable can contain "'s, these will need to be quoted following the printf command, i.e. insert s/"/\\"/g before the last y command.

sed / awk - remove space in file name

I'm trying to remove whitespace in file names and replace them.
Input:
echo "File Name1.xml File Name3 report.xml" | sed 's/[[:space:]]/__/g'
However the output
File__Name1.xml__File__Name3__report.xml
Desired output
File__Name1.xml File__Name3__report.xml
You named awk in the title of the question, didn't you?
$ echo "File Name1.xml File Name3 report.xml" | \
> awk -F'.xml *' '{for(i=1;i<=NF;i++){gsub(" ","_",$i); printf i<NF?$i ".xml ":"\n" }}'
File_Name1.xml File_Name3_report.xml
$
-F'.xml *' instructs awk to split on a regex, the requested extension plus 0 or more spaces
the loop {for(i=1;i<=NF;i++) is executed for all the fields in which the input line(s) is(are) splitted — note that the last field is void (it is what follows the last extension), but we are going to take that into account...
the body of the loop
gsub(" ","_", $i) substitutes all the occurrences of space to underscores in the current field, as indexed by the loop variable i
printf i<NF?$i ".xml ":"\n" output different things, if i<NF it's a regular field, so we append the extension and a space, otherwise i equals NF, we just want to terminate the output line with a newline.
It's not perfect, it appends a space after the last filename. I hope that's good enough...
▶    A D D E N D U M    ◀
I'd like to address:
the little buglet of the last space...
some of the issues reported by Ed Morton
generalize the extension provided to awk
To reach these goals, I've decided to wrap the scriptlet in a shell function, that changing spaces into underscores is named s2u
$ s2u () { awk -F'\.'$1' *' -v ext=".$1" '{
> NF--;for(i=1;i<=NF;i++){gsub(" ","_",$i);printf "%s",$i ext (i<NF?" ":"\n")}}'
> }
$ echo "File Name1.xml File Name3 report.xml" | s2u xml
File_Name1.xml File_Name3_report.xml
$
It's a bit different (better?) 'cs it does not special print the last field but instead special-cases the delimiter appended to each field, but the idea of splitting on the extension remains.
This seems a good start if the filenames aren't delineated:
((?:\S.*?)?\.\w{1,})\b
( // start of captured group
(?: // non-captured group
\S.*? // a non-white-space character, then 0 or more any character
)? // 0 or 1 times
\. // a dot
\w{1,} // 1 or more word characters
) // end of captured group
\b // a word boundary
You'll have to look-up how a PCRE pattern converts to a shell pattern. Alternatively it can be run from a Python/Perl/PHP script.
Demo
Assuming you are asking how to rename file names, and not remove spaces in a list of file names that are being used for some other reason, this is the long and short way. The long way uses sed. The short way uses rename. If you are not trying to rename files, your question is quite unclear and should be revised.
If the goal is to simply get a list of xml file names and change them with sed, the bottom example is how to do that.
directory contents:
ls -w 2
bob is over there.xml
fred is here.xml
greg is there.xml
cd [directory with files]
shopt -s nullglob
a_glob=(*.xml);
for ((i=0;i< ${#a_glob[#]}; i++));do
echo "${a_glob[i]}";
done
shopt -u nullglob
# output
bob is over there.xml
fred is here.xml
greg is there.xml
# then rename them
cd [directory with files]
shopt -s nullglob
a_glob=(*.xml);
for ((i=0;i< ${#a_glob[#]}; i++));do
# I prefer 'rename' for such things
# rename 's/[[:space:]]/_/g' "${a_glob[i]}";
# but sed works, can't see any reason to use it for this purpose though
mv "${a_glob[i]}" $(sed 's/[[:space:]]/_/g' <<< "${a_glob[i]}");
done
shopt -u nullglob
result:
ls -w 2
bob_is_over_there.xml
fred_is_here.xml
greg_is_there.xml
globbing is what you want here because of the spaces in the names.
However, this is really a complicated solution, when actually all you need to do is:
cd [your space containing directory]
rename 's/[[:space:]]/_/g' *.xml
and that's it, you're done.
If on the other hand you are trying to create a list of file names, you'd certainly want the globbing method, which if you just modify the statement, will do what you want there too, that is, just use sed to change the output file name.
If your goal is to change the filenames for output purposes, and not rename the actual files:
cd [directory with files]
shopt -s nullglob
a_glob=(*.xml);
for ((i=0;i< ${#a_glob[#]}; i++));do
echo "${a_glob[i]}" | sed 's/[[:space:]]/_/g';
done
shopt -u nullglob
# output:
bob_is_over_there.xml
fred_is_here.xml
greg_is_there.xml
You could use rename:
rename --nows *.xml
This will replace all the spaces of the xml files in the current folder with _.
Sometimes it comes without the --nows option, so you can then use a search and replace:
rename 's/[[:space:]]/__/g' *.xml
Eventually you can use --dry-run if you want to just print filenames without editing the names.

updating a line in a xml file using a shell function

I'm trying to update a line in an xml file using a custom shell function and sed
In cmd line, I run it as follow:
updateloc db_name
However it does not update anything. Below sample of the code
updateloc(){
db_name=$1
file="file.xml"
olddb="<dbname><![CDATA[olddb]]></dbname>"
newddb="<dbname><![CDATA[$db_name]]></dbname>"
sed -i '' 's/$olddb/$newdb/g' $file
}
The right tool for this job is XMLStarlet. To modify any element named dbname with the new value:
updateloc() {
local db_name=$1 file=file.xml
xmlstarlet ed --inplace -u '//dbname' -v "$db_name" "$file"
}
To replace only elements with the old value olddb:
updateloc() {
local db_name=$1 file=file.xml
xmlstarlet ed --inplace \
-u "//dbname[. = 'olddb']" -v "$db_name" "$file"
}
Note that while the serialization generated by XMLStarlet won't necessarily use CDATA, it is guaranteed to be semantically equivalent, and to behave the precise same way in any XML-compliant parser.
1) $olddb and $newdb are not getting expanded because they're in single quotes
2) Your sed command is getting tripped up by all those xml characters - '[' and '/' are both meaningful to sed. You'd have to escape all those, and perhaps use a different regex delimiter (e.g. 's#$olddb#$newdb#g'). It's probably a bad idea to use sed for this, unless the format of file.xml is very consistent (the closing tag could be on a separate line for example).
That said, this would work for your example:
olddb='<dbname><!\[CDATA\[olddb\]\]></dbname>'
newdb='<dbname><!\[CDATA\[newdb\]\]></dbname>'
sed -i '' "s#$olddb#$newdb#g" $file
Grep and Sed Equivalent for XML Command Line Processing has some good approaches for better ways to mangle xml from the command line.

Replacing tags in xml in bash

I have an xml file that is of the following format:
<list>
<version>1.5</version>
<version>1.4</version>
<version>1.3</version>
<version>1.2</version>
</list>
The idea is that I always update the first version tag with a new version. And when I do so, I replace the subsequent tags.
For example, when I update the 1.6 version as the first tag (which I know how to do), the following tags would be:
<list>
<version>1.6</version>
<version>1.5</version>
<version>1.4</version>
<version>1.3</version>
</list>
I've tried to get two options going.
First Option:
My preferred option would be to search the xml file and replace the version tag i+1 with version tag i. Something like:
sed -E '2,/<version>.*<\/version>/s#<version>(.*)</c>#<version>\1</version>#' file.xml
Where I search for the second instance of version and replace it with the first instance of version (currently not working).
Second Option:
My second option would be to store the version tags in variables like:
version=$(grep -oPm1 "(?<=version>)[^<]+" file.xml)
version2=$(grep -oPm2 "(?<=version>)[^<]+" file.xml)
Then replace version 2 by version 1 and do the replacement:
sed -i "s/${version2}/${version}/g" file.xml
However, this options gives:
sed: -e expression #1, char 9: unterminated 's' command.
And when I try:
sed -i "/$version2/s/${version2}/${version}/g" file.xml
I get:
unterminated address regex
Obviously, the idea for either option would be to put the code in a loop so that I can run it i times. However, I am stuck and both options I've tried don't work.
Don't use text-manipulation tools such as awk or sed to work with XML if you can at all avoid it. While this specific subset may be so simple as to make the approach feasible, having the right tools at hand will avoid headaches later (if the file format gets extended; if someone adds comments to the front; etc).
new_version=1.6
xmlstarlet ed \
-d '/list/version[last()]' \
-i '/list/version[1]' -t elem -n version -v "$new_version" \
<old.xml >new.xml
-d '/list/version[last()]' deletes the last version entry in the list.
-i '/list/version[1]' -t elem -n version -v 1.6 introduces a new element named version, with the value 1.6, in the position currently held by the very first version.
Use ! or # as separator in sed instead of /.
It breaks because your match and replace variables contain /

Delete lines before and after a match within a specified tags in SED

Need to delete before and after of a matching pattern within the tag
< mds:insert>
< attributeValues>
< AttrNames
< Item Value="MyContact_c"/>
< /AttrNames>
< /attributeValues>
< /mds:insert>
Using
sed -i -n '/MyContact_c/{s/.*//;x;d;};x;p;${x;p;}' $file
removes only line before and after the matching pattern, need to delete all the contents within the mds:insert tag... Any pointers will be helpful.
It isn't sed, but here is one for ex using the snippet you posted as the file content for $file:
kitsune:~$ printf '%s\n' 'set ic
1;/="MyContact_c"/<|?<mds:insert?+;/<\/mds:insert>/-d
%p' | ex -s $file
Output:
<mds:insert>
</mds:insert>
That will print the remains of the file after the first instance of the section is removed. If you want this done for all instances, the command line will look like this:
'set ic
g/="MyContact_c"/<|?<mds:insert?+;/<\/mds:insert>/-d
%p'
You can use this in the for loop of a shell script if you want this done to multiple files. Naturally you'll want a backup copy if you do such a thing, so make sure to copy the file before altering it if you intend to overwrite it.
By the way, if you've ever used Vim or even vi, these sorts of commands are used for saving, quitting, etc. It is worth adding ex to your toolbox of knowledge IMHO.
Edit
C Shell users cannot use these commands as-is because they contain quoted newlines, which isn't allowed in a C shell. Instead, you can modify the first command like so:
kitsune:~% printf '%s\n%s\n%s\n' 'set ic' '1;/="MyContact_c"/<|?<mds:insert?+;/<\/mds:insert>/-d' '%p' | ex -s $file
You can similarly do the same with the other string.
Disclaimer: I'm not a C shell user myself, so there might be a better way, but I don't know it.
Here is a way to do it with awk
awk '{a[NR]=$0} /MyContact_b/ {f=NR} END {for (i=1;i<=NR;i++) if (i+2<f || i-2>f || !f) print a[i]}' file
< mds:insert>
< /mds:insert>
It skips two line before and two after pattern if its found.
Here's one way to do it in sed:
sed -e ':a' -e '/ mds:insert/!{p;d;}' -e 'N;/\/mds:insert/{/MyContact_c/!p;d;};ba' filename
EDIT:
You and I may be using different versions of sed or something. Let's try an experiment:
sed -e '/ mds:insert/!{p;d;}' filename
It won't do anything very interesting, but I want to know whether it generates an error.