In other words, does PowerShell's Expand-Archive have an equivalent to unzip's -j command-line argument? If not, are there alternatives on Windows?
I have tried Expand-Archive -Path thing.zip -DestinationPath "somepath" -Force, which just puts the directory structure in another folder called somepath.
This function will do what you want, obviously handling of possible file collision is not implemented, up to you how you want to implement that. Currently, if a file already exists with the same name it will give you an error and skip it. The function is a simplified version of the one from this answer which does actually keep the folder structure.
If no argument is passed to the -DestinationPath parameter, the zip entries will be extracted to the current location.
using namespace System.IO
using namespace System.IO.Compression
function Expand-ZipArchive {
[CmdletBinding(DefaultParameterSetName = 'Path')]
param(
[Parameter(ParameterSetName = 'Path', Mandatory, Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName)]
[string] $Path,
[Parameter(ParameterSetName = 'LiteralPath', Mandatory, ValueFromPipelineByPropertyName)]
[Alias('PSPath')]
[string] $LiteralPath,
[Parameter()]
[string] $DestinationPath
)
begin {
Add-Type -AssemblyName System.IO.Compression
$DestinationPath = $PSCmdlet.GetUnresolvedProviderPathFromPSPath($DestinationPath)
}
process {
$arguments = switch($PSCmdlet.ParameterSetName) {
Path { $Path, $false, $false }
LiteralPath { $LiteralPath, $false, $true }
}
$null = [Directory]::CreateDirectory($DestinationPath)
foreach($item in $ExecutionContext.InvokeProvider.Item.Get.Invoke($arguments)) {
try {
$fileStream = $item.Open([FileMode]::Open)
$zipArchive = [ZipArchive]::new($fileStream, [ZipArchiveMode]::Read)
foreach($entry in $zipArchive.Entries) {
try {
# if it's a folder, exclude it
if(-not $entry.Name) {
continue
}
$path = [Path]::Combine($DestinationPath, $entry.Name)
# will throw if a file with same name exists, intended
# error handling should be implemented in `catch` block
$fs = [FileStream]::new($path, [FileMode]::CreateNew)
$wrappedStream = $entry.Open()
$wrappedStream.CopyTo($fs)
}
catch {
$PSCmdlet.WriteError($_)
}
finally {
$fs, $wrappedStream | ForEach-Object Dispose
}
}
}
catch {
$PSCmdlet.WriteError($_)
}
finally {
$zipArchive, $fileStream | ForEach-Object Dispose
}
}
}
}
Expand-ZipArchive .\myZip.zip
As a PowerShell-only way you could extract the archive to a temporary directory and then move the files to the final location, discarding directory structure.
$archiveName = 'test.zip'
$destination = 'test'
# Create temp path as a sub directory of actual destination path, so the files don't
# need to be moved (potentially) across drives.
$destinationTemp = Join-Path $destination "~$((New-Guid).ToString('n'))"
# Create temp directory
$null = New-Item $destinationTemp -ItemType Directory
# Extract to temp dir
Expand-Archive $archiveName -DestinationPath $destinationTemp
# Move files from temp dir to actual destination, discarding directory structure
Get-ChildItem $destinationTemp -File -Recurse | Move-Item -Destination $destination
# Remove temp dir
Remove-Item $destinationTemp -Recurse -Force
With PowerShell 7+, you could even move each file immediately after extraction, using the new -PassThru switch of Expand-Archive:
$archiveName = 'test.zip'
$destination = 'test'
# Create temp path as a sub directory of actual destination path, so the files don't
# need to be moved (potentially) across drives.
$destinationTemp = Join-Path $destination "~$((New-Guid).ToString('n'))"
# Create temp directory
$null = New-Item $destinationTemp -ItemType Directory
# Expand to temp dir and move to final destination, discarding directory structure
Expand-Archive $archiveName -DestinationPath $destinationTemp -PassThru |
Where-Object -not PSIsContainer | Move-Item -Destination $destination
# Remove temp dir
Remove-Item $destinationTemp -Recurse -Force
Related
I am trying to copy all files in folders and sub-folders not older than 300 minutes, but the code I got working only copies the files in the main folder, it doesn't copy the files in subfolders.
At the destination I don't want to maintain the folder structure of the original files, I just want to put all the origin files into a single specific destination folder.
This is the code I have:
Powershell -NoL -NoP -C "&{$ts=New-TimeSpan -M 300;"^
"Get-ChildItem "C:\Origin" -Filter '*.dat'|?{"^
"$_.LastWriteTime -gt ((Get-Date)-$ts)}|"^
%%{Copy-Item $_.FullName 'C:\Destination'}}"
Could someone help me out please?
Thanks in advance.
Here's a modified script for you you can save as "Copy-Unique.ps1" you can run from a batch file.
function Copy-Unique {
# Copies files to a destination. If a file with the same name already exists in the destination,
# the function will create a unique filename by appending '(x)' after the name, but before the extension.
# The 'x' is a numeric sequence value.
[CmdletBinding(SupportsShouldProcess)] # add support for -WhatIf switch
Param(
[Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)]
[Alias("Path")]
[ValidateScript({Test-Path -Path $_ -PathType Container})]
[string]$SourceFolder,
[Parameter(Mandatory = $true, Position = 1)]
[string]$DestinationFolder,
[Parameter(Mandatory = $false)]
[int]$NewerThanMinutes = -1,
[Parameter(Mandatory = $false)]
[string]$Filter = '*',
[switch]$Recurse
)
# create the destination path if it does not exist
if (!(Test-Path -Path $DestinationFolder -PathType Container)) {
Write-Verbose "Creating folder '$DestinationFolder'"
$null = New-Item -Path $DestinationFolder -ItemType 'Directory' -Force
}
# get a list of file FullNames in this source folder
$sourceFiles = #(Get-ChildItem -Path $SourceFolder -Filter $Filter -File -Recurse:$Recurse)
# if you want only files not older than x minutes, apply an extra filter
if ($NewerThanMinutes -gt 0) {
$sourceFiles = #($sourceFiles | Where-Object { $_.LastWriteTime -gt (Get-Date).AddMinutes(-$NewerThanMinutes) })
}
foreach ($file in $sourceFiles) {
# get an array of all filenames (names only) of the files with a similar name already present in the destination folder
$destFiles = #((Get-ChildItem $DestinationFolder -File -Filter "$($file.BaseName)*$($file.Extension)").Name)
# for PowerShell version < 3.0 use this
# $destFiles = #(Get-ChildItem $DestinationFolder -Filter "$baseName*$extension" | Where-Object { !($_.PSIsContainer) } | Select-Object -ExpandProperty Name)
# construct the new filename
$newName = $file.Name
$count = 1
while ($destFiles -contains $newName) {
$newName = "{0}({1}){2}" -f $file.BaseName, $count++, $file.Extension
}
# use Join-Path to create a FullName for the file
$newFile = Join-Path -Path $DestinationFolder -ChildPath $newName
Write-Verbose "Copying '$($file.FullName)' as '$newFile'"
$file | Copy-Item -Destination $newFile -Force
}
}
# you can change the folder paths, file pattern to filter etc. here
$destFolder = Join-Path -Path 'C:\Destination' -ChildPath ('{0:yyyy-MM-dd_HH-mm}' -f (Get-Date))
Copy-Unique -SourceFolder "C:\Origin" -DestinationFolder $destFolder -Filter '*.dat' -Recurse -NewerThanMinutes 300
Changed the code to now take a datetime object to compare against rather than an amount of minutes. This perhaps makes the code easier to understand, but certainly more flexible.
function Copy-Unique {
# Copies files to a destination. If a file with the same name already exists in the destination,
# the function will create a unique filename by appending '(x)' after the name, but before the extension.
# The 'x' is a numeric sequence value.
[CmdletBinding(SupportsShouldProcess)] # add support for -WhatIf switch
Param(
[Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)]
[Alias("Path")]
[ValidateScript({Test-Path -Path $_ -PathType Container})]
[string]$SourceFolder,
[Parameter(Mandatory = $true, Position = 1)]
[string]$DestinationFolder,
[string]$Filter = '*',
[datetime]$NewerThan = [datetime]::MinValue,
[switch]$Recurse
)
# create the destination path if it does not exist
if (!(Test-Path -Path $DestinationFolder -PathType Container)) {
Write-Verbose "Creating folder '$DestinationFolder'"
$null = New-Item -Path $DestinationFolder -ItemType 'Directory' -Force
}
# get a list of file FullNames in this source folder
$sourceFiles = #(Get-ChildItem -Path $SourceFolder -Filter $Filter -File -Recurse:$Recurse)
# if you want only files newer than a certain date, apply an extra filter
if ($NewerThan -gt [datetime]::MinValue) {
$sourceFiles = #($sourceFiles | Where-Object { $_.LastWriteTime -gt $NewerThan })
}
foreach ($file in $sourceFiles) {
# get an array of all filenames (names only) of the files with a similar name already present in the destination folder
$destFiles = #((Get-ChildItem $DestinationFolder -File -Filter "$($file.BaseName)*$($file.Extension)").Name)
# for PowerShell version < 3.0 use this
# $destFiles = #(Get-ChildItem $DestinationFolder -Filter "$baseName*$extension" | Where-Object { !($_.PSIsContainer) } | Select-Object -ExpandProperty Name)
# construct the new filename
$newName = $file.Name
$count = 1
while ($destFiles -contains $newName) {
$newName = "{0}({1}){2}" -f $file.BaseName, $count++, $file.Extension
}
# use Join-Path to create a FullName for the file
$newFile = Join-Path -Path $DestinationFolder -ChildPath $newName
Write-Verbose "Copying '$($file.FullName)' as '$newFile'"
$file | Copy-Item -Destination $newFile -Force
}
}
# you can change the folder paths, file pattern to filter etc. here
$destFolder = Join-Path -Path 'D:\Destination' -ChildPath ('{0:yyyy-MM-dd_HH-mm}' -f (Get-Date))
Copy-Unique -SourceFolder "C:\Origin" -DestinationFolder $destFolder -Filter '*.dat' -Recurse -NewerThan (Get-Date).AddMinutes(-300)
When you have saved the above code to let's say 'C:\Scripts\Copy-Unique.ps1' you can then call it from a batch file like:
Powershell.exe -NoLogo -NoProfile -File "C:\Scripts\Copy-Unique.ps1"
So I have an assignment where I have to create an PowerShell script that takes three parameters, "$foldername", "$filename" and "$number".
The script checks if the folder "$foldername" exists and if not, creates it. After that it creates as many new files named "$filename" as "$number" specifies. After that it reports how many files have been created and lists them.
What I have so far.
Param (
[string]$foldername,
[string]$filename,
$number=1
)
if ((Test-Path -Path $foldername) -ne $true) {
new-item -path $foldername -ItemType directory #if the folder doesn't exist, create it.
}
$new_file= $foldername+"\$_"+$filename #save the path and name of the new file to an variable
if ((Test-Path -Path $new_file* -PathType leaf) -eq $true) {
Write-Host "$filename already exists in $foldername"
break #if a file with a name that contains $filename in it exists in $foldername, break and do not create any new files.
}
$null=1..$number | foreach { new-item -path $foldername -name $_$filename } #create new files using foreach.
write-host ("Created $number new files") #tell the user how many files were created
Get-ChildItem -path $foldername | where-object Name -like *$filename* | format-table Name #show the created files in a table format, formatted by name
There are a few problems and scuffed solutions in this script, but the main problem is the creation of the new files. Since the name of the new files come from $filename, simply running the script like so:
./script.ps1 -foldername C:\users\example\testing -filename "test.txt" -number 5
Would not work since it tries to create 5 files named "test.txt" and will just return errors.
I sort of solved it by using "foreach" and naming the files $_$filename which creates
1test.txt
2test.txt
...
5test.txt
But I found out that the correct way would be:
test1.txt
test2.txt
...
test5.txt
The number should be running in the filename somehow, but I am not sure how to do that.
Bonus points if you figure out how to check if the $filename files already exist in the target folder.
It's good to use Test-Path however I don't see a need for it here, you can use $ErrorAction = 'Stop' so that if the folder exists the script would instantly stop with a warning message. On the other hand, if the folder is a new folder there is no way the files already exist.
Param (
[parameter(Mandatory)]
[string]$FolderName,
[parameter(Mandatory)]
[string]$FileName,
[int]$Number = 1
)
$ErrorActionPreference = 'Stop'
try {
$newFolder = New-Item -Path $FolderName -ItemType Directory
}
catch {
# If the folder exists, show this exception and stop here
Write-Warning $_.Exception.Message
break
}
$files = 1..$Number | ForEach-Object {
# If this is a new Folder, there is no way the files already exist :)
$path = Join-Path $newFolder.FullName -ChildPath "$FileName $_.txt"
New-Item -Path $path -ItemType File
}
Write-Host 'Script finished successfully.'
$newFolder, $files | Format-Table -AutoSize
EDIT: I might have missed the point where you want to create the files in the folder even if the folder already exists, in that case you could use the following:
Param (
[parameter(Mandatory)]
[string]$FolderName,
[parameter(Mandatory)]
[string]$FileName,
[int]$Number = 1
)
$ErrorActionPreference = 'Stop'
$folder = try {
# If the Folder exists get it
Get-Item $FolderName
}
catch {
# If it does not, create it
New-Item -Path $FolderName -ItemType Directory
}
$files = 1..$Number | ForEach-Object {
$path = Join-Path $folder.FullName -ChildPath "$FileName $_.txt"
try {
# Try to create the new file
New-Item -Path $path -ItemType File
}
catch {
# If the file exists, display the Exception and continue
Write-Warning $_.Exception.Message
}
}
Write-Host "Script finished successfully."
Write-Host "Files created: $($files.Count) out of $Number"
$files | Format-Table -AutoSize
I am trying to create a little patcher in PowerShell which first backs up the files before replacing them. But I can't get it to cooperate when the file is in a subdirectory. To simplify:
First, it creates the backup directory:
$Null = New-Item -Path 'C:\Backup' -ItemType 'Directory'
Next, I check for the file:
[System.IO.FileInfo]$FileInfo = Get-Item -LiteralPath 'C:\App\bin\executable.exe'
Now, I want this file to end up in C:\Backup\bin\executable.exe
The first thing I tried was to get the FullName and replace C:\App with C:\Backup resulting in $FileInfo | Copy-Item -Destination 'C:\Backup\bin\executable.exe'
But it keeps throwing an exception because C:\Backup\bin does not exist. I tried combinations of the -Recurse and -Container switches.
I assume the issue is that I'm setting the full path as my destination, resulting in no relative paths being created. But I don't see another way to get that target path set. I can't tell it to copy to C:\Backup\bin because there's no logical way for me to know about bin without extracting it. At which point I might as well create it in the backup directory.
But I kind of want this to be automagic (for the C:\App\multi\level\path\file.dll at a later stage). This makes the script more flexible if I change the file locations later on.
I'm probably missing something really obvious here. Any suggestions?
This is what I'm doing now, compensating for having to make the parent directories myself. I'm open to ways to optimize and of course still looking for a way to not have to explicitly create the parent directories.
Function Backup-Item {
Param (
[Parameter(Mandatory = $True)] [System.String]$Source,
[Parameter(Mandatory = $True)] [System.String]$SourceRoot,
[Parameter(Mandatory = $False)][System.Management.Automation.SwitchParameter]$Backup = $True,
[Parameter(Mandatory = $False)][System.String]$BackupDirectory
) # Param
Begin {
If (Test-Path -LiteralPath $Source) { [System.IO.FileInfo]$SourceFile = Get-Item -LiteralPath $Source }
Else { } # TODO: Break
If ($Backup) {
If (Test-Path -LiteralPath $BackupDirectory) { } # TODO: Break
Else { $Null = New-Item -Path $BackupDirectory -ItemType 'Directory' }
[System.String]$Destination = $SourceFile.FullName.Replace($SourceRoot,$BackupDirectory)
} # If
} # Begin
Process {
If ($Backup) {
[System.String]$TargetDirectory = $SourceFile.DirectoryName.Replace($SourceRoot,$BackupDirectory)
If (-not (Test-Path -LiteralPath $TargetDirectory)) { $Null = New-Item -Path $TargetDirectory -ItemType 'Directory' }
$SourceFile | Copy-Item -Destination $Destination
} # If
} # Process
End {}
} # Function Backup-Item
I don't like having to provide the SourceRoot but it's the only way to deduce the relative paths I could think of.
I will also need to make BackupDirectory mandatory but only if Backup is True (which is the default and a bit dirty since a switch should be off by default, but I wanted the ability to do -Backup:$False to override).
I would use this as a helper script in the larger patcher-script.
Why not use robocopy to copy the entire C:\Apps folder to the C:\Backup folder, like with
robocopy C:\Apps C:\Backup /MIR
If you want pure PowerShell, you can do
$sourcePath = 'C:\Apps'
$backupPath = 'C:\Backup'
Get-ChildItem $sourcePath -File -Recurse | ForEach-Object {
# create the destination folder, even if it is empty
$targetFolder = Join-Path -Path $backupPath -ChildPath $_.DirectoryName.Substring($sourcePath.Length)
$null = New-Item -ItemType Directory -Path $targetFolder -Force
# next copy the file
$_ | Copy-Item -Destination $targetFolder
}
I'm trying to write script which will copy content of files with .txt extension to one. Script is working but -recurse is not. (It dosn't copy files which are in sub folders) and I don't know why is that. This is how my script looks like:
function UnifyConfigs {
param (
$destination = "C:\temp\all.txt",
[Parameter()]
$files
)
foreach ($config in $files) {
If((Get-ChildItem $config -Recurse).LastWriteTime -gt (Get-Item $destination).LastWriteTime)
{
Clear-Content -path $destination
Set-Content -path $destination -value (Get-Content $config)
}
else {
break
}
}
}
And yes: I have tried it with -force :-)
First up, you need to move the Get-ChildItem -Recurse call to where you resolve the input string to actual files in the filesystem:
foreach ($config in Get-ChildItem $files -Recurse) {
if($config.LastWriteTime -gt (Get-Item $destination).LastWriteTime)
{
Clear-Content -path $destination
Set-Content -path $destination -value (Get-Content $config)
}
else {
break
}
}
If you just want to test that any of the input files are newer than the destination file and then overwrite the contents of the destination with all of the other txt files, that actually becomes a tad simpler - we can discard the outer loop completely:
# Discover all the files
$configFiles = Get-ChildItem $files -Recurse
# use `-gt` and the destination timestamp to "filter" all the config file timestamps
# if _any_ of them are newer that $destination, then the condition is true
if(#($configFiles.LastWriteTime) -gt (Get-Item $destination).LastWriteTime){
# pipe every file to Get-Content, and then overwrite $destination with the whole thing
$configFiles |Get-Content |Set-Content -Path $destination -Force
}
I'd also recommend refactoring the parameter names to better reflect what the expected input is ("C:\path\to*files" is a string representing a "path", it is not "files"):
function Update-UnifiedConfig {
param (
[Parameter(Mandatory = $false)]
[string]$DestinationPath = "C:\temp\all.txt",
[Parameter(Mandatory = $true)]
[string]$Path
)
$destinationLastModified = (Get-Item -LiteralPath $DestinationPath).LastWriteTime
$configFiles = Get-ChildItem $files -Recurse
if(#($configFiles.LastWriteTime) -gt $destinationLastModified){
$configFiles |Get-Content |Set-Content -LiteralPath $DestinationPath -Force
}
}
The reason I'm using -LiteralPath in most places above is because $DestinationPath is just that, -Path on the other hand will treat wildcards as expandable which is only appropriate for the $Path parameter value in this function
I need to unzip a specific directory from a zipfile.
Like for example extract the directory 'test\etc\script' from zipfile 'c:\tmp\test.zip' and place it in c:\tmp\output\test\etc\script.
The code below works but has two quirks:
I need to recursively find the directory ('script') in the zip file (function finditem) although I already know the path ('c:\tmp\test.zip\test\etc\script')
With CopyHere I need to determine the targetdirectory, specifically the 'test\etc' part manually
Any better solutions? Thanks.
The code:
function finditem($items, $itemname)
{
foreach($item In $items)
{
if ($item.GetFolder -ne $Null)
{
finditem $item.GetFolder.items() $itemname
}
if ($item.name -Like $itemname)
{
return $item
}
}
}
$source = 'c:\tmp\test.zip'
$target = 'c:\tmp\output'
$shell = new-object -com shell.application
# find script folder e.g. c:\tmp\test.zip\test\etc\script
$item = finditem $shell.NameSpace($source).Items() "script"
# output folder is c:\tmp\output\test\etc
$targetfolder = Join-Path $target ((split-path $item.path -Parent) -replace '^.*zip')
New-Item $targetfolder -ItemType directory -ErrorAction Ignore
# unzip c:\tmp\test.zip\test\etc\script to c:\tmp\output\test\etc
$shell.NameSpace($targetfolder).CopyHere($item)
I don't know about most elegant, but with .Net 4.5 installed you could use the ZipFile class from the System.IO.Compression namespace:
[Reflection.Assembly]::LoadWithPartialName('System.IO.Compression.FileSystem') | Out-Null
$zipfile = 'C:\path\to\your.zip'
$folder = 'folder\inside\zipfile'
$dst = 'C:\output\folder'
[IO.Compression.ZipFile]::OpenRead($zipfile).Entries | ? {
$_.FullName -like "$($folder -replace '\\','/')/*"
} | % {
$file = Join-Path $dst $_.FullName
$parent = Split-Path -Parent $file
if (-not (Test-Path -LiteralPath $parent)) {
New-Item -Path $parent -Type Directory | Out-Null
}
[IO.Compression.ZipFileExtensions]::ExtractToFile($_, $file, $true)
}
The 3rd parameter of ExtractToFile() can be omitted. If present it defines whether existing files will be overwritten or not.
As far as the folder location in a zip is known, the original code can be simplified:
$source = 'c:\tmp\test.zip' # zip file
$target = 'c:\tmp\output' # target root
$folder = 'test\etc\script' # path in the zip
$shell = New-Object -ComObject Shell.Application
# find script folder e.g. c:\tmp\test.zip\test\etc\script
$item = $shell.NameSpace("$source\$folder")
# actual destination directory
$path = Split-Path (Join-Path $target $folder)
if (!(Test-Path $path)) {$null = mkdir $path}
# unzip c:\tmp\test.zip\test\etc\script to c:\tmp\output\test\etc\script
$shell.NameSpace($path).CopyHere($item)
Windows PowerShell 5.0 (included in Windows 10) natively supports extracting ZIP files using Expand-Archive cmdlet:
Expand-Archive -Path Draft.Zip -DestinationPath C:\Reference