Contents tagged with .NET

  • Returning an exit code from a PowerShell script

    Returning an exit code from a PowerShell script seems easy… but it isn’t that obvious. In this blog post I will show you an approach that works for PowerShell scripts that can be called from both PowerShell and batch scripts, where the command to be executed can be specified in a string, execute in its own context and always return the correct error code.

    Below is a kind of transcript of the steps that I took to get to an approach that works for me. It is a transcript of the steps I took, for the conclusions just jump to the end.

    In many blog posts you can read about calling a PowerShell script that you call from a batch script, and how to return an error code. This comes down to the following:

    c:\temp\exit.ps1:

    Write-Host "Exiting with code 12345"
    exit 12345

    c:\temp\testexit.cmd:

    @PowerShell -NonInteractive -NoProfile -Command "& {c:\temp\exit.ps1; exit $LastExitCode }"
    @echo From Cmd.exe: Exit.ps1 exited with exit code %errorlevel%

    Executing c:\temp\testexit.cmd results in the following output:

    Exiting with code 12345
    From Cmd.exe: Exit.ps1 exited with exit code 12345

    But now we want to call it from another PowerShell script, by executing PowerShell:

    c:\temp\testexit.ps1:

    PowerShell -NonInteractive -NoProfile -Command c:\temp\exit.ps1
    Write-Host "From PowerShell: Exit.ps1 exited with exit code $LastExitCode"

    Executing c:\temp\testexit.ps1 results in the following output:

    Exiting with code 12345
    From PowerShell: Exit.ps1 exited with exit code 1

    This is not what we expected… What happs? If the script just returns the exit code is 0, otherwise the exit code is 1, even if you exit with an exit code!?

    But what if we call the script directly, instead of through the PowerShell command?

    We change exit.ps1 to:

    Write-Host "Global variable value: $globalvariable"
    Write-Host "Exiting with code 12345"
    exit 12345

    And we change testexit.ps1 to:

    $global:globalvariable = "My global variable value"
    & c:\temp\exit.ps1
    Write-Host "From PowerShell: Exit.ps1 exited with exit code $LastExitCode"

    Executing c:\temp\testexit.ps1 results in the following output:

    Global variable value: My global variable value
    Exiting with code 12345
    From PowerShell: Exit.ps1 exited with exit code 12345

    This is what we wanted! But now we are executing the script exit.ps1 in the context of the testexit.ps1 script, the globally defined variable $globalvariable is still known. This is not what we want. We want to execute it is isolation.

    We change c:\temp\testexit.ps1 to:

    $global:globalvariable = "My global variable value"
    PowerShell -NonInteractive -NoProfile -Command c:\temp\exit.ps1
    Write-Host "From PowerShell: Exit.ps1 exited with exit code $LastExitCode"

    Executing c:\temp\testexit.ps1 results in the following output:

    Global variable value:
    Exiting with code 12345
    From PowerShell: Exit.ps1 exited with exit code 1

    We are not executing exit.ps1 in the context of testexit.ps1, which is good. But how can we reach the holy grail:

    1. Write a PowerShell script that can be executed from batch scripts an from PowerShell
    2. That return a specific error code
    3. That can specified as a string
    4. Can be executed both in the context of a calling PowerShell script AND (through a call to PowerShell) in it’s own execution space

    We change c:\temp\testexit.ps1 to:

    $global:globalvariable = "My global variable value"
    PowerShell -NonInteractive -NoProfile -Command  { c:\temp\exit.ps1 ; exit $LastExitCode }
    Write-Host "From PowerShell: Exit.ps1 exited with exit code $LastExitCode"

    This is the same approach as when we called it from the batch script. Executing c:\temp\testexit.ps1 results in the following output:

    Global variable value:
    Exiting with code 12345
    From PowerShell: Exit.ps1 exited with exit code 12345

    This is close. But we want to be able to specify the command to be executed as string, for example:

    $command = "c:\temp\exit.ps1 -param1 x -param2 y"

    We change c:\temp\exit.ps1 to: (support for variables, test if in its own context)

    param( $param1, $param2)
    Write-Host "param1=$param1; param2=$param2"
    Write-Host "Global variable value: $globalvariable"
    Write-Host "Exiting with code 12345"
    exit 12345

    If we change c:\temp\testexit.ps1 to:

    $global:globalvariable = "My global variable value"
    $command = "c:\temp\exit.ps1 -param1 x -param2 y"
    Invoke-Expression -Command $command
    Write-Host "From PowerShell: Exit.ps1 exited with exit code $LastExitCode"

    We get a good exit code, but we are still executing in the context of testexit.ps1.

    If we use the same trick as in calling from a batch script, that worked before?

    We change c:\temp\testexit.ps1 to:

    $global:globalvariable = "My global variable value"
    $command = "c:\temp\exit.ps1 -param1 x -param2 y"
    PowerShell -NonInteractive -NoProfile -Command { $command; exit $LastErrorLevel }
    Write-Host "From PowerShell: Exit.ps1 exited with exit code $LastExitCode"

    Executing c:\temp\testexit.ps1 results in the following output:

    From PowerShell: Exit.ps1 exited with exit code 0

    Ok, lets use the Invoke-Expression again. We change c:\temp\testexit.ps1 to:

    $global:globalvariable = "My global variable value"
    $command = "c:\temp\exit.ps1 -param1 x -param2 y"
    PowerShell -NonInteractive -NoProfile -Command { Invoke-Expression -Command $command; exit $LastErrorLevel }
    Write-Host "From PowerShell: Exit.ps1 exited with exit code $LastExitCode"

    Executing c:\temp\testexit.ps1 results in the following output:

    Cannot bind argument to parameter 'Command' because it is null.
    At :line:3 char:10
    + PowerShell <<<<  -NonInteractive -NoProfile -Command { Invoke-Expression -Command $command; exit $LastErrorLevel }

    From PowerShell: Exit.ps1 exited with exit code 1

    We should go back to executing the command as a string, so not within brackets (in a script block). We change c:\temp\testexit.ps1 to:

    $global:globalvariable = "My global variable value"
    $command = "c:\temp\exit.ps1 -param1 x -param2 y"
    PowerShell -NonInteractive -NoProfile -Command $command
    Write-Host "From PowerShell: Exit.ps1 exited with exit code $LastExitCode"

    Executing c:\temp\testexit.ps1 results in the following output:

    param1=x; param2=y
    Global variable value:
    Exiting with code 12345
    From PowerShell: Exit.ps1 exited with exit code 1

    Ok, we can execute the specified command text as if it is a PowerShell command. But we still have the exit code problem, only 0 or 1 is returned.

    Lets try something completely different. We change c:\temp\exit.ps1 to:

    param( $param1, $param2)

    function ExitWithCode
    {
        param
        (
            $exitcode
        )

        $host.SetShouldExit($exitcode)
        exit
    }

    Write-Host "param1=$param1; param2=$param2"
    Write-Host "Global variable value: $globalvariable"
    Write-Host "Exiting with code 12345"
    ExitWithCode -exitcode 12345
    Write-Host "After exit"

    What we do is specify to the host the exit code we would like to use, and then just exit, all in the simplest utility function.

    Executing c:\temp\testexit.ps1 results in the following output:

    param1=x; param2=y
    Global variable value:
    Exiting with code 12345
    From PowerShell: Exit.ps1 exited with exit code 12345

    Ok, this fulfills all our holy grail dreams! But couldn’t we make the call from the batch script also simpler?

    Change c:\temp\testexit.cmd to:

    @PowerShell -NonInteractive -NoProfile -Command "c:\temp\exit.ps1 -param1 x -param2 y"
    @echo From Cmd.exe: Exit.ps1 exited with exit code %errorlevel%

    Executing c:\temp\testexit.cmd results in the following output:

    param1=x; param2=y
    Global variable value:
    Exiting with code 12345
    From Cmd.exe: Exit.ps1 exited with exit code 12345

    This is even simpler! We can now just call the PowerShell code, without the exit $LastExitCode trick!

    ========================= CONCLUSIONS ============================

    And now the conclusions after this long long story, that took a lot of time to find out (and to read for you):

    • Don’t use exit to return a value from PowerShell code, but use the following function:
    • function ExitWithCode
      {
          param
          (
              $exitcode
          )

          $host.SetShouldExit($exitcode)
          exit
      }

    • Call script from batch using:

    • PowerShell -NonInteractive -NoProfile -Command "c:\temp\exit.ps1 -param1 x -param2 y"
      echo %errorlevel%

    • Call from PowerShell with: (Command specified in string, execute in own context)
      $command = "c:\temp\exit.ps1 -param1 x -param2 y"
      PowerShell -NonInteractive -NoProfile -Command $command

      $LastExitCode contains the exit code
    • Call from PowerShell with: (Direct command, execute in own context)

      PowerShell -NonInteractive -NoProfile -Command { c:\temp\exit.ps1 -param1 x -param2 y }
    • $LastExitCode contains the exit code

    • Call from Powershell with: (Command specified in string, invoke in caller context)
    • $command = "c:\temp\exit.ps1 -param1 x -param2 y"
      Invoke-Expression -Command $command
      $LastExitCode contains the exit code

    • Call from PowerShell with: (Direct command, execute in caller context)

      & c:\temp\exit.ps1 -param1 x -param2 y

    • $LastExitCode contains the exit code