Use sed to replace second pattern found - bash

How can I use sed to replace second pattern found?
<default>...</default>
<default>...</default>
<default>...</default>
<setting id="lookandfeel.font" type="string" parent="lookandfeel.skin" label="13303" help="36107">
<level>1</level>
<default>Default</default>
</setting>
<default>...</default>
<default>...</default>
<default>...</default>
first pattern = lookandfeel.font
second pattern = Default
then replace "Default" with "Arial"

With GNU sed:
sed '/<setting id="lookandfeel.font"/,/<\/setting>/{s|<default>Default</default>|<default>Arial</default>|}' file
If you want to edit "in place" add option -i.

sed is for simple substitutions on individual lines, that is all. For anything even slightly more interesting you should be using awk for clarity, simplicity, robustness, portability, maintainability and almost every other desirable quality of software:
$ awk '/lookandfeel.font/{f=1} f&&sub(/Default/,"Arial"){f=0} 1' file
<default>...</default>
<default>...</default>
<default>...</default>
<setting id="lookandfeel.font" type="string" parent="lookandfeel.skin" label="13303" help="36107">
<level>1</level>
<default>Arial</default>
</setting>
<default>...</default>
<default>...</default>
<default>...</default>

Consider this regex in Perl
s/(lookandfeel\.skin[\S\s]*)Default/$1Arial/gm
see: DEMO
According to this question and this question which tell that sed is difficult to handle pattern in multiple lines then I suggest you to use perl instead of sed.
Note If you want more strict regex please add more information into your question.

Related

how to replace a block of text from a file with a block of text from another file using shell (bash)

I'm trying to replace a block of text from file_to_change.xml with the entire content of changes.xml, which is a tag of the main file with a minor change.
This is the content of file_to_change.xml:
<clients>
<client>
<name>Bob</name>
<age>18</age>
</client>
<client>
<name>Alice</name>
<age>12</age>
</client>
<client>
<name>Carlos</name>
<age>28</age>
</client>
</clients>
And this is the content of changes.xml:
<client>
<name>Carlita</name>
<age>17</age>
</client>
And this is the content of an auxiliary file that contains the line i want to change:
<client>
<name>Carlos</name>
<age>28</age>
</client>
The great difference between the solution i'm looking for from the one explained here is that the files are not entirely equal. It is also important to say that the environment i'm working at do not allow me to use external tools, and for that reason i'm trying to find a "core" solution, using sed, awk, etc.
This is the command i was trying to use:
sed -i -e '1r changes.xml' -e '1,/r aux.xml/d' file_to_change.xml
I'm expecting that the file_to_change.xml look like this:
<clients>
<client>
<name>Bob</name>
<age>18</age>
</client>
<client>
<name>Alice</name>
<age>12</age>
</client>
<client>
<name>Carlita</name>
<age>17</age>
</client>
</clients>
Using pure bash?
#!/bin/bash
file=$( < file_to_change.xml )
search=$( < aux.xml )
replacement=$( < changes.xml )
printf '%s\n' "${file/"$search"/$replacement}"
<clients>
<client>
<name>Bob</name>
<age>18</age>
</client>
<client>
<name>Alice</name>
<age>12</age>
</client>
<client>
<name>Carlita</name>
<age>17</age>
</client>
</clients>
I would harness GNU AWK for this task following way, let file.txt content be
ABC
DEF
GHI
JKL
MNO
and file_detect.txt be
GHI
JKL
and file_replacement.txt be
123
456
789
then
awk 'BEGIN{RS=""}FILENAME=="file_replacement.txt"{repl=$0;next}FILENAME=="file_detect.txt"{find=$0;next}{place=index($0,find);print substr($0,1,place-1) repl substr($0,place+length(find))}' file_replacement.txt file_detect.txt file.txt
gives output
ABC
DEF
123
456
789
MNO
Disclaimer: this solution assumes you have never blank lines in any of your files, if this does not hold change RS to any value which does not exist in any of files involved.
Explanation: firstly I inform GNU AWK that blank lines are to be considered row separators using RS set to empty string, aim there is to make GNU AWK consider whole file as big single line. Then when I encounter file named file_replacement.txt I simply store its' content into variable named repl and instruct GNU AWK to go to next line, so no other action is undertaken, similarly content of file_detect.txt are stored in variable named find. Then I use String functions following way, first index to detect where part to be replace is located, then I use substr to get part before part to be replaced substr($0,1,place-1) and part after part to be replaced substr($0,place+length(find) and then print them concatenated using repl.
Warning: this solution assumes there is exactly one file_detect.txt inside file.txt.
(tested in GNU Awk 5.0.1)
This might work for you (GNU sed):
sed -Ez 's/.*/echo -n "###&###" | cat fileToSearch - fileReplacement/e
s/^(.*)###(.*)\1(.*)###(.*)/\2\4\3/' file

