20

Quick Summary: I need to create a Bash script to change the text within a node automatically every week. The script will match the node and replace the text inside them (if this is possible)? How would I do this?

Long Summary: I host a Minecraft server which has shops, each of which have their own .xml file in the /ShowcaseStandalone/ffs-storage/ directory. Every Sunday my server restarts and executes several commands into the terminal to reset several things. One thing that I am trying to make change is one of the shops. I am wanting to change the text in the node <itemstack> and the text in the node <price>. I am simply wanting to take text from a .txt file in a different folder, and insert it into that node. The problem is, that the text in the node will change every week. Is there any way to replace a specific line or text within two nodes using bash?

XML file:

<?xml version="1.0" encoding="UTF-8"?>
<scs-shop usid="cac8480951254352116d5255e795006252d404d9" version="2" type="storage">
    <enchantments type="string"/>
    <owner type="string">Chadward27</owner>
    <world type="string">Frisnuk</world>
    <itemStack type="string">329:0</itemStack>
    <activity type="string">BUY</activity>
    <price type="double">55.0</price>
    <locX type="double">487.5</locX>
    <locY type="double">179.0</locY>
    <locZ type="double">-1084.5</locZ>
    <amount type="integer">0</amount>
    <maxAmount type="integer">0</maxAmount>
    <isUnlimited type="boolean">true</isUnlimited>
    <nbt-storage usid="23dffac5fb2ea7cfdcf0740159e881026fde4fa4" version="2" type="storage"/>
</scs-shop>

Operating System: Linux Ubuntu 12.04

Gilles Quénot
  • 173,512
  • 41
  • 224
  • 223
Clucky
  • 371
  • 2
  • 4
  • 15
  • So you just want to change usid from scs-shop tag ? – Gilles Quénot Nov 13 '12 at 22:27
  • 2
    XML manipulation using exclusively `bash`? Why limit yourself? – Brian Cain Nov 13 '12 at 22:28
  • @sputnick Sorry, I had to ad &lt and &gt tags because it killed my nodes, but no, I need to replace the text in the nodes "itemstack" and "price" – Clucky Nov 13 '12 at 22:32
  • @Brian-Cain Is there any other way to do this from terminal? Because these commands all execute upon the server shutting off – Clucky Nov 13 '12 at 22:33
  • I'd recommend python -- does your server have python installed, or can it? If not, java might be another option. – Brian Cain Nov 13 '12 at 22:36
  • @Brian-Cain I believe it does have python and it most certainly has Java. I heard you can do this using perl also; however, the only way I think perl would work for this specific script is if I also used RegEx (something I do not know). Also, am I tagging correctly? I'm brand new at this website, although I use it often for script references. – Clucky Nov 13 '12 at 22:39
  • The duplicate http://stackoverflow.com/questions/23560215/replace-xml-value-with-sed has a few more answers if the ones here are unsuitable for some reason. – tripleee Nov 21 '15 at 11:48

3 Answers3

19

You can use xmlstarlet to edit a XML file in a shell like this :

xmlstarlet edit -L -u "/scs-shop/price[@type='double']" -v '99.66' file.xml

NOTE

  • "/scs-shop/price[@type='double']" is a Xpath expression
  • see xmlstarlet ed --help
Gilles Quénot
  • 173,512
  • 41
  • 224
  • 223
16

The XML way is cool, but if you need to use normal bash tools, you can modify a line using sed. For instance:

PRICE=123
sed -i "s/\(<price.*>\)[^<>]*\(<\/price.*\)/\1$PRICE\2/" $XML_FILE_TO_MODIFY

This will replace the price with 123.

That sed command seems daunting, so let me break it down:

\(<price.*>\)[^<>]*\(<\/price.*\) is the pattern to match. \( ... \) are parenthesis for grouping. <price.*> matches the opening price tag. [^<>]* matches anything except angle brackets, and in this case will match the contents of the price tag. <\/price.* matches the end of the price tag. Forward slash is a delimiter in sed, so I escape it with a back slash.

\1$PRICE\2 is the text to replace the matched text with. \1 refers to the first matched parenthesis group, which is the opening price tag. $PRICE is the variable with the desired price in it. \2 refers to the second parenthesis group, in this case the closing tag.

Dan Bliss
  • 1,694
  • 13
  • 10
  • 5
    This is the wrong way. You can't realistically parse tag-based markup languages like HTML or XML using Bash, grep, sed, cut, etc. See http://www.codinghorror.com/blog/archives/001311.html (you know the blog website from SO creator ?) This is about HTML, but this is the same for XML... – Gilles Quénot Nov 13 '12 at 23:30
  • 4
    If linux came with a good xml tool like xmlstarlet then I would agree that this is the wrong way. Until it does, sed is my go-to tool. Realistically, you can use sed in 99% of cases, including this one. – Dan Bliss Nov 13 '12 at 23:34
  • 2
    I don't said "that don't work" but : "that's not reliable". In a simple case like this this seems "fine" for some people, but what happens when the XML will becomes more complicated with nested tags ? I'm talking about reliable way to makes things clean. `sudo apt-get install xmlstarlet` is not a big deal there. – Gilles Quénot Nov 13 '12 at 23:38
  • 2
    "but I think that's just as wrongheaded as demanding every trivial HTML processing task be handled by a full-blown parsing engine" is a quote from the blog post you cite. Regardless, for the record: I prefer the xmlstarlet solution. But if xmlstarlet is not available, sed will get er done. – Dan Bliss Nov 13 '12 at 23:42
  • @sputnick: apt-get install is not so easy, e.g. you need a internet connection and free space on the HD (if you use something like Backtrack you have a 4GB ramdisk and if it is full, it crashes). And recently my apt system broke, and I could not use apt-get install for anything for 3 months! – BeniBela Nov 14 '12 at 00:16
  • It is definately good to have both of these resources at my hands in case one fails. Thank you btw for the use of regular expression, that was where i was initially getting at with this post. I do enjoy the more simplistic design of xmlstarlet – Clucky Nov 14 '12 at 03:33
  • The blanket wildcard `.*` here is definitely dangerous. Try to limit matching with `[^<>]*` to avoid spanning across tags. There are still many corner cases where this could fail; for example, many XML schemata will allow a line break before a close tag, which will completely break one of the fundamental assumptions of this solution. Altogether, these points illustrate exactly why this approach is problematic. – tripleee Sep 11 '16 at 11:38
  • Agree that excluding the angle brackets is better. – Dan Bliss Sep 15 '16 at 20:07
2

I did not have the luxury of having xmlstarlet. I found a solution though simply by doing an inline replacement;

template-parameter.xml

<ns:Parameter>
    <ns:Name required="true">##-ParamName-##</ns:Name>
    <ns:Value>
        <ns:Text>##-ParamValue-##</ns:Text>
    </ns:Value>
</ns:Parameter>

Snippet

tokenName="foo"
tokenValue="bar"    

#Replace placeholders in parameter template element
myParamElement=$(cat template-parameter.xml)
myParamElement=${myParamElement//##-ParamName-##/$tokenName}
myParamElement=${myParamElement//##-ParamValue-##/$tokenValue}  

Result

<ns:Parameter>
    <ns:Name required="true">foo</ns:Name>
    <ns:Value>
        <ns:Text>bar</ns:Text>
    </ns:Value>
</ns:Parameter>
MikeSchinkel
  • 4,947
  • 4
  • 38
  • 46
Denn0
  • 377
  • 3
  • 15