ค้นหาเว็บไซต์

การส่งออกฟังก์ชัน PowerShell ไปยังไฟล์


เมื่อฉันเขียนโมดูล PowerShell โดยทั่วไปจะมีฟังก์ชันการส่งออกมากกว่าหนึ่งฟังก์ชัน ตำแหน่งที่คุณจัดเก็บฟังก์ชันโมดูลของคุณเป็นหัวข้อสนทนาที่ยอดเยี่ยม และฉันไม่คิดว่าจะต้องมีแนวทางปฏิบัติที่ดีที่สุดข้อใดข้อหนึ่งสำหรับทุกคน ฉันคิดว่ามันอาจจะขึ้นอยู่กับจำนวนและความซับซ้อนของฟังก์ชัน มีคนอื่นร่วมเขียนโค้ดให้กับโมดูลหรือไม่? คุณเพลิดเพลินกับ Pester แค่ไหน? นี่เป็นเพียงคำถามสองสามข้อที่ต้องพิจารณา ในงานของฉัน บางครั้งฟังก์ชันต่างๆ จะอยู่ในไฟล์ .psm1 บางโครงการมีไฟล์ public.ps1 พร้อมฟังก์ชันหลายอย่าง ในโปรเจ็กต์ที่ซับซ้อนมากขึ้น เช่น โมดูล PSScriptTools ของฉัน แต่ละฟังก์ชันจะได้รับไฟล์แยกกัน

ความท้าทายของฉันเกิดขึ้นเมื่อฉันตัดสินใจแยกฟังก์ชันทั้งหมดที่กำหนดไว้ในไฟล์เดียว ฉันต้องการให้แต่ละฟังก์ชันอยู่ในไฟล์ของตัวเอง โดยใช้ชื่อฟังก์ชันเป็นชื่อไฟล์ ฉันเบื่อที่จะทำสิ่งนี้ด้วยตนเอง ดังนั้นฉันจึงเขียนฟังก์ชัน PowerShell เพื่อทำงานให้ฉัน

การใช้ AST

PowerShell ใช้แนวคิดการเขียนโค้ดที่เรียกว่า "แผนผังไวยากรณ์นามธรรม" หรือ AST AST ทำให้สามารถแยกวิเคราะห์นิพจน์ PowerShell หรือแม้แต่ไฟล์ได้ การใช้ AST ถือเป็นการเขียนสคริปต์ PowerShell ขั้นสูงอย่างแน่นอน

New-Variable astTokens -Force
New-Variable astErr -Force
$AST = [System.Management.Automation.Language.Parser]::ParseFile($Path, [ref]$astTokens, [ref]$astErr)

ขึ้นอยู่กับสิ่งที่คุณกำลังสรุป คุณอาจได้สิ่งที่คุณต้องการอย่างแน่นอนจากการแยกวิเคราะห์ทันที ในกรณีของฉัน ฉันจำเป็นต้องค้นหาองค์ประกอบคำสั่งประเภทใดประเภทหนึ่งภายในผลลัพธ์ AST

$functions = $ast.FindAll({ $args[0] -is [System.Management.Automation.Language.FunctionDefinitionAst] }, $true)

ในตัวอย่างของฉัน ฉันพบฟังก์ชันเหล่านี้ในไฟล์

ฉันจะชี้ให้เห็นว่าชื่อฟังก์ชันที่ไม่เป็นไปตามหลักการตั้งชื่อคำกริยาและคำนามคือตัวสร้างคลาส แม้ว่าพวกมันอาจเป็นฟังก์ชันผู้ช่วยส่วนตัวที่โดยทั่วไปจะใช้ชื่อที่ไม่เป็นมาตรฐานเนื่องจากไม่เคยเปิดเผยต่อสาธารณะ ข้อมูลเพิ่มเติมเกี่ยวกับวิธีจัดการเรื่องนี้ในอีกสักครู่

แต่ละองค์ประกอบฟังก์ชั่นเป็นวัตถุของตัวเอง

อย่างที่คุณเห็น มันค่อนข้างง่ายในการรับข้อความฟังก์ชัน มันจะง่ายยิ่งขึ้นถ้าฉันใช้วิธี ToString()

 Set-Content -path "d:\temp\$($functions[-1].name).ps1" -Value $functions[-1].ToString()

การทดสอบชื่อฟังก์ชัน

ขณะที่ฉันกำลังสร้างเครื่องมือส่งออก ฉันรู้ว่าฉันต้องการวิธีข้ามฟังก์ชันที่อาจไม่มีวันเปิดเผยต่อสาธารณะ ดังนั้นฉันจึงเขียนฟังก์ชันง่ายๆ เพื่อทดสอบชื่อฟังก์ชัน

Function Test-FunctionName {
    [CmdletBinding()]
    [OutputType("boolean")]
    Param(
    [Parameter(Position = 0,Mandatory,HelpMessage = "Specify a function name.")]
    [ValidateNotNullOrEmpty()]
    [string]$Name
    )

    Write-Verbose "Validating function name $Name"
    #Function name must first follow Verb-Noun pattern
    if ($Name -match "^\w+-\w+$") {
        #validate the standard verb
        $verb = ($Name -split "-")[0]
        Write-Verbose "Validating detected verb $verb"
        if ((Get-Verb).verb -contains $verb ) {
            $True
        }
        else {
            Write-Verbose "$($Verb.ToUpper()) is not an approved verb."
            $False
        }
    }
    else {
        Write-Verbose "$Name does not match the regex pattern ^\w+-\w+$"
        $False
    }
}

