How to Combine and split values by delimiter? - powershell

I have a script that has a switch, -Add, allowing the addition of 1 role and 1 member (into that role) in a database at a time.
PS> script1.ps1 -Add Database1 role1 member1
PS> script1.ps1 -Add Database1 role1 member2
PS> script1.ps1 -Add Database1 role2 memberx
PS> script1.ps1 -Add Database1 role3
PS> script1.ps1 -Add Database1 role4 membery
It wont be practical to have to run that script everytime if there are more than 1 member or 1 roles/members to add a time. hence, i'd like to update my script with bulk addition of roles/members at once, which would be helpful especially for a TFS use case, in which the user wont have to create multiple releases just to add a couple roles/members for the same database.
i am thinking of implementing a delimiter split, in which for every semicolon, that indicates the start of a new role, and any comma delimited members, all belong to one role unless a semicolon follows (not necessary if its the end of the input, unless it would be hard to achieve something like that with regex?)
pseudocode:
$RoleInput.Split(";") | ForEach {
$role = "$_";
$MemberInput.Split(",") | ForEach {
$member = "$_";
#-Add $DBName $role $member
}
}
ultimately, i would like to achieve similar to the following:
PS> script1.ps1 -Add Database1 role1;role2;role3;role4 member1,member2;memberx;;membery
this means that for role1, member1 and member2 would be added
for role2, memberx is added, for role3, no member is added, and for role 4, membery is added
how would i achieve this correctly with regex to account for whitespaces, and end of input?

Save roles and members in a CSV
"Role","Members"
"role1","member1;member2"
"role2",
"role3","memberX"
then load the CSV using Import-Csv, split the member list at the chosen secondary delimiter (semicolons in the example above), and call your script with those arguments.
Import-Csv 'input.csv' | ForEach-Object {
$role = $_.Role
if ($_.Members) {
$_.Members -split ';' | ForEach-Object { & script1.ps1 -Add $role $_ }
} else {
& script1.ps1 -Add $role
}
}
If you can modify the parameters of your script you could change them to something like this:
[CmdletBinding()]
Param(
[Parameter(ParameterSetName='add', Mandatory=$true)]
[Switch]$Add,
[Parameter(ParameterSetName='add', Mandatory=$true)]
[String]$Role,
[Parameter(ParameterSetName='add', Mandatory=$false)]
[String[]]$Members = #()
)
so that the script accepts a list of members via a named parameter (you also need to adjust how the members are processed in the script, of course), and then call it like this:
Import-Csv 'input.csv' | ForEach-Object {
$params = #{'Role' = $_.Role}
if ($_.Members) { $params['Member'] = $_.Members -split ';' }
& script.ps1 -Add #params
}
This second example uses splatting for passing the parameters to the script.

Related

Powershell replace one instance in a csv

