Common solution to zipping files in PoSh

The code below, or very similar variations, seem to be a very widespread recommendation to adding files to a zip archive in PowerShell:

$fileName = 'C:\Temp\test.zip'
$source = 'C:\Temp\reallylargefile.txt'
Set-Content $fileName ('PK' + [char]5 + [char]6 + ("$([char]0)" * 18));
$oShell = New-Object -ComObject Shell.Application;
$zipPackage = $oShell.NameSpace($fileName);
$zipPackage.CopyHere($source);

There’s nothing particularly wrong with this code — it’s easy, it’s short, and it’s effective. However, a number of scripters out there have noticed that there’s a small problem: The CopyHere method returns immediately, rather than waiting for the zip process to complete. If your script wants to do something with that zip file, it’s got to find a way to know when the zip is free to be moved, renamed, copied, and so on.

Fortunately, there’s a way to do that. Here’s the solution I use:

function Test-FileLock {
 
    param (
		[parameter(Mandatory=$true,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
		[Alias("FullName")]
		[string]$Path,
		[switch]$PassThru
	)
 
	begin {}
 
	Process
	{
	    if ((Test-Path -Path $Path) -eq $false)
	    {
			throw [System.IO.FileNotFoundException] 
	    	return
	    }
 
	    $oFile = Get-Item $Path
 
	    if ($oFile.PSIsContainer)
	    {
    		return
	    }
 
	    try
	    {
		    $oStream = $oFile.Open([System.IO.FileMode]::Open, [System.IO.FileAccess]::ReadWrite, [System.IO.FileShare]::None)
		    if ($oStream)
		    {
		        $oStream.Close()
		    }
		    if ($PassThru.IsPresent -eq $false)
		    {
		    	$false
		    }
		    else
		    {
		    	Get-Item $Path
		    }
	    }
	    catch
	    {
	    	# file is locked by a process.
		    if ($PassThru.IsPresent -eq $false)
		    {
		    	$true
		    }
	    }
	}
	end {}
}

The function above can be handy for any instance in which you need to know whether a file is being held open by another process. It just so happens that one of those instances is when waiting for Explorer to finish dumping files into a zip archive! Armed with the example above, you can easily wait for the zip file to become available:

$zipPackage.CopyHere($source);
 
Start-Sleep -Seconds 2
while (Test-FileLock $fileName)
{
	Start-Sleep -Seconds 2
}

Note that while the default for the Test-FileLock function is to return $true or $false, I added a -PassThru parameter that changes the output to either a FileInfo object or $null. This alternate behavior allows you pass unlocked files down the pipeline to be used by other cmdlets.

Helper function: Set-TimeZone

Getting time zone information in PowerShell is an exercise in simplicity. This single command:

[TimeZoneInfo]::Local

… yields about as much information as you’d generally care to know about the current system time zone.

Sample results:

Id                         : Mountain Standard Time
DisplayName                : (UTC-07:00) Mountain Time (US & Canada)
StandardName               : Mountain Standard Time
DaylightName               : Mountain Daylight Time
BaseUtcOffset              : -07:00:00
SupportsDaylightSavingTime : True

Setting the current time zone, unfortunately, isn’t quite as easy or intuitive. This is something I needed to do a while back for my desktop imaging process. Although the runtime portion of the Microsoft Deployment Tools (MDT) are largely built on a VBScript foundation, most of my deployment scripts are PowerShell-based, so I came up with a simple Posh function for setting the system time zone that works for both Windows XP and Windows 7.

There are a number of crazy ways to set time zone information, but rather than delving into methods that directly modify the registry and such, I decided upon documented, supported ways to change the current time zone:

function Set-TimeZone {
 
	param(
		[parameter(Mandatory=$true)]
		[string]$TimeZone
	)
 
	$osVersion = (Get-Item "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion").GetValue("CurrentVersion")
	$proc = New-Object System.Diagnostics.Process
	$proc.StartInfo.WindowStyle = "Hidden"
 
	if ($osVersion -ge 6.0)
	{
		# OS is newer than XP
		$proc.StartInfo.FileName = "tzutil.exe"
		$proc.StartInfo.Arguments = "/s `"$TimeZone`""
	}
	else
	{
		# XP or earlier
		$proc.StartInfo.FileName = $env:comspec
		$proc.StartInfo.Arguments = "/c start /min control.exe TIMEDATE.CPL,,/z $TimeZone"
	}
 
	$proc.Start() | Out-Null
 
}

You can call this function using the friendly time zone name associated with the zone you want. For example,

Set-TimeZone "Pacific Standard Time"
Set-TimeZone "North Asia East Standard Time"
Set-TimeZone "GMT Standard Time"

… and so on.

Note that the function uses the .NET Process class to execute the necessary commands. The Windows 7 tzutil method doesn’t need this, but the control.exe command used for XP and earlier won’t work with the Start-Process cmdlet, so I went with the lowest common denominator.

List GPOs that apply to a specific Active Directory group

I manage an Active Directory environment that’s over ten years old and has undergone a number of upgrades and transitions. Yesterday I found myself trying to clean up some groups that didn’t appear to be used for anything anymore, but I wanted to make sure I wasn’t missing anything. One task I needed to accomplish for this was to make sure there were no active GPOs applying to those AD groups.

The cool thing is, PowerShell makes that task pretty darn easy! I whipped up the function below (which relies on the Microsoft GroupPolicy cmdlets) and was able to find exactly the information I was looking for.

function Get-GroupPoliciesByGroup {
 
	param(
		[Parameter(Mandatory=$true)]
		[Alias("Name")]
		[string]$GroupName,
		[string]$Domain
	)
 
	if ((Get-Command Get-GPO -ErrorAction SilentlyContinue) -eq $null)
	{
		Import-Module GroupPolicy
	}
 
	if ($Domain -eq $null -or $Domain -eq '')
	{
		$Domain = ([System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain().Name)
	}
 
	$gpos = Get-GPO -All -Domain $Domain
 
	foreach ($gpo in $gpos)
	{
		$secinfo = $gpo.GetSecurityInfo() | where { $_.Permission -eq "GpoApply" }
		foreach ($sec in $secinfo)
		{
			if ($sec.Trustee.Name -eq $GroupName)
			{
				Out-Default -InputObject $gpo
			}
		}
	}
}

As you can see, it’s possible to pass this function a specific domain if desired. There are plenty of other ways to extend the function’s behavior as well, but the code above was enough to get me what I needed to know.

List local group members on a remote computer using WMI and PowerShell

Today I needed to sort through the computers on a domain and check the machines’ local Administrators group for non-standard entries. I found quite a few examples of listing local group members using ADSI, but I tend to avoid ADSI when possible, as its use has been falling out of favor for some time.

That meant using WMI — but the only WMI-based examples I found were written in VBScript. So I made a quick and dirty function that would do what I needed:

function Get-LocalGroupMembers 
{ 
    param( 
        [parameter(Mandatory=$true,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)] 
        [Alias("Name")] 
        [string]$ComputerName, 
        [string]$GroupName = "Administrators" 
    ) 
 
    begin {} 
 
    process 
    { 
        # If the account name of the computer object was passed in, it will 
        # end with a $. Get rid of it so it doesn't screw up the WMI query. 
        $ComputerName = $ComputerName.Replace("`$", '') 
 
        # Initialize an array to hold the results of our query. 
        $arr = @() 
 
        $wmi = Get-WmiObject -ComputerName $ComputerName -Query ` 
            "SELECT * FROM Win32_GroupUser WHERE GroupComponent=`"Win32_Group.Domain='$ComputerName',Name='$GroupName'`"" 
 
        # Parse out the username from each result and append it to the array. 
        if ($wmi -ne $null) 
        { 
            foreach ($item in $wmi) 
            { 
                $arr += ($item.PartComponent.Substring($item.PartComponent.IndexOf(',') + 1).Replace('Name=', '').Replace("`"", '')) 
            } 
        } 
 
        $hash = @{ComputerName=$ComputerName;Members=$arr} 
        return $hash 
    } 
 
    end{} 
}

This function turns out to be a cinch to use with my favorite Active Directory tools from Quest:

$AdminList = Get-QADComputer -SizeLimit 0 | Get-LocalGroupMembers -GroupName "Administrators"

Output resembles something like this in the console, but you can do any number of things with the data to format it and send it to a CSV or text file:

ComputerName                   PRINT-TST1
Members                        {NTAdmin, ServerLocalAdmin_DL_IA_F, DesktopServerLocalAdmin_DL_IA_F}
ComputerName                   CTX105
Members                        {NTAdmin, ServerLocalAdmin_DL_IA_F, DesktopServerLocalAdmin_DL_IA_F}
ComputerName                   CTX106
Members                        {NTAdmin, ServerLocalAdmin_DL_IA_F, DesktopServerLocalAdmin_DL_IA_F}
ComputerName                   CTX107
Members                        {NTAdmin, ServerLocalAdmin_DL_IA_F, DesktopServerLocalAdmin_DL_IA_F}

Are two dates equal?

Let’s say you’ve got a scheduled PowerShell task running to pull a list of files created on a certain date. You might try accomplishing such a feat with this code:

$datePreserveWeek = (Get-Date).AddDays(-7)
 
Get-ChildItem -Path "C:\Backups" -Filter *.bak |
        where { $_.CreationTime -eq $datePreserveWeek }

Unfortunately, even if you have files that were created exactly 7 days ago, you will most likely get nothing back from this call. That’s because CreationTime and $datePreserveWeek are DateTime objects, which store both date (year, month, day) and time (hour, minute, second) data. When you compare them with the -eq operator, unless the objects also happen to match exactly the specific time, you’ll get a $false result.

In the past, I’ve solved this problem with the following solution:

Get-ChildItem -Path "C:\Backups" -Filter *.bak |
    where { $_.LastWriteTime.Year -eq $datePreserveWeek.Year -and
        $_.LastWriteTime.Month -eq $datePreserveWeek.Month -and
        $_.LastWriteTime.Day -eq $datePreserveWeek.Day }

However, this method, while successful, apparently exposed my lack of knowledge about .NET’s DateTime class. MVP Shay Levi pointed out that the same thing can be accomplished with a bit less code!

Get-ChildItem -Path "C:\Backups" -Filter *.bak |
        where { $_.CreationTime.Date -eq $datePreserveWeek.Date }

The more you know…

Safely check for a variable’s existence in PowerShell

If you’re needing to access a PowerShell variable, but aren’t guaranteed that the variable has already been declared, you might be looking for a surefire way of finding out. Luckily, the PSProvider that manages the variable store can easily tell you. Simply use the Test-Path cmdlet like so:

if (Test-Path "variable:\VarName")
{
     Write-Host "The variable VarName exists. Its value is $VarName"
}

Note that this can come in handy whether or not you use Strict Mode; Test-Path will return $false for a variable that hasn’t been assigned a value at all, but will report $true if the variable has been explicitly assigned a value of $null. If you were trying to dump the return value of a cmdlet into a variable, for instance, this distinction can sometimes tell you whether the cmdlet ended its execution abnormally.