ฉันรู้ว่านี่อาจเป็นคำสั่งบรรทัดเดียว แต่ฉันต้องการเพิ่มคำอธิบายว่าเหตุใดชื่อฟังก์ชันจึงล้มเหลว ฟังก์ชันจะใช้ชื่อและทดสอบก่อนว่าตรงกับรูปแบบ "Word-Word" หรือไม่ หากผ่าน ฟังก์ชันจะทดสอบว่า “กริยา” เป็นกริยามาตรฐานหรือไม่ ถ้าเป็นเช่นนั้นชื่อจะผ่านการทดสอบ

ฟังก์ชันการส่งออก

ถึงเวลาที่จะรวบรวมทั้งหมดนี้เข้าด้วยกัน

Function Export-FunctionFromFile {
    [cmdletbinding(SupportsShouldProcess)]
    [alias("eff")]
    [OutputType("None", "System.IO.FileInfo")]
    Param(
        [Parameter(Position = 0, Mandatory, HelpMessage = "Specify the .ps1 or .psm1 file with defined functions.")]
        [ValidateScript({
                If (Test-Path $_ ) {
                    $True
                }
                Else {
                    Throw "Can't validate that $_ exists. Please verify and try again."
                    $False
                }
            })]
        [ValidateScript({
                If ($_ -match "\.ps(m)?1$") {
                    $True
                }
                Else {
                    Throw "The path must be to a .ps1 or .psm1 file."
                    $False
                }
            })]
        [string]$Path,
        [Parameter(HelpMessage = "Specify the output path. The default is the same directory as the .ps1 file.")]
        [ValidateScript({ Test-Path $_ })]
        [string]$OutputPath,
        [Parameter(HelpMessage = "Export all detected functions.")]
        [switch]$All,
        [Parameter(HelpMessage = "Pass the output file to the pipeline.")]
        [switch]$Passthru
    )
    Write-Verbose "Starting $($MyInvocation.MyCommand)"

    #always create these variables
    New-Variable astTokens -Force -WhatIf:$False
    New-Variable astErr -Force -WhatIf:$False

    if (-Not $OutputPath) {
        #use the parent path of the file unless the user specifies a different path
        $OutputPath = Split-Path -Path $Path -Parent
    }

    Write-Verbose "Processing $path for functions"
    #the file will always be parsed regardless of WhatIfPreference
    $AST = [System.Management.Automation.Language.Parser]::ParseFile($Path, [ref]$astTokens, [ref]$astErr)

    #parse out functions using the AST
    $functions = $ast.FindAll({ $args[0] -is [System.Management.Automation.Language.FunctionDefinitionAst] }, $true)

    if ($functions.count -gt 0) {
        Write-Verbose "Found $($functions.count) functions"
        Write-Verbose "Creating files in $outputpath"
        Foreach ($item in $functions) {
            Write-Verbose "Detected function $($item.name)"
            #only export functions with standard namees or if -All is detected.
            if ($All -OR (Test-FunctionName -name $item.name)) {
                $newfile = Join-Path -Path $OutputPath -ChildPath "$($item.name).ps1"
                Write-Verbose "Creating new file $newFile"
                Set-Content -Path $newFile -Value $item.ToString() -Force
                if ($Passthru -AND (-Not $WhatIfPreference)) {
                    Get-Item -Path $newfile
                }
            }
            else {
                Write-Verbose "Skipping $($item.name)"
            }
        } #foreach item
    }
    else {
        Write-Warning "No functions detected in $Path."
    }
    Write-Verbose "Ending $($MyInvocation.MyCommand)"
} #end function

ตามค่าเริ่มต้น ฟังก์ชันจะส่งออกเฉพาะฟังก์ชันที่มีชื่อมาตรฐานเท่านั้น

หากคุณดูเอาต์พุตแบบ Verbose อย่างใกล้ชิด คุณจะเห็นว่าฟังก์ชันที่ไม่ได้มาตรฐานที่ตรวจพบถูกข้ามไป แม้ว่าฉันจะสามารถรวมฟังก์ชันที่ตรวจพบทั้งหมดได้ถ้าจำเป็น

ฟังก์ชันนี้ช่วยให้ฉันระบุตำแหน่งอื่นสำหรับไฟล์ใหม่ได้ แม้ว่าค่าเริ่มต้นจะเป็นไดเร็กทอรีหลักของไฟล์ต้นฉบับก็ตาม

ไฟล์ต้นฉบับไม่ได้รับผลกระทบ แม้ว่าฉันคิดว่าฉันสามารถแก้ไขฟังก์ชันเพื่อลบโค้ดออกจากไฟล์ได้ สำหรับตอนนี้ ฉันจะอัปเดตไฟล์ต้นฉบับและไฟล์โมดูลอื่นๆ ด้วยตนเองเพื่อแสดงไฟล์ฟังก์ชันใหม่

ลองมัน

ฉันมีทั้งสองฟังก์ชั่นในไฟล์เดียวกันซึ่งฉันดอทซอร์ส แม้ว่าฉันอาจจะเพิ่มฟังก์ชันเหล่านี้ลงในโมดูล PSScriptTools ในอนาคต ฟังก์ชันส่งออกจะถือว่าไฟล์นั้นเป็นไฟล์ .ps1 หรือ .psm1 คุณสามารถดูการตรวจสอบพารามิเตอร์ที่ฉันใช้ในโค้ดได้ ลองดูสิและแจ้งให้เราทราบว่าคุณคิดอย่างไร รหัสควรทำงานใน Windows PowerShell และ PowerShell 7.x

บทความที่เกี่ยวข้อง