powershell condition not processing all values in an array - arrays

I am newb in powershell but keen to put it into good use. I am working on a script which should do the following:
Check for the existence of a specific folder in a specific location (mapped drive)
If the folder exists, then return a listing
If the folder does not exist, then create it.
Ideally, I would like to improve it in terms of check-if exists-remove-item (subdir); check-if not exists-create
This is to facilitate the automation of an archiving process for a specific piece of software. What I have right now sort of works but I cannot figure out how to make it do exactly what I want.
Here is the code:
$X = #("Server1", "Server2", "Server3")
$ChkFile = "f:\archive\$server\AABackup"
$myFolder = "f:\archive\$server"
$DirExists = Test-Path $ChkFile
Foreach ($server in $x){
IF ($DirExists -eq $True) {
Remove-Item $ChkFile -recurse
import-Module "AppAssurePowerShellModule"
start-archive -protectedserver $server -startdate "18/03/2013 5:30 PM" -path "f:\archive\$server"
}
Elseif ($DirExists -ne $True) {
New-Item -path $myFolder -itemType "directory"
import-Module "AppAssurePowerShellModule"
start-archive -protectedserver $server -startdate "18/03/2013 5:30 PM" -path "f:\archive\$server"
}
}
Yes I know it is rough... It's my first attempt though so I could definitely benefit from the more experienced scripters here.
Thanks in advance.

