How to run interactive commands in another application window from powershell - powershell

I have another command line program which I invoke from my powershell script and would like to run some interactive commands in that window once it is opened from power shell.
In other words - I do a Invoke-Item $link_to_app which opens up the interactive command line for that application and now I would like to use the application specific commands from within powershell scripts.
e.g. app.exe -help to invoke the help command of the app.exe.
Any pointers would help. Thanks!

Try this:
$app = 'app.exe -help'
Invoke-Expression $app
Tested with this and it worked as expected:
$pingTest = 'ping -n 8 127.0.0.1'
Invoke-Expression $pingTest
From your expanded explanation you appear to want to run 2 commands within the same command prompt. This is possible, however, I'm not sure it will work in your scenario. For example:
test1.bat:
echo "hello!"
test2.bat: echo "goodbye!"
$batchTest = "test1.bat && test2.bat"
cmd /c $batchTest
output:
D:\Test>echo "hello!"
"hello!"
D:\Test>echo "goodbye!"
"goodbye!"
Hope this helps.

I'm not sure, but I think what you want is the ability to have a script send input to and receive output from another program, where the other program has "state" that your script needs to be able to interact with. Below is an example of a script that drives CMD.EXE. CMD has state, such as current working directory and environment variables.
Note, that you could do what the other answerer suggested and just start the program, give all the input on the command line, and then do what you need to with the output. However for CMD if you need to make decisions based on the output, and then give CMD more input based on the previous output, you'd have to save and restore the environment and current working directories between each time you executed CMD. The approach below doesn't require that.
However the approach below does have several caveats. First it is dependent on the PS "host". It works (for me) on the command line PS, but not in ISE. This dependency is due to using the Raw host interface to determine if a key is available. Second it is timing dependent, based on the behavior of CMD (or whatever you use instead). You'll see a few sleep commands in the script. I had to experiment a whole lot to get this script to show CMD's output for a particular sub-command when that command was entered, versus CMD giving the output of previous commands after another command was entered. Comment out the sleeps to see what I mean. Third it is easy to hang Powershell. Killing CMD in task manager gets you out of the hung state, which I had to do many times.
You'll see that I added a couple of commands that the script deals with specially. This is to demonstrate that input to command can come from a PS script (versus input from the keyboard).
$global:ver++
if ($ExecutionContext.Host.name -match "ISE Host$") {
write-warning "This script relies on RawUI functionality not implemented in ISE"
return
}
$in = $null
$flExiting = $false
$doDebug = $false
function dot-debug {param($color)
if ($doDebug) {
write-host "." -NoNewline -ForegroundColor $color
}
}
#function dot-debug {param($color) }
$procInfo = new diagnostics.processstartinfo
$procInfo.RedirectStandardOutput=1
$procInfo.RedirectStandardInput=1
$procInfo.RedirectStandardError=1
$procInfo.FileName="cmd.exe"
$procInfo.UseShellExecute=0
$p=[diagnostics.process]::start($procInfo)
$outBuf = new char[] 4096
write-host "Version $ver"
sleep -Milliseconds 300
do {
dot-debug red
# This while loop determines whether input is available from either
# CMD's standard output or from the user typing. You don't want to
# get stuck waiting for input from either one if it doesn't really have input.
:WaitIO while ($true) {
if (-1 -ne $p.StandardOutput.peek()) {
dot-debug yellow
$cnt = $p.StandardOutput.read( $outBuf, 0, 4096)
} else {
dot-debug Gray
if ($host.ui.rawui.KeyAvailable -or $flExiting) {break}
}
$str = $outBuf[0..($cnt-1)] -join ""
write-host "$str" -NoNewline
while (-1 -eq ($rc =$p.StandardOutput.peek())) {
if ($host.ui.rawui.KeyAvailable -or $flExiting) {
break WaitIO
}
dot-debug DarkGray
sleep -milli 200
}
dot-debug cyan
}
dot-debug green
# read-host echoes input, so commands get echoed twice (cmd also echoes)
#
# $host.ui.rawui.ReadKey("NoEcho, IncludeKeyDown") doesn't work on ISE,
# but does work in the PS cli shell
if ($in -ne "exit") {$in = read-host}
if ($in -eq "td") { # toggle debug
$doDebug = -not $doDebug
$p.StandardInput.WriteLine( "echo debug toggled")
sleep -milli 300
continue
}
if ($in -eq "xxx") {
# Example of script driven output being sent to CMD
$p.StandardInput.WriteLine( "echo This is a very long command that I do not want to have to type in everytime I want to use it")
# You have to give CMD enough time to process command before you read stdout,
# otherwise stdout gets "stuck" until the next time you write to stdin
sleep -milli 1
continue
}
if ($in -eq "exit") {
$flExiting = $true
$p.StandardInput.WriteLine($in)
continue
}
foreach ($char in [char[]]$in) {
$p.StandardInput.Write($char)
}
$p.StandardInput.Write("`n")
sleep -milli 1
} until ($p.StandardOutput.EndOfStream)

