Cloud, Scripting

Microsoft Teams and PowerShell

I just started playing around with the MicrosoftTeams PowerShell module (available in the PowerShell Gallery, use Find-Module MicrosoftTeams for more information). Here’s a quick sample of how you can get started using it…

$conn = Connect-MicrosoftTeams

# list all Teams
Get-Team

# get a specific Team
$team = Get-Team -DisplayName "Benefits"

# create a new Team
$team = New-Team -DisplayName "TechSupport" -Description "Technical Support" -Owner "dave@contoso.com"

# add a few channels to the new Team
New-TeamChannel -GroupId $team.GroupId -DisplayName "Forms Library" -Description "Forms and Templates"
New-TeamChannel -GroupId $team.GroupId -DisplayName "Customers" -Description "Information for customers"
New-TeamChannel -GroupId $team.GroupId -DisplayName "Development" -Description "Applications and DevOps teams"

# dump properties for one Team channel
$channelId = Get-TeamChannel -GroupId $team.GroupId |
Where-Object {$_.DisplayName -eq 'Development'} |
Select-Object -ExpandProperty Id

# add a user to a Team
Add-TeamUser -GroupId $team.GroupId -User "dory@contoso.com" -Role Member

Here’s a splatted form of the above example, in case it renders better on some displays…

$conn = Connect-MicrosoftTeams

# list all Teams
Get-Team

# get a specific Team
$team = Get-Team -DisplayName "Benefits"

# create a new Team
$params = @{
DisplayName = "TechSupport"
Description = "Technical Support"
Owner = "dave@contoso.com"
}
$team = New-Team @params

# add a few channels to the new Team
# NOTE: You could form an array to iterate more efficiently
$params = @{
GroupId = $team.GroupId
DisplayName = "Forms Library"
Description = "Forms and Templates"
}
New-TeamChannel @params

$params = @{
GroupId = $team.GroupId
DisplayName = "Customers"
Description = "Information for customers"
}
New-TeamChannel @params

$params = @{
GroupId = $team.GroupId
DisplayName = "Development"
Description = "Applications and DevOps teams"
}
New-TeamChannel @params

# dump properties for one Team channel
$channelId = Get-TeamChannel -GroupId $team.GroupId |
Where-Object {$_.DisplayName -eq 'Development'} |
Select-Object -ExpandProperty Id

# add a user to a Team
$params = @{
GroupId = $team.GroupId
User = "dory@contoso.com"
Role = 'Member'
}
Add-TeamUser @params

Advertisements
Scripting, System Center, Technology

Captain’s Log: cmhealthcheck

I’ve consumed way way waaaaay too much coffee and tea today. Great for getting things done, not great for my future health.

CMHealthCheck 1.0.8 is in the midst of being waterboarded, kicked, beaten, tasered and pepper-sprayed to make it squeal. I’m close to a final release. Among the changes in testing:

  • Discovery Methods
  • Boundary Groups
  • Site Boundaries
  • Packages, Applications, Task Sequences (just summary), Boot Images (summary), etc.
  • User and Device Collections
  • SQL Memory allocation (max/pct)
  • Fixed “Local Groups” bug
  • Fixed “Local Users” bug
  • Enhanced Logical Disks report
  • Fixed “Installed Software” sorting issue
  • Fixed “Services” sorting issue
  • Fixed null-reference issues with “Installed Hotfixes”

Still in the works:

  • Sorting issue with ConfigMgr Roles installation table
  • Local Group Members listing
  • More details for Discovery Methods
  • Client Settings
  • ADR’s
  • Deployment Summary
  • Enhancements to the HTML reporting features

Stay tuned for more.

Note: The current posted version (as of 3/8/19) is 1.0.7, which is what will install if you use Install-Module.

To load the 1.0.8 test branch, go to the GitHub repo, change the branch drop-down from “master” to 1.0.8 (or whatever the other name happens to be at the time) and then use the Download option to get the .ZIP file. Then extract to a folder, and use Import-Module to import the .psd1 file and start playing.

