I'm trying to create a source alias or function to the PowerShell . builtin.
Creating an alias to .
does not work :
PS C:\> Set-Alias source .
PS C:\> source $profile.CurrentUserAllHosts
source : The term '.' is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of the name,
or if a path was included, verify that the path is correct and try again.
At line:1 char:1
+ source $profile.CurrentUserAllHosts
+ ~~~~~~
+ CategoryInfo : ObjectNotFound: (.:String) [], CommandNotFoundException
+ FullyQualifiedErrorId : CommandNotFoundException
PS C:\>
I also tried creating a function instead :
PS C:\> Get-Command source | % Definition
param($script)
if( $script ) {
. $script
}
PS C:\> source $profile.CurrentUserAllHosts
And my newly created lastBoot is not recognized :
PS C:\> source $profile.CurrentUserAllHosts
PS C:\> lastBoot
lastBoot : The term 'lastBoot' is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of
the name, or if a path was included, verify that the path is correct and try again.
At line:1 char:1
+ lastBoot
+ ~~~~~~~~
+ CategoryInfo : ObjectNotFound: (lastBoot:String) [], CommandNotFoundException
+ FullyQualifiedErrorId : CommandNotFoundException
PS C:\>
But if I use the .
character, it works :
PS C:\> . $profile.CurrentUserAllHosts
PS C:\> lastBoot
CSName LastBootUpTime
------ --------------
myPC 15/07/2025 12:02:15
PS C:\>
How can I make my source
function work ?
Thanks to Santiago Squarzon for the optimized code!
This version uses [System.Management.Automation.Language.Parser]::ParseFile for AST parsing, eliminating the need for scriptblock execution (. $scriptBlock):
function source {
param($script)
if ($script) {
$tokens = $null
$errors = $null
$ast = [System.Management.Automation.Language.Parser]::ParseFile($script, [ref]$tokens, [ref]$errors)
if ($errors) {
Write-Error "Errors parsing script: $($errors -join ', ')"
return
}
$functions = $ast.FindAll({ $args[0] -is [System.Management.Automation.Language.FunctionDefinitionAst] }, $true)
foreach ($func in $functions) {
$funcName = $func.Name
$globalFuncBody = $func.Body.GetScriptBlock()
Set-Item -Path "Function:\global:$funcName" -Value $globalFuncBody
}
}
}
Parse the Script: Uses Parser.ParseFile to analyze the script file and get the AST without running any code. It also captures tokens and errors for basic validation.
Find Functions: Searches the AST for all function definitions (supports multiple root-level functions).
Promote to Global Scope: Extracts each function's body as a scriptblock and defines it globally using Set-Item.
With your profile:
`source $profile.CurrentUserAllHosts
lastBoot
Output (example):
`CSName LastBootUpTime
------ --------------
myPC 15/07/2025 12:02:15
Test script (testscript.ps1):
function helloWorld { Write-Output "Hello from the test script!" }
Usage:
source .\testscript.ps1
helloWorld # Outputs: Hello from the test script!
& { helloWorld } # Confirms global scope
Get-Command helloWorld # Shows as Function, Source blank (normal for user-defined)
Add the function to your profile:
notepad $profile.CurrentUserAllHosts
Paste the code, save, and reload: source $profile.CurrentUserAllHosts.
This version handles both functions and root-level variables (with RHS evaluation for variables):
using namespace System.Management.Automation.Language
function source {
param(
[Parameter(Mandatory)] $script
)
$errors = $null
$script = Convert-Path $script
$ast = [Parser]::ParseFile($script, [ref] $null, [ref] $errors)
if ($errors) {
Write-Error "Errors parsing script: $($errors -join ', ')"
return
}
# Promote functions to global scope (reliable for profile reloading)
$functions = $ast.FindAll({ $args[0] -is [FunctionDefinitionAst] }, $true)
foreach ($func in $functions) {
$funcName = $func.Name
$globalFuncBody = $func.Body.GetScriptBlock()
Set-Item -Path "Function:\global:$funcName" -Value $globalFuncBody
}
# Promote root-level variables (including implied/compound assignments) to current scope
$assignments = $ast.FindAll({ $args[0] -is [AssignmentStatementAst] }, $false) # Removed column check for simplicity
foreach ($assignment in $assignments) {
try {
$varName = $assignment.Left.VariablePath.UserPath
$operator = $assignment.Operator
$rhsText = $assignment.Right.Extent.Text
if ($operator -eq 'Equals') {
$value = Invoke-Expression $rhsText
} else {
$currentValue = Get-Variable -Name $varName -ErrorAction SilentlyContinue
$value = Invoke-Expression "($currentValue.Value) $operator.Text.Replace('Equals', '') $rhsText"
}
Set-Variable -Name $varName -Value $value
} catch {
Write-Warning "Failed to promote variable: $_"
}
}
# Promote root-level aliases to global scope
$aliasCommands = $ast.FindAll({ $args[0] -is [CommandAst] -and $args[0].GetCommandName() -eq 'Set-Alias' -and $args[0].Extent.StartColumnNumber -eq 1 }, $false)
foreach ($aliasCmd in $aliasCommands) {
try {
$aliasName = $aliasCmd.CommandElements[1].Value
$aliasValue = $aliasCmd.CommandElements[2].Value
Set-Alias -Name $aliasName -Value $aliasValue -Scope Global
} catch {
Write-Warning "Failed to promote alias: $_"
}
}
# Promote root-level custom drives to current scope (extract parameters for direct call, no table print)
$driveCommands = $ast.FindAll({ $args[0] -is [CommandAst] -and $args[0].GetCommandName() -eq 'New-PSDrive' -and $args[0].Extent.StartColumnNumber -eq 1 }, $false)
foreach ($driveCmd in $driveCommands) {
try {
$name = $driveCmd.CommandElements[2].Value # Elements[1] -Name, [2] value
$psProvider = $driveCmd.CommandElements[4].Value # [3] -PSProvider, [4] value
$root = $driveCmd.CommandElements[6].Value # [5] -Root, [6] value
New-PSDrive -Name $name -PSProvider $psProvider -Root $root | Out-Null # Suppress table
} catch {
Write-Warning "Failed to promote drive: $_"
}
}
}
Parse the Script: Uses Parser.ParseFile to get the AST without executing the script, capturing errors for validation.
Extract and Promote Functions: Finds function definitions and defines them globally.
Extract and Promote Variables: Finds root-level assignments (filtered by column number for no indentation), evaluates the RHS to compute the value, and sets the variable globally.
While this approach works well for simple scripts (e.g., profiles defining unconditional functions and variables), it's not a perfect emulation of dot-sourcing. As noted in the comments:
Incompleteness: It only handles functions and root-level variables—aliases (Set-Alias), drives (New-PSDrive), or other definitions (e.g., enums, classes) are ignored. Extend the AST search (e.g., for CommandAst with Set-Alias) if needed.
Static Analysis Limitations: AST parsing is static and doesn't account for runtime flow control (e.g., functions/variables defined inside if, switch, or loops are extracted unconditionally, potentially creating them when they shouldn't exist). This could lead to incorrect behavior in dynamic scripts.
Scope Enforcement: Functions and variables are always promoted to the global scope, even if the source function is called from a non-global scope (e.g., inside another function or script). True dot-sourcing runs in the caller's scope, which this doesn't replicate.
Variable Evaluation Risks: For variables, evaluating the RHS with Invoke-Expression executes code, which could have side effects or security risks (e.g., if RHS calls Remove-Item). Avoid untrusted scripts.
Other Caveats: Only root-level items (no indentation); nested or conditional definitions may not work as expected. Non-function code (e.g., immediate Write-Output) isn't executed, which might be desirable or a drawback depending on the script.