Ask user for file path to save - powershell

This is what I was going for
$x = Get-Process
$y = Get-Date -Format yyyy-MM-dd_hh.mmtt
$SelPath = Read-Host -Prompt "Choose a location to save the file?"
$Path = $SelPath + 'Running Process' + ' ' + $FixedDate + '.txt'
$x | Out-File $Path

"Read-Host". For example:
$folder = Read-Host "Folder location"

While the links provided in the comments show the use of the FolderBrowserDialog, all of these fail to show it as a Topmost form and also do not dispose of the form when done.
Here's two functions that ensure the dialog gets displayed on top.
The first one uses the Shell.Application Com object:
# Show an Open Folder Dialog and return the directory selected by the user.
function Get-FolderName {
[CmdletBinding()]
param (
[Parameter(Mandatory=$false, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true, Position=0)]
[string]$Message = "Select a directory.",
[string]$InitialDirectory = [System.Environment+SpecialFolder]::MyDocuments,
[switch]$ShowNewFolderButton
)
$browserForFolderOptions = 0x00000041 # BIF_RETURNONLYFSDIRS -bor BIF_NEWDIALOGSTYLE
if (!$ShowNewFolderButton) { $browserForFolderOptions += 0x00000200 } # BIF_NONEWFOLDERBUTTON
$browser = New-Object -ComObject Shell.Application
# To make the dialog topmost, you need to supply the Window handle of the current process
[intPtr]$handle = [System.Diagnostics.Process]::GetCurrentProcess().MainWindowHandle
# see: https://msdn.microsoft.com/en-us/library/windows/desktop/bb773205(v=vs.85).aspx
$folder = $browser.BrowseForFolder($handle, $Message, $browserForFolderOptions, $InitialDirectory)
$result = $null
if ($folder) {
$result = $folder.Self.Path
}
# Release and remove the used Com object from memory
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($browser) | Out-Null
[System.GC]::Collect()
[System.GC]::WaitForPendingFinalizers()
return $result
}
$folder = Get-FolderName
if ($folder) { Write-Host "You selected the directory: $folder" }
else { "You did not select a directory." }
The second one uses a bit of C# code for the 'Topmost' feature and System.Windows.Forms
function Get-FolderName {
[CmdletBinding()]
param (
[Parameter(Mandatory=$false, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true, Position=0)]
[string]$Message = "Please select a directory.",
[System.Environment+SpecialFolder]$InitialDirectory = [System.Environment+SpecialFolder]::MyDocuments,
[switch]$ShowNewFolderButton
)
# To ensure the dialog window shows in the foreground, you need to get a Window Handle from the owner process.
# This handle must implement System.Windows.Forms.IWin32Window
# Create a wrapper class that implements IWin32Window.
# The IWin32Window interface contains only a single property that must be implemented to expose the underlying handle.
$code = #"
using System;
using System.Windows.Forms;
public class Win32Window : IWin32Window
{
public Win32Window(IntPtr handle)
{
Handle = handle;
}
public IntPtr Handle { get; private set; }
}
"#
if (-not ([System.Management.Automation.PSTypeName]'Win32Window').Type) {
Add-Type -TypeDefinition $code -ReferencedAssemblies System.Windows.Forms.dll -Language CSharp
}
# Get the window handle from the current process
# $owner = New-Object Win32Window -ArgumentList ([System.Diagnostics.Process]::GetCurrentProcess().MainWindowHandle)
# Or write like this:
$owner = [Win32Window]::new([System.Diagnostics.Process]::GetCurrentProcess().MainWindowHandle)
# Or use the the window handle from the desktop
# $owner = New-Object Win32Window -ArgumentList (Get-Process -Name explorer).MainWindowHandle
# Or write like this:
# $owner = [Win32Window]::new((Get-Process -Name explorer).MainWindowHandle)
Add-Type -AssemblyName System.Windows.Forms
$dialog = New-Object System.Windows.Forms.FolderBrowserDialog
$dialog.Description = $Message
$dialog.RootFolder = $InitialDirectory
# $dialog.SelectedPath = '' # a folder within the RootFolder to pre-select
$dialog.ShowNewFolderButton = if ($ShowNewFolderButton) { $true } else { $false }
$result = $null
if ($dialog.ShowDialog($owner).ToString() -eq 'OK') {
$result = $dialog.SelectedPath
}
# clear the FolderBrowserDialog from memory
$dialog.Dispose()
return $result
}
$folder = Get-FolderName -InitialDirectory MyPictures
if ($folder) { Write-Host "You selected the directory: $folder" }
else { "You did not select a directory." }
Hope that helps
p.s. See Environment.SpecialFolder Enum for the [System.Environment+SpecialFolder] enumeration values

