Out-File2: Improving Out-File with automatic file backup

Have you ever needed to write some file, but the need for making sure the file size didn’t get out of hand made you have to write a ton of extra code? This is a very typical scenario when writing log files for instance. Out-File2 got you covered!

This function is a wrapper around Out-File, but adds the ability to automatically create backups of the file you are writing to if the file size is larger than what you specify. More over, you can specify how many backups you want to keep, and it will clean up the oldest files when that threshold is reached. You can choose between two different file suffixes; a counter and a date stamp.

You can customize the suffix separator, as well as the number of digits for the counter and the date format for the date suffix. Note that the default value for MaxFileSize is 0, which makes the function behave just like the regular Out-File cmdlet. If you want the advanced functionality you should set MaxFileSize to some value you are comfortable with, as well as use the Append switch.

This function is doing a lot of stuff, so please report any bugs to me (or fork the Gist and help me do it).

Oh! And if you come up with a better name than Out-File2, please let me know 🙂

function Out-File2{
<#
.SYNOPSIS
Sends output to a file.
.DESCRIPTION
This function extends the original Out-File cmdlet by also handling automatic backup
of files. You can choose to use either a running counter, or the current date as a file
suffix for the backed up files.
.EXAMPLE
'Some text' | Out-File2 -FilePath $path
Used liked this, the function works just as the regular Out-File.
.EXAMPLE
'Some text' | Out-File2 -FilePath $path -Append -FilesToKeep 5 -SuffixType Counter -CounterLength 4 -MaxFileSize 1mb
In this example we are appending to the file, and are using 5 backup files with a file suffix type of counter. The counter is 4 digits long,
and a new backup will be made when the original file exeeds 1MB in size.
.EXAMPLE
'Some text' | Out-File2 -FilePath $path -Append -FilesToKeep 5 -SuffixType Date -MaxFileSize 1mb
Same example as above, but with a suffix type of date instead.
.NOTES
NOTE that when using a custom date format for the date suffix, it's possible to get backup files that don't have unique file names.
This will result in an error. Also, if you change suffix type while using the same file name, the function will get confused, and
behave unexpectedly.
Author: Øyvind Kallstad
Date: 29.04.2015
Version: 1.0
#>
[CmdletBinding()]
param (
# Specifies the objects to be written to the file. Enter a variable that contains the objects or type a command or
# expression that gets the objects.
[Parameter(ValueFromPipeline)]
[PSObject] $InputObject,
# Specifies the path to the output file.
[Parameter(Position = 0, Mandatory)]
[ValidateNotNullOrEmpty()]
[string] $FilePath,
# Specifies the type of character encoding used in the file. Valid values are "Unicode", "UTF7", "UTF8", "UTF32",
# "ASCII", "BigEndianUnicode", "Default" and "OEM". "Unicode" is the default.
[Parameter(Position = 1)]
[ValidateSet('Unicode','UTF7','UTF8','UTF32','ASCII','BigEndianUnicode','Default','OEM')]
[string] $Encoding = 'Unicode',
# Adds the output to the end of an existing file, instead of replacing the file contents.
[Parameter()]
[switch] $Append,
# Allows the function to overwrite an existing read-only file. Even using the Force parameter, the function cannot
# override security restrictions.
[Parameter()]
[switch] $Force,
# Will not overwrite (replace the contents) of an existing file. By default, if a file exists in the specified path,
# Out-File2 overwrites the file without warning. If both Append and NoClobber are used, the output is appended
# to the existing file.
[Parameter()]
[switch] $NoClobber,
# Specifies the maximum allowed file size (in bytes) for the file being written to.
# A MaxFileSize of 0 means no size limit. Default is 0.
[Parameter()]
[ValidateRange(1,[int64]::MaxValue)]
[int64] $MaxFileSize = 0,
# Specifies how many files you want to keep, before the function deletes old versions of the file. Default is 1.
# If MaxFileSize is 0 or Append is False, this parameter have not effect.
[Parameter()]
[ValidateRange(1,[int32]::MaxValue)]
[int] $FilesToKeep = 3,
# Choose what kind of file suffix you want to use for the backed up old versions of the file. Valid values are
# "Counter" and "Date". Default is "Counter".
# If MaxFileSize is 0 or Append is False, this parameter have not effect.
[Parameter()]
[ValidateSet('Counter','Date')]
[string] $SuffixType = 'Counter',
# Specify the character(s) to use as separator between the filename and the suffix. Default is "_".
# If MaxFileSize is 0 or Append is False, this parameter have not effect.
[Parameter()]
[ValidateNotNullOrEmpty()]
[string] $SuffixSeparator = '_',
# If suffix type is Counter, this parameter lets you specify the length of the counter, meaning the number of
# digits used in the counter suffix. Default is 3.
# If MaxFileSize is 0 or Append is False, this parameter have not effect.
[Parameter()]
[int] $CounterLength = 3,
# Specify the date format used for the date suffix type. Default is 'yyyyMMddHHmmssff'
# If MaxFileSize is 0 or Append is False, this parameter have not effect.
[Parameter()]
[ValidateNotNullOrEmpty()]
[string] $DateFormat = 'yyyyMMddHHmmssff'
)
try {
# get a list of all backup files based on the suffix type, and remove the oldest ones if the amount is larger than
# the FilesToKeep value. If any files are removed, get a new list so it's correct, as we will be using it further down in the script.
if ($SuffixType -eq 'Counter') {
[System.Array]$backupFiles = Get-Item Path "$(Join-Path Path $file.DirectoryName ChildPath $file.BaseName)$($SuffixSeparator)?*$($file.Extension)" | Where-Object {!$_.PSIsContainer} | Sort-Object LastWriteTime Descending | Sort-Object Name
if ($backupFiles.Count -gt $FilesToKeep) {
$backupFiles | Sort-Object Name | Select-Object Last ($backupFiles.Count $FilesToKeep) | Remove-Item Force:$Force ErrorAction Stop
[System.Array]$backupFiles = Get-Item Path "$(Join-Path Path $file.DirectoryName ChildPath $file.BaseName)$($SuffixSeparator)?*$($file.Extension)" | Where-Object {!$_.PSIsContainer} | Sort-Object LastWriteTime Descending | Sort-Object Name
}
}
else {
[System.Array]$backupFiles = Get-Item Path "$(Join-Path Path $file.DirectoryName ChildPath $file.BaseName)$($SuffixSeparator)?*$($file.Extension)" | Where-Object {!$_.PSIsContainer} | Sort-Object LastWriteTime
if ($backupFiles.Count -gt $FilesToKeep) {
$backupFiles | Sort-Object LastWriteTime Descending | Select-Object Last ($backupFiles.Count $FilesToKeep) | Remove-Item Force:$Force ErrorAction Stop
[System.Array]$backupFiles = Get-Item Path "$(Join-Path Path $file.DirectoryName ChildPath $file.BaseName)$($SuffixSeparator)?*$($file.Extension)" | Where-Object {!$_.PSIsContainer} | Sort-Object LastWriteTime
}
}
if (((Test-Path Path $FilePath) -eq $true) -and ($MaxFileSize -gt 0) -and ($Append)) {
$file = Get-Item Path $FilePath
if ($file.Length -ge $MaxFileSize) {
if ($backupFiles.Count -eq $FilesToKeep) {
# remove the oldest of the backup files
$fileToDelete = $backupFiles[0]
Write-Verbose "Removing $($fileToDelete.Name)"
$fileToDelete | Remove-Item Force:$Force ErrorAction Stop
# for files with a suffix type of counter, we need to rename the remaining files so the counter makes sense.
if ($SuffixType -eq 'Counter') {
foreach ($backupFile in $backupFiles[1..($backupFiles.IndexOf($backupFiles[-1]))]) {
$thisNewFileName = "$($backupFile.BaseName.Split($SuffixSeparator)[0])$($SuffixSeparator)$([Convert]::ToString([Convert]::ToInt32($backupFile.BaseName.Split($SuffixSeparator)[-1])-1).PadLeft($CounterLength,'0'))$($backupFile.Extension)"
Write-Verbose "Renaming $($backupFile.Name) to $thisNewFileName"
Rename-Item Path $backupFile NewName $thisNewFileName Force ErrorAction Stop
}
}
# rename the current file while adding the correct suffix
if ($SuffixType -eq 'Counter') {
$thisNewFileName = "$($file.BaseName.Split($SuffixSeparator)[0])$($SuffixSeparator)$([Convert]::ToString([Convert]::ToInt32($backupFiles[-1].BaseName.Split($SuffixSeparator)[-1])).PadLeft($CounterLength,'0'))$($file.Extension)"
}
else {
$thisNewFileName = "$($file.BaseName.Split($SuffixSeparator)[0])$($SuffixSeparator)$(Get-Date Format $DateFormat)$($file.Extension)"
}
Write-Verbose "Renaming $($file.Name) to $thisNewFileName"
Rename-Item Path $file NewName $thisNewFileName Force ErrorAction Stop
}
# if the FilesToKeep limit still haven't been reached
else {
# again we have to treat files using the Counter suffix type differently
# if this is the first time a backup is made, use 1 as the counter suffix
# if other backup files exist, take the last counter value found, and add 1
if ($SuffixType -eq 'Counter') {
if ($backupFiles.Count -ge 1) {
$thisNewFileName = "$($file.BaseName)$($SuffixSeparator)$([Convert]::ToString([Convert]::ToInt32($backupFiles[-1].BaseName.Split($SuffixSeparator)[-1])+1).PadLeft($CounterLength,'0'))$($file.Extension)"
}
else {
$thisNewFileName = "$($file.BaseName)$($SuffixSeparator)$([Convert]::ToString(1).PadLeft($CounterLength,'0'))$($file.Extension)"
}
Write-Verbose "Renaming $($file.Name) to $($thisNewFileName)"
Rename-Item $file NewName $thisNewFileName Force ErrorAction Stop
}
else {
$thisNewFileName = "$($file.BaseName)$($SuffixSeparator)$(Get-Date Format $DateFormat)$($file.Extension)"
Write-Verbose "Renaming $($file.Name) to $($thisNewFileName)"
Rename-Item $file NewName $thisNewFileName Force ErrorAction Stop
}
}
}
}
Write-Verbose "Writing $FilePath"
Out-File FilePath $FilePath InputObject $InputObject Encoding $Encoding Append:$Append Force:$Force NoClobber:$NoClobber ErrorAction Stop
}
catch [System.IO.IOException] {
Write-Warning $_.Exception.Message
if ($SuffixType -eq 'Date') {
Write-Warning 'Make sure your date format creates unique backup files.'
}
}
catch {
Write-Warning $_.Exception.Message
}
}

view raw
Out-File2.ps1
hosted with ❤ by GitHub

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s