将null传递给set-variable为什么会导致不同的结果?

huangapple go评论58阅读模式
英文:

Why is piping null to set-variable causing different outcomes?

问题

在使用 set-variable 时,当将 null 值传递给它时,我看到了不同的行为。

在第一个设置中,我们创建了一个变量,并用 null 覆盖它,目前为止一切都好。

# 设置 1:传递 null 值
$test1 = "test1" 
$null | set-variable test1
Write-Host "It is: $test1" # "It is: "

然而,在第二个设置中,据我所知,我们也是用 null 覆盖它,但变量没有改变。

# 设置 2:通过减少列表来传递 null 值
$testList = New-Object -TypeName 'System.Collections.ArrayList'
$testList.Add("test")

$test2 = "test2" 
$testList | Select-Object -SkipLast 1 | set-variable test2
Write-Host "It is: $test2" # "It is: test2"

为什么第一个设置会覆盖变量,而第二个设置不会呢?

英文:

When using set-variable I am seeing different behaviors when piping a null value to it.

In the first setup, we create a variable, and overwriting it with null, so far so good.

# Setup 1: Passing a null value
$test1 = "test1" 
$null | set-variable test1
Write-Host "It is: $test1" # "It is: "

# $null.gettype()
# You cannot call a method on a null-valued expression.

However in the second setup, to my believe, we are also overwriting it with null, however the variable is not changed.

# Setup 2: Passing a null value by deducting a list
$testList = New-Object -TypeName 'System.Collections.ArrayList'
$testList.Add("test")

$test2 = "test2" 
$testList | Select-Object -SkipLast 1 | set-variable test2
Write-Host "It is: $test2" # "It is: test2"

# $($testList | Select-Object -SkipLast 1).gettype()
# You cannot call a method on a null-valued expression.

Why is the first setup overwriting the variable and the second setup is not?

答案1

得分: 3

这基本上是关于AutomationNull$null之间的区别。请参阅关于$null的一切,这是一篇深度文章。

$test2 = 'test2'
& { } | Set-Variable test2 # AutomationNull,不会执行Process块!
$test2 # 输出: test2
$null | Set-Variable test2 # $null不是AutomationNull,它会被传递
$test2 # 输出: $null

您可以使用此示例更好地理解发生了什么。正如您可能已经看到的,当通过AutomationNull传递时,我们的函数的process从未被调用。

function Set-Var {
    param(
        [Parameter(ValueFromPipeline)]
        [object] $Value,

        [Parameter(Mandatory, Position = 0)]
        [string] $Name
    )

    process {
        $PSCmdlet.WriteVerbose("Setting value [$Value] to PSVariable [$Name]")
        $psvar = $PSCmdlet.SessionState.PSVariable.Get($Name)
        $psvar.Value = $Value
        $PSCmdlet.SessionState.PSVariable.Set($psvar)
    }
}

$test = 'hello'
$null | Set-Var test -Verbose
# VERBOSE: Setting value [] to PSVariable [test]
$test # 输出: $null
'foo' | Set-Var test -Verbose
# VERBOSE: Setting value [foo] to PSVariable [test]
$test # 输出: foo
& { } | Set-Var test -Verbose
# 没有详细输出,意味着从未调用Process块!
$test # 输出: foo
英文:

This is pretty much the difference between AutomationNull and $null. See Everything you wanted to know about $null, great in depth article.

$test2 = 'test2'
& { } | Set-Variable test2 # AutomationNull, Process block does not run!
$test2 # Outputs: test2
$null | Set-Variable test2 # $null is not AutomationNull, it does get piped
$test2 # Outputs: $null

You can use this example to get a better understanding of what is going on. As you may see, the process block of our function is never invoked when AutomationNull is piped through.

function Set-Var {
    param(
        [Parameter(ValueFromPipeline)]
        [object] $Value,

        [Parameter(Mandatory, Position = 0)]
        [string] $Name
    )

    process {
        $PSCmdlet.WriteVerbose("Setting value [$Value] to PSVariable [$Name]")
        $psvar = $PSCmdlet.SessionState.PSVariable.Get($Name)
        $psvar.Value = $Value
        $PSCmdlet.SessionState.PSVariable.Set($psvar)
    }
}

$test = 'hello'
$null | Set-Var test -Verbose
# VERBOSE: Setting value [] to PSVariable [test]
$test # Outputs: $null
'foo' | Set-Var test -Verbose
# VERBOSE: Setting value [foo] to PSVariable [test]
$test # Outputs: foo
& { } | Set-Var test -Verbose
# No verbose output, meaning the Process Block was never invoked!
$test # Outputs: foo

答案2

得分: 2

让我为Santiago Squarzon的有用回答添加一些背景信息

