tech.guitarrapc.cóm

Technical updates

PowerShell でディレクトリ構造を保ったまま特定のファイルをコピーする (2)

前回の続きです。

今回は、コメントをいただいた箇所を対応してみましょう。

最後の二つ、比較がフェアではないですな…。

つまり

ヴァニシング・あえとす (@aetos382) August 13, 2014

ご指摘の通りです。 基本的には、文字列での差し替えはあまり好みでなかったのでてけとーに比較対象としました

Copy-StrictedFilterFileWithDirectoryStructure.ps1

は、パスをコピーしてから、いらないものを削除のためディレクトリを何度も舐めています。 単純にフィルタの有無の問題ではないよね。ということで、対応しましょう。

対応

単純に、2つ目のCopy-StrictedFilterFileWithDirectoryStructure.ps1と同様のフィルタを1つ目のCopy-StrictItemWithDirectoryStructure.ps1に組むだけです。簡単です。

適当に実装してみましょう。

利用方法

PowerShellで一番使うのはHelpです。

PS> help Copy-ItemEX -Full
NAME
    Copy-ItemEX

SYNTAX
    Copy-ItemEX [-Path] <string> [-Destination] <string> [[-Targets] <string[]>] [[-Excludes] <string[]>] [[-Recurse]]
     [[-Force]] [[-WhatIf]]  [<CommonParameters>]