Projects, Scripting, System Center, windows

sktools

sktools2

UPDATE: 1/14/2019 – version 1901.13.2 was posted to address a problem with the previous upload.  Apparently, I posted an out-of-date build initially, so I’ll call this the “had another cup of coffee build”.

Dove-tailing from the previous idiotic blog post, I’ve taken some time off to retool, rethink, redesign and regurgitate “skattertools” as a single PowerShell module.  The new version blends PoSHServer into the module and removes the need to perform a separate install for the local web listener.  The first version of this is 1901.13.1 (as in 2019, 01=January 13th, 1st release).

How to Install and Configure sktools

  • Open a PowerShell console using Run as Administrator
  • Type: Install-Module sktools
  • Type: Import-Module sktools
  • Type: Install-SkatterTools (this creates a default “sktools.txt” configuration file in your “Documents” folder)
  • Type: Start-SkatterTools
  • Open your browser and navigate to http://localhost:8080

This next part is only temporary, and will be improved upon soon:

  • Once the web console is open, expand “Support” and click “Settings” and modify to suit your Configuration Manager site environment.
  • Close and reopen the PowerShell console (still “Run as Administrator”)
  • Type: Start-SkatterTools
  • Refresh your web browser session

Work will continue until morale is eliminated.  Easter eggs are included, sort of.  Thoughts, feedback, bug reports, enhancement requests, angry snarky comments, are all welcome.  Enjoy!

Personal, Projects, Scripting, System Center, windows

And now for another stupid pet project

First, there was project number one. I called it “WWA”, which was a clever short name for “Windows Web Admin”. Even though I kept hearing it sounded like a name for a wrestling tournament.  Anyhow, it fell over and sank into a swamp.

Then there was project number two, or “AppAdmin”, which almost fell over and sank, but it was built inside a big shipyard, and they don’t let things sink there, so it floated for a while (I’m told it’s still afloat somehow).

Then, there was project number three, but I can’t state it’s name for legal reasons, or because I promised it might result in me delivering a flaming box of dog poo to a certain someone’s porch, after they ruined that project just as it was maturing, but that’s for another time and place.

Then there was project four, but I can’t talk about that one either, so I’ll skip to project five, CMWT.  But nobody cares about that one, so number six, was putting a hand-rubbed wax finish on someone else’s PowerShell script, and tossing it up on GitHub and PowerShell Gallery, along with projects seven, eight and nine.  And I’m surprised I still remember how to spell the number 8.  So anyhow…

Announcing SkatterTools

(imagine Morgan Freeman narrating from here on)

What is it?

Skatterbrainz Tools.  A really clever name.

It’s a portable web console app thing, for viewing and modifying things in your Active Directory and Configuration Manager environments, from the comfort of your beer-stained laptop.  Think of it like CMWT if it were (A) trying to copy the concept from Microsoft Windows Admin Center, and (B) didn’t require using a separate “server” or anything special**.  Yes, those are double-asterisks.  That means there’s some hidden footnote down below, but don’t look yet, I have to finish boring the shit out of you with this part first.

Why is it?

Because I needed a break from other things, like family matters during the holidays, a dog that loves chewing on furniture, and a 20 year old cat that wanders the house at 3am making really weird sounds.  And I just wanted to see if it was possible to…

  • Build a 100% web console UX to interface with AD and ConfigMgr using PowerShell
  • Not have to touch IIS or any web hosting mess
  • Make it customize-able, free, and open-source
  • Make it through the holidays once again

Where is it?

  • Like a lot of my stuff, it’s up on GitHub

What can it do?

  • View and cross-link:
    • AD users, computers, groups, sites, sitelinks, domain controllers, OUs
    • ConfigMgr users, devices, collections, applications, packages, boot images, task sequences, updates, and scripts
    • ConfigMgr site status, queries, discovery methods, certificates, Forest publishing, boundary groups and boundaries
    • Software inventory, software files
  • Manage:
    • Add/remove AD group members
    • View computers by AD user profile paths
    • Add/remove ConfigMgr collection members **
    • Those damned double-asterisks again, hmm.

