Quantcast

PowerShell and Robocopy part 1

listening to the PowerScripting Podcast - Episode 41 RoboCopy was mentioned, and I remembered some Robocopy stuff I still had laying around :

And I will use it as a series about using / wrapping a native tool from PowerShell, I start out with a bit of Text Processing, 

#############################################################################################
## Make RoboCopy Help Object
#############################################################################################

$RoboHelp = robocopy /? | Select-String '::'
$r = [regex]'(.*)::(.*)'
$RoboHelpObject = $RoboHelp | select `
    @{Name='Parameter';Expression={ $r.Match( $_).groups[1].value.trim()}},
    @{Name='Description';Expression={ $r.Match( $_).groups[2].value.trim()}}

$RoboHelpObject = $RoboHelpObject |% {$Cat = 'General'} {
    if ($_.parameter -eq '') { if ($_.Description -ne ''){
        $cat = $_.description -replace 'options :',''}
    } else {
        $_ | select @{Name='Category';Expression={$cat}},parameter,description
    }
}

This peice of PowerShell code basically just starts Robocopy /?  , parses the resulting help text into an Custom object,  this gives us a Robocopy help object in the variable $RoboCopyHelpObject and we can check it like this :

Image may be NSFW.
Clik here to view.
image

You can see that as I have the robocopy help in object form now I can do for example grouping and counting of the Help topics,

I did a series about TextScraping where you can find more examples of text to object translations, so I won't go into much detail here but we will use this object we created later in this series.

Enjoy,

Greetings /\/\o\/\/

Image may be NSFW.
Clik here to view.

PowerShell and Robocopy part 2

In last post we made a RoboCopy help Object

We are going to use that later on, but first we make a sample Robocopy Job :

 

#############################################################################################
## Robocopy example command :
#############################################################################################


robocopy 'c:\Documents and Settings' c:\PowerShellRoboTest /r:2 /w:5 /s /v /np |
  Tee-Object -Variable RoboLog

What we are doing here is starting a Robocopy Job and we also capture the output from robocopy in the variable $robolog.

for the given parameters /NP is an important one, it means "No Process indicator" as this messes up the output.

With the $RoboHelpObject from the first post in this series we can find out what the rest of the parameters mean like this :

Image may be NSFW.
Clik here to view.
image
not very handy yet, but later on in this series we will use this object, to make a more "fancy" way to start the Robocopy job.
We will create a form to help us creating a Robocopy command, as we can  generate the Robocopy command from a form that uses this Help object : 
Image may be NSFW.
Clik here to view.
image

but for now you can change the directories to valid ones for testing out robocopy command (I picked a directory where I was sure some files would be in use, guess why ;-) ), and we will first go on with the output of the example robocpoy command, in the next post we will also parse the output of robocopy into an Object so that we can do some analyzing  on it from PowerShell (and also process saved Robocopy Logs) , after that I go on about how I created the Form above.

Enjoy,

Greetings /\/\o\/\/

Image may be NSFW.
Clik here to view.

PowerShell and Robocopy part 3

In this 3rd part of this  series we go on with the output of a Test copy, and will start to parse the robocopy log, I just made a sTest1 directory with some test files and started the command as explained in last post,

You can see in the output that there are 3 distinct parts in the Robocopy output, Start, Details, Stop information divided by a line, so the first thing is dividing the output of robocopy in the 3 parts and process them separately, we do that by this line :

$null,$StartBegin,$StartEnd,$StopBegin = $RoboLog | Select-String "----" |% {$_.linenumber}

as you see in the output below this gives us the line numbers of lines separating the different parts :

Image may be NSFW.
Clik here to view.
image

After that I make a new object and start parsing the Start information, adding the information to the $RobocopyStatus object,

note that the Date get's translated from text into a "Real" Date-time object (might need adapting for other cultures as en-us)

 -Value ([datetime]::ParseExact($matches[1].trim(),'ddd MMM dd HH:mm:ss yyyy',$null)) `

The Rest of the code for the start part of the Robocopy log :

#############################################################################################
## Robocopy example command :
#############################################################################################


robocopy 'c:\test1' c:\PowerShellRoboTest /r:2 /w:5 /s /v /np |
  Tee-Object -Variable RoboLog


#############################################################################################
## Process the Output
#############################################################################################


$null,$StartBegin,$StartEnd,$StopBegin = $RoboLog | Select-String  "----" |% {$_.linenumber}

$RoboStatus = New-Object object

# Start information 

