views:

986

answers:

4

I am putting an XSL together than will create a NAnt build script using as input an XML file that defines all of the items that need to be built. We have a lot of very similar projects with standard layouts and defined standards for handover areas and so having an XML file that defines what the developers want to happen rather than describing how it needs to be done would greatly help the uptake of the build service.

I want to define early on in the product build XML file the build modes to be used, i.e.

<Build>
    <BuildModes>
        <Mode name="Debug" />
        <Mode name="Release" />
    </BuildModes>

    <ItemsToBuild>
        <Item name="first item" .... />
        <Item name="second item" .... />
    </ItemsToBuild>
 </Build>

I want to have an

<xsl:for-each select="/Build/BuildModes/Mode">
    <xsl:for-each select="/Build/ItemsToBuild/Item">
        <exec program="devenv">
        <xsl:attribute name="line">
            use the @name from the Mode and other stuff from Item to build up the command line
        </xsl:attribute>
    </xsl:for-each>
</xsl:for-each>

Now, I can do it by having a defined between the two for-each lines to hold the Mode/@name value but that's a bit messy, and what I actually want to do is flip the nexting around so that the build mode is inside the Item loop so it builds one mode then the other. At the moment it would build all of the debug and then all of the release builds. To do that I would have to have several declared and that's getting very messy.

So it's nested when the elements in the source document are not nested.

EDIT:

ok, as the accepted answer below shows using for-each is a bad idea in most cases, and I have reworked this example into the following. It's slightly different as the schema I'm using was simplified for the above post but you get the idea.

<?xml version="1.0"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"&gt;
<xsl:output method="xml" encoding="ISO-8859-1" indent="yes"/>

<xsl:template match="/BuildDefinition">
    <xsl:apply-templates select="/BuildDefinition/VS2008SLN/DeploymentProject"/>
</xsl:template>

<xsl:template match="/BuildDefinition/VS2008SLN/DeploymentProject">
    <xsl:apply-templates select="/BuildDefinition/BuildModes/Mode">
        <xsl:with-param name="BuildTarget" select="." />
    </xsl:apply-templates>
</xsl:template>

<xsl:template match="/BuildDefinition/BuildModes/Mode">
    <xsl:param name="BuildTarget" />
    <exec program="devenv"> <!-- not the real call, but for example purposes -->
        <xsl:attribute name="ProjectName" select="$BuildTarget/@ProjectName"/>
        <xsl:attribute name="SolutionName" select="$BuildTarget/../@SolutionName" />
        <xsl:attribute name="ModeName" select="@name"/>
    </exec>
</xsl:template>
</xsl:stylesheet>

and this is the schema against which it runs

<BuildDefinition Version="1.0">

 <BuildModes>
    <Mode name="Debug" />
    <Mode name="Release" />
</BuildModes>

<VS2008SLN 
    SolutionName="MySolution"
    SolutionDirectory="Visual Studio 2008\MySolution">
    <DeploymentProject 
        ProjectName="MyDeploymentProject" 
        DeploymentTargetDirectory="EndsUpHere"
        DeploymentManifestName="AndCalledThisInTheDocumentation" />
</VS2008SLN>
A: 

You could use a named template:

    <xsl:template name="execute">
      <xsl:param name="mode" />
      <xsl:for-each select="/Build/ItemsToBuild/Item">
       <exec program="devenv">
        <xsl:attribute name="line">
            use $mode and other stuff from Item to build up the command line
        </xsl:attribute>
       </exec>
      </xsl:for-each>
    </xsl:template>

then call it:

    
    <xsl:for-each select="/Build/BuildModes/Mode">
     <xsl:call-template name="execute">
      <xsl:with-param name="mode" select="@name" />
     </xsl:call-template>
    </xsl:for-each>

This will help separate things but I'm not sure it's really any more clear.

Unfortunately, no matter how you look at it, you'll have to do some plumbery since you're trying and getting two contexts at the same time.

Julian Aubourg
+1  A: 

I think the key technique you're probably missing is that of saving the current context node in a variable before you do something that changes context nodes. What you've got can be made to work if you use that technique, which the example below uses.

Like a lot of XSLT problems, this is easier to solve if you think in transformations instead of procedures. The question isn't really "how do I nest for-each loops?", it's "how do I transform Item elements into the desired exec elements?"

<xsl:template match="/">
   <output>
      <xsl:apply-templates select="/Build/ItemsToBuild/Item"/>
   </output>
</xsl:template>

<xsl:template match="Item">
   <xsl:variable name="item" select="."/>
   <xsl:for-each select="/Build/BuildModes/Mode">
      <exec program="devenv">
         <xsl:attribute name="itemName" select="$item/@name"/>
         <xsl:attribute name="modeName" select="@name"/>
         <!-- and so on -->
      </exec>
   </xsl:for-each>
</xsl:template>
Robert Rossney
A: 

You can use a variable to store the Item context. Also there is a shorthand for cleaning up the attribute definitions.

<xsl:stylesheet version="1.0"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform"&gt;

    <xsl:template match="/">
        <someroot>
            <xsl:for-each select="/Build/ItemsToBuild/Item">
                <xsl:variable name="item" select="." />
                <xsl:for-each select="/Build/BuildModes/Mode">
                    <exec program="devenv"
                        item="{$item/@name}" line="{@name}" />
                </xsl:for-each>
            </xsl:for-each>
        </someroot>
    </xsl:template>
</xsl:stylesheet>

Result is

<someroot>
    <exec program="devenv" item="item1" line="Debug" />
    <exec program="devenv" item="item1" line="Release" />
    <exec program="devenv" item="item2" line="Debug" />
    <exec program="devenv" item="item2" line="Release" />
</someroot>
Willie Wheeler
+3  A: 

The key to success is not to use <xsl:for-each> at all.

<xsl:template match="/">
  <xsl:apply-templates select="Build/BuildModes/Mode" />
</xsl:template>

<xsl:template match="Build/BuildModes/Mode">
  <exec program="devenv">
    <xsl:apply-templates select="/Build/ItemsToBuild/Item">
      <xsl:with-param name="BuildMode" select="." />
    </xsl:apply-templates>
  </exec>
</xsl:template>

<xsl:template match="Build/ItemsToBuild/Item">
  <xsl:param name="BuildMode" />
  <xsl:attribute name="line">
    <!-- use $BuildMode/@name etc. to build up the command line -->
  </xsl:attribute>
</xsl:template>
Tomalak
Thank you, as you can probably tell my knowlege of xsl was gained from grabbing some examples and extrapolating from there. Much more to learn.
Alan Mullett