Robocopy didn't recognize options when launched from PowerShell - powershell

I'm trying to use the PowerShell cmdlet Invoke-Expression to launch RoboCopy.
In the script below, RoboCopy worked fine when the option was simply '.' but as soon as the option '/MIR' was added I got this "Invalid Parameter #3" error.
It seems that RoboCopy is having problems parsing '/MIR' and has choked on the forward slash in the option. I've tried using all sort of escaping characters to no avail!
# Source & Destination paths
#
[string]$srcPath = 'C:\folderSrc'
[string]$desPath = 'C:\folderDes'
# Example 1
# ----------
# This works - note how $option1 contains only '*.*'
#
[string]$option1 = '*.*'
[string]$line = 'RoboCopy $srcPath $desPath $option1'
Invoke-Expression "$line"
# Example 2:
# ----------
# This doesn't work - after '/MIR' is added to the option, RoboCopy seems to choke on the forward slash in '/MIR'
#
[string]$option2 = '*.* /MIR'
[string]$line = 'RoboCopy $srcPath $desPath $option2'
Invoke-Expression "$line"

I found that this (using double InvokeExpression) worked:
[string]$srcPath = 'C:\folderSrc'
[string]$desPath = 'C:\folderDes'
[string]$option = '*.* /MIR'
[string]$line = 'Invoke-Expression "RoboCopy $srcPath $desPath $option"'
Invoke-Expression "$line"
But couldn't explain why this (using single Invoke-Expression) also works:
[string]$srcPath = 'C:\folderSrc'
[string]$desPath = 'C:\folderDes'
[string]$option = '*.*'
[string]$line = 'RoboCopy $srcPath $desPath $option'
Invoke-Expression "$line"
Note that the sole difference in the 2 scenarios is the $option variable:
'*.*' vs. '*.* /MIR'
Inconsistency like this is utterly demoralizing...

Powershell doesn't expand $variables when using single quotes.
Use double quotes here:
[string]$line = "RoboCopy $srcPath $desPath $option1"
And it might make better sense to not use Invoke-Expression
RoboCopy.exe $srcPath $desPath *.* /MIR
Should work

Related

Converting a line of cmd to powershell

