I'm new to coding trying to figure out powershell and confused at If statements.
Trying to get this code so it'll create the job scheduling to run on login but it won't create it on the initial first run. I want to add in an if statement at the end to run the command to check if it exists on a desktop as some end users I want to deploy this too will already have it. But I'm unsure where the error lies with the code and how to address or simplify the code?
Any help would be greatly appreciated
$PathTest = Test-Path -Path "C:\Users\public\desktop\Google.lnk"
$Google = {$ShortcutPath = "C:\users\public\desktop\Google.lnk"
$IconLocation = "C:\windows\System32\SHELL32.dll"
$IconArrayIndex = 150
$Shortcut = $Shell.CreateShortcut($ShortcutPath)
$Shortcut.TargetPath = "http://www.google.com"
$Shortcut.IconLocation = "$IconLocation, $IconArrayIndex"
$Shortcut.Save()
}
$Trigger = New-JobTrigger -AtLogOn -RandomDelay 00:00:30
Register-ScheduledJob -Trigger $Trigger -RunAs32 -ScriptBlock {$Google} -Name Google
If ($PathTest -is True) {
$PathTest
} Else {
$Google
}
You use a wrong if operator:
-is is used for checking types:
if ( $value -is [type] )
{
# do something
}
For a boolean check you can just use:
$condition = $true
if ( $condition ) # check for $true
{
Write-Output "The condition was true"
}
$condition = $false
if ( !$condition ) # check for $false
{
Write-Output "The condition was also true by inverting with not operator"
}
MS Docs PowerShell IF-Statement
The if statement is using wrong operator. -is is used to test object types, not logical equality which is -eq. In addition, the boolean true is $true
Related
I am working on writing Pester tests for our PowerShell scripts that are used during task sequences. Several of them work with the task sequence variables and so I wrote a mock that allows for testing reading variables and am now trying to figure out how to do it for writing variables.
This is the code to read a task sequence variable:
$TsEnv = New-Object -COMObject Microsoft.SMS.TSEnvironment
$Value = $TsEnv.Value('VariableNameToRead')
By passing in the $TsEnv to a function I can then mock it with the following:
$TsEnv = #{
'VariableNameToRead' = 'TestValue'
}
Add-Member -InputObject $TsEnv -MemberType ScriptMethod -Name Value -Value {
Param( [String]$Key )
$This[$Key]
}
This is the code for writing a task sequence variable:
$TsEnv = New-Object -COMObject Microsoft.SMS.TSEnvironment
$TsEnv.Value('VariableNameToWrite') = 'ValueToWrite'
With it being in parentheses after the $TsEnv.Value I am thinking it is treating it as a method, but I am unable to find any examples on how to assign values to a method.
With Pester 4.3.3+, you might be able to use New-MockObject to create a usable mock of that COM object.
Alternatively, you can do something similar to the below to allow you to mock the functionality of the COM object.
If that COM object is available on the machines where your CI is running, I might consider skipping the mocks and writing an integration test.
# functions.ps1
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop";
function GetTaskSequenceValue(
[Parameter(Mandatory=$true)]
[string] $varNameToRead,
[Parameter(Mandatory=$false)]
[System.Management.Automation.ScriptBlock] $readAction = {
param([string] $readKey)
$tsEnv = New-Object -COMObject 'Microsoft.SMS.TSEnvironment'
$tsEnv.Value($readKey)
}
) {
$value = Invoke-Command `
-ScriptBlock $readAction `
-ArgumentList #($varNameToRead)
return $value
}
function SetTaskSequenceValue(
[Parameter(Mandatory=$true)]
[string] $varNameToWrite,
[Parameter(Mandatory=$false)]
[System.Management.Automation.ScriptBlock] $writeAction = {
param([string] $writeKey, [string] $value)
$tsEnv = New-Object -COMObject 'Microsoft.SMS.TSEnvironment'
$TsEnv.Value($writeKey) = $value
}
) {
try {
Invoke-Command `
-ScriptBlock $writeAction `
-ArgumentList #($varNameToWrite)
return $true
}
catch {
# Swallow it
}
return $false
}
Tests for the functions abov. The tests manually mock out the COM objects
# functions.test.ps1
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.Tests\.', '.'
. "$here\$sut"
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop";
Describe "GetTaskSequenceValue" {
It "gets the expected value" {
$expected = 'value'
$mockAction = {
param($dummy)
return 'value'
}
$actual = GetTaskSequenceValue `
-varNameToRead 'dummyName' `
-readAction $mockAction
$actual | Should Be $expected
}
}
Describe "SetTaskSequenceValue" {
It "sets the expected value" {
$expected = 'value'
$mockAction = {
param($dummy)
return 'value'
}
$actual = SetTaskSequenceValue `
-varNameToWrite 'dummyValue' `
-writeAction $mockAction
$actual | Should Be $true
}
}
Anything to deal with getting environment variables, WMI, or dotnet static method calls, I like to contain within a small helper function, then it's very easy to mock it. Here's what that helper could look like.
Function Get-SMSTsVariable{($VariableName)
return $TSEnv.Value($VariableName)
}
Then you can easily mock this in various contexts to check and see how your code acts when various environmental variables are set.
For instance, if you want it to return a value of BitLockerProvisioning when you run Get-SMSTsVariable -VariableName _SMSTSCurrentActionName, and to return 'C:' when you run _OSDDetectedWinDir you setup a mock like this:
mock Get-SMSTsVariable `
-parameterFilter { $VariableName -eq '_SMSTSCurrentActionName'} `
-mockWith {return 'BitLockerProvisioning'}
mock Get-SMSTsVariable `
-parameterFilter { $VariableName -eq '_OSDDetectedWinDir'} `
-mockWith {return 'C:'}
In this way, you can begin your test setting up a handful of responses for the various ways your functions operate. It's really a breeze.
As a Powershell learner, I ran into -f format operator.
I then thought to myself, is it possible to dynamically change code based on a condition. Take for example a mapping drive scenario:
$creds = Get-Credential
$share_needs_creds = $true
$drive_map = New-Object -ComObject WScript.Network
if ($share_needs_creds){
$drive_map.MapNetworkDrive('Z:', '\\server\share', $false, "$($creds.Username)", $($creds.GetNetworkCredential().Password)")
} else {
$drive_map.MapNetworkDrive('Z:', '\\server\share', $false)
}
can this be re-written as follows:
$creds = Get-Credential
$share_needs_creds = $true
$drive_map = New-Object -ComObject WScript.Network
if ($share_needs_creds){
$condition_smart = ', "$($creds.Username)", $($creds.GetNetworkCredential().Password)"'
} else {
$condition_smart = ''
}
$drive_map.MapNetworkDrive('Z:', '\\server\share', $false{0}) -f $condition_smart
Any advise is appreciated!
That's not going to work - the -f operator returns exactly 1 string as output - and you need to pass a variable number of arguments to the parameter list of the method you're calling.
If you want to invoke a method dynamically on a ComObject, prepare your argument list as an array, and pass it to .Invoke() on the method name exposed by PowerShell's type adapter:
$arguments = #(
'Z:', '\\server\share', $false
if($share_needs_creds){
$creds.Username, $creds.GetNetworkCredential().Password
}
)
$drive_map.MapNetworkDrive.Invoke($arguments)
$RunspaceCollection = #()
$RunspacePool = [RunspaceFactory]::CreateRunspacePool(1,5)
$RunspacePool.Open()
$code = #({'somecode'},{'someothercode'})
Foreach ($test in $case) {
$finalcode= {
Invoke-Command -ScriptBlock [scriptblock]::create($code[$test])
}.GetNewClosure()
$Powershell = [PowerShell]::Create().AddScript($finalcode)
$Powershell.RunspacePool = $RunspacePool
[Collections.Arraylist]$RunspaceCollection += New-Object -TypeName PSObject -Property #{
Runspace = $PowerShell.BeginInvoke()
PowerShell = $PowerShell
}}
The finalcode variable doesn't expand when the GetNewClosure() happens, so $code[$test] gets into the runspace instead of actual code and I can't get my desired results. Any advice?
Using the method from the answer I'm getting this in the runspace, but it doesn't execute properly. I can confirm that my command is loaded into runspace (at least while in debugger inside runspace I can execute it without dot sourcing)
[System.Management.Automation.PSSerializer]::Deserialize('<ObjsVersion="1.1.0.1" xmlns="http://schemas.microsoft.com/powershell/2004/04">
<SBK> My-Command -Par1 "egweg" -Par2 "qwrqwr" -Par3 "wegweg"</SBK>
</Objs>')
This is what I see in debugger in runspace
Stopped at: $a = Invoke-Command -ScriptBlock { $([System.Management.Automation.PSSerializer]::Deserialize('<Objs Version="1.1.0.1" xmlns="http://schemas.microsoft.com/powershell/2004/04">
[DBG]: [Process:8064]: [Runspace12]: PS C:\git\infrastructure_samples>>
Stopped at: $a = Invoke-Command -ScriptBlock { $([System.Management.Automation.PSSerializer]::Deserialize('<Objs Version="1.1.0.1" xmlns="http://schemas.microsoft.com/powershell/2004/04">
[DBG]: [Process:8064]: [Runspace12]: PS C:\git\infrastructure_samples>> s
Stopped at: </Objs>')) }
The problem with your code is that AddScript method of PowerShell class is expecting a string, not ScriptBlock. And any closure will be lost when you convert ScriptBlock to string. To solve this, you can pass argument to script with AddArgument method:
$RunspaceCollection = New-Object System.Collections.Generic.List[Object]
$RunspacePool = [RunspaceFactory]::CreateRunspacePool(1,5)
$RunspacePool.Open()
$code = #({'somecode'},{'someothercode'})
$finalcode= {
param($Argument)
Invoke-Command -ScriptBlock ([scriptblock]::create($Argument))
}
Foreach ($test in $case) {
$Powershell = [PowerShell]::Create().AddScript($finalcode).AddArgument($code[$test])
$Powershell.RunspacePool = $RunspacePool
$RunspaceCollection.Add((New-Object -TypeName PSObject -Property #{
Runspace = $PowerShell.BeginInvoke()
PowerShell = $PowerShell
}))
}
I'm not sure if there's a better way off the top of my head, but you can replace the variables yourself with serialized versions of the same.
You can't use $Using: in this case, but I wrote a function that replaces all $Using: variables manually.
My use case was with DSC, but it would work in this case as well. It allows you to still write your script blocks as scriptblocks (not as strings), and supports variables with complex types.
Here's the code from my blog (also available as a GitHub gist):
function Replace-Using {
[CmdletBinding(DefaultParameterSetName = 'AsString')]
[OutputType([String], ParameterSetName = 'AsString')]
[OutputType([ScriptBlock], ParameterSetName = 'AsScriptBlock')]
param(
[Parameter(
Mandatory,
ValueFromPipeline
)]
[String]
$Code ,
[Parameter(
Mandatory,
ParameterSetName = 'AsScriptBlock'
)]
[Switch]
$AsScriptBlock
)
Process {
$cb = {
$m = $args[0]
$ser = [System.Management.Automation.PSSerializer]::Serialize((Get-Variable -Name $m.Groups['var'] -ValueOnly))
"`$([System.Management.Automation.PSSerializer]::Deserialize('{0}'))" -f $ser
}
$newCode = [RegEx]::Replace($code, '\$Using:(?<var>\w+)', $cb, [System.Text.RegularExpressions.RegexOptions]::IgnoreCase)
if ($AsScriptBlock.IsPresent) {
[ScriptBlock]::Create($newCode)
} else {
$newCode
}
}
}
A better way for me to do this replacement would probably be to use the AST instead of string replacement, but.. effort.
Your Use Case
$finalcode= {
Invoke-Command -ScriptBlock [scriptblock]::create($Using:code[$Using:test])
} | Replace-Using
For better results you might assign a variable first and then just insert that:
$value = [scriptblock]::Create($code[$test])
$finalcode= {
Invoke-Command -ScriptBlock $Using:value
} | Replace-Using
What is the Get-Process output value if it does not find a process specified? For example, I am checking if Outlook is closed and if it is, I back a PST file. Here is my code:
$source = "C:\Users\----\AppData\Local\Microsoft\Outlook\Outlook.pst"
$destination = "\\----\users\----\outlook"
$isOutlookOpen = Get-Process outlook*
$isOutlookOpen
if($isOutlookOpen = $true){
# Outlook is already closed:
Copy-Item -Path $source -Destination $destination
$messageParameters = #{
Subject = "Daily Outlook Backup Report computer"
Body = "Outlook was closed. Backup was complete."
From = "---"
To = "---"
SmtpServer = "---"
Port = ---
}
Send-MailMessage #messageParameters -BodyAsHtml
} else {
$messageParameters = #{
Subject = "Daily Outlook Backup Report computer"
Body = "Outlook was not closed. Backup was not initiated."
From = "---"
To = "---"
SmtpServer = "---"
Port = ---
}
Send-MailMessage #messageParameters -BodyAsHtml
}
It always goes to the else statement.
You use an assignment operator (=) in the condition, so it will always evaluate to $true. The equality comparison operator in PowerShell is -eq.
With that said, you don't need an operator there in the first place. Get-Process returns a list of System.Diagnostics.Process objects (or $null if no matching process is found). You can use the value of the variable $isOutlookOpen like a boolean value, because PowerShell will interpret a non-empty array as a boolean value $true and $null as a boolean value $false.
This should work:
$isOutlookOpen = Get-Process outlook*
if($isOutlookOpen) {
# ...
} else {
# ...
}
In your code you are checking if something is $true when it's value would be the details of the process:
$isOutlookOpen = Get-Process outlook*
$isOutlookOpen
if($isOutlookOpen = $true){
Even if it did give a true or false response you are actually assigning a value of $true in your if statement. It should be:
if ($isOutlookOpen -eq $true)
A better way to do it would be to put it in a try and catch block:
try {
get-process outlook* -errorAction stop
}
catch {
write-host "Outlook not running..."
}
I'm attempting to create a function for a script module that verifies the previous parameter has been set before it shows the next parameter.
The parameters I need are Identity, Share and Quota. I always want Identity to show, I don't want Share to show until the Identity has been set and I don't want Quota to show until the Share has been set.
I'm able to easily access $Identity, I'm not able to access $Share from within DynamicParam{}. I stepped through the script using PowerGUI and I was only able to see $Share when I hit Begin{}.
I have a way to workaround this by just showing Share/Quota if Identity is set, but ultimately I would like to learn how to keep adding additional parameters based on the previously set parameter.
A copy of the function is below. personfileutility.exe is just an executable we use to interact with the various systems to provision and gather information on a user.
function New-PersonalFiles
{
[CmdletBinding()]
Param
(
# Param1 help description
[Parameter(Mandatory=$true)]
$Identity
)
DynamicParam
{
$paramDictionary = new-object -Type System.Management.Automation.RuntimeDefinedParameterDictionary
if (($Identity -notlike "") -or ($Identity -notlike $null)){
$attributes = new-object System.Management.Automation.ParameterAttribute
$attributes.ParameterSetName = "__AllParameterSets"
$attributes.Mandatory = $true
$lookup = [xml](\\servername\personalfiles\personfileutility.exe -a $Identity)
$type = $lookup.User.user_directory.type.type
if ($type -like "other" -or $type -like "staff") {
$arguments = #()
$arguments += "\\fileserver\sharename"
}
elseif ($type -like "faculty") {
$arguments = #()
$arguments += "\\fileserver\sharename"
$arguments += "\\fileserver\sharename2"
}
elseif ($type -like "student") {
$arguments = #()
$arguments += "\\fileserver2\sharename"
}
$ParamOptions = New-Object System.Management.Automation.ValidateSetAttribute -ArgumentList $arguments
$attributeCollection = new-object -Type System.Collections.ObjectModel.Collection[System.Attribute]
$attributeCollection.Add($attributes)
$attributeCollection.Add($ParamOptions)
$dynParam1 = new-object -Type System.Management.Automation.RuntimeDefinedParameter("Share", [String], $attributeCollection)
$paramDictionary.Add("Share", $dynParam1)
}
if (($Share -like "\\fileserver\*"))
{
$attributes2 = new-object System.Management.Automation.ParameterAttribute
$attributes2.ParameterSetName = "__AllParameterSets"
$attributes2.Mandatory = $true
$lookup = [xml](\\servername\personalfiles\personfileutility.exe -a $Identity)
$type = $lookup.User.user_directory.type.type
if ($type -like "other" -or $type -like "staff") {
$arguments = #()
$arguments += "15GB"
$arguments += "20GB"
}
elseif ($type -like "faculty") {
$arguments = #()
$arguments += "10GB"
$arguments += "15GB"
}
elseif ($type -like "student") {
$arguments = #()
$arguments += "5GB"
$arguments += "10GB"
}
$ParamOptions2 = New-Object System.Management.Automation.ValidateSetAttribute -ArgumentList $arguments2
$attributeCollection2 = new-object -Type System.Collections.ObjectModel.Collection[System.Attribute]
$attributeCollection2.Add($attributes2)
$attributeCollection2.Add($ParamOptions2)
$dynParam2 = new-object -Type System.Management.Automation.RuntimeDefinedParameter("Quota", [String], $attributeCollection2)
$paramDictionary.Add("Quota", $dynParam2)
}
return $paramDictionary
}
<#
.Synopsis
Short description
.DESCRIPTION
Long description
.EXAMPLE
Example of how to use this cmdlet
.EXAMPLE
Another example of how to use this cmdlet
#>
Begin
{
}
Process
{
\\servername\personalfiles\personfileutility.exe -a $Identity -c -q ((Invoke-Expression $PSBoundParameters.Quota) / 1KB) -s $Share
}
End
{
}
}
I'm trying to replicate the "workaround" you're using, but I'm only seeing that I have access to the dynamic parameter Share (not Quota). In order to reproduce your setup, since I don't have access to personfileutility.exe I have commented out two lines and added a third where I hardcode $type to "faculty". For example, in two places I have updated the code to be:
#$lookup = [xml](\\servername\personalfiles\personfileutility.exe -a $Identity)
#$type = $lookup.User.user_directory.type.type
$type = "faculty"
With these changes in place, I can access the Share parameter after I specify an Identity. However, I cannot access the Quota. Do you expect Quota to be available?
If I'm understanding the problem you're asking about, you do not want Share to be accessible until Identity is accessible (which you currently have working). But you in addition, you don't want Quota to be accessible until both Identity and Share are filled out and you don't know how to make that work. Is that correct?
If I'm understanding the problem correctly, I don't believe that PowerShell offers a mechanism to achieve that with Commandlet binding. I think you could make that work by either using a GUI application, or interactively prompting the user for inputs.