Public/New-PstFunction.ps1
|
function New-PstFunction { <# .SYNOPSIS Creates a new PowerShell function from a template with configurable complexity levels. .DESCRIPTION This function takes an approved verb and noun, copies a function template based on the specified complexity level, and renames the file and function block to the specified verb-noun combination. Complexity Levels: - Basic: Simple function with minimal structure (no error handling, basic parameters) - Moderate: Function with parameter validation, basic error handling, and help documentation - Complex: Full cmdlet features including begin/process/end blocks, pipeline support, ShouldProcess .PARAMETER Verb (Required) The approved verb for the function name. .PARAMETER Noun (Required) The noun for the function name. .PARAMETER ComplexityLevel (Optional) The complexity level of the function template. Valid values: Basic, Moderate, Complex. Defaults to Complex for backward compatibility. .PARAMETER Force (Optional) Forces creation even if file exists, useful in automation scenarios. .PARAMETER IncludeProjectLayout (Optional) Creates a complete project structure including src/, tests/, docs/ directories and supporting files. .EXAMPLE New-PstFunction -Verb Get -Noun Item Creates a new complex function file named Get-Item.ps1 with full cmdlet features. .EXAMPLE New-PstFunction -Verb Get -Noun Item -ComplexityLevel Basic Creates a simple function with minimal structure for basic automation tasks. .EXAMPLE New-PstFunction -Verb Set -Noun Configuration -ComplexityLevel Moderate Creates a moderately complex function with parameter validation and error handling. .EXAMPLE New-PstFunction -Verb Get -Noun UserData -IncludeProjectLayout Creates a complete project structure with Get-UserData.ps1 in src/, test files, documentation, and .gitignore. .NOTES Ensure that the appropriate template files exist in the Resources/Samples directory. Template files: Function-Basic.ps1, Function-Moderate.ps1, Function-Complex.ps1 .LINK For more information on approved verbs, visit: https://docs.microsoft.com/en-us/powershell/scripting/developer/cmdlet/approved-verbs-for-windows-powershell-commands #> [CmdletBinding(SupportsShouldProcess)] param ( [Parameter(Mandatory = $true, Position = 0, HelpMessage = "The approved verb for the function name.")] [ValidateSet("Add", "Approve", "Assert", "Backup", "Block", "Checkpoint", "Clear", "Close", "Compare", "Complete", "Compress", "Confirm", "Connect", "Convert", "ConvertFrom", "ConvertTo", "Copy", "Debug", "Deny", "Disable", "Disconnect", "Dismount", "Edit", "Enable", "Enter", "Exit", "Expand", "Export", "Find", "Format", "Get", "Grant", "Group", "Hide", "Import", "Initialize", "Install", "Invoke", "Join", "Limit", "Lock", "Measure", "Merge", "Mount", "Move", "New", "Open", "Optimize", "Out", "Ping", "Pop", "Protect", "Publish", "Push", "Read", "Receive", "Redo", "Register", "Remove", "Rename", "Repair", "Request", "Reset", "Resize", "Resolve", "Restart", "Restore", "Resume", "Revoke", "Save", "Search", "Select", "Send", "Set", "Show", "Skip", "Split", "Start", "Step", "Stop", "Submit", "Suspend", "Switch", "Sync", "Test", "Trace", "Unblock", "Undo", "Uninstall", "Unlock", "Unprotect", "Unpublish", "Unregister", "Update", "Use", "Wait", "Watch", "Write")] [string]$Verb, [Parameter(Mandatory = $true, Position = 1, HelpMessage = "The noun for the function name.")] [ValidatePattern('^[A-Za-z][A-Za-z0-9]*$')] [string]$Noun, [Parameter(Mandatory = $false)] [ValidateSet("Basic", "Moderate", "Complex")] [string]$ComplexityLevel = "Complex", [Parameter(Mandatory = $false)] [switch]$Force, [Parameter(Mandatory = $false)] [switch]$IncludeProjectLayout ) begin { Write-Debug -Message "Begin '$($MyInvocation.MyCommand.Name)' at '$(Get-Date)'" # Select template based on complexity level $templateFileName = switch ($ComplexityLevel) { "Basic" { "Function-Basic.ps1" } "Moderate" { "Function-Moderate.ps1" } "Complex" { "Function-Complex.ps1" } default { "Function-Complex.ps1" } # Fallback to Complex } $templatePath = $Samples[$templateFileName] $newFunctionName = "$Verb-$Noun" # Determine file path based on project layout option if ($IncludeProjectLayout) { $projectRoot = $newFunctionName $newFilePath = Join-Path $projectRoot "src" "$newFunctionName.ps1" } else { $newFilePath = "$newFunctionName.ps1" } Write-Verbose "Using complexity level: $ComplexityLevel" Write-Verbose "Selected template: $templateFileName" if (-not $templatePath) { throw "Template '$templateFileName' not found in Samples hashtable. Available templates: $($Samples.Keys -join ', ')" } if (-not (Test-Path $templatePath)) { throw "Template file not found: $templatePath" } else { Write-Verbose "Template file found: $templatePath" } } process { try { # Check if we're in automation mode - inline check to avoid scope issues $isAutomationMode = $Force -or ($Global:PstAutomationMode -eq $true) -or ($env:CI -eq 'true') -or ($env:GITHUB_ACTIONS -eq 'true') -or ($Host.Name -eq 'ServerRemoteHost') -or (-not [Environment]::UserInteractive) -or ($global:ConfirmPreference -eq 'None') # Create project structure if IncludeProjectLayout is specified if ($IncludeProjectLayout) { $projectRoot = $newFunctionName # Create directory structure $directories = @( $projectRoot, (Join-Path $projectRoot "src"), (Join-Path $projectRoot "tests"), (Join-Path $projectRoot "tests" "Unit"), (Join-Path $projectRoot "tests" "Integration"), (Join-Path $projectRoot "docs") ) foreach ($dir in $directories) { if (-not (Test-Path $dir)) { if ($isAutomationMode -or $PSCmdlet.ShouldProcess($dir, "Create directory")) { New-Item -Path $dir -ItemType Directory -Force | Out-Null Write-Verbose "Created directory: $dir" } } } # Create README.md in root $readmeContent = @" # $newFunctionName PowerShell function for [brief description]. ## Installation ``````powershell Import-Module .\src\$newFunctionName.ps1 `````` ## Usage ``````powershell $newFunctionName -Parameter Value `````` ## Testing ``````powershell Invoke-Pester -Path .\tests\ `````` ## License [Specify license] "@ $readmePath = Join-Path $projectRoot "README.md" if ($isAutomationMode -or $PSCmdlet.ShouldProcess($readmePath, "Create README.md")) { $readmeContent | Set-Content -Path $readmePath Write-Verbose "Created: $readmePath" } # Create .gitignore $gitignoreContent = @" # Build artifacts build/ *.nupkg # Test results TestResults*.xml coverage*.xml # Temporary files temp/ *.tmp *.log "@ $gitignorePath = Join-Path $projectRoot ".gitignore" if ($isAutomationMode -or $PSCmdlet.ShouldProcess($gitignorePath, "Create .gitignore")) { $gitignoreContent | Set-Content -Path $gitignorePath Write-Verbose "Created: $gitignorePath" } # Create docs README.md $docsReadmeContent = @" # $newFunctionName Documentation ## Overview [Function overview and purpose] ## Parameters [Parameter documentation] ## Examples [Usage examples] ## Notes [Additional notes] "@ $docsReadmePath = Join-Path $projectRoot "docs" "README.md" if ($isAutomationMode -or $PSCmdlet.ShouldProcess($docsReadmePath, "Create docs README.md")) { $docsReadmeContent | Set-Content -Path $docsReadmePath Write-Verbose "Created: $docsReadmePath" } # Create test file $testContent = @" BeforeAll { Import-Module "`$PSScriptRoot\..\..\src\$newFunctionName.ps1" -Force } Describe "$newFunctionName" { Context "When valid parameters are provided" { It "Should execute without errors" { # Arrange # Act # Assert `$true | Should -Be `$true } } Context "When invalid parameters are provided" { It "Should throw an error" { # Arrange & Act & Assert { $newFunctionName -Parameter `$null } | Should -Throw } } } "@ $testFilePath = Join-Path $projectRoot "tests" "Unit" "$newFunctionName.Tests.ps1" if ($isAutomationMode -or $PSCmdlet.ShouldProcess($testFilePath, "Create test file")) { $testContent | Set-Content -Path $testFilePath Write-Verbose "Created: $testFilePath" } } # Check if file already exists if ((Test-Path $newFilePath) -and -not $isAutomationMode) { if (-not $PSCmdlet.ShouldProcess($newFilePath, "Overwrite existing function file")) { Write-Warning "Operation cancelled by user." return } } elseif ((Test-Path $newFilePath) -and $isAutomationMode -and -not $Force) { Write-Warning "File '$newFilePath' already exists. Use -Force to overwrite in automation mode." return } # Proceed with file creation - skip ShouldProcess in automation mode if ($isAutomationMode -or $PSCmdlet.ShouldProcess($newFilePath, "Create new function file")) { Write-Verbose "Getting file content from '$($templatePath)'" $templateContent = (Get-Content -Path $templatePath) -join "`n" Write-Verbose "Replacing '""Function Verb-Noun"" with ""Function $($newFunctionName)""' " $newContent = $templateContent -replace "Function Verb-Noun", "Function $newFunctionName" # Create parent directory if it doesn't exist (for project layout) $parentDir = Split-Path -Path $newFilePath -Parent if ($parentDir -and -not (Test-Path $parentDir)) { New-Item -Path $parentDir -ItemType Directory -Force | Out-Null } Write-Verbose "Set-Content to path '$($newFilePath)'" $newContent | Set-Content -Path $newFilePath if ($IncludeProjectLayout) { Write-Output "New function project created: $projectRoot" Write-Output " - Function file: $newFilePath" Write-Output " - Test file: tests\Unit\$newFunctionName.Tests.ps1" Write-Output " - Documentation: docs\README.md" } else { Write-Output "New function file created: $newFilePath" } } else { Write-Verbose "Operation cancelled or skipped." } } catch { if ($_.Exception -and $_.Exception.Message) { Write-Error "An error occurred: $($_.Exception.Message)" } else { Write-Error "An error occurred, but no additional information is available." } } } end { if ($?) { Write-Debug -Message "End '$($MyInvocation.MyCommand.Name)' at '$(Get-Date)'" } } } |