Idiomatic powershell translation of (cd $dir && someCommand) - powershell

I have a python script that spits out JSON I'd like to capture with ConvertFrom-Json. Unfortunately, this script requires that I cd to a different directory before I run it. What's the idiomatic powershell way to do this?
This works:
$q = powershell.exe -Command "cd some\other\dir; python JsonMaker.py | ConvertFrom-Json"
As does this:
$cwd=Get-Location
cd some\other\dir
$q=python JsonMaker.py | ConvertFrom-Json
cd "$cwd"
But changing the current working directory seems dicey to me - what if the python script outputs malformed JSON, will I be left in some\other\dir ?
In the unix shell scripting world, I'd obviously do something like
(cd some/other/dir && python JsonMaker.py) | commandThatUsesJson
or read the input in with $(cd some/other/dir && python JsonMaker.py). However, in unix subshells are cheap. In powershell I see a noticeable delay to starting a subshell.
What's the approach long-time Powershell users take to something like this?

I'd probably use pushd/popd:
pushd some\other\dir
$q=python JsonMaker.py | ConvertFrom-Json
popd

Your script looks fine to me. Unless ConvertFrom-Json throws a terminating exception (which I don't think it will), the script will continue and your cd $cwd line would reutnr you back.
You coulod also use Push-/Pop-Location, but it's basically just a "pretty" way of doing what you already have. Ex.
#Save location
Push-Location
#Script
Set-Location some\other\dir
python JsonMaker.py | ConvertFrom-Json
#Return to previous location
Pop-Location

python.exe JsonMaker.py runs as a child process. Changes made to the current directory in a child process don't affect the parent. ConvertFrom-Json also doesn't affect the current directory. It converts a JSON string to an object representing the JSON data or throws a (non-terminating) error if the JSON string is malformed.
If you want to be on the safe side, run the conversion in a try block and put the statement to return from the temporary working directory after that block:
try {
$q = python JsonMaker.py | ConvertFrom-Json
} catch {
# error handling (optional)
}
cd "$cwd"
or in a finally clause:
try {
$q = python JsonMaker.py | ConvertFrom-Json
} catch {
# error handling (optional)
} finally {
cd "$cwd"
}
As others have already mentioned, I'd use Push-Location and Pop-Location (or their aliases pushd and popd) as a simpler way of changing to a different working directory and returning to the original directory. The cmdlets work similar to the Unix shell commands pushd and popd.
I'd also recommend adding the extension to the executable name (to avoid unintentionally running different executable files with the same basename (e.g. python.cmd or python.com) and using the call operator (&). Running the command in a new powershell.exe process is not necessary, and would also return just a string representation of the object created from the JSON string instead of the object itself, which is probably not what you want.
Modified code:
Push-Location 'D:\some\other\dir'
try {
$q = & python.exe JsonMaker.py | ConvertFrom-Json
} catch {
# error handling (optional)
} finally {
Pop-Location
}
or like this if you want to conditionally run the python script only if changing the directory was successful (thus fully emulating the behavior of &&):
try {
Push-Location 'D:\some\other\dir' -ErrorAction Stop
$q = & python.exe JsonMaker.py | ConvertFrom-Json
} catch {
# error handling (optional)
} finally {
Pop-Location
}

Related

How to run command line or PowerShell script if file could exist in two locations?

We have multiple systems with the same Dell program but different versions. The version number is the same but one is in the x86 folder and the other is in the x64 one as shown below. Could someone help me convert this to a command line or PowerShell script so that it would correctly from whichever location exists? It should only exist in one of the two locations and only needs to be run once.
Dell Command | Update
"%ProgramFiles%\Dell\CommandUpdate\dcu-cli.exe" /configure -updatetype=bios,firmware,driver,application,utility,others
"%ProgramFiles%\Dell\CommandUpdate\dcu-cli.exe" /applyupdates -reboot=enable -autosuspendbitlocker=enable
Dell Command | Update for Windows Universal
%programfiles(x86)%\Dell\CommandUpdate\dcu-cli.exe" /configure -updatetype=bios,firmware,driver,application,utility,others
%programfiles(x86)%\Dell\CommandUpdate\dcu-cli.exe" /applyupdates -reboot=enable -autosuspendbitlocker=enable
Using PowerShell, iterate over the paths and use Test-Path to make sure the path exists:
$possiblePaths =
"$env:ProgramFiles\Dell\CommandUpdate\dcu-cli.exe",
"${env:ProgramFiles(x86)}\Dell\CommandUpdate\dcu-cli.exe"
$correctPath = foreach( $path in $possiblePaths ) {
if( Test-Path -PathType Leaf $path ) {
$path
break
}
}
if( !$correctPath ) {
throw "Could not find dcu-cli.exe at any of the following paths: $(#( $possiblePaths ) -join ', ')"
}
# Execute found binary
& $correctPath /configure -updatetype='bios,firmware,driver,application,utility,others'
After the loop, check that $correctPath doesn't have a "falsey" evaluation (in this case, not $null or [string]::Empty). If it evaluates to $False, the file was not found in any of the possible locations, and we throw an error.
To execute, use the call-operator & to execute the path stored in $correctPath. You can include any parameters after this as if you were invoking the binary by literal name instead of as a variable. The sample above uses one of your argument strings from the question as an example.

Running a command with arguments assistance

I have a command which runs a program in silent mode, it uses an XML file for the data repository and a word template to create multiple word documents based on a filter xml file.
The command I use is:
"P:\ath to\executable" -Username:Admin -Password:Pa55w0rd -Datadefinition:"C:\Data.xml" -Datafilter:"C:\Filter.xml" -wordtemplate:"C:\Batch\Paul1.dotx" -Targetdocument:="C:\Batch\Paul1.pdf" -filetype:PDF -Log:"C:\Logs\error.log" -Usage:DOCGENSILENT
I need to run this as a PowerShell script which I have mostly managed:
set-executionpolicy unrestricted
$datadefinition = Get-Content "C:\Data file.xml"
$datafilter = Get-Content "C:\Filter for data file.xml"
$wordTemplate = Get-Content "C:\"C:\Template\Paul1.dotx"
$targetFolder = Get-Content "C:\"C:\Paul\Paul.pdf"
Stop-Job = "Executable path" -Username:Admin -Password:Pa55w0rd -Datadefinition:%dataDefinition% -Datafilter:%dataFilter% -wordtemplate:%wordTemplate% -Targetdocument:%targetFolder% -filetype:docx -Log:%logPath% -Usage:DOCGENSILENT
Stop-Job 1
set-executionpolicy restricted
Write-Host -NoNewLine "Press any key to continue..."
$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
My issue is that the script starts the executable but then doesnt pass the Variables, can anyone guide me in the right direction to fix this?
Getting this working depends on the behavior of your executable. Some things I noticed:
Shouldn't this:
$wordTemplate = Get-Content "C:\"C:\Template\Paul1.dotx"
be this:
$wordTemplate = "C:\Template\Paul1.dotx"
Are you sure you need Get-Content? (Aside from that, the path and quoting in your sample are not correct.)
Shouldn't this:
$targetFolder = Get-Content "C:\"C:\Paul\Paul.pdf"
be this:
$targetDocument = "C:\Paul\Paul.pdf"
I doubt Get-Content is correct here, since presumably your output file doesn't exist yet? I also renamed the variable so it makes more sense in your command.
In fact, are you sure you need Get-Content for any of those? Aren't you specifying filenames, not the content of the files?
In PowerShell, variables are prefixed with $ rather than being surrounded by %.
Using Set-ExecutionPolicy within a script to enable scripts to run is pointless, because the script is already running. (That is, if execution policy prevented script execution, PowerShell wouldn't let you run the script in the first place.)
If my guesses regarding your variables are correct, I think your script should look something like this (note also that I specified a $logFile variable, which I didn't see in your script):
$datadefinition = "C:\Users\Administrator\data\Sample Model_146_object type(s).xml"
$datafilter = "C:\Users\Administrator\data\Sample Model_146_object type(s).xml"
$wordtemplate = "C:\Users\Administrator\Templates\Base object.docx"
$targetdocument = "C:\Users\Administrator\Result\sample test15"
$logfile = "C:\Users\Administrator\Logs\C4W Error.log"
& "C:\Program Files (x86)\Communicator4Word.exe" -Username:Admin -Password: -Datadefinition:$datadefinition -Datafilter:$datafilter -wordtemplate:$wordtemplate -Targetdocument:$targetdocument -filetype:docx -Log:$logfile -Usage:DOCGENSILENT
I don't know the behavior of Communicator4Word.exe when you use -Password: with no password after it. (Is that a syntax error, or should you just omit -Password: altogether?)

Running .exe to get String output is not saving to string variable

Forgive me, I am new to PowerShell in general. I'm updating a build process that works on Linux (in bash) to one that will work on Windows in PowerShell.
My goal is to get the version of the game engine currently present on the build system. The default build location is well-known, so we try to execute it and get the version, like so:
$Version = & 'C:\Program Files\LOVE\love.exe' --version
When this executes, the $Version value is empty:
Write-Output $Version
[no output]
$Version -Eq $True
False
If I run my executable directly from the shell, I notice the line is not presented on a newline:
PS C:\Users\robbm\Myproject\Mygame> $Version = & 'C:\Program Files\LOVE\love.exe' --version
PS C:\Users\robbm\Myproject\Mygame> LOVE 11.3 (Mysterious Mysteries)
This makes me suspect there is some strange output behavior with the executable in the first place.
Is this a problem with LÖVE's --version output, or am I misunderstanding something about redirecting outputs in PowerShell? I've tried a few things to capture output, and $Version always seems to end up a nil value, such as:
$Version = & '\\build\love\love.exe' '--version' | Out-String
Write-Output $Version
$Version = (& '\\build\love\love.exe' '--version' | Out-String)
Write-Output $Version
Help is appreciated. As this works for other cmdlets, I'm inclined to believe it might be a function of LÖVE, but I'd appreciate thoughts as to how I could work with this anyway, or any method in which to capture the version it's clearly outputting to the screen when I execute it directly regardless.
EDIT:
LÖVE definitely does something different in regards to running on Windows. Looking at the version printing source, we are working with the aptly-named LOVE_LEGENDARY_CONSOLE_IO_HACK enabled on Windows, which appears to open a new console entirely, perhaps in cmd and write out there.
Doing the suggestions of commenters, I tried doing the .Exception.Message method, but there is none when called like so:
$Version = &('C:\Program Files\LOVE\love.exe' '--version').Exception.Message
So I'm still looking for ways to make this work within the confines of LÖVE hacking together some strange I/O stream.
EDIT2:
Another fun fact, redirection to a file similarly fails:
PS> (&'C:\Program Files\LOVE\love.exe' '--version') 2>&1 > .\love.txt
PS> LOVE 11.3 (Mysterious Mysteries)
PS> cat .\love.txt
[empty]
So this looks to be overly hacky on behalf of LÖVE, not an issue with PowerShell.
After reading your last edit, this probably won't help you, but may help others.
You could try to capture the output like this:
function runCmdAndCaptureOutput(
[Parameter(Mandatory=$true)]
[string] $cmd
) {
[string] $errOut
[string] $stdOut
# Deliberately dropped '$' from vars below.
Invoke-Expression $cmd -ErrorVariable errOut -OutVariable stdOut
if($LASTEXITCODE -ne 0) {
Write-Host -ForegroundColor Red "LASTEXITCODE: $LASTEXITCODE"
throw $LASTEXITCODE
}
return $stdOut
}
$exeCmd = "'C:\Program Files\LOVE\love.exe' --version"
$output = runCmdAndCaptureOutput -cmd $exeCmd
Write-Host $output

How to create an alias with fixed/static parameters in Powershell [duplicate]

I'm trying to set up a Windows PowerShell alias to run MinGW's g++ executable with certain parameters. However, these parameters need to come after the file name and other arguments. I don't want to go through the hassle of trying to set up a function and all of that. Is there a way to simply say something like:
alias mybuild="g++ {args} -lib1 -lib2 ..."
or something along those lines? I am not all that familiar with PowerShell, and I'm having a difficult time finding a solution. Anyone?
You want to use a function, not an alias, as Roman mentioned. Something like this:
function mybuild { g++ $args -lib1 -lib2 ... }
To try this out, here's a simple example:
PS> function docmd { cmd /c $args there }
PS> docmd echo hello
hello there
PS>
You might also want to put this in your profile in order to have it available whenever you run PowerShell. The name of your profile file is contained in $profile.
There is not such a way built-in. IMHO, a wrapper function is the best way to go so far. But I know that some workarounds were invented, for example:
https://web.archive.org/web/20120213013609/http://huddledmasses.org/powershell-power-user-tips-bash-style-alias-command
To build an function, store it as an alias, and persist the whole thing in your profile for later, use:
$g=[guid]::NewGuid();
echo "function G$g { COMMANDS }; New-Alias -Force ALIAS G$g">>$profile
where you have replaced ALIAS with the alias you want and COMMANDS with the command or string of commands to execute.
Of course, instead of doing that you can (and should!) make an alias for the above by:
echo 'function myAlias {
$g=[guid]::NewGuid();
$alias = $args[0]; $commands = $args[1]
echo "function G$g { $commands }; New-Alias -Force $alias G$g">>$profile
}; New-Alias alias myAlias'>>$profile
Just in case your brain got turned inside out from all the recursion (aliasing of aliases, etc.), after pasting the second code block to your PowerShell (and restarting PowerShell), a simple example of using it is:
alias myEcho 'echo $args[0]'
or without args:
alias myLs 'ls D:\MyFolder'
Iff you don't have a profile yet
The above method will fail if you don't have a profile yet!
In that case, use New-Item -type file -path $profile -force from this answer.
This is a sample function that will do different things based on how it was called:
Function Do-Something {
[CmdletBinding()]
[Alias('DOIT')]
Param(
[string] $option1,
[string] $option2,
[int] $option3)
#$MyInvocation|select *|FL
If ($MyInvocation.InvocationName -eq 'DOIT'){write-host "You told me to do it...so i did!" -ForegroundColor Yellow}
Else {Write-Host "you were boring and said do something..." -ForegroundColor Green}
}
Creating a 'filter' is also an option, a lighter alternative to functions. It processes each element in the pipeline, assigning it the $_ automatic variable. So, for instance:
filter test { Write-Warning "$args $_" }
'foo','bar' | test 'This is'
returns:
WARNING: This is foo
WARNING: This is bar

How can I write a PowerShell alias with arguments in the middle?

I'm trying to set up a Windows PowerShell alias to run MinGW's g++ executable with certain parameters. However, these parameters need to come after the file name and other arguments. I don't want to go through the hassle of trying to set up a function and all of that. Is there a way to simply say something like:
alias mybuild="g++ {args} -lib1 -lib2 ..."
or something along those lines? I am not all that familiar with PowerShell, and I'm having a difficult time finding a solution. Anyone?
You want to use a function, not an alias, as Roman mentioned. Something like this:
function mybuild { g++ $args -lib1 -lib2 ... }
To try this out, here's a simple example:
PS> function docmd { cmd /c $args there }
PS> docmd echo hello
hello there
PS>
You might also want to put this in your profile in order to have it available whenever you run PowerShell. The name of your profile file is contained in $profile.
There is not such a way built-in. IMHO, a wrapper function is the best way to go so far. But I know that some workarounds were invented, for example:
https://web.archive.org/web/20120213013609/http://huddledmasses.org/powershell-power-user-tips-bash-style-alias-command
To build an function, store it as an alias, and persist the whole thing in your profile for later, use:
$g=[guid]::NewGuid();
echo "function G$g { COMMANDS }; New-Alias -Force ALIAS G$g">>$profile
where you have replaced ALIAS with the alias you want and COMMANDS with the command or string of commands to execute.
Of course, instead of doing that you can (and should!) make an alias for the above by:
echo 'function myAlias {
$g=[guid]::NewGuid();
$alias = $args[0]; $commands = $args[1]
echo "function G$g { $commands }; New-Alias -Force $alias G$g">>$profile
}; New-Alias alias myAlias'>>$profile
Just in case your brain got turned inside out from all the recursion (aliasing of aliases, etc.), after pasting the second code block to your PowerShell (and restarting PowerShell), a simple example of using it is:
alias myEcho 'echo $args[0]'
or without args:
alias myLs 'ls D:\MyFolder'
Iff you don't have a profile yet
The above method will fail if you don't have a profile yet!
In that case, use New-Item -type file -path $profile -force from this answer.
This is a sample function that will do different things based on how it was called:
Function Do-Something {
[CmdletBinding()]
[Alias('DOIT')]
Param(
[string] $option1,
[string] $option2,
[int] $option3)
#$MyInvocation|select *|FL
If ($MyInvocation.InvocationName -eq 'DOIT'){write-host "You told me to do it...so i did!" -ForegroundColor Yellow}
Else {Write-Host "you were boring and said do something..." -ForegroundColor Green}
}
Creating a 'filter' is also an option, a lighter alternative to functions. It processes each element in the pipeline, assigning it the $_ automatic variable. So, for instance:
filter test { Write-Warning "$args $_" }
'foo','bar' | test 'This is'
returns:
WARNING: This is foo
WARNING: This is bar