$robolog[$StartBegin..$StartEnd] | % {
  Switch -regex ($_) {
    'Started :(.*)' {
      Add-Member -InputObject $RoboStatus -Name StartTime `
       -Value ([datetime]::ParseExact($matches[1].trim(),'ddd MMM dd HH:mm:ss yyyy',$null)) `
       -MemberType NoteProperty
    }
    'Source :(.*)' {
      Add-Member -InputObject $RoboStatus -Name Source `
        -Value ($matches[1].trim()) -MemberType NoteProperty
    }
    'Dest :(.*)' {
      Add-Member -InputObject $RoboStatus -Name Destination `
        -Value ($matches[1].trim()) -MemberType NoteProperty
    }    
    'Files :(.*)' {
      Add-Member -InputObject $RoboStatus -Name FileName `
        -Value ($matches[1].trim()) -MemberType NoteProperty
    }
    'Options :(.*)' {
      Add-Member -InputObject $RoboStatus -Name Options `
        -Value ($matches[1].trim()) -MemberType NoteProperty
    }
  }
}

This results in a $robostatus object that we can check the start information on like this  :

Image may be NSFW.
Clik here to view.
image

and as the StartTime is a Object also we can for example also select the Year only.

Next thing to do is and the End information also :

# Stop Information

$robolog[$StopBegin..( $RoboLog.Count  -1)] |% {
  Switch -regex ($_) {

    'Ended :(.*)' {
        Add-Member -InputObject $RoboStatus -Name StopTime `
          -Value ([datetime]::ParseExact($matches[1].trim(),'ddd MMM dd HH:mm:ss yyyy',$null))`
          -MemberType NoteProperty
    }

    'Speed :(.*) Bytes' {
        Add-Member -InputObject $RoboStatus -Name BytesSecond `
          -Value ($matches[1].trim()) -MemberType NoteProperty
    }

    'Speed :(.*)MegaBytes' {
        Add-Member -InputObject $RoboStatus -Name MegaBytesMinute `
          -Value ($matches[1].trim()) -MemberType NoteProperty
    }    

    '(Total.*)' {
      $cols = $_.Split() |? {$_}
    }

    'Dirs :(.*)' {
      $fields = $matches[1].Split() |? {$_}
      $dirs = new-object object
      0..5 |% {
          Add-Member -InputObject $Dirs -Name $cols[$_] -Value $fields[$_] -MemberType NoteProperty
          Add-Member -InputObject $Dirs -Name 'toString' -MemberType ScriptMethod `
            -Value {[string]::Join(" ",($this.psobject.Properties |
              % {"$($_.name):$($_.value)"}))} -force
      }
      Add-Member -InputObject $RoboStatus -Name Directories -Value $dirs -MemberType NoteProperty
    }

    'Files :(.*)' {
      $fields = $matches[1].Split() |? {$_}
      $Files = new-object object
      0..5 |% {
          Add-Member -InputObject $Files -Name $cols[$_] -Value $fields[$_] -MemberType NoteProperty
          Add-Member -InputObject $Files -Name 'toString' -MemberType ScriptMethod -Value `
            {[string]::Join(" ",($this.psobject.Properties |% {"$($_.name):$($_.value)"}))} -force
      }
      Add-Member -InputObject $RoboStatus -Name files -Value $files -MemberType NoteProperty
    }

    'Bytes :(.*)' {
      $fields = $matches[1].Split() |? {$_}
      $fields = $fields |% {$new=@();$i = 0 } {
          if ($_ -match '\d') {$new += $_;$i++} else {$new[$i-1] = ([double]$new[$i-1]) * "1${_}B" }
      }{$new}

      $Bytes = new-object object
      0..5 |% {
          Add-Member -InputObject $Bytes -Name $cols[$_] `
            -Value $fields[$_] -MemberType NoteProperty
          Add-Member -InputObject $Bytes -Name 'toString' -MemberType ScriptMethod `
            -Value {[string]::Join(" ",($this.psobject.Properties |
            % {"$($_.name):$($_.value)"}))} -force
      }
      Add-Member -InputObject $RoboStatus -Name bytes -Value $bytes -MemberType NoteProperty
    }
  }
}

Now it already get's more interesting as we can do things like this now :

Image may be NSFW.
Clik here to view.
image

You can see above that we can calculate the running time now and see the count of files, be sure to take a look at the code to parse the Stop information, as you can find some nifty tricks as overriding the ToString() method on the Files and Directory collections to make them more clear in default output:

