Utils/truncate.ps1

Register-GnuUtil @{
    Name         = 'truncate'
    Category     = 'File and directory operations'
    Synopsis     = 'Shrink or extend the size of a file to the specified size.'
    Usage        = @('truncate OPTION... FILE...')
    Description  = @(
        'Shrink or extend the size of each FILE to the specified size.',
        '',
        'A FILE argument that does not exist is created.',
        '',
        'If a FILE is larger than the specified size, the extra data is lost.',
        'If a FILE is shorter, it is extended and the sparse extended part (hole)',
        'reads as zero bytes.'
    )
    FileOperands = $true
    Options      = @(
        @{ Short = 'c'; Long = 'no-create'; Help = 'do not create any files' }
        @{ Short = 'o'; Long = 'io-blocks'; Help = 'treat SIZE as number of IO blocks instead of bytes' }
        @{ Short = 'r'; Long = 'reference'; Arg = 'RFILE'; Help = 'base size on RFILE' }
        @{ Short = 's'; Long = 'size'; Arg = 'SIZE'; Help = 'set or adjust the file size by SIZE bytes' }
    )
    Notes        = @(
        'The SIZE argument is an integer and optional unit (example: 10K is 10*1024).',
        'Units are K,M,G,T,P,E,Z,Y,R,Q (powers of 1024) or KB,MB,... (powers of 1000).',
        'Binary prefixes can be used, too: KiB=K, MiB=M, and so on.',
        '',
        'SIZE may also be prefixed by one of the following modifying characters:',
        "'+' extend by, '-' reduce by, '<' at most, '>' at least,",
        "'/' round down to multiple of, '%' round up to multiple of.",
        '',
        'PowerShell note: an IO block (-o) is 4096 bytes.'
    )
}



function script:Invoke-GnuTruncateFile {
    <#
    .SYNOPSIS
    Implementation of truncate (kept separate for readability).
    #>

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

    $hasSize = Test-GnuOption -Context $Context -Key 'size'
    $hasReference = Test-GnuOption -Context $Context -Key 'reference'
    $operands = @($Context.Operands)
    if (-not $hasSize -and -not $hasReference) {
        Write-GnuUsageError -Context $Context -Message "you must specify either '--size' or '--reference'"
        return
    }
    if ($operands.Count -eq 0) {
        Write-GnuUsageError -Context $Context -Message 'missing file operand'
        return
    }

    $operator = ''
    $amount = [decimal]0
    if ($hasSize) {
        $sizeText = [string](Get-GnuOptionValue -Context $Context -Key 'size')
        $numberText = $sizeText
        if ($sizeText.Length -gt 0 -and '+-<>/%'.IndexOf($sizeText[0]) -ge 0) {
            $operator = [string]$sizeText[0]
            $numberText = $sizeText.Substring(1)
        }
        $parsed = ConvertFrom-GnuSize -Text $numberText
        if ($null -eq $parsed) {
            Write-GnuError -Context $Context -Message "Invalid number: '$sizeText'"
            return
        }
        $amount = $parsed
        if (Test-GnuOption -Context $Context -Key 'io-blocks') { $amount = $amount * 4096 }
        if (($operator -eq '/' -or $operator -eq '%') -and $amount -eq 0) {
            Write-GnuError -Context $Context -Message 'division by zero'
            return
        }
    }

    $referenceSize = $null
    if ($hasReference) {
        $referenceName = [string](Get-GnuOptionValue -Context $Context -Key 'reference')
        $referencePath = Resolve-GnuPath -Path $referenceName
        if ([System.IO.File]::Exists($referencePath)) {
            $referenceSize = [decimal](New-Object System.IO.FileInfo -ArgumentList $referencePath).Length
        }
        elseif ([System.IO.Directory]::Exists($referencePath)) {
            $referenceSize = [decimal]0
        }
        else {
            Write-GnuError -Context $Context -Message "cannot stat '$referenceName': No such file or directory"
            return
        }
        if ($hasSize -and [string]::IsNullOrEmpty($operator)) {
            Write-GnuUsageError -Context $Context -Message "you must specify a relative '--size' with '--reference'"
            return
        }
    }

    foreach ($name in $operands) {
        $path = Resolve-GnuPath -Path $name
        if ([System.IO.Directory]::Exists($path)) {
            Write-GnuError -Context $Context -Message "cannot open '$name' for writing: Is a directory"
            continue
        }
        if (-not [System.IO.File]::Exists($path) -and (Test-GnuOption -Context $Context -Key 'no-create')) { continue }

        $stream = $null
        try {
            $stream = New-Object System.IO.FileStream -ArgumentList $path, ([System.IO.FileMode]::OpenOrCreate), ([System.IO.FileAccess]::Write)
            $current = [decimal]$stream.Length
            if ($null -ne $referenceSize) { $current = $referenceSize }

            $newSize = $current
            switch ($operator) {
                '' { if ($hasSize) { $newSize = $amount } }
                '+' { $newSize = $current + $amount }
                '-' { $newSize = [Math]::Max([decimal]0, $current - $amount) }
                '<' { $newSize = [Math]::Min($current, $amount) }
                '>' { $newSize = [Math]::Max($current, $amount) }
                '/' { $newSize = [Math]::Floor($current / $amount) * $amount }
                '%' { $newSize = [Math]::Ceiling($current / $amount) * $amount }
            }
            $stream.SetLength((ConvertTo-GnuInt64 -Value $newSize))
        }
        catch {
            Write-GnuError -Context $Context -Message "cannot open '$name' for writing: $(Get-GnuErrorReason -Exception $_.Exception)"
        }
        finally {
            if ($null -ne $stream) { $stream.Dispose() }
        }
    }
}