4

I am attempting to setup a Cake task that will use MsDeploy to sync a powershell script to a remote server and then execute that script as a post sync command.

The problem I'm facing is finding a combination of quotes and escapes within the cake file that allow the command to make it all the way to powershell with the file path quoted correctly to allow powershell to find the script; the path has spaces in it.

This appears to be so difficult because of the chain of execution that this command goes through at run time. First because this is written in C# the string either needs to be verbatim (@"command here") or to have all internal double quotes escaped with \.

Next Cake performs several operations on the arguments, though none seemed to affect things like quoting and escaping until it actually got to the execution, at which point it uses the C# Process.Start() static method to run the MsDeploy executable. In my reading it was suggested that this method of running a command requires three double quotes to be properly escaped, though that didn't mesh with what I was seeing when I tried.

Then once on the remote machine MsDeploy uses CMD.exe to execute the command which notably has no support for single quotes so double quotes either needed to be escaped by using \" or "".

The closest I have gotten looks like this:

Task("InitializeIISApplication")
    .IsDependentOn("InjectVariables") 
    .Does(() => {
        MsDeploy(new MsDeploySettings
        {
            Verb = Operation.Sync,
            RetryAttempts = 3,
            RetryInterval = 10000,
            Source = new FilePathProvider
            {
                Direction = Direction.source,
                Path = MakeAbsolute(File(@".\MyPowershell.ps1")).ToString()        
            },
            Destination = new FilePathProvider
            {
                Direction = Direction.dest,
                Path = File(deployParameters.ApplicationDestinationPath + @"\MyPowershell.ps1").ToString(),
                Username = deployParameters.MsDeployUserName,
                Password = deployParameters.MsDeployUserPassword,
                WebManagementService = deployParameters.DeploymentTargetUrl
            },
            AllowUntrusted = true,
            EnableRules = new List<string> {
                "DoNotDeleteRule"
            },
            PostSyncCommand = new CommandProvider {
                AppendQuotesToPath = false,
                Direction = Direction.dest,
                Path = $"powershell -file '{deployParameters.ApplicationDestinationPath}\\MyPowershell.ps1' ",
            }
        });

        MsDeploy(new MsDeploySettings
        {
            Verb = Operation.Delete,
            Destination = new FilePathProvider
            {
                Direction = Direction.dest,
                Path = File(deployParameters.ApplicationDestinationPath + "\MyPowershell.ps1").ToString(),
                Username = deployParameters.MsDeployUserName,
                Password = deployParameters.MsDeployUserPassword,
                WebManagementService = deployParameters.DeploymentTargetUrl
            },
            AllowUntrusted = true
        });
    });

The task dependency is just setting up the deployParameters object.

Which with Cake's Diagnostic verbosity enabled produces the following command into the logs (new lines added for clarity):

"C:/Program Files/IIS/Microsoft Web Deploy V3/msdeploy.exe"  
-verb:sync   
-source:filePath="Y:/PathToBuildArtifact/Deploy/MyPowershell.ps1"
-dest:filePath="C:/Application - With Spaces/MyPowershell.ps1",wmsvc="https://deploy-server/msdeploy.axd",userName=msdeployuser,password=********
-enableRule:DoNotDeleteRule
-retryAttempts:3
-retryInterval:10000
-allowUntrusted
-postSync:runCommand="powershell -file 'C:\Application - With Spaces\MyPowershell.ps1' "

Then ends with the error:

Warning: Processing -File ''C:/Application' failed: The given path's format is not supported. Specify a valid path for the -File parameter.

Any variant I've tried using double quotes inside the postSync command instead results in this error:

Error: Unrecognized argument '-'.

If it matters this is being done on the Bamboo CI server.

2 Answers2

3

Turns out it was likely the -file argument that was causing some weird parsing behaviour. Calling powershell -help within a command prompt displays the following snippet:

Runs the specified script in the local scope ("dot-sourced"), so that the functions and variables that the script creates are available in the current session. Enter the script file path and any parameters. File must be the last parameter in the command, because all characters typed after the File parameter name are interpreted as the script file path followed by the script parameters.

Which hints that there is some special logic there.

So instead I attempted to execute the file using the call operator (&) and after a couple different attempts at quoting I ended up with the following post sync command that ran successfully:

PostSyncCommand = new CommandProvider {
                Direction = Direction.dest,
                Path = $"powershell \"\"& \"\"\"\"{deployParameters.ApplicationDestinationPath}\\MyPowershell.ps1\"\"\"\" \"\"",
            }