Add-Member -InputObject $Files -Name 'toString' -MemberType ScriptMethod -Value ` {[string]::Join(" ",($this.psobject.Properties |% {"$($_.name):$($_.value)"}))} -force

In the next part we will do some more text parsing as we're also going to process the Detail information from the robocopy log .

Enjoy,

Greetings /\/\o\/\/

Image may be NSFW.
Clik here to view.

PowerShell and Robocopy Part 4

The following is the complete code for the wrapper example, and I did put it on PoSHcode.org , the PowerShell code repository, :

As you can see below you can also use the Code in the Repository on your blog by using the provided link (Update try to fix codeexample output)and there are Cmdlets to retrieve the PowerShell code

And now you can dig into the details session like this :

Image may be NSFW.
Clik here to view.
image

I tried to create a in use error by opening a file with EXCEL but seems 2007 does not lock it anymore ;-), but you can create a good test log by copying your userprofile.

at the end of the code are some more examples of using the $RoboStatus object for analizing the log.  

If you find this code usefull and have some improve them feel free to post and updated version of it on PoSHcode.org .

In the next post I will add a PowerShell GUI wrapper to create Robocopy commands from a form, and a small forms lib .

Enjoy,

Greetings /\/\o\/\/

Image may be NSFW.
Clik here to view.

PowerShell FormsLib example

When I made the RoboGUI script, I will show in the next post in the robocopy series, I was experimenting with a Forms Library to make it easier to work with Forms in PowerShell, as The RoboGUI.ps1 uses it, I will post it here on my blog, It is not complete or under active development (and probably will not be anymore ) but it still might give you some ideas, hence I decided to post it outside of the series.

The goal was to make it easier, and more clear using forms code in PowerShell scripts, the library contains also a function the "read out" current controls making it possible to copy over existing settings, it is not used but I left it in for reference again.

This lib makes it possible to write code like this in a PowerShell script ;

$frmRobocopy = new-Object System.Windows.Forms.form
set-controlFormat $frmRobocopy `
  -Location '{X=46,Y=46}'`
  -Size '{Width=1223, Height=850}'`
  -Text 'RoboCopy PowerShell GUI'

you can see you can use parameters and hashTable format to describe control settings, what makes it look much nicer in code.

And the comple Library. (I will also post it on http://PoSHcode.org , but as you could see in last post I still have some problem embedding the code in my blog)

# FormsLib.ps1
# contains some helper functions to create and modify Form controls
# in a PowerShell script used by RoboGUI.ps1
#
# /\/\o\/\/
# http://thePowerShellGuy.com 

Function ConvertTo-HashTable ([string]$StringValue) {
  invoke-expression ("@$StringValue".replace(',',';')) 
}

Function ConvertTo-Point ([string]$StringValue) {
  ConvertTo-HashTable $StringValue |
      % {New-Object System.Drawing.Point([int]($_.x),[int]($_.y))}
}

Function ConvertTo-Size ([string]$StringValue) {
  ConvertTo-HashTable $StringValue |
      % {New-Object System.Drawing.Size([int]($_.Width),[int]($_.Height))}
}


filter get-PropertyList {  
  $o = $_ ; $_ | gm -MemberType Property | 
    select name, 
      @{Name='Type';Expression={$_.definition.split()[0]}},
      @{Name='Value';Expression={$o."$($_.name)"}}
}

Function Get-ControlFormat {
  Param (
    $Control,
    $properties = ('Text','Size','Location','Dock','Anchor'),
    $ExtraProperties
  ) 
  $properties += $ExtraProperties
  "Set-ControlFormat `$$($Control.name) ``"
  $Control | get-PropertyList |
    Where {$Properties -contains $_.name} |
      Foreach  {
        "  -$($_.name) '$($_.Value)'``"
      }
}

Function Set-ControlFormat {
  Param  (
    $Control
  )
  foreach ($arg in $args) {
    if ($arg.startswith('-')) {
      $Property = $arg.trim('-')
      [void] $foreach.MoveNext()
      Switch ($Property) {
        'Location' { $Control.Location = ConvertTo-Point $foreach.current ; break}
        'Size' { $Control.Size = ConvertTo-Size $foreach.current ; break}
        Default {$Control."$Property" = $foreach.current}
      }
    }
  }
}


Function get-FormControls ($psObject) {
  $form = new-object Windows.Forms.Form
  $form.Size = new-object Drawing.Size @(600,600)
  $controls = @("form") 
  $psObject.Controls |% {$controls += $_.name}
  $CB = new-object Windows.Forms.Combobox
  $cb.Size = new-object Drawing.Size @(500,21)
  $cb.Items.AddRange($controls)
  $PG = new-object Windows.Forms.PropertyGrid
  $PG.Size = new-object Drawing.Size @(500,500)
  $PG.Location = New-Object System.Drawing.Point(50 , 50)
  $form.text = "$psObject"
  $PG.selectedobject = $psObject.PsObject.baseobject
  $cb.text = 'form'
  $cb.add_TextChanged({
    if ( $this.SelectedItem -eq 'Form') {
      $PG.selectedobject = $psObject.PsObject.baseobject
    } Else {
      $PG.selectedobject = $psObject.Controls["$($this.SelectedItem)"].PsObject.baseobject
    }
  })
  $form.Controls.Add($PG)
  $form.Controls.Add($CB)
  $Form.Add_Shown({$form.Activate()})
  $form.showdialog()
}

As indicated this was a test project I did about a year ago for testing this way of working, it should not be seen as best practice but as something I was test out at the time and might contain some ideas that you could use.

more examples of using this library in a script can be found in the script in the next blogpost about RoboGUI.PS1 , for the generating part (get-controlformat / get-formControls) your on your own, but note that you can retrieve extra properties also, and the Format of Set-Controlformat, is exactly the same as the Tostring() when you list Control properties from PowerShell ;-).