How to use variable with \ in sed command

I have a strings file that I want to replace with sed command.
String in the file :
<ItemGroup>
<None Include="xyz.config" />
<BundleResource Include="Settings.txt" />
<BundleResource Include="Resources\Base.lproj\Localizable.strings" /> <BundleResource Include="Resources\Base.lproj\InfoPlist.strings" />
</ItemGroup>
Below is my command:
CONTENT="<BundleResource Include="Resources\Base.lproj\Localizable.strings" /><BundleResource Include="Resources\Base.lproj\InfoPlist.strings" /><BundleResource Include="Resources\es.lproj\Localizable.strings" /><BundleResource Include="Resources\es.lproj\Infoplist.strings" />"
sed -i "s|\.*<BundleResource Include=\"Resources\\Base.lproj\\.*|${CONTENT}|g"
but when I use above sed command, it didn't replace it. Do you have any idea why? Did I did wrong with my sed command?
My end result that I want is:
<ItemGroup>
<None Include="xyz.config" />
<BundleResource Include="Settings.txt" />
<BundleResource Include="Resources\Base.lproj\Localizable.strings" />
<BundleResource Include="Resources\Base.lproj\InfoPlist.strings" />
<BundleResource Include="Resources\es.lproj\Localizable.strings" />
<BundleResource Include="Resources\es.lproj\Infoplist.strings" />
</ItemGroup>
Thank you!
This is a problem with bash and sed backslash quoting. If you try
sed -i "s|\.*<BundleResource Include=\"Resources\\\\Base.lproj\\\\.*|${CONTENT}|g"
it should do what you want. Both bash and sed are collapsing backslahes, so you need four!
Also, you've used double quotes when assigning a string containing double quotes to CONTENT, which won't do what you want. Try single quotes:
CONTENT='<BundleResource Include="Resources\Base.lproj\Localizable.strings" /><BundleResource Include="Resources\Base.lproj\InfoPlist.strings" /><BundleResource Include="Resources\es.lproj\Localizable.strings" /><BundleResource Include="Resources\es.lproj\Infoplist.strings" />'
That said, as a commenter has pointed out, you'd be better off using a proper XML tool rather than sed.
As chepner mentioned, you should likely use an xml tool to edit this rather than SED, however, if you wanted to use SED, you likely want something like this:
sed "s|<BundleResource Include=\"Resources\\\\Base.lproj\\\\[^>]*>|${CONTENT}|g" < tmp.txt
I replaced the .*'s, as those can be problematic. The second .* is replaced with [^>]>, so it only matches/replaces until the next > character. I also double escaped the \ characters.

Keep getting an error in using sed command to append line after specific pattern in bash script file

I have a config.xml file and a bash script file. I want to use the bash script to append a line to config.xml and save that result in config.xml.
For example, I want to append the line 'it never works' whenever there is the word 'Cordova' in config.xml. So I used:
sed '/Cordova/ a\ it never works' config.xml
and I get an error of "sed: 1: "/Cordova/ a\ it never works": extra characters after \ at the end of a command".
How do I fix this?
I'm using a Mac at the terminal command line.
The version of sed on a Mac is the BSD variant, and it isn't as forgiving as the GNU sed version (alternatively, it hews more closely to the POSIX specification for sed and doesn't bend the rules as severely as GNU sed does). In particular, commands such as a need the backslash at the end of a line, and the remaining material on the next line:
sed '/Cordova/ a\
it always works like this, even with GNU sed' config.xml
With multiple lines to add, all except the last end with the backslash.
sed '/Cordova/ a\
it always works like this\
even with GNU sed' config.xml
Note that both these do not add a newline after the appended material. For that, you need:
sed '/Cordova/ a\
it always works like this, even with GNU sed
' config.xml
Note too that sed eliminates the leading blanks from the appended material.
sed is for simple substitutions on individual lines, that is all. For anything else you should be using awk for clarity, robustness, portability, efficiency and most other desirable qualities of software:
awk '{print} /Cordova/{print "it ALWAYS works"}' config.xml

