I'm attempting to add a wallpaper, along with certain parameters, to each user on a computer. It's been hit and miss with this working/not working on computers. The ones that fail I get the error "Method invocation failed because [System.Management.Automation.PSObject] does not contain a method named 'op_Addition'."
The variables $WallpaperPath and $Style are coming from another source within Automation Manager (using N-Central).
# Get each user profile SID and Path to the profile
$UserProfiles = Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\*" | Where {$_.PSChildName -match "S-1-5-21-(\d+-?){4}$" } | Select-Object #{Name="SID"; Expression={$_.PSChildName}}, #{Name="UserHive";Expression={"$($_.ProfileImagePath)\NTuser.dat"}}
# Add in the .DEFAULT User Profile
$DefaultProfile = "" | Select-Object SID, UserHive
$DefaultProfile.SID = ".DEFAULT"
$DefaultProfile.Userhive = "C:\Users\Public\NTuser.dat"
$UserProfiles += $DefaultProfile
# Loop through each profile on the machine</p>
Foreach ($UserProfile in $UserProfiles) {
# Load User ntuser.dat if it's not already loaded
If (($ProfileWasLoaded = Test-Path Registry::HKEY_USERS\$($UserProfile.SID)) -eq $false) {
Start-Process -FilePath "CMD.EXE" -ArgumentList "/C REG.EXE LOAD HKU\$($UserProfile.SID) $($UserProfile.UserHive)" -Wait -WindowStyle Hidden
}
# Write to the registry
$key = "Registry::HKEY_USERS\$($UserProfile.SID)\Control Panel\Desktop"
Set-ItemProperty -Path $key -name Wallpaper -value "$WallpaperPath"
Set-ItemProperty -Path $key -name TileWallpaper -value "0"
Set-ItemProperty -Path $key -name WallpaperStyle -value "$Style" -Force
# Unload NTuser.dat
If ($ProfileWasLoaded -eq $false) {
[gc]::Collect()
Start-Sleep 1
Start-Process -FilePath "CMD.EXE" -ArgumentList "/C REG.EXE UNLOAD HKU\$($UserProfile.SID)" -Wait -WindowStyle Hidden| Out-Null
}
}
I'm looking for this to load a temporary HKU hive for each user that's not currently logged in, and has an NTuser.dat file, and write the registry entries specified. It should then unload any hive for users it added.
Instead of $UserProfiles = ..., use either [array] $UserProfiles = ... or $UserProfiles = #(...) in order to ensure that $UserProfiles always contains an array, even if the command happens to return just one object.
That way, your += operation is guaranteed to work as intended, namely to (loosely speaking) append an element to the array.[1]
Note that PowerShell's pipeline has no concept of an array, just a stream of objects. When such a stream is collected, a single object is captured as itself; only two or more objects are captured in an array ([object[]]) - see this answer for more information.
A simple demonstration:
2, 1 | ForEach-Object {
$result = Get-ChildItem / | Select-Object Name -First $_
try {
$result += [pscustomobject] #{ Name = 'another name' }
"`$result has $($result.Count) elements."
} catch {
Write-Warning "+= operation failed: $_"
}
}
In the first iteration, 2 objects are returned, which are
stored in an array. += is then used to "append" another element.
In the second iteration, only 1 object is returned and stored as such.
Since [pscustomobject], which is the type of object returned by Select-Object, doesn't define a + operation (which would have
to be implemented via an op_Addition() method at the .NET level), the error you saw occurs.
Using an [array] type constraint or #(...), the array-subexpression operator operator, avoids this problem:
2, 1 | ForEach-Object {
# Note the use of #(...)
# Alternatively:
# [array] $result = Get-ChildItem \ | Select-Object Name -First $_
$result = #(Get-ChildItem / | Select-Object Name -First $_)
$result += [pscustomobject] #{ Name = 'another name' }
"`$result has $($result.Count) elements."
}
As noted, [array] $results = Get-ChildItem \ | Select-Object Name -First $_ works too, though there are subtle differences between the two approaches - see this answer.
As an aside:
To synchronously execute console applications or batch files and capture their output, call them directly (c:\path\to\some.exe ... or & $exePath ...), do not use Start-Process (or the System.Diagnostics.Process API it is based on) - see this answer. GitHub docs issue #6239 provides guidance on when use of Start-Process is and isn't appropriate.
That is, you can just make calls such as the following:
REG.EXE LOAD "HKU\$($UserProfile.SID)" "$($UserProfile.UserHive)"
Also, it's easier and more efficient to construct [pscustomobject] instances with their literal syntax (v3+; see the conceptual about_PSCustomObject help topic):
$UserProfiles += [pscustomobject] #{
SID = ".DEFAULT"
Userhive = "C:\Users\Public\NTuser.dat"
}
[1] Technically, a new array must be created behind the scenes, given that arrays are fixed-size data structures. While += is convenient, it is therefore inefficient, which matters in loops - see this answer.
Related
I am trying to create a reusable log function for a project I am working on in PowerShell. I am a novice with PowerShell so I am having problems figuring out why my function is not producing expected results. I am attempting to create a function that send NTFS/ACL details to a log file. This function will be incorporated in to a larger script that will change some NTFS/ACL/ACE folder rights. I have excluded some of the other code for simplification (changing rights).
Below is my current stripped down code. When it runs, it creates the log file but the file is empty. If I move the line of code that creates the log inside the log function, it creates the log file with data but it is not formatted correctly - it writes the heading (object attribute names) on one line, then the data, then then a new line with the heading, then the data. I want it to write the heading, then a line of data, line of data, .... Before I created a function for this, it worked as expected. I am a novice at PowerShell so I may not understand how to pass info in and out of the function. My Code:
#variables
$rootDir = "\\server1\share1"
$logDir = "c:\temp"
$date = (Get-Date -Format "MMddyyyy-HHmm")
$logData =#()
#My Logging Function
Function CreateLog {
#create and object to store attributes
$logObj = New-Object -TypeName psobject -Property #{
FolderPath = $Folder.Fullname
IdentityReference = $ACL.IdentityReference.ToString()
folder = $Folder[0]
FileSystemRights = $ACL.FileSystemRights.ToString()
InheritanceFlags = $ACL.InheritanceFlags.ToString()
}
$Folders=(Get-ChildItem -Path $rootDir -Directory -Recurse)
foreach ($Folder in $Folders){
$ACLs = get-acl $Folder.fullname | ForEach-Object {$_.Access}
Foreach ($ACL in $ACLs){
CreateLog
if($ACL.FileSystemRights -eq "FullControl"){
Write-Host "DO SOMETHING"
}
}
$logData | select folder,IdentityReference,FileSystemRights,InheritanceFlags,FolderPath | Format-Table -Wrap | Out-File $logDir\testlog-$date.log -Append
I assume you're looking for something like this:
#variables
$rootDir = "\\server1\share1"
$logDir = "c:\temp"
$date = Get-Date -Format "MMddyyyy-HHmm"
#My Logging Function
Function CreateLog {
param(
[Parameter(Mandatory)]
[System.Security.AccessControl.FileSystemAccessRule] $AccessRule,
[Parameter(Mandatory)]
[string] $Folder
)
[pscustomobject]#{
FolderPath = $Folder
IdentityReference = $AccessRule.IdentityReference
FileSystemRights = $AccessRule.FileSystemRights
InheritanceFlags = $AccessRule.InheritanceFlags
}
}
Get-ChildItem -Path $rootDir -Directory -Recurse | ForEach-Object {
foreach($rule in ($_ | Get-Acl).Access) {
CreateLog -AccessRule $rule -Folder $_.FullName
if($rule.FileSystemRights.HasFlag([Security.AccessControl.FileSystemRights]::FullControl)) {
# Do something here
}
}
} | Export-Csv $logDir\testlog-$date.log -NoTypeInformation
Your current code has a few syntax errors (missing }), your function seem to be assigning the objects to $logObj but then $logObj is never outputted from it, hence producing no output.
Your $logData array is defined but never used, leaving aside is not needed at all in this case. Your function should, ideally, take the arguments for the Access Rules and Path, see Functions with Parameters to learn more.
Format-Table as well as the other Format-* cmdlets should be primarily used for console display, should not be used for exporting data. In this case you should use Export-Csv, this way your data preserves its structure and can be imported back at ease, be filtered and be sorted.
I am trying to modify a variable within Invoke-Command in order to get out of a loop, however I'm having trouble doing that.
In the sample script below, I'm connecting to a host, grabbing information from NICs that are Up and saving the output to a file (Baseline). Then on my next iteration I will keep grabbing the same info and then compare Test file to Baseline file.
From a different shell, I've connected to the same server and disabled one of the NICs to force Compare-Object to find a difference.
Once a difference is found, I need to get out of the loop, however I cannot find a way to update the local variable $test_condition. I've tried multiple things, from Break, Return, $variable:global, $variable:script, but nothing worked so far.
$hostname = "server1"
$test_condition = $false
do {
Invoke-Command -ComputerName $hostname -Credential $credential -ScriptBlock{
$path = Test-Path -LiteralPath C:\Temp\"network_list_$using:hostname-Baseline.txt"
if ($path -eq $false) {
Get-NetAdapter | Where-Object Status -EQ "Up" | Out-File -FilePath (New-Item C:\Temp\"network_list_$using:hostname-Baseline.txt" -Force)
} else {
Get-NetAdapter | Where-Object Status -EQ "Up" | Out-File -FilePath C:\Temp\"network_list_$using:hostname-Test.txt"
$objects = #{
ReferenceObject = (Get-Content C:\Temp\"network_list_$using:hostname-Baseline.txt")
DifferenceObject = (Get-Content C:\Temp\"network_list_$using:hostname-Test.txt")
}
$test_condition = (Compare-Object #objects).SideIndicator -ccontains "<="
$test_condition #this is returning True <-----
}
}
} until ($test_condition -eq $true)
Any tips? What am I doing wrong?
TIA,
ftex
You can pass variables into a remote script block with the $Using:VarName scope modifier, but you can't use typical $Global: or $Script to modify anything in the calling scope. In this scenario the calling scope isn't the parent scope. The code is technically running in a new session on the remote system and $Global: would refer to that session's global scope.
For example:
$var = "something"
Invoke-Command -ComputerName MyComuter -ScriptBlock { $Global:var = "else"; $var}
The remote session will output "else". However, after return in the calling session $var will output "something" remaining unchanged despite the assignment in the remote session.
Based on #SantiagoSquarzon's comment place the assignment inside the Do loop with a few other modifications:
$hostname = "server1"
do {
$test_condition = $false
$test_condition =
Invoke-Command -ComputerName $hostname -Credential $credential -ScriptBlock{
$path = Test-Path -LiteralPath C:\Temp\"network_list_$using:hostname-Baseline.txt"
if ($path -eq $false) {
Get-NetAdapter | Where-Object Status -eq "Up" | Out-File -FilePath (New-Item C:\Temp\"network_list_$using:hostname-Baseline.txt" -Force)
} else {
Get-NetAdapter | Where-Object Status -eq "Up" | Out-File -FilePath C:\Temp\"network_list_$using:hostname-Test.txt"
$objects = #{
ReferenceObject = (Get-Content C:\Temp\"network_list_$using:hostname-Baseline.txt")
DifferenceObject = (Get-Content C:\Temp\"network_list_$using:hostname-Test.txt")
}
(Compare-Object #objects).SideIndicator -contains "<=" # this is returning True <-----
}
}
} until ($test_condition -eq $true)
I don't know why you were using -ccontains considering "<=" has no casing implications. Also it's very unusual to capitalize operators.
Notice there's no explicit return or assignment. PowerShell will emit the Boolean result of the comparison and that will be returned from the remote session and end up assigned to the $test_condition variable.
An aside:
I'm not sure why we want to use -contains at all. Admittedly it'll work fine in this case, however, it may lead you astray elsewhere. -contains is a collection containment operator and not really meant for testing the presence of one string within another. The literal meaning of "contains" makes for an implicitly attractive hazard, as demonstrated in this recent question.
In short it's easy to confuse the meaning, purpose and behavior on -contains.
This "<=" -contains "<=" will return "true" as expected, however "<==" -contains "<=" will return "false" even though the left string literally does contain the right string.
The answer, to the aforementioned question says much the same. My addendum answer offers a some additional insight for the particular problem and how different operators can be circumstantially applied.
So, as a matter of practice for this case wrap the Compare-Object command in the array sub-expression operator like:
#( (Compare-Object #objects).SideIndicator ) -contains "<="
Given the particulars, this strikes me as the least intrusive way to implement such a loosely stated best practice.
I'm trying to create a powershell script that will grab all Active Directory accounts that are enabled, and inactive for 90 days. The script will prompt the user to choose between querying computer or user accounts.
Depending on the choice, it will pass it over to the main command as a variable.
The commands work correctly if I don't pass a variable.
I'm not sure if what I'm trying to do is possible.
Sorry for any bad code formatting. Just starting out.
Clear-Host
write-host "`nProgram searches for Enabled AD users account that have not logged in for more than 90 days. `nIt searches the entire domain and saves the results to a CSV file on users desktop." "`n"
$choice = Read-host -Prompt " What do you want to search for Computer or Users Accounts`nType 1 for users`nType 2 for Computers`n`nChoice"
$account
if ($choice -eq 1) {
$account = UsersOnly
}
Elseif ($choice -eq 2) {
$account = ComputersOnly
}
Else {
write-host "This is not an option `n exiting program"
exit
}
$FileName = Read-Host -Prompt "What do you want to name the CSV file"
$folderPath = "$env:USERPROFILE\Desktop\$FileName.csv"
Search-ADAccount -AccountInactive -TimeSpan 90 -$account | Where-Object { $_.Enabled -eq $true } | select Name, UserPrincipalName, DistinguishedName | Export-Csv -Path $folderPath
Splatting is the way to achieve this. It's so named because you reference a variable with # instead of $ and # kind of looks a "splat".
it works by creating a hashtable, which is a type of dictionary (key/value pairs). In PowerShell we create hashtable literals with #{}.
To use splatting you just make a hashtable where each key/value pair is a parameter name and value, respectively.
So for example if you wanted to call Get-ChildItem -LiteralPath $env:windir -Filter *.exe you could also do it this way:
$params = #{
LiteralPath = $env:windir
Filter = '*.exe'
}
Get-ChildItem #params
You can also mix and match direct parameters with splatting:
$params = #{
LiteralPath = $env:windir
Filter = '*.exe'
}
Get-ChildItem #params -Verbose
This is most useful when you need to conditionally omit a parameter, so you can turn this:
if ($executablesOnly) {
Get-ChildItem -LiteralPath $env:windir -Filter *.exe
} else {
Get-ChildItem -LiteralPath $env:windir
}
Into this:
$params = #{
LiteralPath = $env:windir
}
if ($executablesOnly) {
$params.Filter = '*.exe'
}
Get-ChildItem #params
or this:
$params = #{}
if ($executablesOnly) {
$params.Filter = '*.exe'
}
Get-ChildItem -LiteralPath $env:windir #params
With only 2 possible choices, the if/else doesn't look that bad, but as your choices multiply and become more complicated, it gets to be a nightmare.
Your situation: there's one thing I want to note first. The parameters you're trying to alternate against are switch parameters. That means when you supply them you usually only supply the name of the parameter. In truth, these take boolean values that default to true when the name is supplied. You can in fact override them, so you could do Search-ADAccount -UsersOnly:$false but that's atypical.
Anyway the point of mentioning that is that it may have been confusing how you would set its value in a hashtable for splatting purposes, but the simple answer is just give them a boolean value (and usually it's $true).
So just changing your code simply:
$account = if ($choice -eq 1) {
#{ UsersOnly = $true }
} elseif ($choice -eq 2) {
#{ ComputersOnly = $true }
}
# skipping some stuff
Search-ADAccount -AccountInactive -TimeSpan 90 #account
I also put the $account assignment on the left side of the if instead of inside, but that's your choice.
I've created a proxy function for Remove-Item, which deletes to the recycle bin instead of permanently (using the proxy so that I can seamlessly replace the rm alias, without breaking 3rd party scripts).
However, it doesn't work when a file is piped into the function. The heart of the proxy function is this:
if ($PSBoundParameters['DeletePermanently'] -or $PSBoundParameters['LiteralPath'] -or $PSBoundParameters['Filter'] -or $PSBoundParameters['Include'] -or $PSBoundParameters['Exclude'] -or $PSBoundParameters['Recurse'] -or $PSBoundParameters['Force'] -or $PSBoundParameters['Credential']) {
if ($PSBoundParameters['DeletePermanently']) { $PSBoundParameters.Remove('DeletePermanently') | Out-Null }
$scriptCmd = {& $wrappedCmd #PSBoundParameters }
} else {
$scriptCmd = {& Recycle-Item -Path $PSBoundParameters['Path'] }
}
So, my custom Recycle-Item function is only called if Path is the only parameter. So, something like Get-ChildItem .\temp\ | rm -DeletePermanently works just fine, but Get-ChildItem .\temp\ | rm has an error because the Path passed to Recycle-Item is $null.
I've tried passing $Path instead of $PSBoundParameters['Path'] and tried splatting #PSBoundParameters like the call to $wrappedCmd above, but none of it appears to do much good. I've copied the params from this function to Recycle-Item, to ensure that it is expecting input from the pipeline, but that doesn't seem to help either. Some of those changes appear to pass along the file name, but not the full path, so I don't know if there's some magic inside Remove-Item that I need to replicate to handle a file object from the pipeline.
Recycle-Item is just a basic function:
function Recycle-Item($Path) {
$item = Get-Item $Path
$directoryPath = Split-Path $item -Parent
$shell = new-object -comobject "Shell.Application"
$shellFolder = $shell.Namespace($directoryPath)
$shellItem = $shellFolder.ParseName($item.Name)
$shellItem.InvokeVerb("delete")
}
As mentioned in the comments, the provider cmdlets usually bind on LiteralPath when you pipe objects between them. This way allows Path to support wildcard globbing without the chance of passing ambiguous item paths between cmdlets.
Remove-Item has only two parameter sets, and they are named after their mandatory parameters, Path and LiteralPath
To solve your problem, simply check for all defined parameters that are not one of these two, then pass the appropriate value to Remove-Item based on the $PSCmdlet.ParameterSetName value:
if(#($PSBoundParameters.Keys |Where-Object {#('DeletePermanently','Filter','Include','Exclude','Recurse','Force','Credential') -contains $_}).Count -ge 1){
# a parameter other than the Path/LiteralPath or the common parameters was specified, default to Remove-Item
if ($PSBoundParameters['DeletePermanently']) {
$PSBoundParameters.Remove('DeletePermanently') | Out-Null
}
$scriptCmd = {& $wrappedCmd #PSBoundParameters }
} else {
# apart from common parameters, only Path/LiteralPath was specified, go for Recycle-Item
$scriptCmd = {& Recycle-Item -Path $PSBoundParameters[$PSCmdlet.ParameterSetName] }
}
I'm new to PowerShell and have a script which loops through Active Directory searching for certain computers. I get several variables and then run functions to check things like WMI and registry settings.
In the console, my script runs great and simple Write-Host command prints the data on the screen as I want. I know about Export-Csv when using the pipeline...but I'm not looking to print from the pipeline.
I want to write the variables to a text file, continue the loop, and check the next computer in AD...output the next iteration of the same variables on the next line. Here is my Write-Host:
Write-Host ($computer)","($Speed)","($Regcheck)","($OU)
Output file:
$computer,$Speed,$Regcheck | out-file -filepath C:\temp\scripts\pshell\dump.txt -append -width 200
It gives me the data, but each variable is on its own line. Why? I'd like all the variables on one line with comma separation. Is there a simple way to do this akin to VB writeline? My PowerShell version appears to be 2.0.
Use this:
"$computer, $Speed, $Regcheck" | out-file -filepath C:\temp\scripts\pshell\dump.txt -append -width 200
I usually construct custom objects in these loops, and then add these objects to an array that I can easily manipulate, sort, export to CSV, etc.:
# Construct an out-array to use for data export
$OutArray = #()
# The computer loop you already have
foreach ($server in $serverlist)
{
# Construct an object
$myobj = "" | Select "computer", "Speed", "Regcheck"
# Fill the object
$myobj.computer = $computer
$myobj.speed = $speed
$myobj.regcheck = $regcheck
# Add the object to the out-array
$outarray += $myobj
# Wipe the object just to be sure
$myobj = $null
}
# After the loop, export the array to CSV
$outarray | export-csv "somefile.csv"
You can concatenate an array of values together using PowerShell's `-join' operator. Here is an example:
$FilePath = '{0}\temp\scripts\pshell\dump.txt' -f $env:SystemDrive;
$Computer = 'pc1';
$Speed = 9001;
$RegCheck = $true;
$Computer,$Speed,$RegCheck -join ',' | Out-File -FilePath $FilePath -Append -Width 200;
Output
pc1,9001,True
$computer,$Speed,$Regcheck will create an array, and run out-file ones per variable = they get seperate lines. If you construct a single string using the variables first, it will show up a single line. Like this:
"$computer,$Speed,$Regcheck" | out-file -filepath C:\temp\scripts\pshell\dump.txt -append -width 200
The simple solution is to avoid creating an array before piping to Out-File. Rule #1 of PowerShell is that the comma is a special delimiter, and the default behavior is to create an array. Concatenation is done like this.
$computer + "," + $Speed + "," + $Regcheck | out-file -filepath C:\temp\scripts\pshell\dump.txt -append -width 200
This creates an array of three items.
$computer,$Speed,$Regcheck
FYKJ
100
YES
vs. concatenation of three items separated by commas.
$computer + "," + $Speed + "," + $Regcheck
FYKJ,100,YES
I was lead here in my Google searching. In a show of good faith I have included what I pieced together from parts of this code and other code I've gathered along the way.
# This script is useful if you have attributes or properties that span across several commandlets
# and you wish to export a certain data set but all of the properties you wish to export are not
# included in only one commandlet so you must use more than one to export the data set you want
#
# Created: Joshua Biddle 08/24/2017
# Edited: Joshua Biddle 08/24/2017
#
$A = Get-ADGroupMember "YourGroupName"
# Construct an out-array to use for data export
$Results = #()
foreach ($B in $A)
{
# Construct an object
$myobj = Get-ADuser $B.samAccountName -Properties ScriptPath,Office
# Fill the object
$Properties = #{
samAccountName = $myobj.samAccountName
Name = $myobj.Name
Office = $myobj.Office
ScriptPath = $myobj.ScriptPath
}
# Add the object to the out-array
$Results += New-Object psobject -Property $Properties
# Wipe the object just to be sure
$myobj = $null
}
# After the loop, export the array to CSV
$Results | Select "samAccountName", "Name", "Office", "ScriptPath" | Export-CSV "C:\Temp\YourData.csv"
Cheers