Search code examples
.netpowershellcontinuous-integrationcommand-line-interfaceescaping

Passing multiline parameter to 'dotnet pack' command in PoweShell


I'd like to change release notes during dotnet pack command. Normally this is straightforward:

$releaseNotes = "Something"
dotnet pack --configuration Release -p:PackageReleaseNotes=$releaseNotes

But then $releaseNotes variable contains some special characters, multiple lines and otherwise "illegal" symbols i.e.:

# Release 2.8.1 - Integrate with Github Actions

- Add Github Actions for build. Remove integration with AppVeyor
- Add architecture tests to check if settings can be properly handled
- Add debuging option for code gen

# Improve TupleHelper
User is no longer required to call for TupleHelper.ParseNext method. It's code was incorporated into ParseElement method. It's now enough to call:
```csharp
var _helper = new TupleHelper(',', '∅', '\\', '(', ')');

var input = "(3.14, Ala ma kota)";
var enumerator = _helper.ParseStart(input, 2, "DoubleAndString");

var key = _helper.ParseElement(ref enumerator, TextTransformer.Default.GetTransformer<double>());
var value = _helper.ParseElement(ref enumerator, TextTransformer.Default.GetTransformer<string>(), 2, "DoubleAndString");

_helper.ParseEnd(ref enumerator, 2, "DoubleAndString");
```
All generated calls were updated accordingly

then things go south. How can I force PowerShell to pass proper variables to -p:PackageReleaseNotes parameter ?

I tried dotnet pack --configuration Release -p:PackageReleaseNotes=$releaseNotes

and various other forms of command calling including starting command with "&"

The problematic content can be downloaded from (element body): Github API call


Solution

  • tl;dr

    • Call via cmd /c with an expandable (interpolating) string ("..."), which gives you full control over the quoting in the arguments ultimately placed on the target process command line.

      • Specifically, make sure that only the property value of the -p is enclosed in "...".
    • Additionally, embedded " chars. in your variable value must be escaped as \"

      • -replace '(?<!\\)(\\*)"', '\$1$1"' in the code below does that robustly; that is, it doubles any preexisting \ characters that immediately precede an embedded "
    # NOTE:
    #   In v7.3+, make sure that $PSNativeCommandArgumentPassing is at 
    #   its default, 'Windows' (or set to 'Legacy')
    cmd /c @"
    dotnet pack --configuration Release -p:PackageReleaseNotes="$($releaseNotes -replace '(?<!\\)(\\*)"', '\$1$1"')"
    "@
    

    Note: To simplify embedded quoting, an expandable here-string (@"<newline>...<newline>"@) is used.


    Background information:

    There are two potential problems:

    • One problem - ultimately a moot point in the case at hand - is that in Windows PowerShell and PowerShell (Core) 7+ up to v7.2.x you must manually \-escape embedded " characters in arguments passed to external programs.

      • This long-standing problem is fixed in v7.3+, but with selective exceptions on Windows - see this answer for details.

      • These exceptions and the fact that the old, broken way of argument-passing applies to them are highly problematic; another problematic aspect is that the list of exceptions may grow over time, sowing confusion over which programs are affected.

        • Case in point: GitHub issue #18660 asks that msbuild.exe be placed on the exception list, due to its specific partial-quoting requirements for property-defining arguments; in fact, it is msbuild.exe that dotnet pack passes its -p: (/p:) arguments through to, so dotnet.exe itself may be another candidate - illustrating the risk of an eternal game of catch-up.
    • The other problem is likely that dotnet and /or msbuild.exe require very specific, partial quoting on the process command line (as also discussed in the linked GitHub issue).

      • PowerShell, when it - of necessity - re-quotes arguments when it builds the process command line behind the scenes, does not preserve partial quoting as originally specified; e.g. -p:Foo='bar baz' turns into "-p:Foo=bar baz", i.e. it is double-quoted as a whole (and if the argument didn't contain spaces, it wouldn't use quoting at all).

      • The solution at the top avoids this problem by calling via cmd /c, which offers direct control over quoting.