Format-AzTemplate.ps1

function Format-AzTemplate
{
    <#
    .Synopsis
        Formats a resource manager template in the desired order.
    .Description
        Sorts the content in a resource manager template.
    .Link
        https://github.com/Azure/azure-quickstart-templates/blob/master/1-CONTRIBUTION-GUIDE/best-practices.md
    #>

    param(
    # The path to a file
    [Parameter(Mandatory=$true,ParameterSetName='FilePath',ValueFromPipelineByPropertyName=$true,Position=0)]
    [Alias('Fullname')]
    [string]$FilePath,

    # The path to a file
    [Parameter(Mandatory=$true,ParameterSetName='TemplateObject',ValueFromPipelineByPropertyName=$true,Position=0)]
    [PSObject]$TemplateObject)

    begin {
        $topLevelPropertyOrder =
            '$schema','contentVersion', 'apiProfile',
            'parameters','functions','variables',
            'resources', 'outputs'

        $resourceOrder = 'comments', 'condition', 'type', 'apiVersion', 'name',
            'location', 'sku', 'kind', 'dependsOn', 'tags', 'copy'

        $sortProperties = {
            param([Parameter(ValueFromPipeline=$true)]$in, [string[]]$order,[string[]]$LastOrder, [switch]$Recurse)

            process {
                $newObject = [PSObject]::new() # create a new object to output.
                if (-not $in) { return $null }
                if ([string], [int], [float], [bool] -contains $in.GetType()) { return $in }
                if ($Recurse) {
                    $recurseSplat = @{} + $PSBoundParameters
                    $recurseSplat.Remove('In')
                }
                foreach ($propName in $order) { # Walk thru the properties in the preferred order.
                    if ($in.$propName) { # If the object had that property
                        $newPropValue =
                            if ($Recurse -and $in.$propName -is [Array]) {
                                @($in.$propName | & $MyInvocation.MyCommand.ScriptBlock @recurseSplat)
                            } else {
                                $in.$propName
                            }
                        $newProp = [Management.Automation.PSNoteProperty]::new($propName, $newPropValue)
                        $newObject.psobject.properties.add($newProp) # add it to the new object
                        $in.psobject.properties.remove($propName) # and remove it from the original object.

                    }
                }
                if (@($in.psobject.properties).Count) { # If the template object had any properties left
                    foreach ($prop in $in.psobject.properties) { # add them to the new object in the order they were found.
                        if ($LastOrder -contains $prop.Name) { continue }
                        $newProp =
                            [Management.Automation.PSNoteProperty]::new($prop.Name, $in.($prop.Name))
                        $newObject.psobject.properties.add($newProp)
                    }
                }
                if ($LastOrder) {
                    foreach ($propName in $LastOrder) {
                        if ($in.$propName) { # If the object had that property
                            $newPropValue =
                                if ($Recurse -and $in.$propName -is [Array]) {
                                    $in.$propName | & $MyInvocation.MyCommand.ScriptBlock @recurseSplat
                                } else {
                                    $in.$propName
                                }
                            $newProp =
                                [Management.Automation.PSNoteProperty]::new($propName, $newPropValue)
                            $newObject.psobject.properties.add($newProp) # add it to the new object
                            $in.psobject.properties.remove($propName) # and remove it from the original object.

                        }
                    }
                }
                $newObject
            }
        }
    }

    process {
        if ($PSCmdlet.ParameterSetName -eq 'FilePath') { # If we're provided the path to a file
            $templateObject = Import-Json -FilePath $FilePath

            if (-not $templateObject) { return } # If it was null, return.

            Format-AzTemplate -TemplateObject $TemplateObject # Call ourself, passing in the contents of the file.
            return
        }

        if ($PSCmdlet.ParameterSetName -eq 'TemplateObject') { # If we're provided a template object
            if ($templateObject -is [Collections.IDictionary]) {
                $templateObject = [PSCustomObject]$templateObject
            }
            $templateObjectCopy = @{}
            foreach ($prop in $templateObject.psobject.properties) {
                $templateObjectCopy[$prop.Name] = $prop.Value
            }
            $newTemplate =
                [PSCustomObject]$TemplateObjectCopy |
                    & $sortProperties -Order $topLevelPropertyOrder # sort the top-level properties.

            if ($newTemplate.resources) { # If the template had resources, sort them
                $newTemplate.resources =@(
                    $newTemplate.resources | & $sortProperties -Order $resourceOrder -LastOrder 'properties', 'resources' -Recurse
                )
            }
            return $newTemplate # then return the newly formatted object.
        }
    }
}