Powershell Make a function for directories and add a prefix at the end - function

Make a function that makes 3 directories with the name John_S and add the prefix and appended with the number 1, 2, m
Example
1. John_S1
2. John_S2
3. John_S3
Use a loop (ForEach)
Use a variable for the number of iterations
What I have so far...
$DirName = "John_S"
function mulcheck {New-item "$DirName"}
$i = 1
foreach($DirName in $DirNames)
{$newname = $DirName Rename-Item $($DirName) $newname $i++}

The easiest way to generate the numbers 1 through 3 is with the .. range operator:
foreach($suffix in 1..3){
mkdir "John_S${suffix}"
}
To make the function re-usable with something other than John_S, declare a [string] parameter for the prefix:
function New-Directories([string]$Prefix) {
foreach($suffix in 1..3){
mkdir "${Prefix}${suffix}"
}
}

If I understand your latest comment correctly, you want a function that takes the name of a new folder and checks if a folder with that name altready exists in the rootpath. When that is the case, it should create a new folder with the given name, but with an index number appended to it, so it has a unique name.
For that you can use something like this:
function New-Folder {
[CmdletBinding()]
param (
[Parameter(Mandatory = $false)]
[string]$RootPath = $pwd, # use the current working directory as default
[Parameter(Mandatory = $true)]
[string]$FolderName
)
# get an array of all directory names (name only) of the folders with a similar name already present
$folders = #((Get-ChildItem -Path $RootPath -Filter "$FolderName*" -Directory).Name)
$NewName = $FolderName
if ($folders.Count) {
$count = 1
while ($folders -contains $NewName) {
# append a number to the FolderName
$NewName = "{0}{1}" -f $FolderName, $count++
}
}
# we now have a unique foldername, so create the new folder
$null = New-Item -Path (Join-Path -Path $RootPath -ChildPath $NewName) -ItemType Directory
}
New-Folder -FolderName "John_S"
If you run this several times, you will have created several folders like

Related

Powershell: How to repeat a condition until a valid value is passed?

I am working on a script that clones a directory structure (excluding files) to a new directory. Here's my code so-far:
Function Copy-DirectoryStructure{
[CmdletBinding()]
Param(
[Parameter(Mandatory=$true,ValueFromPipeline=$true)]
[string]$Path
)
# Test whether the path is exactly a drive letter
if($Path -match '^[a-zA-Z]:\\$' -or $Path -match '^[a-zA-Z]:$'){
throw "Path cannot be at the root of a drive. Aborting."
}
# Check if the path is valid
if(Test-Path -Path $Path -PathType Container){
# Create the destination path format
$DestinationPath = (get-item $Path).FullName + ' Folder Copy'
# Test if our destination already exists.
# If so, prompt for a new name
if(Test-Path -Path $DestinationPath -PathType Container){
Write-Warning "The destination path already exists."
$NewFolderName = Read-Host -Prompt "Input the name of the new folder to be created."
$DestinationPath = (get-item $Path).parent.FullName + '\' + $NewFolderName
}
# Begin copy
robocopy $Path $DestinationPath /e /xf *.* | Out-Null
} else {
throw "Invalid directory was passed. Aborting."
}
}
Copy-DirectoryStructure -Path "C:\Users\[username]\Desktop\Test"
The relevant part is here:
# Create the destination path format
$DestinationPath = (get-item $Path).FullName + ' Folder Copy'
# Test if our destination already exists.
# If so, prompt for a new name
if(Test-Path -Path $DestinationPath -PathType Container){
Write-Warning "The destination path already exists."
$NewFolderName = Read-Host -Prompt "Input the name of the new folder to be created."
$DestinationPath = (get-item $Path).parent.FullName + '\' + $NewFolderName
}
Right now it prompts the user to input a new name if $DestinationPath already exists and then proceeds to the robocopy. But what if the user inputs a folder name that ALSO already evaluates to an existing path?
I want to handle this scenario gracefully and re-prompt for a new path until the user enters a destination path that doesn't already exist.
I have no idea how to do this.
I know this is an extreme edge-case, but I want to make my code as safe as possible.
Any help is greatly appreciated.
Remove the interactive bits from the function completely - it should simply fail on destination path existing - and instead give the user a way to explicitly pass an alternative destination path:
Function Copy-DirectoryStructure{
[CmdletBinding()]
Param(
[Parameter(Mandatory=$true,ValueFromPipeline=$true)]
[string]$Path,
[Parameter(Mandatory=$false)]
[string]$DestinationPath
)
$ErrorActionPreference = 'Stop'
# Test whether the path is exactly a drive letter
if($Path -match '^[a-zA-Z]:\\$' -or $Path -match '^[a-zA-Z]:$'){
throw "Path cannot be at the root of a drive. Aborting."
}
# Check if the path is valid
if(-not(Test-Path -Path $Path -PathType Container)){
throw "Path must describe an existing directory"
}
# Create the destination path format
if(-not $PSBoundParameters.ContainsKey('DestinationPath'))
{
$DestinationPath = (Get-Item $Path).FullName + ' Folder Copy'
}
# Test that the destination isn't an existing file system item
if(Test-Path -Path $DestinationPath){
throw "The destination path already exists."
}
# If we've reached this point all validation checks passed, begin copy
robocopy $Path $DestinationPath /e /xf *.* | Out-Null
}
Now that the function predictably fails, we can use error handling to handle the retry-logic outside the function:
while($true){
$path = Read-Host "Give a target path!"
try {
Copy-DirectoryStructure -Path $path
break
}
catch {
if($_ -match 'The destination path already exists.'){
$destPath = Read-Host "Wanna try an alternative destination path (input NO to start over)"
if($destPath -ceq 'NO'){
continue
}
Copy-DirectoryStructure -Path $path
break
}
}
}

