0

I am writing a script to copy all directories and items from one folder to another if they are more than seven days old.

I created a function for writing to a log. I call the function when I want to write something to the log. I am trying to list all items that I am copying. Normally when I do this I would call Out-File, but, again, this time I'm using a function. I've tried numerous iterations of getting the directory listing to log. It either doesn't log correctly (bad format), messes up the format of the rest of the log, or doesn't log at all for the directory listing. Can you please assist?

Basically, I want to write all directories or files greater than seven days old to my logfile. The lines that have an issue ends with Out-File $logfile -Append. I tried two different ways to get what I wanted.

$sourceDir = 'c:\temp\test\'
$targetDir = 'c:\temp\'
$date = (Get-Date).AddDays(-7)
$fileName = "TransfertoStream_"
$LogFile = "$($sourceDir)$($fileName)$(get-date -Format yyyyMMdd_HHmmss).log"

function LogWrite {
    Param ([string]$logstring)
    Add-Content $Logfile -Value $logstring
}
LogWrite "Created Log"

function Get-TimeStamp {
    return "[{0:MM/dd/yy} {0:HH:mm:ss}]" -f (Get-Date)
}

LogWrite "$(Get-Timestamp) Created Timestamp "
LogWrite "$(Get-Timestamp) looking for all files with a date modified older than $($date)"

if (dir $source | %{ dir $_.FullName | ?{ $_.LastWriteTime -lt $date }}) {
    LogWrite "$(Get-Timestamp) There are files to transfer"
    LogWrite "$(Get-Timestamp) Here are the files and directories I am working on today"

    Get-ChildItem -Path $sourceDir -Directory -Recurse -Force | % {
        dir $_.FullName | ?{ $_.LastWriteTime -lt $date }
    } | Out-File $LogFile -Append

    Get-ChildItem $sourceDir -Recurse | % {
        $dest = $targetDir + $_.FullName.SubString($sourceDir.Length)

        if (!($dest.Contains('.')) -and !(Test-Path $dest)) {
            mkdir $dest
        }

        Copy-Item $_.FullName -Destination $dest -Force | Out-File $logfile -Append
    }
} else {
    LogWrite "$(Get-TimeStamp) No files to transfer"
    LogWrite "$(Get-TimeStamp) Ending"
}

The log looks like this, all the information at the end should be on no more than two lines...

Created Log
[07/03/18 17:23:34] Created Timestamp 
[07/03/18 17:23:34] looking for all files with a date modified older than 06/26/2018 17:23:34
[07/03/18 17:23:34] There are files to transfer
[07/03/18 17:23:34] Here are the files and directories I am working on today




         D i r e c t o r y :   C : \ t e m p \ t e s t \ N e w   f o l d e r 





 M o d e                                 L a s t W r i t e T i m e                   L e n g t h   N a m e                                                                                                                                                                                                                                                                                   

 - - - -                                 - - - - - - - - - - - - -                   - - - - - -   - - - -                                                                                                                                                                                                                                                                                   

 - a - - - -                 9 / 1 9 / 2 0 1 4       6 : 4 1   A M                 8 8 4 7 9 8 2   0 6   S h a k e   I t   O f f   -   C o p y . m p 3                                                                                                                                                                                                                                       

Can you think of a cleaner/simpler/more elegant way to get the file listing?

5
  • You may want to provide samples of your desired and actual output. Commented Jul 3, 2018 at 22:04
  • Have you stepped through your code in a debugger like ISE or VSCode? Commented Jul 3, 2018 at 22:06
  • @KoryGill , the code works fine, so debug doesn't show anything. The problem I'm having is taking what would normally write-host, or out-file, doesn't work when I call my function. Commented Jul 3, 2018 at 22:55
  • @AnsgarWiechers - As written in the snippet above, all I get is a super parsed header with partial information making it very difficult to read. It is too large to enter into the comments. Added it to my original post Commented Jul 3, 2018 at 22:58
  • Looks like an encoding error to me. Perhaps try using Add-Content instead of Out-File for consistency. Commented Jul 3, 2018 at 23:24

2 Answers 2

4

You're mixing Add-Content and Out-File output, which use different default encodings. The *-Content cmdlets default to ASCII¹, whereas the Out-File cmdlet defaults to Unicode (little endian UTF-16, to be precise). UTF-16 characters are encoded as 2 bytes, whereas ASCII characters are encoded as 1 byte. Hence the gratuitious spacing in the output added by Out-File.

