Converting a shell script to PowerShell - powershell

After switching an application from Linux to Windows, I need to convert a shell script to a Windows equivalent. My choices were basically batch and PowerShell and I decided to give a shot to PowerShell.
For anyone interested, it's a local check for Check_MK to get information about SoftEther installed version and the number of sessions with performance data.
The initial shell script was as follow:
#!/bin/sh
cmd=$(/usr/local/vpnserver/vpncmd localhost:port /server /password:password /in:/usr/lib/check_mk_agent/local/vpncmd.txt)
version=$(echo "$cmd" | head -4 | tail -1)
sessions=$(echo "$cmd" | grep Sessions | awk '$1=$1' | cut -c21-22)
if [ -z "$version" ]; then
echo "3 VPN_Version - Can't get the information from vpncmd"
else
echo "0 VPN_Version - SoftEther VPN Server $version"
fi
if [ -z "$sessions" ]; then
echo "3 VPN_Sessions - Can't get the information from vpncmd"
else
echo "P VPN_Sessions sessions=$sessions;2;2"
fi
I basically got everything working except the 2 hardest lines of code:
cd "C:\Program Files\SoftEther VPN Server"
$cmd = vpncmd localhost:port /server /password:password /in:vpncmd.txt
$version=
$sessions=
if($version -eq $null) {
echo "3 VPN_Version - Can't get the information from vpncmd"
} else {
echo "0 VPN_Version - SoftEther VPN Server $version"
}
if($sessions -eq $null) {
echo "3 VPN_Sessions - Can't get the information from vpncmd"
} else {
echo "P VPN_Sessions sessions=$sessions;2;2"
}
I need help with going from the head, tail, grep, awk and cut one liners to whatever is equivalent in PowerShell. I read about Get-Content but I'm not sure if it's the most efficient way to do this and would like to prevent going from 1 line definition to 10 lines if that's possible to be as efficient in PowerShell.
Sample output of vpncmd's output: https://pastebin.com/J5FcHzHK

with the data being an array of lines & the word Version appearing multiple times in the actual source, the code needs to change a tad. in this version, it uses the way that -match works on an array to give the whole line as a result. that requires working on the output line to parse the desired data.
$Version = ($Vpncmd_Output -match '^Version \d{1,}\.\d{1,}' -split 'Version ' )[-1].Trim()
$SessionCount = [int]($Vpncmd_Output -match 'Number of Sessions\s+\|').Split('|')[-1].Trim()
$Version
$SessionCount
output ...
4.29 Build 9680 (English)
0
using the data in your PasteBin post, and presuming that is a multiline string, not an array of strings, this seems to work [grin] ...
$Vpncmd_Output -match '(?m)Number of Sessions\s+\|(?<Sessions>.*)'
$Matches.Sessions
# output = 0
$Vpncmd_Output -match '(?m)Version (?<Version>.+)'
$Matches.Version
# output = 4.29 Build 9680 (English)
i tried to combine the regex into one, but failed. [blush] the way i have it requires two passes, but it does work.

Related

Using subexpression with & in powershell

I am completely new to powershell.
I have a requirement to have a set of commands as a subexpression $() because I want the output of the command to be sent to Out-Host and without $() the if loops create an issue.
Now there might also be a possibility that the command has filenames with spaces and to handle that we append & before the file name in command.
Bacially, $(& file_name) | Out-Host fails saying & here is invalid.
How to go about using $() with &
Works ok for me. You'll need to supply a counterexample. In this case you couldn't pipe from "if" without $() or &{}.
$( If (1 -eq 1) { & echo hi } ) | out-host > file
hi
# no output until it's finished
$( If (1 -eq 1) { & 'c:\program files\internet explorer\iediagcmd' } ) |
out-host > file

How to simultaneously capture external command output and print it to the terminal