Related

How to programatically set Shortcuts TargetPath to a website?

I want to use powershell to modify the TargetPath of a shortcut to open a website
I found the below script that almost works
function Set-Shortcut {
param(
[Parameter(ValueFromPipelineByPropertyName=$true)]
$LinkPath,
$Hotkey,
$IconLocation,
$Arguments,
$TargetPath
)
begin {
$shell = New-Object -ComObject WScript.Shell
}
process {
$link = $shell.CreateShortcut($LinkPath)
$PSCmdlet.MyInvocation.BoundParameters.GetEnumerator() |
Where-Object { $_.key -ne 'LinkPath' } |
ForEach-Object { $link.$($_.key) = $_.value }
$link.Save()
}
}
However
Set-Shortcut -LinkPath "C:\Users\user\Desktop\test.lnk" -TargetPath powershell start process "www.youtube.com"
Will default out to attaching a default path if you do not define one to look like:
"C:\Users\micha\Desktop\powershell start process "www.youtube.com""
how do I get rid of that default file path?
BONUS:
I'd be appreciative if someone broke down this line of code:
ForEach-Object { $link.$($_.key) = $_.value }
have you tried to change the properties of the shortcut object directly?
Try this and let me know:
function Set-Shortcut {
[CmdletBinding()]
param (
[Parameter(Mandatory, Position = 0, ValueFromPipeline)]
[System.String]$FilePath,
[Parameter(Mandatory, Position = 1)]
[System.String]$TargetPath
)
Begin {
$shell = new-object -ComObject WScript.Shell
}
Process {
try {
$file = Get-ChildItem -Path $FilePath -ErrorAction Stop
$shortcut = $shell.CreateShortcut($file.FullName)
$shortcut.TargetPath = $TargetPath
$shortcut.Save()
}
catch {
throw $PSItem
}
}
End {
while ($result -ne -1) {
$result = [System.Runtime.InteropServices.Marshal]::ReleaseComObject($shell)
}
}

I need to build a powershell that creates a url shortcut and opens to google chrome

I have very basic knowledge in creating scripts and self taught.
I need to build a powershell that creates a url shortcut and opens to google chrome
The Website has some features that currently only work in chrome
I have created a foundation for creating the icon but need help telling it to open only through Chrome and not the default browser
$new_object = New-Object -ComObject WScript.Shell
$destination = $new_object.SpecialFolders.Item("AllUsersDesktop")
$source_path = Join-Path -Path $destination -ChildPath "\\Procore Login.url"
$source = $new_object.CreateShortcut($source_path)
$source.TargetPath = "https://login.procore.com/"
$source.Save()
As commented, if you create a .url internet shortcut, this wil open in whatever the user has set to be the default browser.
If you want to always have that url open in chrome, you need to create a .lnk shortcut where you set it to start chrome.exe with the wanted url as target to open.
For that you can use below helper function:
function New-Shortcut {
[CmdletBinding()]
Param (
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string]$TargetPath, # the path to the executable
# the rest is all optional
[string]$ShortcutPath = (Join-Path -Path ([Environment]::GetFolderPath("Desktop")) -ChildPath 'New Shortcut.lnk'),
[string[]]$Arguments = $null, # a string or string array holding the optional arguments.
[string[]]$HotKey = $null, # a string like "CTRL+SHIFT+F" or an array like 'CTRL','SHIFT','F'
[string]$WorkingDirectory = $null,
[string]$Description = $null,
[string]$IconLocation = $null, # a string like "notepad.exe, 0"
[ValidateSet('Default','Maximized','Minimized')]
[string]$WindowStyle = 'Default',
[switch]$RunAsAdmin
)
switch ($WindowStyle) {
'Default' { $style = 1; break }
'Maximized' { $style = 3; break }
'Minimized' { $style = 7 }
}
$WshShell = New-Object -ComObject WScript.Shell
# create a new shortcut
$shortcut = $WshShell.CreateShortcut($ShortcutPath)
$shortcut.TargetPath = $TargetPath
$shortcut.WindowStyle = $style
if ($Arguments) { $shortcut.Arguments = $Arguments -join ' ' }
if ($HotKey) { $shortcut.Hotkey = ($HotKey -join '+').ToUpperInvariant() }
if ($IconLocation) { $shortcut.IconLocation = $IconLocation }
if ($Description) { $shortcut.Description = $Description }
if ($WorkingDirectory) { $shortcut.WorkingDirectory = $WorkingDirectory }
# save the link file
$shortcut.Save()
if ($RunAsAdmin) {
# read the shortcut file we have just created as [byte[]]
[byte[]]$bytes = [System.IO.File]::ReadAllBytes($ShortcutPath)
# set bit 6 of byte 21 ON
# ([math]::Pow(2,5) or 1 -shl 5 --> 32)
# see https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-shllink/16cb4ca1-9339-4d0c-a68d-bf1d6cc0f943
# page 13
$bytes[21] = $bytes[21] -bor 32
[System.IO.File]::WriteAllBytes($ShortcutPath, $bytes)
}
# clean up the COM objects
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($shortcut) | Out-Null
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($WshShell) | Out-Null
[System.GC]::Collect()
[System.GC]::WaitForPendingFinalizers()
}
And use it like this:
$chromePath = (Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\chrome.exe').'(Default)'
$props = #{
'ShortcutPath' = Join-Path -Path ([Environment]::GetFolderPath("Desktop")) -ChildPath 'Procore Login.lnk'
'TargetPath' = $chromePath
'Arguments' = 'https://login.procore.com/'
}
New-Shortcut #props

