1

I'm trying to export several information from a collection of objects into CSV files.

For this purpose I always have the following pipe :

 $users | < my filters/grouping/selects and expands> | Export-CSV ...

Instead of copy/paste these lines I would prefer to have an hashtable with as a key the CSV filename and and as a value the part between < ... >

So I did this :

$scriptblocks = @{"NonCompliantMail"={ ? {-not ([bool]($_.mail -as [Net.Mail.MailAddress])) } };
              "NonCompliantSAM"= { ? { ($_.samaccountname.Trim().Length - $_.samaccountname.Length) -ne 0 }};
              "MissingSN" = { ? {[string]::IsNullOrWhiteSpace($_.sn) } };
              "MissingGivenName" = { ? {[string]::IsNullOrWhiteSpace($_.givenname) } };
              "TrimSN" = { ? { (-not ([string]::IsNullOrWhiteSpace($_.sn))) -and (($_.sn.Trim().Length - $_.sn.Length) -ne 0) } };
              "TrimGivenName" = { ? { (-not ([string]::IsNullOrWhiteSpace($_.givenname))) -and (($_.givenname.Trim().Length - $_.givenname.Length) -ne 0) } }
              "MultipleEmails" = { group-object mail |? { $_.Count -gt 1 } | select -ExpandProperty Group | select mail,samaccountname }
              }

and I'm trying to execute it like that but it doesn't work :

$scriptblocks.getEnumerator() |% { $users | & $_.Value | Export-Csv -NoTypeInformation -Force -Encoding $encoding -Delimiter $delimiter -Path (Join-Path $scriptpath ($_.Key + ".csv")) } 

Any ideas on how to do it ?

Regards. JB

Jeanb
  • 23
  • 6
  • 1
    Replace `$scriptblocks` to `$scriptblocks.GetEnumerator()` – user4003407 Feb 05 '15 at 19:36
  • What's the point of this? The first thing you do in your `ForEach` loop is pipe `$users` so you have lost your `$_` from `$scriptblocks` because it is now representative of the current iteration of `$users` – TheMadTechnician Feb 05 '15 at 19:51
  • @PetSerAI : yes exact I forgot the GetEnumerator. Now the files are created but empty :/ – Jeanb Feb 05 '15 at 19:52
  • @TheMadTechnician : exact. I would need to keep both. The goal is to only have to add avoid 5 times the same lines (but I've got other cases so it's more 20/25) – Jeanb Feb 05 '15 at 19:56
  • Got it, I understand now and will have an answer for you shortly. You need to use a `Switch` loop. Writing a decent answer will take a few minutes but it's coming. – TheMadTechnician Feb 05 '15 at 20:01

2 Answers2

1

Ok, so a Switch is a loop that tests each record of an array against a set of filters. If the record passes the filter it it run against the following scriptblock. The filters can be a literal match, a RegEx match, or a scriptblock similar to a Where statement. For your needs we'll be using the last of those. Check out this example and see if it accomplishes what you are going for:

Switch($users){
    {-not ([bool]($_.mail -as [Net.Mail.MailAddress])) }{$_|Export-Csv -NoTypeInformation -Force -Encoding $encoding -Delimiter $delimiter -Path (Join-Path $scriptpath ("NonCompliantMail" + ".csv")) -append}
    {($_.samaccountname.Trim().Length - $_.samaccountname.Length) -ne 0 }{$_|Export-Csv -NoTypeInformation -Force -Encoding $encoding -Delimiter $delimiter -Path (Join-Path $scriptpath ("NonCompliantSAM" + ".csv")) -append}
    {[string]::IsNullOrWhiteSpace($_.sn) }{$_|Export-Csv -NoTypeInformation -Force -Encoding $encoding -Delimiter $delimiter -Path (Join-Path $scriptpath ("MissingSN" + ".csv")) -append}
    {[string]::IsNullOrWhiteSpace($_.givenname) }{$_|Export-Csv -NoTypeInformation -Force -Encoding $encoding -Delimiter $delimiter -Path (Join-Path $scriptpath ("MissingGivenName" + ".csv")) -append}
    {(-not ([string]::IsNullOrWhiteSpace($_.sn))) -and (($_.sn.Trim().Length - $_.sn.Length) -ne 0) }{$_|Export-Csv -NoTypeInformation -Force -Encoding $encoding -Delimiter $delimiter -Path (Join-Path $scriptpath ("TrimSN" + ".csv")) -append}
    {(-not ([string]::IsNullOrWhiteSpace($_.givenname))) -and (($_.givenname.Trim().Length - $_.givenname.Length) -ne 0) }{$_|Export-Csv -NoTypeInformation -Force -Encoding $encoding -Delimiter $delimiter -Path (Join-Path $scriptpath ("TrimGivenName" + ".csv")) -append}
}

That will run each user through the switch, and if it matches any of the conditions it will append it to the associated CSV file.

