Search code examples
xmlperlxml-twig

XML::Twig replace string in a file if attribute of parent tag is something


I have an XML file: uml.model

<UML:Class name="2012">
  <type2>
    <type1>
        <def value="test string ABC" />
    </type1>
  </type2>
</UML:Class>

<UML:Class name="2013">
  <type2>
    <type1>
        <def value="test string ABC" />
    </type1>
  </type2>
</UML:Class>

I want to replace the ABC in 'UML:Class name="2012"' into DEF and have the result to be output in a new file:

<UML:Class name="2012">
  <type2>
    <type1>
        <def value="test string DEF" />
    </type1>
  </type2>
</UML:Class>

<UML:Class name="2013">
  <type2>
    <type1>
        <def value="test string ABC" />
    </type1>
  </type2>
</UML:Class>

The perl script I use is:

#!/usr/bin/perl -w
use strict;
use warnings;

use XML::Twig;
my $twig = XML::Twig->new( twig_roots => { 'UML:Class' => \&uml_class } );
$twig->parsefile( 'uml.model' );
$twig->print_to_file( 'uml.model.new' );

sub uml_class {
    my ( $twig, $section ) = @_;
    my $subTwig;
    my $year = $section->{'att'}->{'name'};

    if ( $year eq '2012' ) {
        $subTwig = XML::Twig->new( twig_roots => { 'type1/def' => \&type_def } );
        $subTwig->parse( $section->sprint() };
    }
}

sub type_def {
    my ($twig, $elt) = @_;
    $elt->print ();
    print "\n";
}

It doesn't work as expected. How can I change it to get the desired result? Thanks a lot,


Solution

  • You're mixing print and print_to_file. print_to_file is not magical, it doesn't cause all prints to go to the file. You need to modify the attribute value in the original document, then print_to_file will output the new value.

    I would do this this way:

    #!/usr/bin/perl
    
    use strict;
    use warnings;
    
    use XML::Twig;
    

    open( my $out, '>', 'uml.model.new') or die "cannot create uml.model.new: $!";

    XML::Twig->new( twig_roots => { 'UML:Class[@name="2012"]' =>
                                       sub { my $def= $_->next_elt( 'def');
                                             $def->latt( 'value')=~ s{ABC}{DEF};
                                             $_->flush;
                                           },
                                     },
                    twig_print_outside_roots => $out,
                    pretty_print => 'indented',
    
    
                  )
             ->parsefile( 'uml.model');
    

    A couple of notes:

    • I used a complex trigger ('UML:Class[@name="2012"]') instead of a simple UML:CLASS and then testing the attribute value in the handler.
    • the latt method gives you an attribute as an lvalue that you can change in place, you could alternatively use $v= $def->att( 'value'); $v=~ s{ABC}{DEF}; $def->set_att( $v);... but using latt makes it much simpler
    • I chained the method calls (new/parse/print_to_file), I like this style for code that's as simple as this one YMMV