Powershell: SORT-OBJECT offers a misleading error message

Created on 5 Aug 2020  Â·  12Comments  Â·  Source: PowerShell/PowerShell

Steps to reproduce

[PSCUSTOMOBJECT] @{ 0 = 0 } | SORT 0

Expected behavior

0
-
0

Actual behavior

Sort-Object: Cannot convert System.Int32 to one of the following types {System.String, System.Management.Automation.ScriptBlock}.

Environment data

Name                           Value
----                           -----
PSVersion                      7.0.3
PSEdition                      Core
GitCommitId                    7.0.3
OS                             Microsoft Windows 10.0.18363
Platform                       Win32NT
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0…}
PSRemotingProtocolVersion      2.3
SerializationVersion           1.1.0.1
WSManStackVersion              3.0
Issue-Question

Most helpful comment

Certainly misleading, though I think the right solution is not to report an error at all and actually perform to-string conversion (the implied -Property parameter is [object[]]-typed).

All 12 comments

Certainly misleading, though I think the right solution is not to report an error at all and actually perform to-string conversion (the implied -Property parameter is [object[]]-typed).

Why misleading? The message says clearly that PowerShell expects either a string or a scriptblock but not int. Since PowerShell expects _strongly_ one from two types it does not know what would be right conversion. It is a generic code and we can not force to string conversion because there is a high risk to break something in Engine or in custom scripts/codes.

See how works internal List<MshParameter> ProcessParameters(object[] p, TerminatingErrorContext invocationContext).

  • It is misleading, because you obviously _can_ convert a System.32 instance to a System.String instance.

  • It shouldn't be an error to begin with, because the only sensible interpretation of something that isn't already a string or a hashtable / script block (with a calculated-property definition) is to _convert it to a string_.

While it's debatable whether to-string conversion should be applied to just _any_ data type, something that _looks like a number_, such as 0, in _argument mode_ - in the absence of quoting - is inherently ambiguous: is it the number 0 or string '0'?
Here, the only sensible interpretation is as a string.

If in the end there _is_ a technical reason that this cannot be implemented, the error message should be revised to say something like: "if this is a property name that looks like a number, enclose it in quotes".

There's no technical reason I can think of that this couldn't be implemented. Seems like it's most likely just a simple case of doing a type check where it probably should have been a type conversion, i.e., we're probably doing something like:

if Property.BaseObject is IDictionary
  # handle as calculated property
else if Property.BaseObject is ScriptBlock
  # handle as unnamed calculated property expression
else if Property.BaseObject is string
  # treat as property name to sort on

that last branch should not have a type check at all, and instead simply opt for LanguagePrimitives.ConvertTo<string>(Property) (which will correctly handle the string conversion for parameters parsed as numbers as well).

Thanks, @vexx32, that should indeed suffice.

I got myself confused earlier: there isn't a need to change the -Property parameter type, given that the engine implicitly wraps in PSObject those arguments that were parsed as numbers _whose default string representation differs from the one given_, with the PSObject instance containing the _original (string) representation_, which, as you state, LanguagePrimitives.ConvertTo<string>() does utilize.

This ensures that passing a bareword such as 1e2, which becomes a [double] that by default stringifies as 100, is still represented as '1e2 when converted back to a string.

Why misleading? The message says clearly that PowerShell expects either a string or a scriptblock but not int.

The message clearly states that whatever comes over must be convertible to both a string and a script block because it is a problem when the value is not convertible to one of them. A script block is convertible to a string but it is debatable whether a string is convertible to a script block, so for the clarity of exposition I shall assume that it is not. I also do not know of any other type that is convertible to ScriptBlock. That means the winner is… ScriptBlock 🎺🎺 and anything that is not one cannot be given as a parameter. Nice?

that last branch should not have a type check at all, and instead simply opt for LanguagePrimitives.ConvertTo(Property) (which will correctly handle the string conversion for parameters parsed as numbers as well).

It would be a huge breaking change - current code explicitly throws on unexpected type. The code is generic. There can be any allowed types, ex., byte,int,long - why would you convert another type to string in the case?

Also for generic code an error message can be only generic too.

current code explicitly throws on unexpected type. The code is generic.

Code that throws on an unexpected type is definitely not generic.

@iSazonov

why would you convert another type to string in the case?

It isn't another type, conceptually. In the example in the OP, the intent is clearly to pass a _string_, 0.

Yes, you can force interpretation as a string with explicit quoting ('0'), _but you shouldn't have to_, given that in _argument-parsing mode_ quoting strings is _optional_ - just like you're free to use Get-Date -UFormat 0 (to use a contrived example) without having to quote the 0, or just like Write-Host 1e2 and Write-Host '1e2' produce the same output.)

What we're seeing is the confluence of two behaviors:

  • The parameter binder parses a bareword that _looks like_ a number literal as a number rather than a string, but, if it then binds to an effectively _untyped_ parameter (object or PSObject) wraps it in a PSObject that caches the original string representation as specified on the command line, leaving the door open to treat this argument as a string later.

    • However, as an optimization, this wrapping happens _only_ if the original string representation differs from the resulting number's default string representation.

    • Incidentally, this is currently broken for number literals with the d ([decimal]) and n ([bigint]) suffixes, specifically - see #13380

  • Sort-Object doesn't have distinct parameters for the distinct types it accepts as sort criteria and uses object instead. (I guess it has to, given that the parameter is array-valued and must be able to accept a mix of strings and script blocks / hashtables.)

Note that a [string]-typed parameter would indeed accept _any_ type, given that PowerShell happily converts anything to a string.

If you're really concerned that existing code _relies_ on Sort-Object throwing an error for non-string property arguments, we can limit what non-string types are accepted to the primitive number types plus [decimal] and [bigint].

(However, due to the optimization that results in a PSObject wrapper only when truly needed, we can't reliably tell if a given value started out as a _bareword_, so an unambiguously-passed-as-a-number argument would succeed too; e.g.
Sort-Object (0)).


Incidentally: the code in question appears to be shared among cmdlets; for instance, Select-Object has the same problem:

PS> [pscustomobject] @{ 0 = 'zero' } | Select-Object 0
Select-Object: Cannot convert System.Int32 to one of the following types {System.String, System.Management.Automation.ScriptBlock}.

Incidentally: the code in question appears to be shared among cmdlets; for instance,

Yes, it is a generic code. There may be an unpredictable number of cmdlets where this is not the problem but the desired behavior.

As far as I'm aware, this code is used exclusively for property expressions (which would all benefit from fixing the issue). Are there other use cases?

Incidentally: the code in question appears to be shared among cmdlets; for instance,

Yes, it is a generic code. There may be an unpredictable number of cmdlets where this is not the problem but the desired behavior.

No, it is shared code. I think we would all appreciate an example where converting a number to a string leads to a zombie apocalypse. Also, HELP ABOUT_PARSING: In argument mode, each value is treated as an expandable string unless it begins with one of the following special characters.

Was this page helpful?
0 / 5 - 0 ratings