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?
3 Answers 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
3 Comments
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 # 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
}
1 Comment
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'-Waitswitch - in a new window - due to absence of-NoNewWindow- and the inability to query the exit code due to absence of-PassThruand inspection of the returned object's.ExitCodeproperty; additionally, the only way to capture the executable's output would be via files
(-RedirectStandardOutputand-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$LASTEXITCODEvariable.
Do not useStart-Process(or theSystem.Diagnostics.ProcessAPI 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"'
3 Comments
'...' 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.Explore related questions
See similar questions with these tags.
'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,-NoNewWindowand-PassThruto make the execution synchronous, run in the same window, allow querying the exit code. However, direct invocation would make all this easier.