2

I have a tool for editing the VersionInfo portion of our executables so that we can tie all of the different components to one installer part number. The tool I am using is resourcehacker (https://www.angusj.com/resourcehacker/) which supports command line arguments. However, they don't play well when used within a Powershell script.

The operation I am trying to complete is compiling the .rc file into a .res file that I can use at a later step. What is happening instead is that only the resourcehacker exe starts but the compile never happens.

A snip of what I have is as follows (it's ugly because I've been trying everything):

$rhPath = (Join-Path $CICDBaseScriptPath 'ResourceHacker.exe') #Path to rh.exe
$arg1 = "`'-open "
$arg2 = "`"" 
$arg3 = $ReplaceTTKVersionInfoRC # The name of the .rc file to be compiled
$arg4 = "`""
$arg5 = " -save `""
$arg6 = $TTKVersionInfoRes # The name of the output .res file
$arg7 = "`""
$arg8 = " -action compile`'"
$allArgs = $arg1,$arg2,$arg3,$arg4,$arg5,$arg6,$arg7,$arg8 -join ""
 
Start-Process -FilePath $rhPath -ArgumentList $allArgs

To run from the command line, I would need to have the following:

C:\> ResourceHacker.exe '-open "C:\agent_svc\_work15円\s\CICD\TTKVersionInfo_Temp.rc" -save "C:\agent_svc\_work15円\s\CICD\TTKVersionInfo.res" -action compile -log con'

When I run this from the commandline it works correctly. Also, from a powershell terminal interactively, if I run Start-Process with the same parameters, it will work. However, the Start-Process call does not seem to pass the arguments (because my .res file never changes) when called from inside the powershell script.

Can anyone please help me understand what needs to be done to get this to work?

mklement0
451k68 gold badges726 silver badges985 bronze badges
asked Sep 19 at 21:26
1
  • The only immediate problem with your (needlessly complicated) own attempt was the inclusion of extraneous (and needlessly escaped) ' chars. around the embedded argument list; the same extraneous chars. are present in your "run from the command line" command. Sahil's answer doesn't address this problem and simply uses a single, single-line string literal without the extraneous characters instead. It also uses -NoWait, -NoNewWindow and -PassThru to make the execution synchronous, run in the same window, allow querying the exit code. However, direct invocation would make all this easier. Commented Sep 20 at 17:19

3 Answers 3

3

in many cases, you can pass a set of parameters/options to an .exe using an array of those options. for instance, the below code works with RoboCopy.exe ... and it is NOT using a splat, it just looks like one.

using Start-Process is not needed at all.

$SourceComputerName = $env:COMPUTERNAME
$Source = "\\$SourceComputerName\c$\Temp\zzz - copy"
 
$DestComputerName = $env:COMPUTERNAME
$Destination = "\\$DestComputerName\d$\Temp\aaa"
 
$FileSpec = '*.*'
 
$TimeStamp = Get-Date -Format 'yyyy-MM-dd_hh-mm-ss'
$Subject = 'RobocopyTest'
$LogFileName = -join ($Subject, '_-_', $TimeStamp, '.log')
$FullLogFileName = Join-Path -Path $env:TEMP -ChildPath $LogFileName
 
$RC_Params = @(
 $Source
 $Destination
 $FileSpec 
 # put your current options below
 "/Log:$FullLogFileName"
 '/NP'
 '/E'
 '/TEE'
 )
 
$RC_Params
#robocopy $RC_Params
answered Sep 19 at 21:57
Sign up to request clarification or add additional context in comments.

3 Comments

Indeed, Start-Process isn't needed; it rarely is, except in unusual cases. What you're showing is an instance of splatting, just an implicit case, due to passing an array as-is to an external executable. There is one subtle - and largely irrelevant in practice - difference between passing $RC_Params vs. @RC_Params, however: only the explicit splatting syntax (@RC_Params) recognizes --%, the stop-parsing token, as an array element; see stackoverflow.com/a/59071135/45375
Aha! that makes sense ... thank you for the clarification. [grin]
Glad to hear it; I see that this has come up before (as have you, apparently - glad to hear you're still alive). Tailoring your answer to the case at hand would have been helpful.
2
# Paths
$rhPath = Join-Path $CICDBaseScriptPath 'ResourceHacker.exe'
$rcFile = $ReplaceTTKVersionInfoRC
$resFile = $TTKVersionInfoRes
# Check that ResourceHacker exists
if (-not (Test-Path $rhPath)) {
 Write-Error "ResourceHacker.exe not found at $rhPath"
 exit 1
}
# Check that input RC file exists
if (-not (Test-Path $rcFile)) {
 Write-Error "Input RC file not found at $rcFile"
 exit 1
}
# Build a single argument string exactly like the CLI expects
# Note: Quotes inside the string ensure paths with spaces are handled correctly
$arguments = "-open `"$rcFile`" -save `"$resFile`" -action compile -log con"
Write-Host "Running ResourceHacker..."
Write-Host "Command: $rhPath $arguments"
try {
 # Start the process, wait for completion, keep output in the current console
 $process = Start-Process -FilePath $rhPath -ArgumentList $arguments -Wait -NoNewWindow -PassThru
 # Check exit code
 if ($process.ExitCode -ne 0) {
 Write-Error "ResourceHacker failed with exit code $($process.ExitCode)"
 exit $process.ExitCode
 }
 Write-Host "Resource compilation completed successfully."
} catch {
 Write-Error "Failed to run ResourceHacker: $_"
 exit 1
}
answered Sep 19 at 21:34

1 Comment

Thanks for your help. While I did try something like this before, using your snippet somehow worked for me in a powershell terminal. However, I am calling this script from within an Azure devops pipeline and I'm seeing the file actually be updated. Like I said, you answered the question I originally asked but now I need to look into why it behaves differently in a pipeline.
1

Fundamentally, your way of constructing the arguments list is awkward and needlessly complicated (including unnecessary escaping of extraneous ' chars. as `').[1]

However, the only immediate problem is the mistaken enclosure of the content of string $allArgs in embedded '...', which breaks the call, given that ResourceHacker.exe interprets ' instances literally (it only recognizes " as having syntactic function) and, in general, expects arguments to be passed individually - these behaviors are shared by most CLIs):
To fix this problem, remove `' from $arg1 and $arg8.

Note:

  • Likely subsequent problems would be the asynchronous execution of the target executable - due to absence of Start-Process' -Wait switch - in a new window - due to absence of -NoNewWindow - and the inability to query the exit code due to absence of -PassThru and inspection of the returned object's .ExitCode property; additionally, the only way to capture the executable's output would be via files
    (-RedirectStandardOutput and -RedirectStandardError).

  • However, all these problems can be solved more simply via direct execution of the target executable, as discussed in the next section.


Taking a step back:

  • To synchronously execute console applications (or batch files) in the current console (terminal), call them directly (& $rhPath ... in your case).
    This simplifies the syntax, allows you to directly capture output, as well as to later query their exit code via the automatic $LASTEXITCODE variable.
    Do not use Start-Process (or the System.Diagnostics.Process API it is based on), except in unusual scenarios such as needing to run with a different user identity.
    See this answer and this guidance.

Therefore (note that '...' enclosure of the argument list, as shown in the last command in your question, would be equally mistaken here; for an explanation of the syntactic need to use &, the call operator, in this case, see this answer):

& $rhPath -open $ReplaceTTKVersionInfoRC -save $TTKVersionInfoRes -action compile -log con

You can alternatively use (potentially implicit) splatting if you want to specify the command-line arguments by way of an array (whose definition can easily be spread across multiple lines for readability), along the lines of $rhArgs = @('-open', $ReplaceTTKVersionInfoRC, ...), followed by invocation & $rhPath @rhArgs

Note that, either way, using embedded "..." quoting around individual arguments (as necessary in your $allArgs argument passed to Start-Process) is not required in these cases.


[1] Inside "..." (expandable string literals), ' chars. do not require escaping (only " does).
If you want the readability and ease of definition of a multiline string you can use a here-string - which doesn't even require you to embedded " chars. - and subsequently replace its line breaks with spaces, as the following simplified example shows:

$allArgs = @" 
-username
"$env:USERNAME"
"@ -replace '\r?\n', ' ' # -> e.g., '-username "jdoe"'
answered Sep 19 at 22:01

3 Comments

I agree that the way the arguments are constructed in my example is not ideal. I did it this way because using Invoke-Expression and call operator (&) failed me. While Start-Process also did not seem to work at first, it did, however, replicate the constructed argument list of the source material I had been following to call the executable. Since the source material had been functional and used the rh.exe '...' format, I followed this because leaving them out didn't yield anything positive. Sahil's example did work in a ps terminal. Now, I just need it to work via Azure Devops Pipeline.
If Sahil's example works (which also (correctly) has no '...' enclosure), so does the &-based direct invocation in this answer - with all the advantages over a Start-Process-based call discussed in the answer. It should also work in an Azure pipeline.
You are correct. I was able to make the script work for me within an Azure pipeline. I believe Sahil's approach was the initial fix, though after reading through your explanations, I was able to modify and simplify the code which further allowed me to resolve the issue I was seeing in the pipeline. Thank you for your help as well.

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.