Installation and Setup

  • Download PoSH Server here and install it (don’t worry, I checked it and it seems safe, you can trust me, I worked for the government once, sort of)
  • Download the GitHub repo (big green button – top right – zip option)
  • Extract the “poshserver” folder from the GitHub download into a local path like C:\ThisIsTheDumbestShitEver
  • Open the “config.txt” file and edit the settings to suit your needs
  • It is now ready to blow your mind, almost

Starting it Up

  • Add some gasoline and finely-crushed road flares, oh, wait, wrong stuff…
  • Create a desktop shortcut named “Start SkatterTools”…
Target: powershell.exe Start-PoshServer -HomeDirectory "c:\ThisIsTheDumbestShitEver" -CustomConfig "c:\ThisIsTheDumbestShitEver\sktools.ps1"
  • Create another desktop shortcut, named “Open SkatterTools” or “Coolest Shit Ever!”…
Target: http://localhost:8080/
  • Right-click the first shortcut, select “Run as Administrator”, and wait for it to open and say something like this…
  • Double-click the second shortcut and have your Kleenex box nearby
  • Click on one of the sidebar headings and watch the slick CSS stylings ooze all over your eyeballs and onto the floor.  Compliments of some sample code I found on W3Schools.  What a great site.

If you need to shut it down, just close the browser and close the PowerShell console.  There’s instructions on the PoSH Server site for how to configure it like a service, so it runs as a background job. You don’t have to do that though.

Is there any official support?

  • Are you kidding?
  • You can submit bug reports and enhancement requests using the “Issues” link on the GitHub repo.
  • Work comes first.  I have to keep my customers happy and my bills paid
  • I’m still adding things to it frequently, but work may cause some delays getting around to it
  • You can submit your own changes via GitHub (pull requests, etc.) or just submit Issues if you prefer

Is there a roadmap?  Where is it going next?

  • Real (stupid) men don’t use maps!  The journey is the dream, man.
  • Where are any of us really going?  You ever ask that question?
  • Don’t ask that question, it’s depressing.  Enjoy the now.
  • Seriously, yes, I have a metric butt-ton of things I plan to add or improve

Double Asterisk-o-rama

  • Double-asterisks denote two things here:
    • Features are not yet complete.  Things will change.  Oceans dry up. Mountains wear down. Regimes are toppled.  Keith Richards is forever.
    • This is free stuff, and it comes without any strings attached.  No warranties, or guarantees.  No promises (other than it might possibly entertain you if you’re bored), and poor you gets to assume any and all liability, risk and responsibility for anything bad if you use it improperly or in a production environment of some kind, or any environment where alleged (love that word) damages may have occurred or been coerced by tertiary incidental hereinafters forthwith and notwithstanding, that are void where prohibited, taxed or regulated. Batteries not included.
    • Whatever that means

Cheers!

humor, Personal, Scripting, Technology

$HoHoHo = ($HoList | Do-HoHos -Days 12) version 1812.18.01

santa-riot

UPDATE: 2018.12.18 (1812.18.01) = Thanks to Jim Bezdan (@jimbezdan) for adding the speech synthesizer coolness!  I also fixed the counter in the internal loop.  Now it sounds like HAL 9000 but without getting your pod locked out of the mother ship. 😀

I’m feeling festive today.  And stupid.  But they’re not mutually exclusive, and neither am I, and so can you!   Let’s have some fun…

Paste all of this sticky mess into a file and save it with a .ps1 extension.  Then put on your Bing Crosby MP3 list and run it.

Download from GitHub: https://raw.githubusercontent.com/Skatterbrainz/Utilities/master/Invoke-HoHoHo.ps1

The function…

function Write-ProperCounter {
    param (
      [parameter(Mandatory=$True)]
      [ValidateRange(1,12)]
      [int] $Number
    )
    if ($Number -gt 3) {
        return $([string]$Number+'th')
    }
    else {
        switch ($Number) {
            1 { return '1st'; break; }
            2 { return '2nd'; break; }
            3 { return '3rd'; break; }
        }
    }
}