EDIT2: Final code below
I need help on converting some codes as I am very new to mkvmerge, powershell and command prompt.
The CMD code is from https://github.com/Serede/mkvtoolnix-batch/blob/master/mkvtoolnix-batch.bat
for %%f in (*.mkv) do %mkvmerge% #options.json -o "mkvmerge_out/%%f" "%%f"
What I've managed so far
$SourceFolder = "C:\tmp" #In my actual code, this is done using folder browser
$SourceFiles = Get-ChildItem -LiteralPath $SourceFolder -File -Include *.mkv
$SourceFiles | foreach
{
start-process "F:\Desktop\#progs\mkvtoolnix\mkvmerge.exe"
}
I'd be grateful for any help as I'm having trouble understanding and converting while learning both sides. Thank you very much.
**EDIT 2:**Here's my final working code.
Function Get-Folder($initialDirectory) {
#Prompt to choose source folder
[void] [System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms')
$FolderBrowserDialog = New-Object System.Windows.Forms.FolderBrowserDialog
$FolderBrowserDialog.Description = 'Choose the video folder'
$FolderBrowserDialog.RootFolder = 'MyComputer'
if ($initialDirectory) { $FolderBrowserDialog.SelectedPath = $initialDirectory }
[void] $FolderBrowserDialog.ShowDialog()
return $FolderBrowserDialog.SelectedPath
}
Function ExitMessage
{
#endregion Function output
Write-Host "`nOperation complete";
Write-Host -NoNewLine 'Press any key to continue...';
$null = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown');
Exit;
}
($SourceFolder = Get-Folder | select )
#Check for output folder and create if unavailable
$TestFile = "$SourceFolder" + "\mkvmerge_out"
if ((Test-Path -LiteralPath $TestFile) -like "False")
{
new-item -Path $SourceFolder -name "mkvmerge_out" -type directory
Write-Host 'Folder created';
}
#Checking for the presence of a Json file
$TestFile = (Get-ChildItem -LiteralPath $SourceFolder -File -Filter *.json)
if ($TestFile.count -eq 0)
{
Write-Host 'json file not found';
ExitMessage;
}
$TestFile = "$SourceFolder" + "\$TestFile"
#Getting the total number of files and start timer.
[Int] $TotalFiles = 0;
[Int] $FilesDone = 0;
$TotalFiles = (Get-ChildItem -LiteralPath $SourceFolder -File -Filter *.mkv).count
$PercentFiles = 0;
$Time = [System.Diagnostics.Stopwatch]::StartNew()
#Start mkvmerge process with progress bar
$mkvmergeExe = 'F:\Desktop\#progs\mkvtoolnix\mkvmerge.exe'
$JsonFile = "$TestFile" # alternatively, use Join-Path
Get-ChildItem -LiteralPath $SourceFolder -File -Filter *.mkv | ForEach-Object {
$PercentFiles = [math]::truncate(($FilesDone/$TotalFiles)*100)
Write-Progress -Activity mkvmerge -Status ("{0}% Completed; {1}/{2} done; Time Elapsed: {3:d2}:{4:d2}:{5:d2}" -f $PercentFiles, $FilesDone, $TotalFiles, $Time.Elapsed.Hours, $Time.Elapsed.minutes, $Time.Elapsed.seconds) -PercentComplete $PercentFiles;
Write-Host "Processing $_"
$f = $_.FullName
$of = "$SourceFolder\mkvmerge_out\$($_.Name)"
& $mkvmergeExe -q `#$JsonFile -o $of $f
$FilesDone++
}
Remove-Item -LiteralPath $JsonFile #Remove this line if you want to keep the Json file
$PercentFiles = [math]::truncate(($FilesDone/$TotalFiles)*100)
Write-Progress -Activity mkvmerge -Status ("{0}% Completed; {1}/{2} done; Time Elapsed: {3:d2}:{4:d2}:{5:d2}" -f $PercentFiles, $FilesDone, $TotalFiles, $Time.Elapsed.Hours, $Time.Elapsed.minutes, $Time.Elapsed.seconds) -PercentComplete $PercentFiles;
ExitMessage;
$mkvmergeExe = 'F:\Desktop\#progs\mkvtoolnix\mkvmerge.exe'
$optionsFile = "$SourceFolder\options.json" # alternatively, use Join-Path
Get-ChildItem -LiteralPath $SourceFolder -File -Filter *.mkv | ForEach-Object {
$f = $_.FullName
$of = "$SourceFolder\mkvmerge_out\$($_.Name)"
& $mkvmergeExe `#$optionsFile -o $of $f
}
Note that your cmd code assumes that it's operating in the current directory, while your PowerShell code passes a directory explicitly via $SourceFolder; therefore, the options.json file must be looked for in $SourceFolder and too, and the output file path passed to -o must be prefixed with $SourceFolder too which is achieved via expandable strings ("...") .
The main points to consider:
for %%f in (*.mkv) has no direct counterpart in PowerShell; you correctly used Get-ChildItem instead, to get a list of matching files, which are returned as System.IO.FileInfo instances.
However, -Include won't work as intended in the absence of -Recurse (unless you append \* - see this GitHub issue; -Filter does, and is also the faster method, but it has its limitations and legacy quirks (see this answer).
While PowerShell too allows you to execute commands whose names or paths are stored in a variable (or specified as a quoted string literal), you then need &, the call operator, to invoke it, for syntactic reasons.
Inside a script block ({ ... }) passed to the ForEach-Object cmdlet, automatic variable $_ represents the pipeline input object at hand.
$_.FullName ensures that the System.IO.FileInfo input instances are represented by their full path when used in a string context.
This extra step is no longer necessary in PowerShell [Core] 6+, where System.IO.FileInfo instances thankfully always stringify as their full paths.
The # character is preceded by ` (backtick), PowerShell's escape character, because # - unlike in cmd - is a metacharacter, i.e. a character with special syntactic meaning. `# ensures that the # is treated verbatim, and therefore passed through to mkvmerge.
Alternatively, you could have quoted the argument instead of escaping just the #: "#$optionsFile"
See this answer for background information.
You generally do not need to enclose arguments in "..." in PowerShell, even if they contain spaces or other metacharacters.

Powershell dot-sourcing variables inside variables not expanding

I've got the following problem:
Say I have 2 powershell scripts, named A.ps1 and conf.ps1.
conf.ps1 contents are just a few vars that will be dot-sourced by A.ps1, like this:
$dateLogs = Get-Date -UFormat '%Y%m%d'
$installDir = 'C:\Gcloud\'
$logDir = '$installDir\GcloudLogs'
$logFile = '$logDir\$dateLogs\logFile.txt'
When imported, funny thing is that $dateLogs is expanded and in the debugger I can see "..\20190805\logFile.txt" but $installDir, for some reason, won't expand.
So instead of having $logFile = "C:\Gcloud\GcloudLogs\20190805\logFile.txt" I end up having "$installDir\GcloudLogs\20190805\logFile.txt"and that $installDir won't ever expand to its real value.
Am I missing something?
Any lead would be much appreciatd since I've been struggling for a long time with this. I tried several things like:
- ${$installDir}\GcloudLogs
- $($installDir\GcloudLogs)
- $(${installDir}\GcloudLogs)
With single quotes, double quotes and no quotes at all... None of that worked out.
Thank you all beforehand.
In order to not confuse single or double quotes and to save you from getting paths with double backslashes, it is always safer to use the Join-Path cmdlet.
$dateLogs = '{0:yyyyMMdd}' -f (Get-Date) # a more 'PowerShelly' way of formatting a date
$installDir = 'C:\Gcloud'
$logDir = Join-Path -Path $installDir -ChildPath 'GcloudLogs'
$logFile = Join-Path -Path $logDir -ChildPath "$dateLogs\logFile.txt" # use double-quotes here
It is also possible to use the .NET [System.IO.Path]::Combine() function
$dateLogs = '{0:yyyyMMdd}' -f (Get-Date)
$installDir = 'C:\Gcloud'
$logDir = [System.IO.Path]::Combine($installDir, "GcloudLogs")
$logFile = [System.IO.Path]::Combine($logDir, $dateLogs, "logFile.txt")
Both methods will create these paths:
$logDir --> C:\Gcloud\GcloudLogs
$logFile --> C:\Gcloud\GcloudLogs\20190805\logFile.txt
There's a difference between single and double quotes in string constants in Powershell. Try using double quotes instead.

Re-acl from text file

This one is stumping me. I am trying to do this with PowerShell, but it does not have to be...
Basically, I have a text file containing ACLs information for a large directory structure that has been migrated (I realize there is much to be desired from the contents, but we no longer can access the original filesystem, so this is what we have to work with).
I need to generate a batch file to re-acl the new (copied) file system.
So in a nutshell, I need to convert text like this (sorry for the scroll, but I am trying to preserve the line spacing):
Path: \\Share.Domain.com\Directory01$\Subdirectory01
AccessToString : DOMAIN\Group01-Knuckleheads Allow ReadAndExecute, Synchronize
BUILTIN\Administrators
Path: \\Share.Domain.com\Directory02$\Subdirectory01
AccessToString : DOMAIN\Different-Group02 Allow FullControl
BUILTIN\Administrators Allow FullControl
Into new files (or the same files, doesn't matter really) with the content like this:
ICacls "\\Share.Domain.com\Directory01$\Subdirectory01" /Grant "DOMAIN\Group01-Knuckleheads":(OI)(CI)R,X,S /t /c /l /q /inheritance:r
ICacls "\\Share.Domain.com\Directory01$\Subdirectory01" /Grant "BUILTIN\Administrators":(OI)(CI)F /t /c /l /q /inheritance:r
ICacls "\\Share.Domain.com\Directory02$\Subdirectory01" /Grant "DOMAIN\Different-Group02":(OI)(CI)F /c /l /q /inheritance:r
ICacls "\\Share.Domain.com\Directory02$\Subdirectory01" /Grant "BUILTIN\Administrators":(OI)(CI)F /c /l /q /inheritance:r
My semi-pathetic attempt to work this out are still progressing, but I realize that this foe is greater than my mojo:
#$ACLs = Get-Content C:\Scripts\Test\AndTest.txt
#ForEach-Object ($ACL in $ACLs)
#{
#Figure out how to break strings into variables....
#}
#Declare Var - Need to populate from the imported text file
$FilePath = "\\Share.Domain.com\Directory01$\Subdirectory01"
$GroupName = "DOMAIN\Domain Admins"
$TestPerm = "ReadAndExecute"
If ($TestPerm = "FullControl"){$Perms = "F"}
Elseif ($TestPerm = "ReadAndExecute"){$Perms = "RX"}
Elseif ($TestPerm = "Modify"){$Perms = "M"}
Elseif ($TestPerm = "Deny"){$Perms = "D"}
Elseif ($TestPerm = "Read"){$Perms = "R"}
Elseif ($TestPerm = "Write"){$Perms = "W"}
cls
#Build icacls string -Test Output
Write-Host "ICacls ""$FilePath"" /Grant ""$GroupName"":(OI)(CI)$Perms /t /c /l /q /inheritance:r"
#Write icacls batch file
#Out-File "C:\Scripts\Test\re-acl.cmd" "ICacls ""$FilePath"" /Grant ""$GroupName"":(OI)(CI)$Perms /t /c /l /q /inheritance:r"
I realize that there is a bunch of work to do here & I am just starting to figure this out. For example, I am thinking I need to list the permissions into an array and build the string as such. There are also bound to be special permissions etc...
But for now, I am trying to figure out how to import the text file and then break it down into variables... Like I said, I am working in Powershell, but it really can be anything... VB or Python perhaps?
Big thanks in advance!
Ok, I think most of your issue is parsing the text, so that's mainly what I helped with here. I did build the strings, and if you have more permissions that you want to include you should be able to figure out how to add them to the switch. For this we will want to read the entire text file in as a large multi-line string. Then we split it up into chunks based on the keyword "Path:". Then for each record I get the path out as a string, grab the permissions, and parse out accounts, allow/deny, and individual accesses. Then I convert the accesses to short versions accepted by icacls, and build the arguments out as a formatted string.
I output to the host, but once you are satisfied that it looks right to you, you can remove the Write-Host " and the trailing " and it will just execute it (assuming icacls is in the same folder that you're in, or in the PATH environment variable).
$Text = Get-Content C:\Scripts\Test\AndTest.txt -Raw
$Records = $Text -split "(?s)(Path:.*?)(?=Path:|$)"|?{$_}
#Loop through each file/ACL
ForEach($Record in $Records){
$FilePath = ($Record -split "[\r\n]+")[0].Substring(6)
$PermRecords = ($Record -split "(?s)AccessToString : "|Select -skip 1) -split "[\r\n]+"|?{$_}
$Perms = $PermRecords|?{$_ -match "(.*\\.*?)\s+(Allow|Deny)(.*)$"}|%{[pscustomobject]#{'Account'=$Matches[1].Trim();'Type'=$Matches[2];'Perms'=$Matches[3].Trim()}}
#Loop through each perm for the current file
$Perms | %{
#Convert friendly names to abbreviations
$ShortPerms = ''
Switch -regex ($_.Perms){
"FullControl" {$ShortPerms = "F";Continue}
"ReadAndExecute" {$ShortPerms += "RX,"}
"Synchronize" {$ShortPerms += "S,"}
"Modify" {$ShortPerms += "M,"}
"Read(?=,|$)" {$ShortPerms += "R,"}
"Write" {$ShortPerms += "W,"}
}
$ShortPerms = $ShortPerms.TrimEnd(',')
$Arguments = '"{0}" /{4} "{1}":(OI)(CI)({2}) /t /c /l /q /inheritance:r' -f $FilePath, $_.Account, $ShortPerms,$(If($_.Type -eq 'Allow'){'Grant'}else{'Deny'})
write-host "& ICAcls $Arguments"
}
}
If the -Raw argument doesn't work for you, you can work around that with
(Get-Content C:\Scripts\Test\AndTest.txt) -join "`r`n"
Let me know if you have questions or issues.

Powershell with Robocopy and Arguments Passing

I'm trying to write a script that uses robocopy. If I were just doing this manually, my command would be:
robocopy c:\hold\test1 c:\hold\test2 test.txt /NJH /NJS
BUT, when I do this from powershell, like:
$source = "C:\hold\first test"
$destination = "C:\hold\second test"
$robocopyOptions = " /NJH /NJS "
$fileList = "test.txt"
robocopy $source $destination $fileLIst $robocopyOptions
I get:
-------------------------------------------------------------------------------
ROBOCOPY :: Robust File Copy for Windows
-------------------------------------------------------------------------------
Started : Fri Apr 10 09:20:03 2015
Source - C:\hold\first test\
Dest - C:\hold\second test\
Files : test.txt
Options : /COPY:DAT /R:1000000 /W:30
------------------------------------------------------------------------------
ERROR : Invalid Parameter #4 : " /NJH /NJS "
However, if I change the robocopy command to
robocopy $source $destination $fileLIst /NJH /NJS
everything runs successfully.
So, my question is, how can I pass a string as my robocopy command options (and, in a larger sense, do the same for any given external command)
Start robocopy -args "$source $destination $fileLIst $robocopyOptions"
or
robocopy $source $destination $fileLIst $robocopyOptions.split(' ')
Use the arrays, Luke. If you specify an array of values, PowerShell will automatically expand them into separate parameters. In my experience, this is the most reliable method. And it doesn't require you to mess with the Start-Process cmdlet, which is in my opinion is overkill for such tasks.
This trick is from the best article I've seen on the PowerShell behavior towards external executables: PowerShell and external commands done right.
Example:
$source = 'C:\hold\first test'
$destination = 'C:\hold\second test'
$robocopyOptions = #('/NJH', '/NJS')
$fileList = 'test.txt'
$CmdLine = #($source, $destination, $fileList) + $robocopyOptions
& 'robocopy.exe' $CmdLine
You can't use a string to pass options in that way because when you write
robocopy $source $destination $fileList $robocopyOptions
PowerShell will evaluate the last variable ($robocopyOptions) as a single string and it will quote it. This means robocopy will get "/NJH /NHS" (single string, quoted) on its command line. (Obviously not the intent.)
For details on how to work around these kinds of issues, see here:
http://windowsitpro.com/powershell/running-executables-powershell
The article includes the following function:
function Start-Executable {
param(
[String] $FilePath,
[String[]] $ArgumentList
)
$OFS = " "
$process = New-Object System.Diagnostics.Process
$process.StartInfo.FileName = $FilePath
$process.StartInfo.Arguments = $ArgumentList
$process.StartInfo.UseShellExecute = $false
$process.StartInfo.RedirectStandardOutput = $true
if ( $process.Start() ) {
$output = $process.StandardOutput.ReadToEnd() `
-replace "\r\n$",""
if ( $output ) {
if ( $output.Contains("`r`n") ) {
$output -split "`r`n"
}
elseif ( $output.Contains("`n") ) {
$output -split "`n"
}
else {
$output
}
}
$process.WaitForExit()
& "$Env:SystemRoot\system32\cmd.exe" `
/c exit $process.ExitCode
}
}
This function will let you run an executable in the current console window and also let you build an array of string parameters to pass to it.
So in your case you could use this function something like this:
Start-Executable robocopy.exe $source,$destination,$fileList,$robocopyOptions
Putting the options in separate arguments worked for me. Using Robocopy for copying excluding any CSV files.
$roboCopyPath = $env:ROBOCOPY_PATH
$otherLogsPath = [System.IO.Path]::Combine($basePath, "Logs-Other")
$atrTestResults = [System.IO.Path]::Combine($Release, $BuildNumber)
$ResultsSummary = [System.IO.Path]::Combine($basePath, "Result")
$robocopyOptions = #("/log:$otherLogsPath\robocopy.log", '/xf', '*.csv')
$CmdLine = #($atrTestResults, $ResultsSummary) + $robocopyOptions
&$roboCopyPath $CmdLine

Executing external command from PowerShell is not accepting a parameter

I am executing the following code attempting to execute the 7z.exe command to unzip files.
$dir contains the user input of the path to the zip file which can contain spaces of course! And $dir\temp2 below is a directory that I previously created.
Get-ChildItem -path $dir -Filter *.zip |
ForEach-Object {
$zip_path = """" + $dir + "\" + $_.name + """"
$output = " -o""$dir\temp2"""
&7z e $zip_path $output
}
When I execute it I get the following from 7z.exe:
7-Zip [64] 9.20 Copyright (c) 1999-2010 Igor Pavlov 2010-11-18
Processing archive: C:\test dir\test.zip
No files to process
Files: 0
Size: 0
Compressed: 50219965
If I then copy the value from $zip_path and $output to form my own cmd line it works!
For example:
7z e "c:\test dir\test.zip" -o"c:\test output"
Now, I can reproduce the same message "no files to process" I get when I execute within PowerShell by using the following cmd in cli.
7z e "c:\test dir\test.zip" o"c:\test output"
So, it seems that PowerShell is removing the dash char from my -o option. And yes, it needs to be -o"C:\test output" and not -o "c:\test output" with 7z.exe there is no space between the -o parameter and its value.
I am stumped. Am I doing something wrong or should I be doing this a different way?
I can never get Invoke-Expression (alias = &) to work right either, so I learned how to use a process object
$7ZExe = (Get-Command -CommandType Application -Name 7z )
$7ZArgs = #(
('-o"{0}\{1}"' -f $dir, $_.Name),
('"{0}\{1}"' -f $dir, 'temp2')
)
[Diagnostics.ProcessStartInfo]$7Zpsi = New-Object -TypeName:System.Diagnostics.ProcessStartInfo -Property:#{
CreateNoWindow = $false;
UseShellExecute = $false;
Filename = $7ZExe.Path;
Arguments = $7ZArgs;
WindowStyle = 'Hidden';
RedirectStandardOutput = $true
RedirectStandardError = $true
WorkingDirectory = $(Get-Location).Path
}
$proc = [System.Diagnostics.Process]::Start($7zpsi)
$7ZOut = $proc.StandardOutput
$7ZErr = $proc.StandardError
$proc.WaitForExit()
I was able to duplicate the exact issue and tried numerous combinations escaping the -o switch and escaping quotes " and what not.
But as one answer mentioned Sysinternals, and I used Process Monitor to find out the format it was passing to 7z.exe. Things that work on a plain commandline doesn't work inside PowerShell the same way.
For example, if I tried to construct parameters inside PowerShell just like cmdline it would fail. I.e., -o"C:\scripts\so\new folder" doesn't work. But if you include the -o switch inside quotes then PowerShell passes the string "-oC:\scripts\so\new folder" which 7z.exe is happy to accept. So I learned that 7z.exe would accept both the formats such as
"C:\Program Files\7-zip\7z.exe" e "C:\scripts\so\new folder.zip" -o"C:\scripts\so\new folder"
and
"C:\Program Files\7-zip\7z.exe" e "C:\scripts\so\new folder.zip" "-oC:\scripts\so\new folder"
And both examples contain spaces in them.
[string]$pathtoexe = "C:\Program Files\7-Zip\7z.exe"
$dir = "C:\scripts\so"
$output = "$dir\new folder"
Get-ChildItem -path $dir -Filter *.zip | % {
[array]$marguments = "e",$_.FullName,"-o$output";
& $pathtoexe $marguments
}
Another approach in PowerShell V3 is to escape the PowerShell parsing feature. You can use the --% command to tell PowerShell to stop parsing any more commands like this.
$zipfile = "C:\scripts\so\newfolder.zip"
$destinationfolder = "C:\scripts\so\New Folder"
[string]$pathtoexe = "C:\Program Files\7-Zip\7z.exe"
& $pathtoexe --% e "C:\scripts\so\newfolder.zip" -o"C:\scripts\so\new folder"
Using the --% syntax, you type commands just like you would type them on the command line. I tested this logic, and it extracts files to the destination folder.
To learn more about --%, check PS> help about_parsing.
The issue with this approach is after --% it is not possible to include a variable. The solution to this issue is to just include the --% as another string variable and pass it like this. And this approach is similar to the commandline approach which wasn't working originally.
[string]$pathtoexe = "C:\Program Files\7-Zip\7z.exe"
$dir = "C:\scripts\so"
$output = "$dir\new folder"
Get-ChildItem -path $dir -Filter *.zip | % {
$zipfile = $_.FullName;
[string]$formatted = [System.String]::Concat("e ", """$zipfile"""," -o""$output""");
[string]$stopparser = '--%';
& $pathtoexe $stopparser $formatted;
}
Using the excellent Process Explorer from the Windows Sysinternals suite I was able to observe some very interesting behavior. I simplified your command line a little as seen below:
dir -Path $dir -Filter *.zip |
select FullName |
% { & 7za.exe e $_ "-o$dir\tmp" }
This was actually invoking the following command line according to Process Explorer:
C:\temp\7za.exe #{FullName="C:\temp\test.zip"} -oC:\temp\test
Telling PowerShell to expand the FullName property forces it out of the hashmap and treats it as a regular string which 7-Zip can deal with:
dir -Path $dir -Filter *.zip |
select -ExpandProperty FullName |
% { & 7za.exe e $_ "-o$dir\tmp" }
There may still be other issues like dealing with spaces in file names that I really didn't consider or account for, but I thought it was worth adding a note that PowerShell (v2 in this case) wasn't quite passing the parameters as you might expect.