实际上,PowerShell有两种类型的null值

  • 标量null,可以说是类似于C#和其他语言中的null,例如,它是自动**$null**变量的值。

    • 可以将其视为缺失对象的占位符
    • $null会原样传递到管道中
    • 有趣的是,将$null用作foreach语句的输入时,情况并非如此:foreach ($val in $null) { 'here!' }不会产生任何输出,这意味着循环从未进入。
    • 不幸的是,尝试访问不存在的变量的时候,会评估为$null,但在PowerShell中,除了在_测试_中使用(例如$null -eq $value之类的)之外,很少会明确使用$null
  • 可枚举null,可以说是PowerShell特有的概念:

    • 可以将其视为枚举不到任何内容的可枚举

    • 不幸的是,截至本文编写时,这个特殊值没有官方名称,尽管通常称为**“Automation null”**,有时称为“empty null”。

    • 对于它,没有自动变量,即没有类似于标量null的自动$null变量。

    • 鉴于可枚举null在技术上是产生_没有输出_的命令的**“返回值”**,获取它的最简单方法是执行$nullEnum = & {},即执行一个空脚本块。具体来说,可枚举null是[System.Management.Automation.Internal.AutomationNull]::Value单例(文档链接没有提供有意义的信息)。

    • 在**管道**中(例如,$value | Write-Output)...

      • ... 可枚举null表现得像一个没有元素的集合,这意味着,鉴于集合在管道中被_逐个元素地发送_(一个接一个地发送它们的元素),没有数据_被发送到管道中,这通常是_无操作:后续管道段中的命令不接收输入以进行操作

      • 请注意,自动枚举逻辑也适用于switch语句和比较运算符(它们作为具有可枚举LHS值的_过滤器_进行操作);将可枚举null作为switch的输入有效地_跳过_语句(例如switch (& {}) { default { 'never get here' } });可枚举null作为比较操作的LHS值是否被视为可枚举取决于具体的运算符;-match将其视为可枚举((& {}) -match '' -> 空数组),-eq则不会((& {}) -eq '' -> $false)。

    • 在**表达式**中(例如,$null -eq $value)...

      • ... 可枚举null表现得像$null
      • 此外,当将可枚举null 作为参数(参数值)传递给命令时,不可避免地会进行转换为$null - 请参阅GitHub问题#9150

上述内容解释了$null | Set-Variable test1(通过管道接收到的$null值设置了变量test1)和& {} | Set-Variable test2之间的区别(变量test2从未创建或更新,因为Set-Variable未接收到输入;您对1元素输入集合的Select-Object -SkipLast 1调用产生了_没有输出_,因此发出了可枚举null)。

另请参阅:

  • 这个答案还涉及到$null[System.Management.Automation.Null]处理的_历史_方面,涵盖了从v2到v3+的过渡中发生的行为更改。

  • GitHub问题#9150上的此评论总结了如何以一致的方式处理null二分法,如果不考虑向后兼容性


鉴于基本行为差异,很重要:

  • 正确地 记录 这两种null类型,以及为可枚举null 指定一个_官方名称_

  • 使其容易 以编程方式区分 这两种类型。

截至本文编写时(PowerShell 7.3.6),这两个要求都没有得到满足。

检测可枚举null目前令人费解且复杂:

$value = & {} # 获取可枚举null。

# 如果没有

<details>
<summary>英文:</summary>

&lt;!-- language-all: sh --&gt;

