Some friends here on the Hyper-V team shared a PowerShell 2.0 script for copying a VM:

################################################################################
#
#   Copyright ©2008 Microsoft Corporation.  All rights reserved.
#
#   File: Copy-Vm.ps1
#
#   THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF
#   ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO
#   THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
#   PARTICULAR PURPOSE.
#
#   Copyright ©2008 Microsoft Corporation.  All rights reserved.
#
################################################################################

param
(
    [string]$VmName = $(throw "VmName required"),
    [string]$Path = $(throw "Path required"),
    [string]$NewName = ""
)

#
# Constants
#
$SilentlyContinue = [System.Management.Automation.ActionPreference]0

#
# Utility functions
#

# Processes the (possibly asynchronous) result of a method call on one
# of Hyper-V's service objects, and return the resulting object, if any.
function ProcessResult
{
    param
    (
        [System.Management.ManagementBaseObject]$Result,
        [ScriptBlock]$ExtractScript = $null,
        [string]$ResultClass = ""
    )

    $RetObj = $null

    if ($Result.ReturnValue -eq 0)
    {
        if ($ExtractScript -ne $null)
        {
            $RetObj = [WMI] ($Result | Select-Object -inputObject $ExtractScript)
        }
    }
    else
    {
        if ($Result.ReturnValue -ne 4096)
        {
            throw $Result.ReturnValue
    }
        $Job = [WMI]$Result.Job
        while ($Job.JobState -eq 4)
        {
            Write-Progress $Job.Caption "% Complete" -PercentComplete $Job.PercentComplete
            Start-Sleep 1
            $Job.PSBase.Get()
        }

        if ($Job.JobState -ne 7)
        {
            throw $Job.ErrorDescription
        }

        Write-Progress $Job.Caption "Completed" -Completed $true

        if ($ResultClass -ne "")
        {
            $Query = "Associators of {$Job} Where ResultClass=$ResultClass AssocClass=Msvm_AffectedJobElement"
            $RetObj = Get-WmiObject -Namespace root\virtualization -Query $Query
    }
    }

    if ($RetObj -ne $null)
    {
    $RetObj
    }
}

#
# Main script body
#

#Stop script exection on errors
Set-Variable $ErrorActionPreference Stop

# If no target name was specified, generate a generic one
if ($NewName -eq "")
{
    $NewName = "Copy of " + $VmName
}

# If we are going to display progress bars, clear the console window so that
# we can see the progress bars more clearly,
if ($ProgressPreference -ne $SilentlyContinue)
{
    Clear-Host
    Write-Host "Copying `"$VmName`" to `"$NewName`"..."
}

# Get the service object
$VmSvc = Get-WmiObject -Namespace root\virtualization -Class Msvm_VirtualSystemManagementService

# Find the virtual machine to copy.
$Query = @"
Select * From Msvm_ComputerSystem Where
    ElementName='$VmName' Or
    Name='$VmName'
"@

$SourceVm = Get-WmiObject -Namespace root\virtualization -Query $Query

# Since the virtual machine name is not unique, we need to check for
# multiples. If there is more than one, we'll return an error.
$SourceCount = [int]0
if ($SourceVm -ne $null)
{
    $SourceCount = [int]($SourceVm | Measure-Object).Count
}

if ($SourceCount -eq 0)
{
    throw "Virtual machine `"$VmName`" not found."
    return
}
elseif ($SourceCount -ne 1)
{
    if ($ProgressPreference -ne $SilentlyContinue)
    {
        Write-Host "The name `"$VmName`" has been given to multiple virtual machines. Specify the source virtual machine by ID instead. The IDs for virtual machines with this name are listed below."
        $SourceVm | `
            Select-Object @{Name="ID";Expression={$_.Name}},@{Name="Name";Expression={$_.ElementName}} | `
            Format-Table -AutoSize
    }
    throw "The name `"$VmName`" has been given to multiple virtual machines."
}

# Export the virtual machine
$Result = $VmSvc.ExportVirtualSystem($SourceVm, $true, $Path)
$Temp = ProcessResult $Result

# Rename the export directory
$CopyPath = (Join-Path $Path $VmName | Rename-Item -NewName $("Copy of " + $VmName) -PassThru)

# Import the virtual machine
$Result = $VmSvc.ImportVirtualSystem($CopyPath, $true)
$CopyVm = ProcessResult $Result {throw "Unexpected error."} Msvm_ComputerSystem

# Rename the copy virtual machine to "Copy of $VmName"
$Query = @"
Associators of {$CopyVm} Where
    ResultClass=Msvm_VirtualSystemSettingData
    AssocClass=Msvm_SettingsDefineState
"@

$CopyVssd = Get-WmiObject -Namespace root\virtualization -Query $Query
$CopyVssd.ElementName = "Copy of " + $CopyVssd.ElementName

$Result = $VmSvc.ModifyVirtualSystem($CopyVm, $CopyVssd.PSBase.GetText(1))
$Temp = ProcessResult $Result {$_.ModifiedSettingData} Msvm_VirtualSystemSettingData

# Update and return the copied virtual machine object
$CopyVm.PSBase.Get()
$CopyVm

For more info on how to use PS cmdlets see: http://www.microsoft.com/technet/scriptcenter/topics/msh/cmdlets/index.mspx

See also James O’Neil’s New and improved PowerShell Library for Hyper-V. Now with more functions and... documentation!

For all 35 sample Hyper-V PS1 scripts in a zipfile, go to: Hyper-V PowerShell Example Scripts.zip-download