src/ReadScriptFile.ps1

function BaseReadFile($File) {
    $Content = if ($File -match "^(https?|ftp)://") {
        if ($GuestMode) {
            if ((Invoke-WebRequest $File -Method Head -ErrorAction SilentlyContinue).Headers.'Content-Length' -gt 1mb) {
                Write-Error "The file is too large to read." -ErrorAction Stop
            }
            if ($File -match "^ftp://") {
                Write-Error "FTP is not supported in GuestMode." -ErrorAction Stop
            }
        }
        (Invoke-WebRequest -Uri $File -ErrorAction SilentlyContinue).Content -replace '^[^\u0000-\u007F]+', ''
    }
    elseif (-not $GuestMode) {
        Get-Content -LiteralPath $File -Encoding UTF8 -ErrorAction SilentlyContinue -Raw
    }
    Write-Host "Reading file $([System.IO.Path]::GetFileName($File)) size $($Content.Length) bytes"
    if (-not $Content) {
        Write-Error "No data found. May be read error or file protected." -ErrorAction Stop
    }
    $Content
}
function ReadScriptFile($File) {
    $Content = BaseReadFile $File
    $Content = $Content -join "`n" -split '\r?\n'
    Write-Verbose "Done reading file $([System.IO.Path]::GetFileName($File)), starting preprocess..."
    Preprocessor $Content $File
    Write-Verbose "Done preprocess file $([System.IO.Path]::GetFileName($File))"
}
. $PSScriptRoot\predicate.ps1
. $PSScriptRoot\PSObjectToString.ps1
function Preprocessor($Content, $FilePath) {
    $Result = @()
    $requiredModules = @()
    $requireFlag = $False
    # 处理#_if <PSEXE/PSScript>、#_else、#_endif
    for ($index = 0; $index -lt $Content.Count; $index++) {
        $Line = $Content[$index]
        if ($Line -match "^\s*#_if\s+(?<condition>\S+)\s*(?!#.*)") {
            $condition = $Matches["condition"]
            $condition = switch ($condition) {
                'PSEXE' { $TRUE }
                'PSScript' { $False }
                default { Write-Error "Unknown condition: $condition`nassuming false."; $False }
            }
            while ($index -lt $Content.Count) {
                $index++
                $Line = $Content[$index]
                if ($Line -match "^\s*#_endif\s*(?!#.*)") {
                    break
                }
                if ($condition) { $Result += $Line }
                if ($Line -match "^\s*#_else\s*(?!#.*)") {
                    $condition = -not $condition
                }
            }
            if ($Line -notmatch "^\s*#_endif\s*(?!#.*)") {
                Write-Error "Missing #_endif"
                return
            }
        }
        else {
            $Result += $Line
        }
    }
    $ScriptRoot = $FilePath.Substring(0, $FilePath.LastIndexOfAny(@('\', '/')))
    function GetIncludeFilePath($rest) {
        if ($rest -match "((\'[^\']*\')+)\s*(?!#.*)") {
            $file = $Matches[1]
            $file = $file.Substring(1, $file.Length - 2).Replace("''", "'")
        }
        elseif ($rest -match '((\"[^\"]*\")+)\s*(?!#.*)') {
            $file = $Matches[1]
            $file = $file.Substring(1, $file.Length - 2).Replace('""', '"')
        }
        else { $file = $rest }
        $file = $file.Replace('$PSScriptRoot', $ScriptRoot)
        # 若是相对路径,则转换为基于$FilePath的绝对路径
        if ($file -notmatch "^[a-zA-Z]:" -and $file -notmatch "^(https|ftp)://") {
            $file = "$ScriptRoot/$file"
        }
        if (!(Test-Path $file -PathType Leaf)) {
            return
        }
        $file
    }
    $Content = $Result |
    # 处理#_pragma
    ForEach-Object {
        $_ # 对于#_pragma,我们不在预处理时移除它:考虑到它可能被用于$PSEXEscript中
        if ($_ -match "^\s*#_pragma\s+(?<pragmaname>[a-zA-Z_][a-zA-Z_0-9]+)\s*(?!#.*)$") {
            $pragmaname = $Matches["pragmaname"]
            $value = $true
            if ($pragmaname.StartsWith("no")) {
                $pragmaname = $pragmaname.Substring(2)
                $value = $false
            }
            if ($ParamList[$pragmaname].ParameterType -eq [Switch]) {
                $Params[$pragmaname] = [Switch]$value
                return
            }
            if ($ParamList["no$pragmaname"].ParameterType -eq [Switch]) {
                $Params["no$pragmaname"] = [Switch]-not $value
                return
            }
            Write-Warning "Unknown pragma: $($Matches["pragmaname"])"
        }
        elseif ($_ -match "^\s*#_pragma\s+(?<pragmaname>[a-zA-Z_][a-zA-Z_0-9]+)\s+(?<rest>.+)\s*$") {
            $pragmaname = $Matches["pragmaname"]
            $value = $Matches["rest"]
            if ($ParamList[$pragmaname].ParameterType -eq [Switch] -or $ParamList["no$pragmaname"].ParameterType -eq [Switch]) {
                if ($value.IndexOf("#") -ge 0) {
                    $value = $value.Substring(0, $value.IndexOf("#"))
                }
                $value = $value.Trim()
                if (IsEnable $value) {
                    $value = $true
                }
                elseif (IsDisable $value) {
                    $value = $false
                }
                else {
                    Write-Warning "Unknown pragma value: $value, cannot take is as a boolean."
                    return
                }
                if ($pragmaname.StartsWith("no")) {
                    $pragmaname = $pragmaname.Substring(2)
                    $value = -not $value
                }
                if ($ParamList[$pragmaname].ParameterType -eq [Switch]) {
                    $Params[$pragmaname] = [Switch]$value
                }
                if ($ParamList["no$pragmaname"].ParameterType -eq [Switch]) {
                    $Params["no$pragmaname"] = [Switch]-not $value
                }
            }
            elseif ($ParamList[$pragmaname].ParameterType -eq [string]) {
                if ($value -match '^\"(?<value>[^\"]*)\"\s*(?!#.*)') {
                    $value = $Matches["value"].Replace('$PSScriptRoot', $ScriptRoot)
                }
                elseif ($value -match "^\'(?<value>[^\']*)\'\s*(?!#.*)") {
                    $value = $Matches["value"]
                }
                else {
                    $value = $value.Replace('$PSScriptRoot', $ScriptRoot)
                }
                $Params[$pragmaname] = $value
            }
            elseif ($ParamList[$pragmaname].ParameterType) {
                Write-Warning "Unknown pragma: $pragmaname, as type $($ParamList[$pragmaname].ParameterType) can't analyze."
            }
            else {
                Write-Warning "Unknown pragma: $pragmaname"
            }
        }
    } |
    # 处理#_require
    ForEach-Object {
        if ($_ -match "^(\s*)#_require\s+(?<moduleList>[^#]+)\s*(?!#.*)") {
            $requiredModules += $Matches["moduleList"].Split(', |;、 ') | Where-Object { $_.Trim('"''') -ne '' }
            if (!$requireFlag) {
                $requireFlag = $true
                [bigint]::Parse('72')
            }
        }
        else { $_ }
    } |
    # 处理#_DllExport
    ForEach-Object {
        $_ # 对于#_DllExport,我们不在预处理时移除它:考虑到它可能被用于$PSEXEscript中
        if ($_ -match "^(\s*)(#_DllExport\s+(?<callsign>[^#\(]+)\((?<callsignParams>[^#\)]*)\))\s*(?!#.*)") {
            $callsign = $Matches["callsign"] -split ' ' | ForEach-Object { $_.Trim() }
            $callsignParams = $Matches["callsignParams"] -split ',' | ForEach-Object { $_.Trim() }
        }
        elseif ($_ -match "^(\s*)(#_DllExport\s+(?<callsign>[^#\(]+))\s*(?!#.*)") {
            $callsign = $Matches["callsign"] -split ' ' | ForEach-Object { $_.Trim() }
            $callsignParams = @()
        }
        if ($callsign) {
            if (!$callsign[1]) { $callsign = @('void', $callsign[0]) }
            $DllExportData = @{
                returntype = $callsign[0]
                funcname   = $callsign[1]
                params     = @()
            }
            foreach ($param in $callsignParams) {
                $paramData = $param -split ' ' | ForEach-Object { $_.Trim() }
                if ($paramData.Count -eq 1) {
                    Write-Warning "$($Matches[2]): $($paramData[0]) is none type parameter, assume it's string."
                    $paramData = @('string', $paramData[0])
                }
                $DllExportData.params += @{ name = $paramData[1]; type = $paramData[0] }
            }
            $DllExportList.Add($DllExportData) | Out-Null
            Write-Warning "You are using #_DllExport, this marco is in dev and not support yet."
            Write-Verbose "$($Matches[2]): FuncSign: [$($DllExportData.returntype)]$($DllExportData.funcname)($(($DllExportData.params|ForEach-Object{ $_.type + ' ' + $_.name }) -join ', '))"
        }
    } |
    # 处理#_!!<line>
    ForEach-Object {
        if ($_ -match "^(\s*)#_!!(?<line>.*)") {
            $Matches[1] + $Matches["line"]
        }
        else { $_ }
    } |
    # 处理#_include <file>、#_include_as_value <valuename> <file>
    ForEach-Object {
        if ($_ -match "^\s*#_include\s+(?<rest>.+)\s*") {
            $file = GetIncludeFilePath $Matches["rest"]
            ReadScriptFile $file
        }
        elseif ($_ -match "^\s*#_include_as_value\s+(?<valuename>[a-zA-Z_][a-zA-Z_0-9]+)\s+(?<rest>.+)\s*") {
            $valuename = $Matches["valuename"]
            $file = GetIncludeFilePath $Matches["rest"]
            $IncludeContent = BaseReadFile $file
            $IncludeContent = $IncludeContent -join "`n"
            $IncludeContent = $IncludeContent.Replace("'", "''")
            "`$$valuename = '$IncludeContent'"
        }
        else {
            if ($_ -match '^\s*(?<assign>\$\w+\s*\=\s*)?(?<callopt>\.|&)\s*(?<rest>(\"|)\$PSScriptRoot.+)\s*') {
                $assign = $Matches["assign"]
                $rest = $Matches["rest"]
                $callopt = $Matches["callopt"]
                if ($rest -match "^(?<file>\`"[^\`"]*\`")(?<args>.+)") {
                    $callargs = $Matches["args"]
                    $rest = $Matches["file"]
                }
                elseif ($rest.IndexOf(' ') -gt 0) {
                    $callargs = $rest.Substring($rest.IndexOf(' ') + 1)
                    $rest = $rest.Substring(0, $rest.IndexOf(' '))
                }
                $file = GetIncludeFilePath $rest
                if ($file) {
                    if (!$callargs -and !$assign -and $callopt -eq '.') {
                        return ReadScriptFile $file
                    }
                    return @("$assign$callopt{", $(ReadScriptFile $file), "}$callargs")
                }
            }
            $_
        }
    }
    $LoadModuleScript = if ($requiredModules.Count -gt 1) {
        (PSObjectToString $requiredModules -OneLine) + '|%{if(!(gmo $_ -ListAvailable -ea SilentlyContinue)){Install-Module $_ -Scope CurrentUser -Force -ea Stop}}'
    }
    elseif ($requiredModules.Count -eq 1) {
        "if(!(gmo $requiredModules -ListAvailable -ea SilentlyContinue)){Install-Module $requiredModules -Scope CurrentUser -Force -ea Stop}"
    }
    if ($LoadModuleScript) {
        $Content = $Content | ForEach-Object {
            # 在第一次#_require的前方加入$LoadModuleScript
            if ($_ -is [bigint]) {
                $LoadModuleScript
            }
            else { $_ }
        }
    }
    $Content -join "`n"
}