Let me add some **background information** to [Santiago Squarzon&#39;s helpful answer](https://stackoverflow.com/a/76631170/45375):

Indeed, PowerShell has **two types of null values**:

  * The **_scalar_ null**, so to speak, analogous to `null` in C# and other languages, for instance, which is the value of the [automatic **`$null`** variable](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_Automatic_Variables#null).
     * You can think of it as **placeholder for a _missing object_**.
     * **`$null` is sent through the pipeline as-is**.
     * Curiously, the same is _not_ true for using `$null` as the input to a [`foreach`](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_Foreach) _statement_: `foreach ($val in $null) { &#39;here!&#39; }` produces _no_ output, implying that the loop is never entered.
     * `$null` is what trying to access *nonexistent variables* evaluates to, unfortunately,&lt;sup&gt;[1]&lt;/sup&gt; but its explicit use - other than in _tests_ such as `$null -eq $value` - is rare in PowerShell.


  * The **_enumerable_ null,** so to speak, which is a PowerShell-specific concept:

     * You can think of it as an **enumerable that _enumerates nothing_**.

    * Sadly, this special value has no official name as of this writing, thought it is often called **&quot;Automation null&quot;** and sometimes &quot;empty null&quot;.  
Also, there is ***no* automatic variable for it**, i.e. there is _no_ analog to the automatic `$null` variable for the _scalar_ null.

    * Given that the enumerable null is **technically the &quot;return value&quot; of commands that produce _no output_**, the simplest way to obtain it is to execute `$nullEnum = &amp; {}`, i.e. to execute an empty script block. Specifically, the enumerable null is the [`[System.Management.Automation.Internal.AutomationNull]::Value`](https://learn.microsoft.com/en-US/dotnet/api/System.Management.Automation.Internal.AutomationNull.Value) singleton (the documentation link provides no meaningful information).

    * In a **_pipeline_** (e.g., `$value | Write-Output`)...
       * ... the enumerable null **behaves like a _collection with no elements_**, which means that, given that collections are _enumerated_ in the pipeline (have their elements sent _one by one_), _no data_ is sent through the pipeline, which is (typically) a _no-op_: the **commands in subsequent pipeline segments receive *no input* to operate on**.

       * Note that the automatic enumeration logic also applies to the [`switch`](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_Switch) statement and to the LHS of comparison operators (which act as _filters_ with enumerable LHS values); providing the enumerable null as input to `switch` effectively _skips_ the statement (e.g. `switch (&amp; {}) { default { &#39;never get here&#39; } }`); whether the enumerable null is treated as an enumerable as the LHS of a comparison operation depends on the specific operator; `-match` treats it as an enumerable (`(&amp; {}) -match &#39;&#39;` -&gt; empty array), `-eq` does not (`(&amp; {}) -eq &#39;&#39;` -&gt; `$false`)

    * In an **_expression_** (e.g, `$null -eq $value`)...
       * ... the enumerable null **behaves like `$null`**
       * Additionally, when **passing the enumerable null _as an argument_** (parameter value) to a command, **_conversion to `$null`_ invariably happens** - see [GitHub issue #9150](https://github.com/PowerShell/PowerShell/issues/9150)

The above explains the difference between `$null | Set-Variable test1` (variable `test` is set to the `$null` value received via the pipeline) and `&amp; {} | Set-Variable test2` (variable `test2` is never created or updated, because `Set-Variable` receives no input; your `Select-Object -SkipLast 1` call on the 1-element input collection produced _no output_, and therefore emitted the enumerable null).

See also:

* [This answer](https://stackoverflow.com/a/41568525/45375) also touches on _historic_ aspects of `$null` vs. `[System.Management.Automation.Null]` handling, covering the behavioral changes that happened in the transition from v2 to v3+.

* [This comment on GitHub issue #9150](https://github.com/PowerShell/PowerShell/issues/9150#issuecomment-473743805) summarizes how the null dichotomy _could_ be handled in a consistent manner, _if backward compatibility weren&#39;t a concern_.


---

**Given the fundamental behavioral differences**, it is important:

* to **properly _document_** these two null types, as well as **give the enumerable null an _official name_**.

* to **make it easy to *programmatically distinguish*** the two types.

As of this writing (PowerShell 7.3.6), neither requirement is met.

**Detecting the enumerable null** is currently cumbersome and obscure:

$value = & {} # Obtain the enumerable null.

Without the -and $value.psobject part, you couldn't distinguish

$null from the enumerable null.

$isNullEnumerable =
$null -eq $value -and $value.psobject


While `$null -eq $value` returns `$true` for _both_ a true `$null` and the enumerable null, `$value.psobject` only returns a value for the enumerable null (which, when coerced to a Boolean, evaluates to `$true`). The reason is that, unlike `$null`, the enumerable null is technically _an object_ and therefore returns a value for the [intrinsic `psobject` property](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_Intrinsic_Members#object-views).
As a result of the discussion in [GitHub issue #13465](https://github.com/PowerShell/PowerShell/issues/13465), **the following improvement has been *green-lit*, but is *yet to be implemented***:

NOT YET IMPLEMENTED as of PowerShell 7.3.6

$isNullEnumerable =
$value -is [System.Management.Automation.Null]


That is, you&#39;ll be able to use `-is`, the [type(-inheritance) / interface test operator](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_Type_Operators) with the yet-to-be-introduced `[System.Management.Automation.Null]` type, which will supersede the &quot;pubternal&quot; `[System.Management.Automation.Internal.AutomationNull]` type.&lt;sup&gt;[2]&lt;/sup&gt;
Unfortunately, also introducing a _type accelerator_ - which would simplify the test to `$value -is [AutomationNull]` - was decided _against_.
---
&lt;sup&gt;[1] This default value is unfortunate, because it means that `$noSuchVariable | ...` _sends `$null`_ through the pipeline. If the default were the &quot;enumerable null&quot;  (&quot;Automation null&quot;, `[System.Management.Automation.Null]::Value`) instead, no data would be sent, the way the `foreach` _statement_ already - but surprisingly - handles `$null` (too). With the enumerable null as the default, there would be no need for the asymmetry between pipeline and `foreach` behavior, and `$null` could _consistently_ be preserved as such.&lt;/sup&gt;
&lt;sup&gt;[2] This change involves more than just a new _type name_: the new type&#39;s singleton (`[System.Management.Automation.Null]::Value`), i.e. the actual enumerable null, will then be *of that same type*, whereas the current singleton (`[System.Management.Automation.Internal.AutomationNull]::Value`) is of a _different_ type, namely just `[psobject]` - see [this GitHub comment](https://github.com/PowerShell/PowerShell/issues/9997#issuecomment-580743572) for details.&lt;/sup&gt;
</details>

huangapple
  • 本文由 发表于 2023年7月7日 00:35:59
  • 转载请务必保留本文链接:https://go.coder-hub.com/76630900.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定