Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Vim macro or plugin to sequentially renumber xml elements?

Tags:

vim

xml

I have multiple XML files that I need to edit everyday. I have no control over the source that reads these files and no control over the format of the XML.

The problem is that each element needs its own sequential number within each block. Sometimes there can be upwards of 200 elements within each block. When I need to insert a command early in an existing long block I must manually renumber each following element which is tedious and error prone.

Also, the name/value pairs have no connection with the sequential numbering needed. The value "origcmd1" below could have been "foobar98765". They are number just to illustrate my problem.

Example:

Starting with this:

<block1>
    <cmd1 name="origcmd1"></cmd1>
    <cmd2 name="origcmd2">someCmdsTakeParams,param2</cmd2>
    <cmd3 name="origcmd3"></cmd3>
</block1>

<block3>                             <c>no guarantee blocks are sequential #</c>
    <cmd1 name="cmd1"></cmd1>
    <cmd2 name="cmd2"></cmd2>
    <cmd3 name="cmd3"></cmd3>
</block3>

If I need to add a command early, say between origcmd1 and origcmd2:

<block1>
    <cmd1 name="origcmd1"></cmd1>
    <cmd2 name="NEWcmd1"></cmd2>                    <c>cmd2 & cmd3 inserted</c>
    <cmd3 name="NEWcmd1"></cmd3>
    <cmd4 name="origcmd2">someCmdsTakeParams,param2</cmd4>
    <cmd5 name="origcmd3"></cmd5>
</block1>

<block3>                             <c>no guarantee blocks are sequential #</c>
    <cmd1 name="cmd1"></cmd1>
    <cmd2 name="cmd2"></cmd2>
    <cmd3 name="cmd3"></cmd3>
</block3>

I must now go through and manually renumber what are now cmd4 and cmd5. When there are hundreds of commands this gets very frustrating.

Solutions so far have included trying to write a macro to renumber from the first line making the assumption that it is always correctly numbered as 1. I would then use a series of delete/pastes and Ctrl-a to increment each proceeding line number. Unfortunately, I could never get the macro to work correctly.

I also looked through the vim plugins at vim.org but I found nothing that I recognized as a solution.

Vim is new to me but I've taken a liking to it and this seems like the type of problem it's well suited to solving. Any ideas on a fast technique or plugin I missed is appreciated.

like image 674
Tye Avatar asked Dec 07 '12 00:12

Tye


2 Answers

The following seems to work for me:

function! FixBlock()
  let g:pos_end = search("<\/bloc")
  call search("<block", "be")
  let s:i = 0
  while getpos(".")[1] < g:pos_end
    call search("cmd", "e")
    let s:i = s:i + 1
    exe 's/^\(.*\)\(<cmd[^ ]*\) \(.*\)/\1cmd' . s:i . ' \3/'
    "exe 's/^\(.*\)\(cmd.*\) \(.*\)/\1cmd' . s:i . ' \3/'
    exe 's?\(.*\)\(</cmd.*\)>\(.*\)?\1</cmd' . s:i . '>\3'
    normal j0
  endwhile
endfunction

map ,fb :call FixBlock()

for it to work, you should be inside the block you want to fix. Just type ,fb in mormal mode and that should do the work.

like image 76
skeept Avatar answered Nov 10 '22 18:11

skeept


If the formatting of the XML is rather fixed, you can indeed use (somewhat complex) Vim macros to manipulate the contents, but be aware that subtle changes in the XML formatting (or commented out blocks) may wreak havoc and corrupt your data. Since Vim as a general text editor has no real understanding of the XML structure, it is difficult to make the macro more robust.

An XML processor like XSLT may be better suited for the job. (Although for me, it's more difficult to write a transformation in XSLT than record a Vim macro! But if you need this often, it may be a worthwhile investment.) You can even invoke it from within Vim: :%!xsltproc ...

Example XSLT 1.0 Stylesheet...

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:output indent="yes"/>
    <xsl:strip-space elements="*"/>

    <xsl:template match="@*|node()">
        <xsl:copy>
            <xsl:apply-templates select="@*|node()"/>
        </xsl:copy>
    </xsl:template>

    <xsl:template match="*[starts-with(name(),'cmd')]">
        <xsl:element name="cmd{position()}">
            <xsl:apply-templates select="@*|node()"/>
        </xsl:element>
    </xsl:template>

</xsl:stylesheet>
like image 20
Ingo Karkat Avatar answered Nov 10 '22 18:11

Ingo Karkat