The bag-o-gifts…

$gifts = (
    'a partridge in a Pear tree',
    'Turtle doves, and',
    'French hens',
    'Colly birds',
    'gold rings',
    'geese a-laying',
    'swans a-swimming',
    'maids a-milking',
    'ladies dancing',
    'lords a-leaping',
    'pipers piping',
    'drummers drumming'
)
# the sleigh ride...
Add-Type -AssemblyName System.Speech
$Speak = New-Object System.Speech.Synthesis.SpeechSynthesizer

for ($i = 0; $i -lt $gifts.Count; $i++) {
    Write-Host "On the $(Write-ProperCounter $($i + 1)) day of Christmas, my true love gave to me:"
    $speak.speak(“On the $(Write-ProperCounter $($i + 1)) day of Christmas, my true love gave to me,”)
    $mygifts = [string[]]$gifts[0..$i]
    [array]::Reverse($mygifts)
    $x = $i + 1
    foreach ($gift in $mygifts) {
        if ($x -eq 1) {
            $thisGift = $gift
        }
        else {
            $thisGift = "$x $gift"
        }
        Write-Host "...$thisGift"
        $Speak.Speak($thisGift)
        $x--
    }
}

Enjoy!

Projects, Scripting, Technology

The Little (Code) Stuff That (Sometimes) Matters

As a follow-up to the post about tuning PowerShell scripts, this is going to be more general (language neutral).  I’d like to run through some of the “efficiency” or optimization techniques that apply to all program/script languages, due to how they’re parsed, and executed at the lowest layer of a conventional x86/x64 system.

Why?  Good question.  I’ve been digging into some of the MIT OpenCourseware content and it brought back (good) memories from college studies.  So I figured, why not.

Condition Prioritization

Performance isn’t mentioned as much these days outside of gaming or content streaming topics.  But processing any iterative or selective tasks that deals with larger volumes of data can still benefit greatly from some very simple techniques.

Place the most-common case higher in the condition tests.  This is also a part of heuristics, which is basically intuition or educated guessing, etc.  Using pseudo-code, here’s an example:

while ($rownum -lt $total) {
  switch ($dataset[$rownum].SomeProperty) {
    value1 { Do-Something; break; }
    value2 { Do-SomethingElse; break; }
    default { Fuck-It; break; }
  }
  $rownum++
}

Let’s assume that “value2” is found in 90% of the $dataset rows.  In this basic while-loop with a switch-case condition test, a small data set (chewed up into $dataset), won’t reveal much in terms of prioritizing the switch() tests.  Remember, that mess above is “pseudo-code” so don’t yell at me if it blows up if you try to run it.

Anyhow, what happens when you’re chewing through 400 billion rows of terabytes of data? The difference between putting “value2” above “value1” can be significant.

This is most commonly found with initialization loops.  Those are when you start with a blank or unassigned value, and as the loops continue, the starting value is incremented or modified.  There is often a test within the iteration that checks if the value has been modified from the original.  Since the initial (null) value may only exist until the first cycle of the iteration, it would make sense to move the condition [is modified] above [is not modified] since it skip an unnecessary test on each subsequent iteration cycle.

Make sense?  Geez.  I ran out of coffee 3 hours ago, and it almost makes sense to me.  Just kidding.

Sorted Conditions / Re-Filtering

Another pattern that you may run across is when you need to check if a value is contained within an Array of values.  For most situations, you can just grab the array and check if the value is contained within it and all is good.  But when the search array contains thousands or more elements, and you’re also looping another array to check for elements, you may find that sorting both arrays first reduces the overall runtime.  That’s not all, however.

What happens when the search value begins with “Z” and your search array contains a million records starting with “A”?  You will waste condition testing on A-Y.

What if you instead add a step within the iteration (loop) to essentially “pop” the previously checked items off of the search array?  So, after getting to search value “M”, the search array only contains elements which begin with “M” and on to “Z”, etc.

