Why functions are not available locally until after first ran? - powershell

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.

Related

Powershell: Debug function to print out failed function name (from a module)

Hi,
I wrote a nice debug function that provides the name of function from which it's called. The code is:
Function Write-Log {
$callerName = (Get-Variable MyInvocation -Scope 1).Value.MyCommand.Name
$stamp = (Get-Date).toString("yyyy/MM/dd HH:mm:ss")
$line = "$stamp [$level`]: $callerName $Message"
Write-Host $line -ForegroundColor $color
}
Works great when called from inside .ps1 script; however I started refactoring code so that some functions are moved to a .psm1 module - and this is where the function fails. Instead of printing the function name, it only provides the name of module.
How can I fix this function so that it only shows the function name, or scope & function name?
Thank you leeharvey1. Posting your suggestions as an answer to help other community members.
From your module, you can try using $callerName = "{0} - {1}:" -f (Get-PSCallStack)[1].Location, (Get-PSCallStack)[1].FunctionName.

Strange behavior of PSConsoleReadLine

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.

Powershell: Checking if Network Drive exists, if not, map it then double check

I am trying to write a simple script to check if a network drive is available, map it if it isn't, then double check the mapping worked (to report any issues like the account mapping it has accidentally expired etc). If it fails at the double check, it will send an email, otherwise it reports all ok.
I cannot get the double check to work. I think I have my statements wrong?
$Networkpath = "X:\Testfolder"
$pathExists = Test-Path -Path $Networkpath
If (-not ($pathExists)) {
(new-object -com WScript.Network).MapNetworkDrive("X:","\\Server-01\Share")
}
ELSEIF (-not ($pathExists)) {
Write-Host "Something went very wrong"
#Insert email code here
}
ELSE {Write-Host "Drive Exists already"}
I like James' answer but want to explain why you're having this issue. The reason your double check is failing is that you actually only check for the Path a single time.
In the beginning of the code, you check to see if the path exists on these two lines
$Networkpath = "X:\Testfolder"
$pathExists = Test-Path -Path $Networkpath
The variable $pathExists is created at this point and stores the outcome from that point in time. That's why your double check if failing later in the code, it's actually using the same output from the first time.
The code continues to test if the path exists, and if not, then creates the path.
If (-not ($pathExists)) {
(new-object -com WScript.Network).MapNetworkDrive("X:","\\Server-01\Share")
}
What you should do is add one more test here, then you'll know the drive already exists.
I added the extra test for you and slightly tweaked the flow through the script, with a Write-Host output for each branch. Here's the completed code.
$Networkpath = "X:\Testfolder"
$pathExists = Test-Path -Path $Networkpath
If ($pathExists) {
Write-host "Path already existed"
Return #end the function if path was already there
}
else {
(new-object -com WScript.Network).MapNetworkDrive("X:","\\Server-01\Share")
}
#Path wasn't there, so we created it, now testing that it worked
$pathExists = Test-Path -Path $Networkpath
If (-not ($pathExists)) {
Write-Host "We tried to create the path but it still isn't there"
#Insert email code here
}
ELSE {Write-Host "Path created successfully"}
You can use an if within an if (nested if) to perform a check after the drive has been mapped.
I also changed the logic of the first check so it doesn't use -not as it makes the code simpler.
$Networkpath = "X:\Testfolder"
If (Test-Path -Path $Networkpath) {
Write-Host "Drive Exists already"
}
Else {
#map network drive
(New-Object -ComObject WScript.Network).MapNetworkDrive("X:","\\Server-01\Share")
#check mapping again
If (Test-Path -Path $Networkpath) {
Write-Host "Drive has been mapped"
}
Else {
Write-Host "Something went very wrong"
}
}

How to return the name of the calling script from a Powershell Module?

