4

I am trying to generate a tree from a XML file in Bash.

This is a part of the XML file:

  <menu name="main_menu" display="Main Menu">
      <application name="load_profiles" display="Load Profile"/>
      <application name="save_profiles" display="Save Profile"/>
      <application name="remove_profiles" display="Delete Profile"/>
      </menu>

I have tried to use CAT and GREP and AWK:

cat menu.xml | grep menu\ name | awk -v FS="(display=\"|\" help)" '{print $2}' > menulist.txt

I have first GREPed using the lines that has "Menu Name" and then printed the Tests between 'display="' and '" help' and came out with this output:

Main Menu">
Broadband
Load and Save Profiles
xDSL Interface

But what I want is to Grep all the lines that has "Menu Name", "parameter type", "application name" and "value id" and print their display name in a tree like output. I am not sure how I can Grep multiple values from multiple lines and print specific string from it.

Then I have seen that it is comparatively easier to do this with a XML parser tool. So I have tried with XMLStarlet:

xmlstarlet el menu.xml|awk -F'/' 'BEGIN{print "digraph{"}{print $(NF-1)" -> "$NF}END{print"}"}'> menumenutxt.txt

Using this command I have found the following output:

menu -> menu
menu -> onenter
menu -> menu
menu -> application
menu -> application
menu -> application
menu -> parameter
parameter -> value
parameter -> value

Which definitely looks better and closer to what I want. But it's not printing the display name.

What I am trying to print is something like this:

 Main Menu -> 
           -> Broadband 
                        -> Load and Save Profiles
                                                  -> Load Profile
                                                  -> Save Profile
                                                  -> Delete Profile

Or the following:

Main Menu 
-> Broadband 
--> Load and Save Profiles
---> Load Profile
---> Save Profile
---> Delete Profile

My aim to to get an output as close to it as possible. Can anyone suggest me how I should proceed with this?

2 Answers 2

4

Adapting one of the examples from the xmlstarlet docs:

xmlstarlet sel -T -t -m '//*' \
    -i '@display' \
        -m 'ancestor-or-self::*' \
            -i '(position()=last())' \
                -o '-> ' -v '@display' -b \
            -o $'\t' -b \
        -n foo.xml

The example is:

Print structure of XML element using xml sel (advanced XPath expressions and xml sel command usage)

xml sel -T -t -m '//*' \
-m 'ancestor-or-self::*' -v 'name()' -i 'not(position()=last())' -o . -b -b -n \
xml/structure.xml

Result Output:

a1
a1.a11
a1.a11.a111
a1.a11.a111.a1111
a1.a11.a112
a1.a11.a112.a1121
a1.a12
a1.a13
a1.a13.a131

From here, the things we need to modify are:

  • print the display attribute instead of the name, so @display instead of name()
  • print it only for the last element. We already have the test for printing . for all but the last element, so it's easy to invert that.
  • print tabs to indent (we can do it after every element, it will just leave trailing, invisible tab), so just -o $'\t'. $'\t' in bash will get you a tab character.
  • print only for elements which have the display attribute, so -i '@display'

I have indented the command above to make the flow clearer.

The output I get:

$ xmlstarlet sel -T -t -m '//*' -i '@display' -m 'ancestor-or-self::*' -i '(position()=last())' -o '-> ' -v '@display' -b -o $'\t' -b -n foo.xml
-> English
    -> Main Menu
        -> Broadband
            -> Load and Save Profiles
                -> Load Profile
                -> Save Profile
                -> Delete Profile
            -> Interface
                -> xDSL
                -> SFP
                -> Ethernet
                -> SHDSL
            -> xDSL Interface
                -> xDSL Mode
                    -> Annex A/M
                    -> Annex B/J
                -> MAC Address
                    -> MAC Address
                -> Vectoring Mode
                    -> Disabled
                    -> Enabled
                    -> Friendly
                -> G.FAST
                    -> Disabled
                    -> Enabled

After thinking a bit, the following is simpler:

xmlstarlet sel -T -t -m '//*' \
    -i '@display' \
        -m 'ancestor::*' \
            -o $'\t' -b \
        -o '-> ' -v '@display' -n foo.xml

Using ancestor::* instead of ancestor-or-self::* makes printing the tabs correctly easier, and eliminates the extra test for last element.

Similar output, but without trailing tabs:

-> English
    -> Main Menu
        -> Broadband
            -> Load and Save Profiles
                -> Load Profile
                -> Save Profile
                -> Delete Profile
            -> Interface
                -> xDSL
                -> SFP
                -> Ethernet
                -> SHDSL
            -> xDSL Interface
                -> xDSL Mode
                    -> Annex A/M
                    -> Annex B/J
                -> MAC Address
                    -> MAC Address
                -> Vectoring Mode
                    -> Disabled
                    -> Enabled
                    -> Friendly
                -> G.FAST
                    -> Disabled
                    -> Enabled
4
  • Hi Muru, I have a question. If I want to add a counter instead of tab how should I do that? I tried adding a counter instead of tab here: -o $'\t' -b but it just appends the value(as it should), and I can't figure out how I should print the number of tabs instead of tabs.
    – Russo
    Sep 1, 2017 at 8:21
  • @Russo I'm sure xmlstarlet probably has some way of doing this, but for now, maybe use awk to process the output: xmlstarlet sel -T -t -m '//*' -i '@display' -m 'ancestor::*' -o $'\t' -b -o '-> ' -v '@display' -n foo.xml | awk -F '\t' '{print NF,$NF}'
    – muru
    Sep 1, 2017 at 9:04
  • Yes, this works perfectly. It's not mandatory that I use xmlstarlet. Thank you.
    – Russo
    Sep 1, 2017 at 9:20
  • @ Muru, very cool!!
    – user216043
    Sep 6, 2017 at 12:58
0

(If not already installed, install xidel)

xidel ex.xml  \
  -e '//@display/concat(substring("------",1,count(ancestor::*)),">",.)'
  • substring("------",1,n) is a dirty way of building a string with n "-"

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.