Welcome to TechNet Blogs Sign in | Join | Help

Hey, Scripting Guy! Quick-Hits Friday: The Scripting Guys Respond to a Bunch of Questions (11/20/09)

Bookmark and Share

In this post:

 

Troubleshooting a Windows PowerShell Script Using the Replace Operator

Hey, Scripting Guy! Question

Hey, Scripting Guy! I am learning Windows PowerShell in preparation for a Windows 7 and Server 2008 rollout. In the How Can I Use Windows PowerShell to Replace Characters in a Text File? post, you use the replace operator. I tried to use the replace operator, but it didn't work from an interactive shell. Does this only work from inside a script? Here is an example:

$a = "apple"

$a -replace "a", "b"


It would output what I wanted, but when I would enter $a, it would still print apple. It only overwrites the existing value of $a when I run this from a script, not interactively.

Do you have any suggestions?


-- SC

 

Hey, Scripting Guy! AnswerHello SC,

You are not storing the value that is being replaced inside a variable. You are merely replacing the value that is displayed, but not the value that is stored. You need to write back to the $a variable. This is seen here:

Image of writing back to the $a variable

 

 

How Can I Get a List of Certificates and Their Expiration Dates?

Hey, Scripting Guy! Question

Hey, Scripting Guy! Is there any way to get a list from Microsoft Certificate Services of the certificates and their expiration dates?

 

-- SM

 

Hey, Scripting Guy! Answer Hello SM, 

Using Windows PowerShell, it is easy. You can use the Get-ChildItem cmdlet on the Cert: drive. The Get-ChildItem cmdlet has an alias of Dir, so it is easy to remember. If you wish to see all of the certificates in all of the stores, you can use the -recurse switch. To see the expiration date of your certificates, use the Dir command on your certificate drive. The expiration date is the NotAfter property. This is seen here:

PS C:\> dir cert:\CurrentUser -Recurse

 

 

Name : SmartCardRoot

 

Name : UserDS

 

Name : AuthRoot

 

Subject      : CN=UTN-USERFirst-Object, OU=http://www.usertrust.com, O=The USERTRUST Network, L=Salt Lake City, S=UT, C

               =US

Issuer       : CN=UTN-USERFirst-Object, OU=http://www.usertrust.com, O=The USERTRUST Network, L=Salt Lake City, S=UT, C

               =US

Thumbprint   : E12DFB4B41D7D9C32B30514BAC1D81D8385E2D46

FriendlyName : USERTrust

NotBefore    : 7/9/1999 2:31:20 PM

NotAfter     : 7/9/2019 2:40:36 PM

Extensions   : {System.Security.Cryptography.Oid, System.Security.Cryptography.Oid, System.Security.Cryptography.Oid, S

               ystem.Security.Cryptography.Oid...}

 

To determine if a certificate is expired, check the value of the NotAfter property and compare it to the value of the current date. If the NotAfter property is less than the current date, the certificate is expired. This is seen here:

PS C:\> dir cert:\CurrentUser -Recurse | Where { $_.NotAfter -le (Get-Date) }

 

 

Name : SmartCardRoot

 

Name : UserDS

 

Name : AuthRoot

 

Subject      : OU=Class 3 Public Primary Certification Authority, O="VeriSign, Inc.", C=US

Issuer       : OU=Class 3 Public Primary Certification Authority, O="VeriSign, Inc.", C=US

Thumbprint   : 4F65566336DB6598581D584A596C87934D5F2AB4

FriendlyName : VeriSign

NotBefore    : 1/28/1996 7:00:00 PM

NotAfter     : 1/7/2004 6:59:59 PM

Extensions   : {}

 

 

Can I Change a Registry Value on 200 Computers?

Hey, Scripting Guy! Question

Hey, Scripting Guy! I need to change a value in the registry. The registry key is listed here:

HKEY_CURRENT_USER\Identities\{GUID for Identity}\Software\Microsoft\Outlook
Express\5.0

The problem is that I need to change this value on 200 computers. Is there a way to write some constant instead of the real GUID for Identity?

-- MK

 

Hey, Scripting Guy! AnswerHello MK,

I don’t think so because the GUID is what Outlook Express uses to keep track of the different users. What you will have to do is to read the value that is stored in the identities key and store the GUID that is returned in a variable. After you have that value, you can proceed to modify the child keys. Because you want to get this on 200 computers, you will need to use the WMI stdregprov class if you wish to run remotely, or you can use the VBScript registry classes if you will do this via login script.

Of course, Windows PowerShell makes it extremely easy to work with the registry, but based upon the registry key you are modifying, I doubt you have Windows PowerShell installed on all of your desktop machines yet.

Here are some registry scripts from the Script Repository that might help (you can refine the search results by selecting languages and other options from the lower left side of the screen).

In addition to the scripts from the Script Repository, you may be interested in some of the Hey, Scripting Guy! posts that talk about modifying the registry. The Hey, Scripting Guy! posts can provide you with more information than simple script samples (you can further refine the search results by clicking the keywords in the tag cloud above the list of articles).

 

 

How Can I Use Windows PowerShell to Delete Folders Within a Directory?

Hey, Scripting Guy! Question

Hey, Scripting Guy! As a complete newbie to Windows PowerShell scripting, I seek your advice. Usually, I would use a DOS batch file to complete this task, but Powershell is the way to go and so what better way to start learning than with a real life example?

I have a number of folders within a directory, all with files inside (the numbers at the end are randomly generated). The folders are shown here:

Folder14

Folder26

Folder01

I would like to delete the folders. I could use an old school loop through with FOR /F and RMDIR, but how would I do this using a Windows PowerShell script?

-- AS

 

Hey, Scripting Guy! AnswerHello AS, You could use the Del command (an alias for the Remove-Item cmdlet). Here is an example:

Dir C:\test  | Del -recurse

 

This code deletes all of the files and folders in the C:\test folder, but does not delete the C:\test folder itself. To delete the C:\test folder and all of its contents, use the command seen here:

del  C:\test –Recurse

 

   

Can I Use a Script to Connect to IPP Printers Through http://pserver/printer?

Hey, Scripting Guy! Question

Hey, Scripting Guy! I hope you can help me with this because I can’t find a straight answer elsewhere. I need to connect to IPP printers from client computers through http://pserver/printer via a script.

I can connect to IPP printers through http://pserver/printers. I choose the printer, click Connect, and it downloads the driver from the server. How can I achieve this via a script? I have tried PrintUI, but PrintUI does not pull the driver from the server; it installs the driver from the local ntprint.inf file that's on the local client computer, but I want to download the driver from the server where the IPP printers are installed. I always want to install the driver from the server in case there is a driver setting change, and by downloading the driver, it pulls the settings down to the client computer.

I can create an IPP printer with the PrintUI command by giving the port path, but the IPP printer is not the same as when I connect from the browser because it is not downloading the driver from the server and as a result I end up with a driver version mismatch. The PrintUI command downloads the driver only if you connect to the UNC path, \\server\printer. It does not recognize IPP path, http://pserver/printer. Also Con2prt does not recognize the http: path, but works great with the UNC path.

With what script language can I accomplish installing the IPP printers as if I were connecting via the browser? Any help will be appreciated.

-- NS

 

Hey, Scripting Guy! AnswerHello NS,

I have no idea. Sorry. Iif you figure it out, please let me know.

 

 

The Real Answer to the Previous Question 

Hey, Scripting Guy! Question

Hey, Scripting Guy! You told me to let you know if I figured out how to install the IPP printers. Here is what I did.

To connect the IPP printers via a browser, I created a .cmd script and entered bunch of “start URLs” for each printer. The “start URLs are the “connect” URL of each printer. I used the ScriptIt NT4.0 utility to answer the Windows Security prompts. I left the ScriptIt utility running until I was done connecting all the IPP printers. It works pretty well. ScriptIt works somewhat similar to the way VBScript sendkey does, but I am not familiar with VBScript that much. This utility works well.

Here is an example:

Run the utility: Scriptit.exe IPP-Prompt.ini

Run the batch file that contains the following:

start http://PrintServer/printers/ipp_0015.asp?eprinter=NICU7NurStation&view=q

FOR /F "tokens=1,2,3 delims=: " %%i IN ('time /t') DO waitfor.exe pauseFor%%i%%j%%k /t 45

 

start http://PrintServer/printers/ipp_0015.asp?eprinter=NICU6BloodGas&view=q

FOR /F "tokens=1,2,3 delims=: " %%i IN ('time /t') DO waitfor.exe pauseFor%%i%%j%%k /t 45

 

Well, this concludes another edition of Quick-Hits Friday. It also concludes another exciting week on the Script Center. Join us next week as we delve into the mysteries of…well, we will let that remain a mystery for now.

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

 

Hey, Scripting Guy! How Can I Quickly Check Stocks with Windows PowerShell?

Bookmark and Share

 

Hey, Scripting Guy! Question

Hey, Scripting Guy! I have a customized MSN home page. I really like the new layout, but it is rather inefficient to open Internet Explorer, and wait for a few minutes just to check out the latest stock prices. What I need is a Windows PowerShell script that I can use to quickly retrieve my stock information without the need to open Internet Explorer and scroll down half the page to find a current stock quote. I wrote a Windows PowerShell script that opens a page in Internet Explorer and scrapes the information, but it is only marginally faster than clicking on the Internet Explorer icon on my quick launch bar. Can you write a Windows PowerShell script to retrieve quickly a stock quote?

-- HM

Hey, Scripting Guy! AnswerHello HM,

Microsoft Scripting Guy Ed Wilson here. the cool thing about using Windows PowerShell 2.0 to connect to Web services is there are hundreds of Web services on the Internet. Some are highly specialized and rather complex, and others such as the one for retrieving stock quotes are very straightforward. By performing a Bing search for “Web service” or for “WSDL,” you can find hundreds of free and easy-to-use Web services that will do everything from resolving a ZIP code to performing currency conversions.

HM, the Get-StockQuote.ps1 script uses a Web service to retrieve the latest quote for a given stock. The complete Get-StockQuote.ps1 script is seen here.

Get-StockQuote.ps1

#Requires -Version 2.0

Function Get-StockQuote($symbol)

{

 $URI = "http://www.webservicex.net/StockQuote.asmx?WSDL"

 $stockProxy = New-WebServiceProxy -uri $URI -namespace WebServiceProxy -class stock

 $stockProxy.getQuote($symbol)

} #end Get-StockQuote

 

$quote = Get-StockQuote -symbol msft

[xml]$quote |

ForEach-Object {

 $_.stockQuotes.Stock

} #end Foreach-Object

The Get-StockQuote.ps1 script begins with the #Requires –Version 2.0 tag. This is a best practice to prevent the script from attempting to run on a computer with Windows PowerShell 1.0 installed. Because the New-WebServiceProxy cmdlet does not work on Windows PowerShell 1.0, the script would generate an error. The #Requires command is shown here:

#Requires -Version 2.0

It then uses the Function keyword to create the Get-StockQuote function. The Get-StockQuote function accepts a single parameter—the stock symbol. This is shown here:

Function Get-StockQuote($symbol)

{

The path to the StockQuote Web Service Definition Language (WSDL) is assigned to the $URI variable. The WSDL describes the Web service. As seen in the following image, the StockQuote Web service contains a method named GetQuote that accepts a stock symbol as a string:

Image of GetQuote method


The path to the StockQuote WSDL is assigned to the $URI variable as shown here:

$URI = "http://www.webservicex.net/StockQuote.asmx?WSDL"

The New-WebServiceProxy cmdlet is used to create a new Web service proxy for the StockQuote Web service. To do this, the New-WebServiceProxy cmdlet requires the path to the WSDL for the StockQuote Web service. The resulting proxy is stored in the $stockProxy variable as shown here:

$stockProxy = New-WebServiceProxy -uri $URI -namespace WebServiceProxy -class stock

The getQuote method from the StockQuote Web service is used to retrieve the stock quote information. The getQuote method requires a stock symbol as a string to return the information. This is shown here:

$stockProxy.getQuote($symbol)

} #end Get-StockQuote

The entry point to the script calls the Get-StockQuote function and passes a string value for the –symbol parameter. The returned data is stored in the $quote variable. This is seen here:

$quote = Get-StockQuote -symbol msft

The XML type accelerator ([xml]) is used to cast the data that is returned from the Get-StockQuote function into an XML document. The ForEach-Object cmdlet is used to walk through the document, and the stock property is used to return the stock quote. This code is seen here:

[xml]$quote |

ForEach-Object {

 $_.stockQuotes.Stock

}

When the Get-StockQuote.ps1 script runs, the following output is displayed:

Image of script output

 

Well, HM, as you can see, Web services are fun and can be relatively easy to use from within Windows PowerShell 2.0. Join us tomorrow as we open the virtual mailbag and address questions that require short answers. That’s right, it is time for Quick-Hits Friday. 

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

 

Hey, Scripting Guy! How Can I Use a Web Service to Find Weather for a Specific City?

Bookmark and Share

 

Hey, Scripting Guy! Question

Hey, Scripting Guy! I thought your script yesterday was pretty cool, but there is a problem with it. Retrieving the weather from a script is awesome; however, I need to know which cities are available. If I want to know the weather in Sydney, Australia (your example), I am all set. I’m sure you can see this coming: I do not live in Sydney and therefore the script is useless. How about displaying a list of available cities so that I can figure out how to use the script?

-- NR

Hey, Scripting Guy! AnswerHello NR,

I am listening to the Travelling Wilburys on my new Zune HD. It is an absolutely gorgeous day outside—deep blue cloudless skies and a temperature of 69 degrees Fahrenheit (20.5 degrees Celsius). I have opened all the windows in my scripting house, and I am reclining in my large leather scripting chair checking the scripting e-mail sent to scripter@microsoft.com on my Windows 7 scripting laptop. I have a fresh pot of scripting English Breakfast tea, lemon grass, and cinnamon stick. It is a perfect way to enjoy the mellow Charlotte, North Carolina, weather after the weeklong deluge gifted to the area by tropical storm Ida. NR, you are right about the practicality of knowing the weather in Sydney, Australia. Whereas I might dream of heading back to Australia, with the realities of the current budget crunch, I probably will not make it back to Australia before 2011. Therefore, I better see if I can check out the weather report for Charlotte, North Carolina.

NR, the Get-InternationalCities.ps1 script uses the globalweather Web service and the GetCitiesByCountry method to return a list of available cities. The complete Get-InternationalCities.ps1 script follows.

