7

I am trying to generate a tree from an 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 have "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 have "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 a 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 get an output as close to it as possible. Can anyone suggest me how I should proceed with this?

2 Answers 2

5

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
5
  • 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
  • If you get Attempt to load network entity, add --net after the subcommand Aug 25, 2023 at 22:24
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 "-"

This site is temporarily in read-only mode and not accepting new answers.

Not the answer you're looking for? Browse other questions tagged .