BrowseForFolders doesn't display on top

I'm writing PS Script and following block of code shows dialogbox below windows forms gui.
$btn1 = New-Object Windows.Forms.Button
$btn1.Text = "Wybierz folder projektowy"
$btn1.Location = New-Object System.Drawing.Point(170,140)
$btn1.Size = New-Object System.Drawing.Size(160,20)
$btn1.add_Click({
function Select-Folder($message='Select a folder', $path = 0) {
$object = New-Object -comObject Shell.Application
$object.topmost=$true
$folder = $object.BrowseForFolder(0, $message, 0, $path)
if ($folder -ne $null) {
$folder.self.Path
}
}
$folderPath = Select-Folder 'Select the folder where the move scripts reside'
If ($folderPath) {
Set-Content -Path "C:\Projekty\logs\temp_path.txt" -Value $folderPath.ToString() -Encoding Unicode
write-host $folderPath
get-content -Path "C:\Projekty\logs\temp_path.txt"
}
Else { Write-Host 'I do not have a folder path' }
})
$form_acl.Controls.Add($btn1)
Is there any way to make it display on top?
Here's screenshot of a problem:
You can try this alternative instead:
function Select-Folder {
[CmdletBinding()]
param (
# sets the descriptive text displayed above the tree view control in the dialog box
[Parameter(Mandatory=$false, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true, Position=0)]
[string]$Message = "Please select a directory.",
# sets the (pre)selected path
[Parameter(Mandatory=$false, Position=1)]
[string]$InitialDirectory,
# sets the root folder where the browsing starts from
[Parameter(Mandatory=$false)]
[System.Environment+SpecialFolder]$RootFolder = [System.Environment+SpecialFolder]::Desktop,
# sets a value indicating whether the 'New Folder' button appears in the folder browser dialog box
[switch]$ShowNewFolderButton
)
Add-Type -AssemblyName System.Windows.Forms
[System.Windows.Forms.Application]::EnableVisualStyles()
$dialog = New-Object System.Windows.Forms.FolderBrowserDialog
$dialog.Description = $Message
$dialog.SelectedPath = $InitialDirectory
$dialog.RootFolder = $RootFolder
$dialog.ShowNewFolderButton = $ShowNewFolderButton.IsPresent
$selected = $null
# force the dialog TopMost:
# because the owning window will not be used after the dialog has been closed,
# you can simply create a new form inside the method call.
$result = $dialog.ShowDialog((New-Object System.Windows.Forms.Form -Property #{TopMost = $true; TopLevel = $true}))
if ($result -eq [Windows.Forms.DialogResult]::OK){
$selected = $dialog.SelectedPath
}
# clear the FolderBrowserDialog from memory
$dialog.Dispose()
# return the selected folder
$selected
}
Select-Folder -Message 'Select the folder where the move scripts reside' -ShowNewFolderButton
Theo's helpful answer shows an alternative, WinForms-based way to invoke a folder-browsing dialog, via the System.Windows.Forms.FolderBrowserDialog class.
However, it seems that all that is missing from your original approach is to pass your form's window handle (hWND, .Handle) as the first argument to the Shell.Application COM object's .BrowseForFolder() method, which makes it the dialog's owner window and therefore shows the dialog on top of it - even if the form itself has the .TopMost property set:
$folder = (New-Object -ComObject Shell.Application).BrowseForFolder(
$form_acl.Handle, # Pass your form's window handle to make it the owner window
$message,
0,
$path)
Here's a simplified, self-contained example (requires PSv5+, but can be adapted to earlier versions):
using namespace System.Windows.Forms
using namespace System.Drawing
Add-Type -AssemblyName System.Windows.Forms
# Create a sample topmost form with a single
# button that invokes a folder-browsing dialog.
($form = [Form] #{
Text = "Topmost Form"
Size = [Size]::new(300, 100)
TopMost = $true # Make the form topmost.
StartPosition = 'CenterScreen'
}).Controls.AddRange(#(
($folderBrowseButton = [Button] #{
Location = [Point]::new(70, 20)
Size = [Size]::new(160,30)
Text = 'Browse for Folder'
})
))
$folderBrowseButton.add_Click({
$path = 'C:\'
# IMPORTANT: Pass $form.Handle, the form's window handle (HWND) as the first argument, which
# makes the form the owning window, ensuring that the dialog is displayed on
# top - even if the form itself is set to topmost.
$folder = (New-Object -ComObject Shell.Application).BrowseForFolder(
$form.Handle,
'Pick a target folder:',
0, # Options
$path # Starting directory path.
)
if ($null -ne $folder) {
Write-Verbose -vb ('Folder picked: ' + $folder.self.Path)
} else {
Write-Verbose -vb 'Folder browsing canceled.'
}
})
$null = $form.ShowDialog()