Powershell not returning correct value

As some background, this should take an excel file, and convert it to PDF (and place the PDF into a temporary folder).
E.g. 'C:\Users\gjacobs\Desktop\test\stock.xlsx'
becomes
'C:\Users\gjacobs\Desktop\test\pdf_merge_tmp\stock.pdf'
However, the new file path does not return correctly.
If I echo the string $export_name from within the function, I can see that it has the correct value: "C:\Users\gjacobs\Desktop\test\pdf_merge_tmp\stock.pdf".
But once $export_name is returned, it has a different (incorrect value): "C:\Users\gjacobs\Desktop\test\pdf_merge_tmp C:\Users\gjacobs\Desktop\test\pdf_merge_tmp\stock.pdf".
function excel_topdf{
param(
$file
)
#Get the parent path
$parent = Split-Path -Path $file
#Get the filename (no ext)
$leaf = (Get-Item $file).Basename
#Add them together.
$export_name = $parent + "\pdf_merge_tmp\" + $leaf + ".pdf"
echo ($export_name) #prints without issue.
#Create tmp dir
New-Item -Path $parent -Name "pdf_merge_tmp" -ItemType "Directory" -Force
$objExcel = New-Object -ComObject excel.application
$objExcel.visible = $false
$workbook = $objExcel.workbooks.open($file, 3)
$workbook.Saved = $true
$xlFixedFormat = “Microsoft.Office.Interop.Excel.xlFixedFormatType” -as [type]
$workbook.ExportAsFixedFormat($xlFixedFormat::xlTypePDF, $export_name)
$objExcel.Workbooks.close()
$objExcel.Quit()
return $export_name
}
$a = excel_topdf -file 'C:\Users\gjacobs\Desktop\test\stock.xlsx'
echo ($a)
The issue you're experiencing is caused by the way how PowerShell returns from functions. It's not something limited to New-Item cmdlet. Every cmdlet which returns anything would cause function output being altered with the value from that cmdlet.
As an example, let's take function with one cmdlet, which returns an object:
function a {
Get-Item -Path .
}
$outputA = a
$outputA
#### RESULT ####
Directory:
Mode LastWriteTime Length Name
---- ------------- ------ ----
d--hs- 12/01/2021 10:47 C:\
If you want to avoid that, these are most popular options (as pointed out by Lasse V. Karlsen in comments):
# Assignment to $null (or any other variable)
$null = Get-Item -Path .
# Piping to Out-Null
Get-Item -Path . | Out-Null
NOTE: The behavior described above doesn't apply to Write-Host:
function b {
Write-Host "bbbbbb"
}
$outputB = b
$outputB
# Nothing displayed
Interesting thread to check if you want to learn more.

