I am attempting to create a powershell script that can perform the following:
1-Filter by Get-Group on a device via an installed xml file.
2-Only perform the copy function if the group equals Get-Group.
So far, the only thing I've succeeded in having it copy the first or the last file, on an incorrect device. I believe you can only use 2 IF statements a single powershell script? If so, how would I achieve this result I am looking for? I've also tried ElseIF, Else, Switch, and WhatIF with no joy. I have also tried different arguments for the IF statements. -eq, =, -like, and -match. None of which seem to be overly helpful in this situation.
$ActiveFilePath = "C:\ProgramData\JKCS\jkupdate\jku.ini"
Function Get-Group
{$Group = ([xml](Get-Content D:\Tools\SystemInformation\SystemInformation.xml)).'system-information'.'device-group'}
Get-Group
If ($Group -eq "XXXXX101Master")
{Copy-Item ".\JKU Files\101\jku.ini" "$ActiveFilePath"}
#----------------------------------------------#
If ($Group -eq "XXXXX102Master")
{Copy-Item ".\JKU Files\102\jku.ini" "$ActiveFilePath"}
#----------------------------------------------#
If ($Group -eq "XXXXX103Master")
{Copy-Item ".\JKU Files\103\jku.ini" "$ActiveFilePath"}
#----------------------------------------------#
If ($Group -eq "XXXXX104Master")
{Copy-Item ".\JKU Files\104\jku.ini" "$ActiveFilePath"}
#----------------------------------------------#
If ($Group -eq "XXXXX105Master")
{Copy-Item ".\JKU Files\105\jku.ini" "$ActiveFilePath"}
#----------------------------------------------#
If ($Group -eq "XXXXX106Master")
{Copy-Item ".\JKU Files\106\jku.ini" "$ActiveFilePath"}
The assignment to $Group during the execution of your Get-Group function is not visible to the rest of the script. You could have the function return the value instead, and then assign it to $Group outside the function, like this:
Function Get-Group
{return ([xml](Get-Content D:\Tools\SystemInformation\SystemInformation.xml)).'system-information'.'device-group'}
$Group = Get-Group
Or, use a scope modifier in the assignment within the function to make it visible to the whole script:
Function Get-Group
{ $Script:Group = ([xml](Get-Content D:\Tools\SystemInformation\SystemInformation.xml)).'system-information'.'device-group'}
Get-Group
Or, if you don't need the function anywhere else, just directly assign $Group like this (at Script scope):
$Group = ([xml](Get-Content D:\Tools\SystemInformation\SystemInformation.xml)).'system-information'.'device-group'
See the help document "about_Scopes" (i.e. run help about_Scopes).
I would change your Get-Group function somewhat to use
function Get-Group {
# if you load the xml file this way, you are ensured to get the file encoding correct
$xml = [System.Xml.XmlDocument]::new()
$xml.Load('D:\Tools\SystemInformation\SystemInformation.xml')
$xml.'system-information'.'device-group'
}
Then, if all your groups have the same format of "XXXXX" + numeric value + "Master" you could do this:
$group = Get-Group
$subfolder = [regex]::Match($group, '(\d+)Master$').Groups[1].Value
if (![string]::IsNullOrWhiteSpace($subfolder)) {
$path = Join-Path '.\JKU Files' -ChildPath ('{0}\jku.ini' -f $subfolder)
Copy-Item -Path $path -Destination $ActiveFilePath
}
else {
Write-Warning "Could not find a matching subfolder name in '$group'"
}
Otherwise, if the group names do not all have the same format, I would suggest creating a lookup Hashtable where you can match the group name with the path of the ini file
$lookup = #{
'XXXXX101Master' = 101
'XYZ102Demo' = 102
'103Copy' = 103
'X123456789104Huh' = 104
# etc.
}
$group = Get-Group
if ($lookup.ContainsKey($group)) {
$subfolder = $lookup[$group]
$path = Join-Path '.\JKU Files' -ChildPath ('{0}\jku.ini' -f $subfolder)
Copy-Item -Path $path -Destination $ActiveFilePath
}
else {
Write-Warning "Could not find a matching subfolder name in '$group'"
}
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'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.
1. Code Description alias how it is intended to work
User enters a path to a directory in PowerShell. Code checks if any folder within the declared directory contains no data at all. If so, the path of any empty folder will be shown on the prompt to the user and eventually removed from the system.
2. The Issue alias what I am struggling with
The code I just wrote doesn't count the depth of a folder hierarchy as I would expect (the column in the output table is blank). Besides that, the program works okay - I've still got to fix the issue where my code removes empty parent directories first and child directories later, which of course will cause an error in PowerShell; for instance, take
C:\Users\JohnMiller\Desktop\Homework
where Homework consists of Homework\Math\School Project and Homework\Computer Science\PowerShell Code. Note that all directories are supposed to be empty with the exception of PowerShell Code, the folder containing this script. (Side note: A folder is considered empty when no file dwells inside. At least that's what my code is based on for now.)
3. The Code
# Delete all empty (sub)folders in [$path]
[Console]::WriteLine("`n>> Start script for deleting all empty (sub)folders.")
$path = Read-Host -prompt ">> Specify a path"
if (test-path $path)
{
$allFolders = Get-ChildItem $path -recurse | Where {$_.PSisContainer -eq $True}
$allEmptyFolders = $allFolders | Where-Object {$_.GetFiles().Count -eq 0}
$allEmptyFolders | Select-Object FullName,#{Name = "FolderDepth"; Expression = {$_.DirectoryName.Split('\').Count}} | Sort-Object -descending FolderDepth,FullName
[Console]::WriteLine("`n>> Do you want do remove all these directories? Validate with [True] or [False].") #'#
$answer = Read-Host -prompt ">> Answer"
if ([System.Convert]::ToBoolean($answer) -eq $True)
{
$allEmptyFolders | Remove-Item -force -recurse
}
else
{
[Console]::WriteLine(">> Termination confirmed.`n")
exit
}
}
else
{
[Console]::WriteLine(">> ERROR: [$($path)] is an invalid directory. Program terminates.`n")
exit
}
The depth-count problem:
Your code references a .DirectoryName property in the calculated property passed to Select-Object, but the [System.IO.DirectoryInfo] instances output by Get-ChildItem have no such property. Use the .FullName property instead:
$allEmptyFolders |
Select-Object FullName,#{Name='FolderDepth'; Expression={$_.FullName.Split('\').Count}} |
Sort-Object -descending FolderDepth,FullName
Eliminating nested empty subfolders:
To recap your problem with a simple example:
If c:\foo is empty (no files) but has empty subdir. c:\foo\bar, your code outputs them both, and if you then delete c:\foo first, deleting c:\foo\bar next fails (because deleting c:\foo also removed c:\foo\bar).
If you eliminate all nested empty subdirs. up front, you not only declutter what you present to the user, but you can then safely iterative of the output and delete one by one.
With your approach you'd need a 2nd step to eliminate the nested empty dirs., but here's a depth-first recursive function that omits nested empty folders. To make it behave the same way as your code with respect to hidden files, pass -Force.
function Get-RecursivelyEmptyDirectories {
[cmdletbinding()]
param(
[string] $LiteralPath = '.',
[switch] $Force,
[switch] $DoNotValidatePath
)
$ErrorActionPreference = 'Stop'
if (-not $DoNotValidatePath) {
$dir = Get-Item -LiteralPath $LiteralPath
if (-not $dir.PSIsContainer) { Throw "Not a directory path: $LiteralPath" }
$LiteralPath = $dir.FullName
}
$haveFiles = [bool] (Get-ChildItem -LiteralPath $LiteralPath -File -Force:$Force | Select-Object -First 1)
$emptyChildDirCount = 0
$emptySubdirs = $null
if ($childDirs = Get-ChildItem -LiteralPath $LiteralPath -Directory -Force:$Force) {
$emptySubDirs = New-Object System.Collections.ArrayList
foreach($childDir in $childDirs) {
if ($childDir.LinkType -eq 'SymbolicLink') {
Write-Verbose "Ignoring symlink: $LiteralPath"
} else {
Write-Verbose "About to recurse on $($childDir.FullName)..."
try { # If .AddRange() fails due to exceeding the array list's capacity, we must fail too.
$emptySubDirs.AddRange(#(Get-RecursivelyEmptyDirectories -DoNotValidatePath -LiteralPath $childDir.FullName -Force:$Force))
} catch {
Throw
}
# If the last entry added is the child dir. at hand, that child dir.
# is by definition itself empty.
if ($emptySubDirs[-1] -eq $childDir.FullName) { ++$emptyChildDirCount }
}
} # foreach ($childDir ...
} # if ($childDirs = ...)
if (-not $haveFiles -and $emptyChildDirCount -eq $childDirs.Count) {
# There are no child files and all child dirs., if any, are themselves
# empty, so we only output the input path at hand, as the highest
# directory in this subtree that is empty (save for empty descendants).
$LiteralPath
} else {
# This directory is not itself empty, so output the (highest-level)
# descendants that are empty.
$emptySubDirs
}
}
Tips regarding your code:
Get-ChildItem -Directory is available in PSv3+, which is not only shorter but also more efficient than Get-ChildItem | .. Where { $_.PSisContainer -eq $True }.
Use Write-Host instead of [Console]::WriteLine
[System.Convert]::ToBoolean($answer) only works with the culture-invariant string literals 'True' and 'False' ([bool]::TrueString and [bool]::FalseString, although case variations and leading and trailing whitespace are allowed).
I have a function that will change the filename and move it to a specific computer. The function is duplicated for another computer, so I now have two specific functions for each filename. I can't seem to figure out how to get the script to choose the function I want it to use. Here is what I've tried with no luck:
$files = dir -Path \\server\PPTV\*\*.* -Include atrium.*, clirel.* -Recurse {
(If ($file.FullName -eq "atrium.*" | Rename-Copy-Atrium),
(If ($file.FullName -eq "clirel.*" | Rename-Copy-Clirel)))
}
How can I get this to pipe to the function and run? Rename-Copy-Atrium for example is one of the function names.
The proper syntax for an if() statement in powershell is:
if([condition])
{
[execute this code]
}
So you would want to do something like:
$files = dir -Path \\server\PPTV\*\*.* -Include atrium.*, clirel.* -Recurse
foreach($file in $files)
{
if($file.FullName -like "atrium.*")
{
# I assume the function takes a fileinfo object as a parameter argument
Rename-Copy-Atrium $file
}
elseif($file.FullName -like "clirel.*")
{
Rename-Copy-Clirel $file
}
}
I'm facing the problem of moving and copying some items on the file system with PowerShell.
I know by experiments the fact that, even with PowerShell v3, the cmdlet Copy-Item, Move-Item and Delete-Item cannot handle correctly reparse point like junction and symbolic link, and can lead to disasters if used with switch -Recurse.
I want to prevent this evenience. I have to handle two or more folder each run, so I was thinking to something like this.
$Strings = #{ ... }
$ori = Get-ChildItem $OriginPath -Recurse
$dri = Get-ChildItem $DestinationPath -Recurse
$items = ($ori + $dri) | where { $_.Attributes -match 'ReparsePoint' }
if ($items.Length -gt 0)
{
Write-Verbose ($Strings.LogExistingReparsePoint -f $items.Length)
$items | foreach { Write-Verbose " $($_.FullName)" }
throw ($Strings.ErrorExistingReparsePoint -f $items.Length)
}
This doen't work because $ori and $dri can be also single items and not arrays: the op-Addition will fail. Changing to
$items = #(#($ori) + #($dri)) | where { $_.Attributes -match 'ReparsePoint' }
poses another problem because $ori and $dri can also be $null and I can end with an array containing $null. When piping the join resutl to Where-Object, again, I can end with a $null, a single item, or an array.
The only apparently working solution is the more complex code following
$items = $()
if ($ori -ne $null) { $items += #($ori) }
if ($dri -ne $null) { $items += #($dri) }
$items = $items | where { $_.Attributes -match 'ReparsePoint' }
if ($items -ne $null)
{
Write-Verbose ($Strings.LogExistingReparsePoint -f #($items).Length)
$items | foreach { Write-Verbose " $($_.FullName)" }
throw ($Strings.ErrorExistingReparsePoint -f #($items).Length)
}
There is some better approch?
I'm interested for sure if there is a way to handle reparse point with PowerShell cmdlets in the correct way, but I'm much more interested to know how to join and filters two or more "PowerShell collections".
I conclude observing that, at present, this feature of PowerShell, the "polymorphic array", doen't appear such a benefit to me.
Thanks for reading.
Just add a filter to throw out nulls. You're on the right track.
$items = #(#($ori) + #($dri)) | ? { $_ -ne $null }
I've been on Powershell 3 for a while now but from what I can tell this should work in 2.0 as well:
$items = #($ori, $dri) | %{ $_ } | ? { $_.Attributes -match 'ReparsePoint' }
Basically %{ $_ } is a foreach loop that unrolls the inner arrays by iterating over them and passing each inner element ($_) down the pipeline. Nulls will automatically be excluded from the pipeline.