You're setting $ChkFile, $myFolder and $DirExists before the loop, which means that $server doesn't have a value yet, and -- since variables are evaluated immediately -- these variables will contain garbage.
You need to move those three statements inside the foreach loop.
You also don't need to compare -eq $true; this would be simpler:
if ($dirExists) {
# ...
}
else {
# ...
}
Oh, and you only need to import the module once -- do it at the top of the script.
Also, in terms of style: PowerShell keywords should generally be in lowercase: foreach, if, elseif; be consistent when invoking cmdlets (you have a mixture of lower-case and Upper-Case and lower-Case. Note that these don't make any real difference, but using a consistent style makes the code easier to read for someone else coming to it. I'm basing those rules on what I've seen on TechNet, PoshCode, and here, by the way -- they're definitely subjective.
And, personally, I use $lowerCase for local variables, and $UpperCase for parameters (because it makes the auto-generated help text look nicer).

Give this a shot.
$specificPath = Z:\MYDir\MySubDir
if(!(Test-Path $SpecificPath))
{
Mkdir $SpecificPath
}
Else
{
Get-ChildItem $specificPath
}
Explanation:
This checks for the existence of the path contained in $SpecificPath using Test-Path, which will return a Boolean value.
Since I used the (!()) syntax in my IF statement, it will try to evaluate the statement to false, IF the path DOES NOT EXIST, it will run the first block of code.
MkDir is an alias for New-ItemProperty, if you pass just a path to Mkdir it will make a directory, similar to the windows MkDir command.
If the statement contained in the IF statement does not evaluate to false, the ELSE block will run, and execute a get-childitem on the $specificpath variable.

Related

Optimizing Powershell Script to find old files and delete them on DFS replicated folders

Here is the story. I have a fileshare that is replicated between 2 servers located in different places in the world. DFS will not replicate a file if it has only been viewed, but I wouldn't want to delete that file/folder because it was used within the time period I have set (7 days). So to make sure that I don't remove still used files I have to check both locations for their LastAccessTime.
I currently have this
Set-ExecutionPolicy RemoteSigned
$limit = (Get-Date).AddDays(-7)
$PathOne = "FirstPath"
$PathTwo = "SecondPath"
$ToBeDeletedPathOne = Get-ChildItem -Path $PathOne -Recurse -Force | Where-Object { !$_.PSIsContainer -and $_.LastAccessTime -lt $limit }
$TobeDeletedPathTwo = Get-ChildItem -Path $PathTwo -Recurse -Force | Where-Object { !$_.PSIsContainer -and $_.LastAccessTime -lt $limit }
$DiffObjects = Compare-Object -referenceobject $ToBeDeletedPathOne -differenceobject $ToBeDeletedPathTwo -IncludeEqual
$ToBeDeletedOverall = DiffObjects | where {$_.SideIndicator -eq "=="}
After this, I loop through and delete the files that are marked for deletion by both locations.
Part of the problem I have is that there are a tremendous amount of files and this can take a very long time. So I wanted to make it better/faster. My idea is to have this script run the scan as a different script on each FS server and wait for them to return the output. That way it can scan on the local machine easier than remotely and it would scan both locations simultaneously.
The other part of the problem comes in with the fact that I have no idea how to do this. I will continue to work on this and if I solve it, I will post here in case anyone in the future finds this useful.
You could run everything locally. Copy the script to the machines you want (make a script to copy them if you need to) then use something like PSTools to kick them off on the local machines. This should essentially run the script simultaneously on all machines.

Running Get-ChildItem on UNC path works in Powershell but not in Powershell run in batch file

I am writing a batch file that executes a Powershell script that at one point loops items with UNC paths as attributes and uses Get-ChildItem on those paths. In a minimal version, this is what is happening in my scripts:
Master.bat
powershell -ExecutionPolicy ByPass -File "Slave.ps1"
Slave.ps1
$foo = #{Name = "Foo"}
$foo.Path = "\\remote-server\foothing"
$bar = #{Name = "Bar"}
$bar.Path = "\\remote-server\barthing"
#( $foo, $bar ) | ForEach-Object {
$item = Get-ChildItem $_.Path
# Do things with item
}
The problem I'm running into is that when I run Master.bat, it fails at Get-ChildItem with an error along the lines of
get-childitem : Cannot find path '\\remote-server\foothing' because it does not exist.
However, it seems to work perfectly fine if I run the Slave.ps1 file directly using Powershell. Why might this be happening only when the Master.bat file is run?
Things I have tried
Prepending the UNC paths with FileSystem:: with providers http://powershell.org/wp/2014/02/20/powershell-gotcha-unc-paths-and-providers/
Making sure there are no strange characters in the actual paths
Using the -literalPath parameter instead of the plain -path parameter for Get-ChildItem
Running Get-ChildItem \\remote-server\foothing in PowerShell and succeeding to verify connection to the remote server
I have found this issue when running scripts referring to UNC paths - but the error only occurs when the root of the script is set to a non file system location. e.g. PS SQLSEVER\
So the following fails with the same error:
cd env:
$foo = #{Name = "Foo"}
$foo.Path = "\\remote-server\foothing"
$bar = #{Name = "Bar"}
$bar.Path = "\\remote-server\barthing"
#( $foo, $bar ) | ForEach-Object {
$item = Get-ChildItem $_.Path
# Do things with item
Write-Host $item
}
So my resolution was to ensure that the PS prompt was returned to a file system location before executing this code. e.g.
cd env:
$foo = #{Name = "Foo"}
$foo.Path = "\\remote-server\foothing"
$bar = #{Name = "Bar"}
$bar.Path = "\\remote-server\barthing"
cd c: #THIS IS THE CRITICAL LINE
#( $foo, $bar ) | ForEach-Object {
$item = Get-ChildItem $_.Path
# Do things with item
Write-Host $item
}
I hope this helps - I would be very happy with the bounty as this is my first answer on stack overflow.
P.S. I forgot to add - the PS command prompt root may be set by auto loaded modules in the configuration of your machine. I would check with Get-Location to see if you are actually executng from a non FileSystem location.
Rory's answer provides an effective workaround, but there's a solution that doesn't require changing the current location to a FileSystem provider location first:
Prefix your UNC paths with FileSystem:: to ensure that they are recognized correctly, irrespective of the current location:
$foo = #{
Name = "Foo"
Path = "FileSystem::\\remote-server\foothing"
}
$bar = #{
Name = "Bar"
Path = "FileSystem::\\remote-server\barthing"
}
Alternatively, here is a tweak to Rory's answer to avoid changing the current location session-globally (to preserve whatever the current location is), using Push-Location and Pop-Location:
try {
# Switch to the *filesystem provider's* current location, whatever it is.
Push-Location (Get-Location -PSProvider FileSystem)
# Process the paths.
$foo, $bar | ForEach-Object {
$item = Get-ChildItem $_.Path
# Do things with item
}
} finally {
# Restore the previous location.
Pop-Location
}
Optional background information
This excellent blog post explains the underlying problem (emphasis added):
PowerShell doesn't recognize [UNC paths] as "rooted" because they're not on a PSDrive; as such, whatever provider is associated with PowerShell's current location will attempt to handle them.
Adding prefix FileSystem:: unambiguously identifies the path as being a FileSystem provider path, irrespective of the provider underlying the current location.
I read somewhere else about the Push-Location and Pop-Location commands to counter this kind of problem - I landed on your question while manually, step-by-step, testing a new routine where the script has push/pop, but I forgot to do them on my PS window. After checking #Rory's answer I noticed I was on PS SQLServer:\ instead of PS C:\ prompt.
So a way to use this on your "slave" script would be:
$foo = #{Name = "Foo"}
$foo.Path = "\\remote-server\foothing"
$bar = #{Name = "Bar"}
$bar.Path = "\\remote-server\barthing"
#( $foo, $bar ) | ForEach-Object {
$item = Get-ChildItem $_.Path
Push-Location
# Do things with item
Pop-Location
}
Thought of adding the Push/Pop before and after the # Do things because it seems that it's those things that change the location.