Related

How can I check if the PowerShell profile script is running from an SSH session?

I'm trying to work around a bug in Win32-OpenSSH, where -NoProfile -NoLogo is not respected when using pwsh.exe (Core) and logging in remotely via SSH/SCP. One way (of several) I tried, was to add the following in the very beginning of my Microsoft.PowerShell_profile.ps1 profile.
function IsInteractive {
$non_interactive = '-command', '-c', '-encodedcommand', '-e', '-ec', '-file', '-f'
-not ([Environment]::GetCommandLineArgs() | Where-Object -FilterScript {$PSItem -in $non_interactive})
}
# No point of running this script if not interactive
if (-not (IsInteractive)) {
exit
}
...
However, this didn't work with a remote SSH, because when using [Environment]::GetCommandLineArgs() with pwsh.exe, all you get back is:
C:\Program Files\PowerShell\6\pwsh.dll
regardless whether or not you are in an interactive session.
Another way I tried, was to scan through the process tree and look for the sshd parent, but that was also inconclusive, since it may run in another thread where sshd is not found as a parent.
So then I tried looking for other things. For example conhost. But on one machine conhost starts before pwsh, whereas on another machine, it starts after...then you need to scan up the tree and maybe find an explorer instance, in which case it is just a positive that the previous process is interactive, but not a definite non-interactive current process session.
function showit() {
$isInter = 'conhost','explorer','wininit','Idle',
$noInter = 'sshd','pwsh','powershell'
$CPID = ((Get-Process -Id $PID).Id)
for (;;) {
$PNAME = ((Get-Process -Id $CPID).Name)
Write-Host ("Process: {0,6} {1} " -f $CPID, $PNAME) -fore Red -NoNewline
$CPID = try { ((gwmi win32_process -Filter "processid='$CPID'").ParentProcessId) } catch { ((Get-Process -Id $CPID).Parent.Id) }
if ($PNAME -eq "conhost") {
Write-Host ": interactive" -fore Cyan
break;
}
if ( ($PNAME -eq "explorer") -or ($PNAME -eq "init") -or ($PNAME -eq "sshd") ) {
# Write-Host ": non-interactive" -fore Cyan
break;
}
""
}
}
How can I check if the profile script is running from within a remote SSH session?
Why am I doing this? Because I want to disable the script from running automatically through SSH/SCP/SFTP, while still being able to run it manually (still over SSH.) In Bash this is a trivial one-liner.
Some related (but unhelpful) answers:
Powershell test for noninteractive mode
How to check if a Powershell script is running remotely

Powershell: How to run an external command and checks its success in a single line?