Enjoy,

Greetings /\/\o\/\/

Image may be NSFW.
Clik here to view.

PowerShell and Robocopy part 5

I this part I will cover the code for the "PowerShell Robocopy GUI" as shown in PowerShell and Robocopy part 2 , before I will put everything together with some usage examples in the next and final post in this series.

What happens in this PowerShell script ?

First, I convert the Robocopy help object that we created in PowerShell and Robocopy part 1 (for convenience also added to code below) into 2 datatables, one for Switches and one for Parameters (I make that distinction here as Parameters need an inputbox next to a checkbox, hence I will display them in different DataGrids)

Then I load the FormsLib I discussed in last post PowerShell FormsLib example  that contains helper functions to configure the form controls and make this script more readable, to create a form for displaying the possible options for robocopy with the accompanying help, and update button is provided to "generate" the robocopy command for the options selected in the GUI,

*Update* to this to work in PowerShell V1 you need to have the forms libary loaded (thanks to tojo2000 for pointing me to that in the comments) 

[system.reflection.assembly]::LoadWithPartialName('system.windows.forms') 

And here is the code, :

# a helper GUI to create RoboCopy commands
#
uses FormsLib.ps1
#
#
/\/\o\/\/
#
http://thePowerShellGuy.com

# load FormsLib

. .\FormsLib.ps1

#############################################################################################
## Make RoboCopy Help Object

#############################################################################################

$RoboHelp = robocopy /? | Select-String '::'
$r = [regex]'(.*)::(.*)'
$RoboHelpObject = $RoboHelp | select `
    @{Name
='Parameter';Expression={ $r.Match( $_).groups[1
].value.trim()}},
    @{Name
='Description';Expression={ $r.Match( $_).groups[2
].value.trim()}}

$RoboHelpObject = $RoboHelpObject |% {$Cat = 'General'
} {
   
if ($_.parameter -eq '') { if ($_.Description -ne ''
){
       
$cat = $_.description -Replace 'options :',''
}
    }
else
{
       
$_ | select @{Name='Category';Expression={$cat
}},parameter,description
    }
}


# Form

##################################

$frmRobocopy = new-Object System.Windows.Forms.form
set-controlFormat $frmRobocopy
`
 
-Location '{X=46,Y=46}'
`
 
-Size '{Width=1223, Height=850}'
`
 
-Text 'RoboCopy PowerShell GUI'
`

# SwitchOptions


$dtSwitches = new-object Data.datatable

$Col =  new-object
Data.DataColumn
$col.DataType = [bool
]
$Col.ColumnName = "Enabled"


$dtSwitches.Columns.Add($Col)

"Category","Name","Description" |

 
Foreach {
   
$Col =  new-object
Data.DataColumn
   
$Col.ColumnName = $_

  
$dtSwitches.Columns.Add($Col)
  }

$RoboHelpObject |? { $_.Parameter -match '/[a-z]*$' } |

 
Foreach {
   
$row = $dtswitches
.NewRow()
   
$row.Category = $_
.Category
   
$row.Name = $_
.Parameter
   
$row.Description = $_
.Description  
   
$dtSwitches.Rows.Add($row
)
  }

# ParameterOptions


$dtParameters = new-object Data.datatable

$Col =  new-object
Data.DataColumn
$col.DataType = [bool
]
$Col.ColumnName = "Enabled"

$dtParameters.Columns.Add($Col)


"Category","Name","Value","Description" |

 
Foreach {
   
$Col =  new-object
Data.DataColumn
   
$Col.ColumnName = $_

  
$dtParameters.Columns.Add($Col)
  }

$RoboHelpObject |? { $_.Parameter -match '/[a-z]*:' } |

 
Foreach {
   
$row = $dtParameters
.NewRow()
   
$row.Category = $_
.Category
   
$row.Name = $_
.Parameter
   
$row.Description = $_
.Description  
   
$dtParameters.Rows.Add($row
)
  }


# Main Parameters


"Source",
"Destination"
,
"Files"
,
"Options"
,
"CommandLine" |% {$Y = 30
}{
 
$label = New-Object
System.Windows.Forms.Label
 
Set-ControlFormat $label
`
   