From the Out-File documentation:

-Encoding

Specifies the type of character encoding used in the file. The acceptable values for this parameter are:

[...]

Unicode is the default.

From the Add-Content documentation:

-Encoding

Specifies the file encoding. The default is ASCII.

You see the 2 bytes of the Unicode characters displayed as 2 characters because the file was originally created by Add-Content (using that cmdlet's default encoding). Had the file originally been created as a Unicode file and you wrote ASCII text to it afterwards then you'd see what is called "mojibake" (two ASCII characters being displayed as one Unicode character). That normally shouldn't happen when appending to a Unicode file with Add-Content, though, because the cmdlet honors the BOM (Byte Order Mark) that indicates the encoding used for the file.

The general recommendation is not to mix *-Content cmdlets with Out-File. Use one or the other, and stick with it. If for some reason you must use Out-File along with *-Content cmdlets enforce the desired encoding via the parameter -Encoding.

With that said, if you already have a logging function present: use it for all the logging. Do not log some lines one way and others a different way. Extend your logging function so that it accepts input from the pipeline:

function LogWrite {
    [CmdletBinding()]
    Param (
        [Parameter(Position=0, Mandatory=$false, ValueFromPipeline=$true)]
        [string]$logstring = ""
    )

    Process {
        $logstring | Add-Content $Logfile
    }
}

so that you can use it like this:

LogWrite "something"

or like this:

Get-ChildItem | LogWrite

or like this:

Get-ChildItem | Out-String | LogWrite

depending on the kind of log output you want to get.

I would also recommend adding a timestamp inside the logging function rather than adding timestamps to individual strings:

"$(Get-Timestamp) ${logstring}" | Add-Content $Logfile

¹ Yes, I am aware that technically the encoding is ANSI (or rather one of the many ANSI encodings), and that actual ASCII encoding uses 7 bits, not 8 bits like ANSI. However, in the context of the question that is a distinction without a difference, because the problem at hand is the fact that UTF-16 characters are stored using 2 bytes per character, while ASCII and ANSI characters alike are stored using just one byte per character.

Sign up to request clarification or add additional context in comments.

5 Comments

Presence is implicitly true for parameter attributes and parameters are not mandatory as a default.
I like to be explicit about a lot of things. ;)
Fair enough. Nice explanation on the encoding; I've always wondered why some things end up double-spaced.
Nicely done; note, however, that the *-Content cmdlets default to "ANSI" encoding (the code page implied by the legacy system locale), not ASCII, in spite of what the documentation claims - see github.com/PowerShell/PowerShell-Docs/issues/1483
Yes, I'm aware that the encoding is actually ANSI (one of the many ANSI encoding to be precise), and I did think about mentioning that. But seeing how both the documentation and the parameter value say "ASCII", and considering that in the context of the question the difference is practically moot, that would've been a distinction without a difference, so I decided to gloss over it to avoid confusing readers.
0

Here's a simple logging function that will accomplish what you want:

function Write-Log {
    [CmdletBinding()]
    param(
        [Parameter(ValueFromPipeline)]
        [string] $Message,

        [string] $Path = "$sourceDir$fileName$(Get-Date -UFormat %Y%m%d_%H%M%S).log"
    )

    process {
        "$(Get-Date -UFormat '[%m/%d/%Y %H:%M:%S]') $Message" |
            Out-File -FilePath $Path -Append -Encoding UTF8 -Force
    }
}

In use:

Write-Log 'Created timestamp'
Write-Log "Looking for all files with a date modified older than $date"

if ($files = Get-ChildItem -Path $source | Get-ChildItem | Where-Object LastWriteTime -lt $date) {
    Write-Log 'There are files to transfer'
    Write-Log 'Here are the files and directories I am working on today'

    $files.FullName | Write-Log

    Get-ChildItem -Path $sourceDir -Recurse |
        ForEach-Object {
            $dest = $targetDir + $_.FullName.Substring($sourceDir.Length)

            if (-not $dest.Contains('.') -and -not (Test-Path -Path $dest)) {
                New-Item -Path $dest -ItemType Directory
            }

            Copy-Item -Path $_.FullName -Destination $dest -Force -PassThru |
                ForEach-Object FullName |
                Write-Log
        }
} else {
    Write-Log 'No files to transfer'
    Write-Log 'Ending'
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.