In bash, I can do this:
if this_command >/dev/null 2>&1; then
ANSWER="this_command"
elif that_command >/dev/null 2>&1; then
ANSWER="that_command"
else
ANSWER="neither command"
fi
but in Powershell, I have to do this:
this_command >/dev/null 2>&1
if ($?) {
ANSWER="this_command"
} else {
that_command >/dev/null 2>&1
if ($?) {
ANSWER="that_command"
} else {
ANSWER="neither command"
}
}
or something similar with ($LASTEXITCODE -eq 0). How do I make the Powershell look like bash? I'm not a Powershell expert, but I cannot believe that it doesn't not provide some means of running a command and checking its return code in a single statement in a way that could be used in an if-elseif-else statement. This statement would be increasingly difficult to read with every external command that must be tested in this way.
For PowerShell cmdlets you can do the exact same thing you do in bash. You don't even need to do individual assignments in each branch. Just output what you want to assign and collect the output of the entire conditional in a variable.
$ANSWER = if (Do-Something >$null 2>&1) {
'this_command'
} elseif (Do-Other >$null 2>&1) {
'that_command'
} else {
'neither command'
}
For external commands it's slightly different, because PowerShell would evaluate the command output, not the exit code/status (with empty output evaluating to "false"). But you can run the command in a subexpression and output the status to get the desired result.
$ANSWER = if ($(this_command >$null 2>&1; $?)) {
'this_command'
} elseif ($(that_command >$null 2>&1; $?)) {
'that_command'
} else {
'neither command'
}
Note that you must use a subexpression ($(...)), not a grouping expression ((...)), because you effectively need to run 2 commands in a row (run external command, then output status), which the latter doesn't support.
You can't do it inline like in bash, but you can one-line this with two statements on one line, separated by a semi-colon ;:
MyProgram.exe -param1 -param2 -etc *>$null; if( $LASTEXITCODE -eq 0 ) {
# Success code here
} else {
# Fail code here
}
Also, you can't use $? with commands, only Powershell cmdlets, which is why we check that $LASTEXITCODE -eq 0 instead of using $?.
Note that you CAN evaluate cmdlets inline, just not external commands. For example:
if( Test-Connection stackoverflow.com ){
"success"
} else {
"fail"
}
Another approach is to have it output an empty string if it's false:
if (echo hi | findstr there) { 'yes' }
if (echo hi | findstr hi) { 'yes' }
yes
PowerShell's native error handling works completely differently from the exit-code-based error signaling performed by external programs, and, unfortunately, error handling with external programs in PowerShell is cumbersome, requiring explicit checks of the automatic $? or $LASTEXITCODE variables.
PowerShell [Core]:
introduced support for Bash-style && and || pipeline-chain operators in v7 - see this answer.
but this will not also enable use of external-program calls in if statements, because there PowerShell will continue to operate on the output from commands, not on their implied success status / exit code; see this answer for more information.
Solutions:
PowerShell [Core] 7.0+:
$ANSWER = this_command *>$null && "this_command" ||
(that_command *>$null && "that_command" || "neither command")
Note:
If this_command or that_command don't exist (can't be found), a statement-terminating error occurs, i.e. the statement fails as a whole.
Note the need to enclose the 2nd chain in (...) so that && "that_command" doesn't also kick in when this_command succeeds.
*>$null is used to conveniently silence all streams with a single redirection.
Unlike an if-based solution, this technique passes (non-suppressed) output from the external programs through.
Windows PowerShell and PowerShell Core 6.x:
If the external-program calls produce no output or you actively want to suppress their output, as in your question:
See the $(...)-based technique in Ansgar Wiechers' helpful answer.
If you do want the external programs' output:
An aux. dummy do loop allows for a fairly "low-noise" solution:
$ANSWER = do {
this_command # Note: No output suppression
if ($?) { "this_command"; break }
that_command # Note: No output suppression
if ($?) { "that_command"; break }
"neither command"
} while ($false)

Switch statement not processing

I'm attempting to use a switch statement to control flow in my PowerShell script, but the switch isn't being processed at all. If I run it interactively (highlight + F8 in ISE), it works without a hitch. I validate the argument, and have tested it thoroughly. If(){} statements process properly. Is there some kind of weird bug with switch statements? Even the Default argument isn't being triggered.
[CmdletBinding()]
Param(
[Parameter(Mandatory=$True)]
[ValidateSet('Install','Uninstall','Verify','Enable','Disable')]
[String]$Switch
)
**FUNCTIONS**
[Int]$R = 324
Start-Transcript 'Path\ESD.log' -Append -Force
Switch ($Switch)
{
"VERIFY" { $R = Verify; Break }
"INSTALL" { $R = Install; Break }
"UNINSTALL" { $R = Uninstall; Break }
"ENABLE" { $R = Enable; Break }
"DISABLE" { $R = Disable; Break }
Default { Write-Host "Unable to match: $Switch" -BackgroundColor Red }
}
Write-Host "Exiting with: $R"
Stop-Transcript
EXIT $R
Output:
PS Path\EnterpriseSiteDiscovery> .\ESD verify
Transcript started, output file is Path\ESD.log
Exiting with: 324
Transcript stopped, output file is Path\ESD.log
PS Path\EnterpriseSiteDiscovery> $LASTEXITCODE
324
Content of log:
**********************
Windows PowerShell transcript start
Start time: 20170705154435
**SYSTEMINFO**
**********************
Transcript started, output file is Path\ESD.log
Exiting with: 324
**********************
Windows PowerShell transcript end
End time: 20170705154436
**********************
It turns out when you are inside a Switch construct, the variable
$Switch value is redefined (presumably by the switch construct itself)
as an empty variable of type System.Collections.IEnumerator. The value
is set to $null. This won’t be a problem if you’re not using a
variable with the name $Switch. Unfortunately I was because I was
working with a set to Virtual Switches so $Switch seemed like a fair
choice of variable name.
https://dscottraynsford.wordpress.com/2015/09/08/powershell-dont-of-the-week-switch-in-a-swtich/