-Anchor 'Top, Left'
`
   
-Dock 'None'
`
   
-Location "{X=50,Y=$y}"
`
   
-Name "lbl$_"
`
   
-Size '{Width=100, Height=23}'
`
   
-Text $_


 
$TextBox = New-Object System.Windows.Forms.TextBox
 
Set-ControlFormat $textBox
`
   
-Anchor 'Top, Left'
`
   
-Dock 'None'
`
   
-Location "{X=150,Y=$Y}"
`
   
-Name "txt$_"
`
   
-Size '{Width=400, Height=21}'



 
$FrmRobocopy.Controls.Add($label)
 
$FrmRobocopy.Controls.Add($TextBox
)
 
$Y += 25

}

$command = "RoboCopy"

$FrmRobocopy.Controls['txtCommandLine'].Enabled = $false
$FrmRobocopy.Controls['txtOptions'].Enabled = $false
$FrmRobocopy.Controls['txtCommandLine'].Text = $command

$dgSwitches = new-object windows.forms.DataGridView
Set-ControlFormat $dgSwitches
`
 
-Dock 'Fill'
`
 
-Name 'dgOptions'
`
 
-DataSource  $dtSwitches
.psObject.baseobject  

$dgOptions = new-object
windows.forms.DataGridView
Set-ControlFormat $dgOptions
`
 
-Dock 'Fill'
`
 
-Name 'dgOptions'
`
 
-DataSource $dtParameters
.psObject.baseobject

$scOptions = new-object
System.Windows.Forms.SplitContainer
Set-ControlFormat $scOptions
`
 
-Name 'scOptions'
`
 
-Location '{X=20,Y=170}'
`
 
-Size '{Width=1170, Height=615}'
`
 
-SplitterDistance '534'
`
 
-Anchor "Top, Bottom, Left, Right"


$scOptions.Panel1.Controls.Add( $DGswitches )
$scOptions.Panel2.Controls.Add( $dgOptions
)
$FrmRobocopy.Controls.Add($scOptions
)


$Switches = "$($dtSwitches |? {$_.Enabled -eq $True} |% {$_.name})"


function Update-Commandline {
 
$Switches = "$($dtSwitches |? {$_.Enabled -eq $True} |% {$_.name})"

 
$Options = "$($dtParameters |? {$_.Enabled -eq $True} |% {$_.name.split(':')[0] + "":$($_.Value)""})"
 
$FrmRobocopy.Controls['txtOptions'].Text = "$options $Switches"
 
$Command = '$robocopy ' `
   
+ $FrmRobocopy.Controls['txtSource'
].Text `
   
+ ' ' + $FrmRobocopy.Controls['txtDestination'
].Text `
   
+ " " + $FrmRobocopy.Controls['txtFiles'
].Text `
   
+ ' ' + $FrmRobocopy.Controls['txtOptions'
].Text

 
$FrmRobocopy.Controls['txtCommandLine'].Text = $command

}

$btnUpdate = new-Object System.Windows.Forms.Button
set-controlFormat $btnUpdate
`
 
-Anchor 'Top, Left'
`
 
-Dock 'None'
`
 
-Location '{X=560,Y=30}'
`
 
-Size '{Width=75, Height=23}'
`
 
-Text 'Update'

$btnUpdate.add_Click({Update-Commandline})

$FrmRobocopy.Controls.Add($btnUpdate
)

#Edit-Form $FrmRobocopy

#
get-FormControls $FrmRobocopy

$FrmRobocopy.Add_Shown({$frmRoboCopy.Activate()
 
$dgSwitches
.AutoResizeColumns()
 
$dgOptions
.AutoResizeColumns()
})
$FrmRobocopy.ShowDialog()

Now you have all the code and as said in the next and last part of this series, I will show how to put it all the parts we created together and show how to put it all in practice by giving some more usage examples

Enjoy,

Greetings /\/\o\/\/

Image may be NSFW.
Clik here to view.