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.
     
    #>

    [Alias('bicep-init')]
    [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 = 5,
                [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)
        }

    }

    END {

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

        
        $tempDir = "{0}/bicep-staging/{1}" -f [System.IO.Path]::GetTempPath(), [System.Guid]::NewGuid()
        $tempDir = New-Item -ItemType Directory -Path $tempDir

        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()
        }
        
        if ($IsWindows) {
            $destination = New-Object -ComObject "Shell.Application"
            $destination = $destination.NameSpace($Target.FullName)
            $destination.CopyHere("$tempDir/*")
            return
        }

        try {
            Copy-Helper -sourceDir $tempDir -targetDir $Target
        }
        catch {
            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) "

            Copy-Helper -sourceDir $tempDir -targetDir $Target -overwrite:$($overwrite) -onlyWarn
        }
    }

    CLEAN {

        # 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
            }
        }

    }
}