How to send keystroke to executable?

How can I enter a keystroke programmatically through a PowerShell script?
Write-Host -ForegroundColor Green 'Loading...'
Function EnterKey {
[Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms')
#Where I want to get "|" keystroke programmatically
[System.Windows.Forms.SendKeys]::SendWait("{|}")
}
Function StartUp {
Write-Host "Environment"
$exe = ([IO.Path]::Combine("D:\7ZipApp\7ZipApp\7ZipApp\bin\Debug","7ZipApp.exe"))
& $exe 3 # argument 3 = 'Run local Sync'
EnterKey
Read-Host -Prompt $exe.ToString()
}
StartUp
Write-Host -ForegroundColor Green 'Loading...'
function StartUp {
Write-Host 'Environment'
$exe = Join-Path "D:\7ZipApp\7ZipApp\7ZipApp\bin\Debug" "7ZipApp.exe"
#& $exe 3 # argument 3 = 'Run local Sync'
start $exe -ArgumentList 3
Write-Host 'Type {|} to continue'
while ((Read-Host) -ne '{|}') {}
Read-Host -Prompt $exe.ToString()
}
StartUp
I have to go with the crowd here (from the comments):
I would abandon your approach. Too problematic.
My question was why you want to do it
The correct solution, then, is to get the author of 7zipapp.exe to fix the program so it stops doing that or to add a command-line parameter that prevents this behavior.
That said, if you want a total hack, and this program only takes ONE input, at the end presumably, then the below appears to work. I would use sparingly, perhaps never use it, but rather get the program fixed, but in my testing, this worked.
PowerShell:
$exe = 'C:\ConsoleApplication2\bin\Debug\ConsoleApplication2.exe'
'\r\n' | & $exe
Annoying C# program:
using static System.Console;
namespace ConsoleApplication2
{
class Program
{
static void Main(string[] args)
{
WriteLine("I will force you to hit Enter to exit.");
ReadLine();
}
}
}

echo in while loop get's added to my return value

I wasn't sure how to describe this problem in the title so here goes.
I call a function from a script in another script. In that function i have a while loop that basically keeps looping through a set of ip's and looks up their hostname. when the while loop times out or we have all the host names.
it returns the hostnames.
My problem is that the return value contains every single Write-Host i'm doing in that function.
i know it's because Write-Host puts stuff on the pipeline and the return just returns whatever it has.
How do i go about fixing this?
The entire script i run get's logged in a log file which is why i want to have some verbose logging.
| out-null on write-host fixes the issue but it doesn't print the write-host values in the script.
in main.psm1 i have a function like so:
$nodes = #("ip1", "ip2", "ip3", "ip4")
$nodesnames = DoStuff -nodes $nodes
then in functions.psm1 i have functions like:
Function DoStuff
{
param($nodes)
$timeout = 300
$timetaken = 0
$sleepseconds = 5
$nodenames = #("$env:COMPUTERNAME")
while(($nodenames.count -lt $nodes.count) -and ($timetaken -lt $timeout))
{
try
{
Write-Host "Stuff"
foreach($node in $nodes)
{
$nodename = SuperawesomeFunction $node
Write-Host "$nodename"
if($nodenames -notcontains $nodename)
{
$nodenames += #($nodename)
}
}
}
catch
{
Write-Host "DoStuff Failed because $_"
}
Start-Sleep $sleepseconds
$timetaken += $sleepseconds
}
return $nodenames
}
Function SuperawesomeFunction
{
param($node)
$nodename = [System.Net.Dns]::GetHostEntry("$node")
return $nodename
}
Thanks.
So the answer is, your function is working like it is by design. In PowerShell a function will return output in general to the pipeline, unless specifically directed otherwise.
You used Echo before, which is an alias of Write-Output, and output is passed down the pipe as I mentioned before. As such it would be collected along with the returned $nodenames array.
Replacing Echo with Write-Host changes everything because Write-Host specifically tells PowerShell to send the information to the host application (usually the PowerShell Console or PowerShell ISE).
How do you avoid this? You could add a parameter specifying a path for a logfile, and have your function update the logfile directly, and only output the relevant data.
Or you can make an object with a pair of properties that gets passed back down the pipe which has the DNS results in one property, and the errors in another.
You could use Write-Error in the function, and set it up as an advanced function to support -errorvariable and capture the errors in a separate variable. To be honest, I'm not sure how to do that, I've never done it, but I'm 90% sure that it can be done.