I wrote a script to perform svn update
on all the SVN repositories inside a folder. It would have been great if the folder containing all these repositories itself was a repository, but it was not setup like that, which is why I opted to write this script.
$timestamp = get-date -f "yyyy-M-d-h-m-s"
$SvnLogPath="C:\out\svnlogs\"
if(test-path -path $SvnLogPath)
{
write-host "SVN Log path doesn't exist. So I am creating it . . ."
new-item -type directory $SvnLogPath | out-null
}
$logfile = "${SvnLogPath}svn_update_${timestamp}.log"
new-item -type file $logfile | out-null
$BaseDir="C:\TRUNK\"
$RepositoryList = "Alpha","Bravo","Charlie","Delta","Echo","Foxtrot","Golf","Hotel"
# BUILDING FULL PATHS
$FullPaths = $RepositoryList | %{ $BaseDir+$_}
# ISSUING SVN UPDATE TO EACH DIR
$FullPaths | % {svn update $_ 2>&1 | ft -AutoSize -Wrap | Out-File -Append $logfile }
# OPEN LOG FILE AFTER THE COMPLETION
notepad $logfile
Can this script be further simplified and written more idiomatically?
PS: I am using only svn update
without credentials as they are stored in the shell already.
2 Answers 2
Reusable Commands
One of the most important ideas in PowerShell is the Get/Update/Set pattern. A clear delineation between resources (nouns) and commands (verbs) helps to write code that is useful not only for scripting, but also in the interactive shell.
We only need a couple of new commands. First we need to Find
the working copies under some directory, then Update
each working copy. The list of approved verbs for PowerShell is a great resource for help with naming your commands. Let's use these names.
- Find-SvnWorkingCopy (
Find
looks for an object in a container) - Update-SvnWorkingCopy (
Update
brings a resource up-to-date)
Functions and Modules
Now you're convinced that we should create reusable commands with these great new names. What next? PowerShell v2 introduced the concept of a module, which is just a file that contains functions. (Of course, you could just create a new script for each command but it helps to keep things tidy by bundling all of the functions together in one file.)
What should our functions look like? Yes they will need to take some parameters. But more importantly, they should return objects to the pipeline, even if they aren't a "getter" function! This makes it easier to review what impact a function had.
Some actual code
Luckily for us, somebody else already found a way to find all the svn working copies underneath a directory. We can reuse that.
SvnTools.psm1
function Find-SvnWorkingCopy
{
Param (
[Parameter(ValueFromPipeline=$true,Mandatory=$true)]
$BasePath
)
process
{
$prev = "^$"
Get-ChildItem -Recurse -Force -Include ".svn" -Path $BasePath |
Where-Object {
$_.PSIsContainer -and $_.Fullname.StartsWith($prev)-eq $false
} |
ForEach-Object {
$prev=$_.Fullname.TrimEnd(".svn")
$prev
}
}
}
I took some liberties with the command Update-SvnWorkingCopy
. In addition to calling "svn update" like your original script, it also calls "svn info" and parses the results to find the current revision number and include that in the object returned to the pipeline.
SvnTools.psm1 (continued)
function Update-SvnWorkingCopy
{
Param (
[Parameter(ValueFromPipeline=$true,Mandatory=$true)]
$WorkingCopy
)
process {
svn update $WorkingCopy > $null
[String[]] $result = svn info $WorkingCopy
[String] $revStr = $result -like "Revision:*"
[int] $revNumber = $revStr.Replace("Revision: ", "")
New-Object -TypeName PSObject -Prop @{
WorkingCopy=$WorkingCopy;
Revision=$revNumber;
} | Write-Output
}
}
Now we can reap the benefits from creating these commands. Here's your original script, rewritten to take advantage of our new module using just one line.
UpdateMyWorkingCopies.ps1
Import-Module C:\path\to\module\SvnTools.psm1
Find-SvnWorkingCopy -BasePath "C:\TRUNK\" | Update-SvnWorkingCopy
You could easily redirect the output to a file. If you don't, it will show up on screen looking something like this.
WorkingCopy Revision
----------- --------
C:\TRUNK\Alpha 31
C:\TRUNK\Bravo 42
C:\TRUNK\Charlie 16
References
The Scripting Guy! blog on TechNet has a really excellent series about building your own PowerShell cmdlet that I recommend reading.
Coding Style
- https://stackoverflow.com/a/2031927/190298
- https://stackoverflow.com/questions/5260125/whats-the-better-cleaner-way-to-ignore-output-in-powershell
- http://www.powershellcommunity.org/Wikis/BestPractices/tabid/79/topic/Naming/Default.aspx
Bonus Points
For bonus points, install the module into one of the module path folders ($env:PSModulePath
) so that you can import it from the shell more easily.
And if you want to get even more fancy, implement another command called Get-SvnWorkingCopy
which parses all of the results from svn info
. It should integrate nicely with the two commands we already created here.
-
\$\begingroup\$ This almost works perfectly! The only thing not quite right (here in 2021!) is that the Rev # stays the same throughout the loop. I'm going to see why it's not changing, and I'll be back. \$\endgroup\$John Dunagan– John Dunagan2021年09月01日 13:36:48 +00:00Commented Sep 1, 2021 at 13:36
Personally, I would look to use:
$FullPaths = $RepositoryList | %{ Join-path $BaseDir $_ }
vs.
$FullPaths = $RepositoryList | %{ $BaseDir+$_}
You could alternatively also do something like:
$FullPaths = Join-path $BaseDir "*\.svn" | Resolve-Path | Split-Path
This would build a list of all subfolders of $BaseDir that have a .SVN folder.
Resolve path will return the path with ..SVN\ and split path will remove the folder.
If you have .LOG registered with Notepad, you could also use
Invoke-Item $logFile
This may be more flexible if someone else runs the script and would rather see output in a different editor.