Strange behavior of PSConsoleReadLine - powershell

Trying to seemingly generate text in Powershell console (result predetermined), did not like cls + Write-Host because of constant blinking, tried to use PSConsoleReadLine. Code below works in some situations, while it does not in others.
Code works when:
directly copy pasted into Powershell window
when stored in a script file and the file is called from Powershell window
Code does not work when:
launched in ISE (known issue)
right click on the script file / run with powershell
Tried to get around the error with Add-Type, but it ends with IOException and null reference instead. Is present in code below, but commented.
Tried to write a script in a way that relaunches itself in a new Powershell process to handle it, did not help.
Code itself (probably could reproduce the issue with a single line of code, but the guide says one line of a code is not enough):
function tryOne($oldExpression, $targetExpression, $currentPosition, $src)
{
While($targetExpression[$currentPosition] -cne $triedCharacter)
{
[Microsoft.PowerShell.PSConsoleReadLine]::RevertLine()#here problem
$triedCharacter = Get-Random -InputObject $src
$expression = $oldExpression + $triedCharacter
[Microsoft.PowerShell.PSConsoleReadLine]::Insert($expression)#here problem
Start-sleep -Milliseconds 50
}
Return $expression
}
cd $PSScriptRoot
#Add-Type -path "C:\Program Files\WindowsPowerShell\Modules\PSReadline\1.2\Microsoft.PowerShell.PSReadLine.dll"
#above helps with not found error, with it uncommented the errors at RevertLine and Insert are IOException and null reference
$targetExpression = "Not ideal"
$oldExpression = ""
$currentPosition = 0
#region AllowedCharacters
$src = New-Object System.Collections.ArrayList
for($i=65;$i-le 90;$i++)
{
$src.Add([char]$i) | Out-Null
}
for($i=97;$i-le 122;$i++)
{
$src.Add([char]$i) | Out-Null
}
for($i=0;$i-le 9;$i++)
{
$src.Add($i) | Out-Null
}
$src.Add(" ") | Out-Null #allowed characters in the target sentence - A-Z, a-z, 0-9, space
#endregion
While($currentPosition -lt ($targetExpression.length))
{
$oldExpression = tryOne $oldExpression $targetExpression $currentPosition $src
$currentPosition++;
}
I'd like to be able to launch the script via right click / run with Powershell.

Related

Powershell Global Variable usage as parameter to argument