Get-InternationalCities.ps1

$country = "united states"
$URI = "http://www.webservicex.net/globalweather.asmx?wsdl"
$Proxy = New-WebServiceProxy -uri $URI -namespace WebServiceProxy
$countries = [xml]$proxy.GetCitiesByCountry($country)
$countries.NewDataSet.Table | Sort-Object -Property city |
Select-Object -Property city

The first thing the Get-InternationalCities.ps1 script does is assign a value for the $country variable. The value of the $country variable is used to retrieve the list of cities. This is shown here:

$country = "united states"

After the country has been set for the script, you need to assign the path to the globalweather Web Service Definition Language (WSDL) to the $URI variable. The WSDL is an XML document that describes the service offering. Because the WSDL string is a Uniform Resource Locator (URL), you can open it directly in Internet Explorer and look for methods and properties described by the WSDL. The following image illustrates this:

Image of methods and properties described by the WSDL


After you assign the path to the $URI variable, you can use the New-WebServiceProxy cmdlet to create a Web service proxy that allows you to connect and to retrieve information from the globalweather Web service. The returned object is a special webserviceproxy object that is created from the Web service that is specified in the $URI variable. The members of the WebServiceProxy.GlobalWeather object are shown here:

   TypeName: WebServiceProxy.GlobalWeather

