|
|
-
Hey, Scripting Guy! With the release of the Community Technology Preview (CTP) 3 for Windows PowerShell, some of our staff have gone ahead and updated their computers. I am now beginning to worry that I need to check the version of Windows PowerShell that is installed on a computer before I run a script. I have two questions, the first of which is obvious: How do I do this. The second question is this: What the heck is a CTP anyway?
- WS 
Hi WS,
Let’s answer the second question first. What is a CTP? In the old days, when you were writing your computer program (probably poking holes in key punch cards—I still have some lying around), you would first analyze the requirements for the application, do some flow-charting to determine the inputs and the outputs, and then come up with a mock-up. After the customer gave you the go-ahead, you would develop a working prototype. We typically would refer to these things as an alpha. We could continue to refine the product, work on things, and as we were working on the code many times we would realize that things maybe were not working the way we had envisioned that they would work. After we had things working pretty well, we would again show it to the customer. This is a beta. Now we need to fix bugs. So we work real hard fixing the bugs. When we have things working well, we call that a release candidate. Sometimes we go through several release candidates before we actually release the product.
The problem with that approach was the long lead time between the pre-beta product, and the short lead time between the beta product and the released product. In other words, when a product hits beta, its features are pretty much set. So getting input from a customer during the beta phase is good for finding bugs, but it is bad for finding out if what you are making is actually what the customer wants. To resolve this situation, several groups at Microsoft came up with the idea of the community technology preview(CTP), which allows for earlier feedback from the community to the product group doing the coding. Windows PowerShell 1.0 went through a CTP, and it worked out great. Windows PowerShell 2.0 is once again going through a CTP, and many changes to the product have been directly attributed to feature requests coming from our customers. So you can think of a CTP as not being feature complete or bug free. You can think of a beta as pretty much being feature complete, but not bug free. A release candidate is feature complete, and mostly bug free. When working with a release candidate, what you see is pretty much what you will get when the software finally ships.
If you want to identify the version of Windows PowerShell that is running on your computer, you can read the RuntimeVersion value from the registry. This registry key is seen here:
One way to read from the registry is to use the registry provider from within Windows PowerShell and read a registry value as you might read a property from a file or a folder. To do this you need to use the HKLM PS drive—HKLM: in this case—and follow it with the path to the registry key, which is \SOFTWARE\Microsoft\PowerShell\1\PowerShellEngine. You can then select the item property you are interested in, which is RunTimeVersion in this example. This is seen in this script: $path = "HKLM:\SOFTWARE\Microsoft\PowerShell\1\PowerShellEngine"
$psv = get-itemproperty -path $path
$psv.RunTimeVersion
People who are familiar with VBScript may wish to create the WshShell object and use the RegRead method. To do this we use the HKLM moniker as a shortcut to refer to the HKEY_LOCAL_MACHINE registry key. The HKLM moniker when used with the WshShell object is case sensitive.
We store the path to the Windows PowerShell configuration information in the $path variable. Next we use the New-Object cmdlet to create an instance of the WshShell object. This COM object has the program ID of Wscript.Shell. We store the returned object in the $wshShell variable. After we have the WshShell object, we use the RegRead method to read the registry key value, which we specify by placing the path and value name in an expanding string: “$path\RunTimeVersion”. This script is shown here: $path = "HKLM\SOFTWARE\Microsoft\PowerShell\1\PowerShellEngine"
$WshShell = New-Object -ComObject Wscript.Shell
$WshShell.RegRead("$path\RunTimeVersion")
There may be times when you wish to use WMI to read the registry. To use WMI to read the registry, we need to use the stdRegProv WMI class. The stdRegProv WMI class has always been in the root\default WMI namespace. Beginning with Windows Vista, we also have an instance of the stdRegProv WMI class in the root\cimv2 (which incidentally really is the default namespace). What this means is that you can use the stdRegProv WMI class from either the root\default WMI namespace or from the root\cimv2 WMI namespace. It does not matter as it is the same WMI class. As a best practice, I recommend that you use the instance of the class from the root\default WMI namespace (as is done in the GetPsVersionWmi.Ps1 script below) to ensure compatibility with older versions of the Microsoft Windows operating system. WMI uses coded values to determine the registry tree (also known as a hive). These coded values are seen in the table just below. HKEY_DYN_DATA registry tree only exists on Windows 95 and Windows 98.
Table 1 WMI registry tree values
|
HKEY_CLASSES_ROOT |
2147483648 |
|
HKEY_CURRENT_USER |
2147483649 |
|
HKEY_LOCAL_MACHINE |
2147483650 |
|
HKEY_USERS |
2147483651 |
|
HKEY_CURRENT_CONFIG |
2147483653 |
|
HKEY_DYN_DATA |
2147483654 |
We use the value 2147483650 and assign it to the $HKLM variable. This value points the WMI query to the HKEY_LOCAL_MACHINE registry tree. We then assign the string software\Microsoft\PowerShell\1\PowerShellEngine to the variable $key. When using WMI to read the registry, the key is not preceded with a backslash.
We assign the registry property value we wish to read to the $value variable. Now we use the [wmiclass] type accelerator to obtain an instance of the stdRegProv WMI class. We specify the root\default WMI namespace to specify which version of the stdRegProv WMI class we wish to use. We could also have preceded the namespace with the name of a computer and read the registry from a remote computer. After we have created an instance of the stdregProv WMI class, we use the resulting System.Management.Managementclass to call the GetStringvalue method. The GetStringValue method takes three arguments: the registry key coded value, the registry subkey string, and the property name. Two values are returned by the method call. The first returned value is the returnvalue, which will indicate the success or the failure of the method call; the second is the svalue, which is the string value that was stored in the registry property. The complete GetPsVersionWmi.Ps1 script is seen here: $hklm = 2147483650
$key = "SOFTWARE\Microsoft\PowerShell\1\PowerShellEngine"
$value = "RunTimeVersion"
$wmi = [wmiclass]"root\default:stdRegProv"
($wmi.GetStringValue($hklm,$key,$value)).svalue
We can also use the .NET Framework classes to obtain information from the registry. To do this, we use the Microsoft.Win32.Registry .NET Framework class. We use the GetValue static method, which takes three parameters. The first parameter is the registry root and key name. The second parameter is the registry value you wish to read, and the last parameter is the default value of the registry key value. In the GetPsVersionNet.Ps1 script, we assign the HKEY_LOCAL_MACHINE string value to the $hklm variable. Next we assign the string representing the remainder of the registry path to the $key variable. The registry key property value we wish to retrieve is stored in the $value variable. We then use the [Microsoft.Win32.Registry] class and use the two colons to signify that we wish to use a static method and then use the GetValue method with the $hklm, $key, and $value parameters passed to it. The GetPsversionNet.ps1 script is seen here: $hklm = "HKEY_LOCAL_MACHINE"
$key = "SOFTWARE\Microsoft\PowerShell\1\PowerShellEngine"
$value = "RunTimeVersion"
[Microsoft.Win32.Registry]::GetValue("$hklm\$key",$value,$null)
Well, WS, as you have seen, to work with the registry there is a Windows PowerShell provider, a COM object, a WMI class, and a .NET Framework class. This plethora of methodologies is one of the big strengths of Windows PowerShell. It is also a significant source of confusion for people just learning Windows PowerShell. What is the best thing to use? If you are just making the transition from VBScript, use whatever is most comfortable to you.
I personally like using regread from the wshshell object. It is easy, fast, and makes sense. When I need to connect to a remote computer, I use WMI. I almost never use the .NET version. At times, however, I like the simplicity of using the Windows PowerShell provider. It makes it very easy to use. Stay tuned. We will be working with the registry all this week, and maybe by the end of the week you will find what works for you. See you tomorrow. Peace.
|
-
Hey, Scripting Guy! We are implementing a document management system at work, and users are no longer allowed to store PDF files. I need to search a folder for the existence of PDF files. If the folder contains any PDF files, I want to delete them. If on the other hand there are no PDF files in the folder, I would like to create a file called nopdf.txt. This is because we will use Systems Center Configuration manager software inventory to inventory computers that have the nopdf.txt file.
- RU 
Hi RU,
That is a rather interesting idea. Back in the days when I used to work with Systems Management Server (SMS), I used to do a similar trick to allow me easily track the progress of various operations.
For information about working with files and folders in Windows PowerShell see this article. You may also wish to see this section of "What Can I Do with Windows PowerShell" that deals specifically with files and folders. The Windows PowerShell scripting hub is a good place to get started with Windows PowerShell, and it includes download links and other items of use. The Sesame Script archive has several VBScript articles dealing with files and folders. They are worth a look for VBScript examples. The community script repository has a good selection of files and folders scripts in VBscript. You also may want to check the Hey, Scripting Guy! archive for a number of VBScript examples of working with files and folders.
Interestingly enough, with Windows PowerShell we have a short syntax that can be used from the command line. If we wanted to search a folder to see if there are any PDF files in it, and then print out a message if there were none, we could use this kind of syntax: if(!(gci C:\fso *.pdf -R)) { "no pdf"}
The problem with this type of syntax is that it is hard to read. This is the difference between working from the command line and writing a script. The above command begins by using an if statement. The if statement has the condition that is evaluated in smooth parentheses and the code that will be executed in curly brackets. In the if statement, we use the Get-ChildItem cmdlet (or “gci”) to look for PDF files in the fso directory. The -r means to recurse. The exclamation mark, !, is used as the not operator. This is similar syntax to when your friend comes up to you and says, "You are looking good today. Not." In the same way, the not operator means if we do not find any PDF files in the fso directory, we will print out "no pdf". Let's see how we would use this in a script. The script we came up with is called CheckForPdfAndCreateMarker.ps1, and it is seen here. $path = "c:\fso"
$include = "*.pdf"
$name = "nopdf.txt"
if(!(Get-ChildItem -path $path -include $include -Recurse))
{
"No pdf was found in $path. Creating $path\$name marker file."
New-Item -path $path -name $name -itemtype file -force |
out-null
} #end if not Get-Childitem
ELSE
{
$response = Read-Host -prompt "PDF files were found. Do you wish to delete <y> /<n>?"
if($response -eq "y")
{
"Pdf files will be deleted."
Get-ChildItem -path $path -include $include -recurse |
Remove-Item
} #end if response
ELSE
{
"PDF files will not be deleted."
} #end else reponse
} #end else not Get-Childitem
In the script, the first thing we do is define three variables. The first variable holds the path to the folder we are going to search, and we name it $path. The second variable holds the files we will be searching for. Because this value gets supplied to the -include parameter, I called the variable $include. Lastly, we have the name of the text file we wish to create. I call it $name. This section of code is seen here: $path = "c:\fso"
$include = "*.pdf"
$name = "nopdf.txt"
As a best practice, when working with scripts I tend to make the variable name the same as the parameter name. This makes the code easy to read and easier to understand.
Next we need to search for the PDF files. To do this, we use the Get-ChildItem cmdlet. This cmdlet is great at searching folders, and it returns either a fileinfo object or a directoryinfo object, depending on whether the item found is a file or a folder. Because we want to search the folder specified in the -path parameter and look for the filetypes (PDF) included in the $include, variable we need to use the -recurse parameter. We are only interested in the fact that the files either exist or don’t exist. This is a Boolean condition. Just like a light switch is either on or off, something is true or false, exists or does not exist. These are all Boolean conditions. You could use an if statement like this in VBScript, but most people never did. While it might look a little strange at first, it is a nice construction. Note that the condition we are negating needs its own set of parentheses. Just like in high school algebra, things inside parentheses get operated upon before things outside the parentheses. Here is the line of code: if(!(Get-ChildItem -path $path -include $include -Recurse))
In the code block for the if statement, we first print out a message that states no PDF file was found, and we are going to create the marker file. The cool thing here is the way the expanding strings work. An expanding string is a double quotation mark. The advantage of an expanding string is that it will expand the value of a variable when it is placed in the quotation marks. Single quotation marks are literal quotation marks. What is placed inside them is what is printed out. Here are some examples of this: PS C:\> $a = "string"
PS C:\> "$a is a string"
string is a string
PS C:\> '$a is a string'
$a is a string
PS C:\>
In our code block, after we have printed out a message, we then create the empty file in our folder. To do this we use the New-Item cmdlet. New-Item can be used to create files, folders, and other things as well. This is why it is called New-Item instead of New-File. To create a file, we use the -itemtype of file. To create a folder we use the -itemtype of directory. It is possible the file already exists in the directory. To handle this eventuality, we use the -force parameter, which will cause the command to overwrite the existing file. We do not want to clutter the screen with confirmation messages, so we pipe the returned information to the out-null cmdlet. This code is seen here: {
"No pdf was found in $path. Creating $path\$name marker file."
New-Item -path $path -name $name -itemtype file -force |
out-null
} #end if not Get-Childitem
When no PDF files are found, we see this message:
In the else clause, we need to handle the situation in which there are PDF files in the folder. We could just go ahead and delete the things, and then comment out the Read-Host line and the if and else clause. But this also gives me a chance to show you how to use read-host to solicit input from the user. The Read-Host cmdlet displays a prompt. If you want to capture the user response, use a variable to hold the results of the cmdlet. In this example, I use the variable $response to hold the user response. This section of the script is seen here: ELSE
{
$response = Read-Host -prompt "PDF files were found. Do you wish to delete <y> /<n>?"
Do not use $input for the input to the read-host cmdlet, as $input is an automatically created variable that is used for other things in Windows PowerShell. It will cause strange things to happen if you use this variable for your own purposes.
When we run the script and PDF files are found, we are greeted with this message:
We need to evaluate the response. To do this we use the if statement. In Windows PowerShell, we do not use the equals sign (=) for the equality operator. Instead, we use -eq, -ieq, and -ceq for equality. The first one is case insensitive, the second is likewise case insensitive, and the last one is the case sensitive equality operator. If the value of $response is equal to "y", we print out a message that states the PDF files will be deleted, and we once again use the Get-ChildItem cmdlet to provide a collection of all the PDF files in the folder. The cool thing is we pipeline the resulting information to the Remove-Item cmdlet, which deletes the files. This is seen here: if($response -eq "y")
{
"Pdf files will be deleted."
Get-ChildItem -path $path -include $include -recurse |
Remove-Item
} #end if response
When the user types "y", the screen changes as seen here:
But if the value of $response was anything other than "y", we print out a message that the files will not be deleted and we exit the script. This is seen here: ELSE
{
"PDF files will not be deleted."
} #end else reponse
} #end else not Get-Childitem
RU, hope U R satisfied with this script. Get it? Script you again soon.
|
-
Hey, Scripting Guy! In a previous column, you told us a couple different ways to save Excel spreadsheets. In that column, you said that one of the things you could do with the SaveAs method was password-protect a spreadsheet. However, you didn’t show us an example of this. How can I use a script to password-protect a spreadsheet?
-- MC 
Hey, MC. Well, as we hinted at in the previous column you mentioned, any time you call the SaveAs method you can include password-protection as an optional parameter. To demonstrate, here’s a script that creates a new worksheet, writes the current date and time in cell A1, and then saves the worksheet as C:\Scripts\Test.xls. On top of that, it password-protects the spreadsheet, giving it the password %reTG54w: Set objExcel = CreateObject("Excel.Application")
objExcel.Visible = True
objExcel.DisplayAlerts = FALSE
Set objWorkbook = objExcel.Workbooks.Add
Set objWorksheet = objWorkbook.Worksheets(1)
objWorksheet.Cells(1, 1).Value = Now
objWorkbook.SaveAs "C:\Scripts\Test.xls",,"%reTG54w"
objExcel.Quit
If you’ve done any scripting with Microsoft Excel, this is about as simple a script as you’ll ever write. The only “gotcha” to be aware of occurs in this line of code, where you actually save the file and password-protect it: objWorkbook.SaveAs "C:\Scripts\Test.xls",,"%reTG54w"
Admittedly, there’s nothing fancy about this line of code; you just have to make sure the password (%reTG54w) is the third parameter passed to the SaveAs method. The first parameter is, of course, the file name. The second parameter is the file format. Because we’re using the default format, we don’t need to set a value for the second parameter; however, we do need to include a placeholder for that parameter. That’s what the back-to-back commas (,,) are for: they simply indicate that the value for the second parameter would go here if we actually had a value for the second parameter. By including this placeholder, the password becomes the third parameter, which is exactly what we want.
After you run this script, try to open the file C:\Scripts\Test.xls; you’ll be prompted to enter the password before the file will actually be opened. Incidentally, this will happen even if you try opening the file using a script. (Sorry, but using a script won’t allow you to bypass the password protection.) But can’t you specify the password when you open the file? Of course you can; that’s what happens with this script: Set objExcel = CreateObject("Excel.Application")
objExcel.Visible = True
objExcel.DisplayAlerts = FALSE
Set objWorkbook = objExcel.Workbooks.Open("C:\Scripts\Test.xls",,,,"%reTG54w")
Note that when opening a spreadsheet the password has to be the fifth parameter; thus we have the file name, three placeholders, and then the password. This can be a little confusing, to say the least, but here’s a rule of thumb: just put in one more comma than you have placeholders. In this example, we have three placeholders, so we insert four commas. If we had nine placeholders, we’d insert ten commas. And so on.
But what if you decide later on to remove the password? No problem: simply open the file and set the value of the Password property to an empty string. Here’s a script that does just that: it opens the spreadsheet, removes the password, and then uses the SaveAs method to re-save the file. After running this script, try to open this spreadsheet from within Windows Explorer; you should be able to do so without being prompted for a password. Set objExcel = CreateObject("Excel.Application")
objExcel.Visible = True
objExcel.DisplayAlerts = FALSE
Set objWorkbook = objExcel.Workbooks.Open("C:\Scripts\Test.xls",,,,"%reTG54w")
Set objWorksheet = objWorkbook.Worksheets(1)
objWorkbook.Password = ""
objWorkbook.SaveAs "C:\Scripts\Test.xls"
objExcel.Quit
What if you didn’t want to remove the password, but merely wanted to change it? In that case, just set the Password property to the new password rather than to an empty string. And before you ask, yes, we could have used the Save method here rather than SaveAs. We stuck with SaveAs simply to be consistent with the previous script. We’re also aware that “Consistency is the hobgoblin of little minds.” But, hey, what size minds would you expect the Scripting Guys to have?!?
|
-
Hey, Scripting Guy! How can I boldface a specific word throughout a Microsoft Word document?
-- SB 
Hey, SB. You’ll probably find this hard to believe, but when you work with scripts and scripting all day you tend to get a little jaded. You say you have a script that automatically backs up and clears all the event logs on all your computers? How nice. You have a script that monitors folders and lets you know when new files are added? Been there, done that. You have a script that can bring the dead back to life? Hey, who doesn’t?
For some reason, though, SB, your question piqued our interest. Maybe it’s because we’re always looking for a script that uses Microsoft Word; scripting with Word is actually kind of fun. Or maybe it’s because this request offered a bit of a challenge: although we were pretty sure that we could write a script that would boldface specific words in a document we’d never actually tried it. Or maybe it’s because all the dead people we brought back to life have been pretty insistent that we go through their documents and boldface specific words. You know, things like the phrase I am too alive.
Oh, well. Regardless of what motivated us, here’s what we came up with: Const wdReplaceAll = 2
Set objWord = CreateObject("Word.Application")
objWord.Visible = True
Set objDoc = objWord.Documents.Open("C:\Scripts\Test.doc")
Set objSelection = objWord.Selection
objSelection.Find.Text = "Fabrikam"
objSelection.Find.Forward = TRUE
objSelection.Find.MatchWholeWord = TRUE
objSelection.Find.Replacement.Font.Bold = True
objSelection.Find.Execute ,,,,,,,,,,wdReplaceAll
As you can see, not only was this a fun script to write but - as an added bonus - it was an easy script to write, too. After defining a constant named wdReplaceAll (we’ll talk about that in a moment), we create an instance of the Word.Application object and then set the Visible property to True; this gives us an instance of Microsoft Word that we can see on screen. We use the Open method to open the document C:\Scripts\Test.doc, then create an instance of the Word Selection object (which, by default, positions the cursor at the beginning of the document).
Now we’re ready to roll. What we want to do is boldface any instances of the word Fabrikam; hence we set the value of the Find object’s Text property to “Fabrikam” (in other words, that’s what we’re searching for). Next we set the Forward property to True; this ensures that our search will start at the beginning of the document and end at, well, the end of the document. Finally, we set the MatchWholeWord property to True; we do this just in case there happens to be some crazy construction like FabrikamCorporation in the document. If MatchWholeWord was set to False, then any instance of the string Fabrikam would be boldfaced. That would give us something similar to this: FabrikamCorporation. Which we probably don’t want.
|
Note. Yes, we are kind of zipping through the explanation here, aren’t we? But that’s OK: after all, we have an Office Space article that discusses the process of finding and replacing text in more detail. |
All that gives us a mechanism for finding all instances of the word Fabrikam. But once we find them how do we boldface each of those instances? Here’s how: objSelection.Find.Replacement.Font.Bold = True
Yes, it’s that easy. Here we use the Replacement object (a child object of the Find object) and specify that the Font.Bold property should be True. That’s all we have to do. If we wanted to un-boldface each instance of Fabrikam we would set the Font.Bold property to False. Needless to say, we could specify new replacement text, a new replacement font size, pretty much anything we want.
See why we like scripting with Microsoft Word so much?
After configuring the replacement object we then call the Execute method to start the find-and-replace operation: objSelection.Find.Execute ,,,,,,,,,,wdReplaceAll
And, no, we didn’t fall asleep with our finger on the comma key. Well, not this time anyway. As it turns out, there are tons of optional parameters available for the Execute method. For this script we didn’t care about any of those. However, because parameters have to be specified in order, we couldn’t just do something like this: objSelection.Find.Execute wdReplaceAll
That would make the constant ReplaceAll (which tells Word we want to replace all instances of our search text) the first parameter passed to the Execute method, which would cause the script to fail. Hence all the “empty” parameters represented by the commas. And don’t worry: if that doesn’t make any sense to you, take a look at the documentation for the Execute method and you’ll see a list of all the parameters we skipped.
OK. That was fun, wasn’t it? In fact, we’d love to stay and chat about it some more, but Abe Lincoln and Genghis Khan are both out of iced tea. You know, writing a script to bring the dead back to life really wasn’t all it’s cracked up to be ….
|
-
Hey, Scripting Guy! I’m wondering if you can help me before I tear all my hair out. I’m trying to write a script using WMI that checks for a few core services such as antivirus, remote desktop software, and the like. The intention is to bolt this information onto another one of your (brilliant little) scripts I use to e-mail this information if certain things aren’t installed.
I’ve found a really convoluted script somewhere on the Internet that enumerates all services along with service users and a count of services at the end, but this is really overkill. I just want an “If exist” type script for a handful of services, which we can then run on all our PCs!
- RT 
Hi RT,
The Scripting Guide talks about listing the status of services, and probably is the kind of script you were railing against. People sometimes make a big deal about Windows PowerShell "one liners," but you can certainly use a more compact syntax within VBScript as well. The thing is, no one ever did. I actually came up with this type of syntax when I was writing the Microsoft Press book, Microsoft Windows Scripting with WMI: Self-Paced Learning Guide. I also used it quite a bit when I was writing scripts for the Windows Vista Resource Kit (First Edition). In that book, I was interested in printing out the user name and the computer name. Because the script might be run remotely, I could not use any of the properties from the WshShell object. You can also use something such as this VBScript to solve your problem.
|
|
|
|