Public/Get-SecondLineStandard.ps1

function Get-SecondLineStandard {
    <#
    .SYNOPSIS
        Loads and validates a SecondLine standard file.
 
    .DESCRIPTION
        A standard is a .psd1 file listing the tools every machine should have,
        the version each should be at, and the rule used to compare versions.
 
        It can also have a GitAttributes section, used by Test-SLRepo and
        Set-SLGitAttributes, to add or replace line-ending profiles and pin repos
        to a profile:
 
          GitAttributes = @{
              Profiles = @{ web = @('* text=auto', '*.ts text eol=lf', '*.png binary') }
              Repos = @{ 'docs-site' = 'docs' }
          }
 
        It can pin the SecondLine version the project approves (and other
        modules), and suggest update-notice defaults. Both sections merge by key
        through Extends, with the child's values winning:
 
          ModuleVersions = @{ SecondLine = '0.4.0' }
          Updates = @{ RefreshHours = 24 }
 
        ModuleVersions is chosen before import by Bootstrap\Import-SecondLine.ps1.
        See Get-SLSetting for every layer and where each value comes from.
 
        A standard needs Tools, GitAttributes, ModuleVersions or Updates.
 
        A tool can have AppliesWhen, a list of file patterns. Then it is only
        checked in a git repo that contains a matching file (outside a repo: in a
        folder that does), and reported as SKIPPED elsewhere. The shared
        @development baseline uses this so the .NET SDK is only checked in .NET
        repos:
 
          AppliesWhen = @('global.json', '*.sln', '*.slnx', '*.csproj')
 
        Where the file is looked for, in order:
          1. -Path
          2. the SECONDLINE_STANDARD environment variable
          3. nearest secondline.standard.psd1 in the current folder or its parents
 
    .EXAMPLE
        Get-SecondLineStandard -Path .\secondline.standard.psd1
    #>

    [CmdletBinding()]
    param(
        [string] $Path
    )

    if (-not $Path) { $Path = $env:SECONDLINE_STANDARD }
    if (-not $Path) { $Path = Find-SLStandardPath }
    if (-not $Path) {
        throw ("SecondLine standard not found in the current folder or its parents. " +
            "Pass -Path, set SECONDLINE_STANDARD, or create secondline.standard.psd1 here or in a parent folder " +
            "(for example your home folder, to cover every repo under it). " +
            "Start from the example: https://github.com/douglaine/SecondLine/blob/main/examples/secondline.standard.psd1")
    }

    if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) {
        throw "SecondLine standard not found: $Path. Pass -Path, set SECONDLINE_STANDARD, or put secondline.standard.psd1 in the current folder."
    }
    $Path = (Resolve-Path -LiteralPath $Path).ProviderPath

    # Read-SLStandardData names the exact file that failed (which may be a parent
    # reached through Extends), so pass its message through unchanged.
    $data = Read-SLStandardData -Path $Path

    $problems = New-Object System.Collections.Generic.List[string]
    $hasTools = $data.ContainsKey('Tools') -and $null -ne $data.Tools -and @($data.Tools).Count -gt 0
    $hasSection = @('GitAttributes', 'ModuleVersions', 'Updates' | Where-Object { $data.ContainsKey($_) }).Count -gt 0
    if (-not $hasTools -and -not $hasSection) {
        $problems.Add("'Tools' is missing or empty. Add at least one tool, or a GitAttributes, ModuleVersions or Updates section.")
    }
    $moduleVersions = @{}
    if ($data.ContainsKey('ModuleVersions')) {
        if ($data.ModuleVersions -isnot [hashtable]) { $problems.Add("'ModuleVersions' must be @{ ModuleName = '1.2.3' }.") }
        else {
            foreach ($key in $data.ModuleVersions.Keys) {
                $value = "$($data.ModuleVersions[$key])"
                if ($value -notmatch '^\d+(\.\d+){1,3}$') { $problems.Add("ModuleVersions.$key is '$value', which is not a version like 0.4.0.") }
                else { $moduleVersions["$key"] = $value }
            }
        }
    }
    $updates = @{}
    if ($data.ContainsKey('Updates')) {
        foreach ($problem in @(Test-SLUpdateSettings -Settings $data.Updates -Label 'Updates')) { $problems.Add($problem) }
        if ($data.Updates -is [hashtable]) { $updates = $data.Updates }
    }

    $tools = @()
    $toolEntries = @()
    if ($hasTools) { $toolEntries = @($data.Tools) }
    $i = 0
    foreach ($t in $toolEntries) {
        $i++
        $label = "Tools[$i]"
        if ($t -isnot [hashtable]) { $problems.Add("$label is not a @{ ... } entry."); continue }
        if ($t.Name) { $label = "Tools[$i] ($($t.Name))" }

        foreach ($key in 'Name', 'Command', 'Expect', 'Rule') {
            if (-not $t.ContainsKey($key) -or -not "$($t[$key])") { $problems.Add("$label is missing '$key'.") }
        }
        if ($t.Rule -and $script:SLRules -notcontains $t.Rule) {
            $problems.Add("$label has Rule '$($t.Rule)'. Use one of: $($script:SLRules -join ', ').")
        }
        if ($t.Expect -and $null -eq (ConvertTo-SLVersionParts "$($t.Expect)")) {
            $problems.Add("$label has Expect '$($t.Expect)', which is not a version number like 8.0 or 2.13.1.")
        }

        $match = 'First'
        if ($t.ContainsKey('Match')) { $match = "$($t.Match)" }
        if (@('First', 'Any') -notcontains $match) {
            $problems.Add("$label has Match '$match'. Use First (default) or Any.")
        }

        $arguments = '--version'
        if ($t.ContainsKey('Arguments')) { $arguments = "$($t.Arguments)" }
        $timeout = 15
        if ($t.ContainsKey('TimeoutSeconds')) { $timeout = [int] $t.TimeoutSeconds }

        $appliesWhen = @()
        if ($t.ContainsKey('AppliesWhen')) {
            $appliesWhen = @(@($t.AppliesWhen) | Where-Object { "$_" } | ForEach-Object { "$_" })
            if ($appliesWhen.Count -eq 0) {
                $problems.Add("$label has an empty AppliesWhen. List file patterns, like @('*.sln', 'global.json'), or remove it.")
            }
        }

        $tools += [pscustomobject]@{
            Name           = "$($t.Name)"
            Command        = "$($t.Command)"
            Arguments      = $arguments
            Expect         = "$($t.Expect)"
            Rule           = "$($t.Rule)"
            Match          = $match
            VersionPattern = "$($t.VersionPattern)"
            Install        = "$($t.Install)"
            TimeoutSeconds = $timeout
            AppliesWhen    = $appliesWhen
        }
    }

    $gitAttributes = $null
    if ($data.ContainsKey('GitAttributes')) {
        $gitAttributes = ConvertTo-SLGitAttributesSection -Section $data.GitAttributes -Problems $problems
    }

    if ($problems.Count -gt 0) {
        throw ("SecondLine standard $Path has problems:`n - " + ($problems -join "`n - "))
    }

    $name = 'SecondLine standard'
    if ($data.Name) { $name = "$($data.Name)" }
    $version = ''
    if ($data.Version) { $version = "$($data.Version)" }

    [pscustomobject]@{
        Name           = $name
        Version        = $version
        Path           = $Path
        Tools          = $tools
        GitAttributes  = $gitAttributes
        ModuleVersions = $moduleVersions
        Updates        = $updates
    }
}