Note that everything after powershell is contained within two double quotes the first acts as an escape for the call to msdeploy and strings within must have four quotes the first and third to escape the second and fourth for the call to msdeploy and the second then escaping the fourth for the final call to powershell.

  • 2
    The real special logic is unfortunately _not_ mentioned in the docs (the part about dot-sourcing is of no practical relevance unless you keep the session open with `-NoExit`): `-File` requires a _double_-quoted script-file argument, and all remaining arguments are treated as _literals_. By contrast, `-Command` (implied) space-concatenates all remaining arguments and treats the result _as a PowerShell script_, which implies that _single_-quoted arguments are supported too (whereas individually double-quoted arguments have their quotes stripped before concatenation). – mklement0 May 27 '19 at 20:09
2

Using PowerShell's -File CLI parameter requires use of " as the - only supported - quote character.

(By contrast, using -Command instructs PowerShell to treat the rest of the command line as if it were PowerShell source code, in which case ' is recognized as a quote character, for strings with literal contents[1]).

Since your PowerShell command line becomes a double-quoted argument to another command that is ultimately passed to cmd.exe, you must additionally, \-escape the " chars.

Therefore, the following should work [update: but doesn't - see comments]:

Path = $@"powershell -file \""{deployParameters.ApplicationDestinationPath}\MyPowershell.ps1\""",

[1] -File requires a double-quoted script-file argument, and all remaining arguments are treated as literals.
By contrast, -Command (which is the implied option in Windows PowerShell, whereas in PowerShell Core it is now -File) space-concatenates all remaining arguments and treats the result as PowerShell code, which implies that single-quoted arguments are supported too, with individually double-quoted arguments having their quotes stripped before concatenation.

mklement0
  • 382,024
  • 64
  • 607
  • 775
  • I have tried that set of quotes, but something about how one of `Process.Start()`, MsDeploy.exe, or CMD.exe process the string prevent it from getting to the powershell call –  May 27 '19 at 19:58
  • @Phaeze: Intriguing. How do `Process.Start` and `cmd.exe` come into the picture (I know nothing about MSDeploy)? Neither should require an extra set of quotes. – mklement0 May 27 '19 at 20:03
  • Cake uses `Process.Start` in order to make the call to msdeploy. Then msdeploy sends the command to `cmd.exe` once on the remote server, i couldn't find a way to just have it call ps directly. –  May 27 '19 at 20:32
  • @Phaeze: Thanks for updating your question; please see my update. – mklement0 May 27 '19 at 23:07
  • Awesome, Ill try to find some time to test that out, it may be a while though as I don't want to break our deployment now that its working and our QA team is backlogged. –  May 28 '19 at 16:27
  • So for another reason i've had to revisit my initial solution and im trying again to get it working with the -file argument, and the updated solution doesn't even make it to msdeploy, the string basically stops at the filename start quote, so msdeploy treats the rest as an argument to it. I've tried few other escaping sequences and they all do exactly the same: `\\\"`, `\\\"\"\"`, `\\\"\"`, `\"\"\"`, `\"\"` –  May 31 '19 at 18:48
  • @Phaeze: What does the Cake diagnostic show, assuming you get there? – mklement0 May 31 '19 at 22:09
  • So me and our devops lead hammered this out on Friday, at this point what i'm pretty sure is missing is the ability to provide the `--%` argument to msdeploy from the cake add in. This argument tells it not to parse the arguments that come after. without this argument msdeploy scans and tokenizes all the arguments and ignores all quotes when doing so. Our new working solution is to use msdeploy to sync and remove the file, but using the powershell cake addin to do the actual execution. –  Jun 03 '19 at 15:10
  • I'll update the question, and my answer as necessary once i have some spare time. –  Jun 03 '19 at 15:11
  • 1
    @Phaeze: `--%` is a PowerShell construct - I wouldn't expect `msdeploy` to understand it. The [`msdeploy` docs](https://learn.microsoft.com/en-us/aspnet/web-forms/overview/deployment/web-deployment-in-the-enterprise/deploying-web-packages#using-msdeployexe) suggests that quoting _is_ supported (seemingly both single and double quotes). – mklement0 Jun 03 '19 at 22:47
  • well then I'm just going to go with it being undefined behaviour and the lesson is don't do it :) –  Jun 03 '19 at 23:33