A few weeks ago, the Iron Scripter challenge was to move files meeting some criteria to a new location and leave a link behind. As I've written before, these challenges are a great way to test your PowerShell skills and stretch yourself. This challenge has a number of moving parts.
ManageEngine ADManager Plus - Download Free Trial
Exclusive offer on ADManager Plus for US and UK regions. Claim now!
Often with PowerShell some of the hardest work is planning and organization. How are you going to achieve a given task? If you are writing code that someone else will be using, what expectations or assumptions will they have? How are they likely to use your commands? I also think about flexibility and re-use. I try to avoid writing a monolithic function that does several things. Rather, you should focus on small, single-use tasks that you can combine in a pipelined expression. Or orchestrate from a controller script, which is what I will be showing you later.
Testing the Core Command
The core command in this task is New-Item. You can use this cmdlet to create files, folders and links. I'm assuming you will look at full help and examples. I'll give it a spin.
New-Item -ItemType SymbolicLink -Path . -Name procmon.exe -Value D:\OneDrive\tools\Procmon.exe
This command will create a link to D:\OneDrive\tools\Procmon.exe in the current directory.
I can run procmon.exe in C:\Work without caring where the actual file resides. Excellent. The premise of the challenge is to go through a list of files, move them to a destination and leave a link behind.
Move and Link
This sounds like a task I might want to re-use. I don't want to limit what files to process. I want to be able to pass any file to my command and have it move and link. Technically, PowerShell has a Move-Item command, but I'm thinking of the "move and link" process as a single action so I will code it that way. Plus, there are a few other challenge features I want to address.
Here's my function for "move and link".
#requires -version 5.1
Function Move-FileWithLink {
[CmdletBinding(SupportsShouldProcess)]
Param(
[Parameter(Position = 0, Mandatory, HelpMessage = "Specify the path of a file to move.", ValueFromPipelineByPropertyName)]
[alias("fullname")]
[ValidateNotNullOrEmpty()]
[ValidateScript({ Test-Path $_ })]
[string]$Path,
[Parameter(HelpMessage = "Specify the top-level target path. It will be created if it doesn't exist.")]
[ValidateNotNullOrEmpty()]
[string]$Destination = "\\172.16.10.100\backup\lts",
[switch]$Passthru
)
Begin {
Write-Verbose "[$((Get-Date).TimeofDay) BEGIN ] Starting $($myinvocation.mycommand)"
#define a timestamped logfile
$logname = "{0}_MoveLink.log" -f (Get-Date -Format "yyyy_MM-dd-hhmm")
#using .NET to support cross-platform
$logfile = Join-Path -Path $([System.io.path]::GetTemppath()) -ChildPath $logname
Write-Verbose "[$((Get-Date).TimeofDay) BEGIN ] Logging activity to $logfile"
#define a logging helper function
function log {
[cmdletbinding(SupportsShouldProcess)]
param([string]$Message, [string]$LogFile)
$text = "[{0}] {1}" -f (Get-Date -Format u), $Message
$text | Out-File -FilePath $logFile -Force -Append
}
#set the default parameter value for the log function
$PSDefaultParameterValues["log:logfile"] = $logfile
log "Starting: $($myinvocation.MyCommand)"
$who = "$([System.Environment]::UserDomainName)\$([System.Environment]::UserName)"
$where = [System.Environment]::MachineName
log "User: $who"
log "Computer: $where"
if (-Not (Test-Path -Path $Destination)) {
Try {
log "Creating $destination"
[void](New-Item -ItemType Directory -Path $Destination -Force -ErrorAction Stop)
}
Catch {
Throw $_
}
}
} #begin
Process {
$Path = Convert-Path $path
Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Processing $Path"
log "Processing $path"
$parent = Split-Path -Path $path
$name = Split-Path -Path $path -Leaf
#get the relative path so it can be reconstructed in the destination
$target = Join-Path -Path $Destination -ChildPath $parent.Substring(3)
if ($pscmdlet.ShouldProcess($path, "Move file to $target")) {
Try {
#recreate file structure
if (-Not (Test-Path -Path $Target)) {
Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Creating $Target"
[void](New-Item -ItemType Directory -Path $Target -Force -ErrorAction Stop)
log "Created target $target"
}
Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Moving file to $Target"
$m = Move-Item -Path $path -Destination $target -PassThru -ErrorAction Stop
log "Moved $path to $target"
}
Catch {
$msg = "Failed to move $path to $target. $($_.Exception.message)"
Write-Warning $msg
log $msg
}
}
Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Creating link for $path"
if ($pscmdlet.ShouldProcess($path, "Creating link")) {
if (Test-Path $m.fullname) {
Try {
$link = New-Item -ItemType SymbolicLink -Name $name -Path $parent -Value $m.FullName -Force -ErrorAction Stop
log "Created symboliclink from $($m.fullname)"
if ($passthru) {
$link
}
}
Catch {
$msg = "Failed to create link to $path from $($m.fullname). $($_.Exception.message)"
Write-Warning $msg
log $msg
}
}
}
} #process
End {
log "Ending: $($myinvocation.MyCommand)"
if (Test-Path $logfile) {
Write-Verbose "[$((Get-Date).TimeofDay) END ] Activity logged at $logfile"
}
if ($PSDefaultParameterValues.ContainsKey("log:logfile")) {
$PSDefaultParameterValues.Remove("log:logfile")
}
Write-Verbose "[$((Get-Date).TimeofDay) END ] Ending $($myinvocation.mycommand)"
} #end
}
The function will take a filename as a parameter, copy it to the destination, then create a link in the original location. The function has a hard-coded logging function that will create a text-based log file in the user's %TEMP% folder.
$logname = "{0}_MoveLink.log" -f (Get-Date -Format "yyyy_MM-dd-hhmm")
#using .NET to support cross-platform
$logfile = Join-Path -Path $([System.io.path]::GetTemppath()) -ChildPath $logname
function log {
[cmdletbinding(SupportsShouldProcess)]
param([string]$Message, [string]$LogFile)
$text = "[{0}] {1}" -f (Get-Date -Format u), $Message
$text | Out-File -FilePath $logFile -Force -Append
}
I wrote a short helper function so that each log entry will include a UTC time stamp. I wanted to keep my code simple, so I temporarily set a PSDefaultParameterValue for the function.
#set the default parameter value for the log function
$PSDefaultParameterValues["log:logfile"] = $logfile
log "Starting: $($myinvocation.MyCommand)"
This makes it easy to log events throughout the script. At the end of the function, I remove the PSDefaultParameterValue.
if ($PSDefaultParameterValues.ContainsKey("log:logfile")) {
$PSDefaultParameterValues.Remove("log:logfile")
}
The function uses the cmdletbinding attribute to enable -WhatIf. Even though most of the commands I'm using will automatically detect -WhatIf, I opted to create my own -WhatIf code to have a bit more control over the messaging.
if ($pscmdlet.ShouldProcess($path, "Move file to $target")) {
Try {
#recreate file structure
if (-Not (Test-Path -Path $Target)) {
Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Creating $Target"
[void](New-Item -ItemType Directory -Path $Target -Force -ErrorAction Stop)
log "Created target $target"
}
Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Moving file to $Target"
$m = Move-Item -Path $path -Destination $target -PassThru -ErrorAction Stop
log "Moved $path to $target"
}
Catch {
$msg = "Failed to move $path to $target. $($_.Exception.message)"
Write-Warning $msg
log $msg
}
}
I can use the function like this:
The WhatIf is also detected for the logging function. I'll move and link the file.
And here's what ends up in my log.
[2020-12-02 16:16:37Z] Starting: Move-FileWithLink
[2020-12-02 16:16:37Z] User: PROSPERO\Jeff
[2020-12-02 16:16:37Z] Computer: PROSPERO
[2020-12-02 16:16:37Z] Creating d:\archive
[2020-12-02 16:16:37Z] Processing C:\work\s.clixml
[2020-12-02 16:16:37Z] Created target d:\archive\work
[2020-12-02 16:16:37Z] Moved C:\work\s.clixml to d:\archive\work
[2020-12-02 16:16:37Z] Created symboliclink from D:\archive\work\s.clixml
[2020-12-02 16:16:37Z] Ending: Move-FileWithLink
Using a Controller Script
Now that I have a working tool to move and link files I need an easy way to use it. This is where considering who will be using your command and how, comes into play. The move and link function will take any input. An experienced PowerShell user could simply run a command like this:
But, the challenge was looking for a tool a user could run where they could specify locations. The challenge was also looking for a tool to move and link files last modified before a given date. I wrote a controller script.
This type of script is designed to wrap-around a few core commands and execute them in a structured manner. You could run the commands in the script manually and achieve the same result. Controller scripts can also use parameters and almost any scripting feature you would have in a function.
#requires -version 5.1
<#
move specified files to an alternate location, leaving
linked copies behind
Specify the source folder
Specify the target folder
Specify the last modified age
Support recursion
Support an exclude filter
This could be turned into a function instead of a control script
#>
[CmdletBinding(SupportsShouldProcess)]
Param (
[parameter(Position = 0, Mandatory, HelpMessage = "The folder to process for old files.", ValueFromPipeline)]
[ValidateScript({Test-Path $_ })]
[string]$Path,
[parameter(Position = 1, Mandatory, HelpMessage = "The top-level destination folder.")]
[string]$Destination,
[Parameter(HelpMessage = "Specify the last modified cutoff date.")]
[datetime]$LastModified = (Get-Date).AddDays(-180).Date,
[parameter(HelpMessage = "Recurse for files from the given path.")]
[switch]$Recurse,
[parameter(HelpMessage = "Specify a file pattern to exclude. Wildcards are permitted.")]
[string]$Exclude,
[switch]$Passthru
)
Begin {
Write-Verbose "[$((Get-Date).TimeofDay) BEGIN ] Starting $($myinvocation.mycommand)"
#dot source the function that moves files and creates links
. $PSScriptRoot\Move-FileLink.ps1
$getParams = @{
ErrorAction = "Stop"
File = $True
}
if ($Recurse) {
$getParams.Add("Recurse",$True)
}
$moveParams = @{
Destination = $Destination
Passthru = $Passthru
}
Write-Verbose "[$((Get-Date).TimeofDay) BEGIN ] Finding files last modified before $LastModified"
if ($Exclude) {
Write-Verbose "[$((Get-Date).TimeofDay) BEGIN ] Excluding files that match the pattern $Exclude"
}
Write-Verbose "[$((Get-Date).TimeofDay) BEGIN ] Using a destination of $destination"
} #begin
Process {
#convert to a complete file-system path
$Path = Convert-Path $Path
Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Processing $Path"
$getParams.Path = $Path
#build a filter for Where-Object. -Exclude only works with Get-Childitem when using
#recurse, which the user might not want.
if ($Exclude) {
Get-ChildItem @getParams | Where-object {($_.LastWriteTime -lt $LastModified) -AND ($_.name -notlike $Exclude)} |
Move-FileWithLink @moveParams
}
else {
Get-ChildItem @getParams | Where-Object { $_.LastWriteTime -lt $LastModified } | Move-FileWithLink @moveParams
}
} #process
End {
Write-Verbose "[$((Get-Date).TimeofDay) END ] Ending $($myinvocation.mycommand)"
} #end
It wouldn't take much to convert this into a function if I wanted to. In fact, I often start with stand-alone scripts because I can run them without the extra step of dot-sourcing. Once I'm happy with the code, adding the function bits is trivial.
Let's see this script in action.
The Verbose output makes it clear what the command is going to do.
Summary
The core command is flexible and re-usable beyond the challenge requirements. The rest of the challenge requirements are met in the control script. I think many people overlook this concept. Not everything has to be put into a function. A control script is a great way of ensuring consistency. You can sign these scripts, check them into source control, and manage them the same way you handle your PowerShell modules.
If you feel you need to improve your PowerShell scripting skills, consider grabbing a copy of The PowerShell Scripting and Toolmaking Book.
Hi Jeff,
I am searching PS script to find just percentage usage memory of one of the service ( ex: sqlservices on windows server) on windows servers out of its total memory.
Such that if sql services consumes consistently more than 70% of memory.I can i ask sql team to increase the memory.
As when we see either in task manger or process explore or perfmon- we will get private memory ,shared memory , however that i can show the accurate usage in generic words ( straight forward).
Or is there any simple way i missing, either on systeminternal tools or windows itself.
Request your help.
Note: i followed all Pluralsight or udemy courses i couldn’t any course or chapter related to percentge of the memory usage.
I don’t have anything that exactly meets your needs. You would need to identify the process associated with the service then look at the memory. Or if you know the process name, use that. You can add up with Workingset values and divide by the total amount of installed memory. This would make a good Iron Scripter challenge.,