Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
The definitive way to use PowerShell from an msbuild script
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<!-- #1 Place this line at the top of any msbuild script (ie, csproj, etc) -->
<PropertyGroup><PowerShell># 2>nul || type %~df0|find /v "setlocal"|find /v "errorlevel"|powershell.exe -noninteractive -&amp; exit %errorlevel% || #</PowerShell></PropertyGroup>
<!-- #2 in any target you want to run a script -->
<Target Name="default" >
<PropertyGroup> <!-- #3 prefix your powershell script with the $(PowerShell) variable, then code as normal! -->
<myscript>$(PowerShell)
#
# powershell script can do whatever you need.
#
dir ".\*.cs" -recurse |% {
write-host Examining file named: $_.FullName
# do other stuff here...
}
$answer = 2+5
write-host Answer is $answer !
</myscript>
</PropertyGroup>
<!-- #4 and execute the script like this -->
<Exec Command="$(myscript)" EchoOff="true" />
<!--
Notes:
======
- You can still use the standard Exec Task features! (see: https://msdn.microsoft.com/en-us/library/x8zx72cd.aspx)
- if your powershell script needs to use < > or & characters, just place the contents in a CDATA wrapper:
<script2><![CDATA[ $(PowerShell)
# your powershell code goes here!
write-host "<<Hi mom!>>"
]]></script2>
- if you want return items to the msbuild script you can get them:
<script3>$(PowerShell)
# your powershell code goes here!
(dir "*.cs" -recurse).FullName
</script3>
<Exec Command="$(script3)" EchoOff="true" ConsoleToMSBuild="true">
<Output TaskParameter="ConsoleOutput" PropertyName="items" />
</Exec>
<Touch Files="$(items)" /> <- see! then you can use those items with another msbuild Task
-->
</Target>
</Project>
@BernMcCarty

This comment has been minimized.

Copy link

@BernMcCarty BernMcCarty commented Aug 6, 2018

This is amazing. Can you explain the value of the PowerShell property? What does the leading # mean to CMD? And how might one get something like this to work using dotnet core on a non-windows platform (to invoke PowerShell Core from bash platforms?) I have come up with the following which seems close but is giving me strange, erratic errors from PowerShell:

<IsOSX Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::OSX)))' == 'true'">true</IsOSX>
<IsLinux Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Linux)))' == 'true'">true</IsLinux>
<IsUnix Condition="'true' == '$(IsOSX)' or 'true' == '$(IsLinux)'">true</IsUnix>
<PowerShell Condition="'true' == '$(IsUnix)'" > sed 1d $BASH_SOURCE | pwsh -noprofile -noninteractive -Command - ; exit $?</PowerShell>
@ximon

This comment has been minimized.

Copy link

@ximon ximon commented May 30, 2019

This was a massive help! Whenever i tried running powershell scripts another way notepad just opened with the contents of the script!?!?

@jtbrower

This comment has been minimized.

Copy link

@jtbrower jtbrower commented Dec 23, 2019

I must say that if this approach works (which I assume it does) then I owe you a big thank you. Being able to define the script inside the csproj itself is just pure awesomeness.

@rachelgshaffer

This comment has been minimized.

Copy link

@rachelgshaffer rachelgshaffer commented Jan 17, 2020

This is great - do you know how to reference other variables from within the script? For example, accessing the MyNumber property from within the script:

<PropertyGroup>
    <MyNumber>2</MyNumber>
</PropertyGroup>
<PropertyGroup> <!-- #3 prefix your powershell script with the $(PowerShell) variable, then code as normal! -->
    <myscript>$(PowerShell)
         .....
         $x = 2
         If ($x -eq $MyNumber) {
              .....
         }
         .....        
    </myscript>
</PropertyGroup>
@fearthecowboy

This comment has been minimized.

Copy link
Owner Author

@fearthecowboy fearthecowboy commented Jan 17, 2020

did you try $(MyNumber) ? IIRC that should work

@fred-gagnon

This comment has been minimized.

Copy link

@fred-gagnon fred-gagnon commented Jun 24, 2020

I found this to be extremely helpful ! However, I ran through some limitations with the PowerShell command. Those limitations are:

  1. It fails if the PowerShell script is inlined in the Exec task. e.g. <Exec Command="$(PowerShell)" Write-Host &quot;ok&quot;" />
  2. The actual return code of PowerShell is not returned. This is a limitation of the Windows Command Line. With the & exit %errorlevel% on the same line as the PowerShell command, 0 is always returned even if Exit -1 is in the PowerShell script. In order for the PowerShell process return code to be returned to Visual Studio, the exit %errorlevel% must be on a separate line.
  3. It fails if the PowerShell script declared in the property ends with last powershell command ]] (where ]] are the closing braces of the ![CDATA[ tag on the same line as the last PowerShell command).
  4. If the PowerShell script is a function ending with a call to that function and that function has required parameters, the PowerShell command fails if an explicit %0D%0A is not added at the end of the command. e.g. <Exec Command="$(PowerShell) $(ScriptWithFunctionRequiringParameters) &quot;@(ItemGroupUsedAsParameter)&quot; %0D%0A" />

Now, that being said, I used your scheme and changed the find /v commands with PowerShell -replace commands which allow using regular expressions. What I came up with is:

<PropertyGroup>
  <PowerShellCommand>
    powershell.exe -Command "&amp;{((Get-Content \"%~df0\" -Raw) -replace \"(?sm).*:START_POWERSHELL_SCRIPT\", \"\" -replace \"exit %%.*%%\", \"\") + \"`n\"}"|powershell.exe -ExecutionPolicy Bypass -NoLogo -NonInteractive -NoProfile -
    exit %ERRORLEVEL%
    :START_POWERSHELL_SCRIPT
    %0D%0A
  </PowerShellCommand>
</PropertyGroup>

With this improved PowerShell command, I was able to fix all the limitations previously mentioned, I thought I'd share it with you since the original command found here really helped be grasp how MsBuild was actually calling PowerShell.

@priyankajayaswal1

This comment has been minimized.

Copy link

@priyankajayaswal1 priyankajayaswal1 commented Dec 31, 2020

  <Target Name="Trigger" AfterTargets="Generate" >
    <PropertyGroup>
      <PowerShellCommand Condition=" '$(OS)' == 'Windows_NT' " >powershell</PowerShellCommand>
      <PowerShellCommand Condition=" '$(OS)' == 'Unix' " >pwsh</PowerShellCommand>
      <ClientGenTriggerScript>$(MSBuildProjectDirectory)\..\..\..\.build\client_gen.ps1</ClientGenTriggerScript>
    </PropertyGroup>
    <Exec Command="$(PowerShellCommand) -ExecutionPolicy Unrestricted -NoProfile -File $(ClientGenTriggerScript) -parameter1 p1 -parameter2 $(p2) --Verbose" />
  </Target>

I use it this way.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.