久々の記事です。更新なくてごめんなさい。
今回は、PowerShell でよく問題になる、ディレクトリ構造を保ったままの特定ファイルのコピーです。
続編について
この記事中の方法は効率面でよろしくないため、事前にコピー対象をフィルタしてから実行するものを用意しました。よろしければこちらもどうぞ
目次
- 続編について
- 目次
- Copy-Item を使ってフォルダを丸ごとコピーする
- 特定のファイルだけコピーする
- 複数のファイルをディレクトリ構造を平坦化してコピーする
- ディレクトリ構造を維持して特定のファイルをコピーしたい
- 応用
- まとめ
Copy-Item を使ってフォルダを丸ごとコピーする
PowerShell v3 において、Copy-Item を使うことで容易に可能です。
たとえば、c:\valentia というフォルダがこういった構造だとしましょう。
ディレクトリ: C:\valentia Mode LastWriteTime Length Name ---- ------------- ------ ---- d---- 2014/08/11 18:59 cs d---- 2014/08/11 18:59 example d---- 2014/08/11 18:59 functions d---- 2014/08/11 18:59 Test d---- 2014/08/11 18:59 Tools d---- 2014/08/11 18:59 valentia-Help -a--- 2014/08/11 2:33 4532 valentia.psd1 -a--- 2014/08/11 2:33 22031 valentia.psm1 -a--- 2014/08/11 2:33 15665 valentia.pssproj ディレクトリ: C:\valentia\cs Mode LastWriteTime Length Name ---- ------------- ------ ---- -a--- 2014/08/11 2:33 3815 CredRead.cs -a--- 2014/08/11 2:33 670 CredWrite.cs ~~中略~~ ディレクトリ: C:\valentia\Tools Mode LastWriteTime Length Name ---- ------------- ------ ---- -a--- 2014/08/11 2:33 313 install.bat -a--- 2014/08/11 2:33 10129 install.ps1 -a--- 2014/08/11 2:33 3755 New-valentiaManufest.ps1 -a--- 2014/08/11 2:33 7580 RemoteInstall.ps1 ディレクトリ: C:\valentia-Help Mode LastWriteTime Length Name ---- ------------- ------ ---- -a--- 2014/08/11 2:33 902 valentia.psm1.xml
このフォルダを、まるまる c:\hoge に移動したい場合は、単純です。
Copy-Item に -Recurse スイッチを付けて、対象のパスを指定するだけで完了です。簡単ですね。
Copy-Item -Path C:\valentia -Destination c:\hoge -Recurse
特定のファイルだけコピーする
では、特定のファイルだけ、コピーしてみましょう。
初めに、ディレクトリ構造を維持せずに、そのファイルを指定先にコピーする場合です。
例えば、C:\valentia\Tools\install.bat を c:\hoge にコピーしてみるならこうですか?
Copy-Item -Path C:\valentia\Tools\install.bat -Destination c:\hoge
もし、C:\hoge が存在するフォルダならこれでいいです。
ディレクトリ: C:\hoge Mode LastWriteTime Length Name ---- ------------- ------ ---- -a--- 2014/08/11 2:33 313 install.bat
しかし、もし存在しないフォルダだった場合はダメです。 c:\hoge という「ファイル名」でコピーされてしまいます。
ここで、コピー先を c:\hoge\install.bat とフルパスで指定すると、
Copy-Item -Path C:\valentia\Tools\install.bat -Destination c:\hoge\install.bat
c:\hoge がない状態からダメだよと怒られます。
Copy-Item : パス 'C:\hoge\install.bat' の一部が見つかりませんでした。 発生場所 行:1 文字:1 + Copy-Item -Path C:\valentia\Tools\install.bat -Destination c:\hoge\install.bat + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + CategoryInfo : NotSpecified: (:) [Copy-Item], DirectoryNotFoundException + FullyQualifiedErrorId : System.IO.DirectoryNotFoundException,Microsoft.PowerShell.Commands.CopyItemCommand
つまり、コピー先を保証する必要があるのです。そのため、先にコピー先フォルダを作成しておくのがいいでしょう。
$destinationFolder = "c:\hoge" New-Item -Path $destinationFolder -ItemType Directory -Force Copy-Item -Path C:\valentia\Tools\install.bat -Destination c:\hoge
New-Item でのディレクトリ作成は、mkdir
でも問題ありません。また、ここで -Force を付けることで、すでに存在していてもエラーを出すことなくディレクトリ生成を保証できます。
うまくコピーできましたね。
複数のファイルをディレクトリ構造を平坦化してコピーする
特定のフォルダにあるファイルを、階層を問わず平坦化してコピーするのも容易です。やる必要があるかは別として。
ようは、コピー元がなんであっても、コピー先が同一フォルダとすればいいのです。つまりこうです。
ls C:\valentia -Recurse | Copy-Item -Destination c:\hoge -Force
あるいは、Foreach-Object で展開しても一緒です。
ls C:\valentia -Recurse | %{Copy-Item -Path .FullName -Destination c:\hoge -Force}
ここで -Force をつけているのは、コピー元がコピー先に平坦化されるため、同一ファイル名/ディレクトリ名のアイテムができてしまうからです。
-Force を付けなければエラーがでます。
一方で、-Force を付ければ、上書きを自動実行します。
コピーされた結果は、まさに平坦です。元のディレクトリ構造を無視して、コピー先にしていしたパスにすべて展開します。もし、ディレクトリ構造を維持してコピーすると思った場合は驚きだと思います。
ディレクトリ構造を維持して特定のファイルをコピーしたい
ではどうやればいいのでしょうか?試行錯誤してみますか?
指定したファイルだけにしたらどうか
つまり、ファイルをあらかじめ絞ってしまうのですね。
ls C:\valentia -Recurse | where Name -eq "install.bat"
結果こうです。
ディレクトリ: C:\valentia\Tools Mode LastWriteTime Length Name ---- ------------- ------ ---- -a--- 2014/08/11 2:33 313 install.bat
あとは、コピー元パスとコピー先のフォルダパスを文字列をいじって差し替えて
$directoryName = Join-Path "c:\hoge" ($_.DirectoryName -split "c:\\valentia" |select -Last 1)
コピー先のps1フルパスを生成します。
Join-Path $directoryName $_.Name
先ほどお伝えした通り、コピー先へのディレクトリ構造を再現しないと起こられるのでそれもやってしまいます。
ということで、コード全文です。
$hoge = ls C:\valentia -Recurse | where {$_.Extension -eq ".ps1"} $hoge ` | %{ $directoryName = Join-Path "c:\hoge" ($_.DirectoryName -split "c:\\valentia" |select -Last 1) [PSCustomObject]@{ Path = $_.FullName DirectoryName = $directoryName Destination = Join-Path $directoryName $_.Name }} ` | %{ New-Item $_.DirectoryName -ItemType Directory -Force Copy-Item -Path $_.Path -Destination $_.Destination -Force }
結果です。うまくいってますね。
ディレクトリ: C:\hoge Mode LastWriteTime Length Name ---- ------------- ------ ---- d---- 2014/08/12 6:29 example d---- 2014/08/12 6:29 functions d---- 2014/08/12 6:29 Test d---- 2014/08/12 6:29 Tools ~~中略~~ ディレクトリ: C:\hoge\Test Mode LastWriteTime Length Name ---- ------------- ------ ---- d---- 2014/08/12 6:29 Credential ディレクトリ: C:\hoge\Test\Credential Mode LastWriteTime Length Name ---- ------------- ------ ---- -a--- 2014/08/11 2:33 128 Credential.ps1 -a--- 2014/08/11 2:33 65 Import-Module.ps1 -a--- 2014/08/11 2:33 146 PingAsync.ps1 -a--- 2014/08/11 2:33 226 Target.ps1 ディレクトリ: C:\hoge\Tools Mode LastWriteTime Length Name ---- ------------- ------ ---- -a--- 2014/08/11 2:33 10129 install.ps1 -a--- 2014/08/11 2:33 3755 New-valentiaManufest.ps1 -a--- 2014/08/11 2:33 7580 RemoteInstall.ps1
簡単にfunction にしてみましょう。
function Copy-StrictItemWithDirectoryStructure { [cmdletBinding()] param ( [parameter( Mandatory = 1, Position = 0, ValueFromPipeline = 1, ValueFromPipelineByPropertyName =1)] $inputPath, [parameter( Mandatory = 1, Position = 1, ValueFromPipelineByPropertyName =1)] [string[]] $Destination, [parameter( Mandatory = 1, Position = 1, ValueFromPipelineByPropertyName =1)] [string] $InputRoot ) begin { $root = $InputRoot.Replace("\", "\\") } process { $inputPath ` | %{ $directoryName = Join-Path $Destination ($_.DirectoryName -split $root |select -Last 1) [PSCustomObject]@{ Path = $_.FullName DirectoryName = $directoryName Destination = Join-Path $directoryName $_.Name }} ` | %{ New-Item $_.DirectoryName -ItemType Directory -Force Copy-Item -Path $_.Path -Destination $_.Destination -Force } } }
先ほどの例がこうなります。
$hoge = ls C:\valentia -Recurse | where {$_.Extension -eq ".ps1"} Copy-StrictItemWithDirectoryStructure -inputPath $hoge -Destination c:\hoge -InputRoot c:\valentia
このやり方のメリットは、
- 初めに必要なファイルをふつーにフィルタリングできる
- ディレクトリ構造を複数回なめてない
デメリットは、
- コピー元のフィルタを外に出しているので、コピー元が特定できず-InputRoot という感じで渡す必要がある
- もとディレクトリ構造をフルネームでテキスト置換をかけててダサすぎる
まぁできないとかいうのを封じるには容易なやり方なわけですが。
他の方法を考えましょう。
一回コピーしてからいらないものを削除してはどうか
ということで、今回の本題はこれです。
もともとは、とある人が困ってたのでそういえばやったことなかったと思って対応したものです。
コードから見てみましょう。
function Copy-StrictedFilterFileWithDirectoryStructure { [CmdletBinding()] param ( [parameter( mandatory = 1, position = 0, ValueFromPipeline = 1, ValueFromPipelineByPropertyName = 1)] [string] $Path, [parameter( mandatory = 1, position = 1, ValueFromPipelineByPropertyName = 1)] [string] $Destination, [parameter( mandatory = 1, position = 2, ValueFromPipelineByPropertyName = 1)] [string[]] $Targets, [parameter( mandatory = 0, position = 3, ValueFromPipelineByPropertyName = 1)] [string[]] $Excludes ) begin { $list = New-Object 'System.Collections.Generic.List[String]' } process { Foreach ($target in $Targets) { # Copy "All Directory Structure" and "File" which Extension type is $ex Copy-Item -Path $Path -Destination $Destination -Force -Recurse -Filter $target } } end { # Remove -Exclude Item Foreach ($exclude in $Excludes) { Get-ChildItem -Path $Destination -Recurse -File | where Name -like $exclude | Remove-Item } # search Folder which include file $allFolder = Get-ChildItem $Destination -Recurse -Directory $containsFile = $allFolder | where {$_.GetFiles()} $containsFile.FullName ` | %{ $fileContains = $_ $result = $allFolder.FullName ` | where {$_ -notin $list} ` | where { $shortPath = $_ $fileContains -like "$shortPath*" } $result | %{$list.Add($_)} } $folderToKeep = $list | sort -Unique # Remove All Empty (none file exist) folders Get-ChildItem -Path $Destination -Recurse -Directory | where fullName -notin $folderToKeep | Remove-Item -Recurse } }
先ほどと大きな違いがあります。
- フィルタ処理が内部に組まれている
- フィルタがワイルドカードでファイル名で指定可能 (つまり拡張子なども)
- いったん必要なフィルタ結果と空ディレクトリをディレクトリ構造ごとコピーしている
- いらないファイルをExcludeで追加していして削除できる
- あとからコピー先の空ディレクトリを削除している
何度もディレクトリ構造をなめていますが、こちらは文字列処理を回避しています。膨大な大きさでなければ大きな遅延はないので、まぁ。
では、先ほどの .ps1ファイルのみをコピーしてみましょう。
Copy-StrictedFilterFileWithDirectoryStructure -Path c:\valentia -Destination C:\hoge -Targets *.ps1
簡単ですね。
.md もコピーしてる時に、一部Readme*.md
ファイルのみをコピーから除きたい場合でも簡単です。
Copy-StrictedFilterFileWithDirectoryStructure -Path C:\valentia -Destination C:\hoge -Targets *.ps1, *.md -Excludes Readme*.md
個人的にはこちらが好みです。
応用
これを欲しがった人は、リリース用に必要なファイルを集めてzipしたかったようです。
彼のように、開発者にとっても便利に使えればうれしいと思います。
まとめ
もうこれでPowerShell の コピーが辛いとはいわせない?