PowerShell Repeatedly calling a function

The task at hand is to compare permissions from a source folder with a target folder, and this for all its sub folders. I've already created the function that does this check on one folder, which returns $True or $False.
I would like to know if it's possible to create a function that calls itself to be executed on every sub folder it finds to call Test-ACLequalHC. So that when it blocks or errors out in one of the sub folders, due to permission issues or something else, it can still continue with the others.
Something like a crawler, if that makes sense. Ideally it would be great if it could run in parallel. I read that a Workflow is most suited for this, but I've never used it before.
Unfortunately it's not possible to just do $AllSubFolders = Get-ChildItem -Recurse followed by a foreach, because there are over thousands of files and folders under the root folder. So it needs to be dynamically so that we can do extra stuff on every folder it finds, like say if Test-ACLequalHC results in $False on one folder, we can still call other functions to set the permissions correct or add the result to a CSV.
Permission test:
Function Test-ACLequalHC {
[CmdletBinding(SupportsShouldProcess=$True)]
Param(
[parameter(Mandatory=$true,Position=0)]
[ValidateScript({Test-Path $_})]
[String]$Source,
[parameter(Mandatory=$true,Position=1)]
[ValidateScript({Test-Path $_})]
[String]$Target,
[parameter(Mandatory=$true,Position=2)]
[ValidateSet('Access','Owner','All')]
[String]$Type
)
Begin {
$Props = Switch ($Type) {
'Access' {'Access', 'AreAccessRulesProtected'}
'Owner' {'Owner'}
'All' {'sddl'}
}
}
Process {
$CompParams = #{
Property = $Props
ReferenceObject = Get-Acl $Source #| Sort-Object
DifferenceObject = Get-Acl $Target #| Sort-Object
PassThru = $True
}
$Result = Compare-Object #CompParams
if ($Result -ne $null) {
Write-Output $false
}
else {
Write-Output $True
}
}
}
It would be great if it could check files to, for inheritance and that no extra permissions are added. But I'll add that stuff later on myself if I find out how to make such a crawler thingy that digs its way through the folder structure.
Thank you for your help.
Ok, I think your aversion of doing Get-ChildItem -Recurse is something you're going to need to get over. If you are only looking at directories use the -directory switch for Get-ChildItem. It's a provider level switch so it will speed things up dramatically.
Next, what I think you need to consider is the Ad--Member cmdlet. Something like this:
$Source = C:\GoodFolder
$AllFolders = GCI C:\ -Directory -Recurse
$AllFolders | ForEach{Add-Member -InputObject $_ -NotePropertyName "ACLGood" -NotePropertyValue (Test-ACLequalHC $source $_.fullname all)}
Then you can just filter on that for folders that have issues and address them as needed.
$AllFolders | Where{!$_.ACLGood} | ForEach{ Do stuff to fix it }

Windows PowerShell invoking function with parameters