Edit: Ok, you didn't like Switch. If you really want to be able to execute scriptblocks in a ForEach-Object loop like that you can add parameters to your scriptblocks to allow piped data, but this doesn't completely solve your issue. I'll get to that in a moment. First, let's take your Group-Object mail option and set it up to accept input:

"MultipleEmails" = { group-object mail |? { $_.Count -eq 1 } | select -ExpandProperty Group | select mail,samaccountname }

becomes

"MultipleEmails" = {Param([Parameter(ValueFromPipeline=$True)][Object[]]$Users);$Users| group-object mail |? { $_.Count -eq 1 } | select -ExpandProperty Group | select mail,samaccountname }

I added Param([Parameter(ValueFromPipeline=$True)][Object[]]$Users);$Users| to the beginning of the scriptblock to do this. You can add that to the beginning of each scriptblock and they should all run similarly.

Then we have to force $users to be passed to it as an array by wrapping it as such: (,$users)
That allows this:

(,$users)|& $scriptblocks["multipleemails"]

That provides the output that you would expect it to. All that's left is to put that in your ForEach for $ScriptBlocks, along with keeping track of your current scriptblock:

$scriptblocks.keys|%{$sb=$_;(,$users)|& $scriptblocks["$sb"]}

That outputs everything from all of the scriptblocks. The only issue you now have is that you have no way to specify what CSV to output to. But this at least answers your original question.

TheMadTechnician
  • 34,906
  • 3
  • 42
  • 56
  • It can be a solution but I don't always have a where. I also have this for example : `$users | group-object mail |? { $_.Count -gt 1 } | select -ExpandProperty Group | select mail,samaccountname | export` ... And I wanted to just have the "middle" part variable, the export csv is always the same. That's why I wanted to factor these. – Jeanb Feb 05 '15 at 20:20
  • Except that the export csv is *not* always the same. You want the export to be to different files dependent on the middle portion, so really the only thing that stays the same is that you are piping `$users` in, and expect to be able to export to a variety of different csv files at the end. You can't just have the middle change and the end stay the same, what if a user matches multiple criteria? Export-Csv does not support writing to multiple files in one pass. The -Path parameter accepts a string, not an array of strings. – TheMadTechnician Feb 05 '15 at 20:47
  • Yes in my case I loop throught each user in each scriptblock. It's quite fast actually, more than your switch (I did a test of 10 iterations on 42000 and more users) 10% faster using $users | ... | Export-CSV .... I kind of like your method, but It doesn't work for some of my scriptblocks. (just added one of them on initial question) – Jeanb Feb 05 '15 at 21:41
  • Ok, that should do it for you. Updated answer to allow you to loop through scriptblocks. Now I'm pretty sure you just have an issue with output. – TheMadTechnician Feb 06 '15 at 02:58
  • Thanks! I think I'll stick with original method fully expanded with `$users | ...| export-csv ...` on each line as it become too ugly and it doesn't fully resolve the issue of the export csv filename – Jeanb Feb 06 '15 at 10:38
1

The problem with this approach is that it is inefficient: you have to loop through all your $users once for each output file.

Another approach would be to use a switch statement to get around this (psuedocode to follow):

$file1 = "NonCompliantMail.csv"
$file2 = "NonCompliantSAM.csv"
$file3 = "MissingSN.csv"
$file4 = "MissingGivenName.csv"
$file5 = "TrimSN.csv"
$file6 = "TrimGivenName.csv"
$file7 = "UsersWhoDontMatchAnything.csv"

function WriteUserDataToFile
{

    param
    (
        $User,
        $OutFile
    )

    "Process the $User object to append to $OutFile"
}

switch ($users) 
{ 
    ("User matching criteria 1") {WriteUserDataToFile $_ $file1}
    ("User matching criteria 2") {WriteUserDataToFile $_ $file2} 
    ("User matching criteria 3") {WriteUserDataToFile $_ $file3} 
    ("User matching criteria 4") {WriteUserDataToFile $_ $file4} 
    ("User matching criteria 5") {WriteUserDataToFile $_ $file5}
    ("User matching criteria 6") {WriteUserDataToFile $_ $file6}
    default {WriteUserDataToFile $_ $file7}
}

So the users are matched against your criteria one-by-one and when a match is made the function to append that user's data to the file for that type of match is called.

Hope that is a fitting suggestion.

ConanW
  • 486
  • 3
  • 7
  • You just posted the same answer I did 20 minutes after me, except you don't explain `switch`, added in a fairly useless function, and only posted psuedocode. You did this *after* the OP already said that the `switch` cmdlet didn't really accomplish what he wants. – TheMadTechnician Feb 05 '15 at 20:41
  • 1
    The problems of working on an answer and posting it without refreshing the page. The function is there to simplify the scriptblocks: I always try to reduce repetition in that way. Apologies to JB if this is not suitable. – ConanW Feb 05 '15 at 20:49