Can I pipe back from:
$OUTPUT = $(flutter build ios --release --no-codesign | tail -1)
I would like to get both the last line from the build AND show progress, something like
$OUTPUT = $(flutter build ios --release --no-codesign | out | tail -1)
where the hypothetical out utility would also send the output to the terminal.
Do you know how?
Note:
On Unix-like platforms, with external-program output, js2010's elegant tee /dev/tty solution is the simplest.
The solutions below, which also work on Windows, may be of interest for processing external-program output line by line in PowerShell.
A general solution that also works with the complex objects that PowerShell-native commands can output, requires different approaches:
In PowerShell (Core) 7+, use the following:
# PS v7+ only. Works on both Windows and Unix
... | Tee-Object ($IsWindows ? 'CON' : '/dev/tty')
In Windows PowerShell, where Tee-Object unfortunately doesn't support targeting CON, a proxy function that utilizes Out-Host is required - see this answer.
A PowerShell solution (given that the code in your question is PowerShell[1]):
I'm not sure how flutter reports its progress, but the following may work:
If everything goes to stdout:
$OUTPUT = flutter build ios --release --no-codesign | % {
Write-Host $_ # print to host (console)
$_ # send through pipeline
} | select -Last 1
Note: % is the built-in alias for ForEach-Object, and select the one for Select-Object.
If progress messages go to stderr:
$OUTPUT = flutter build ios --release --no-codesign 2>&1 | % {
Write-Host $_.ToString() # print to host (console)
if ($_ -is [string]) { $_ } # send only stdout through pipeline
} | select -Last 1
[1] As evidenced by the $ sigil in the variable name in the LHS of an assignment and the spaces around =
($OUTPUT = ), neither of which would work as intended in bash / POSIX-like shells.
I assume you mean bash because to my knowledge there is no tail in powershell.
Here's how you can see a command's output while still capturing it into a variable.
#!/bin/bash
# redirect the file descriptor 3 to 1 (stdout)
exec 3>&1
longRunningCmd="flutter build ios --release --no-codesign"
# use tee to copy the command's output to file descriptor 3 (stdout) while
# capturing 1 (stdout) into a variable
output=$(eval "$longRunningCmd" | tee >(cat - >&3) )
# last line of output
lastline=$(printf "%s" "$output" | tail -n 1)
echo "$lastline"
I use write-progress in the pipeline.
In order to keep readable pipeline, I wrote a function
function Write-PipedProgress{
<#
.SYNOPSIS
Insert this function in a pipeline to display progress bar to user
.EXAMPLE
$Result = (Get-250Items |
Write-PipedProgress -PropertyName Name -Activity "Audit services" -ExpectedCount 250 |
Process-ItemFurther)
>
[cmdletBinding()]
param(
[parameter(Mandatory=$true,ValueFromPipeline=$true)]
$Data,
[string]$PropertyName=$null,
[string]$Activity,
[int]$ExpectedCount=100
)
begin {
Write-Verbose "Starting $($MyInvocation.MyCommand)"
$ItemCounter = 0
}
process {
Write-Verbose "Start processing of $($MyInvocation.MyCommand)($Data)"
try {
$ItemCounter++
# (3) mitigate unexpected additional input volume"
if ($ItemCounter -lt $ExpectedCount) {
$StatusProperty = if ($propertyName) { $Data.$PropertyName } > > else { ""}
$StatusMessage = "Processing $ItemCounter th $StatusProperty"
$statusPercent = 100 * $ItemCounter / $ExpectedCount
Write-Progress -Activity $Activity -Status $StatusMessage -> > PercentComplete $statusPercent
} else {
Write-Progress -Activity $Activity -Status "taking longer than expected" -PercentComplete 99
}
# return input data to next element in pipe
$Data
} catch {
throw
}
finally {
Write-Verbose "Complete processing of $Data in > $($MyInvocation.MyCommand)"
}
}
end {
Write-Progress -Activity $Activity -Completed
Write-Verbose "Complete $($MyInvocation.MyCommand) - processed $ItemCounter items"
}
}
Hope this helps ;-)
I believe this would work, at least in osx or linux powershell (or even Windows Subsystem for Linux) that have these commands available. I tested it with "ls" instead of "flutter". Is there actually an "out" command?
$OUTPUT = bash -c 'flutter build ios --release --no-codesign | tee /dev/tty | tail -1'
Or, assuming tee isn't aliased to tee-object. Actually, tee-object would work too.
$OUTPUT = flutter build ios --release --no-codesign | tee /dev/tty | tail -1
It would work with the $( ) too, but you don't need it. In powershell, it's used to combine multiple pipelines.

In powershell, i want Ioop twice through a text file but in second loop i want to continue from end of first loop