I am totally new to PowerShell, and trying to write a simple script to produce log file. I searched forums and could not find the answer for my question.
I found the example in the net, that I thought would be useful, and applied it to my script:
## Get current date and time. In return, you’ll get back something similar to this: Sat January 25 10:07:25 2014
$curDateTime = Get-Date
$logDate = Get-Date -format "MM-dd-yyyy HH:mm:ss"
$LogPath = "C:\Temp\Log"
$LogName = "log_file_" + $logDate + ".log"
$sFullPath = $LogPath + "\" + $LogName
<#
param(
## The path to individual location files
$Path,
## The target path of the merged file
$Destination,
## Log path
$LogPath,
## Log name
$LogName
## Full LogFile Path
## $sFullPath = $LogPath + "\" + $LogName
)
#>
Function Log-Start {
<#
.SYNOPSIS
Creates log file
.DESCRIPTION
Creates log file with path and name that is passed.
Once created, writes initial logging data
.PARAMETER LogPath
Mandatory. Path of where log is to be created. Example: C:\Windows\Temp
.PARAMETER LogName
Mandatory. Name of log file to be created. Example: Test_Script.log
.INPUTS
Parameters above
.OUTPUTS
Log file created
#>
[CmdletBinding()]
Param ([Parameter(Mandatory=$true)][string]$LogPath, [Parameter(Mandatory=$true)][string]$LogName)
Process {
## $sFullPath = $LogPath + "\" + $LogName
# Create file and start logging
New-Item -Path $LogPath -Value $LogName –ItemType File
Add-Content -Path $sFullPath -Value "***************************************************************************************************"
Add-Content -Path $sFullPath -Value "Started processing at [$([DateTime]::Now)]."
Add-Content -Path $sFullPath -Value "***************************************************************************************************"
Add-Content -Path $sFullPath -Value ""
}
}
Set-StrictMode -Version "Latest"
Log-Start
....
The question is how can I make the Log_Start function to use variables I assigned in the beginning of the script, or it is not possible with declaration of [CmdletBinding()] and function itself.
If I try to run it the way it is coded it is prompting me to enter the path and logname, I thought it should have used what I already defined. Apparently I am missing the concept. I surely can just assign the values I need right in the param declaration for the function, but I am planning to use couple of more functions like log-write and log-finish,and would not want to duplicate the same values.
What am I missing?
You defined your custom parameters at the top of your script and now you must pass them them to the function by changing
Log-Start
line to read
Log-Start $LogPath $LogName
Though you would be better off naming your parameters differently to avoid confussion. You don't really need CmdletBinding() declaration unless you plan to utilise common parameters like -Verbose or -Debug with your function so you could get rid of the following 2 lines:
[CmdletBinding()]
Param ([Parameter(Mandatory=$true)][string]$LogPath, [Parameter(Mandatory=$true)][string]$LogName)
and your script would still work.
If you want to include settings from a config file, one approach is hashtables. Your config file would look like this:
logpath=c:\somepath\
server=server.domain
Then your script would have an extra var pointing to a config file and a function to import it:
$configFile = "c:\some.config";
function GetConfig(){
$tempConfig = #{};
$configLines = cat $configFile -ErrorAction Stop;
foreach($line in $configLines){
$lineArray = $line -split "=";
$tempConfig.Add($lineArray[0].Trim(), $lineArray[1].Trim());
}
return $tempConfig;
}
$config = GetConfig
You can then assign config values to variables:
$LogPath = $conifg.Item("logpath")
$server = $conifg.Item("server")
Or use them access directly
$conifg.Item("server")

Changing NTFS security on user with fullcontrol to modify

