Utils/cp.ps1
|
Register-GnuUtil @{ Name = 'cp' Category = 'File and directory operations' Synopsis = 'Copy files and directories.' Usage = @( 'cp [OPTION]... [-T] SOURCE DEST', 'cp [OPTION]... SOURCE... DIRECTORY', 'cp [OPTION]... -t DIRECTORY SOURCE...' ) Description = @('Copy SOURCE to DEST, or multiple SOURCE(s) to DIRECTORY.') FileOperands = $true Options = @( @{ Short = 'a'; Long = 'archive'; Key = 'deref'; Value = 'never'; Sets = @{ recursive = 1; preserve = 'all' }; Help = 'same as -dR --preserve=all' } @{ Short = 'd'; Key = 'deref'; Value = 'never'; Sets = @{ 'preserve-links' = 1 }; Help = 'same as --no-dereference --preserve=links' } @{ Short = 'f'; Long = 'force'; Key = 'clobber'; Value = 'force'; Help = 'if an existing destination file cannot be opened, remove it and try again' } @{ Short = 'i'; Long = 'interactive'; Key = 'clobber'; Value = 'ask'; Help = 'prompt before overwrite (overrides a previous -n option)' } @{ Short = 'l'; Long = 'link'; Key = 'make'; Value = 'hard'; Help = 'hard link files instead of copying' } @{ Short = 'L'; Long = 'dereference'; Key = 'deref'; Value = 'always'; Help = 'always follow symbolic links in SOURCE' } @{ Short = 'n'; Long = 'no-clobber'; Key = 'clobber'; Value = 'no'; Help = 'do not overwrite an existing file (overrides a previous -i option)' } @{ Short = 'P'; Long = 'no-dereference'; Key = 'deref'; Value = 'never'; Help = 'never follow symbolic links in SOURCE' } @{ Short = 'p'; Key = 'preserve'; Value = 'mode,ownership,timestamps'; Help = 'same as --preserve=mode,ownership,timestamps' } @{ Long = 'preserve'; Arg = 'ATTR_LIST'; OptionalArg = $true; Help = 'preserve the specified attributes (default: mode,ownership,timestamps), if possible additional attributes: context, links, xattr, all' } @{ Long = 'no-preserve'; Arg = 'ATTR_LIST'; Help = "don't preserve the specified attributes" } @{ Short = 'R'; Key = 'recursive'; Help = 'copy directories recursively' } @{ Short = 'r'; Long = 'recursive'; Help = 'copy directories recursively' } @{ Short = 's'; Long = 'symbolic-link'; Key = 'make'; Value = 'symbolic'; Help = 'make symbolic links instead of copying' } @{ Short = 't'; Long = 'target-directory'; Arg = 'DIRECTORY'; Help = 'copy all SOURCE arguments into DIRECTORY' } @{ Short = 'T'; Long = 'no-target-directory'; Help = 'treat DEST as a normal file' } @{ Short = 'u'; Long = 'update'; Help = 'copy only when the SOURCE file is newer than the destination file or when the destination file is missing' } @{ Short = 'v'; Long = 'verbose'; Help = 'explain what is being done' } ) Notes = @( 'By default, sparse SOURCE files are detected by a crude heuristic and the', 'corresponding DEST file is made sparse as well.', '', 'PowerShell notes: "timestamps" and "mode" (file attributes on Windows, permissions on Unix) can be', 'preserved; ownership, context and xattr are accepted and ignored. Without -L, -r copies symbolic', 'links and junctions as links. Backups (-b, -S) and --parents are not supported.' ) } function script:Invoke-GnuCopy { <# .SYNOPSIS Implementation of cp: validates options and copies every planned pair. #> param([Parameter(Mandatory)][hashtable]$Context) $recursive = Test-GnuOption -Context $Context -Key 'recursive' $preserveList = @() if (Test-GnuOption -Context $Context -Key 'preserve') { $value = [string](Get-GnuOptionValue -Context $Context -Key 'preserve') if ([string]::IsNullOrEmpty($value)) { $value = 'mode,ownership,timestamps' } $preserveList = @($value.Split(',')) } foreach ($attribute in $preserveList) { if (@('mode', 'ownership', 'timestamps', 'links', 'context', 'xattr', 'all') -notcontains $attribute) { Write-GnuUsageError -Context $Context -Message "invalid argument '$attribute' for '--preserve'" return } } $removed = @() if (Test-GnuOption -Context $Context -Key 'no-preserve') { $removed = @(([string](Get-GnuOptionValue -Context $Context -Key 'no-preserve')).Split(',')) } $wants = { param($Name) ($preserveList -contains $Name -or $preserveList -contains 'all') -and $removed -notcontains $Name -and $removed -notcontains 'all' } $default = 'always' if ($recursive) { $default = 'never' } $settings = @{ Recursive = $recursive Clobber = [string](Get-GnuOptionValue -Context $Context -Key 'clobber' -Default 'overwrite') Update = Test-GnuOption -Context $Context -Key 'update' Verbose = Test-GnuOption -Context $Context -Key 'verbose' Make = [string](Get-GnuOptionValue -Context $Context -Key 'make' -Default 'copy') Follow = ([string](Get-GnuOptionValue -Context $Context -Key 'deref' -Default $default)) -eq 'always' Timestamps = & $wants 'timestamps' Mode = & $wants 'mode' Output = New-Object 'System.Collections.Generic.List[string]' } foreach ($pair in @(Get-GnuTransferPlan -Context $Context)) { $sourcePath = Resolve-GnuPath -Path $pair.Source $targetPath = Resolve-GnuPath -Path $pair.Target $source = Get-Item -LiteralPath $sourcePath -Force -ErrorAction SilentlyContinue if ($null -eq $source) { Write-GnuError -Context $Context -Message "cannot stat '$($pair.Source)': No such file or directory" continue } $isDirectory = $source -is [System.IO.DirectoryInfo] -and ($settings.Follow -or $null -eq (Get-GnuLinkTarget -Path $sourcePath)) if ($isDirectory -and -not $recursive) { Write-GnuError -Context $Context -Message "-r not specified; omitting directory '$($pair.Source)'" continue } if ($isDirectory -and (Test-GnuInsidePath -Path $targetPath -Ancestor $sourcePath)) { Write-GnuError -Context $Context -Message "cannot copy a directory, '$($pair.Source)', into itself, '$($pair.Target)'" continue } Copy-GnuCpItem -Context $Context -Source $source -SourceDisplay $pair.Source -TargetPath $targetPath -TargetDisplay $pair.Target -Settings $settings foreach ($line in $settings.Output) { $line } $settings.Output.Clear() } } function script:Copy-GnuCpItem { <# .SYNOPSIS Copies one file, link or directory tree (recursively), reporting GNU style errors. #> param( [Parameter(Mandatory)][hashtable]$Context, [Parameter(Mandatory)][System.IO.FileSystemInfo]$Source, [Parameter(Mandatory)][string]$SourceDisplay, [Parameter(Mandatory)][string]$TargetPath, [Parameter(Mandatory)][string]$TargetDisplay, [Parameter(Mandatory)][hashtable]$Settings ) $linkTarget = Get-GnuLinkTarget -Path $Source.FullName $target = Get-Item -LiteralPath $TargetPath -Force -ErrorAction SilentlyContinue try { if ($null -ne $linkTarget -and -not $Settings.Follow) { if ($null -ne $target) { if ($Settings.Clobber -eq 'no') { return } if ($target -is [System.IO.DirectoryInfo]) { [System.IO.Directory]::Delete($TargetPath, $false) } else { [System.IO.File]::Delete($TargetPath) } } if ($Source.LinkType -eq 'Junction') { $null = New-Item -ItemType Junction -Path ([System.Management.Automation.WildcardPattern]::Escape($TargetPath)) -Value $linkTarget -ErrorAction Stop } else { New-GnuSymbolicLink -Path $TargetPath -Target $linkTarget -Directory:($Source -is [System.IO.DirectoryInfo]) } if ($Settings.Verbose) { $Settings.Output.Add("'$SourceDisplay' -> '$TargetDisplay'") } return } if ($Source -is [System.IO.DirectoryInfo]) { if ($null -ne $target -and $target -isnot [System.IO.DirectoryInfo]) { Write-GnuError -Context $Context -Message "cannot overwrite non-directory '$TargetDisplay' with directory '$SourceDisplay'" return } if ($null -eq $target) { $null = [System.IO.Directory]::CreateDirectory($TargetPath) if ($Settings.Verbose) { $Settings.Output.Add("'$SourceDisplay' -> '$TargetDisplay'") } } $entries = @([System.IO.Directory]::GetFileSystemEntries($Source.FullName)) [System.Array]::Sort($entries, [System.StringComparer]::Ordinal) foreach ($entry in $entries) { $child = Get-Item -LiteralPath $entry -Force Copy-GnuCpItem -Context $Context -Source $child -SourceDisplay (Join-GnuDisplayPath -Directory $SourceDisplay -Leaf $child.Name) ` -TargetPath ([System.IO.Path]::Combine($TargetPath, $child.Name)) -TargetDisplay (Join-GnuDisplayPath -Directory $TargetDisplay -Leaf $child.Name) -Settings $Settings } if ($Settings.Mode) { [System.IO.File]::SetAttributes($TargetPath, $Source.Attributes) } if ($Settings.Timestamps) { [System.IO.Directory]::SetLastWriteTime($TargetPath, $Source.LastWriteTime) [System.IO.Directory]::SetLastAccessTime($TargetPath, $Source.LastAccessTime) } return } $sourceFile = $Source.FullName if ($null -ne $target) { if (Test-GnuSamePath -Left $sourceFile -Right $TargetPath) { Write-GnuError -Context $Context -Message "'$SourceDisplay' and '$TargetDisplay' are the same file" return } if ($target -is [System.IO.DirectoryInfo] -and $null -eq (Get-GnuLinkTarget -Path $TargetPath)) { Write-GnuError -Context $Context -Message "cannot overwrite directory '$TargetDisplay' with non-directory" return } if ($Settings.Clobber -eq 'no') { return } if ($Settings.Update -and $target.LastWriteTime -ge $Source.LastWriteTime) { return } if ($Settings.Clobber -eq 'ask' -and -not (Read-GnuConfirmation -Context $Context -Question "overwrite '$TargetDisplay'?")) { return } } switch ($Settings.Make) { 'hard' { if ($null -ne $target) { [System.IO.File]::Delete($TargetPath) } $null = New-Item -ItemType HardLink -Path ([System.Management.Automation.WildcardPattern]::Escape($TargetPath)) -Value $sourceFile -ErrorAction Stop } 'symbolic' { if ($null -ne $target) { [System.IO.File]::Delete($TargetPath) } New-GnuSymbolicLink -Path $TargetPath -Target $sourceFile } default { try { [System.IO.File]::Copy($sourceFile, $TargetPath, $true) } catch { # GNU -f: remove a destination that cannot be opened and try again. if ($Settings.Clobber -ne 'force' -or $null -eq $target) { throw } $target.Attributes = [System.IO.FileAttributes]::Normal [System.IO.File]::Delete($TargetPath) [System.IO.File]::Copy($sourceFile, $TargetPath, $true) } # File.Copy keeps the attributes (like GNU keeps the mode bits) and, on Windows, the modification # time; GNU cp keeps the times only with --preserve=timestamps. $now = [datetime]::Now if ($Settings.Timestamps) { [System.IO.File]::SetLastWriteTime($TargetPath, $Source.LastWriteTime) [System.IO.File]::SetLastAccessTime($TargetPath, $Source.LastAccessTime) } else { [System.IO.File]::SetLastWriteTime($TargetPath, $now) [System.IO.File]::SetLastAccessTime($TargetPath, $now) } } } if ($Settings.Verbose) { $Settings.Output.Add("'$SourceDisplay' -> '$TargetDisplay'") } } catch { Write-GnuError -Context $Context -Message "cannot create regular file '$TargetDisplay': $(Get-GnuErrorReason -Exception $_.Exception)" } } |