I have a text file to process. Text file has some configuration data and some networking commands. I want to run all those network commands and redirect output in some log file.
At starting of text file,there are some configuration information like File-name and file location. This can be used for naming log file and location of log file. These line starts with some special characters like '<#:'. just to know that rest of the line is config data about file not the command to execute.
Now, before i want start executing networking commands (starts with some special characters like '<:'), first i want to read all configuration information about file i.e. file name, location, overwrite flag etc. Then i can run all commands and dump output into log file.
I used get-content iterator to loop over entire text file.
Question: Is there any way to start looping over file from a specific line again?
So that i can process config information first (loop till i first encounter command to execute, remember this line number), create log file and then keep running commands and redirect output to log file (loop from last remembered line number).
Config File looks like:
<#Result_File_Name:dump1.txt
<#Result_File_Location:C:\powershell
<:ping www.google.com
<:ipconfig
<:traceroute www.google.com
<:netsh interface ip show config
My powerhsell script looks like:
$content = Get-Content C:\powershell\config.txt
foreach ($line in $content)
{
if($line.StartsWith("<#Result_File_Name:")) #every time i am doing this, even for command line
{
$result_file_arr = $line.split(":")
$result_file_name = $result_file_arr[1]
Write-Host $result_file_name
}
#if($line.StartsWith("<#Result_File_Location:"))#every time i am doing this, even for command line
#{
# $result_file_arr = $line.split(":")
# $result_file_name = $result_file_arr[1]
#}
if( $conf_read_over =1)
{
break;
}
if ($line.StartsWith("<:")) #In this if block, i need to run all commands
{
$items = $line.split("<:")
#$items[0]
#invoke-expression $items[2] > $result_file_name
invoke-expression $items[2] > $result_file_name
}
}
If all the config information starts with <# just process those out first separately. Once that is done you can assume the rest are commands?
# Collect config lines and process
$config = $content | Where-Object{$_.StartsWith('<#')} | ForEach-Object{
$_.Trim("<#") -replace "\\","\\" -replace "^(.*?):(.*)" , '$1 = $2'
} | ConvertFrom-StringData
# Process all the lines that are command lines.
$content | Where-Object{!$_.StartsWith('<#') -and ![string]::IsNullOrEmpty($_)} | ForEach-Object{
Invoke-Expression $_.trimstart("<:")
}
I went a little over board with the config section. What I did was convert it into a hashtable. Now you will have your config options, as they were in file, accessible as an object.
$config
Name Value
---- -----
Result_File_Name dump1.txt
Result_File_Location C:\powershell
Small reconfiguration of your code, with some parts missing, would look like the following. You will most likely need to tweak this to your own needs.
# Collect config lines and process
$config = ($content | Where-Object{$_.StartsWith('<#')} | ForEach-Object{
$_.Trim("<#") -replace "\\","\\" -replace "^(.*?):(.*)" , '$1 = $2'
} | Out-String) | ConvertFrom-StringData
# Process all the lines that are command lines.
$content | Where-Object{!$_.StartsWith('<#') -and ![string]::IsNullOrEmpty($_)} | ForEach-Object{
Invoke-Expression $_.trimstart("<:") | Add-Content -Path $config.Result_File_Name
}
As per your comment you are still curious about your restart loop logic which was part of your original question. I will add this as a separate answer to that. I would still prefer my other approach.
# Use a flag to determine if we have already restarted. Assume False
$restarted = $false
$restartIndexPoint = 4
$restartIndex = 2
for($contentIndex = 0; $contentIndex -lt $content.Length; $contentIndex++){
Write-Host ("Line#{0} : {1}" -f $contentIndex, $content[$contentIndex])
# Check to see if we are on the $restartIndexPoint for the first time
if(!$restarted -and $contentIndex -eq $restartIndexPoint){
# Set the flag so this does not get repeated.
$restarted = $true
# Reset the index to repeat some steps over again.
$contentIndex = $restartIndex
}
}
Remember that array indexing is 0 based when you are setting your numbers. Line 20 is element 19 in the string array for example.
Inside the loop we run a check. If it passes we change the current index to something earlier. The write-host will just print the lines so you can see the "restart" portion. We need a flag to be set so that we are not running a infinite loop.

Piping from a variable instead of file in Powershell

Is ther any way in Powershell to pipe in from an virable instead of a file?
There are commands that I need to pipe into another command, right now that is done by first creating a file with the additional commands, and then piping that file into the original command. Code looks somehting like this now:
$val = "*some command*" + "`r`n" + "*some command*" + "`r`n" + "*some command*"
New-Item -name Commands.txt -type "file" -value $val
$command = #'
db2cmd.exe /C '*custom db2 command* < \Commands.txt > \Output.xml'
'#
Invoke-Expression -Command:$command
So instead of creating that file, can I somehow just pipe in $val insatead of Commands.txt?
Try this
$val = #("*some command*1","*some command2*","*some command3*")
$val | % { db2cmd.exe /C $_ > \Output.xml }
You should be able to pipe in from $val provided you use Write-Output or its shorthand echo, but it may also be worth trying passing the commands directly on the command line. Try this (and if it doesn't work I can delete the answer):
PS C:\> filter db2cmd() { $_ | db2cmd.exe ($args -replace '(\\*)"','$1$1\"') }
PS C:\> $val = #"
>> *custom db2 command*
>> *some command*
>> *some command*
>> *some command*
>> "#
>>
PS C:\> db2cmd /C $val > \Output.xml
What happens here is that Windows executables receive their command line from a single string. If you run them from cmd.exe you cannot pass newlines in the argument string, but Powershell doesn't have that restriction so with many programs you can actually pass multiple lines as a single argument. I don't know db2cmd.exe so it might not work here.
The strange bit of string replacement is to handle any double quotes in the arguments: Powershell doesn't quote them and the quoting rules expected by most exe files are a bit bizarre.
The only limitation here would be that $val must not exceed about 32,600 characters and cannot contain nulls. Any other restrictions (such as whether non-ascii unicode characters work) would depend on the application.
Failing that:
echo $val | db2cmd.exe /C '*custom db2 command*' > \Output.xml
may work, or you can use it in combination with the filter I defined at the top:
echo $val | db2cmd /C '*custom db2 command*' > \Output.xml

How to run interactive commands in another application window from 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)