Use Powershell to run Psexec command

First post so please don't hurt me. I've searched around but can't seem to find a way to do what I want. I've made a script that copies a folder I have to numerous computers at \$computer\c$. In these folders is a batch file that runs an .exe. What I want to do is have Powershell pull from the same computers.txt that I used to copy the folder and then use psexec to run the batch file. I could do this all manually but scripting it seems to be a problem, here's what I thought would work but apparently not.
$computers = gc "C:\scripts\computers.txt"
foreach ($computer in $computers) {
if (test-Connection -Cn $computer -quiet) {
cd C:\pstools
psexec \\%computer cmd
C:\Folder\install.bat"
} else {
"$computer is not online"
}
}
Ok, let's take it from the top then.
$computers = gc "C:\scripts\computers.txt"
That loads the contents of the "computers.txt" file into the variable $computers. Simple enough, no issues there.
Next we have a ForEach loop. It splits up the contents of $computers and processes each line (presumably the name of a computer) as $computer against all the code within the curly braces.
foreach ($computer in $computers) {
That loop starts up with a standard If-Then statement. If (condition) then {do stuff}. In this case it is testing to see if the $computer is available on the network. If it is, then it attempts to run PSExec on it. If it isn't online it runs the Else clause, we'll get to that in a second.
if (test-Connection -Cn $computer -quiet) {
Then it changes directory. Kind of pointless, but ok, whatever. You could have just called it explicitly, such as C:\PSTools\PSExec.exe <arguments> and saved a line, but there's really no harm done.
cd C:\pstools
Then you are calling PSExec, though there's a little syntax error here. It should be $computer and not %computer. Also, it should just have the command you want to execute, not cmd and the command on a second line. You may have better results if you use the Call operator (&) to make powershell realize that it's trying to execute something and not run a cmdlet or function or what not.
& psexec \\$computer C:\Folder\install.bat
After that is the Else clause that says if the computer isn't online to write the string "$computer is not online" followed by closing braces for the Else clause and the ForEach loop.
} else {
"$computer is not online"
}
}
Edit: Ok, your finished script should look something like this (enclosed target in quotes in case there are spaces in the path):
$computers = gc "C:\scripts\computers.txt"
foreach ($computer in $computers) {
if (test-Connection -Cn $computer -quiet) {
& C:\pstools\psexec.exe \\$computer "C:\folder\install.bat"
} else {
"$computer is not online"
}
}
I realize this question is from 2014, and this answer will not directly address the question the user asked. But for people who come across this question these days, I want to throw out there that you don't need to use PSExec if you're using PowerShell*. Since you're already in PowerShell, just use Invoke-Command.
Syntax would be
Invoke-Command -ComputerName $Computer -ScriptBlock { C:\Folder\install.bat }
It's really that easy.
*Requires PowerShell remoting to be enabled on the target server. << Some people are using PSExec to enable PSRemoting by running winrm quickconfig... So it's still a valid question and the two things "PSExec" and "PSRemoting" are different.
Give this a try. You had a % where you wanted a $. Also the cmd.exe call is unnecessary.
$computers = gc "C:\scripts\computers.txt"
foreach ($computer in $computers) {
if (test-Connection -Cn $computer -quiet) {
cd C:\pstools
psexec \\$computer "C:\Folder\install.bat"
} else {
"$computer is not online"
}
}
use & (call) operator , &psexec \$computer "C:\Folder\install.bat"