Figure 1 – Static Target Search Array

filtersearch1.png

Figure 2 – Reduction Target Search Array

filtersearch2

To help explain the quasi-mathematical gibberish above: S = Search Time, R = Array Reduction Overhead Time, N = Elements in Search Set.  So R+1 denotes the time incurred by calculating the positional offset, and moving the search array starting index to the calculated value.  Whereas, S alone indicates just starting each iteration on the first element of the (static) target array and incrementing until the matching value is found.

So, what does this look like with PowerShell?  Here’s *one* example…

Figure 3 – PowerShell sample code

param (
  [parameter(Mandatory=$False, HelpMessage="Pretty progressbar, but slower to run!")]
  [switch] $PrettyProgress
)
# build an array of ("A1","A2",...,"A100","B1","B2",...) up to 26 x 100 = 2600 elements

$searchArray = @()
$elementCount = 1000
$tcount = $elementCount * 26
$charArray = @()

cls

Write-Host "building search array..."
for ($i = 65; $i -le (65+25); $i++) {
  $c = [char]$i
  $charArray += $c
  for ($x = 1; $x -le $elementCount; $x++) {
     $cc = "$c$x"
     $searchArray += $cc
     if ($PrettyProgress) { Write-Progress -Activity "$($charArray -join ' ')" -Status "Building array set" -CurrentOperation "$c $x" -PercentComplete $(($m / $tcount) * 100) }
  }
}
# define list of search values...
$elementList = @("A50","C99","D75","K400","M500","T600","Z900")
$randomList  = @("T505","C99","J755","K400","A55","U401","Z960")

Write-Host "`nStatic search array"
foreach ($v in $elementList) {
  $t1 = Get-Date
  $test = ($v -in $searchArray)
  $t2 = Get-Date
  Write-Output "$v = $((New-TimeSpan -Start $t1 -End $t2).TotalSeconds)"
}

# protect the original target array for possible future use...
$tempArray = $searchArray

Write-Host "`nReduction search array"
foreach ($v in $elementList) {
  $t1 = Get-Date
  $test = ($v -in $tempArray)
  $t2 = Get-Date
  # this is the real "R"...
  $pos = [array]::IndexOf($tempArray, $v)
  $tempArray = $tempArray[$pos..$tempArray.GetUpperBound(0)]
  Write-Output "$v = $((New-TimeSpan -Start $t1 -End $t2).TotalSeconds)"
}

Figure 4 – PowerShell example process output

arraysearch1.png

The time values are in seconds, and will vary with each run depending upon thread processing overhead incurred by the host background processes.  But in general, the delta between the matched values in each iteration will be roughly the same.  To see this visually, here’s an Excel version…

Figure 4 – Spreadsheet table and Graph result

arraysearch2

It’s worth noting that the impact of R may vary by language, as well as processing platform (hardware, operating system, etc.) along a different vector than others, but that within the iteration tests, the differences should be roughly similar.

There are other methods to reduce the target array as well, which may depend upon the software language used to process the tasks.  For example, whether the interpreter or compiler makes a complete copy of the search array in the background in order to provide the index offset starting point to the script.

Again, this is all relatively meaningless for smaller data sets, or less complex data structures.  And it really only provides significant value for sequential (ordered) search operations, not for random search operations.

So, some questions might arise from this:

  1. If the source array is not sorted, does the sorting operation itself wipe out the aggregate time savings of the reduction approach?
  2. Where is the “tipping point” that would cause this approach to be of value?

These are difficult to answer.  The nature of the data within the array will have an impact I’m sure, as might the nature by which the array is provided (on demand or static storage, etc.) . To paraphrase a Don Jones statement: “try it and see.”

Now that I’m done pretending to be smart, I’m going to grab a beer and go back to being stupid.  As always – I welcome your feedback.  – Enjoy your weekend!

Scripting, Technology

The Basic Basics of Evolving a Basic (PowerShell) Script, Basically Speaking

hangover_movie_ap

