functions/Initialize-BicepTemplate.ps1

function Initialize-BicepTemplate {
    <#
    .SYNOPSIS
    Initializes a directory with files from a predefined template library.
 
    .DESCRIPTION
    Generates files based on a selected template from a library of templates into a directory.
     
    #>

    [CmdletBinding()]
    param (
        [Parameter()]
        [System.IO.DirectoryInfo]
        $Target = '.',

        [Parameter()]
        [ValidateSet('deployment', 'registry')]
        [System.String]
        $Template = 'deployment',

        [Parameter()]
        [System.Collections.Hashtable]
        $InitParameter
    )

    BEGIN {

        <#
        ////////////////////////////////////////////////////////////////////////////////
         
        Helper Script for getting all choice folders recursively.
 
        #>


        function Get-AllFiles{
            param (
                [System.Int32]$Depth = 10,
                [System.IO.DirectoryInfo] $path
            )

            if ($Depth -LE 0) {
                Write-Verbose "[Get-AllFiles] Max Depth reached at $($path.FullName)"
                return @()
            }

            $files = @()

            $files += Get-ChildItem -Path $path -File

            $searchFolders = @()
            $searchFolders += Get-ChildItem -Path $path -Directory
            if ($IsLinux) {
                # On Linux hidden folders need to be searched separately
                $searchFolders += Get-ChildItem -Path $path -Directory -Hidden
            }

            foreach ($folder in $searchFolders) {
                $files += Get-AllFiles -path $folder -Depth ($Depth - 1)
            }

            return $files
        }
        
        <#
        ////////////////////////////////////////////////////////////////////////////////
 
        Helper script for copying files.
 
        This avoids problems with existing subdirectories on Copy-Item in the staging directory.
        By only creating subdirectories when they do not exist and then copying all files.
 
        #>

        function Copy-Helper {

            param(
                [System.IO.DirectoryInfo] $sourceDir,
                [System.IO.DirectoryInfo] $targetDir,
                [switch] $overwrite,
                [switch] $onlyWarn
            )
        
            $sourceFiles = Get-AllFiles -path $sourceDir

            foreach ($file in $sourceFiles) {
                $relativePathDir = Resolve-Path -Relative -Path $file.Directory.FullName -RelativeBasePath $sourceDir
                $templateDir = [System.IO.DirectoryInfo]::new("$targetDir/$relativePathDir")
    
                Write-Verbose "[Copy-Helper] Ensuring directory $($templateDir.FullName) exists"
                if (-NOT $templateDir.Exists) {
                    $null = $templateDir.Create()
                }

                $relativePathFile = Resolve-Path -Relative -Path $file.FullName -RelativeBasePath $sourceDir
                $templateFile = [System.IO.FileInfo]::new("$targetDir/$relativePathFile")
    
                if ($onlyWarn.IsPresent -AND -NOT $overwrite.IsPresent -AND $templateFile.Exists) {
                    Write-Warning "SKIPPING | File Exists: $($relativePathFile)"
                }
                elseif (-NOT $overwrite.IsPresent -AND $templateFile.Exists) {
                    Write-Error "ERROR | File Exists: $($relativePathFile)"
                }
                else {
                    Write-Verbose "[Copy-Helper] Copying file $($relativePathFile) to $($templateFile.FullName)"
                    Copy-Item -Path $file.FullName -Destination $templateFile.FullName
                }
            }

        }


        <#
        ////////////////////////////////////////////////////////////////////////////////
 
        Initialize all relevant variables:
        - targetDir where to copy files
        - sourceDir for the template
        - commonDir for common files
        - initPs1 for the template initialization script
 
        #>

        $common = Get-Item -Path "$PSScriptRoot/library/common"
        $initPs1 = Get-Item -Path "$PSScriptRoot/library/init.ps1"
        $rootDir = Get-Item -Path "$PSScriptRoot/library/$Template"

        if (-NOT [System.IO.Path]::IsPathFullyQualified($Target)) {
            $rootedPath = [System.IO.Path]::Join((Get-Location).Path, $Target)
            $Target = [System.IO.DirectoryInfo]::new($rootedPath)
        }

        $tempDirPath = [System.IO.Path]::GetFullPath(
            [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), 'bicep-staging', [System.Guid]::NewGuid().ToString())
        )
        $tempDir = $null

    }

    END {

        <#
        ////////////////////////////////////////////////////////////////////////////////
 
        All files are copied to the staging directory,
        where they are modified and then copied to the target directory.
 
        #>


        $tempDir = New-Item -ItemType Directory -Path $tempDirPath

        Copy-Helper -sourceDir $rootDir -targetDir $tempDir
        Copy-Helper -sourceDir $common -targetDir $tempDir

        . $initPs1 @InitParameter -StagingDir $tempDir

        # Folders with choice.<something> are removed afterwards.
        # These folders help organize the templates better.
        $choiceFolders = $null
        $maxLoops = 1000
        do {
            $choiceFolders = Get-AllFiles -path $tempDir
            | Select-Object -ExpandProperty Directory
            | Where-Object -Property Name -Like 'choice.*'
            | Sort-Object -Property FullName -Descending
            | Select-Object -Unique

            # We can break early when no choice folders are found.
            if ($choiceFolders.Count -EQ 0) {
                break
            }

            # This will move all items in the choice folder to the parent folder

            # We do only one choice folder per iteration,
            # - Some choice folders are nested in others and moving will change the paths of some nested choice folders.
            # - Removing one layer at each iteration reduces the complexity of taking this into account
            $folder = $choiceFolders | Select-Object -First 1
            $items = Get-ChildItem -Path $folder.FullName
            foreach ($item in $items) {
                Write-Verbose "[Initialize-BicepTemplate] Moving item $($item.FullName) to $($folder.Parent.FullName)"
                Move-Item -Path $item.FullName -Destination $folder.Parent.FullName
            }

            # The empty folder is removed
            $items = Get-ChildItem -Path $folder.FullName
            if ($items.Count -EQ 0) {
                $null = $folder.Delete($true)
            }
            else {
                Write-Warning "Something went wrong when copying items"
            }

        } while ($maxLoops-- -GT 0)

        <#
        ////////////////////////////////////////////////////////////////////////////////
 
        This is the final copy operation from staging to the user directory
        Uses the copy dialog for Windows and the shell copy for Linux.
 
        #>


        if (-NOT $Target.Exists) {
            $Target.Create()
        }
        
        $existingFiles = Get-ChildItem -Path $Target -Recurse -Force -File -ErrorAction SilentlyContinue
        $overwrite = $existingFiles.Count -EQ 0

        if (-NOT $overwrite) {
            Write-Host -ForegroundColor RED "`n`nTarget: $Target"
            Write-Host -ForegroundColor RED "Files already exist in the target directory."
            $overwrite = Select-UtilsUserOption -Prompt "Overwrite? (Yes/No) "
        }

        Get-ChildItem -Path $tempDir -Force | Copy-Item -Destination $Target.FullName -Recurse -Force:$overwrite
    }

    CLEAN {

        if ($null -NE $tempDir) {
            # This cleans up staging directory from directories older than 10 minutes.
            # In case of a crash, when the staging directory was not deleted.
            Get-ChildItem -Path $tempDir.Parent -Directory
            | Where-Object -Property CreationTime -LT (Get-Date).AddMinutes(-10)
            | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue

            [System.GC]::Collect()
            [System.GC]::WaitForPendingFinalizers()

            # To make sure that the staging directory is definitly deleted.
            for ($tries = 1; $tries -LE 5; $tries++) {
                try {
                    Remove-Item -Path $tempDir -Recurse -Force -ErrorAction Stop -ProgressAction SilentlyContinue
                }
                catch {
                    Start-Sleep -Milliseconds 50
                }
            }
        }

    }
}