Powershell folder creation - powershell

I have a powershell script that creates folders on our NAS for each student according to their student numbers. The names for the folders comes from a .csv file that I import from the server. This is what I have got:
Set-Location "C:\studentdata"
$StudFolders = import-csv \\servername\datafolder\studfolders.csv
ForEach ($StudFolders in $StudFolders) {
if(Test-Path -Path C:\Studentdata\$StudFolders) {
New-Item $StudFolders.Name -type directory
}else{
"Folders already created"
}
}
This script works great, if I run it only once. If I run it again, I get errors in the console window about the folders already existing. What I want to do is catch the errors with the IF part of the script, but I am not sure if I have the correct usage of the IF for powershell. This will help if I edit the .csv with more student number it will display without errors.
Can someone point me in the right direction?
EDIT:
This is what I have in the studfolders.csv
Name
2003040052
2003060213
2003060310
2003060467

Lets take a look at your logic:
you are using an if statement, which works if something returns true
you are using a test-path cmdlet which returns true if something exists
See the problem? you need to do it vice versa:
if (!(test-path ...)) { ... } # ! - is the operator to invert true to false
or you can switch if and else content, so when if executes it returns "Folders already created", and else creates folders

As a matter of coding style, I would avoid using the same variable name with different semantics, as in ($Studfolders in $Studfolders). Code like this is very hard to read a few months down the road. I generally use the singular for each object in the collection, as in ($Studfolder in $Studfolders). But if your style works for you, OK.
Previous answers have already pointed out that your logic is backwards. You need to reverse it.
Next, it doesn't look to me as though you are accessing the component of each item in the loop. When you do an Import-Csv, several things happen: The first record in the csv file is treated as a header, providing the names for the fields that follow. If there is indeed a header in your csv file, you need to reference it when you retrieve the first field from each item, even if it's the only field.
The result of an import-csv is an array of custom objects. Each custom object looks like a hashtable that contains key, value pairs. Something like this might work
Set-Location "C:\studentdata"
$StudFolders = import-csv \\servername\datafolder\studfolders.csv
ForEach ($StudFolder in $StudFolders) {
if(Test-Path -Path C:\Studentdata\$StudFolder.Name) {
"Folders already created"
}else{
New-Item $StudFolder.Name -type directory
}
}
I have presumed that the first record in the Csv file looks like this:
"Name"
That is why I referenced the field as $Studfolder.Name"
If this isn't the case, you are going to have to do something different.

Having tried everything with the script, I could not get it to catch the errors. I decided to ask my manager, and he came up with the following solution that works quite well and catches the errors.
import-csv '\\servername\datafolder\studfolders.csv'|ForEach-Object -process {
$path =$_.Name
$path='C:\Studentdata\'+$path
if (Test-Path -Path $path){
"Folder already created"
} else {
New-Item $path -type directory
}
}
Thanks to #Walter Mitty and #4c74356b41 for help try to find an answer.

Related

How do I copy a list of files and rename them in a PowerShell Loop