In case it wasn’t obvious from the heading: This is very very very very basic basic stuff.  This is intended for people just starting to work with PowerShell.  Typical scenario:

  • Person creates a script to perform a single task (example: copy files)
  • Person doesn’t consider the future of that script (additional uses)
  • Person reads this article and decides their script may have a future
  • Person reads this article and decides to increase their alcohol consumption rate

I put PowerShell in parenthesis because this topic is really language-agnostic. I’m basing much of this on one of the course lectures from back when I attended Christopher Newport University years ago. In fact, that was when “software” was made from leather, “hardware” was either stone or wood, and processors ran on coal.

But anyhow, the point of this is to revisit something I often see with people who are just starting out with programming or scripting.  That is, how to give a basic script a tune-up, to make it more useful, and develop better coding habits going forward.

The format of this article will take an example script, and gradually (iteratively) modify it to address a few basic aspects that are commonly overlooked.  The example script is “copy-stuff.ps1”.

Version 1.0 – no diaper. poo everywhere, plays with loaded guns and broken liquor bottles in traffic.  But so, sooooooo cute…

$TargetPath = "\\fs02\docs\files"
$files = Get-ChildItem "c:\foo" -Filter "*.txt"
foreach ($file in $files) {
  copy $file.FullName $TargetPath
}

Example usage:

.\Copy-Stuff.ps1

This little chunk of tasty goodness works fine, and you just want to pinch its little fat cheeks and say stupid baby-talk things.  But it’s really rough around the edges.  It sticks forks in electrical outlets, yanks the dog’s tail, and keeps puking on everything.

Some things that would help make this script more useful:

  • Portability
    • What if you wanted to use this same script for different situations?
  • Exception handling
    • What if some (or all) of the things expected cannot be found at runtime?
    • What if the user doesn’t have permissions to source or target locations?
    • What if the target location doesn’t have enough free disk space?
    • What if you want to enforce some safeguards to prevent killing your network or disk space?
  • Self-describing information (help)
    • How can you make this easier for a new person to “figure out”?
  • Gold Teeth
    • Add a little spit-shine polish with your roommate’s best t-shirt

Version 1.1 – Portability and diaper added, learning “da da” already.

param (
  $TargetPath = "\\fs02\docs\files",
  $SourcePath = "c:\foo",
  $FileType = "*.txt"
)
$files = Get-ChildItem $SourcePath -Filter $FileType
foreach ($file in $files) {
  copy $file.FullName $TargetPath
}

Now, the script can be called with -TargetPath, -SourcePath and -FileType parameters to work with different paths and file types.

Example usage:

.\Copy-Stuff.ps1 -TargetPath "c:\folder2" -SourcePath "c:\folder1" -FileType "*.jpg"

But this still doesn’t help with Error Handling.  For example, if the user enter “x:\doofusbrain” or “y:\YoMamaSoBigSheGotLittleMamasOrbitingHer” and they don’t actually exist in the corporeal reality we call “Earth”

Version 1.2 – Error Handling and utensils added, in a high-chair with a cold beer

param (
  $TargetPath = "\\fs02\docs\files",
  $SourcePath = "c:\foo",
  $FileType = "*.txt"
)
if (!(Test-Path $SourcePath) -or !(Test-Path $TargetPath)) {
  Write-Warning "check those paths son. you might be on drugs."
  break
}
$files = Get-ChildItem $SourcePath -Filter $FileType
foreach ($file in $files) {
  copy $file.FullName $TargetPath
}

At this point, it’s portable and checking for things before putting both feet in.  But it still needs some body work.  For example, suppose that the user staggers in from a night in jail, drops their liquor bottle and tries to invoke your script, but instead of putting in a non-existent -SourcePath value, they enter “” (an empty string).

.\Copy-Stuff -TargetPath "dog" -SourcePath "" -FileType "I'm sooo wastsed"

It’s time to add some parameter input validation gyration to this…

Version 1.3 – Kevlar-lined diapers, baby bottle converts into a pink “Hello Kitty!” RPG launcher…

