2

Some sample code to illustrate what I'm talking about. I've got a utility class with a single method. That method takes one argument: a System.IO.FileInfo object. From the PowerShell side I can pass in a System.String object and everything "just works". I'm curious as a develop getting more into PowerShell this year I'm curious:

  1. What feature of PowerShell allows this to happen?
  2. Is this open to extension / use by any developer in PowerShell 7.x? (non-internal)

C#:

using System.IO;
using System.Linq;

namespace TestProject
{
    public class Utility
    {
        public string GetFirstLine(FileInfo fileInfo)
        {
            string firstLine = File.ReadLines(fileInfo.FullName).First();
            return firstLine;
        }
    }
}

PowerShell:

Add-Type -Path "C:\assemblies\TestProject.dll"
$util = [TestProject.Utility]::New()
$util.GetFirstLine("C:\temp\random-log.txt")

Note: I realize this is trivial example. Code is meant for quickly illustrating the capability in PowerShell I am interested in.

BuddyJoe
  • 69,735
  • 114
  • 291
  • 466
  • 1
    PowerShell casts automatically if possible; it is as if you wrote `$util.GetFirstLine(([IO.FileInfo] "C:\temp\random-log.txt"))`. – Bill_Stewart Feb 02 '22 at 16:54
  • By feature you mean the ability of type conversion whenever possible? – Santiago Squarzon Feb 02 '22 at 16:55
  • 2
    It's just implicit conversion - PowerShell tries to resolve the correct method signature, fails to find an exact match, and then attempts to convert the string value to the target type. If a type converter or type adapter is registered for the target type, it's used, otherwise PowerShell resolves a constructor for the target type that takes 1 argument of the source type, and uses the result of instantiating such an object (eg. `GetFirstLine("C:\temp\random-log.txt")` is evaluated as `GetFirstLine([System.IO.FileInfo]::new("C:\temp\random-log.txt"))` at runtime) – Mathias R. Jessen Feb 02 '22 at 16:56
  • Thanks @MathiasR.Jessen that was the level I was needed. Led me to the right type of language to locate the following. https://learn.microsoft.com/en-us/powershell/scripting/developer/ets/typeconverters?view=powershell-7.2 – BuddyJoe Feb 02 '22 at 17:01
  • @MathiasR.Jessen You should post your explanation as an answer. – BuddyJoe Feb 02 '22 at 17:02
  • 1
    @BuddyJoe I'm on my phone rn, I'll post an answer as soon as I get back to a full keyboard :) – Mathias R. Jessen Feb 02 '22 at 17:06
  • 1
    Good to know that there's official documentation, @BuddyJoe - though the linked topic is quite technical. A perhaps more easily digestible summary of PowerShell's automatic type conversions / casts can be found in [this answer](https://stackoverflow.com/a/63982452/45375). – mklement0 Feb 02 '22 at 17:41

1 Answers1

4

Although PowerShell is more or less a "real .NET language", it does extend the common type system with an array of behaviors that sometimes conflict with what you know about .NET's type system behavior from languages like C# or VB.NET.

This additional type system layer bolted on top of the CLR is aptly named the Extended Type System (ETS), and it's directly responsible for making argument conversion "just work" the way you've observed.


When the PowerShell runtime reaches a method invocation statement like $util.GetFirstLine("C:\temp\random-log.txt"), it has to pick an appropriate overload, just like the C# compiler does.

The first step is to identity overloads for which the number of arguments passed can cover all mandatory parameters of the given overload signature. In your case, only 1 such signature can be resolved at this step, so PowerShell now has two pieces of the puzzle:

  • A method stub with the signature string GetFirstLine(FileInfo fileInfo)
  • An argument value of type [string]

At this point, the C# compiler would give up and report CS1503: Argument 1: cannot convert from 'string' to 'System.IO.FileInfo'.

PowerShell, on the other hand, is designed to be helpful in the hands of system administrators and operators - people who might not put too much thought into data structure design, but who are chiefly concerned with string representations of data (after all, this is what bash, cmd, etc. taught them to use).

To meaningfully convert from a string (or any other non-compliant argument type) at just the right time, ETS comes with a number of facilities and hooks for implicit type conversion that the runtime then attempts, one by one, until it either finds a valid conversion mechanism, or fails (at which point the method invocation fails too):

  • Built-in converters

    • These take priority to handle the most common edge cases, like null-conversions, "truthy/falsy"-to-real-[bool] conversions, stringification of scalars, etc. - without these, PowerShell would be a pain in the ass to work with interactively.
    • Example:
      • These flow control statements behave exactly like you'd expect thanks to built-in conversions: if($null){ <# unreachable #> }, do{ Stop-Process chrome }while(Get-Process chome)
  • Custom type converters

    • These are concrete type converter implementations registered to one or more target types - this can be useful for modifying the binding behavior of complex types you've brought with you into the runtime.
    • Example:
      • The RSAT ActiveDirectory module uses type adapters to interchange between the different data types modeling specific directory object classes - allowing you to seamlessly pipe output from Get-ADComputer (very specific output type) to Get-ADObject (generalized output type) and vice versa.
  • Parse converters

    • If no appropriate built-in or custom type converter can be found, PowerShell will attempt to resolve a Parse() method with the appropriate return type on the target type.
    • Example:
      • The cast operation [timespan]'1.02:15:25' can succeed this way.
  • Constructor-based conversions

    • If none of the above works, PowerShell will attempt to resolve a constructor that can be invoked with a single parameter argument of the source type given.
    • This is what happens in your case - PowerShell effectively excutes $util.GetFirstLine([System.IO.FileInfo]::new("C:\temp\random-log.txt")) for you
  • CTS conversions

    • Finally, if all of ETS' conversation attempts fail, it falls back to resolving implicit (and eventually explicit) conversions defined by the types themselves.
    • Example:
      • This conversion succeeds because the [DateTimeOffset] type has an implicit conversion operator for [DateTime]: (Get-Date) -as [DateTimeOffset]

The concrete type converters mentioned above will automatically be respected by ETS if they've either been included in a type data file (see about_Types.ps1xml), or if the target type is public and the source definition was decorated with a [TypeConverter()] attribute.

Additionally, you can register new type conversion primitives at runtime with the Update-TypeData cmdlet.

Of course the madness doesn't stop there, there are also additional facilities specifically for converting/transform command arguments, but that's beyond the scope of this question :)

Mathias R. Jessen
  • 157,619
  • 12
  • 148
  • 206