PowerScript: System.Windows.Forms.FolderBrowserDialog opening in the background

PowerScript Noob here.
I've found a snippet of code that lets the user select a folder thru a Folder Browser Dialog, instead of having enter the path to the folder manually.
Works as expected, except the Folder Browser Dialog often opens behind other windows on the screen, which is getting tiresome.
Here is the code:
Function Get-Folder($initialDirectory)
{
[System.Reflection.Assembly]::LoadWithPartialName("System.windows.forms")|Out-Null
$foldername = New-Object System.Windows.Forms.FolderBrowserDialog
$foldername.Description = "Select a folder"
$foldername.rootfolder = "MyComputer"
$foldername.SelectedPath = $initialDirectory
if($foldername.ShowDialog() -eq "OK")
{
$folder += $foldername.SelectedPath
}
return $folder
}
$FolderNavn = Get-Folder($StartFolder)
How do I get the Folder Browser Dialog to open 'on top of' all other Windows?
Thanks.
To set the BrowseForFolder dialog TopMost, you need to use the ShowDialog() overloaded method with a parameter that specifies the dialogs owner (parent) form.
The easiest I think it to just create a new Form with property Topmost set to $true and use that as owner form:
function Get-Folder {
[CmdletBinding()]
param (
[Parameter(Mandatory=$false, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true, Position=0)]
[string]$Message = "Please select a directory.",
[Parameter(Mandatory=$false, Position=1)]
[string]$InitialDirectory,
[Parameter(Mandatory=$false)]
[System.Environment+SpecialFolder]$RootFolder = [System.Environment+SpecialFolder]::Desktop,
[switch]$ShowNewFolderButton
)
Add-Type -AssemblyName System.Windows.Forms
$dialog = New-Object System.Windows.Forms.FolderBrowserDialog
$dialog.Description = $Message
$dialog.SelectedPath = $InitialDirectory
$dialog.RootFolder = $RootFolder
$dialog.ShowNewFolderButton = if ($ShowNewFolderButton) { $true } else { $false }
$selected = $null
# force the dialog TopMost
# Since the owning window will not be used after the dialog has been
# closed we can just create a new form on the fly within the method call
$result = $dialog.ShowDialog((New-Object System.Windows.Forms.Form -Property #{TopMost = $true }))
if ($result -eq [Windows.Forms.DialogResult]::OK){
$selected = $dialog.SelectedPath
}
# clear the FolderBrowserDialog from memory
$dialog.Dispose()
# return the selected folder
$selected
}

Dynamic Parameters - with Dynamic ValidateSet

I have a script that I've been working on to provide parsing of SCCM log files. This script takes a computername and a location on disk to build a dynamic parameter list and then present it to the user to choose the log file they want to parse. Trouble is I cannot seem to get the ValidateSet portion of the dynamic parameter to provide values to the user. In addition the script won't display the -log dynamic parameter when attempting to call the function.
When you run it for the first time you are not presented with the dynamic parameter Log as I mentioned above. If you then use -log and then hit tab you’ll get the command completer for the files in the directory you are in. Not what you’d expect; you'd expect that it would present you the Logfile names that were gathered during the dynamic parameter execution.
PSVersion 5.1.14409.1012
So the question is how do I get PowerShell to present the proper Validate set items to the user?
If you issue one of the items in the error log you get the proper behavior:
Here are the two functions that i use to make this possible:
function Get-CCMLog
{
[CmdletBinding()]
param([Parameter(Mandatory=$true,Position=0)]$ComputerName = '$env:computername', [Parameter(Mandatory=$true,Position=1)]$path = 'c:\windows\ccm\logs')
DynamicParam
{
$ParameterName = 'Log'
if($path.ToCharArray() -contains ':')
{
$FilePath = "\\$ComputerName\$($path -replace ':','$')"
if(test-path $FilePath)
{
$logs = gci "$FilePath\*.log"
$LogNames = $logs.basename
$logAttribute = New-Object System.Management.Automation.ParameterAttribute
$logAttribute.Position = 2
$logAttribute.Mandatory = $true
$logAttribute.HelpMessage = 'Pick A log to parse'
$logCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
$logCollection.add($logAttribute)
$logValidateSet = New-Object System.Management.Automation.ValidateSetAttribute($LogNames)
$logCollection.add($logValidateSet)
$logParam = New-Object System.Management.Automation.RuntimeDefinedParameter($ParameterName,[string],$logCollection)
$logDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
$logDictionary.Add($ParameterName,$logParam)
return $logDictionary
}
}
}
begin {
# Bind the parameter to a friendly variable
$Log = $PsBoundParameters[$ParameterName]
}
process {
# Your code goes here
#dir -Path $Path
$sb2 = "$((Get-ChildItem function:get-cmlog).scriptblock)`r`n"
$sb1 = [scriptblock]::Create($sb2)
$results = Invoke-Command -ComputerName $ComputerName -ScriptBlock $sb1 -ArgumentList "$path\$log.log"
[PSCustomObject]#{"$($log)Log"=$results}
}
}
function Get-CMLog
{
param(
[Parameter(Mandatory=$true,
Position=0,
ValueFromPipelineByPropertyName=$true)]
[Alias("FullName")]
$Path,
$tail =10
)
PROCESS
{
if(($Path -isnot [array]) -and (test-path $Path -PathType Container) )
{
$Path = Get-ChildItem "$path\*.log"
}
foreach ($File in $Path)
{
if(!( test-path $file))
{
$Path +=(Get-ChildItem "$file*.log").fullname
}
$FileName = Split-Path -Path $File -Leaf
if($tail)
{
$lines = Get-Content -Path $File -tail $tail
}
else {
$lines = get-cotnet -path $file
}
ForEach($l in $lines ){
$l -match '\<\!\[LOG\[(?<Message>.*)?\]LOG\]\!\>\<time=\"(?<Time>.+)(?<TZAdjust>[+|-])(?<TZOffset>\d{2,3})\"\s+date=\"(?<Date>.+)?\"\s+component=\"(?<Component>.+)?\"\s+context="(?<Context>.*)?\"\s+type=\"(?<Type>\d)?\"\s+thread=\"(?<TID>\d+)?\"\s+file=\"(?<Reference>.+)?\"\>' | Out-Null
if($matches)
{
$UTCTime = [datetime]::ParseExact($("$($matches.date) $($matches.time)$($matches.TZAdjust)$($matches.TZOffset/60)"),"MM-dd-yyyy HH:mm:ss.fffz", $null, "AdjustToUniversal")
$LocalTime = [datetime]::ParseExact($("$($matches.date) $($matches.time)"),"MM-dd-yyyy HH:mm:ss.fff", $null)
}
[pscustomobject]#{
UTCTime = $UTCTime
LocalTime = $LocalTime
FileName = $FileName
Component = $matches.component
Context = $matches.context
Type = $matches.type
TID = $matches.TI
Reference = $matches.reference
Message = $matches.message
}
}
}
}
}
The problem is that you have all the dynamic logic inside scriptblock in the if statement, and it handles the parameter addition only if the path provided contains a semicolon (':').
You could change it to something like:
if($path.ToCharArray() -contains ':') {
$FilePath = "\\$ComputerName\$($path -replace ':','$')"
} else {
$FilePath = $path
}
and continue your code from there
PS 6 can do a dynamic [ValidateSet] with a class:
https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_functions_advanced_parameters?view=powershell-6#dynamic-validateset-values