param (
  [parameter(Mandatory=$False)]
    [ValidateNotNullOrEmpty()]
    [string] $TargetPath = "\\fs02\docs\files", 
  [parameter(Mandatory=$False)]
    [ValidateNotNullOrEmpty()]
    [string] $SourcePath = "c:\foo", 
  [parameter(Mandatory=$False)]
    [ValidateSet('TXT','JPG')]
    [string] $FileType = "TXT" 
)
if (!(Test-Path $SourcePath) -or !(Test-Path $TargetPath)) {
  Write-Warning "check those paths son. you might be on drugs."
  break
}
$files = Get-ChildItem $SourcePath -Filter "*.$FileType"
$filecount = $files.Count
$copycount = 1
foreach ($file in $files) { 
  copy $file.FullName $TargetPath 
  Write-Output "copied $copycount of $filecount files"
  $copycount++
}

The indention of each [ValidateNotNullOrEmpty()] and [string] within the param() block are really not necessary.  I added them for visual clarity.  In fact, you could put the entire param() block on a single line, as long as you use comma separators and inhale enough paint solvent fumes first.  I recommend Xylene.

Notice that I switched from nude beach free-for-all party time on -FileType to a suit-wearing, neatly groomed, conservative business person variation using ValidateSet().  This takes away the loaded gun and gives the baby a squirt gun with only a few teaspoons of clean, luke-warm water.

Note: You could swap the [ValidateNotNullOrEmpty()] stuff with [ValidateScript()] and apply some voodoo toilet water magic to test for valid path references *before* diving into the murkiness.  But I already spent $5 on the (Test-Path) bundle and didn’t want to waste it.  Option C would be to inform every user that intentional misuse may result in their vehicle experiencing sudden loss of paint and tire pressure.

But – there’s still at least one more “error” case to consider.  What if the copy operations can’t be completed, no matter what?

What if the script is invoked by a user or service account/context, which doesn’t have sufficient permissions to the source or target locations to read and/or copy (write) the files?  Or what if the target location doesn’t have enough free disk space to allow the files to be copied?  So many “what-if’s”.

Version 1.4 – Old enough to drink, and shoot guns, but still getting carded at the door

param (
  [parameter(Mandatory=$False)]
    [ValidateNotNullOrEmpty()]
    [string] $TargetPath = "\\fs02\docs\files",
  [parameter(Mandatory=$False)]
    [ValidateNotNullOrEmpty()]
    [string] $SourcePath = "c:\foo",
  [parameter(Mandatory=$False)]
    [ValidateSet('TXT','JPG')]
    [string] $FileType = "TXT" 
)
if (!(Test-Path $SourcePath) -or !(Test-Path $TargetPath)) {
  Write-Warning "check those paths son. you might be on drugs."
  break
} 
$files = Get-ChildItem $SourcePath -Filter "*.$FileType"
$filecount = $files.Count
$copycount = 1
foreach ($file in $files) { 
  try {
    copy $file.FullName $TargetPath -ErrorAction Stop
    Write-Output "copied $copycount of $filecount files"
    copycount++
  }
  catch {
    Write-Error $Error[0].Exception.Message
    break
  }
}

There’s much more you can do with error (exception) handling.  You could enforce restrictions on file types, or file sizes.  You could check for the error type and display more targeted explanations, rather than just dumping the $Error[0].Exception.Message content.  For more on this topic, I recommend this.

Version 1.5 – Self-Describing Help with french fries and a lobster bib