PARAMETERS
    -Destination <string>

        Required?                    true
        Position?                    1
        Accept pipeline input?       true (ByPropertyName)
        Parameter set name           (All)
        Aliases                      None
        Dynamic?                     false

    -Excludes <string[]>

        Required?                    false
        Position?                    3
        Accept pipeline input?       true (ByPropertyName)
        Parameter set name           (All)
        Aliases                      None
        Dynamic?                     false

    -Force

        Required?                    false
        Position?                    5
        Accept pipeline input?       false
        Parameter set name           (All)
        Aliases                      None
        Dynamic?                     false

    -Path <string>

        Required?                    true
        Position?                    0
        Accept pipeline input?       true (ByValue, ByPropertyName)
        Parameter set name           (All)
        Aliases                      PSParentPath
        Dynamic?                     false

    -Recurse

        Required?                    false
        Position?                    4
        Accept pipeline input?       true (ByPropertyName)
        Parameter set name           (All)
        Aliases                      None
        Dynamic?                     false

    -Targets <string[]>

        Required?                    false
        Position?                    2
        Accept pipeline input?       true (ByPropertyName)
        Parameter set name           (All)
        Aliases                      None
        Dynamic?                     false

    -WhatIf

        Required?                    false
        Position?                    6
        Accept pipeline input?       false
        Parameter set name           (All)
        Aliases                      None
        Dynamic?                     false

    <CommonParameters>
        This cmdlet supports the common parameters: Verbose, Debug,
        ErrorAction, ErrorVariable, WarningAction, WarningVariable,
        OutBuffer, PipelineVariable, and OutVariable. For more information, see
        about_CommonParameters (https://go.microsoft.com/fwlink/?LinkID=113216).


INPUTS
    System.String
    System.String[]
    System.Management.Automation.SwitchParameter


OUTPUTS
    System.Object

ALIASES
    None


REMARKS
    None

基本的な利用方法

前回の2番目に紹介したCopy-StrictedFilterFileWithDirectoryStructure.ps1と同じように利用できます。

例えば、 c:\valentia\valentia\Toolsから.ps1がつくファイル(と、そのディレクトリ構造)だけc:\hogeにコピーするならそのままこうです。

Targetsはファイル名で指定できるので、ワイルドカード*も使えますよ?

Copy-ItemEX -Path c:\valentia\valentia\Tools -Destination C:\hoge -Targets *.ps1

デフォルトでは、コピーするか確認されます。

image

Yesを選択すると、そのファイルだけコピーされて、他のファイルでもまた聞かれます。

Yes to Allを選択すると、以降のファイルも含めてすべて確認ダイアログが出ずにコピーされます。

NoやNo to AllはYesの逆です。

もし該当フォルダに対象のファイルがなければエラーになります。

image

確認ダイアログを省略する

確認ダイアログを出さずに一気にコピーしたい場合は、 -Forceスイッチを付けてください。

Copy-ItemEX -Path c:\valentia\valentia\Tools -Destination C:\hoge -Targets *.ps1 -Force

対象フォルダのディレクトリツリーを舐めてコピーする

また、 c:\valentiaのフォルダから奥の階層もそのまま取得するなら-Recurseスイッチを付けてください。

Copy-ItemEX -Path c:\valentia -Destination C:\hoge -Targets *.ps1 -Recurse

コピーを実行せずどうなるかだけ確認する

もし、コピーせずにどうなるかを試すなら-WhatIfを使います。もちろん-Recurseとも併用できます。

Copy-ItemEX -Path c:\valentia -Destination C:\hoge -Targets *.ps1 -Recurse -WhatIf

特定のファイルを除く

例えば、*.ps1をコピーするが、install.ps1は除外なら-Excludesに指定しています。

Copy-ItemEX -Path c:\valentia -Destination C:\hoge -Targets *.ps1 -Excludes install.ps1 -Recurse

はい、簡単。ファイル名で指定できるので、ワイルドカード`*も使えます。

パイプライン入力への対応

対応しています。

内容

説明するまでもない簡単なコードです。

process{} 節で必要な処理がされています。

  1. 必要なファイルのフィルタ
  2. いらないファイルの除去
  3. コピーに実施

以上です。基本的には、[I/O.FileInfo]と[I/O.DicretoryInfo]のみに対応しているので他のプロバイダはシリマセン。

まとめ

Tuple可愛いよTuple。

PowerShell で シンボリックリンクを 使えるようにしよう

WindowsはVista以降にシンボリックリンクが利用可能になりました。

いやはやほんと遅い、やっとです。

ということで、PowerShellでシンボリックリンクを扱ってみたいですよね? 扱いたいなら書けばいいんです。

ジャンクション、ハードリンクと シンボリックリンクの違い

これまでも使えた、ジャンクションとハードリンクはシンボリックリンクとどのように違うのか把握しておきましょう。

PowerShell でシンボリックリンクを扱う

v4までは標準コマンドレットでサポートされてなく、v5 では管理者権限が必要でした。 v6 (PowerShell Core) では、Windowsにおいてもmklink 同様に開発者モードが有効ならユーザー権限で実行できるようになっています。

本当にPowerShell Coreのほうが格段に使いやすいので検討されるといいでしょう。私は非常に限定されたシーンでしかWindows PowerShellを起動しなくなりました。

標準Cmdlet のシンボリックリンク処理

v5からNew-Itemでシンボリックリンクを作成できるようになりました。これは改めて記事で紹介しました。

http://www.buildinsider.net/enterprise/powershelldsc/03

では、PowerShell 6.2 でユーザー権限でシンボリックリンクを作ってみましょう。~/,gitconfig へシンボリックリンクした ~/.gitconfig.localファイルを作るならこうです。

New-Item -Type SymbolicLink ~\.gitconfig.local -Value .gitconfig

シンボリックリンクの削除がRemove-Itemなので、実ファイルとリンクファイルを間違えないように気を付けてください。lnのようにunlinkがあると安全ですがそうではない。

Windows でシンボリックリンクは ln ではない。

で、lnでいけるのか? 残念、Windowsではmklinkコマンドです。別にいいでしょう。

せめて、PowerShellからmklinkでよべるのかというと、まさかのNoです。

PowerShellからmklinkを呼ぶには、cmd /c "mklink"とします。

つまり、 mklinkというコマンドが良く知られていますが、 cmd.exeに実装されているため、PowerShellから直接呼べません。

Remove-Item が使えない

v4において、シンボリックリンク処理がないのはともかく削除のハンドルもできないのは苦しいです。cmdでシンボリックリンクを消したいと思った時、対象がフォルダへのReparse Pointならrmdir、ファイルならdelで大丈夫でした。

ところが、PowerShellでフォルダシンボリックに対してRemove-Item -Recurseするとシンボリックリンク先のアイテムを消します。操作ミスを容易に誘発するので、これは結構いやな制約です。

つまり、v4では標準Cmdletがシンボリックリンクに対応していません。

.NETで処理する

cmdで呼び出すとかはいいんですが、失敗時の処理が面倒なので好みではありません。

PSCXはオワコンなので使うのやめましょう。

.NETで簡単に書けるんだから、自分で書けばいいんですよ。

Get処理

これは、FileInfoDirectoryInfoからふつーにAttributesを取得すれば問題ありません。

シンボリックリンクは、 System.IO.FileAttributesからReparsePointとして取得できます。

function IsFileReparsePoint &#40[System.IO.FileInfo]$Path&#41
{
    Write-Verbose &#40'File attribute detected as ReparsePoint'&#41
    $fileAttributes = [System.IO.FileAttributes]::Archive, [System.IO.FileAttributes]::ReparsePoint -join ', '
    $attribute = [System.IO.File]::GetAttributes&#40$Path&#41
    $result = $attribute -eq $fileAttributes
    if &#40$result&#41
    {
        Write-Verbose &#40'Attribute detected as ReparsePoint. : {0}' -f $attribute&#41
        return $result
    }
    else
    {
        Write-Verbose &#40'Attribute detected as NOT ReparsePoint. : {0}' -f $attribute&#41
        return $result
    }
}

Remove処理

幸いにして、.NET Frameworkでは、シンボリックリンクの削除はSystem.IO.FileのDeleteメソッドやSystem.IO.DirectoryのDeleteメソッドでふつーに処理できます。

The behavior of this method differs slightly when deleting a directory that contains a reparse point, such as a symbolic link or a mount point. If the reparse point is a directory, such as a mount point, it is unmounted and the mount point is deleted. This method does not recurse through the reparse point. If the reparse point is a symbolic link to a file, the reparse point is deleted and not the target of the symbolic link.

ということで、Removeは問題ありませんね。

function RemoveFileReparsePoint &#40[System.IO.FileInfo]$Path&#41
{
    [System.IO.File]::Delete&#40$Path.FullName&#41
}

function RemoveDirectoryReparsePoint &#40[System.IO.DirectoryInfo]$Path&#41
{
    [System.IO.Directory]::Delete&#40$Path.FullName&#41
}

Set処理

シンボリックリックを作る処理だけは、 P/Invoke が必要なのでしれっとやります。

internal static class Win32
{
    [DllImport&#40"kernel32.dll", SetLastError = true&#41]
    [return: MarshalAs&#40UnmanagedType.I1&#41]
    public static extern bool CreateSymbolicLink&#40string lpSymlinkFileName, string lpTargetFileName, SymLinkFlag dwFlags&#41;

    internal enum SymLinkFlag
    {
        File = 0,
        Directory = 1
    }
}
public static void CreateSymLink&#40string name, string target, bool isDirectory = false&#41
{
    if &#40!Win32.CreateSymbolicLink&#40name, target, isDirectory ? Win32.SymLinkFlag.Directory : Win32.SymLinkFlag.File&#41&#41
    {
        throw new System.ComponentModel.Win32Exception&#40&#41;
    }
}

これで必要な処理は集まりました。あとは書くだけです。

コード

GitHubで公開しておきます。valentiaにも組み込まれているのでぜひどうぞ。

それぞれのコードはGistでも置いておきましょう。

https://gist.github.com/guitarrapc/edfd466d2cb2a0deeea7

https://gist.github.com/guitarrapc/731e2ed15f9f3a17afe4

https://gist.github.com/guitarrapc/bae12eb263dcc784df74

使い方

簡単にまとめます。シンボリックリンクは、管理者で実行してください (ユーザー権限では実行できません)

  • Getでシンボリックリンクがあった場合に、その情報を取得
  • Removeで、対象のシンボリックリンクを安全に削除
  • Setで、シンボリックリンクを作成

ちなみにシンボリックリンクは対象のパスが存在しなくてもリンクを作れます。

Set-SymbolicLinkは、-ForceFile $trueとすると、ファイルシンボリックリンクをリンク対象ファイルがなくても指定したパスに作れます。

フォルダの場合は、-ForceDirectory $true としてください。

もし両方がついていない場合は、対象パスが存在するときだけ、シンボリックリンクを作成できます。

まとめ

P/Invoke可愛い、P/Invoke。