We are copying a long list of files from their different directories into a single location (same server). Once there, I need to rename them.
I was able to move the files until I found out that there are duplicates in the list of file names to move (and rename). It would not allow me to copy the file multiple times into the same destination.
Here is the list of file names after the move:
"10.csv",
"11.csv",
"12.csv",
"13.csv",
"14.csv",
"15.csv",
"16.csv",
"17.csv",
"18.csv",
"19.csv",
"20.csv",
"Invoices_Export(16) - Copy.csv" (this one's name should be "Zebra.csv")
I wrote a couple of foreach loops, but it is not working exactly correctly.
The script moves the files just fine. It is the rename that is not working the way I want. The first file does not rename; the other files rename. However, they leave the moved file in place too.
This script requires a csv that has 3 columns:
Path of the file, including the file name (eg. c:\temp\smefile.txt)
Destination of the file, including the file name (eg. c:\temp\smefile.txt)
New name of the file. Just the name and extention.
# Variables
$Path = (import-csv C:\temp\Test-CSV.csv).Path
$Dest = (import-csv C:\temp\Test-CSV.csv).Destination
$NN = (import-csv C:\temp\Test-CSV.csv).NewName
#Script
foreach ($D in $Dest) {
$i -eq 0
Foreach ($P in $Path) {
Copy-Item $P -destination C:\Temp\TestDestination -force
}
rename-item -path "$D" -newname $NN[$i] -force
$i += 1
}
There were no error per se, just not the outcome that I expected.
Welcome to Stack Overflow!
There are a couple ways to approach the duplicate names situation:
Check if the file exists already in the destination with Test-Path. If it does, start a while loop that appends a number to the end of the name and check if that exists. Increment the number you append after each check with Test-Path. Keep looping until Test-Path comes back $false and then break out of the loop.
Write an error message and skip that row in the CSV.
I'm going to show a refactored version of your script with approach #2 above:
$csv = Import-Csv 'C:\temp\Test-CSV.csv'
foreach ($row in $csv)
{
$fullDestinationPath = Join-Path -Path $row.Destination -ChildPath $row.NewName
if (Test-Path $fullDestinationPath)
{
Write-Error ("The path '$fullDestinationPath' already exists. " +
"Skipping row for $($row.Path).")
continue
}
# You may also want to check if $row.Path exists before attempting to copy it
Copy-Item -Path $row.Path -Destination $fullDestinationPath
}
Now that your question is answered, here are some thoughts for improving your code:
Avoid using acronyms and abbreviations in identifiers (variable names, function names, etc.) when possible. Remember that code is written for humans and someone else has to be able to understand your code; make everything as obvious as possible. Someone else will have to read your code eventually, even if it's Future-You™!
Don't Repeat Yourself (called the "DRY" principle). As Lee_daily mentioned in the comments, you don't need to import the CSV file three times. Import it once into a variable and then use the variable to access the properties.
Try to be consistent. PowerShell is case-insensitive, but you should pick a style and stick to it (i.e. ForEach or foreach, Rename-Item or rename-item, etc.). I would recommend PascalCase as PowerShell cmdlets are all in PascalCase.
Wrap literal paths in single quotes (or double quotes if you need string interpolation). Paths can have spaces in them and without quotes, PowerShell interprets a space as you are passing another argument.
$i -eq 0 is not an assignment statement, it is a boolean expression. When you run $i -eq 0, PowerShell will return $true or $false because you are asking it if the value stored in $i is 0. To assign the value 0 to $i, you need to write it like this: $i = 0.
There's nothing wrong with $i += 1, but it could be shortened to $i++, if you want to.
When you can, try to check for common issues that may come up with your code. Always think about what can go wrong. "If I copy a file, what can go wrong? Does the source file or folder exist? Is the name pulled from the CSV a valid path name or does it contain characters that are invalid in a path (like :)?" This is called defensive programming and it will save you so so many headaches. As with anything in life, be careful not to go overboard. Only check for likely scenarios; rare edge-cases should just raise errors.
Write some decent logs so you can see what happened at runtime. PowerShell provides a pair of great cmdlets called Start-Transcript and Stop-Transcript. These cmdlets log all the output that was sent to the PowerShell console window, in addition to some system information like the version of PowerShell installed on the machine. Very handy!

Search a directory structure and report folder present or missing?

I am looking for some help with writing the code in PowerShell to check for and report back.
I want to search C:\MyData\MyCustomerNames for folders MyTests, MyProducts, and MyReturns, and then report back C:\MyData\MyCustomerNames\MyTests is Present or C:\MyData\MyCustomerNames\MyProducts is Missing.
I know that the code Test-Path C:\MyData\MyCustomerNames\MyProductscould work but I also want to test for *.xml, *.docx, etc. Plus, the only part of the path that is a variable is MyCustomerNames.
If you can point me to something that would work, that would be awesome, or provide an example more so than what I provided.
Please and Thank you!
To expand on what sodawillow recommended. Save your locations in an array, then use the foreach statement to run the loop for each value in the array. You can use Get-ChildItem in an if statement to search for those directories and evaluate if they exist.
$folders = "MyTests", "MyProducts", "MyReturns"
foreach ($folder in $folders) {
if (Get-ChildItem -Path "C:\MyData\MyCustomerNames" -Filter $folder -Directory) {
"$folder exists"
} else {
"$folder does not exist"
}
}

Check for File in multiple directories with an IF statement Powershell

I am facing some problems in powershell. I want to be able to let a powershell command search for multiple directories.
With the name being a variable like "$VM_DISK=VM_DISK.vhdx" and let powershell search in that manor so that if that file exists in a folder such as C:\VM_DISK\ it exit the script.
I have already tried the "Get-Childitem" but it doesn't seem to work when I put my variable in it. Here is an example:
$VM_DISK= "Example.vhdx"
$search=Get-ChildItem -Path C:\VM_DISK\* -Filter $VM_DISK -Recurse
if ($search -eq $VM_DISK) {write-host "Goodbye!" exit} else {write-host "Continue"}
I just cant seem to figure out why this isn't working, hope some can figure it out.
You need to alter your if statement.
if ($search.Name -contains $VM_Disk)
This way you are comparing an Array of names (which is what you want, names of objects, not objects) to a name of particular object (to a string, basically).
This makes little sense in your case, tbh. Since $search would always include $VM_Disk or would be null if nothing was found.
So the proper way to test would be if ($search) (just like Mathias advised). Which would test if anything was returned. Which, basically equals what you are trying to do.

Create variable from CSV

I want to make variables from a particular column in a CSV.
CSV will have the following headers:
FolderName,FolderManager,RoleGroup,ManagerEmail
Under FolderName will be a list of rows with respective folder names such as: Accounts,HR,Projects, etc... (each of these names is a separate row in the FolderName column)
So I would like to create a list of variables to call on in a later stage. They would be something like the following:
$Accounts,
$HR,
$Projects,
I have done a few different scripts based on searching here and google, but unable to produce the desired results. I am hoping someone can lead me in the right direction here to create this script.
Versions of this question ("dynamic variables" or "variable variables" or "create variables at runtime") come up a lot, and in almost all cases they are not the right answer.
This is often asked by people who don't know a better way to approach their problem, but there is a better way: collections. Arrays, lists, hashtables, etc.
Here's the problem: You want to read a username and print it out. You can't write Hello Alice because you don't know what their name is to put in your code. That's why variables exist:
$name = Read-Host "Enter your name"
Write-Host "Hello $name"
Great, you can write $name in your source code, something which never changes. And it references their name, which does change. But that's OK.
But you're stuck - how can you have two people's names, if all you have is $name? How can you make many variables like $name2, $name3? How can you make $Alice, $Bob?
And you can...
New-Variable -Name (Read-Host "Enter your name") -Value (Read-Host "Enter your name again")
Write-Host "Hello
wait
What do you put there to write their name? You're straight back to the original problem that variables were meant to solve. You had a fixed thing to put in your source code, which allowed you to work with a changing value.
and now you have a varying thing that you can't use in your source code because you don't know what it is again.
It's worthless.
And the fix is that one variable with a fixed name can reference multiple values in a collection.
Arrays (Get-Help about_Arrays):
$names = #()
do {
$name = Read-Host "Enter your name"
if ($name -ne '')
{
$names += $name
}
} while ($name -ne '')
# $names is now a list, as many items long as it needs to be. And you still
# work with it by one name.
foreach ($name in $names)
{
Write-Host "Hello $name"
}
# or
$names.Count
or
$names | foreach { $_ }
And more collections, like
Hashtables (Get-Help about_Hash_Tables): key -> value pairs. Let's pair each file in a folder with its size:
$FileSizes = #{} # empty hashtable. (aka Dictionary)
Get-ChildItem *.txt | ForEach {
$FileSizes[$_.BaseName] = $_.Length
}
# It doesn't matter how many files there are, the code is just one block
# $FileSizes now looks like
#{
'readme' = 1024;
'test' = 20;
'WarAndPeace' = 1048576;
}
# You can list them with
$FileSizes.Keys
and
foreach ($file in $FileSizes.Keys)
{
$size = $FileSizes[$file]
Write-Host "$file has size $size"
}
No need for a dynamic variable for each file, or each filename. One fixed name, a variable which works for any number of values. All you need to do is "add however many there are" and "process however many there are" without explicitly caring how many there are.
And you never need to ask "now I've created variable names for all my things ... how do I find them?" because you find these values in the collection you put them in. By listing all of them, by searching from the start until you find one, by filtering them, by using -match and -in and -contains.
And yes, New-Variable and Get-Variable have their uses, and if you know about collections and want to use them, maybe you do have a use for them.
But I submit that a lot of people on StackOverflow ask this question solely because they don't yet know about collections.
Dynamic variables in Powershell
Incrementing a Dynamic Variable in Powershell
Dynamic variable and value assignment in powershell
Dynamically use variable in PowerShell
How to create and populate an array in Powershell based on a dynamic variable?
And many more, in Python too:
https://stackoverflow.com/a/5036775/478656
How can you dynamically create variables via a while loop?
Basically you want to create folders based on the values you are getting from CSV File.
(FileName has headers such as FolderName,
FolderManager,
RoleGroup,
ManagerEmail)
$File=Import-csv "FileName"
$Path="C:\Sample"
foreach ($item in $File){
$FolderName=$item.FolderName
$NewPath=$Path+"\$FolderName"
if(!(Test-Path $NewPath))
{
New-Item $NewPath -ItemType Directory
}
}
Hope this HElps.
In PowerShell, you can import a CSV file and get back custom objects. Below code snippet shows how to import a CSV to generate objects from it and then dot reference the properties on each object in a pipeline to create the new variables (your specific use case here).
PS>cat .\dummy.csv
"foldername","FolderManager","RoleGroup"
"Accounts","UserA","ManagerA"
"HR","UserB","ManagerB"
PS>$objectsFromCSV = Import-CSV -Path .\dummy.csv
PS>$objectsFromCSV | Foreach-Object -Process {New-Variable -Name $PSItem.FolderName }
PS>Get-Variable -name Accounts
Name Value
---- -----
Accounts
PS>Get-Variable -name HR
Name Value
---- -----
HR
`

Powershell - Correct Way of Checking if a directory is empty

What is the correct way of determining if a file or folder is empty in powershell? I came across this post and tried the example that the second user gave, and it worked.
Now, I've been checking if a directory is empty by using the length property, like this:
(Get-ChildItem -Path "C:\Users\someUser\Downloads\" -Recurse).Length)
However, that only returns the number of bytes that the file/folder has. I tested the length property and found that if you were to have a blank text file/folder placed in the downloads then the length property wouldn't return the correct value.
Can you use the length property to correctly determine the number of files/folders that a directory has?
Try this, it will count the number of items:
Get-ChildItem -Path "C:\Users\someUser\Downloads\" -Recurse | Measure-Object
if you want to be sure a file or folder is empty you can use the following:
for a file:
If ((Get-Content C:\YouFile.txt) -eq $Null) {
"The file is empty"
}
if the file contains white space it wont return as null, so if you want to check even white space you can use the following:
If ([string]::IsNullOrWhiteSpace((Get-Content C:\YourFile.txt))) {
"The file is empty"
}
for a folder:
If ((Get-ChildItem -Force C:\SomeFolder) -eq $Null) {
"the Folder is empty"
}
The other way to handle this is just to use the base static method in System.Io.Directory -- Directory.EnumerationFileSystemInfos() and see how many objects, if any, are returned. Note that a FileSystemInfo can be either a directory or file, so it'll work in your case.
[system.io.directory]::EnumerateFileSystemEntries("C:\temp")