I have two Powershell files, a module and a script that calls the module.
Module: test.psm1
Function Get-Info {
$MyInvocation.MyCommand.Name
}
Script: myTest.ps1
Import-Module C:\Users\moomin\Documents\test.psm1 -force
Get-Info
When I run ./myTest.ps1 I get
Get-Info
I want to return the name of the calling script (test.ps1). How can I do that?
Use PSCommandPath instead in your module:
Example test.psm1
function Get-Info{
$MyInvocation.PSCommandPath
}
Example myTest.ps1
Import-Module C:\Users\moomin\Documents\test.psm1 -force
Get-Info
Output:
C:\Users\moomin\Documents\myTest.ps1
If you want only the name of the script that could be managed by doing
GCI $MyInvocation.PSCommandPath | Select -Expand Name
That would output:
myTest.ps1
I believe you could use the Get-PSCallStack cmdlet, which returns an array of stack frame objects. You can use this to identify the calling script down to the line of code.
Module: test.psm1
Function Get-Info {
$callstack = Get-PSCallStack
$callstack[1].Location
}
Output:
myTest.ps1: Line 2
Using the $MyInvocation.MyCommand is relative to it's scope.
A simple example (Of a script located : C:\Dev\Test-Script.ps1):
$name = $MyInvocation.MyCommand.Name;
$path = $MyInvocation.MyCommand.Path;
function Get-Invocation(){
$path = $MyInvocation.MyCommand.Path;
$cmd = $MyInvocation.MyCommand.Name;
write-host "Command : $cmd - Path : $path";
}
write-host "Command : $cmd - Path : $path";
Get-Invocation;
The output when running .\c:\Dev\Test-Script.ps1 :
Command : C:\Dev\Test-Script.ps1 - Path : C:\Dev\Test-Script.ps1
Command : Get-Invocation - Path :
As you see, the $MyInvocation is relative to the scoping. If you want the path of your script, do not enclose it in a function. If you want the invocation of the command, then you wrap it.
You could also use the callstack as suggested, but be aware of scoping rules.
I used this today after trying a couple of techniques.
$scriptPath = split-path -parent $MyInvocation.MyCommand.Definition
$ScriptName = $MyInvocation.MyCommand | select -ExpandProperty Name
Invoke-Expression ". $Script\$ScriptName"
To refer to the invocation info of the calling script, use:
#(Get-PSCallStack)[1].InvocationInfo
e.g.:
#(Get-PSCallStack)[1].InvocationInfo.MyCommand.Name
This provides the script path with trailing backslash as one variable and the script name as another.
The path works with Powershell 2.0 and 3.0 and 4.0 and probably 5.0
Where with Posershell $PSscriptroot is now available.
$_INST = $myinvocation.mycommand.path.substring(0,($myinvocation.mycommand.path.length - $MyInvocation.mycommand.name.length))
$_ScriptName = $myinvocation.mycommand.path.substring($MyInvocation.MyCommand.Definition.LastIndexOf('\'),($MyInvocation.mycommand.name.length +1))
$_ScriptName = $_ScriptName.TrimStart('\')
If you want a more reusable approach, you can use:
function Get-CallingFileName
{
$cStack = #(Get-PSCallStack)
$cStack[$cStack.Length-1].InvocationInfo.MyCommand.Name
}
The challenge I had was having a function that could be reused within the module. Everything else assumed that the script was calling the module function directly and if it was removed even 1 step, then the result would be the module file name. If, however, the source script is calling a function in the module which is, in turn, calling another function in the module, then this is the only answer I've seen that can ensure you're getting the source script info.
Of course, this approach is based on what #iRon and #James posted.
For you googlers looking for quick copy paste solution,
here is what works in Powershell 5.1
Inside your module:
$Script = (Get-PSCallStack)[2].Command
This will output just the script name (ScriptName.ps1) which invoked a function located in module.
I use this in my module:
function Get-ScriptPath {
[CmdletBinding()]
param (
[string]
$Extension = '.ps1'
)
# Allow module to inherit '-Verbose' flag.
if (($PSCmdlet) -and (-not $PSBoundParameters.ContainsKey('Verbose'))) {
$VerbosePreference = $PSCmdlet.GetVariableValue('VerbosePreference')
}
# Allow module to inherit '-Debug' flag.
if (($PSCmdlet) -and (-not $PSBoundParameters.ContainsKey('Debug'))) {
$DebugPreference = $PSCmdlet.GetVariableValue('DebugPreference')
}
$callstack = Get-PSCallStack
$i = 0
$max = 100
while ($true) {
if (!$callstack[$i]) {
Write-Verbose "Cannot detect callstack frame '$i' in 'Get-ScriptPath'."
return $null
}
$path = $callstack[$i].ScriptName
if ($path) {
Write-Verbose "Callstack frame '$i': '$path'."
$ext = [IO.Path]::GetExtension($path)
if (($ext) -and $ext -eq $Extension) {
return $path
}
}
$i++
if ($i -gt $max) {
Write-Verbose "Exceeded the maximum of '$max' callstack frames in 'Get-ScriptPath'."
return $null
}
}
return $null
}
You can grab the automatic variable MyInvocation from the parent scope and get the name from there.
Get-Variable -Scope:1 -Name:MyInvocation -ValueOnly
I did a basic test to check to see if it would always just get the direct parent scope and it worked like a treat and is extremely fast as opposed to Get-PSCallStack
function ScopeTest () {
Write-Information -Message:'ScopeTest'
}
Write-nLog -Message:'nLog' -Type:110 -SetLevel:Verbose
ScopeTest

How to pin to start menu using PowerShell

I can pin some programs to taskbar on Win7 using PowerShell.
$shell = new-object -com "Shell.Application"
$folder = $shell.Namespace('C:\Windows')
$item = $folder.Parsename('notepad.exe')
$verb = $item.Verbs() | ? {$_.Name -eq 'Pin to Tas&kbar'}
if ($verb) {$verb.DoIt()}
How do I modify the above code to pin a program to the Start menu?
Another way
$sa = new-object -c shell.application
$pn = $sa.namespace($env:windir).parsename('notepad.exe')
$pn.invokeverb('startpin')
Or unpin
$pn.invokeverb('startunpin')
Use the code below
$shell = new-object -com "Shell.Application"
$folder = $shell.Namespace('C:\Windows')
$item = $folder.Parsename('notepad.exe')
$verb = $item.Verbs() | ? {$_.Name -eq 'Pin to Start Men&u'}
if ($verb) {$verb.DoIt()}
Note: the change is in the fourth line.
The main problem with most of the solution is that they enumerate the verbs on a file, search for the string to perform the action (“Pin to Startmenu” etc.) and then execute it. This does not work if you need to support 30+ languages in your company, except you use external function to search for the localized command (see answer from shtako-verflow).
The answer from Steven Penny is the first that is language neutral and does not need any external code. It uses the verbs stored in the registry HKEY_CLASSES_ROOT\CLSID\{90AA3A4E-1CBA-4233-B8BB-535773D48449} and HKEY_CLASSES_ROOT\CLSID\{a2a9545d-a0c2-42b4-9708-a0b2badd77c8}
Based on this, here’s the code we are now using:
function PinToTaskbar {
param([Parameter(Mandatory=$true)][string]$FilePath)
ExecuteVerb $FilePath "taskbarpin"
}
function UnpinFromTaskbar {
param([Parameter(Mandatory=$true)][string]$FilePath)
ExecuteVerb $FilePath "taskbarunpin"
}
function PinToStartmenu {
param([Parameter(Mandatory=$true)][string]$FilePath)
ExecuteVerb $FilePath "startpin"
}
function UnpinFromStartmenu {
param([Parameter(Mandatory=$true)][string]$FilePath)
ExecuteVerb $FilePath "startunpin"
}
function ExecuteVerb {
param(
[Parameter(Mandatory=$true)][string]$File,
[Parameter(Mandatory=$true)][string]$Verb
)
$path = [System.Environment]::ExpandEnvironmentVariables($File)
$basePath = split-path $path -parent #retrieve only the path File=C:\Windows\notepad.exe -> C:\Windows
$targetFile = split-path $path -leaf #retrieve only the file File=C:\Windows\notepad.exe -> notepad.exe
$shell = new-object -com "Shell.Application"
$folder = $shell.Namespace($basePath)
if ($folder)
{
$item = $folder.Parsename($targetFile)
if ($item)
{
$item.invokeverb($Verb)
# "This method does not return a value." (http://msdn.microsoft.com/en-us/library/windows/desktop/bb787816%28v=vs.85%29.aspx)
# Therefore we have no chance to know if this was successful...
write-host "Method [$Verb] executed for [$path]"
}
else
{
write-host "Target file [$targetFile] not found, aborting"
}
}
else
{
write-host "Folder [$basePath] not found, aborting"
}
}
#PinToTaskbar "%WINDIR%\notepad.exe"
#UnpinFromTaskbar "%WINDIR%\notepad.exe"
PinToStartmenu "%WINDIR%\notepad.exe"
#UnpinFromStartmenu "%WINDIR%\notepad.exe"
See the script (international) here : http://gallery.technet.microsoft.com/scriptcenter/b66434f1-4b3f-4a94-8dc3-e406eb30b750
If you want to add an action like Pin to Modern UI interface (Windows 8), at $verbs, add 51201
Steven Penny's second answer above worked well for me. Here are a couple more tidbits.
It's doing COM through PowerShell, so you can do the same thing with pretty much any COM client. For example, here's an AutoHotkey version.
Shell := ComObjCreate("Shell.Application")
Target := Shell.Namespace(EnvGet("WinDir")).ParseName("Notepad.exe")
Target.InvokeVerb("startpin")
VBScript or InnoSetup would look almost the same except for the function used to create the object.
I also found that I have one program that pinned OK, but didn't have the right icon and/or description because of limitations in the compiler. I just made a little 1-line WinForms app that starts the target with Process.Start, and then added the appropriate icon, and the name I wanted in the Start Menu in the Title property in AppInfo.cs.