$global:af_fp = "C:\Path\to\folder\"
Function function-name {
do things …
$global:af_fp = $global:af_fp + $variableFromDo_things + "_AF.csv"
}
function-name | ConvertTo-CSV -NoTypeInformation | Add-Content -Path $($af_fp)
Above is the generalized (and abbreviated) script contents for a powershell script.
Every time I run the script in this way, I get the following error:
Add-Content : Could not find a part of the path 'C:\Users\timeuser\Documents\'.
At C:\Users\timeuser\Documents\get_software.ps1:231 char:51
+ ... ware | ConvertTo-CSV -NoTypeInformation | Add-Content -Path $($af_fp)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : ObjectNotFound: (C:\Users\timeuser\Documents\:String) [Add-Content], DirectoryNotFoundException
+ FullyQualifiedErrorId : GetContentWriterDirectoryNotFoundError,Microsoft.PowerShell.Commands.AddContentCommand
When I run
Get-Variable -Scope global
after running the script and seeing the error, the variable af_fp contains exactly the information I am seeking for the file name, however, the error shows the variable contents ending in ':String'.
To confuse me even more, if I comment out the lines containing '$global:...' and re-run the same script, IT ACTUALL RUNS AND SAVES THE FILE USING THE LINE
function-name | ConvertTo-CSV -NoTypeInformation | Add-Content -Path $($af_fp)
AS INTENDED. Of course, I had to run the script and watch it error first, then re-run the script with the global variable declaration and update commented out for it to actually work. I want to run the script ONCE and still get the same results.
FYI, I am a complete noob to powershell, but very familiar with the concept of variable scope.....but why is this global not working when initially created and updated, but then work the second time around, when, as far as I can tell, the CONTENT AND SCOPE of the global remains the same...…. any assistance to finding a solution to this small issue would be greatly appreciated; I have tried sooooo may different methods from inquiries through here and on Google...…..
EDIT: not sure why this will matter, because the script ran before as intended when I explicitly typed the parameter for -Path as 'C:\path\to\file'. The ONLY CHANGES MADE to the original, working script (below) were my inclusion of the global variable declaration, the update to the contents of the global variable (near the end of the function), and the attempt to use the global variable as the parameter to -Path, that is why I omitted the script:
'''
$global:af_fp = "C:\Users\timeuser\Documents\"
Function Get-Software {
[OutputType('System.Software.Inventory')]
[Cmdletbinding()]
Param(
[Parameter(ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)]
[String[]]$Computername = $env:COMPUTERNAME
)
Begin {
}
Process {
ForEach ($Computer in $Computername) {
If (Test-Connection -ComputerName $Computer -Count 1 -Quiet) {
$Paths = #("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall", "SOFTWARE\\Wow6432node\\Microsoft\\Windows\\CurrentVersion\\Uninstall")
ForEach ($Path in $Paths) {
Write-Verbose "Checking Path: $Path"
# Create an instance of the Registry Object and open the HKLM base key
Try {
$reg = [microsoft.win32.registrykey]::OpenRemoteBaseKey('LocalMachine', $Computer, 'Registry64')
}
Catch {
Write-Error $_
Continue
}
# Drill down into the Uninstall key using the OpenSubKey Method
Try {
$regkey = $reg.OpenSubKey($Path)
# Retrieve an array of string that contain all the subkey names
$subkeys = $regkey.GetSubKeyNames()
# Open each Subkey and use GetValue Method to return the required values for each
ForEach ($key in $subkeys) {
Write-Verbose "Key: $Key"
$thisKey = $Path + "\\" + $key
Try {
$thisSubKey = $reg.OpenSubKey($thisKey)
# Prevent Objects with empty DisplayName
$DisplayName = $thisSubKey.getValue("DisplayName")
If ($DisplayName -AND $DisplayName -notmatch '^Update for|rollup|^Security Update|^Service Pack|^HotFix') {
$Date = $thisSubKey.GetValue('InstallDate')
If ($Date) {
Try {
$Date = [datetime]::ParseExact($Date, 'yyyyMMdd', $Null)
}
Catch {
Write-Warning "$($Computer): $_ <$($Date)>"
$Date = $Null
}
}
# Create New Object with empty Properties
$Publisher = Try {
$thisSubKey.GetValue('Publisher').Trim()
}
Catch {
$thisSubKey.GetValue('Publisher')
}
$Version = Try {
#Some weirdness with trailing [char]0 on some strings
$thisSubKey.GetValue('DisplayVersion').TrimEnd(([char[]](32, 0)))
}
Catch {
$thisSubKey.GetValue('DisplayVersion')
}
$UninstallString = Try {
$thisSubKey.GetValue('UninstallString').Trim()
}
Catch {
$thisSubKey.GetValue('UninstallString')
}
$InstallLocation = Try {
$thisSubKey.GetValue('InstallLocation').Trim()
}
Catch {
$thisSubKey.GetValue('InstallLocation')
}
$InstallSource = Try {
$thisSubKey.GetValue('InstallSource').Trim()
}
Catch {
$thisSubKey.GetValue('InstallSource')
}
$HelpLink = Try {
$thisSubKey.GetValue('HelpLink').Trim()
}
Catch {
$thisSubKey.GetValue('HelpLink')
}
$Object = [pscustomobject]#{
#Potential Candidate for AssetID in the TIME system
AssetID = $Computer
#String that contains word or word combinations for the product field of CPE WFN; may also contain the valid values necessary for update, edition, language, sw_edition, target_hw/sw fields as well.
cpeprodinfo = $DisplayName
cpeversion = $Version
InstallDate = $Date
cpevendor = $Publisher
UninstallString = $UninstallString
InstallLocation = $InstallLocation
InstallSource = $InstallSource
HelpLink = $thisSubKey.GetValue('HelpLink')
EstimatedSizeMB = [decimal]([math]::Round(($thisSubKey.GetValue('EstimatedSize') * 1024) / 1MB, 2))
}
$Object.pstypenames.insert(0, 'System.Software.Inventory')
Write-Output $Object
}
}
Catch {
Write-Warning "$Key : $_"
}
}
}
Catch { }
$reg.Close()
}
}
Else {
Write-Error "$($Computer): unable to reach remote system!"
}
$global:af_fp = $global:af_fp + $Computer + "_AF.csv"
}
}
}
Get-Software | ConvertTo-CSV -NoTypeInformation | Add-Content -Path $($af_fp)
'''
IGNORE FORMATTING PLEASE- HAD TROUBLE MAKING INDENTS CORRECTLY FROM COPY-PASTE AND RESTRICTIONS ON SITE FOR CODE BLOCKS.....
NOTE: the ONLY changes I made, that I am asking about, are the global declaration, the global variable update in the function, and the attempt to use the global variable for the -Path parameter....script otherwise runs and will even run WITH THE LAST LINE AS IS if I ran it and errored the first time.....not sure how the addition script will help in any way, shape, or form!
With a little effort, Nasir's solution worked! HOWEVER, I ran across a sample file that had a way of adding to a parameter that inspired me to make a change to my ORIGINAL, that also worked: remove global variable from script entirely and add this code the very end:
$file_suffix = '_AF.csv'
Get-Software | ConvertTo-CSV -NoTypeInformation | Add-Content -Path $env:COMPUTERNAME$file_suffix
In this way, I was able to accomplish exactly what I was setting out to do! Thanks Nasir for your response as well! I was able to also make that work as intended!
Global variables are generally frowned upon, since they often lead to poor scripts, with hard to debug issues.
It seems like your function returns some stuff, which you need to write to a file, the name of which is also generated by the same function. You can try something like this:
function function-name {
param($PathPrefix)
#do things
[pscustomobject]#{"DoThings_data" = $somevariablefromDoThings; "Filename" = "$($PathPrefix)$($variableFromDo_Things)_AF.csv"}
}
function-name -PathPrefix "C:\Path\to\folder\" | Foreach-Object { $_.DoThings_data | Export-Csv -Path $_.Filename -NoTypeInformation }
Or just have your function write the CSV data out and then return the data if you need to further process it outside the function.
Edit: this is just me extrapolating from partial code you have provided. To Lee_Dailey's point, yes, please provide more details.

