about_Scopes
简短说明
介绍 PowerShell 中作用域的概念,并演示如何设置和更改元素的范围。
长说明
PowerShell 通过限制可以读取和更改的地方,来保护对变量、别名、函数和 PowerShell 驱动器 (PSDrives) 的访问。 PowerShell 使用范围规则来确保不会对其他作用域中的项进行意外更改。
范围规则
启动 PowerShell 时,主机 (pwsh.exe
) 将创建 PowerShell 运行空间。
主机进程可以有多个运行空间。 每个 Runspace 都有自己的会话状态和作用域容器。 无法跨运行空间实例访问会话状态和范围。
以下是作用域的基本规则:
- 作用域可以嵌套。 外部作用域称为父作用域。 所有嵌套的作用域都是该父级的子作用域。
- 除非显式将其设为私有,否则该项在创建的范围和任何子作用域中可见。
- 可以为当前作用域之外的作用域声明变量、别名、函数和 PowerShell 驱动器。
- 除非显式地指定其他作用域,否则项只能在其被创建的作用域中更改。
- 当在运行空间中运行的代码引用某个项时,PowerShell 会搜索范围层次结构,从当前作用域开始,然后继续执行每个父范围。
- 如果未找到该项,则会在当前范围内创建新项。
- 如果找到匹配项,则会从找到的作用域中检索项的值。
- 如果更改该值,则会将该项复制到当前范围,以便更改仅影响当前范围。
- 如果显式创建一个与不同作用域中的项共享其名称的项,则新项可能会隐藏原始项,但不会重写或更改它。
父作用域和子作用域
调用脚本或函数可以创建新的子作用域。 执行调用的作用域是父作用域。 被调用的脚本或函数是子作用域。 调用的函数或脚本可能会调用其他函数,从而创建出子作用域的层次结构,其根作用域是全局作用域。
注意
模块中的函数不会在执行调用作用域的子作用域中运行。 模块具有其自己的会话状态,这些状态链接到模块导入的范围。 所有模块代码都在模块专用的作用域层次结构中运行,后者具有其自己的根作用域。 有关详细信息,请参阅本文的“ 模块 ”部分。
创建子范围时,它包括具有 AllScope 选项的所有别名和变量,以及一些自动变量。 本文稍后将介绍此选项。
除非显式地将项设为私有,否则父作用域中的项可用于子作用域。 在子范围中创建或更改的项不会影响父范围,除非在创建项时显式指定范围。
若要查找特定作用域中的项,请使用 Get-Variable
或 Get-Alias
的作用域参数。
例如,若要获取本地作用域中的所有变量,请键入:
Get-Variable -Scope local
若要获取全局作用域中的所有变量,请键入:
Get-Variable -Scope global
对变量、别名或函数进行引用时,PowerShell 将搜索当前范围。 如果未找到该项,则会搜索父范围。 此搜索一直重复到全局范围。 如果变量在父范围内是私有的,则通过范围链继续搜索。 示例 4 显示了作用域搜索中私有变量的效果。
PowerShell 范围名称
PowerShell 定义某些范围的名称,以便更轻松地访问该范围。 PowerShell 定义以下命名范围:
- 全局:PowerShell 启动时或创建新会话或运行空间时生效的作用域。 PowerShell 启动时存在的变量和函数(例如自动变量和首选项变量)在全局范围内创建。 PowerShell 配置文件中的变量、别名和函数也在全局作用域内创建。 全局范围是运行空间中的根父范围。
- 本地:当前作用域。 本地作用域可以是全局作用域或任何其他作用域。
- 脚本:脚本文件运行时创建的作用域。 脚本中的命令在脚本范围内运行。 对于脚本中的命令,脚本范围是本地范围。
对于支持范围的 cmdlet,范围可以通过描述一个范围的相对位置的数字来引用范围。 范围 0 表示当前(本地)范围,范围 1 是当前范围的父级,范围 2 是当前范围的祖父母。 此模式会一直持续到到达根范围。
作用域修饰符
变量、别名或函数名称可以包括以下任一可选的作用域修饰符:
global:
- 说明名称存在于全局作用域。local:
- 说明名称存在于本地作用域。 当前作用域始终是本地作用域。private:
- 说明名称是私有的,并且仅对当前作用域可见。script:
- 说明名称存在于脚本作用域。 脚本作用域是最近的上级脚本文件的作用域或全局作用域(如果没有最近的上级脚本文件)。using:
- 用于访问在远程会话、后台作业或线程作业中运行时在另一作用域中定义的变量。workflow:
- 说明名称存在在工作流内。 注意:PowerShell v6 及更高版本不支持工作流。<variable-namespace>
- PowerShell PSDrive 提供程序创建的修饰符。 例如:命名空间 说明 Alias:
当前作用域内定义的别名 Env:
当前作用域内定义的环境变量 Function:
当前作用域内定义的函数 Variable:
当前作用域内定义的变量
脚本的默认作用域是脚本作用域。 函数和别名的默认作用域是本地作用域,即使它们是在脚本中定义的。
使用作用域修饰符
若要指定新变量、别名或函数的作用域,请使用作用域修饰符。
变量中作用域修饰符的语法为:
$[<scope-modifier>:]<name> = <value>
函数中作用域修饰符的语法为:
function [<scope-modifier>:]<name> {<function-body>}
以下命令不使用作用域修饰符,在当前或本地作用域中创建变量:
$a = "one"
若要在全局作用域中创建相同的变量,请使用作用域修饰符 global:
:
$global:a = "one"
Get-Variable a | Format-List *
请注意可见性和选项属性值。
Name : a
Description :
Value : one
Visibility : Public
Module :
ModuleName :
Options : None
Attributes : {}
与私有变量进行比较:
$private:pVar = 'Private variable'
Get-Variable pVar | Format-List *
使用 private
作用修饰符会将选项属性设置为 Private
。
Name : pVar
Description :
Value : Private variable
Visibility : Public
Module :
ModuleName :
Options : Private
Attributes : {}
若要在脚本作用域中创建相同的变量,请使用作用域修饰符 script:
:
$script:a = "one"
也可以在函数中使用作用域修饰符。 以下函数定义在全局作用域内创建函数:
function global:Hello {
Write-Host "Hello, World"
}
也可以使用作用域修饰符来引用不同作用域中的变量。
以下命令首先在本地作用域,然后在全局作用域内引用 $test
变量:
$test
$global:test
using:
作用域修饰符
Using 是一个特殊的作用域修饰符,可以识别远程命令中的局部变量。 如果没有修饰符,PowerShell 要求远程命令中的变量在远程会话中定义。
PowerShell 3.0 引入了 using
作用域修饰符。
对于任何在会话外执行的脚本或命令,需要使用 using
作用域修饰符来嵌入来自调用会话作用域的变量值,以便会话外代码可以访问它们。 以下上下文支持 using
作用域修饰符:
- 远程执行的命令,开头的
Invoke-Command
使用 ComputerName、HostName、SSHConnection 或会话参数(远程会话) - 后台作业,从
Start-Job
开始(进程外会话) - 线程作业,以
Start-ThreadJob
或ForEach-Object -Parallel
开始(单独的线程会话)
根据上下文,嵌入的变量值可以是调用方作用域数据的独立副本,也可以是对其的引用。 在远程和进程外会话中,这些变量值始终是独立的副本。
有关详细信息,请参阅 about_Remote_Variables。
引用 $using:
仅扩展到变量的值。 如果要更改调用方作用域中的变量的值,则必须具有对变量本身的引用。 可以通过获取 变量的 PSVariable 实例来创建对变量的引用。 以下示例演示如何创建引用并在线程作业中进行更改。
$Count = 1
$refOfCount = Get-Variable Count
Start-ThreadJob {
($using:refOfCount).Value = 2
} | Receive-Job -Wait -AutoRemoveJob
$Count
2
注意
这不是线程安全的操作。 如果尝试同时更改多个线程的值,可能会导致数据损坏。 应使用线程安全的数据类型或同步基元来保护共享数据。 有关详细信息,请参阅 线程安全集合。
变量值的序列化
远程执行的命令和后台作业在进程外运行。 进程外会话使用基于 XML 的序列化和反序列化,使变量值可以跨进程边界使用。 序列化进程将对象转换为 PSObject,其中包含原始对象属性,但不包含其方法。
对于一组有限类型,反序列化解除对象冻结,回到原始类型。 解除冻结的对象是原始对象实例的副本。 它具有类型属性和方法。 对于简单类型(如 System.Version),副本完全相同。 对于复杂类型,副本不完善。 例如,解除冻结的证书对象不包括私钥。
所有其他类型的实例都是 PSObject 实例。 PSTypeNames 属性包含前缀为 Deserialized 的原始类型名称,例如,Deserialized.System.Data.DataTable
AllScope 选项
变量和别名具有选项属性,该属性的值可以为 AllScope。 具有 AllScope 属性的项将成为任何所创建的子作用域的一部分,尽管它们不会反过来由父作用域继承。
具有 AllScope 属性的项在子作用域中可见,并且它是该作用域的一部分。 对任何作用域中的项的更改都会影响定义变量的所有范围。
管理作用域
多个 cmdlet 具有 Scope 参数,可用于在特定作用域中获取或设置(创建和更改)项。 使用以下命令查找会话中具有作用域参数的所有 cmdlet:
Get-Help * -Parameter scope
若要查找在特定作用域内可见的变量,请使用 Get-Variable
的 Scope
参数。 可见变量包括全局变量、父作用域中的变量和当前作用域中的变量。
例如,以下命令可以获取在本地作用域内可见的变量:
Get-Variable -Scope local
若要在特定作用域中创建变量,请使用作用域修饰符或 Set-Variable
的作用域参数。 以下命令在全局作用域内创建变量:
New-Variable -Scope global -Name a -Value "One"
也可以使用 New-Alias
、Set-Alias
或 Get-Alias
cmdlet 的作用域参数来指定作用域。 以下命令在全局范围内创建别名:
New-Alias -Scope global -Name np -Value Notepad.exe
若要获取特定作用域中的函数,请使用该作用域中的 Get-Item
cmdlet。 Get-Item
cmdlet 不具有作用域参数。
注意
对于使用作用域参数的 cmdlet,也可以按数字引用作用域。 数字描述一个作用域相对另一个作用域的相对位置。 Scope 0 表示当前或本地作用域。 Scope 1 表示直接的父作用域。 Scope 2 表示父作用域的父级,以此类推。 如果创建了多个递归作用域,那么编号作用域非常有用。
对作用域使用点源表示法
脚本和函数都遵循范围规则。 在特定作用域中创建的脚本和函数仅影响该作用域,除非使用 cmdlet 参数或作用域修饰符来更改该作用域。
但是,可以使用点源表示法将脚本或函数的内容添加到当前作用域。 使用点源表示法运行脚本或函数时,它会在当前作用域内运行。 脚本或函数中的任何函数、别名和变量都添加到当前作用域中。
例如,若要从脚本作用域(脚本的默认作用域)中的 C:\Scripts
目录运行 Sample.ps1
脚本,只需在命令行上输入脚本文件的完整路径。
c:\scripts\sample.ps1
脚本文件的文件扩展名必须是 .ps1
才能执行。 路径包含空格的文件必须用引号括起来。 如果尝试执行带引号的路径,PowerShell 将显示带引号的字符串的内容,而不是运行脚本。 使用调用运算符 (&
) 可以执行包含文件名字符串的内容。
使用调用运算符在脚本作用域内运行函数或脚本。 使用调用运算符与按名称运行脚本没有什么不同。
& c:\scripts\sample.ps1
可以在 about_Operators 中详细了解调用运算符。
若要在本地作用域中运行 Sample.ps1
脚本,请在脚本的路径之前输入一个点和一个空格 (.
):
. c:\scripts\sample.ps1
如此,脚本或函数中定义的任何函数、别名和变量都添加到了当前作用域中。
限制无作用域
PowerShell 具有一些类似于作用域的选项和功能,可能与作用域交互。 这些功能可能与作用域或作用域行为混淆。
会话、模块和嵌套提示是独立环境,而不是会话中全局作用域的子作用域。
会话
会话是运行 PowerShell 的环境。 在远程计算机上创建会话时,PowerShell 会与远程计算机建立持久连接。 持久连接允许对多个相关命令使用会话。
由于会话是被包含的环境,因此它有自己的作用域,但会话不是创建其会话的子作用域。 会话从自己的全局作用域开始。 此作用域独立于会话的全局作用域。 可以在会话中创建子作用域。 例如,可以运行脚本在会话中创建子作用域。
模块
可以使用 PowerShell 模块来共享和交付 PowerShell 工具。 模块是可以包含 cmdlet、脚本、函数、变量、别名和其他有用项的单元。 除非显式导出(使用 Export-ModuleMember
或模块清单),否则模块中的项在模块外部无法访问。 因此,可以将模块添加到会话并使用公共项,而无需担心其他项可能会覆盖会话中的 cmdlet、脚本、函数和其他项。
默认情况下,模块将加载到运行空间的根级别(全局)范围内。 导入模块不会更改范围。
在会话中,模块有自己的范围。 假设以下模块 C:\temp\mod1.psm1
:
$a = "Hello"
function foo {
"`$a = $a"
"`$global:a = $global:a"
}
现在,我们将创建一个全局变量 $a
,为其指定一个值,并调用函数 foo。
$a = "Goodbye"
foo
该模块在模块作用域中声明变量 $a
,然后函数 foo 输出这两个作用域中的变量值。
$a = Hello
$global:a = Goodbye
模块创建链接到导入这些容器的范围的并行范围容器。 从导入模块的范围级别开始,模块导出的项可用。 未从模块导出的项仅在模块的范围容器中可用。 模块中的函数可以访问在其中导入的项以及模块作用域容器中的项。
如果从 Module1 中加载 Module2,则 Module2 将加载到 Module1 的范围容器中。 来自 Module2 的任何导出都放置在 Module1 的当前模块范围内。 如果使用 Import-Module -Scope local
,那么导出将放入当前作用域对象而不是顶层。 如果使用的是模块并使用 (或Import-Module -Global
)加载另一个模块Import-Module -Scope global
,该模块及其导出将加载到全局范围,而不是模块的本地范围。
WindowsCompatibility 功能用于将代理模块导入全局会话状态。
嵌套提示
嵌套提示没有自己的作用域。 输入嵌套提示时,嵌套提示是环境的子集。 但是,你仍留在本地作用域内。
脚本具有自己的作用域。 调试脚本时,如果到达了脚本的断点,则进入了脚本作用域。
私有选项
变量和别名具有选项属性,该属性的值可以为 Private
。 可以在创建项的作用域中查看和更改具有 Private
选项的项,但无法在该作用域之外查看或更改它们。
例如,如果创建一个变量,该变量在全局作用域内具有私有选项,那么运行脚本时,脚本中的 Get-Variable
命令不会显示私有变量。 在此实例中使用全局作用域修饰符不会显示私有变量。
可以使用 New-Variable
、Set-Variable
、New-Alias
和 Set-Alias
cmdlet 的选项参数将选项属性的值设置为“私有”。
能见度
变量或别名的可见性属性决定是否可以从创建项的容器外部查看项。 容器可以是模块、脚本或管理单元。 可见性对容器的作用与Private
选项属性的 值对作用域的作用相同。
可见性属性的值可以是 Public
和 Private
。 具有私有可见性的项只能在创建其的容器中查看和更改。 如果添加或导入容器,则无法查看或更改具有私有可见性的项。
由于可见性专为容器设计,因此它在作用域内的作用方式不同。
- 如果在全局作用域内创建具有私有可见性的项,则无法在任何作用域内查看或更改该项。
- 如果要查看或更改具有私有可见性的变量值,PowerShell 将返回一条错误消息。
可以使用 New-Variable
和 Set-Variable
cmdlet 创建具有私有可见性的变量。
示例
示例 1:仅在脚本中更改变量值
以下命令更改脚本中 $ConfirmPreference
变量的值。 此更改不会影响全局作用域。
首先,若要在本地作用域内显示 $ConfirmPreference
变量值,请使用以下命令:
PS> $ConfirmPreference
High
创建包含以下命令的 Scope.ps1 脚本:
$ConfirmPreference = "Low"
"The value of `$ConfirmPreference is $ConfirmPreference."
运行该脚本。 该脚本更改 $ConfirmPreference
变量的值,然后在脚本作用域中报告值。 输出与下文类似:
The value of $ConfirmPreference is Low.
接下来,测试当前作用域内 $ConfirmPreference
变量的当前值。
PS> $ConfirmPreference
High
通过此示例可以看出对脚本作用域中变量值的更改不会影响父作用域内的变量值。
示例 2:查看不同作用域内的变量值
可以使用作用域修饰符查看本地作用域和父作用域中的变量值。
首先,在全局作用域内定义 $test
变量。
$test = "Global"
接下来,创建一个定义 $test
变量的 Sample.ps1
脚本。 在脚本中,使用作用域修饰符引用 $test
变量的全局版本或本地版本。
在 Sample.ps1
中:
$test = "Local"
"The local value of `$test is $test."
"The global value of `$test is $global:test."
运行 Sample.ps1
时,输出应与下文类似:
The local value of $test is Local.
The global value of $test is Global.
脚本完成后,会话中仅定义 $test
的全局值。
PS> $test
Global
示例 3:更改父作用域中的变量值
除非使用私有选项或其他方法保护项,否则可以在父作用域中查看和更改变量值。
首先,在全局作用域内定义 $test
变量。
$test = "Global"
接下来,创建一个定义 $test
变量的 Sample.ps1 脚本。 在脚本中,使用作用域修饰符引用 $test
变量的全局版本或本地版本。
在 Sample.ps1 中:
$global:test = "Local"
"The global value of `$test is $global:test."
脚本完成后,$test
的全局值会改变。
PS> $test
Local
示例 4:创建私有变量
可以使用范围修饰符或创建设置为 Private
Option 属性的变量来私有private:
变量。 只能在创建专用变量的作用域中查看或更改它们。
在此示例中, ScopeExample.ps1
脚本将创建五个函数。 第一个函数调用下一个函数,该函数创建子范围。 其中一个函数具有一个专用变量,该变量只能在创建它的作用域中看到。
PS> Get-Content ScopeExample.ps1
# Start of ScopeExample.ps1
function funcA {
"Setting `$funcAVar1 to 'Value set in funcA'"
$funcAVar1 = "Value set in funcA"
funcB
}
function funcB {
"In funcB before set -> '$funcAVar1'"
$private:funcAVar1 = "Locally overwrite the value - child scopes can't see me!"
"In funcB after set -> '$funcAVar1'"
funcC
}
function funcC {
"In funcC before set -> '$funcAVar1' - should be the value set in funcA"
$funcAVar1 = "Value set in funcC - Child scopes can see this change."
"In funcC after set -> '$funcAVar1'"
funcD
}
function funcD {
"In funcD before set -> '$funcAVar1' - should be the value from funcC."
$funcAVar1 = "Value set in funcD"
"In funcD after set -> '$funcAVar1'"
'-------------------'
ShowScopes
}
function ShowScopes {
$funcAVar1 = "Value set in ShowScopes"
"Scope [0] (local) `$funcAVar1 = '$(Get-Variable funcAVar1 -Scope 0 -ValueOnly)'"
"Scope [1] (parent) `$funcAVar1 = '$(Get-Variable funcAVar1 -Scope 1 -ValueOnly)'"
"Scope [2] (parent) `$funcAVar1 = '$(Get-Variable funcAVar1 -Scope 2 -ValueOnly)'"
"Scope [3] (parent) `$funcAVar1 = '$(Get-Variable funcAVar1 -Scope 3 -ValueOnly)'"
"Scope [4] (parent) `$funcAVar1 = '$(Get-Variable funcAVar1 -Scope 4 -ValueOnly)'"
}
funcA
# End of ScopeExample.ps1
PS> .\ScopeExample.ps1
输出显示每个作用域中变量的值。 可以看到,专用变量仅在创建它的作用域中 funcB
可见。
Setting $funcAVar1 to 'Value set in funcA'
In funcB before set -> 'Value set in funcA'
In funcB after set -> 'Locally overwrite the value - child scopes can't see me!'
In funcC before set -> 'Value set in funcA' - should be the value set in funcA
In funcC after set -> 'Value set in funcC - Child scopes can see this change.'
In funcD before set -> 'Value set in funcC - Child scopes can see this change.' - should be the value from funcC.
In funcD after set -> 'Value set in funcD'
-------------------
Scope [0] (local) $funcAVar1 = 'Value set in ShowScopes'
Scope [1] (parent) $funcAVar1 = 'Value set in funcD'
Scope [2] (parent) $funcAVar1 = 'Value set in funcC - Child scopes can see this change.'
Scope [3] (parent) $funcAVar1 = 'Locally overwrite the value - child scopes can't see me!'
Scope [4] (parent) $funcAVar1 = 'Value set in funcA'
如输出 ShowScopes
所示,可以使用和指定范围编号从其他范围 Get-Variable
访问变量。
示例 5:在远程命令中使用局部变量
对于在本地会话中创建的远程命令变量,请使用 using
作用域修饰符。 PowerShell 会假定远程命令中的变量是在远程会话中创建的。
语法为:
$using:<VariableName>
例如,以下命令在本地会话中创建 $Cred
变量,然后在远程命令中使用 $Cred
变量:
$Cred = Get-Credential
Invoke-Command $s {Remove-Item .\Test*.ps1 -Credential $using:Cred}
PowerShell 3.0 中引入了 using
作用域修饰符。