Utils/mv.ps1

Register-GnuUtil @{
    Name         = 'mv'
    Category     = 'File and directory operations'
    Synopsis     = 'Move (rename) files.'
    Usage        = @(
        'mv [OPTION]... [-T] SOURCE DEST',
        'mv [OPTION]... SOURCE... DIRECTORY',
        'mv [OPTION]... -t DIRECTORY SOURCE...'
    )
    Description  = @('Rename SOURCE to DEST, or move SOURCE(s) to DIRECTORY.')
    FileOperands = $true
    Options      = @(
        @{ Short = 'f'; Long = 'force'; Key = 'clobber'; Value = 'force'; Help = 'do not prompt before overwriting' }
        @{ Short = 'i'; Long = 'interactive'; Key = 'clobber'; Value = 'ask'; Help = 'prompt before overwrite' }
        @{ Short = 'n'; Long = 'no-clobber'; Key = 'clobber'; Value = 'no'; Help = 'do not overwrite an existing file' }
        @{ Long = 'strip-trailing-slashes'; Help = 'remove any trailing slashes from each SOURCE argument' }
        @{ Short = 't'; Long = 'target-directory'; Arg = 'DIRECTORY'; Help = 'move all SOURCE arguments into DIRECTORY' }
        @{ Short = 'T'; Long = 'no-target-directory'; Help = 'treat DEST as a normal file' }
        @{ Short = 'u'; Long = 'update'; Help = 'move 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        = @(
        'If you specify more than one of -i, -f, -n, only the final one takes effect.',
        'PowerShell notes: directories are moved across drives by copying and deleting. Backups (-b, -S) are not supported.'
    )
}



function script:Invoke-GnuMove {
    <#
    .SYNOPSIS
    Implementation of mv.
    #>

    param([Parameter(Mandatory)][hashtable]$Context)

    if (Test-GnuOption -Context $Context -Key 'strip-trailing-slashes') {
        $Context.Operands = [string[]]@(foreach ($operand in $Context.Operands) { Remove-GnuTrailingSeparator -Path $operand })
    }
    $clobber = [string](Get-GnuOptionValue -Context $Context -Key 'clobber' -Default 'force')
    $update = Test-GnuOption -Context $Context -Key 'update'
    $verbose = Test-GnuOption -Context $Context -Key 'verbose'

    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
        }
        $sourceIsDirectory = $source -is [System.IO.DirectoryInfo] -and $null -eq (Get-GnuLinkTarget -Path $sourcePath)
        if (Test-GnuSamePath -Left $sourcePath -Right $targetPath) {
            Write-GnuError -Context $Context -Message "'$($pair.Source)' and '$($pair.Target)' are the same file"
            continue
        }
        if ($sourceIsDirectory -and (Test-GnuInsidePath -Path $targetPath -Ancestor $sourcePath)) {
            Write-GnuError -Context $Context -Message "cannot move '$($pair.Source)' to a subdirectory of itself, '$($pair.Target)'"
            continue
        }

        $target = Get-Item -LiteralPath $targetPath -Force -ErrorAction SilentlyContinue
        if ($null -ne $target) {
            if ($clobber -eq 'no') { continue }
            if ($update -and $target.LastWriteTime -ge $source.LastWriteTime) { continue }
            if ($clobber -eq 'ask' -and -not (Read-GnuConfirmation -Context $Context -Question "overwrite '$($pair.Target)'?")) { continue }
            $targetIsDirectory = $target -is [System.IO.DirectoryInfo] -and $null -eq (Get-GnuLinkTarget -Path $targetPath)
            if ($sourceIsDirectory -and -not $targetIsDirectory) {
                Write-GnuError -Context $Context -Message "cannot overwrite non-directory '$($pair.Target)' with directory '$($pair.Source)'"
                continue
            }
            if (-not $sourceIsDirectory -and $targetIsDirectory) {
                Write-GnuError -Context $Context -Message "cannot overwrite directory '$($pair.Target)' with non-directory"
                continue
            }
            try {
                if ($targetIsDirectory) {
                    if (@([System.IO.Directory]::GetFileSystemEntries($targetPath)).Count -gt 0) {
                        Write-GnuError -Context $Context -Message "cannot move '$($pair.Source)' to '$($pair.Target)': Directory not empty"
                        continue
                    }
                    [System.IO.Directory]::Delete($targetPath, $false)
                }
                elseif ($target -is [System.IO.DirectoryInfo]) { [System.IO.Directory]::Delete($targetPath, $false) }
                else {
                    $target.Attributes = [System.IO.FileAttributes]::Normal
                    [System.IO.File]::Delete($targetPath)
                }
            }
            catch {
                Write-GnuError -Context $Context -Message "cannot remove '$($pair.Target)': $(Get-GnuErrorReason -Exception $_.Exception)"
                continue
            }
        }

        try {
            if ($sourceIsDirectory -and -not (Test-GnuSameVolume -Left $sourcePath -Right $targetPath)) {
                # Directory.Move cannot cross drives: copy, then delete the original.
                Copy-GnuDirectoryTree -Source $sourcePath -Destination $targetPath
                Remove-GnuTree -Path $sourcePath
            }
            elseif ($source -is [System.IO.DirectoryInfo]) {
                [System.IO.Directory]::Move($sourcePath, $targetPath)
            }
            else {
                [System.IO.File]::Move($sourcePath, $targetPath)
            }
            if ($verbose) { "renamed '$($pair.Source)' -> '$($pair.Target)'" }
        }
        catch {
            Write-GnuError -Context $Context -Message "cannot move '$($pair.Source)' to '$($pair.Target)': $(Get-GnuErrorReason -Exception $_.Exception)"
        }
    }
}