Combine the results of two distinct Get-ChildItem calls into single variable to do the same processing on them

I'm trying to write a PowerShell script to build a list of files, from several directories. After all directories have been added to the main list, I'd like to do the same processing on all files.
This is what I have:
$items = New-Object Collections.Generic.List[IO.FileInfo]
$loc1 = #(Get-ChildItem -Path "\\server\C$\Program Files (x86)\Data1\" -Recurse)
$loc2 = #(Get-ChildItem -Path "\\server\C$\Web\DataStorage\" -Recurse)
$items.Add($loc1) # This line fails (the next also fails)
$items.Add($loc2)
# Processing code is here
which fails with this error:
Cannot convert argument "0", with
value: "System.Object[]", for "Add" to
type "System.IO.FileInfo": "Cannot
convert the "System.Object[]" va lue
of type "System.Object[]" to type
"System.IO.FileInfo"."
I am mostly interested in what is the correct approach for this type of situation. I realize that my code is a very C way of doing it -- if there is a more PowerShell way to acomplish the same task, I'm all for it. The key, is that the number of $loc#'s may change over time, so adding and removing one or two should be easy in the resulting code.
Not sure you need a generic list here. You can just use a PowerShell array e.g.:
$items = #(Get-ChildItem '\\server\C$\Program Files (x86)\Data1\' -r)
$items += #(Get-ChildItem '\\server\C$\Web\DataStorage\' -r)
PowerShell arrays can be concatenated using +=.
From get-help get-childitem:
-Path
Specifies a path to one or more locations. Wildcards are permitted. The default location is the current directory (.).
$items = get-childitem '\\server\C$\Program Files (x86)\Data1\','\\server\C$\Web\DataStorage\' -Recurse
Here is some perhaps even more PowerShell-ish way that does not need part concatenation or explicit adding items to the result at all:
# Collect the results by two or more calls of Get-ChildItem
# and perhaps do some other job (but avoid unwanted output!)
$result = .{
# Output items
Get-ChildItem C:\TEMP\_100715_103408 -Recurse
# Some other job
$x = 1 + 1
# Output some more items
Get-ChildItem C:\TEMP\_100715_110341 -Recurse
#...
}
# Process the result items
$result
But the code inside the script block should be written slightly more carefully to avoid unwanted output mixed together with file system items.
EDIT: Alternatively, and perhaps more effectively, instead of .{ ... } we can
use #( ... ) or $( ... ) where ... stands for the code containing several
calls of Get-ChildItem.
Keith's answer is the PowerShell way: just use #(...)+#(...).
If you actually do want a typesafe List[IO.FileInfo], then you need to use AddRange, and cast the object array to a FileInfo array -- you also need to make sure you don't get any DirectoryInfo objects, or else you need to use IO.FileSystemInfo as your list type:
So, avoid directories:
$items = New-Object Collections.Generic.List[IO.FileInfo]
$items.AddRange( ([IO.FileSystemInfo[]](ls '\\server\C$\Program Files (x86)\Data1\' -r | Where { -not $_.PSIsContainer } )) )
$items.AddRange( ([IO.FileSystemInfo[]](ls '\\server\C$\Web\DataStorage\' -r | Where { -not $_.PSIsContainer } )) )
Or use FileSystemInfo (the common base class of FileInfo and DirectoryInfo):
$items = New-Object Collections.Generic.List[IO.FileSystemInfo]
$items.AddRange( ([IO.FileSystemInfo[]](ls '\\server\C$\Program Files (x86)\Data1\' -r)) )
$items.AddRange( ([IO.FileSystemInfo[]](ls '\\server\C$\Web\DataStorage\' -r)) )
-Filter is more performant than -Include, so if you don't have a lot of different extensions, simply concatenating two filtered lists might be faster.
$files = Get-ChildItem -Path "H:\stash\" -Filter *.rdlc -Recurse
$files += Get-ChildItem -Path "H:\stash\" -Filter *.rdl -Recurse
I compared the output with a timer like this:
$stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
# Do Stuff Here
$stopwatch.Stop()
Write-Host "$([Math]::Round($stopwatch.Elapsed.TotalSeconds)) seconds ellapsed"

How to change read attribute for a list of files?

I am powershell newbie. I used a sample script and made substitute from get-item to get-content in the first line.
The modified script looks like below:
$file = get-content "c:\temp\test.txt"
if ($file.IsReadOnly -eq $true)
{
$file.IsReadOnly = $false
}
So in essence I am trying to action items contained in test.txt stored as UNC paths
\\testserver\testshare\doc1.doc
\\testserver2\testshare2\doc2.doc
When running script no errors are reported and no action is performed even on first entry.
Short answer:
sp (gc test.txt) IsReadOnly $false
Long answer below
Well, some things are wrong with this.
$file is actually a string[], containing the lines of your file. So the IsReadOnly property applies to the string[] and not to the actual files represented by those strings, which happen to be file names.
So, if I'm understanding you correctly you are trying to read a file, containing other file names, one on each line. And clear the read-only attribute on those files.
Starting with Get-Content isn't wrong here. We definitely are going to need it:
$filenames = Get-Content test.txt
Now we have a list of file names. To access the file's attributes we either need to convert those file names into actual FileInfo objects and operate on those. Or we pass the file names to a -Path argument of Set-ItemProperty.
I will take the first approach first and then get to the other one. So we have a bunch of file names and want FileInfo objects from them. This can be done with a foreach loop (since we need to do this for every file in the list):
$files = (foreach ($name in $filenames) { Get-Item $name })
You can then loop over the file names and set the IsReadOnly property on each of them:
foreach ($file in $files) {
$file.IsReadOnly = $false
}
This was the long and cumbersome variant. But one which probably suits people best with no prior experience to PowerShell. You can reduce the need for having multiple collections of things lying around by using the pipeline. The pipeline transports objects from one cmdlet to another and those objects still have types.
So by writing
Get-Content test.txt | Get-Item | ForEach-Object { $_.IsReadOnly = $false }
we're achieving exactly the same result. We read the contents of the file, getting a bunch of strings. Those are passed to Get-Item which happens to know what to do with pipeline input: It treats those objects as file paths; exactly what we need here. Get-Item then sends FileInfo objects further down the pipeline, at which point we are looping over them and setting the read-only property to false.
Now, that was shorter and, with a little practise, maybe even easier. But it's still far from ideal. As I said before, we can use Set-ItemProperty to set the read-only property on the files. And we can take advantage of the fact that Set-ItemProperty can take an array of strings as input for its -Path parameter.
$files = Get-Content test.txt
Set-ItemProperty -Path $files -Name IsReadOnly -Value $false
We are using a temporary variable here, since Set-ItemProperty won't accept incoming strings as values for -Path directly. But we can inline this temporary variable:
Set-ItemProperty -Path (Get-Content test.txt) -Name IsReadOnly -Value $false
The parentheses around the Get-Content call are needed to tell PowerShell that this is a single argument and should be evaluated first.
We can then take advantage of the fact that each of those parameters is used in the position where Set-ItemProperty expects it to be, so we can leave out the parameter names and stick just to the values:
Set-ItemProperty (Get-Content test.txt) IsReadOnly $false
And then we can shorten the cmdlet names to their default aliases:
sp (gc test.txt) IsReadOnly $false
We could actually write $false as 0 to save even more space, since 0 is converted to $false when used as a boolean value. But I think it suffices with shortening here.
Johannes has the scoop on the theory behind the problem you are running into. I just wanted to point out that if you happen to be using the PowerShell Community Extensions you can perform this by using the Set-Writable and Set-ReadOnly commands that are pipeline aware e.g.:
Get-Content "c:\temp\test.txt" | Set-Writable
or the short, aliased form:
gc "c:\temp\test.txt" | swr
The alias for Set-ReadOnly is sro. I use these commands weekly if not daily.

Resources