
TechNet コラムへようこそ。このコラムでは、よく寄せられるシステム管理スクリプトに関する質問に Scripting Guys がお答えします。システム管理スクリプトについて質問がある場合は、scripter@microsoft.com (英語のみ) までお送りください。すべての質問に回答することはできないかもしれませんが、可能な限り対応いたします。
詳細情報
| • | |
| • | |
| • |
![]()
Scripting Guy さん、よろしくお願いします。ファイルはコピーしないで、フォルダのフォルダ構造 (つまり、フォルダとすべてのサブフォルダ) だけをコピーする方法はありますか。
-- AS

AS さん、こんにちは。このコラムを執筆している Scripting Guy はイタリア中を歩き回っていますが、今、彼はどうしているのかを聞かないことにはその日 1 日を送れない方はたくさんいらっしゃることでしょう。そんな方のために、最新情報をお知らせします。今日、Scripting Guy 一家は飛行機に乗り込んでベネチアに向かいます。では、このコラムを執筆している Scripting Guy はイタリア中を歩き回っていますが、今、彼はどうしているのかはまったく気にせずに (実は知りたくないので) ご自分の生活を送っている方はどうなるのでしょう。そんな方にも朗報があります。Scripting Guy 一家の旅行資金が底を突き始めてきたので、このコラムを執筆している Scripting Guy が旅を終えて、ヨーロッパのみやげ話をうるさいほど披露して、皆さんを疎ましく思わせなくなるのも時間の問題です。
その時点では、彼は、また息子の野球の試合の話をあれこれと、始めることでしょう。
それでは、ファイルをコピーしないでフォルダの構造をコピーする方法だけを知りたい方はどうでしょう。どうだと思いますか。そんな方にも朗報があります。まさにその処理を行うスクリプトを、たまたまご用意しました。
Hey, Scripting Guy! は、どんな方にもそれなりにお役に立つ日刊のコラムです。
お役に立つものとしては、次のようなスクリプトがあります。
On Error Resume Next
Dim arrFolders()
intSize = 0
strComputer = "."
Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2")
strFolderName = "c:\scripts"
GetSubFolders strFolderName
Sub GetSubFolders(strFolderName)
Set colSubfolders = objWMIService.ExecQuery _
("Associators of {Win32_Directory.Name='" & strFolderName & "'} " _
& "Where AssocClass = Win32_Subdirectory " _
& "ResultRole = PartComponent")
For Each objFolder in colSubfolders
strFolderName = objFolder.Name
ReDim Preserve arrFolders(intSize)
arrFolders(intSize) = strFolderName
intSize = intSize + 1
GetSubFolders strFolderName
Next
End Sub
Set objFSO = CreateObject("Scripting.FileSystemObject")
For Each strFolder in arrFolders
strFolderName = strFolder
strNewFolder = Replace(strFolderName, "c:\scripts", "c:\test")
Set objFolder = objFSO.CreateFolder(strNewFolder)
Next
もちろん、わかっています。これは、見た感じ恐ろいスクリプトですね。こうなってしまうのは、入れ子になっているサブフォルダを取得する単純明快な方法がないからです。ちなみに、入れ子になっているサブフォルダとは、フォルダの中にフォルダがあり、その下位フォルダの中にあるフォルダのことです (少なくとも「VBScript にはない」ということで、Windows PowerShell には、入れ子になったサブフォルダを取得する単純明快な方法があります)。したがって、再帰的なサブルーチンを使用する必要があります。再帰的なサブルーチンとは、それ自体を必要な回数呼び出すサブルーチンです。再帰的なサブルーチンは、あまり直感的に理解できるものではありません。そんなわけでコードも恐ろしげに見えるのです。
私たちからのアドバイスですか。再帰の詳細については、「Microsoft Windows 2000 Scripting Guide」のこのトピック (英語) を参照してください。これ以外の部分については、そう心配することはありません。このスクリプトをそのまま使用してください。ただし、必要に応じてフォルダ名を変更してください。
これから行う処理は、C:\Scripts 内のすべてのサブフォルダの名前 (厳密には、パス) を取得して、配列に格納し、配列内の値をループ処理して、フォルダ構造を C:\Test にコピーすることです。それを念頭において、まず arrFolders という名前の動的配列を初期化し、intSize という名前のカウンタ変数に値 0 を割り当てます。この処理を行うのが次のコード行です。
Dim arrFolders() intSize = 0
ローカル コンピュータの WMI サービスに接続したら、親フォルダのパス (C:\Scripts) を strFolderName という名前の変数に代入します。この後 (すぐに) 行う処理は、親フォルダの最上位のサブフォルダコレクションを返す、コードの実行です。最上位のサブフォルダとはどういうことか、ご説明しましょう。たとえば、次のようなフォルダ構造があるとします。
| • | C:\Scripts |
| • | C:\Scripts\Folder 1 |
| • | C:\Scripts\Folder 1\Folder A |
| • | C:\Scripts\Folder 2 |
| • | C:\Scripts\Folder 2\Folder B |
C:\Scripts が親フォルダであれば (実際にそうなのですが)、最上位のサブフォルダは Folder 1 と Folder 2 です。この 2 つのフォルダは、親フォルダから見て 1 レベル下のサブフォルダです。では Folder A と Folder B はどうでしょう。親フォルダから 2 レベルも入れ子になっています。そこが問題なのです。このように何重にも入れ子になっているサブフォルダに辿り着くの厄介です。そこで、再帰的なサブルーチンが必要になるのです。
おわかりいただけましたか。それは良かったです。ではここでちょっとお遊びです。
GetSubFolders strFolderName
そうですね。"お遊び" という表現は適切ではなかったかもしれません。「フォルダとサブフォルダの名前を取得する処理に戻りましょう」という表現の方が良さそうですね。次に実行するのは、再帰的なサブルーチン (GetSubFolders という名前のサブルーチン) を呼び出すことです。この際、親フォルダのパスをサブルーチンの唯一のパラメータとして渡しています。忘れてしまった方のために、GetSubFolders サブルーチンのコードを次に示します。
Sub GetSubFolders(strFolderName)
Set colSubfolders = objWMIService.ExecQuery _
("Associators of {Win32_Directory.Name='" & strFolderName & "'} " _
& "Where AssocClass = Win32_Subdirectory " _
& "ResultRole = PartComponent")
For Each objFolder in colSubfolders
strFolderName = objFolder.Name
ReDim Preserve arrFolders(intSize)
arrFolders(intSize) = strFolderName
intSize = intSize + 1
GetSubFolders strFolderName
Next
End Sub
そう、この辺から話が少しややこしくなりますね。このサブルーチンで最初に実行するのは、C:\Scripts の最上位のサブフォルダのコレクションを返す Associators Of クエリです。このクエリが C:\Scripts フォルダのサブフォルダを返すことが、どうしてわかるのでしょう。1 つ目の理由は、このクエリでは Win32_Subdirectory クラスのインスタンスが返されており、このインスタンスが、まさにサブフォルダであるからです。2 つ目の理由は、ここで親フォルダは必ず C:\Scripts であるとわかっているからです。というのも、これが strFolderName 変数の値だからです。
このような理由から、このクエリが C:\Scripts フォルダのサブフォルダを返すことがわかるのです。
既に説明したように、Associators Of クエリは C:\Scripts フォルダの最上位のサブフォルダのコレクションを返します (たとえば、C:\Scripts\Folder 1 と C:\Scripts\Folder 2)。次に、このサブフォルダのコレクションをループ処理する For Each ループを設定します。このループの内部では、次のコード行を使用して、フォルダの Name プロパティ (つまり、フォルダのパス名) を取得し、その値を strFolderName 変数に代入しています。
strFolderName = objFolder.Name
その後、次のコード ブロックを使用して、arrFolders 配列に格納されている既存のデータを維持しながら、この配列のサイズを変更します。最初のサブフォルダのパス (たとえば C:\Scripts\Folder 1) を配列の最初の要素 (要素 0) に割り当て、カウンタ変数 intSize の値を 1 つずつ増やします。
ReDim Preserve arrFolders(intSize) arrFolders(intSize) = strFolderName intSize = intSize + 1
カウンタ変数の値を 1 ずつ増加するのはなぜでしょう。その理由は明快です。ここまでの時点では、配列にフォルダを 1 つ追加し、その要素を要素 0 としました。配列に別のフォルダを追加する際には、そのフォルダは配列内の要素 1 として追加する必要があります。つまり、カウンタ変数 intSize の値も 1 である必要があります。
では、次の行に移りましょう。
GetSubFolders strFolderName
そうです、皆さんはこのコードに見覚えがあるはずです。再び GetSubFolders サブルーチンを呼び出します。今度はパラメータとして C:\Scripts\Folder 1 を渡しています。ご指摘のように、GetSubFolders サブルーチン内から、このサブルーチンを呼び出しています。これが再帰関数の性質です。ここでは、C:\Scripts\Folder 1 フォルダに対してサブルーチンを実行して、このフォルダの最上位のサブフォルダを取得しています。その後、このサブルーチンは他のフォルダに対しても実行され、それぞれにサブフォルダが含まれているかどうかを調べます。この再帰的なプロセスはフォルダ ツリー内で調査するフォルダがなくなるまで、つまりすべてのフォルダ名が arrFolders 配列に追加されるまで継続します。
そうですね。私たちも、この動作については考えるだけで頭痛がします。しかし幸運なことに、この動作については、それほど考える必要はありません。フォルダ構造のどこまで処理を終えていて、次はどのフォルダに対して実行するのかを追跡するなどの、つらい仕事は VBScript が代行します。
では、すべてのフォルダ パスを arrFolders 配列に追加し終えたらどうなるでしょうか。まず、Scripting.FileSystemObject オブジェクトのインスタンスを作成します。これは、フォルダを新規作成するのに使用します。次に、arrFolders 配列に格納された各ファイルパスに対して実行する For Each ループを設定します。フォルダ パスは次のようなものです。
| • | C:\Scripts\Folder 1 |
| • | C:\Scripts\Folder 1\Folder A |
| • | C:\Scripts\Folder 2 |
| • | C:\Scripts\Folder 2\Folder B |
言うまでもなく、これらは既存のフォルダのパスです。ここで必要な作業は、これらのフォルダをすべて C:\Test にコピーすることです。確かに、この作業を行う方法はいくつかあります。ここでは最も簡単な、Replace 関数を使用して C:\Scripts の各インスタンスを C:\Test で置き換える方法を採用しました。配列の最初の要素に格納されているのは、次のパスです。
C:\Test\Folder 1
さて、どうでしょう。これは偶然にも、C:\Scripts のフォルダ構造を C:\Test にコピーするのに必要なパスの 1 つです。つまり、ここで実行する必要があるのは、CreateFolder メソッドの呼び出しです (その際、この新しいフォルダ名をパラメータとして渡す必要があります)。このようにして、フォルダ構造の一部を、ファイルをコピーしないで C:\Test にコピーしました。その後、ループ処理の先頭に戻り、配列内の次のフォルダについても同じ処理を繰り返します。
すべての処理が完了すると、C:\Test には次のようなサブフォルダが作成されます。
| • | C:\Test\Folder 1 |
| • | C:\Test\Folder 1\Folder A |
| • | C:\Test\Folder 2 |
| • | C:\Test\Folder 2\Folder B |
C:\Test に作成されるべきサブフォルダがすべて作成されています。
最初にお知らせしたように、今日 Scripting Guy 一家はベネチアに向かいます。皆さん、ベネチアはご存知ですよね。干潟のちょうど真ん中に作られた都市です (ほとんどの建物は海底に突き立てた木の杭の上に建てられています)。20 世紀の間に、ほぼ 1 世紀という時間をかけて、ベネチアは干潟の上で少しずつ沈下が進みました。町の外郭部分と共に沈下した掘り抜き井戸が原因です。1960 年代に掘り抜き井戸は禁止となり、これによって町の沈下速度は落ちました。しかし、これで沈下が止まったかどうかについては、専門家の意見は今でも分かれたままです。
このような事情により、このコラムを執筆している Scripting Guy はベネチアという都市に非常に親近感を覚えています。無に向かってゆっくりと沈みつつあるものですよ。マイクロソフト社内での彼の昇進の見通しを言い表すのに、これよりも的確な表現はないと思います。