Bash script to find placeholders in file

I'm trying to write a bash script to find all placeholders in a file.
For example
I have following file:
<property name="sdfasdf" value="$ABC.D"></property>
<property name="sadf" value="$DFG.F.G"></property>
<property name="sadf" value="hello"></property>
<property name="ddd" value="$HJK"></property
and I would like to get these:
$ABC.D
$DFG.F.G
$HJK
I tried many options but without success.
Could someone help me?
Can grep for these values and placeholders and further grep to get the symbol names.
example
$ grep -o 'value="$.\+"' input.txt | grep -oE '\$(\w|\.)+'
$ABC.D
$DFG.F.G
$HJK
note: assumes there is only one placeholder value per line
details
o flag only prints the matches to the pattern
E flag for extended regex used to match either word or .
You can use sed:
sed -n 's/.*value="\($[\.a-zA-Z]*\)".*/\1/p' ./input.txt
where input.txt file contains your text.
Here we use a substitution group to only print the actual match (not the entire matching line).
sed -nr 's%(\$[A-Za-z][A-Za-z.]*)%\n\1\n%gp' test | grep '^\$[A-Za-z][A-Za-z.]*'
This is universal method to find placeholders, independent of whether or not there is only one placeholder per each line and whether or not there is "value=" context near it. All placeholders will be printed on STDOUT.

grep a specific line, print upper lines to a specific word and/or below lines to a specific word

I'm recently running into an issue where I seem not to find a consistent solution.
Let's say we have some xml file and it's built like the following:
...
<tenant>
<name>bla</name>
<id>1</id>
<something>whatever</something>
</tenant>
<tenant>
<name>foo</name>
<id>55</id>
<something>whatever</something>
</tenant>
<tenant>
<name>waaaaaaaaaaaaaaaey</name>
<id>8013</id>
<something>what</something>
</tenant>
...
And let's say there might even be more options like <e-mail> and some other stuff. So it really can vary of the length there.
Now we know the "something" that it is "whatever" and grep for it. But we don't only want that result, we want all results between <tenant> and </tenant> that include <something>whatever</something>.
Since the number of lines might vary between <tenant> and </tenant>, I can not use -A, -B or -C on grep.
Any help would be apprechiated here.
I currently just make -C big enough so I have at least all infos between there but maybe once the length will be longer and my method is screwed up.
awk/grep/sed (regex) is not the right tool for your requirement. because my understanding of your question is:
valid xml file
text format could be different, elements could vary, it may be broken into lines, it can contain empty lines.
so, xpath is the right way to go:
//tenant[something='whatever']
change the something and whatever you will get the corresponding tenant elements.
If you prefer linux cmd tool to do that, xmllint is one example:
xmllint --xpath "//tenant[something='whatever']" your.xml
Using GNU awk for multi-char RS and RT:
$ awk -v RS='</tenant>' '/<something>whatever<\/something>/{print $0 RT}' file
<tenant>
<name>bla</name>
<id>1</id>
<something>whatever</something>
</tenant>
<tenant>
<name>foo</name>
<id>55</id>
<something>whatever</something>
</tenant>
The below pcregrep will get the contents between tenant tag only if it's contain the string <something>whatever</something>
$ pcregrep -M -o '(?s)<tenant>\n\K.*?<something>whatever<\/something>.*?(?=\n<\/tenant>)' file
<name>bla</name>
<id>1</id>
<something>whatever</something>
<name>foo</name>
<id>55</id>
<something>whatever</something>
With <tenant> tag.
$ pcregrep -M -o '(?s)<tenant>\n.*?<something>whatever<\/something>.*?<\/tenant>' file
<tenant>
<name>bla</name>
<id>1</id>
<something>whatever</something>
</tenant>
<tenant>
<name>foo</name>
<id>55</id>
<something>whatever</something>
</tenant>
This might work for you (GNU sed):
sed -n '/<tenant>/{:a;N;\|</tenant>|!ba;\|<something>whatever</something>|p}' file

Resources