I have thousands of folders I need to change users with Fullcontrol access to modify access. The following is a list of what I have:
A script that changes NTFS perms:
$acl = Get-Acl "G:\Folder"
$acl | Format-List
$acl.GetAccessRules($true, $true, [System.Security.Principal.NTAccount])
#second $true on following line turns on inheritance, $False turns off
$acl.SetAccessRuleProtection($True, $True)
$rule = New-Object System.Security.AccessControl.FileSystemAccessRule("Administrators","FullControl", "ContainerInherit, ObjectInherit", "None", "Allow")
$acl.AddAccessRule($rule)
$rule = New-Object System.Security.AccessControl.FileSystemAccessRule("My-ServerTeam","FullControl", "ContainerInherit, ObjectInherit", "None", "Allow")
$acl.AddAccessRule($rule)
$rule = New-Object System.Security.AccessControl.FileSystemAccessRule("Users","Read", "ContainerInherit, ObjectInherit", "None", "Allow")
$acl.AddAccessRule($rule)
Set-Acl "G:\Folder" $acl
Get-Acl "G:\Folder" | Format-List
A text file with the directories and users that need to be changed from fullcontrol to modify.
I can always create a variable for the path and/or username and create a ForEach loop, but I'm not sure how to change the users that exist in the ACL for each folder to Modify, but keep the Admin accounts as full control. Any help would be appreciated.
Went another route and got what I needed. I'm not surprised noone tried to help me on this one.... it was tough. I'll post the scripts for the next person who has this issue.
There are two scripts. The first I obtained from the internet and altered a bit. The second script launches the first with the parameters required to automate.
First Script Named SetFolderPermission.ps1:
param ([string]$Path, [string]$Access, [string]$Permission = ("Modify"), [switch]$help)
function GetHelp() {
$HelpText = #"
DESCRIPTION:
NAME: SetFolderPermission.ps1
Sets FolderPermissions for User on a Folder.
Creates folder if not exist.
PARAMETERS:
-Path Folder to Create or Modify (Required)
-User User who should have access (Required)
-Permission Specify Permission for User, Default set to Modify (Optional)
-help Prints the HelpFile (Optional)
SYNTAX:
./SetFolderPermission.ps1 -Path C:\Folder\NewFolder -Access Domain\UserName -Permission FullControl
Creates the folder C:\Folder\NewFolder if it doesn't exist.
Sets Full Control for Domain\UserName
./SetFolderPermission.ps1 -Path C:\Folder\NewFolder -Access Domain\UserName
Creates the folder C:\Folder\NewFolder if it doesn't exist.
Sets Modify (Default Value) for Domain\UserName
./SetFolderPermission.ps1 -help
Displays the help topic for the script
Below Are Available Values for -Permission
"#
$HelpText
[system.enum]::getnames([System.Security.AccessControl.FileSystemRights])
}
<#
function CreateFolder ([string]$Path) {
# Check if the folder Exists
if (Test-Path $Path) {
Write-Host "Folder: $Path Already Exists" -ForeGroundColor Yellow
} else {
Write-Host "Creating $Path" -Foregroundcolor Green
New-Item -Path $Path -type directory | Out-Null
}
}
#>
function SetAcl ([string]$Path, [string]$Access, [string]$Permission) {
# Get ACL on FOlder
$GetACL = Get-Acl $Path
# Set up AccessRule
$Allinherit = [system.security.accesscontrol.InheritanceFlags]"ContainerInherit, ObjectInherit"
$Allpropagation = [system.security.accesscontrol.PropagationFlags]"None"
$AccessRule = New-Object system.security.AccessControl.FileSystemAccessRule($Access, $Permission, $AllInherit, $Allpropagation, "Allow")
# Check if Access Already Exists
if ($GetACL.Access | Where {$_.IdentityReference -eq $Access}) {
Write-Host "Modifying Permissions For: $Access on directory: $Path" -ForeGroundColor Yellow
$AccessModification = New-Object system.security.AccessControl.AccessControlModification
$AccessModification.value__ = 2
$Modification = $False
$GetACL.ModifyAccessRule($AccessModification, $AccessRule, [ref]$Modification) | Out-Null
} else {
Write-Host "Adding Permission: $Permission For: $Access"
$GetACL.AddAccessRule($AccessRule)
}
Set-Acl -aclobject $GetACL -Path $Path
Write-Host "Permission: $Permission Set For: $Access on directory: $Path" -ForeGroundColor Green
}
if ($help) { GetHelp }
if ($Access -AND $Permission) {
SetAcl $Path $Access $Permission
}
The next script calls the first script and adds the needed parameters. A CSV containing 2 columns with the folders and usernames with full control.
$path = "C:\Scripts\scandata\TwoColumnCSVwithPathandUserwithFullControl.csv"
$csv = Import-csv -path $path
foreach($line in $csv){
$userN = $line.IdentityReference
$PathN = $line.Path
$dir = "$PathN"
$DomUser = "$userN"
$Perm = "Modify"
$scriptPath = "C:\Scripts\SetFolderPermission.ps1"
$argumentList1 = '-Path'
$argumentList2 = "$dir"
$argumentList3 = '-Access'
$argumentList4 = "$DomUser"
$argumentList5 = '-Permission'
$argumentList6 = "$Perm"
Invoke-Expression "$scriptPath $argumentList1 $argumentList2 $argumentList3 $argumentList4 $argumentList5 $argumentList6"