Sublime Text causing PowerShell script to fail

I have been using SublimeText for a while now and haven't had any issues until recently. Every now and again if I write a PowerShell script in sublime text, the script will fail to run with generic errors such as:
Missing closing '}' in statement block or type definition.
At C:\Users\user\OneDrive\Scripts\~Projects\TRIS-AzureMigration\TRIS-AzureMigration.ps1:7 char:25
+ function Get-SQLInstall {
These errors only show if I call the script from PowerShell. If I copy the script to the console it works. If I run it in ISE it works. I can also run it if I copy the code to notepad and then save it as a .ps1 from there, this tells me that it is something to do with sublime text.
I have had a look around and found a few examples:
My script runs in powershell ISE but not powershell console? :: solution was to copy to a new file, which i dont want to have to do every time.
Any help on this would be much appreciated.
The code block of code that for the error above:
function Get-SQLInstall {
param ( [parameter(Mandatory=$false)]$GetMax = $true)
$instances = #();
$inst = (get-itemproperty 'HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server').InstalledInstances;
foreach ($i in $inst)
{
$p = (Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\Instance Names\SQL').$i;
[version]$v = ((Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\$p\Setup").Edition).Major;
$instances += New-Object –TypeName PSObject -Property #{
Edition = ((Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\$p\Setup").Edition);
version = (Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\$p\Setup").Version;
SQLPath = (Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\$p\Setup").SqlPath;
SQLFullPath = ((Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\$p\Setup").SqlPath) + "\" + [string]$v.major + "0";
}
}
if ($GetMax -eq $true) {
## Get latest SQL version
$max = ($instances | measure-object -Property version -maximum).maximum;
return #($instances | ? { $_.version -eq $max})[0];
}
else {
return $instances;
}
}

Locate MSTest.exe using powershell

I'm in the process of automating my .Net solution build to be completely in PowerShell. I want to locate MSTest.exe using PowerShell.
I used the following script to locate MSBuild.exe and I hope that I can have something similar to locate MSTest.exe
$msBuildQueryResult = reg.exe query "HKLM\SOFTWARE\Microsoft\MSBuild\ToolsVersions\4.0" /v MSBuildToolsPath
$msBuildQueryResult = $msBuildQueryResult[2]
$msBuildQueryResult = $msBuildQueryResult.Split(" ")
$msBuildLocation = $msBuildQueryResult[12] + "MSBuild.exe"
Any directions ?
The following works with Visual Studio 2010 and higher[1]:
# Get the tools folder location:
# Option A: Target the *highest version installed*:
$vsToolsDir = (
Get-Item env:VS*COMNTOOLS | Sort-Object {[int]($_.Name -replace '[^\d]')}
)[-1].Value
# Option B: Target a *specific version*; e.g., Visual Studio 2010,
# internally known as version 10.0.
# (See https://en.wikipedia.org/wiki/Microsoft_Visual_Studio#History)
$vsToolsDir = $env:VS100COMNTOOLS
# Now locate msbuild.exe in the "IDE" sibling folder.
$msTestExe = Convert-Path -EA Stop (Join-Path $vsToolsDir '..\IDE\MSTest.exe')
The approach is based on this answer and is generalized and adapted to PowerShell.
It is based on system environment variables VS*COMNTOOLS, created by Visual Studio setup, where * represents the VS version number (e.g., 100 for VS 2010).
Re option A: Sort-Object is used to ensure that the most recent Visual Studio installation is targeted, should multiple ones be installed side by side:
The script block used for sorting first extracts only the embedded version number from the variable name ($_.Name -replace '[^\d]'; e.g., 100 from VS100COMNTOOLS) and converts the result to an integer ([int]); [-1] then extracts the last element from the sorted array - i.e., the variable object whose names has the highest embedded version number - and accesses its value (.Value).
The IDE subfolder, in which MSTest.exe is located is a sibling folder of the tools folder that VS*COMNTOOLS points to.
If MSTest.exe is NOT in the expected location, Convert-Path will throw a non-terminating error by default; adding -EA Stop (short for: -ErrorAction Stop) ensures that the script is aborted instead.
[1]
- I've tried up to Visual Studio 2015; do let me know whether or not it works on higher versions.
- Potentially also works with VS 2008.
Perhaps you are wanting something like this?
$regPath = "HKLM:\SOFTWARE\Microsoft\MSBuild\ToolsVersions\4.0"
$regValueName = "MSBuildToolsPath"
$msBuildFilename = "MSBUild.exe"
if ( Test-Path $regPath ) {
$toolsPath = (Get-ItemProperty $regPath).$regValueName
if ( $toolsPath ) {
$msBuild = Join-Path $toolsPath $msBuildFilename
if ( -not (Test-Path $msBuild -PathType Leaf) ) {
Write-Error "File not found - '$msBuild'"
}
}
}
# Full path and filename of MSBuild.exe in $msBuild variable
My way of getting mstest path.
GetMSTestPath function is main function which you call and then if first GetMsTestPathFromVswhere function will find something it returns path if not your will be making a long search for mstest.exe. Usually, it takes approximately 10 sec. I know that this is not the best but at least it is something when you struggle to find mstest.exe. Hope it will be helpful for somebody. :)))
function GetMSTestPath
{
function GetTime()
{
$time_now = Get-Date -format "HH:mm:ss"
return $time_now;
}
function GetMsTestPathFromVswhere {
$vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe"
$path = & $vswhere -latest -prerelease -products * -requires Microsoft.Component.MSBuild -property installationPath
#write-host $path
if ($path) {
$tool = join-path $path 'Common7\IDE\MSTest.exe'
if (test-path $tool) {
return $tool
}
return ""
}
}
function SeachForMsTestPath
{
write-host $(GetTime)
$path = Get-ChildItem C:\ -Filter MSTest.exe -Recurse -ErrorAction Ignore | ? { $_.VersionInfo.FileDescription -eq 'Test Execution Command Line Tool' } | Select -First 1
write-host $(GetTime)
return $path
}
$msTestExePath = GetMsTestPathFromVswhere
if ([string]::IsNullOrEmpty($msTestExePath))
{
$msTestExePath = SeachForMsTestPath;
if ([string]::IsNullOrEmpty($msTestExePath))
{
Write-host "MsTest path is not found. Exiting with error"
Exit -1
}
}
return $msTestExePath;
}
Thanks #Bill_Stewart , I used your comments to write this working function:
function Get-MSTest-Location {
$msTests = #()
$searchResults = Get-ChildItem C:\* -Filter MSTest.exe -Recurse -ErrorAction Ignore
foreach($searchResult in $searchResults) {
try{
if(($searchResult.VersionInfo -ne $null) -and ($searchResult.VersionInfo.FileDescription -eq "Test Execution Command Line Tool"))
{ $msTests = $msTests + $searchResult.FullName }
}
catch{}
}
if($msTests.Length -eq 0)
{return "MSTest not found."}
return $msTests[0]
}

Copying to the clipboard in PowerShell without a new line

Is there a way to remove a new line from out-clipboard or clip in PowerShell?
I'm using this code to copy current path to clipboard:
function cl() {
(Get-Location).ToString() | clip
}
And every time I use this, a new line is added to the copied text. It's frustrating, because then I can't paste it in the CLI, like I would with text that is copied from elsewhere. Because a new line makes a command on the CLI automatically executed.
Example: I'm in C:\Users and type cl, and then I use Alt + SPACE + E + P to pass the text, the command is executed, and I can't type any more. But when text is passed without a new line nothing is executed, and I can continue to type.
Use the Set-Clipboard function:
(get-location).ToString()|Set-Clipboard
Add-Type -Assembly PresentationCore
$clipText = (get-location).ToString() | Out-String -Stream
[Windows.Clipboard]::SetText($clipText)
As pointed out by #PetSerAl in the comments, the newline is added by PowerShell when the string object is sent through the pipeline. The stringified output of Get-Location does not have that trailing newline:
PS C:\> $v = (Get-Location).ToString()
PS C:\> "-$v-"
-C:\-
You could try something like this:
Add-Type -AssemblyName System.Windows.Forms
$tb = New-Object Windows.Forms.TextBox
$tb.MultiLine = $true
$tb.Text = (Get-Location).ToString()
$tb.SelectAll()
$tb.Copy()
Ending the string with a null byte will take care of it. Useful for powershell core, which doesn't contain Set-Clipboard
function set-clipboard{
param(
[parameter(position=0,mandatory=$true,ValueFromPipeline=$true)]$Text
)
begin{
$data = [system.text.stringbuilder]::new()
}
process{
if ($text){
[void]$data.appendline($text)
}
}
end{
if ($data){
$data.tostring().trimend([environment]::newline) + [convert]::tochar(0) | clip.exe
}
}
}
"asdf" | set-clipboard

Why functions are not available locally until after first ran?

I have two questions here, why does the following function in a script not recognized when I run the script:
Script:
$pathN = Select-Folder
Write-Host "Path " $pathN
function Select-Folder($message='Select a folder', $path = 0) {
$object = New-Object -comObject Shell.Application
$folder = $object.BrowseForFolder(0, $message, 0, $path)
if ($folder -ne $null) {
$folder.self.Path
}
}
I get error:
The term 'Select-Folder' is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of the name, or if a path was included, verify that the path is correct and try aga
in.
But if I load and run it in the Windows Powershell ISE, it will give me the error the first time, and then act like it has "registered" the function and work after that.
And in case it is a procedural issue, I have tried listing the function at the top with no better luck.
Note I have tried simple functions like:
Write-host "Say "
Hello
function Hello {
Write-host "hello"
}
With the same exact results/error, it complains that Hello is not function....
Also, it still won't every work just running the script in powershell (only in ISE after the first initial attempt).
You need to declare your Select-Folder function before you try to use it. The script is read from top to bottom, so on the first pass when you try to use Select-Folder it has no idea what that means.
When you load it into the Powershell ISE it'll find out what Select-Folder means on the first run, and it'll still know that the 2nd time you try to run it (so you won't get the error then).
So if you change your code to:
function Select-Folder($message='Select a folder', $path = 0) {
$object = New-Object -comObject Shell.Application
$folder = $object.BrowseForFolder(0, $message, 0, $path)
if ($folder -ne $null) {
$folder.self.Path
}
}
$pathN = Select-Folder
Write-Host "Path " $pathN
that should work each time you run it.