param (
  [parameter(Mandatory=$False, HelpMessage = "Destination Path")]
    [ValidateNotNullOrEmpty()]
    [string] $TargetPath = "\\fs02\docs\files",
  [parameter(Mandatory=$False, HelpMessage = "Source Path")]
    [ValidateNotNullOrEmpty()]
    [string] $SourcePath = "c:\foo",
  [parameter(Mandatory=$False, HelpMessage = "File extension filter")]
    [ValidateSet('TXT','JPG')]
    [string] $FileType = "TXT" 
)
if (!(Test-Path $SourcePath) -or !(Test-Path $TargetPath)) {
  Write-Warning "check those paths son. you might be on drugs."
  break
} 
$files = Get-ChildItem $SourcePath -Filter "*.$FileType"
$filecount = $files.Count
$copycount = 1
foreach ($file in $files) { 
  try {
    copy $file.FullName $TargetPath -ErrorAction Stop
    Write-Output "copied $copycount of $filecount files"
    $copycount++
  }
  catch {
    Write-Error $Error[0].Exception.Message
    break
  }
}

Now the script can be poked to display information that describes the purpose of each parameter.  But there’s so much more we can do to this.  But hit pause for a second…

> Why bother?  What’s wrong with a simple “copy from-this to-that” script?

The point of developing a skill/craft/caffeine-habit is to expand your capabilities and your value as a technical resource.  Anyone can fix a leak with duct tape.  But the person who can fix it with duct tape, while chugging a six-pack of beer, and singing Bohemian Rhapsody at the same time, is going to make a higher income.  And besides, it’s just cool stuff to learn.

Version 1.6 – the spit-polish, hand-rubbed, gluten-free version

[CmdletBinding(SupportsShouldProcess=$True)]
param (
  [parameter(Mandatory=$True, HelpMessage = "Destination Path")]
    [ValidateNotNullOrEmpty()]
    [string] $TargetPath,
  [parameter(Mandatory=$True, HelpMessage = "Source Path")]
    [ValidateNotNullOrEmpty()]
    [string] $SourcePath,
  [parameter(Mandatory=$False, HelpMessage = "File extension filter")]
    [ValidateSet('TXT','JPG')]
    [string] $FileType = "TXT" 
)
$time1 = Get-Date
if (!(Test-Path $SourcePath) -or !(Test-Path $TargetPath)) {
  Write-Warning "check those paths son. you might be on drugs."
  break
} 
$files = Get-ChildItem $SourcePath -Filter "*.$FileType"
$filecount = $files.Count
$copycount = 1
foreach ($file in $files) { 
  $pct = $($copycount / $filecount) * 100
  try {
    copy $file.FullName $TargetPath -ErrorAction Stop
    Write-Progress -Activity "Copying $copycount of $filecount files" -Status "Copying Files" -PercentComplete $pct
    $copycount++
  }
  catch {
    Write-Error $Error[0].Exception.Message
    break
  }
}
$time2 = Get-Date
Write-Verbose "completed in $([math]::Round((New-TimeSpan -Start $time1 -End $time2).TotalSeconds,2)) seconds"

Example usage (using -WhatIf):

.\Copy-Stuff.ps1 -SourcePath "c:\folder1" -TargetPath "x:\folder3" -Verbose -WhatIf

Some of the changes added to this iteration:

  • [CmdletBinding(SupportsShouldProcess=$True)] added so we can use Write-Verbose to toggle output display only when we really need it, and use -WhatIf to see what would happen if it actually did happen.
  • Write-Progress added for impressing people who like visual progress indication.
  • Displays total run time at the end, for those who are impatient.

[CmdletBinding()] vs. [CmdletBinding(SupportsShouldProcess=$True)] ?

If your code is going to modify things somewhere, and you’d like to have an option to try it in a “what-if?” mode first, use the longer form above.  If you only want to see verbose output (a la debugging/testing), you can use the shorter form above.  For more detail about this cool feature, and the other options it provides, click here.

Anyhow, I hope this was at least mildly helpful or amusing.  I’m sure half of you didn’t read this far, and half of those that did are rolling your eyes “He should’ve ____.  What a loser.”

Updated: Changed highlight color from dark red to turquoise because it sounds better. 🙂

Updated 2: Fixed “$FileType.*” to “*.$FileType” – thanks to @gpunktschmitz for catching that!

Anyhow, post feedback if you would like.  I’m once again weighing the future of this blog by the feedback (or lack thereof).  It’s starting to feel like talking into an empty room again.