Learn about Windows PowerShell
(Note: These solutions were written for Advanced Event 6 of the 2010 Scripting Games.)
Andy Schneider is a systems engineer who works for Avanade in Seattle, Washington, USA. You can find his blog at http://get-powershell.com.
------------
I will be the first to admit that I learned a few things while writing this script. When I saw that it had to respond to USB devices, I immediately started thinking of events and the new support for events in Windows PowerShell 2.0. I decided to work on this part of the code first. Using the WMI Query Language, I came up with this block of code:
$wql = @"
SELECT * FROM __InstanceCreationEvent
WITHIN 2
WHERE TargetInstance
ISA 'Win32_LogicalDisk'
"@
Let’s break it down, line by line. First, select all the events that have to do with something being created (for example, a new USB drive), and check for it every 2 seconds. Then you filter the events by making sure that what you are responding to (the TargetInstance) is a logical disk. After we have the query, we can register for those events using the Register-WMIEvent cmdlet.
Now that this is all wired up, the next step is figuring out how to do something when this event fires. There is a parameter called –action for Register-WMIEvent which is a ScriptBlock that you can specify. This ScriptBlock will run every time the event fires. There were two problems I came across when working with the –action parameter. The first was, “How do I get at the Win32_LogicalDisk info that caused the event to fire in the first place?” It turns out, within the action ScriptBlock, there are some automatic variables. I was able to find the drive device ID with the following code:
$drive = $event.SourceEventArgs.newEvent.TargetInstance.DeviceID
The second problem was a scoping issue. Within the ScriptBlock, I was not able to reference my variables in my script. Frankly, there is probably a better way to do this, but I resorted to global variables to solve this problem. This is where the $GLOBAL:_xyz variables come from.
The next issue was dealing with how Copy-Item treats files differently than directories. Copy-Item will automatically overwrite files, but it will not automatically overwrite directories unless you use the –force parameter. The logic I wrote to determine what to copy depending on what mode the script is in assumed that I either had a FileInfo object or a DirectoryInfo object. I needed these because my script used the .Name property and the .FullName property on either of these object types.
Rather than putting this logic in the body of the script, I dealt with it during parameter declaration. There is an advanced function parameter attribute called [ValidateScript].
param (
[Parameter( Position = 0,
ValueFromPipeline = $true)]
[Alias("Directory")]
[ValidateScript({($_.GetType().Name -eq "DirectoryInfo") -or `
($_.GetType().Name -eq "FileInfo")})]
$file,
Basically, I am using the GetType() method to validate that the entries are either fileinfo or directoryinfo objects. This allowed me to specify either a group of files, folders, or any combination of the two. I also used the ValueFromPipeline so that I can pipe a dir into the script. In the Process block, I then gathered up all the pipelined objects into an array so that I can use all of them in my $action ScriptBlock.
Here is the entire script:
<#
.Synopsis
Event 6 in the 2010 Scripting Games - Automatically copy files to a USB drive when it is plugged in
.Description
Requirements:
The Script should allow you to select a file, a group of files, a folder, or a group of folders
to contain the files that will be copied. The script should offer a “quiet mode” and a “prompt mode” based
upon a switch supplied to the script when it is launched. Quiet mode should be the default. In quiet mode,
the script copies the target files/folders to the newly inserted drive without user interaction.
The script should run in continuous loop fashion until it is stopped, or it should offer to make a specific
number of copies based upon a command-line argument. For example, make five copies of the data, and then stop.
The script should offer a “force” mode that will overwrite existing files, or a “prompt” mode that will prompt
before overwriting existing files. This should also be configurable via the command argument.
.Example
ls *.txt | Event6.ps1
.Notes
Author: Andy Schneider
Blog: http://get-powershell.com
#>
[Parameter(Position = 2)]
[int]
$timesToCopy = 2,
[Parameter(Position = 1)]
[switch]
$promptMode = $false,
[Parameter(Position = 3)]
$force = $false
)
Begin {
$GLOBAL:_files = @()
$GLOBAL:_promptMode = $promptMode
$GLOBAL:_timesToCopy = $timesToCopy
$GLOBAL:_force = $force
} # End BEGIN
Process {
$GLOBAL:_files += $file
} # End Process
End {
# Give user some feedback about which files and directories will be copied
Write-Host "The files and directories below will be copied to a USB Drive"
$GLOBAL:_files
write-host "There are $GLOBAL:_timesToCopy copies left until automatic exit - press Ctrl+C to stop"
$action = {
$GLOBAL:_timesToCopy -=1;
write-host "There are $GLOBAL:_timesToCopy copies left until automatic exit"
foreach ($file in $GLOBAL:_files) {
$fileName = $file.Name
$fullName = $file.FullName
$fileExists = Test-Path -path ("$drive\$fileName")
# Note - Copy-Item overwrites files without warning - thus using $fileExists
if (!($fileExists)) { Copy-Item -Path $fullName -Destination $drive -Recurse}
# Force parameter required if you are copying a directory and you want to overwrite
# Files will always be overwritten - so it will still work
if ($_force) {Copy-Item -Path $fullName -Destination $drive -force -Recurse}
if ($_promptMode -and $fileExists) {
write-warning "Will Overwrite: $drive\$fileName"
Copy-Item -Path $fullName -Destination $drive -force -confirm -Recurse
}
if ($_promptMode -and (!$fileExists)) {Copy-Item -Path $file -Destination -confirm -Recurse}
} #End foreach
} # End $action
# Unregister if we are already subscribed
Unregister-Event newUsbDrive -ErrorAction SilentlyContinue
Register-WmiEvent -Query $wql -SourceIdentifier "newUsbDrive" -action $action | out-null
while ($GLOBAL:_timesToCopy -gt 0) {
# sit and wait for a USB Drive to be inserted
sleep -Milliseconds 50
} # While
# Clean up global variable garbage
Remove-Variable _files
Remove-Variable _promptMode
Remove-Variable _timesToCopy
Remove-Variable _force
} # End
The following image shows the result of running the script.
Steve Lee, Senior Test Manager VBScriptWindows Management Platformhttp://blogs.msdn.com/wmi
For this challenge, the goal was to write a script that would be triggered when a USB drive was inserted and copy a selected number of files and folders to that drive. Because the trigger was an operating system event, I immediately thought about finding a WMI event class for notifying when a new drive was available.
WMI has a Win32_VolumeChangeEvent class that fires an event whenever there is any change to the disk volumes. For this specific challenge, I was only concerned when a new drive arrived, so I created a WQL notification query for this purpose: select * from Win32_VolumeChangeEvent where EventType = 2. Based on this WMI class, Values, and ValueMap qualifiers, I can see that a value of 2 means disk arrival, which suits my purpose.
In general, I like to separate getting the command-line arguments (also called parameters) from the main subroutine. In the GetArguments() subroutine, I tried to simplify the number of arguments that would be accepted to the source path, a regular expression for pattern matching, and two switches whether to prompt and overwrite (as required by the challenge).
To keep the script simple, I decided to just use regular expressions. Most scripters are familiar with regular expression. If I had to support basic wildcard characters, I would have written a function to convert a wildcarded string into a regular expression.
After I have parsed all the arguments, the main function is relatively simple. I start an asynchronous WMI event notification query, and the script basically sleeps from there. For this particular challenge, I could have used a simpler semi-sync notification query, but in general, async is preferred because it allows my script to process further and is a pattern I’ve become accustomed to even when not necessary.
My SINK_OnObjectReady() subroutine is called when WMI has an event that matches my filter. In here, I prompt if needed to verify that the user wants to copy the files to this particular USB drive. When confirmed, I wrote my own file copy routine primarily for the purpose of showing progress. I could have used the VBScript CopyFolder method, but it would have been less user-friendly because no progress information is given. Writing my own routine also allows me to satisfy the bonus goals of checking free space and listing files to be copied, although I did not actually do that here.
The file copy subroutine is pretty straight forward. I iterate through the files in the source folder and use the regular expression to check for matches, and finally copy the file. After that, I recursively go through the subfolders.
doPrompt = false
doOverwrite = false
sourceFolder = ""
pattern = "*"
set wmiSink = WScript.CreateObject("wbemscripting.SWbemSink","SINK_")
set fso = CreateObject("scripting.filesystemobject")
set shell = CreateObject("wscript.shell")
call GetArguments()
call Main()
sub Main()
set cimv2 = GetObject("winmgmts:root/cimv2")
call cimv2.ExecNotificationQueryAsync(wmiSink, "Select * from Win32_VolumeChangeEvent " & _
"where EventType = 2")
wscript.echo "Waiting for new USB Drive..."
while (true)
wscript.sleep 1000
wend
end sub
sub CopyFilesTo(source, dest)
wscript.echo "Copying files from " & source & " to " & dest
set regex = new RegExp
regex.pattern = pattern
set srcFolder = fso.GetFolder(source)
for each srcFile in srcFolder.Files
if (regex.Test(srcFile.name)) then
wscript.echo "Copying " & srcFile.Path
On Error Resume Next
call srcFile.Copy(dest, doOverwrite)
if (err) then
wscript.echo "Error: " & err.description
end if
On Error Goto 0
next
for each subFolder in srcFolder.SubFolders
destFolder = dest & subFolder.Name
if (regex.Test(subFolder.name)) then
if (not fso.FolderExists(destFolder)) then
wscript.echo "Creating folder " & destFolder
fso.CreateFolder (destFolder)
call CopyFilesTo(subFolder.path, destFolder & "\")
sub SINK_OnObjectReady(obj, ctx)
if (doPrompt) then
input = ""
while (input <> "y" and input <> "n")
wscript.echo "Copy files to " & obj.DriveName & " (Y/N)?"
input = lcase(WScript.stdin.ReadLine())
if (input = "n") then
exit sub
call CopyFilesTo(sourceFolder, obj.DriveName & "\")
wscript.echo "Done" & vbnewline & "Waiting for new USB Drive..."
sub GetArguments()
set args = WScript.Arguments
if (args.Count < 2) then
ParamError("<source> and <pattern> needs to be specified")
elseif (args.Count > 2) then
for i = 2 to args.Count - 1
select case lcase(args(i))
case "-prompt"
doPrompt = true
case "-overwrite"
doOverwrite = true
case else
ParamError("Unknown option " & args(i))
end select
sourceFolder = WScript.Arguments(0)
pattern = WScript.Arguments(1)
sub ParamError(str)
WScript.Echo "Error: " & str & vbNewline
PrintHelp()
WScript.Quit 1
sub PrintHelp()
WScript.Echo "Usage: " & WScript.ScriptName & " <source> <regExp> [-prompt] [-overwrite]" & _
vbNewline & vbNewline & "Ex: " & WScript.ScriptName & " c:\test .*?\.ps1 -overwrite"
The following image shows the results of running the script:
If you want to know exactly what we will be looking at tomorrow, follow us on Twitter or Facebook. If you have any questions, send e-mail to us at scripter@microsoft.com or post your questions on the Official Scripting Guys Forum. See you tomorrow. Until then, peace.
Ed Wilson and Craig Liebendorfer, Scripting Guys