PowerShell: Parameter invalid when it contains a colon. Works prior to 7.3.0.

Prerequisites

Steps to reproduce

Enter:

$test = "&sqlcmd -v MDF_FILEPATH=```"C:\Databases\junk.mdf```""
invoke-expression $test

Expected behavior

sqlcmd is launched and you can enter:

select '$(MDF_FILEPATH)'
go

And be presented with:

----------------------------
C:\Databases\junk.mdf

Actual behavior

sqlcmd is no longer able to parse parameter value and you get the following after the invoke-expression:

Sqlcmd: 'MDF_FILEPATH=\"C:\Databases\junk.mdf\""': Invalid argument. Enter '-?' for help.

Error details

n/a

Environment data

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

Visuals

PowerShell_7 3

PowerShell_prior_to_7 3

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Comments: 28 (20 by maintainers)

Most upvoted comments

The original script uses it because the command is dynamically built; there are a bunch of different parameters that might get used.

You can still do that without Invoke-Expression

$sqlCmdArgs = @(
    'arg that is always set'
    if ($someCondition) { 'args for sqlcml' }
    'arg that is always set'
)
sqlcmd @sqlCmdArgs 

You can also splat multiple arrays or use args inline with splats so you aren’t restricted to just this. Using Invoke-Expression is dangerous if the data comes from a user source and adds yet another layer you need to deal with when it comes to strings. It is best to try and avoid it in as many cases as you can.

Is there something I’m missing regarding passing a colon in the value? Note: This is the same behavior in the cmd shell. I have to wrap the value in quotes to get it to work there too. I’m sure the problem lies in how sqlcmd parses the value containing a colon, which forces the user to wrap the value in quotes.

You need to quote the argument to avoid pwsh parameter binder from reading it differently

sqlcmd -v 'MDF_FILEPATH=c:\test\junk.mdf'

Hi,

we are facing the same problem with msbuild and property values containing a semicolon or comma. We have to support powershell 5.1 up to pwsh 7.3 and are using escaped quotes which are breaking in pwsh 7.3.

We dont want to set $PSNativeCommandArgumentPassing script-wide because it may cause incompatibilities when including 3rd party modules/scripts which dont expect this.

So we decided to go with local changes in scripts with the help of the following function:

function Use-LegacyNativeCommandArgumentPassing {
  [CmdletBinding()]
  param (
    [Parameter(Mandatory=$true)]
    [scriptblock]
    $Script
  )

  if (!(test-path variable:PSNativeCommandArgumentPassing)) {
    & $Script
    return
  }

  $oldPSNativeCommandArgumentPassing = $PSNativeCommandArgumentPassing

  try {
    $PSNativeCommandArgumentPassing = 'Legacy'
    & $Script
  }
  finally {
    $PSNativeCommandArgumentPassing = $oldPSNativeCommandArgumentPassing
  }
}

Which we are using this way:

Use-LegacyNativeCommandArgumentPassing {
  & 'msbuild.exe' @('test.target', "/p:Category=`"CI,Nightly`"")
}

Hope this helps someone else.

@mklement0 testexe -echoargs is used in tests, and the tests only expect the individual arguments, not extra texts for the raw command line. The -echoraw flag is only for interactive use in cases like this one.

@jborean93 Thank you for sharing the code! I submitted https://github.com/PowerShell/PowerShell/pull/18591 to update testext with a -echoraw switch, to show the raw command line and the individual args. I think the existing testexe -echoraw is sufficient on Unix platforms, but let me know if that’s not the case.

image

@daxian-dbw if it helps this is what I use to test out argv stuff on Windows

Add-Type -OutputType ConsoleApplication -OutputAssembly print_argv.exe -TypeDefinition @'
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;

namespace PrintArgv
{
    class Program
    {
        [DllImport("Kernel32.dll")]
        public static extern IntPtr GetCommandLineW();

        static void Main(string[] args)
        {
            IntPtr cmdLinePtr = GetCommandLineW();
            string cmdLine = Marshal.PtrToStringUni(cmdLinePtr);

            Console.WriteLine(cmdLine);
            for (int i = 0; i < args.Length; i++)
            {
                Console.WriteLine("[{0}] {1}", i, args[i]);
            }
        }
    }
}
'@

This unfortunately needs to run in WinPS but it can be run anywhere. It will output the raw command line value as well as the arguments as parsed by dotnet

image

Note I haven’t updated to 7.3.0 yet

For Linux I’ve also used the following before

#include<stdio.h>

int main(int argc, char *argv[])
{
    int i;
    for(i = 1;i < argc;i++)
    {
        printf("[%d] %s\n", i, argv[i]);
    }
    return 0;
}

You can compile it with gcc print_argv.c -o print_argv

image

It is a bit more confusing there because Linux doesn’t have a command line string but uses char *argv[].

@daxian-dbw and @mklement0 This was an eye opening conversation. Thanks a ton for weighing in.

Indeed, a .NET (Core) application (as opposed to a .NET Framework application) does not report the true raw command line: see https://github.com/dotnet/runtime/issues/11305#issuecomment-674554010

@daxian-dbw I agree with @mklement0, the issue is unrelated to Invoke-Expression as I can recreate the problem without using it, and using the legacy workaround does allow the command to complete as before.

Once we have the list updated and backported to 7.3.x, the idea is that you can keep $PSNativeCommandArgumentPassing = 'Windows' (which is the default) and commands in the list automatically use legacy when needed for compat. So you shouldn’t have to make any changes. However, if you uset legacy at the top of your script, then the changes we’re making should still not break you and you can keep it like that as well.