I need to keep track of port assignments for users. I have a csv that contains this:
USERNAME,GUI,DCS,SRS,LATC,TRSP
joeblow,8536,10631,5157,12528,14560
,8118,10979,5048,12775,14413
,8926,10303,5259,12371,14747
,8351,10560,5004,12049,14530
johndoe,8524,10267,5490,12809,14493
,8194,10191,5311,12275,14201
,8756,10813,5714,12560,14193
,8971,10006,5722,12078,14378
janblow,8410,10470,5999,12123,14610
bettydoe,8611,10448,5884,12040,14923
,8581,10965,5832,12400,14230
,8708,10005,5653,12111,14374
,8493,10016,5464,12827,14115
I need to be able to add users and remove users which will leave the csv looking as it does now. I have the remove part with this bit of code:
[io.file]::readalltext("c:\scripts\RNcsv.csv").replace("$username","") | Out-File c:\scripts\RNcsv.csv -Encoding ascii -Force
I tried the reverse of the code above but it does not want to work with empty value in that context. I have been unsuccessful finding a way to add $username to a single record. The first record with an empty name column to be precise. So when joeshmo comes along he ends up in the record below joeblow. This csv represents that people have come and gone.
I would take an object oriented approach using Import-Csv and a re-usable function that takes the input from pipeline:
function Add-User {
param(
[Parameter(Mandatory)]
[string] $Identity,
[Parameter(Mandatory, ValueFromPipeline, DontShow)]
[object] $InputObject
)
begin { $processed = $false }
process {
# if the user has already been added or the UserName column is populated
if($processed -or -not [string]::IsNullOrWhiteSpace($InputObject.UserName)) {
# output this object as-is and go to the next object
return $InputObject
}
# if above condition was not met we can assume this is an empty value in the
# UserName column, so set the new Identity to this row
$InputObject.UserName = $Identity
# output this object
$InputObject
# and set this variable to `$true` to skip further updates on the csv
$processed = $true
}
}
Adding a new user to the Csv would be:
(Import-Csv .\test.csv | Add-User -Identity santiago) | Export-Csv .\test.csv -NoTypeInformation
Note that, since the above is reading and writing to the same file in a single pipeline, the use of the Grouping operator ( ) is mandatory to consume all output from Import-Csv and hold the object in memory. Without it you would end up with an empty file.
Otherwise just break it into 2 steps (again, this is only needed if reading and writing to the same file):
$csv = Import-Csv .\test.csv | Add-User -Identity santiago
$csv | Export-Csv .\test.csv -NoTypeInformation
Adding this slight modification to the function posted above allowing the ability to add multiple users in one function call. All credits to iRon for coming up with a clever and and concise solution.
function Add-User {
param(
[Parameter(Mandatory)]
[string[]] $Identity,
[Parameter(Mandatory, ValueFromPipeline, DontShow)]
[object] $InputObject
)
begin { [System.Collections.Queue] $queue = $Identity }
process {
# if there are no more Identities in Queue or the UserName column is populated
if(-not $queue.Count -or -not [string]::IsNullOrWhiteSpace($InputObject.UserName)) {
# output this object as-is and go to the next object
return $InputObject
}
# if above condition was not met we can assume this is an empty value in the
# UserName column, so dequeue this Identity and set it to this row
$InputObject.UserName = $queue.Dequeue()
# output this object
$InputObject
}
}
(Import-Csv .\test.csv | Add-User -Identity Santiago, 4evernoob, mrX, iRon) | Export-Csv ...
In addition to where you ask for and #Santiago's helpful answer (and note), you might want to be able to add multiple usernames at once to avoid that you need to recreate the whole file for each user you want to add.
$Csv = ConvertFrom-Csv #'
USERNAME, GUI, DCS, SRS, LATC, TRSP
joeblow, 8536, 10631, 5157, 12528, 14560
, 8118, 10979, 5048, 12775, 14413
, 8926, 10303, 5259, 12371, 14747
, 8351, 10560, 5004, 12049, 14530
johndoe, 8524, 10267, 5490, 12809, 14493
, 8194, 10191, 5311, 12275, 14201
, 8756, 10813, 5714, 12560, 14193
, 8971, 10006, 5722, 12078, 14378
janblow, 8410, 10470, 5999, 12123, 14610
bettydoe, 8611, 10448, 5884, 12040, 14923
, 8581, 10965, 5832, 12400, 14230
, 8708, 10005, 5653, 12111, 14374
, 8493, 10016, 5464, 12827, 14115
'#
$NewUser = 'Santiago', '4evernoob', 'mrX', 'iRon'
$Csv |ForEach-Object { $i = 0 } {
if (!$_.USERNAME) { $_.USERNAME = $NewUser[$i++] }
$_
} |Format-Table
USERNAME GUI DCS SRS LATC TRSP
-------- --- --- --- ---- ----
joeblow 8536 10631 5157 12528 14560
Santiago 8118 10979 5048 12775 14413
4evernoob 8926 10303 5259 12371 14747
mrX 8351 10560 5004 12049 14530
johndoe 8524 10267 5490 12809 14493
iRon 8194 10191 5311 12275 14201
8756 10813 5714 12560 14193
8971 10006 5722 12078 14378
janblow 8410 10470 5999 12123 14610
bettydoe 8611 10448 5884 12040 14923
8581 10965 5832 12400 14230
8708 10005 5653 12111 14374
8493 10016 5464 12827 14115
Note that an outbound index (as e.g. NewUser[99]) returns a $Null (which is casted to an empty string) by default. This feature will produce an error if you set the StricMode to a higher level.
To overcome this, you might also do something like this instead:
if (!$_.USERNAME -and $i -lt #($NewUser).Count) { ...

Use value directly in script

I have the following script. The issue i have is that i need to create a value before using the information gathered from the csv file. The first code is what works for me, the second code is how i would want to use it, where the value $uid is not create before
$users = import-csv c:\temp\users.csv
foreach ($user in $users){
$uid= $user.UserPrincipalName+":\calendar"
add-mailboxfolderpermission -identity $uid -user "calendar_reviewer" -AccessRights LimitedDetails
}
$users = import-csv c:\temp\users.csv
foreach ($user in $users){
add-mailboxfolderpermission -identity $user.UserPrincipalName+":\calendar" -user "calendar_reviewer" -AccessRights LimitedDetails
}
In short, your string composition isn't working like you're expecting it to.
If we use the following test function it'll demonstrate what's happening:
function Invoke-MyFunction
{
param( $Identity, $OtherParams )
write-host "identity = '$Identity'";
write-host "otherparams = '$OtherParams'";
}
In your first (working) example, PowerShell is concatenating the strings:
$uid = $user.UserPrincipalName+":\calendar"
Invoke-MyFunction -Identity $uid
# identity = 'myname:\calendar'
# otherparams = ''
but in your broken sample, it's not treating the $user.UserPrincipalName+":\calendar" as a single expression - it's treating +":\calendar" as a separate string that it passes as a second positional parameter:
Invoke-MyFunction -Identity $user.UserPrincipalName+":\calendar"
# identity = 'myname'
# otherparams = '+:\calendar'
There's a few different ways to get PowerShell to treat your parameter as an expression so it evaluates it before passing the value as a parameter:
Grouping Operator
As recommended by #zilog80 in the comments, wrap it in the Grouping operator (i.e. ( ... )) to force PowerShell to evaluate the expression before passing the result into the parameter:
Invoke-MyFunction -Identity ($user.UserPrincipalName+":\calendar")
# identity = 'myname:\calendar'
# otherparams = ''
String Interpolation
Per #Abraham Zinala's comment, use string interpolation (note the use of the Subexpression operator (i.e. $( ... ) to substitute in the value of $User.UserPrincipalName)
Invoke-MyFunction -Identity "$($User.UserPrincipalName):\calendar"
# identity = 'myname:\calendar'
# otherparams = ''
Assign to a variable
As you've already found, you can force the expression to be evaluated by assigning it to a temporary variable, then pass the variable as a parameter:
$uid = $user.UserPrincipalName+":\calendar"
Invoke-MyFunction -Identity $uid
# identity = 'myname:\calendar'
# otherparams = ''

My script shows outputs of multiple Get-LocalGroupMember commands as one object. How to split them?

I have to make a script in PowerShell that shows all groups that user belongs to. What's more, if the switch -ListAllMembers is on, it should also show all members of these groups. The thing is that my script shows all members of different groups as one object (array) and I want them to be divided. Get-LocalGroupMember command is in a foreach loop. What can I do?
Param(
$userToCheck,
[switch]$listAllMembers
)
function CheckIfBelongs {
foreach ($user in Get-LocalGroupMember $args[0]) {
if ($user.Name -eq ($env:USERDOMAIN + "\" + $args[1])) {
return $true
}
}
return $false
}
if (!$userToCheck) {
$userToCheck = $env:USERNAME
}
Write-Host "`nUser $userToCheck belongs to these local groups:`n"
foreach ($group in Get-LocalGroup) {
if (CheckIfBelongs $group $userToCheck) {
Write-Host $group.Name
if ($listAllMembers) {
Get-LocalGroupMember $group
}
}
}
exit 0
Write-Host writes directly to the host console. Default output (as produced by Get-LocalGroupMember) goes to the success output stream. Output order is not guaranteed across streams. Also, PowerShell default formatting suppresses additional table headers when outputting objects of the same type as a table. Because of that you see only one table, and the host output before that table.
Change Write-Host to Write-Output and the output will become what you expect.
Finally, I managed to solve the issue. I used Out-String to convert the object to a string and then to send it to the console. Now, it works perfectly.

Using a variable in a PowerShell script

I'm trying to put a small collection of simple scripts together for PowerShell to make life easier but am having some problems with variables in these scripts.
In a Linux environment I would use a variable in my scripts (usually $1, $2, etc....) like this to make things easier
sed -i 's/$1/$2/g' filename.conf
So that all I would need to do is this
./findreplace.sh old new filename.conf
In powershell, I want to achieve similar, specifically with this command:
Get-ADPrincipalGroupMembership account.name | select name
In this case, the $1 would be where 'user.name' is, so that I would be doing:
.\groups.ps1 user.name
Is there the facility for this?
Groups.ps1 should have the following content:
param( $user )
Get-ADPrincipalGroupMembership $user | Select Name
You can then invoke it as shown, or as .\Groups.ps1 -user user.name.
However, I'd probably try to do it as an "advanced command", and allow it to handle multiple names, and also names passed in via the pipeline:
[CmdletBinding()]
param (
[Parameter(ValueFromPipeline=$true;ValueFromPipelineByPropertyName=$true)]
[Alias("Identity","sAMAccountName")
string[] $Users
)
PROCESS {
ForEach ($User in $Users) {
Get-ADPrincipalGroupMembership $User | Select Name
}
}
which would also allow you to do something like Get-ADUser | .\Groups.ps1
You can add for loop form example script above:
for ($i = 0; $i -lt $args.Length; $i++) { Set-Variable -Scope Script -Name ($i + 1) -Value $args[$i] }
Write-Host "1: $1"
Write-Host "2: $2"
But more easy is just to declare script parameter as:
param($1, $2)
Write-Host "1: $1"
Write-Host "2: $2"
By the way it's bad practice to declare variables as numbers.

Effective permissions on remote share for domain users in Powershell

I searched and read some topics here but I didn't found what I am looking for.
Basically, I want to check the effective permissions for a specific user for several shares, I want a script such as :
$user = Read-Host "Enter username"
$shares = "\\serverABC\share2","\\serverABC\share1"
foreach ($share in $shares)
{
Cmdlet-EffectivePermissions $share
}
Output expected :
\\serverABC\share1
Full Control : No
Traverse folder / execute / file : YEs
List folder / read data : No
...
\\serverABC\share2"
Full Control : No
Traverse folder / execute / file : YEs
List folder / read data : No
...
In fact, I want to do in Powershell exactly the same way that effective permissions Tab.
Does it exist a built-in solution (without importing any modules, add-ins, ...) with .NET Method (GetUserEffectivePermissions) or with Get-ACL?
I'm not aware of a .NET/PowerShell way to do this natively. There is a PowerShell module here that should be able to do what you're looking for, though. After importing that, you should be able to modify your pseudo code to the following:
$user = Read-Host "Enter username"
$shares = "\\serverABC\share2","\\serverABC\share1"
foreach ($share in $shares) {
Get-EffectiveAccess -Path $share -Principal $user -ListAllRights
}
That returns PS objects instead of simple text. If the format isn't to your liking, you can use some of the utility commands to shape it however you like. Here are two examples of doing that:
First, a simple change to original that doesn't return the exact format you mentioned, but it's pretty close:
foreach ($share in $shares) {
$share
Get-EffectiveAccess -Path $share -Principal $user -ListAllRights | ForEach-Object {
"{0}: {1}" -f $_.Permission, $_.Allowed
}
""
}
Next, a more complicated change that formats the output exactly how you were asking (at least I think):
# Go through each FileSystemRights enum name and add them to a hash table if their value is
# a power of 2. This will also keep track of names that share a value, and later those can
# be combined to provide a friendly permission name
$Ht = #{}
foreach ($Name in [System.Enum]::GetNames([System.Security.AccessControl.FileSystemRights])) {
$Value = [System.Security.AccessControl.FileSystemRights]::$Name
if ($Value.value__ -band ($Value.value__ - 1)) {
# Not a power of 2, so ignore this
continue
}
if (-not $Ht.$Value) {
$Ht.$Value = #()
}
$Ht.$Value += $Name
}
# FullControl isn't a power of 2, but it's useful to test for access, so add it manually
$Ht.([System.Security.AccessControl.FileSystemRights]::FullControl) = "FullControl"
function YesNoTest {
param(
[System.Security.AccessControl.FileSystemRights] $EffectiveAccess,
[System.Security.AccessControl.FileSystemRights] $AccessToTest
)
if (($EffectiveAccess -band $AccessToTest) -eq $AccessToTest) {
"Yes"
}
else {
"No"
}
}
$shares | Get-EffectiveAccess -Principal $user | ForEach-Object {
$_.DisplayName
$EffectiveAccess = $_.EffectiveAccess
$Ht.GetEnumerator() | sort { $_.Key.value__ } -Descending | ForEach-Object {
"{0}: {1}" -f ($_.Value -join " / "), (YesNoTest $EffectiveAccess $_.Key)
}
""
}
Note that this won't be completely accurate if you run this against a remote system and the following conditions are met:
The security descriptor contains groups that are local to the remote system, i.e., non domain groups
The user(s) you're checking is a member of one of the local groups