Name                                 MemberType Definition                                             
----                                 ---------- ----------                                             
Disposed                             Event      System.EventHandler Disposed(System.Object, System.Ev...
GetCitiesByCountryCompleted          Event      WebServiceProxy.GetCitiesByCountryCompletedEventHandl...
GetWeatherCompleted                  Event      WebServiceProxy.GetWeatherCompletedEventHandler GetWe...
Abort                                Method     System.Void Abort()                                    
BeginGetCitiesByCountry              Method     System.IAsyncResult BeginGetCitiesByCountry(string Co...
BeginGetWeather                      Method     System.IAsyncResult BeginGetWeather(string CityName, ...
CancelAsync                          Method     System.Void CancelAsync(System.Object userState)       
CreateObjRef                         Method     System.Runtime.Remoting.ObjRef CreateObjRef(type requ...
Discover                             Method     System.Void Discover()                                 
Dispose                              Method     System.Void Dispose()                                  
EndGetCitiesByCountry                Method     string EndGetCitiesByCountry(System.IAsyncResult asyn...
EndGetWeather                        Method     string EndGetWeather(System.IAsyncResult asyncResult)  
Equals                               Method     bool Equals(System.Object obj)                         
GetCitiesByCountry                   Method     string GetCitiesByCountry(string CountryName)          
GetCitiesByCountryAsync              Method     System.Void GetCitiesByCountryAsync(string CountryNam...
GetHashCode                          Method     int GetHashCode()                                      
GetLifetimeService                   Method     System.Object GetLifetimeService()                     
GetType                              Method     type GetType()                                          
GetWeather                           Method     string GetWeather(string CityName, string CountryName) 
GetWeatherAsync                      Method     System.Void GetWeatherAsync(string CityName, string C...
InitializeLifetimeService            Method     System.Object InitializeLifetimeService()              
ToString                             Method     string ToString()                                      
AllowAutoRedirect                    Property   System.Boolean AllowAutoRedirect {get;set;}            
ClientCertificates                   Property   System.Security.Cryptography.X509Certificates.X509Cer...
ConnectionGroupName                  Property   System.String ConnectionGroupName {get;set;}           
Container                            Property   System.ComponentModel.IContainer Container {get;}      
CookieContainer                      Property   System.Net.CookieContainer CookieContainer {get;set;}  
Credentials                          Property   System.Net.ICredentials Credentials {get;set;}         
EnableDecompression                  Property   System.Boolean EnableDecompression {get;set;}          
PreAuthenticate                      Property   System.Boolean PreAuthenticate {get;set;}              
Proxy                                Property   System.Net.IWebProxy Proxy {get;set;}                  
RequestEncoding                      Property   System.Text.Encoding RequestEncoding {get;set;}        
Site                                 Property   System.ComponentModel.ISite Site {get;set;}            
SoapVersion                          Property   System.Web.Services.Protocols.SoapProtocolVersion Soa...
Timeout                              Property   System.Int32 Timeout {get;set;}                        
UnsafeAuthenticatedConnectionSharing Property   System.Boolean UnsafeAuthenticatedConnectionSharing {...
Url                                  Property   System.String Url {get;set;}                           
UseDefaultCredentials                Property   System.Boolean UseDefaultCredentials {get;set;}        
UserAgent                            Property   System.String UserAgent {get;set;}             

The name webserviceproxy comes from the value we specified for the namespace parameter with the New-WebServiceProxy cmdlet. This is seen here:

$URI = "http://www.webservicex.net/globalweather.asmx?wsdl"
$Proxy = New-WebServiceProxy -uri $URI -namespace WebServiceProxy

The properties of the new Web service proxy are displayed by printing the value stored in the $Proxy variable as shown here:

PS C:\Users\ed.NWTRADERS> $proxy


SoapVersion                          : Default
AllowAutoRedirect                    : False
CookieContainer                      :
ClientCertificates                   : {}
EnableDecompression                  : False
UserAgent                            : Mozilla/4.0 (compatible; MSIE 6.0; MS Web Services Client Protoco
                                       l 2.0.50727.4927)
Proxy                                :
UnsafeAuthenticatedConnectionSharing : False
Credentials                          :
UseDefaultCredentials                : False
ConnectionGroupName                  :
PreAuthenticate                      : False
Url                                  : http://www.webservicex.net/globalweather.asmx
RequestEncoding                      :
Timeout                              : 100000
Site                                 :
Container                            :

Once the WebServiceProxy.GlobalWeather object is created, the GetCitiesByCountry method is called. The Get-Member cmdlet can be used to provide a clue to the input required by the GetCitiesByCountry method (although it would seem rather obvious that the method wants a string that represents the country to query). This is shown here:

PS C:\Users\ed.NWTRADERS> $Proxy | Get-Member -Name GetCitiesByCountry


   TypeName: WebServiceProxy.GlobalWeather

Name               MemberType Definition                                  
----               ---------- ----------                                   
GetCitiesByCountry Method     string GetCitiesByCountry(string CountryName)

The data that is returned by the GetCitiesByCountry method is in the form of a data set with tables that represent each city/country combination. This is seen here:

PS C:\Users\ed.NWTRADERS> $Proxy.GetCitiesByCountry("mexico")
<NewDataSet>
  <Table>
    <Country>Mexico</Country>
    <City>Acapulco / G. Alvarez</City>
  </Table>
  <Table>
    <Country>Mexico</Country>
    <City>Aerop. Internacional Monterrey, N. L.</City>
  </Table>
  <Table>
    <Country>Mexico</Country>
    <City>Aguascalientes, Ags.</City>
  </Table>
  <Table>
    <Country>Mexico</Country>
    <City>Bahias De Huatulco</City>
  </Table>
  <Table>
    <Country>Mexico</Country>
    <City>Cuernavaca, Mor.</City>
  </Table>
  <Table>
    <Country>Mexico</Country>
    <City>Ciudad Del Carmen</City>
  </Table>
  <Table>
    <Country>Mexico</Country>
    <City>Culiacan, Sin.</City>
  </Table>
  <Table>
    <Country>Mexico</Country>
    <City>Chetumal, Q. Roo</City>
  </Table>
  <Table>
    <Country>Mexico</Country>
    <City>Santa Rosalia, B. C. S.</City>
  </Table>
  <Table>
    <Country>Mexico</Country>
    <City>Campeche, Camp.</City>
  </Table>
  <Table>
    <Country>Mexico</Country>
    <City>Ciudad Juarez International</City>
  </Table>
  <Table>
    <Country>Mexico</Country>
    <City>Chihuahua International Airport</City>
  </Table>
  <Table>
    <Country>Mexico</Country>
    <City>Ciudad Victoria Airport</City>
  </Table>
  <Table>
    <Country>Mexico</Country>
    <City>Cozumel Civ / Mil</City>
  </Table>
  <Table>
    <Country>Mexico</Country>
    <City>Durango Airport</City>
  </Table>
  <Table>
    <Country>Mexico</Country>
    <City>Tepic, Nay.</City>
  </Table>
  <Table>
    <Country>Mexico</Country>
    <City>Don Miguel / Guadalaj</City>
  </Table>
  <Table>
    <Country>Mexico</Country>
    <City>Guaymas International Airport</City>
  </Table>
  <Table>
    <Country>Mexico</Country>
    <City>Hermosillo, Son.</City>
  </Table>
  <Table>
    <Country>Mexico</Country>
    <City>Colima</City>
  </Table>
  <Table>
    <Country>Mexico</Country>
    <City>Saltillo, Coah.</City>
  </Table>
  <Table>
    <Country>Mexico</Country>
    <City>Los Mochis Airport</City>
  </Table>
  <Table>
    <Country>Mexico</Country>
    <City>Del Bajio / Leon</City>
  </Table>
  <Table>
    <Country>Mexico</Country>
    <City>La Paz International Airport</City>
  </Table>
  <Table>
    <Country>Mexico</Country>
    <City>Loreto, B. C. S.</City>
  </Table>
  <Table>
    <Country>Mexico</Country>
    <City>Matamoros International</City>
  </Table>
  <Table>
    <Country>Mexico</Country>
    <City>Aerop. Internacional Merida, Yuc</City>
  </Table>
  <Table>
    <Country>Mexico</Country>
    <City>Mexicali International Airport</City>
  </Table>
  <Table>
    <Country>Mexico</Country>
    <City>Morelia New</City>
  </Table>
  <Table>
    <Country>Mexico</Country>
    <City>Minatitlan</City>
  </Table>
  <Table>
    <Country>Mexico</Country>
    <City>Monclova, Coah.</City>
  </Table>
  <Table>
    <Country>Mexico</Country>
    <City>Mexico City / Licenci</City>
  </Table>
  <Table>
    <Country>Mexico</Country>
    <City>Monterrey / Gen Maria</City>
  </Table>
  <Table>
    <Country>Mexico</Country>
    <City>Mazatlan / G. Buelna</City>
  </Table>
  <Table>
    <Country>Mexico</Country>
    <City>Nuevo Laredo International</City>
  </Table>
  <Table>
    <Country>Mexico</Country>
    <City>Oaxaca / Xoxocotlan</City>
  </Table>
  <Table>
    <Country>Mexico</Country>
    <City>Puebla, Pue.</City>
  </Table>
  <Table>
    <Country>Mexico</Country>
    <City>Piedras Negras, Coah.</City>
  </Table>
  <Table>
    <Country>Mexico</Country>
    <City>Uruapan / Gen Rayon</City>
  </Table>
  <Table>
    <Country>Mexico</Country>
    <City>Puerto Vallarta / Lic</City>
  </Table>
  <Table>
    <Country>Mexico</Country>
    <City>Puerto Escondido</City>
  </Table>
  <Table>
    <Country>Mexico</Country>
    <City>Queretaro, Qro.</City>
  </Table>
  <Table>
    <Country>Mexico</Country>
    <City>Reynosa International Airport</City>
  </Table>
  <Table>
    <Country>Mexico</Country>
    <City>San Jose Del Cabo</City>
  </Table>
  <Table>
    <Country>Mexico</Country>
    <City>San Luis Potosi, S. L. P.</City>
  </Table>
  <Table>
    <Country>Mexico</Country>
    <City>Torreon, Coah.</City>
  </Table>
  <Table>
    <Country>Mexico</Country>
    <City>Tuxtla Gutierrez, Chis.</City>
  </Table>
  <Table>
    <Country>Mexico</Country>
    <City>Tijuana International Airport</City>
  </Table>
  <Table>
    <Country>Mexico</Country>
    <City>Tulancingo</City>
  </Table>
  <Table>
    <Country>Mexico</Country>
    <City>Tampico / Gen Fj Mina</City>
  </Table>
  <Table>
    <Country>Mexico</Country>
    <City>Toluca / Jose Maria</City>
  </Table>
  <Table>
    <Country>Mexico</Country>
    <City>Tapachula</City>
  </Table>
  <Table>
    <Country>Mexico</Country>
    <City>Cancun International Airport</City>
  </Table>
  <Table>
    <Country>Mexico</Country>
    <City>Villahermosa</City>
  </Table>
  <Table>
    <Country>Mexico</Country>
    <City>Gen. Heriberto Jara</City>
  </Table>
  <Table>
    <Country>Mexico</Country>
    <City>Zacatecas Airport</City>
  </Table>
  <Table>
    <Country>Mexico</Country>
    <City>Ixtapa-Zihuatanejo</City>
  </Table>
  <Table>
    <Country>Mexico</Country>
    <City>Manzanillo International</City>
  </Table>
</NewDataSet>

Because of the way the list of cities returns, it is necessary to retrieve the tables that contain the data. The tables are pipelined to the Sort-Object cmdlet where the cities are sorted in alphabetical order. The cities are then selected by using the Select-Object cmdlet. This is shown here:

$countries = [xml]$proxy.GetCitiesByCountry($country)

$countries.NewDataSet.Table | Sort-Object -Property city |

Select-Object -Property city

When the Get-InternationalCities.ps1 script runs inside the Windows PowerShell ISE, the following results are seen:

Image of results of running the script

 

Well, NR, as you can see, XML provides a great way to organize data and to easily retrieve properties of objects. Join us tomorrow as XML Week continues.

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

 

Hey, Scripting Guy! How Can I Use Web Services?

Bookmark and Share

 

Hey, Scripting Guy! Question

Hey, Scripting Guy! I remember reading about things called “Web services” a long time ago. It seems that I do not hear much about these anymore. Do they really exist, and if so what are they good for?

-- GK

Hey, Scripting Guy! AnswerHello GK,

Microsoft Scripting Guy Ed Wilson here. We have had to batten down the hatches here in Charlotte, North Carolina, as the tropical storm formerly known as Hurricane Ida makes a guest appearance in our neighborhood. The 50-foot tall southern pines that inhabit my yard are doing their version of the hula, and the handmade specially tuned wind chime on our front porch is playing a ghostly tune. The sky is dark and the wind whips down the street like a freight train crossing the great planes on a mission of sinister importance. Strangely, eerily perhaps, it seems there is no noise—and then it hits—all at once a great cacophony of rushing wind, overturned lawn furniture banging and clanging as it rolls across yards, and all the time the 4/4 staccato of the wind chime marks the beat of an unseen conductor.

GK, because we often receive extreme weather during hurricane season, I am extremely interested in the weather. However, I am even more interested in writing scripts. It would be awesome if I could combine both activities. What one needs is an easy-to-use way to query for current weather information. I know of dozens of Web sites I can go to (including the newly redesigned MSN page) where I can find current weather information. I can, of course, open the Web page in a script and display it, but I prefer a quick and easy way to obtain only the weather information, and to bypass the browser completely. To do this, I need to use a Web service, which returns information in an optimized manner. The data returns in XML format, and an application or script easily parses the information. This requires a little understanding of XML. The complete Get-InternationalWeather.ps1 script shown here illustrates the technique.

Get-InternationalWeather.ps1

#Requires -version 2.0

Function Get-Weather
{
 Param(
  [Parameter(Mandatory=$true)]
  [string]$city,
  [Parameter(Mandatory=$true)]
  [string]$country
 )#end param
 $URI = "http://www.webservicex.net/globalweather.asmx?wsdl"
 $Proxy = New-WebServiceProxy -uri $URI -namespace WebServiceProxy
 $Proxy.GetWeather($city,$country)
} #end Get-Weather

[xml]$xml = Get-Weather -city "sydney airport" -country "Australia"
$xml.CurrentWeather

The Get-InternationalWeather.ps1 script begins by declaring a function named Get-Weather. The Get-Weather function accepts two input parameters, city and country. Both parameters are strings, and both are mandatory. The [Parameter(Mandatory=$true)] tag is new for Windows PowerShell 2.0 and is used to force a parameter to be present when the script runs. If the script runs without values for either the country or the city parameter, the following error appears:

Image of error that appears when script is run without either the country or city parameter

The Get-Weather function parameter section is shown here:

Function Get-Weather
{
 Param(
  [Parameter(Mandatory=$true)]
  [string]$city,
  [Parameter(Mandatory=$true)]
  [string]$country
 )#end param

The Uniform Resource Identifier (URI) string points to the Web Services Description Language (WSDL) page. The WSDL describes the Web service offerings. Each service is a collection of network end points. The WSDL uses XML to define the query method. In the WSDL seen here, the GetWeather method takes two inputs: CityName and CountryName, both of which are strings:

  <?xml version="1.0" encoding="utf-8" ?>
- <wsdl:definitions xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:tm="http://microsoft.com/wsdl/mime/textMatching/" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/" xmlns:mime="http://schemas.xmlsoap.org/wsdl/mime/" xmlns:tns="http://www.webserviceX.NET" xmlns:s="http://www.w3.org/2001/XMLSchema" xmlns:soap12="http://schemas.xmlsoap.org/wsdl/soap12/" xmlns:http="http://schemas.xmlsoap.org/wsdl/http/" targetNamespace="http://www.webserviceX.NET" xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/">
- <wsdl:types>
- <s:schema elementFormDefault="qualified" targetNamespace="http://www.webserviceX.NET">
- <s:element name="GetWeather">
- <s:complexType>
- <s:sequence>
  <s:element minOccurs="0" maxOccurs="1" name="CityName" type="s:string" />
  <s:element minOccurs="0" maxOccurs="1" name="CountryName" type="s:string" />
  </s:sequence>
  </s:complexType>
  </s:element>

The $URI variable holds the path to the globalweather WSDL. The string assignment listing follows:

$URI = http://www.webservicex.net/globalweather.asmx?wsdl

The key feature of the Get-InternationalWeather.ps1 script uses the New-WebServiceProxy Windows PowerShell cmdlet to make the connection to the globalweather Web service. After the connection to the Web service is established, the resulting connection is stored in the $proxy variable as seen here:

$Proxy = New-WebServiceProxy -uri $URI -namespace WebServiceProxy

It is finally time to retrieve the weather. To do this, call the GetWeather method from the globalweather Web service. The GetWeather method call receives both a city and a country as seen here:

$Proxy.GetWeather($city,$country)
} #end Get-Weather

When calling the GetWeather method, keep in mind that the returned data is XML. The raw XML returned by the GetWeather method is shown here:

<?xml version="1.0" encoding="utf-16"?>
<CurrentWeather>
  <Location>Sydney Airport, Australia (YSSY) 33-57S 151-11E 3M</Location>
  <Time>Nov 12, 2009 - 01:00 PM EST / 2009.11.12 1800 UTC</Time>
  <Wind> from the S (180 degrees) at 22 MPH (19 KT):0</Wind>
  <Visibility> greater than 7 mile(s):0</Visibility>
  <SkyConditions> partly cloudy</SkyConditions>
  <Temperature> 68 F (20 C)</Temperature>
  <DewPoint> 64 F (18 C)</DewPoint>
  <RelativeHumidity> 88%</RelativeHumidity>
  <Pressure> 30.03 in. Hg (1017 hPa)</Pressure>
  <Status>Success</Status>
</CurrentWeather>

[xml]$xml = Get-Weather -city "sydney airport" -country "Australia"

By exploring the contents of the $xml variable, you can see there are two properties that are reported. The first is XML and the second property is CurrentWeather. This is seen here:

PS C:\Users\ed.NWTRADERS> $xml | get-member -MemberType property


   TypeName: System.Xml.XmlDocument

Name           MemberType Definition                                
----           ---------- ----------                                
CurrentWeather Property   System.Xml.XmlElement CurrentWeather {get;}
xml            Property   System.String xml {get;set;}     

The CurrentWeather property returns a CurrentWeather object. This object contains a number of properties that are seen here:

PS C:\Users\ed.NWTRADERS> $xml.CurrentWeather | Get-Member -MemberType property


   TypeName: System.Xml.XmlElement

Name             MemberType Definition                              
----             ---------- ----------                              
DewPoint         Property   System.String DewPoint {get;set;}       
Location         Property   System.String Location {get;set;}        
Pressure         Property   System.String Pressure {get;set;}       
RelativeHumidity Property   System.String RelativeHumidity {get;set;}
SkyConditions    Property   System.String SkyConditions {get;set;}  
Status           Property   System.String Status {get;set;}         
Temperature      Property   System.String Temperature {get;set;}    
Time             Property   System.String Time {get;set;}           
Visibility       Property   System.String Visibility {get;set;}     
Wind             Property   System.String Wind {get;set;}           

Because each of the weather elements is available as an individual property, it is possible to access specific values such as the temperature. This is seen here:

PS C:\Users\ed.NWTRADERS> $xml.CurrentWeather.Temperature
 66 F (19 C)

For the Get-InternationalWeather.ps1 script, the complete weather information is shown. To do this, the CurrentWeather property is queried as seen here:

$xml.CurrentWeather

The result of running the Get-InternationalWeather.ps1 script is shown here:

Image of result of running script

 

Well, GK, as you can see, Web services are alive and well. In fact, they have taken on new importance because of their ease of use from within Windows PowerShell. Join us tomorrow as XML Week continues.

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

 

Hey, Scripting Guy! Is There an Easier Way to Work with XML Files?

Bookmark and Share

 

Hey, Scripting Guy! Question

Hey, Scripting Guy! I have a file that I need to parse. It is an XML file, and it is very ugly. Who in the world ever thought that an XML file would be better than a plain CSV file? Unfortunately, Microsoft seems to be enamored with XML, and I cannot get away from it. Surely, there must be a better way to work with XML than to go cross-eyed looking at all those slashes and angle brackets! Can you throw a fellow a bone?

-- TS

Hey, Scripting Guy! AnswerHello TS,

Microsoft Scripting Guy Ed Wilson here. It is Friday in Charlotte, North Carolina, in the United States. There was frost on the lawn this morning, and I was tempted to make a mug of hot chocolate and forego my usual pot of English Breakfast tea, but I prefer real hot chocolate (not the powdered stuff) and I did not feel like fooling with the double boiler on a cold November morning. (I use milk, cream, whole vanilla beans, cinnamon sticks, almonds, hazelnuts, honey, and chunks of Mexican chocolate when I make hot chocolate so it is a big production.) So I ate my omelet while sipping my hot tea and checking my e-mail on my Windows Mobile 6.5 smart phone, and then headed up to my office primed and ready for a great day. It is after all no-meetings Friday.

TS, I am sincerely sorry that XML files upset you (my feelings were similar when I first saw XML). If you were here, I would fix you a cup of my special “Scripting Guy Hot Chocolate.” We would sit down, and I would show you that with Windows PowerShell 2.0, XML files need not be a hassle. Because you are not here, I suggest you make yourself some hot chocolate, and open up Windows PowerShell 2.0, and we’ll work through this together.

One thing that makes working with XML documents easier is having the right tool. If you double-click the XML file, the default association is Internet Explorer. This view lets you open and close nodes by clicking the minus signs on the left side of the code, as seen here:

Image of viewing the XML file in Internet Explorer


Using Internet Explorer to view an XML file is better than using Notepad. The color highlighting makes it easier to find nodes, and you can hide nodes you are not interested in seeing by clicking the minus (-) symbol to the left of the node. There is a better tool to use; it is the XML Notepad. The XML Notepad is freely downloadable from the Microsoft Download Center. Using the XML Notepad, you can easily figure out the structure of your XML file and find the names of the various nodes. This is shown here:

Image of viewing the same file with XML Notepad


The books.xml file that is loaded into XML Notepad, seen in the previous image, is included in various software development kits (SDKs), but you do not need to download and install an entire SDK just to gain access to this rather small XML file. You can easily create it by copying it from MSDN, pasting it into Notepad, and saving it as books.xml (You can use normal Notepad or XML Notepad to do this, whichever one you prefer to use.) It is useful to have a well-formed XML file upon which to practice your techniques because if you run into problems, you will at least know the XML file is working properly.

The easiest way to read an XML file is to use the Select-XML cmdlet. You can use the –path parameter to point to an XML file, and the –Xpath parameter to provide the query. The command and associated results are shown here:

PS C:\> Select-Xml -Path C:\fso\books.xml -XPath "//title"

Node                                    Path                                    Pattern
----                                    ----                                    -------
title                                   C:\fso\books.xml                        //title
title                                   C:\fso\books.xml                        //title
title                                   C:\fso\books.xml                        //title
title                                   C:\fso\books.xml                        //title
title                                   C:\fso\books.xml                        //title
title                                   C:\fso\books.xml                        //title
title                                   C:\fso\books.xml                        //title
title                                   C:\fso\books.xml                        //title
title                                   C:\fso\books.xml                        //title
title                                   C:\fso\books.xml                        //title
title                                   C:\fso\books.xml                        //title
title                                   C:\fso\books.xml                        //title

One of the problems with the output above is that it is useless. If you examine the output, it lists title, the file name, and the search criteria; however, it provides no detail about the title, which is the information we were really interested in obtaining in the first place. To obtain the actual title of the book, you can pipe the results to the Select-Object cmdlet. However, before doing that, you need to know which kind of object you are working with, so that you will know which properties are available to select. To do this, you use the Get-Member cmdlet and choose the property MemberType. The Get-Member command and the resulting properties of the SelectXMLInfo object are in this code list:

PS C:\> Select-Xml -Path C:\fso\books.xml -XPath "//title" | Get-Member -MemberType property

   TypeName: Microsoft.PowerShell.Commands.SelectXmlInfo

Name    MemberType Definition
----    ---------- ----------
Node    Property   System.Xml.XmlNode Node {get;set;}
Path    Property   System.String Path {get;set;}
Pattern Property   System.String Pattern {get;set;}

The SelectXMLInfo object only exposes three properties. From our earlier output, TS, you know that the path refers to the path to the XML file, and the pattern refers to the XPath query; therefore, the node must be the title information. You can use the Select-Object cmdlet to expand the node property. The revised Windows PowerShell command is contained in the code here:

PS C:\> Select-Xml -Path C:\fso\books.xml -XPath "//title" | Select-Object -ExpandProperty node

#text
-----
XML Developer's Guide
Midnight Rain
Maeve Ascendant
Oberon's Legacy
The Sundered Grail
Lover Birds
Splish Splash
Creepy Crawlies
Paradox Lost
Microsoft .NET: The Programming Bible
MSXML3: A Comprehensive Guide
Visual Studio 7: A Comprehensive Guide

TS, you might be wondering what the XPath parameter is all about. XPath is a way of querying XML files. There are numerous books written about XPath, and hundreds of pages on MSDN, but a good starting point is to take a look a look at the syntax documentation on MSDN. From this page, you can see that the //title query I used told the Select-Xml cmdlet to look for the use of title anywhere within the XML document. If you want to return the name of the root node, you can use the “/*” syntax seen in the listing here:

PS C:\> Select-Xml -Path C:\fso\books.xml -XPath "/*"

Node                                    Path                                    Pattern
----                                    ----                                    -------
catalog                                 C:\fso\books.xml                        /*

To find a book with the id attribute that is equal to bk101, you use the // to find all of the books, and then use a set of square brackets to indicate you are looking for an attribute that is equal to a certain value. This command is in the code that follows.

Keep in mind that XML values and attribute names are case sensitive. For example, book must begin with a small “b”, and “id” is lower case, as in “bk101.”

PS C:\> Select-Xml -Path C:\fso\books.xml -XPath "//book[@id = 'bk101']"

Node                                    Path                                    Pattern
----                                    ----                                    -------
book                                    C:\fso\books.xml                        //book[@id = 'bk101']


PS C:\>

Well, TS, as you can see, working with XML files does not need to be complicated. Join us tomorrow as XML Week continues.

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 them on the Official Scripting Guys Forum. See you tomorrow. Until then, peace.
 

Ed Wilson and Craig Liebendorfer, Scripting Guys

 

Hey, Scripting Guy! Quick-Hits Friday: The Scripting Guys Respond to a Bunch of Questions (11/13/09)

Bookmark and Share

In this post:

 

Dealing with Quarantined File Types and Exchange Server

Hey, Scripting Guy! Question

Hey, Scripting Guy! I have spent most of this week Googling, experimenting, and fighting with Exchange 2007. The problem I have been working on was caused by one of my colleagues who came to me and asked if I can make it possible for him to receive an .exe attachment that one of our customers wants to send to him. Being pretty sure that I could, I logged on to the Exchange server just to allow the attachment to be sent, and to my surprise, I discovered that Exchange has deleted it. I can’t get Exchange 2007 to quarantine .exe files. The only thing it can do is delete all of the.exe attachments, or let them all into my network (a change that obviously sets my security level below 0). Is there a way of setting Exchange 2007 attachment filtering rules, so I can decide which .exe files can be received?

-- MK

Hey, Scripting Guy! AnswerHello MK,

Microsoft Scripting Guy Ed Wilson here. This is an Exchange Server question, and I am no longer an Exchange Server guru. The last version I was an expert on was Exchange 5.5. At that time, there was no such thing as file quarantine. These days, what I do when I need to receive an executable file from someone is have them rename the .exe file to with a .ex_ extension and email it to me. Most of the time this simple change will get the file through. At times, however, it will not (it seems to depend on certain antivirus products that use advanced file-matching algorithms). For those occasions when I am unable to receive a renamed executable via e-mail, I use SkyDrive from Windows Live. It is free and allows up to 25 GB of storage. It works most excellently for sharing files. My SkyDrive is seen here:

Image of Ed's SkyDrive



 

Scanning Multiple Remote Computers and Creating a Resultant CSV File

Hey, Scripting Guy! Question

Hey, Scripting Guy! How can I scan a list of multiple remote PCs (running Windows XP) on the same domain for particular installation files on their hard drives and submit the positive results to a .csv file? I came across one of your scripts (listed below) but this is only for one PC at a time and all of the results are shown in the shell.

ScanPCForSpecificFile.vbs

strComputer = "."
Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2")
Set colFiles = objWMIService. _
    ExecQuery("Select * From CIM_DataFile Where FileName = 'VisioMUI' and Extension = 'msi'")
If colFiles.Count = 0 Then
    Wscript.Echo "The file does not exist on the remote computer."
Else
    Wscript.Echo "The file exists on the remote computer."
End If

-- JD

 

Hey, Scripting Guy! AnswerHello JD,

Believe it or not, the script is pretty well set up to run on multiple computers. What you will need to do is populate the value of the strComputer variable. The ScanPcForSpecificFile.vbs uses a period, which is WMI shorthand for “run on the local computer.”

There are several ways that you can get values for the strComputer variable. You can read a text file (really easy to do), you can ping a range of IP addresses (really cool to do), you can read a database (a bit of work but groovy none the less), or you can even query Active Directory (similar to querying a database) to get your computer names.

After you have the computer names, modify the wscript.echo commands to write to a text file, Word document, Excel spreadsheet, Access or SQL database, or whatever other output you like. Because of the work involved in opening and reading your data source, it often makes sense to stay with the same type of output for logging your results. For example, if you open a text file and read it for your computer names, use a text file to log your results.

You can find numerous examples of working with text files in the Hey, Scripting Guy! archive and in the TechNet Script Center Gallery. The same is true for working with Office Word, Excel, databases, and Active Directory.

Here is an example I wrote to illustrate what needs to be done.

ReadFromTextFileWriteToTextFile.vbs

'==========================================================================
'
'
' NAME: ReadFromTextFileWriteToTextFile.vbs
'
' AUTHOR: ed wilson , Microsoft
' DATE  : 10/20/2009
'
' COMMENT: Demo read from a text file, and writing information to a different
' text file. If the other text file does not exist, it will be created.
'==========================================================================
strServers = "c:\fso\servers.txt"
strOutFile = "c:\fso\output.txt"
Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objTextFile = objFSO.OpenTextFile(strServers)
Set objOutFile = objFSO.OpenTextFile(strOutFile,8,True)
Do until  objTextFile.AtEndOfStream
    strComputer = objTextFile.Readline
    'WScript.Echo strComputer

   objOutFile.WriteLine strComputer
Loop
objTextFile.Close
objOutFile.Close

Here is your script pasted into the above script:
strServers = "c:\fso\servers.txt"
strOutFile = "c:\fso\output.txt"
Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objTextFile = objFSO.OpenTextFile(strServers)
Set objOutFile = objFSO.OpenTextFile(strOutFile,8,True)
Do until  objTextFile.AtEndOfStream
strComputer = objTextFile.Readline
Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2")
Set colFiles = objWMIService. _
    ExecQuery("Select * From CIM_DataFile Where FileName = 'VisioMUI' and Extension = 'msi'")
If colFiles.Count = 0 Then
    Wscript.Echo "The file does not exist on the remote computer."
Else
   objOutFile.WriteLine  "The file exists on the: " & strComputer
End If
Loop
objTextFile.Close
objOutFile.Close

I have not tested the script, but I believe it should work.   


How Can I Check for the Latest Daylight Saving Time Update?

Hey, Scripting Guy! Question

Hey, Scripting Guy! I’m hoping that you could help me here. Please see Bug ID 422388 on the Microsoft Connect Feedback Web page. This is a bug I ran into while scanning my domain during the 2009 DST spring forward. 

The problem is with recently patched Windows XP and Windows Server 2003 machines; WMI appears to follow the pre-2007 DST rules. The case on the Connect forum says that it has been fixed and closed, but it makes no mention of what has been fixed and how the end user might be able to remedy this on their own machines.

I was hoping that you might know internal Microsoft people who can shed some light about whether any operating system hotfixes are being issued to fix this problem.

You obviously see the seriousness of this issue when the instrumentation interface that you are using is giving incorrect replies.

Thank you for your attention.

-- WD


Hey, Scripting Guy! AnswerHello WD,

This is a Windows PowerShell script I wrote to check for the latest daylight saving time update. Daylight saving time updates are now cumulative. As far as I know, the latest one is 955839.

CheckDSTPatch.ps1

# CheckDSTPatch.ps1
# ed wilson, msft
# checks for patch 955839 Dec. 2008 TZ patch update
http://bit.ly/yukqp $computer = $env:computerName Get-WmiObject -Class Win32_QuickFixEngineering -computer $computer | Where-Object { $_.hotFixID -match '955839' }


 

Troubleshooting the Use of WSMan with Windows PowerShell

Hey, Scripting Guy! Question

Hey, Scripting Guy! I am having an issue with Windows PowerShell. I hope someone can help. I could not find any relevant documentation or thread referring to same. While running a script using WSMan in Windows PowerShell, we created the script to get the data for CIM_Sensor class. The script ran successfully in a C# project, but when we ran the code in a Windows PowerShell script, it threw an exception. The exception that was thrown is seen here:

"Get-WSManInstance: The response that the WS-Management service computed exceeds the internal limit for envelope size."

Is this a Windows PowerShell issue because we were able to retrieve data successfully using C# and Perl? Any pointers would be highly appreciated.

-- SK
 

Hey, Scripting Guy! AnswerHello SK,

You can configure the envelope size limit (and a bunch of other things) in the WSMan provider. To do this, you will need to start Windows PowerShell as an administrator. Use the Set-Location cmdlet to change to the WSMan drive. You can then Set-Location to the localhost container. Use the Get-Item cmdlet to retrieve your maxenvelopesizekb property. Use the Set-Item cmdlet to configure a new size. This is seen here:

PS C:\Windows\system32> Set-Location wsman:
PS WSMan:\> Set-Location localhost

Start WinRM Service
WinRM service is not started currently. Running this command will start the WinRM service.

Do you want to continue?
[Y] Yes  [N] No  [S] Suspend  [?] Help (default is "Y"): y
WARNING: Waiting for service 'Windows Remote Management (WS-Management) (WinRM)' to finish starting...
PS WSMan:\localhost> Get-Item .\MaxEnvelopeSizekb


   WSManConfig: Microsoft.WSMan.Management\WSMan::localhost

Name                      Value                                                             Type
----                      -----                                                             ----
MaxEnvelopeSizekb         150                                                               System.String

 

For more information on this, refer to the WSMan provider help by using the following command: Get-Help wsman.

 

Well, this concludes another edition of Quick-Hits Friday. It also concludes another exciting week on the Script Center. Join us next week as we delve into the mysteries of…well, we will let that remain a mystery for now.

If you want to know exactly what we will be looking at on Monday, follow us on Twitter or Facebook. If you have any questions, send e-mail to us at scripter@microsoft.com or post them on the Official Scripting Guys Forum. See you on Monday. Until then, have an awesome weekend.

 

Ed Wilson and Craig Liebendorfer, Scripting Guys

 

Hey, Scripting Guy! How Can I Create Variables in Windows PowerShell?

Bookmark and Share

 

Hey, Scripting Guy! Question

Hey, Scripting Guy! In VBScript, it was easy to create a variable; all I had to do was use the DIM statement. I did not need to specify a type or a value or anything. I just used DIM and the variable would be created. How do I create variables in Windows PowerShell? Please tell me you haven’t made it harder.

-- TL

Hey, Scripting Guy! Answer Hello TL,

Microsoft Scripting Guy Ed Wilson here. Today has been a very eventful day for me. I have had four meetings—all of which were essential. One meeting in particular, the meeting with Karen who helps us manage the Script Center Web site was especially productive. We are talking about modifying the Script Center home page to make it more user-friendly (don’t worry, it will not be a radical change like our recent migration). We all had some good ideas about what needs to be done, but I think Karen wins the best suggestion award for her idea to…well, you will just have to wait and see. I can tell you that we are really psyched!

Over the course of the last five months, we have gone from a great Web site with no bells or whistles to a great Web site with so much more available to us in terms of making the site easier for you to use. However, we won’t ever use any technical Web gewgaws just for the sake of using gewgaws. I am listening to White Snake (the early stuff, not the later commercial dross) on my Zune, and sipping a cup of Rooibos tea and nibbling on a small piece of 85 percent cacao from France while I review the e-mail sent to scripter@microsoft.com.

Note: Portions of today's Hey, Scripting Guy! post are excerpted from the Microsoft Press book, Windows PowerShell 2.0 Best Practices by Ed Wilson. The book is available for pre-order.

TL, as with creating aliases (which was discussed in Monday’s Hey Scripting Guy! post), there are several different ways to create a variable and to assign a value to it. You can use the New-Item cmdlet on the variable drive as seen here:

New-Item -Name temp -Value $env:TEMP -Path variable:

You could also use the Set-Item cmdlet to do this. The advantage to the Set-Item cmdlet is that it does not generate an error if the variable already exists. Here is an example of using the Set-item cmdlet to create a variable. One thing to keep in mind is that the Set-item cmdlet does not have a parameter named –name:

Set-Item -Value $env:TEMP -Path variable:\temp

Neither the New-Item nor the Set-Item cmdlet has the ability to specify the option or the description parameter. With variables this is an important distinction because you cannot create a constant and you cannot create a read-only variable without using either Set-Variable or New-Variable. If a variable already exists and you use the Set-Variable cmdlet, the value of the variable will be overwritten if it has not been marked read-only or constant. If it is read-only, you can still modify the value of it by specifying the Force parameter. If the variable is marked as a constant, the only way to modify the value of the variable is to close the Windows PowerShell console and start over with a new value.

You can also create a variable and assign a value to it at the same time. This technique if often used when the value to be stored in the variable is the result of a calculation or concatenation. In this example, we decide to create a variable named $wuLog to store the path to the Windows Update log, which is stored in a rather obscure location in the user’s local Application Data folder. Though there is an environmental variable for local application data, the path continues to go on for a couple of levels before terminating in the WindowsUpdate.log file. As a best practice when building file paths, you should use the path cmdlets such as Join-Path to avoid concatenation errors. By using the environmental localappdata variable and Join-Path with the –resolve switched parameter, we also have a formula that will store the path to the WindowsUpdate log file on any user’s computer. This is exactly the kind of variable you would want to create and store in a user’s Windows PowerShell profile. This command is seen here.

PS C:\> $wuLog = Join-Path -Path $env:LOCALAPPDATA `
-ChildPath microsoft\windows\windowsupdate.log -Resolve
PS C:\> $wuLog
C:\Users\edwils.NORTHAMERICA\AppData\Local\microsoft\windows\windowsupdate.log

When using a variable to hold a computed value, you are not limited to using a direct value assignment. You can use the New-Variable cmdlet to perform exactly the same task:

PS C:\> New-Variable -Name wulog -Value (Join-Path -Path $env:LOCALAPPDATA `
-ChildPath microsoft\windows\windowsupdate.log -Resolve)

When using the New-Variable cmdlet to create a variable that will hold a computed result, you will often need to use parentheses to force the value to be created before attempting to assign it to the Value parameter. You may see an error about some missing or invalid parameter. This is because when the New-Variable cmdlet sees a parameter outside of a set of parentheses, it attempts to locate that parameter. An example of such an error is seen in the following image.

Image of missing parameter error

 

You can also use some of the automatic variables when creating variables for your profile. A large number of applications place files in the user’s documents directory. Though this is convenient for applications and for users who access the documents location via a link on the Start menu, it is nearly impossible to locate via the command line. An additional problem is that the Documents folder may not be displayed on the Start menu of the user. As seen in the following image, the folder may have been deselected:

Image of deselected Documents folder

 

To facilitate ease of access to the user’s Documents folder, you may decide to create a variable that could easily be used to refer to the path. This is another good opportunity to use the Join-Path cmdlet to aid in building the location to the Documents folder. There is an automatic variable that already points to the user's home directory. The home directory on my Windows Vista laptop points to the %username% folder under the user’s folder. This is seen here:

PS C:\> $home
C:\Users\edwils.NORTHAMERICA

Because the Documents folder is located under this home directory, as seen in the following image, we can add to this location and build the path to the documents directory:

Image of Documents folder under home directory


By using the New-Variable cmdlet, you can specify the Value parameter, which is contained in a set of parentheses because of the need to resolve the value of the Join-Path command before assigning it to the docs variable. The variable will be read-only, which allows you to modify the variable if needed, but it will also be protected from accidental deletion or modification. The Description parameter provides an easy way to keep track of all the custom variables. This line of code is shown here:

New-Variable -Name docs -Value (Join-Path -Path $home -ChildPath documents) `
-Option readonly -Description "MrEd Variable"

When I was first learning Windows PowerShell, I was often frustrated when I attempted to use the New-Variable, Set-Variable, and Remove-Variable cmdlets because a variable is prefixed with the dollar sign when working at the command line, but the name parameter does not use the dollar sign as part of the name of the variable.

Another way to obtain the path to the Documents folder is to use the WshShell object from VBScript fame. Because Windows PowerShell provides easy access to Component Object Model (COM) objects, there is no reason to avoid these devices. One way to use this COM object is to create and use the object all in the same line. This is seen here:

$docs = (New-Object -ComObject Wscript.Shell).specialFolders.item("MyDocuments")

From a best practice standpoint, there are at least two problems with the above syntax. The most obvious issue is that it is not very readable. While this is rather common usage, it would better to split the command into two lines of code as seen here:

$wshShell = New-Object -ComObject Wscript.Shell
$docs = $wshShell.SpecialFolders.Item("MyDocuments")

 

Well, TL, this should get you started working with variables in Windows PowerShell. Join us tomorrow as we open the virtual mail bag and answer questions that require only shorter answers.

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 them on the Official Scripting Guys Forum. See you tomorrow. Until then, keep on scripting!

 

Ed Wilson and Craig Liebendorfer, Scripting Guys

 

Hey, Scripting Guy! Can I Change on the Fly the Way Windows PowerShell Functions Work?

Bookmark and Share

 

Hey, Scripting Guy! Question

Hey, Scripting Guy! I have started using functions in Windows PowerShell to encapsulate complex commands, and it works pretty well. However, I would like to be able to change the way the functions work on the fly. By this I do not mean that I will throw the computer across the room and see how the function works, but rather I want to modify what the function does based upon information I give it. Can I do this?

-- LK

Hey, Scripting Guy! AnswerHello LK,

Microsoft Scripting Guy Ed Wilson here, I can sympathize with the desire to encourage one’s computer to take flight. Keep in mind that it is generally not the computer’s fault—it is the software. In the message/messenger dynamic, hardware is almost always the messenger hoping not to be shot. The good thing about Windows PowerShell is that it is highly configurable. If you do not like the way a particular cmdlet works, you can create a function and modify it. Let us look at how to accomplish this task.

Note: Portions of today's Hey, Scripting Guy! article are excerpted from the Microsoft Press book, Windows PowerShell 2.0 Best Practices by Ed Wilson. The book is available for pre-order.

When using a function it is quite common to want to accept two or more parameters for input. This adds flexibility and usefulness to the function. The first method of passing parameters in Windows PowerShell is to use the $args automatic variable as seen in yesterday’s Hey, Scripting Guy! post.

Another way to pass parameters is to use named parameters. When using named parameters with a script, the Param statement precedes them. To use a named parameter within a function, you do not need to use the Param statement. You simply supply variables in each position for which you wish to have a parameter. The name of the variable becomes the name of the parameter. There are a few tricks to keep in mind when using both methods of passing multiple parameters. To that end, let us first examine the $args variable in a bit more detail.


Multiple parameters with $args

One way to pass two parameters is to use the $args automatic variable, and when passing two values to the function, you index into the array to retrieve a specific value. In the Get-WmiClass function, two values are passed when calling the function. The first value is used to hold the WMI namespace to search for WMI class names, and the second value is the type of WMI class for which to search. This function is useful for locating WMI classes. Use of the Get-WmiClass function is seen here:

Image of using the Get-WmiClass function


The Get-WmiClass function begins by retrieving two values from the $args variable. The $args variable is an automatic variable and is populated with whatever is fed to the function. The element from the first position is stored in the $ns variable, and the second element is kept in the $class variable. This is seen here:

$ns = $args[0]
$class = $args[1]

The Get-WmiObject cmdlet has a list switched parameter that will produce a list of all the WMI classes in the namespace. The namespace used is the one specified in the first position of the command used to call the function. The resulting list of all the WMI classes in the particular namespace is shunted to the pipeline. This line of code is seen here:

Get-WmiObject -List -Namespace $ns |

To make the list of WMI classes useful, the Where-Object cmdlet is used to filter out the unwanted WMI class names. Inside the code block for the Where-Object cmdlet, the automatic variable $_ is used to refer to the current item on the pipeline. The –match operator allows you to use a regular expression if desired to filter out the list of WMI class names. This line of code is shown here:

Where-Object { $_.name -match $class }

The complete Get-WmiClass.ps1 script is shown here.

Get-WmiClass.ps1

Function Get-WmiClass()
{
 #.Help Get-WmiClass "root\cimv2" "Processor"
 $ns = $args[0]
 $class = $args[1]
 Get-WmiObject -List -Namespace $ns |
 Where-Object { $_.name -match $class }
} #end Get-WmiClass


Multiple named parameters

When you have more than two parameters to supply to a function, it can get really confusing to keep track of both the position and the meaning of the parameters. In addition, when using named parameters, you can apply type constraints to prevent basic types of errors that could occur when supplying values from the command line.

In Get-WmiClass2.ps1, the Get-WmiClass function has been rewritten to take advantage of command-line arguments. The primary change was moving the $ns and the $class variables inside the parentheses following the name of the function. In addition, because both the namespace and the class names should be strings, we use the [string] type constraint to prevent the inadvertent entry of an illegal value such as an integer. Because the revised function is using named parameters, the two lines that parsed the $args variable have also been removed. The Get-WmiClass2.ps1 script file is therefore shorter than the Get-WmiClass.ps1 script, and it has more capability. The first line of the Get-WmiClass function is seen here:

Function Get-WmiClass([string]$ns, [string]$class)

An example of value of the type constraints is seen here:

Image of type constraints

 

In the first example shown in the previous image, the Get-WmiClass function is called with the value of 5 for the –ns parameter. This violates the type constraint of [string] for the ns parameter. The resulting error is “Invalid parameter.”

In the second example shown in the previous image, the Get-WmiClass function is called with the value of root/cimv3 for the –ns parameter. Because there is no root/cimv3 namespace in the WMI hierarchy (at least not yet), the function actually executes. The resulting error comes from WMI, which states the problem is an “Invalid namespace.” As a best practice, you should always apply type constraints to your function parameters. The rudimentary protection afforded by them easily justifies the minimal effort required to type them.

To call the Get-WmiClass function, you can use the entire parameter name, a shortened unique version of the parameter name, or no parameter at all. Examples of each way to call the function are shown in the following code. When supplying a parameter, you only need to type enough of the parameter name to ensure that it is unique. As a best practice, you should take this feature into account when naming parameters. If each parameter begins with a unique letter, users of the function can supply single-letter parameter names, and still maintain a rudimentary level of readability. As an example, in the Get-WmiClass function, if we had named the namespace namespace and the class name simply name, we would have been required to type the entire word name for the name parameter and names for the namespace. That does not shorten very well.

Get-WmiClass -ns "root\cimv2" -class "disk"
Get-WmiClass -n root\cimv2 -c disk
Get-WmiClass root\cimv2 disk

When using named parameters with functions, you do not need to include a string inside quotation marks unless it contains a comma, semicolon, or other special character that could be misinterpreted by the runtime engine. When working from the command line, I often take advantage of this technique to reduce typing. However, when working in a script, I like to include the quotation marks to improve readability and understandability of the code.

The complete Get-WmiClass2.ps1 script is seen here.

Get-WmiClass2.ps1

Function Get-WmiClass([string]$ns, [string]$class)
{
 #.Help Get-WmiClass -ns "root\cimv2" -class "Processor"
 
 Get-WmiObject -List -Namespace $ns |
 Where-Object { $_.name -match $class }
} #end Get-WmiClass

You can also create an alias for the function at the same time you define the function. Because this was for the Get-WmiClass function, we used the Get-Alias cmdlet to check for the existence of the chosen alias letter combination of gwc (selecting the first letter of each main word in the function name. You can use this command to see if the gwc alias is available:

Get-Alias -Name gwc

This is one of those times when you hope to receive an error because it means your chosen alias can be used. The error is seen in the following image:

Image of the error we were hoping to see


The completed Get-WmiClassWithAlias.ps1 script is seen here.

Get-WmiClassWithAlias.ps1

Function Get-WmiClass([string]$ns, [string]$class)
{
 #.Help Get-WmiClass -ns "root\cimv2" -class "Processor"
 
 Get-WmiObject -List -Namespace $ns |
 Where-Object { $_.name -match $class }
} #end Get-WmiClass
New-Alias -Name gwc -Value Get-WmiClass -Description "Mred Alias" `
-Option readonly,allscope

 

Well, LK, this should get you started with passing parameters to functions. Join us tomorrow as we continue examining ways to customize our Windows PowerShell environment.

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 them on the Official Scripting Guys Forum. See you tomorrow. Until then, keep on scripting!

 

Ed Wilson and Craig Liebendorfer, Scripting Guys

 

New! On-demand translation of our blog!


You may have noticed something new about the Hey, Scripting Guy! Blog today (very top of the page):

Image of translation widget 

If you would prefer to read this blog in another language, you can now do that--on demand! Available languages are:

  • Arabic
  • Chinese Simplified
  • Chinese Traditional
  • Czech
  • Danish
  • Dutch
  • English
  • French
  • German
  • Greek
  • Hebrew
  • Italian
  • Japanese
  • Korean
  • Polish
  • Portuguese
  • Russian
  • Spanish
  • Swedish
  • Thai

Just choose the version of "Translate this page" from the drop-down list that is in the language in which you want to read a Hey, Scripting Guy! post, and within 15-30 seconds the content will be translated. The longer the content, the longer the translator will take to translate it.

If it seems the translator "isn't working," be sure to look at the green progress bar to see how much has already been translated:

 Image of translation progress

We will be working on this feature in the coming weeks. If you have feedback for us about it, please send e-mail to scripter@microsoft.com.

Ed Wilson and Craig Liebendorfer, Scripting Guys

 

Hey, Scripting Guy! How Can I Create a Custom Function?

Bookmark and Share

 

Hey, Scripting Guy! Question

Hey, Scripting Guy! I am getting tired of typing the long commands used in Windows PowerShell. I know you Scripting Guys seem to show people always typing commands inside the Windows PowerShell console, but I do not think I like to work that way. It is TOO MUCH TYPING. I would love to be able to shorten some of the commands, but I do not think that is possible.

-- DR

Hey, Scripting Guy! AnswerHello DR,

Microsoft Scripting Guy Ed Wilson here, it is a cold day in Charlotte, North Carolina, in the United States. The sky is clear, and the sun is shining, but it’s an autumn sun and is not doing that much good. I even saw some people wearing jackets outside. For the sunny south, 50 degrees Fahrenheit (10 degrees Celsius) is enough to drive the children inside and bring the big wooly mothball-smelling coats outside. I am sipping a cup of aged Earl Gray tea with a cinnamon stick and a dash of warm milk to smooth the flavor. Along with the tea, I have a slice of Fuji apple and Wisconsin sharp cheddar cheese. It is a departure from my normal afternoon snack, but the combination seems to work well with autumn. The neighborhood Halloween decorations persist in some yards of overzealous homeowners in the subdivision, but that has not stopped others from putting out giant blow-up turkeys in anticipation of Thanksgiving. Yet with all that going on outside, the stores are skipping past Thanksgiving in hopes that St. Nick will soon be there. It is a confusing time. Therefore, a confused snack.

DR, I am not confused by your request. I have felt the same way myself. What I generally do is create a custom function that does what I want, and then create an alias for the function. I then add these items to my Windows PowerShell profile. Let’s begin by looking at creating a custom function.

Note: Portions of today's Hey, Scripting Guy! post are excerpted from the Microsoft Press book, Windows PowerShell 2.0 Best Practices by Ed Wilson. The book is available for pre-order.

Functions provide a nearly endless capability of customization from within Windows PowerShell. The profile is a great place to supply some of this customization. As an example, suppose that when using the Get-Help cmdlet you prefer to see the full article, but you also know that in most cases the article is too long to fit on a single screen; therefore, you pipeline the output to the more function, which provides paging control. If you were looking for information about the Get-Process cmdlet, the command would be the one shown here:

Get-Help Get-Process -Full | more

There is nothing wrong with typing the above command except that, even when paired with tab expansion, it is more than 30 keystrokes. It does not take very long before you get tired of typing such a command. This is a perfect candidate for a function. When naming functions, it is a best practice to use the Verb-Noun naming convention because this syntax will be familiar to users of Windows PowerShell, and you can take advantage of tab expansion. As seen here, I named our function Get-MoreHelp.

Get-MoreHelp.ps1                                                                             

Function Get-MoreHelp()
{
 Get-Help $args[0] -Full |
 more
} #end Get-MoreHelp

The Get-MoreHelp function begins by using the Function keyword to declare the function. After the Function keyword, we specify the name of the function, which in this example is Get-MoreHelp. The empty parentheses are not required after the function name. The parentheses would be used to define parameters, and without any parameters, the parenthesis are not required. I generally include them as an indicator that a parameter could be specified in the position. This is seen here:

Function Get-MoreHelp()

Following the Function keyword, the function opens the code block by using an opening curly bracket. When typing the function I always open the code block with one curly bracket, and immediately on the next line type the closing curly bracket. In this way, I never forget to close a code block. As a best practice, I always include a comment that indicates the bottom curly bracket closes the function. The end comments also are a tremendous help when it comes time to troubleshoot the script because they promote readability and make it easier to understand the delimiters of the function. In addition, if you have a long function that scrolls off the screen, the end comment, with its repetition of the function name, makes it easier to create the alias for the function as well. This is seen here:

{

} #end Get-MoreHelp

The Get-MoreHelp function uses the $args automatic variable to hold the argument that was passed to the function when it is called. Because the Get-Help cmdlet does not accept an array for the name parameter, we use [0] to index into the first element of the $args array. If, as is required, there is only one item passed to the function, the item will always be element 0 of the array. The function passes the –full switched parameter to the Get-Help cmdlet. The resulting help information is passed along the pipeline via the pipe (“|”) symbol. This is shown here:

Get-Help $args[0] -full |


Overriding existing commands

Because it is possible for the Get-MoreHelp function to return more than a single screen of textual information, the function pipes the help information to the more function. Functions are first-class citizens in Windows PowerShell, and they have priority over executables and even native Windows PowerShell cmdlets. Because of this, it is easy to modify the behavior of an executable or cmdlet by creating a function with the same name as an existing one. This is illustrated by the more function. More.com is an executable that provides the ability to return information to the screen one page at a time—it has been available since the DOS days. The more function is used to modify the behavior of more.com. The content of the more function is shown here:

param([string[]]$paths)
if($paths)
{
    foreach ($file in $paths)
    {
        Get-Content $file | more.com
    }
}
else
{
    $input | more.com
}

By looking at the content of the More function, we see that there has been a useful addition to the functionality of more.com. If you supply a path to the More function, it will retrieve the content of the file and pipe the result to the more.com executable. This is shown here:

Image of supplying a path to the More function


Alias the function

When I create utility functions, I generally like to create an alias for the function. This will enable quick and easy access to the function. It is possible to create the function and the alias in the same script, but not within the function definition. The problem is that within the function definition, the function has not yet been created and therefore you cannot create an alias for a function that does not yet exist. But there is nothing wrong with creating the alias and the function in the same script. Interestingly enough, you can create the alias on a line before the function is declared or after the function is declared. Position does not matter.

Get-MoreHelpWithAlias.ps1

Function Get-MoreHelp()
{
 Get-Help $args[0] -full |
 more
} #End Get-MoreHelp
New-Alias -name gmh -value Get-MoreHelp -Option allscope


Loop the array

The $args variable returns an array; we can use this to our advantage and add the ability to pass two or more pieces of information and receive help for each topic. To do this, we use the For statement to loop through the elements of $args. The For statement uses three parameters: the beginning, the destination, and the method of travel. In this example, the variable $i is used to keep track of the position within the array. The variable $i is set equal to 0, and the –le operator (less than or equal to) is used to allow the loop to continue for the number of times that is represented by the count of the number of items in $args. As the loop progresses, the value of $i is incremented by one each time through the loop. The $i++ construction does this. This line of code is seen here:

For($i = 0 ;$i -le $args.count ; $i++)

One small change is required to the line of code that calls the Get-Help cmdlet. Instead of using $args[0], which will always retrieve the first element in the array, we change the 0 to $i. As the value of $i increases for each loop, the Get-Help cmdlet will query the next item in the array. This modified line of code is seen here:

Get-Help $args[$i] -full |

The remainder of the Get-MoreHelp function is the same as previous versions, which were discussed earlier. The complete function is seen in the Get-MoreHelp2.ps1 script.

Get-MoreHelp2.ps1

Function Get-MoreHelp
{
 # .help Get-MoreHelp Get-Command Get-Process
 For($i = 0 ;$i -le $args.count ; $i++)
 {
  Get-Help $args[$i] -full |
  more
 } #end for
} #end Get-MoreHelp
New-Alias -name gmh -value Get-MoreHelp -Option allscope

To run the Get-MoreHelp function, you can use the alias gmh and supply it one or more cmdlet names to get the help. This is seen in the following image where the function code was typed directly into the Windows PowerShell console:

Image of using the gmh alias

 

Well, DR, this should get you started on creating custom functions and aliases. Join us tomorrow as we continue examining ways to customize our Windows PowerShell environment.

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 them on the Official Scripting Guys Forum. See you tomorrow. Until then, keep on scripting!

 

Ed Wilson and Craig Liebendorfer, Scripting Guys

 

Hey, Scripting Guy! Tell Me About Aliases in Windows PowerShell

Bookmark and Share

 

Hey, Scripting Guy! Question

Hey, Scripting Guy! I have been playing around with Windows PowerShell 2.0 in Windows 7 and I think that I like it. However, it seems to require an awful lot of typing. The double command names, such as Get-Process, are somewhat helpful for remembering things, but it is quite a bit of typing. Yes, it is shorter than writing a VBScript to return the same information, but did you have to make everything so long? And by the way, while I am whining, I hate typing the hyphen. It is too hard to find on my keyboard. I do not expect you to change Windows PowerShell just for me, but surely there are others out there who feel the same way I do. If Windows PowerShell is supposed to be the all-new management tool for administrators, you could have made it easier to use.

-- RR

Hey, Scripting Guy! AnswerHello, RR.

Microsoft Scripting Guy Ed Wilson here. Well I just got off a two-hour Live Meeting with a customer. I was talking about some of the new remoting features in Windows PowerShell 2.0. It was an awesome talk, if I do say so myself. I love talking to customers, and I love working with Windows PowerShell 2.0. On those occasions when I combine those two passions, it is a great day.

RR, it seems that I need to introduce you to the concept of aliases. Consider a command such as Measure-Object that is used to count information and provide statistical information such as the minimum and maximum values of an object. Measure-Object can be a bit cumbersome to type from a command line, and given the relative frequency of its use, it becomes a good candidate for aliasing.

Note: Portions of today's Hey, Scripting Guy! post are excerpted from the Microsoft Press book, Windows PowerShell 2.0 Best Practices by Ed Wilson. The book is available for pre-order.

Verifying the existence of an alias

Before creating a new alias, it is a best practice to see if there is a suitable alias already created for the cmdlet in question. By default, Windows PowerShell ships with more than 130 predefined aliases for its 271 cmdlets. When you consider that several of the cmdlets have more than one alias defined, you can see there is great opportunity for the creation of additional aliases. The ListCmdletsWithMoreThanOneAlias.ps1 script lists all cmdlets that have more than one alias defined. This script is seen here.

ListCmdletsWithMoreThanOneAlias.ps1

Get-Alias |
Group-Object -Property definition |
Sort-Object -Property count -Descending |
Where-Object { $_.count -gt 2 }

 

When the ListCmdletsWithMoreThanOneAlias.ps1 script runs, the following appears:

Count Name                      Group
----- ----                      -----
    6 Remove-Item               {del, erase, rd, ri...}
    3 Set-Location              {cd, chdir, sl}
    3 Get-History               {ghy, h, history}
    3 Get-ChildItem             {dir, gci, ls}
    3 Get-Content               {cat, gc, type}
    3 Move-Item                 {mi, move, mv}
    3 Copy-Item                 {copy, cp, cpi}

To see if an alias for the Measure-Object cmdlet exists already, you can use the Get-Alias cmdlet and the -definition parameter as shown here:

PS C:\> Get-Alias -Definition Measure-Object

CommandType     Name                                      Definition
-----------     ----                                      ----------
Alias           measure                                   Measure-Object

If you like the alias measure, you can use that alias. However, you may decide that the readability of the alias measure is hampered by the fact, that it only saves two key strokes. Because of the implementation of the tab expansion feature, all you need to do is type measure-o and press TAB. In general, when creating personal aliases, I prefer to sacrifice readability for ease of use. My very favorite aliases are one-letter and two-letter aliases. I use the one-letter aliases for commands I frequently use, because they are the most useful and most obscure. I use the two-letter aliases for most of my other alias needs. The two-letter combination can easily correspond to the verb noun naming convention. Therefore, mo would be a logical alias for the Measure-Object cmdlet. To ensure its availability, use the Get-Alias cmdlet as shown here:

PS C:\> Get-Alias -Name mo

How many two-letter aliases are there?

The two-letter alias namespace is rather large. How large? You have to take every letter in the range A-Z, and pair it with every other letter in the range of A-Z. If you are good with algebra then you immediately know there are 676 possible letter combinations (26 x 26). However, if your algebra is a bit rusty, you may wish to write a Windows PowerShell script to figure it out for you. The problem with such an approach is that we cannot use the range operator to produce a range of letters. It works with numbers, so 1..10 automatically creates a range of numbers with the values 1 through 10, and can save you a lot of typing. However, because we have ASCII numeric representations of the letters A-Z, you can use this technique to create a range of letters. The ASCII value 97 is the “A” character, and the ASCII value 122 is “Z”. After we have the numeric range, we use the ForEach-Object cmdlet and convert each letter to a character by using the [char] type. We store the resulting array of letters in the $letters variable. We then do two loops through the array and store the resulting letter combinations in the $lettercombination variable, which is constrained as an array by using the [array] type. The Measure-Object cmdlet is used to count the number of possible letter combinations. The ListTwoLetterCombinations.ps1 script is shown here.

ListTwoLetterCombinations.ps1


$letterCombinations = $null
$asciiNum = 97..122
$letters = $asciiNum | ForEach-Object { [char]$_ }
Foreach ($1letter in $letters)
{
 Foreach ($2letter in $letters)
 {
  [array]$letterCombinations += "$1letter$2letter"
 }
}
"There are " + ($letterCombinations | Measure-Object).count + " possible combinations"
"They are listed here: "
$letterCombinations

To create a new alias, you can use either the New-Alias cmdlet or the Set-Alias cmdlet. You could also use the New-Item cmdlet and target the Alias drive. The problem with that technique is it does not support the Description parameter, which allows you to specify additional information about the alias. The other problem with using the New-Item cmdlet to create an alias is that it is more typing. So as a best practice, I always use either the New-Alias or the Set-Alias cmdlet.

In choosing between the two cmdlets, which should you use when creating a new alias? Before we answer that, we should actually talk about what the cmdlets are intended to be used for. The New-Alias cmdlet, obviously creates a new alias. The Set-Alias cmdlet is used to modify an existing alias. If an alias does not exist, it will create the alias for you. Therefore, many people use the Set-Alias cmdlet to both create and modify an alias. There is a danger in this approach, however, in that you can inadvertently modify a previously existing alias with no notification. If this is your desired behavior, that approach is fine. A better approach is to use the New-Alias cmdlet when creating an alias. This allows you to specify the Description parameter, and to receive notification if an alias that you are trying to create already exists. To assign a description to an alias when creating it, you use the Description parameter as seen here:

New-Alias -Name mo -Value Measure-Object -Description "MrEd Alias"

In an enterprise-scripting environment, many companies like defining a corporate set of aliases—this provides for a consistent environment. A network administrator working on one machine can be assured that a particular alias is available. This also helps to ensure a predictable and consistent environment. By using the same value for the Description parameter of the alias, it is easy to list all the corporate aliases. To do this, you would filter the list of aliases by the description. An example of this is seen here:

PS C:\> Get-Alias | Where-Object { $_.description -eq 'mred alias' }

CommandType     Name                                      Definition
-----------     ----                                      ----------
Alias           mo                                        Measure-Object

When using the -eq operator in the code block of the Where-Object cmdlet, the filter is case insensitive. If you need a case sensitive operator, you would use -ceq. The “c” is added to all the operators to form a case sensitive form of the operator—by default the operators are case insensitive. As shown here, when using the case sensitive operator, the filter does not return any aliases:

PS C:\> Get-Alias | Where-Object { $_.description -ceq 'mred alias' }
PS C:\>

In addition to specifying the Description parameter, many companies also like to use the Option parameter to make the alias either read-only or constant. To make the alias read-only, you supply the readonly keyword to the Option parameter. This is shown here:

New-Alias -Name mo -Value Measure-Object -Description "MrEd Alias" -Option readonly

The advantage of making the alias read-only is that it offers protection against accidental modification or deletion. This is seen here:

Image of read-only alias protecting against modification or deletion


There is an additional advantage to making the alias read-only: It can be modified or deleted if needed. If you wish to modify the description, you use the Set-Alias cmdlet to specify the name, value the new description, and use the Force parameter. This is seen here:

Set-Alias -Name mo -value measure-object -Description "my alias" –Force

If you need to delete a read-only cmdlet, you use the Remove-Item cmdlet and specify the Force parameter. This is seen here:

Remove-Item Alias:\mo -Force

To create a constant alias, you use the constant keyword with the option parameter. This technique is shown here:

New-Alias -Name mo -Value Measure-Object -Description "MrEd Alias" -Option constant

As a best practice, you should not create constant aliases unless you are certain you do not need to either modify them or delete them. This is because a constant alias can be neither modified nor deleted—in effect, they really are constant. The error message is a bit misleading in that it says the alias is either read-only or constant, and it suggests attempting to use the Force parameter. The reason this is misleading is the message is displayed even when the command was run with the Force parameter. This error message is seen here:

Image of error message

 

Well, RR, this should get you started on using aliases in Windows PowerShell. Join us tomorrow as we continue examining ways to customize our Windows PowerShell environment.

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 them on the Official Scripting Guys Forum. See you tomorrow. Until then, keep on scripting!

 

Ed Wilson and Craig Liebendorfer, Scripting Guys

 

Hey, Scripting Guy! Quick-Hits Friday: The Scripting Guys Respond to a Bunch of Questions (11/6/09)

Bookmark and Share

In this post:

 

Using the Active Directory DirectoryEntry Class

Hey, Scripting Guy! Question

Hey, Scripting Guy! I have been writing some scripts for Active Directory, and this past weekend I ran into something that puzzled me. When a search is performed against Active Directory (using the DirectoryEntry class and searcher class), and when the GetDirectoryEntry method is called against the returned SearchResult objects, the members of the newly returned DirectoryEntry objects do not match the members listed on MSDN at all (for the .NET class).

Why is that? The Windows PowerShell GetDirectoryEntry method seems to have bound directly to the Active Directory object, whereas the MSDN documentation speaks of returning a DirectoryEntry object with a number of methods and properties available to it. And is there a way to return a true instance of the .NET class in Windows PowerShell?

-- AD

Hey, Scripting Guy! AnswerHello AD,

Microsoft Scripting Guy Ed Wilson here. The DirectoryEntry class is documented on MSDN. To gain access to some of the methods and properties described in the MSDN article, you will need to access the base object, which is the .NET Framework class that was used to create the object we see in Windows PowerShell. In many cases, accessing the base object is not required. But because of the way that Active Directory Services Interface (ADSI) created it, we need to use the base object from time to time. An example of obtaining a DirectoryEntry object by using the ADSI type accelerator and displaying its base properties is seen here:

$de = [adsi]”LDAP://dc=nwtraders,dc=com”

$de.psbase | Get-Member

The base members of the DirectoryEntry object are seen here:

Image of base members of DirectoryEntry object


You can, of course, create a DirectoryEntry object directly as seen here. There are five different constructors available for this class. The different constructors are all documented on MSDN:

$de = new-object System.DirectoryServices.DirectoryEntry

You will still need to use PSBase to access all of the methods and properties, but the advantage of creating your own object is you get to define the constructor that is used. I have written several Hey, Scripting Guy! articles that talk about this.


 

How Can I Run a Script Whenever a New USB Drive Is Attached to a Computer?

Hey, Scripting Guy! Question

Hey, I need to run a script whenever a new USB drive is attached to a computer. Is there a trigger I can use to call my script?

-- JG

 

Hey, Scripting Guy! AnswerHello JG,

Yes. The Monitor Process Event script from the TechNet Script Center Gallery should help. You will need to change Win32_Process to Win32_LogicalDisk, and it will generate an event when a new logical drive is detected.  


Making a Specific Windows PowerShell 2.0 Script Work with Windows PowerShell 1.0

Hey, Scripting Guy! Question

Hey, Scripting Guy! In regards to the Can I Determine Which Folders Are Using the Most Space on My Computer? article, I appreciate the level of detail and helpfulness of this article. The script was written using Windows PowerShell 2.0. For those folks like me who are still using Windows PowerShell 1.0, how can such a task be done?

-- JS


Hey, Scripting Guy! AnswerHello JS,

Basically the only Windows PowerShell 2.0 things I did in that script was use the new help tags. When they are removed, the script will work. I took the time to do this for you, and to test it on my Windows XP box that runs Windows PowerShell 1.0. I then posted the script to the new Script Center Script Gallery. You can access the script from http://bit.ly/14dzcY.


 

Can I Use VBScript to Distinguish Between Versions of Windows Server 2008?

Hey, Scripting Guy! Question

Hey, Scripting Guy! How can I make the distinction between a 32-bit and a 64-bit version of Windows Server 2008 using VBScript?

-- VJ


 

Hey, Scripting Guy! AnswerHello VJ,

Here is some Windows PowerShell code I wrote to determine if a computer is 32-bit or if it is 64-bit.

Function Test-64

{ If($env:PROCESSOR_ARCHITECTURE -match '64') { $true } ELSE { $false }}

 

Function Test-32

{ if($env:PROCESSOR_ARCHITECTURE -match '86') { $true } ELSE { $false} }

 

In the two test functions, I read the value of the Processor_Architecture environmental variable. Here is a VBScript that you can modify to search for the Processor_Architecture variable to use in your VBScript code.

 

 

How Can I Determine the Drive Letter of My CD-ROM Drive?

Hey, Scripting Guy! Question

Hey Scripting Guy! The script in the How Can I Determine the Drive Letter for the Drive My Script is Currently Running On? article does not return the CD drive letter (like it says it does) but rather the "C:\" drive every time, no matter which PC I put the CD into. Any idea why? 

-- JW

 

Hey, Scripting Guy! AnswerHello JW,

The script that you refer to does not return the drive of the CD-ROM necessarily. It returns the drive letter from where the script was launched. In the email from RKG (in the article), his script was on a CD-ROM and he wanted to know the drive letter from where the script was running—in his case the CD-ROM. If you are running the script from your C: drive (as would often be the case), the script will always return the drive letter “C”. If you are running it from your D: drive, the script will always return “D”. If you have it on a portable USB drive, and do not know which drive letter has been assigned to your USB drive, the script will convey this information to you. This is in fact the purpose of the script: to tell you from where the script is being launched. It is useful in situations when, for instance, you have the script on a USB drive, and want to copy files from your computer to the USB drive, but you do not know the letter of the USB drive. You would include this code at the top of your script to obtain the drive letter of the USB drive, and then begin your copy operation. For simply determining the drive letter of your CD-ROM drive, you could use the List CD-ROM Properties script from the new TechNet Script Center Gallery. 

 

Well, this concludes another edition of Quick-Hits Friday. It also concludes another exciting week on the Script Center. Join us next week as we delve into the mysteries of…we’ll let that remain a mystery for now.

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 them on the Official Scripting Guys Forum. See you tomorrow. Until then, peace. 

Ed Wilson and Craig Liebendorfer, Scripting Guys

 

Hey, Scripting Guy! Can I Start an Event Based on When a Registry Value Is Changed?

Bookmark and Share

 

Hey, Scripting Guy! Question

Hey, Scripting Guy! I need to be notified when a particular registry key value gets changed. The registry value is in the HKEY_LOCAL_MACHINE hive. What I am trying to accomplish is this: I am using a script to install software. After the first piece of software is successfully installed, I want to install a subsequent piece of software. And after this is done, I will reboot the computer. I have tested this technique in our lab, and I am able to postpone the reboot. I have tried pausing the execution of the install script by using the Start-Sleep cmdlet, and this works okay. The problem is that on fast computers the pause is too long, and on slow computers the pause is not long enough. It seems that I cannot figure out a really good way to do this.

I am thinking that if I cannot figure this out, I will need to just record the computers on which the install fails, and manually visit each machine; however, several of the computers are at remote locations and this is not a really practical solution. The idea for the registry key is that each software package creates its own registry key, and if I can detect when this occurs, I can then start the next install. Can you help me?

-- LK

Hey, Scripting Guy! AnswerHello LK,

Microsoft Scripting Guy Ed Wilson here. I am listening to the The Kingston Trio on my Zune as I check the scripter@microsoft.com e-mail inbox. I am sipping a cup of Constant Comment tea and munching on an ANZAC biscuit. The weather outside is frightful, but inside my office it is delightful. And since I’ve no place to go, let it snow!

By the way, Craig and I have been having several discussions about the 2010 Scripting Games. One thing we can tell you, they will not be held in the summer. They will be earlier! I have been busy working on events. If you have any ideas for the 2010 Scripting Games, e-mail us at scripter@microsoft.com. What should you say? Just about anything. We are interested in ideas for events, ideas to make the Games go better, ideas to increase participation—just about anything.

LK, I wrote the MonitorRegistryKeyValueChangeEvent.ps1 for you. The MonitorRegistryKeyValueChangeEvent.ps1 script watches a registry key value, and when that value changes, it fires an event. To use the script, you must have Windows PowerShell 2.0 installed. You will need to modify the $keyPath variable and the $valueName variable to use it. The complete MonitorRegistryKeyValueChangeEvent.ps1 script is seen here.

MonitorRegistryKeyValueChangeEvent.ps1

#Requires –version 2.0

$hive = "HKEY_LOCAL_MACHINE"

$keyPath = "Software\\Microsoft\\WBEM\\Scripting"

$valueName = "Default NameSpace"

$gciPath = $keyPath -replace "\\","\"

$query = "Select * from RegistryValueChangeEvent where Hive='$hive' AND " +

         "KeyPath='$keyPath' AND ValueName='$ValueName'"

Register-WmiEvent -Query $query -SourceIdentifier KeyChanged

Wait-Event -SourceIdentifier KeyChanged

"New value for $valueName is " + (Get-ItemProperty -Path HKLM:\$gciPath).$valueName

The MonitorRegistryKeyValueChangeEvent.ps1 actually begins with a comment that states the script requires version 2.0. This is not really a comment; it is a directive to Windows PowerShell that will prevent script execution on a Windows PowerShell 1.0 computer. Of course the script will not execute on Windows PowerShell 1.0, but this directive will save you time and generate a more readable error message. The #Requires statement is seen here:

#Requires -version 2.0

The $hive variable holds the registry hive that will be monitored. In this script, you monitor the HKEY_LOCAL_MACHINE hive. This is the only hive the RegistryKeyChangeEvent WMI class supports. Any of the WMI classes that are derived from the RegistryEvent abstract class only work on the HKEY_LOCAL_MACHINE hive. The WMI registry event classes are seen in Table 1.

Table 1  WMI Registry Event Classes

Event class

Hive location

Description

RegistryEvent

N/A Abstract

Base class for changes in the registry.

RegistryTreeChangeEvent

RootPath

Monitors changes to a hierarchy of keys.

RegistryKeyChangeEvent

KeyPath

Monitors changes to a single key.

RegistryValueChangeEvent

ValueName

Monitors changes to a single value.

 

The value of the registry hive, HKEY_LOCAL_MACHINE, is normally listed as all capital letters, and in some instances, this is actually a requirement. However, with the RegsitryKeyChangeEvent WMI class as used in Windows PowerShell, this is not case-sensitive. You could easily use all lowercase letters for your hive name and it would work fine. Keep in mind that though it is not case sensitive, you must spell it correctly for the script to work:

$hive = "HKEY_LOCAL_MACHINE"

The key path points to the registry key you wish to monitor. There are two things that you need to remember. The first is that you do not have a backward slash at the beginning of the string. The second thing to remember is that all of the backward slashes are escaped—you have double backward slashes between portions of the registry key name. The value assignment to the $keyPath variable is seen here:

$keyPath = "Software\\Microsoft\\WBEM\\Scripting"

The last portion of the path to the registry value you want to monitor is the value name itself. The variable $valueName stores the name of the value to monitor. This is shown here:

$valueName = "Default NameSpace"

The path to the registry value that is being monitored is seen here:

Image of the path to the registry value being monitored


After the path to the registry key value has been assigned via the three different variables, the replace operator is used to replace the double backward slashes with a single backward slash. This is only necessary for the value stored in the $keyPath variable because the hive and the value strings do not have any backward slashes in them. The resulting path is stored in the $gipPath variable. This path will be used with the Get-ItemProperty cmdlet to retrieve the new value that is assigned to the registry value—and the Get-ItemProperty cmdlet does not need to have the backward slashes escaped the way WMI does:

$gipPath = $keyPath -replace "\\","\"

The WMI query is the same as the WMI query that is used in VBScript. An example of monitoring the registry from VBScript can be seen in the Hey, Scripting Guy! article, How Can I Monitor Changes to a Registry Key?

When writing your VBScripts, if you keep your query separated from the WMI moniker, it makes it easier to reuse your WMI queries in a Windows PowerShell script.

$query = "Select * from RegistryValueChangeEvent where Hive='$hive' AND " +

         "KeyPath='$keyPath' AND ValueName='$ValueName'"

Two cmdlets are used to provide access to the WMI event system. The first cmdlet is the Registry-WmiEvent cmdlet. The WMI query that is stored in the $query variable is used, and the SourceIdentifier KeyChanged is assigned to the event. Only one event query can be assigned the SourceIdentifier of KeyChanged because SourceIdentifiers must be unique. If you inadvertently run the script a second time, this error appears:

Image of error that appears when inadvertently running script a second time

The Wait-Event cmdlet is used to pause the script execution until the event identified by the SourceIdentifier occurs. These two lines of code are seen here:

Register-WmiEvent -Query $query -SourceIdentifier KeyChanged

Wait-Event -SourceIdentifier KeyChanged

When the event triggers, the script continues to the next line. The Get-ItemProperty cmdlet is used to retrieve the new value that caused the event to trigger. This is seen here:

"New value for $valueName is " + (Get-ItemProperty -Path HKLM:\$gipPath).$valueName

After you run the script, the information seen in the following image appears in the Windows PowerShell ISE:

Information appearing in Windows PowerShel ISE after running script

Well, LK, that is all there is to monitoring the registry for a key value change event. Join us tomorrow as we dive into the virtual mail bag and retrieve questions that require shorter answers. Yes, it is time for Quick-Hits Friday.

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 them on the Official Scripting Guys Forum. See you tomorrow. Until then, peace.
 

Ed Wilson and Craig Liebendorfer, Scripting Guys

 

Hey, Scripting Guy! Can I Format a Portable Drive When It Is Inserted Into a Computer?

Bookmark and Share

 

Hey, Scripting Guy! Question

Hey, Scripting Guy! I work with portable USB drives all the time. These are used for backup purposes, for temporary storage, for Sneakernet, and as a means to work with extremely large files that I do not want cluttering up the small hard disk drive on my corporate standard desktop. The problem is that I now have more than a dozen portable USB drives, some of which are continually connected, and others that I shuttle in and out of my computer on an hourly basis to transfer files from one computer to another. (Our IT department has disabled file sharing on our computers, and therefore I cannot simply copy one file from one computer to another. In addition, all file attachments get stripped out of e-mail and so I cannot use e-mail to transfer files either. I am therefore left with sneaker net.)

The problem is that, once I have used a portable drive to transfer a file from one computer to another, I do not always delete the file to clean up the drive. As a result some of these drives tend to fill up with useless information. Here is what I currently do: I plug the portable USB drive into my computer, and then navigate to the drive in Windows Explorer to check for files on the drive. If the drive has files, I do a quick format to clean it up. I would love to automate this process. Do you know of a script that will automatically format a portable drive when it is inserted into a computer?

-- JM

Hey, Scripting Guy! AnswerHello JM, Microsoft Scripting Guy Ed Wilson here. Well this afternoon, the Scripting Wife made me a nice pot of Earl Grey tea in my little blue tea pot, and even shared one of her Tim Tams with me. I guess she is still excited about her new flat panel monitor for her Windows 7 Ultimate computer. I am listening to “Zapatos de Tacon Alto” (High-Heeled Shoes) by Jose Feliciano on my Zune. I bought the CD-ROM a couple years ago while I was teaching a VBScript class in Monterrey, Mexico. Anyway, I am reviewing the e-mail in the scripter@microsoft.com mail box (we receive several hundred e-mails a week to this alias, and it takes a decent amount of time to go through them all). All in all, it is a terrific afternoon in Charlotte, North Carolina, in the United States—a fine scripting day!

JM, I wrote the MonitorDiskFormatDrive.ps1 script for you. It will detect when a new drive has been added to your system. After the new drive has initialized, the contents of the drive are displayed on your Windows PowerShell console. If there are any files on the portable drive, you will be prompted to format the drive. If no files exist on the drive, it will automatically be formatted. The complete MonitorDiskFormatDrive.ps1 script is seen here.

MonitorDiskFormatDrive.ps1

#Requires -version 2.0

function Test-IsAdministrator
{
    <#
    .Synopsis
        Tests if the user is an administrator
    .Description
        Returns true if a user is an administrator, false if the user is not an administrator       
    .Example
        Test-IsAdministrator
    #>  
    param()
    $currentUser = [Security.Principal.WindowsIdentity]::GetCurrent()
    (New-Object Security.Principal.WindowsPrincipal $currentUser).IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator)
} #end function Test-IsAdministrator
 
Function Get-IsWindowsXP
 {
   if( (Get-WmiObject -Class win32_operatingsystem).version  -gt "6.1.2600" )
     { $false }
   else { $true }
 } #end function Get-IsWindowsXP

Function Get-DriveEvent
 {
  Register-WmiEvent -Class win32_VolumeChangeEvent -SourceIdentifier volumeChange
  $newEvent = Wait-Event -SourceIdentifier volumeChange
  Get-DriveContents -path $newEvent.SourceEventArgs.NewEvent.DriveName
 } #end function Get-DriveEvent

Function Get-DriveContents ($path)
 {
  If( (Get-ChildItem -Path $path).count -gt 0)
    { 
      Get-ChildItem -Path $path
     $in = Read-Host -Prompt "Drive contains files. Format anyway? y / n"
     if ($in -match "y") { Set-DriveFormat -path $path }
     if ($in -match "n") { "Drive will not be formatted. Exiting" ; exit }
    }
   Else { Set-DriveFormat -path $path }

 } #end function Get-DriveContents
 
Function Set-DriveFormat($path)
 {
  "Formatting drive $path this can take some time ..."
  $drive = Get-WmiObject -Class win32_volume -Filter "DriveLetter = '$path'"
  $drive.Format("FAT32",$true,4096,"testFormat",$false)
 } #end function Set-DriveFormat
 
# *** Entry Point to Script ***
if( -not (Test-IsAdministrator)) { "This script requires admin rights." ; exit }
if(Get-IsWindowsXP) { "This script requires Windows Server 2003 or newer." ; exit }
"Watching for new Drive ..."
Get-DriveEvent

If you want to format a disk drive, it requires administrator rights. There is no point in running the MonitorDiskFormatDrive.ps1 script if the user does not have administrator rights. To check for these rights, use a function I wrote for the Windows 7 Resource Kit called Test-IsAdministrator. The Test-IsAdministrator function uses the GetCurrent static method from the Security.Principal.WindowsIdentity .NET Framework class to determine the current user. After the current user identity is established, the IsInRole method from the Security.Principal.WindowsPrincipal .NET Framework class is used to determine if the current user is an administrator. The Test-IsAdministrator function is seen here:

function Test-IsAdministrator
{
    <#
    .Synopsis
        Tests if the user is an administrator
    .Description
        Returns true if a user is an administrator, false if the user is not an administrator       
    .Example
        Test-IsAdministrator
    #>  
    param()
    $currentUser = [Security.Principal.WindowsIdentity]::GetCurrent()
    (New-Object Security.Principal.WindowsPrincipal $currentUser).IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator)
} #end function Test-IsAdministrator

Because the MonitorDiskFormatDrive.ps1 script relies on the Win32_Volume WMI class, the first thing the script does is to determine the version of the operating system (OS) the script is running on. This is because the Win32_Volume WMI class does not exist on Windows XP. The OS check is performed in the Get-IsWindowsXP function. The Win32_OperatingSystem WMI class is used to determine the version of the OS. If the version of the OS is higher than 6.1.2600 (which was the OS version of Windows XP), the script returns false to the calling code. If the version of the OS is not greater than 6.1.2600, the function returns true to the calling code. The complete Get-IsWindowsXP function is seen here:

Function Get-IsWindowsXP
 {
   if( (Get-WmiObject -Class win32_operatingsystem).version  -gt "6.1.2600" )
     { $false }
   else { $true }
 } #end function Get-IsWindowsXP

The Get-DriveEvent function is used to create the WMI event that will monitor for a volume change. To do this, the Win32_VolumeChangeEvent WMI class is used. This WMI class is designed to detect when a volume change event occurs. The event could be a new drive or a drive that was removed from the system. Because of the way the MonitorDiskFormatDrive.ps1 script will be used, it is safe to assume the volume change event that will occur will be a drive-added event. The Register-WmiEvent Windows PowerShell cmdlet is used to create the WMI event. The Wait-Event Windows PowerShell cmdlet will pause the Windows PowerShell console until an event occurs. This is great for halting the execution of your script when you know that something is about to occur, such as if you are getting ready to insert a USB drive into your computer. When the event occurs, the script will continue and will retrieve the DriveName property from the instance of the Win32_VolumeChangeEvent WMI class. This is important because when a removable drive is added to my system, a drive letter is assigned to it. The letter is not always the next available drive letter, nor is it always the same drive letter. This is seen here:

Image of the next drive letter assigned


The complete Get-DriveEvent function is seen here:

Function Get-DriveEvent

 {

  Register-WmiEvent -Class win32_VolumeChangeEvent -SourceIdentifier volumeChange

  $newEvent = Wait-Event -SourceIdentifier volumeChange

  Get-DriveContents -path $newEvent.SourceEventArgs.NewEvent.DriveName

 } #end function Get-DriveEvent

The Get-DriveContents cmdlet uses the Get-ChildItem Windows PowerShell cmdlet to retrieve the contents of the newly added drive. If the drive has files (in other words, if the count is greater than zero), the Read-Host cmdlet is used to prompt the user for permission to format the drive. If the user types “y”, the Set-DriveFormat function is called. If the user types “n”, the script exits without making any changes. If the drive contains no files, the Set-DriveFormat function is called and the drive is formatted. The Get-DriveContents function is seen here:

Function Get-DriveContents ($path)

 {

  If( (Get-ChildItem -Path $path).count -gt 0)

    { 

      Get-ChildItem -Path $path

     $in = Read-Host -Prompt "Drive contains files. Format anyway? y / n"

     if ($in -match "y") { Set-DriveFormat -path $path }

     if ($in -match "n") { "Drive will not be formatted. Exiting" ; exit }

    }

   Else { Set-DriveFormat -path $path }

 

 } #end function Get-DriveContents

The Set-DriveFormat function uses the Win32_Volume WMI class, which is documented on MSDN. When the script is run, it must be run with administrator rights to format the drive. If it is not run with administrator rights, the script will display a message that indicates administrator rights are required. This is shown here:

Image showing administrator rights are required


The complete Set-DriveFormat function is seen here:

Function Set-DriveFormat($path)

 {

  "Formatting drive $path this can take some time ..."

  $drive = Get-WmiObject -Class win32_volume -Filter "DriveLetter = '$path'"

  $drive.Format("FAT32",$true,4096,"testFormat",$false)

 } #end function Set-DriveFormat

When the MonitorDiskFormatDrive.ps1 script is run, the script first uses the WMI event to monitor for a new drive event. When the drive event is detected, the drive letter is captured, and the contents of the drive are displayed. If contents are present on the drive, a prompt is used to ask if you wish to format the drive. If you type “y” for yes, the drive will be formatted. If you type “n” for no, the script will exit. If the drive is to be formatted, a message that states the drive is being formatted is displayed. The return code from the format method of the Win32_Volume WMI class is displayed when the format job is complete. This is shown here:

Image of the return code shown when format job is complete

 

JM, that is all there is to using a WMI event class to monitor for the insertion of a new drive on your computer. WMI Event Week will continue tomorrow when we will talk about…

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 them on the Official Scripting Guys Forum. See you tomorrow. Until then, peace.
 

Ed Wilson and Craig Liebendorfer, Scripting Guys

 

Hey, Scripting Guy! Can I Be Informed When a Portable Drive Is Added by My Computer?

Bookmark and Share

 

Hey, Scripting Guy! Question

Hey, Scripting Guy! I would love to be able to write a Windows PowerShell script that will inform me when a portable drive is added to my computer. Do you have ideas you could suggest?

-- RC

Hey, Scripting Guy! AnswerHello RC,

Microsoft Scripting Guy Ed Wilson here. Well it has already been a rather interesting week. I bought a new computer and loaded Windows 7 over the weekend. My new computer is hot. I loaded it up with 8 GB of RAM, 4 processor cores, and a solid-state boot drive for the operating system. I practically need to bolt it to the floor because the thing really flies. Of course, the Scripting Wife was unimpressed when I told her I needed to also purchase an ESATA array (for all my scuba-diving pictures). So I had to bribe her with a new flat-panel monitor for her Windows 7 Ultimate computer downstairs. Each of my high-resolution pictures is around 10 MB in size, and on my old computer scrolling, through the pictures was frustrating as I had to wait several seconds for each picture to load. On my new computer, I can click through the pictures as fast as I wish with no appreciable lag time. It makes reliving some of my old dives much more pleasant.

As an example, the following picture is a green moray eel I found hiding on the shipwreck Sea Tiger off the coast of Honolulu, Hawaii, in the United States. That dive was in June 2008 with a maximum depth of 110 feet (33 meters). The visibility was over 100 feet (30 meters), and the water was 81 degrees Fahrenheit (27 degrees Celsius). I was over there teaching a Windows PowerShell class and decided to combine a bit of holiday while I was on the island. All in all it was a lovely dive—and I get to relive it because of my new Windows 7 computer.

Image of green moray eel

RC, you want to be notified when an external drive (such as a USB drive) is attached to your computer. Using Windows PowerShell 2.0 (in the box on Windows 7 and Windows 2008 Server R2; downloadable for other operating systems), I wrote the MonitorDriveEvents.ps1 script. This event-driven script is shown here.

MonitorDriveEvents.ps1

#requires -version 2.0

Function Get-EventType($eventType)

{

 switch ($eventType)

 {

  1 {"Configuration changed"}

  2 {"Device arrival"}

  3 {"Device removal"}

  4 {"docking"}

 } #end switch

} #end get-eventType function

 

$MaxEvents = 5

$ErrorActionPreference = "stop"

Register-WmiEvent -Class win32_VolumeChangeEvent -SourceIdentifier volumeChange

 

For($i = 1 ; $i -le $MaxEvents ; $i ++)

{

 Try {

 $newEvent = Get-Event -SourceIdentifier volumeChange

 write-host "Drive: " $newEvent.SourceEventArgs.NewEvent.DriveName "changed on $env:COMPUTERNAME"

 write-host "Drive event was " $(Get-EventType -eventType $newEvent.SourceEventArgs.NewEvent.EventType)

 Remove-Event -SourceIdentifier volumeChange }

 Catch [System.Management.Automation.RuntimeException]{"No events"}

 Start-Sleep -Seconds 5

}

The MonitorDriveEvents.ps1 script begins by using the #requires –version 2.0 tag. The #requires statement looks like it is a comment that would be ignored by Windows PowerShell. However, the #requires statement is a directive that tells Windows PowerShell that the script will run only on Windows PowerShell 2.0. If you attempt to run a script that includes the #requires –version 2.0 directive on Windows PowerShell 1.0, the script will fail. The error message that is produced is seen here:

Image of error message when using requires statement on Windows PowerShell 1.0


The next thing that is done in the MonitorDriveEvents.ps1 script is to create the Get-EventType function. The Get-EventType function receives the eventType property when an event is generated. The eventType property is defined on the Win32_VolumeChangeEvent WMI class. This class is documented on MSDN and has two properties that we are interested in using. The first property is the drive letter that is assigned to the newly arrived drive, and the second is the eventType property from the instance of the Win32_VolumeChangedEvent WMI class that is created when an event arrives. This property lets us know if the drive has been added to the system or removed from the system. Because the eventType property is a numeric coded value, we need to translate the number that is returned into a string that makes more sense to us when the script is run. The switch statement in Windows PowerShell is similar to the Select-Case statement from VBScript (although it is more powerful, and can do more than choose between different input values). You can also think of the switch statement as a series of if statements. “If the value of the $eventType input variable is equal to 1, then output ‘Configuration changed’.” The Get-EventType function is seen here:

Function Get-EventType($eventType)

{

 switch ($eventType)

 {

  1 {"Configuration changed"}

  2 {"Device arrival"}

  3 {"Device removal"}

  4 {"docking"}

 } #end switch

} #end get-eventType function

After the Get-EventType function has been created, you come to the entry point to the script. Two variables receive values. The first variable, $MaxEvents, is used to determine how many event cycles will be examined. The second variable, $ErrorActionPreference, is a system variable that governs the way the script will behave if an error is detected. By setting the $ErrorActionPreference to stop, errors will cause the script to stop. This stop condition will be handled later in the script. Here are the two lines of code that assign values to variables:

$MaxEvents = 5

$ErrorActionPreference = "stop"

To create a registration for the WMI event, you use the Register-WmiEvent Windows PowerShell cmdlet. Because the Win32_VolumeChangeEvent WMI class is designed to raise events, no special constructions are required to use the class—you can use the WMI class directly with the cmdlet. The SourceIdentifier parameter is used to give a name to the event source that will be used later to retrieve WMI events. This line of code is shown here:

Register-WmiEvent -Class win32_VolumeChangeEvent -SourceIdentifier volumeChange

A For loop is used to check for events a certain number of times. The $MaxEvents variable controls how many loops will be made to check for new WMI events. This is shown here:

For($i = 1 ; $i -le $MaxEvents ; $i ++)

{

A Try…Catch construction is used to check for new events and to remove events from the WMI event queue. If you use the Get-Event cmdlet and there are no events, an error is generated. To avoid this, the Try…Catch construction is used. This is new syntax for Windows PowerShell 2.0. The Windows PowerShell script will attempt to retrieve new events by using the Get-Event cmdlet. If an error is generated, it is trapped by the Catch statement. Because the $ErrorActionPreference was set to stop, any error that is generated inside the Try statement will be caught by the Catch statement. If no errors are generated, the new events that are in the WMI event queue, are stored in the $newEvent variable. The Write-Host cmdlet is used to display the drive name from the new event and the event type. The value of the EventType is translated by calling the Get-EventType function. After this has been done, the event is removed by using the Remove-Event cmdlet. This section of the script is shown here:

Try {

 $newEvent = Get-Event -SourceIdentifier volumeChange

 write-host "Drive: " $newEvent.SourceEventArgs.NewEvent.DriveName "changed on $env:COMPUTERNAME"

 write-host "Drive event was " $(Get-EventType -eventType $newEvent.SourceEventArgs.NewEvent.EventType)

 Remove-Event -SourceIdentifier volumeChange }

The Catch statement is used to catch any Windows PowerShell errors. If an error occurs when reading the event from the event queue, it means that no event has occurred. The phrase “No events” is displayed if an error is detected. The Start-Sleep cmdlet is used to pause the execution of the script for five seconds. This section of the script is shown here:

Catch [System.Management.Automation.RuntimeException]{"No events"}

 Start-Sleep -Seconds 5

}

When the script runs, the drive letter of newly added drives is displayed in the output pane of the Windows PowerShell ISE. If no new drive is detected, the phrase “No events” is displayed. The output from the Windows PowerShell ISE is seen here:

Image of output from Windows PowerShell ISE


Well, RC, that is about all there is to checking for a drive event. Keep in mind that the Windows PowerShell script needs to be running to detect if a drive has been inserted. In addition, because of the way the script is written, it will only display information for one event per polling cycle (the amount of time the script is sleeping). If you have multiple events that occur during the polling cycle, you would need to use either a For statement or a ForEach statement to iterate through the collection of events that are returned and display the drive information from the event. This script is not intended to be a security auditing script, but rather a convenience script you could easily modify to automatically transfer files from a folder to the portable drive in an automated fashion. Join us tomorrow as WMI Event Week continues.

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 them on the Official Scripting Guys Forum. See you tomorrow. Until then, peace.

 

Ed Wilson and Craig Liebendorfer, Scripting Guys

 

More Posts Next page »
 
Page view tracker