I'm trying to create small script in powershell that would move files and directories to correct localizations. I made the following command:
Get-ChildItem -Path '.\list\' | ForEach-Object { if ($($_.Name) -like '*[1]*') {
$file = $($_.Name)
$path = $($_.FullName)
echo "$file ==> $path"
Move-Item -Path $path -Destination .\[1]\}}
and it detects correct files and directories, but doesn't move them.
Then I decided to modify command a bit and create hardlinks instead:
Get-ChildItem -Path '.\list\' | ForEach-Object { if ($($_.Name) -like '*[1]*') {
$file = $($_.Name)
$path = $($_.FullName)
echo "$file ==> $path"
New-Item -Path ".\``[1``]\" -Name $file -Type HardLink -Target "$path"}}
and I received the following response (cut to only 1 loop):
[1] dir1 ==> D:\test_move\list\[1] dir1
New-Item:
Line |
5 | New-Item -Path ".\``[1``]\" -Name $file -Type HardLink -Target "$path …
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
| Cannot find path 'D:\test_move\list\[1] dir1' because it does not exist.
The same error appears both with and without administrative privileges.
What do I have to do to make it work?
Try the following:
Get-ChildItem -LiteralPath .\list -File -Filter '*[1]*' | ForEach-Object {
$file = $_.Name
$path = $_.FullName
"$file ==> $path" # implicit `echo` aka `Write-Output`
New-Item -Force -Type HardLink `
-Path (Join-Path .\[1] $file) `
-Target ([WildcardPattern]::Escape($path)) ` # !! see PowerShell Core comments below
-WhatIf
}
Note: The -WhatIf common parameter in the command above previews the operation. Remove -WhatIf once you're sure the operation will do what you want.
-Filter '*[1]*' prefilters the Get-ChildItem output to only include files whose name contains substring [1] verbatim, because the -Filter parameter uses a filesystem-native wildcard language that does not treat [ and ] as metacharacters.
By contrast, with PowerShell's more powerful wildcard patterns, '*[1]*' would match any name that contains just 1, because the [...] is interpreted as a character set or range. With the -like, the wildcard matching operator operator, you'd have to use '*`[1`]*' (escaping of the metacharacters to be interpreted verbatim with `) to find verbatim [1] substrings.
-File limits matching items to files, because hardlinks are only supported for files, not also directories.
-Path (Join-Path .\[1] $file) use only a -Path argument - rather than a directory-path-only -Path argument combined with a filename-only -Name argument - which ensures that the argument is treated as a literal (verbatim) path, without interpretation of wildcard metacharacters such as [ and ].
Regrettably, combining -Path with -Name causes the -Path argument to be interpreted as a wildcard pattern.
-Force creates the target directory on demand, if needed, but note that it would also replace any preexisting target file.
Windows PowerShell: ([WildcardPattern]::Escape($path)) escapes the -Target (aka -Value) argument (target path) in order to treat it verbatim, because it is - unfortunately - interpreted as a wildcard pattern. Not performing this escaping prompted the error you saw.
Caveat:
In PowerShell [Core] 7+, a breaking change was approved in GitHub proposal #13136 to - more sensibly - treat the -Target argument as a literal (verbatim) path, in which case you would simply use -Target $path.
However, as of PowerShell 7.2.2 this change isn't implemented yet, and, unfortunately, targeting a path that contains [ and ] is currently broken altogether - see GitHub issue #14534.
As of PowerShell 7.2.2 you must escape twice(!) to make it work: ([WildcardPattern]::Escape([WildcardPattern]::Escape($path)))
Note that many, but not all, file-processing cmdlets offer a -LiteralPath parameter to explicitly pass paths to be taken literally (verbatim), whereas the -Path parameter - usually the implied parameter for the first positional argument - is designed to accept wildcard patterns.
Therefore, your could have made your original approach with Move-Item work as follows:
# Ensure that the target dir. exists.
# No escaping needed for -Path when not combined with -Name.
$null = New-Item -Type Directory -Path .\[1] -Force
# Move the file, targeted with -LiteralPath, there.
# No escaping needed for -Destination.
Move-Item -LiteralPath $path -Destination .\[1]\
Note: Unlike with New-Item, Move-Item's -Force does not create the target directory on demand. On the flip side, Move-Item's -Destination more sensibly interprets its argument literally (verbatim), unlike New-Item's -Target parameter.
Related
I am struggling with a PowerShell script to return .json file types in all users %APPDATA% folders. When I run it in ISE, it returns "Get-ChildItem : Access is denied" and when I run ISE as admin, no output is returned.
Here is the script I am working with:
Get-ChildItem C:\Users\*\AppData\Roaming -Filter '*.json' -File -Recurse -Force -ErrorAction SilentlyContinue |
ForEach-Object {
Write-Host $('.json found in {1}' -f $_.Name, ([System.IO.Path]::GetDirectoryName($_.FullName)))
}
Any help would be greatly appreciated!
tl;dr
It is the use of the -File switch that prevents your command from working - see next section.
Given that you're filtering by *.json and that directories are unlikely to match that pattern, you can simply omit it.
If you do need to guard against that, insert a Where-Object { -not $_.PSIsContainer } pipeline segment after the Get-ChildItem call; in PowerShell (Core) 7+, you can simplify to Where-Object -Not PSIsContainer
# Do NOT use -File
# Note: Run from an ELEVATED session (as admin), which is necessary to
# access other users' directories.
Get-ChildItem C:\Users\*\AppData\Roaming -Filter '*.json' -Recurse -Force -ErrorAction SilentlyContinue |
ForEach-Object { '{0} found in {1}' -f $_.Name, $_.DirectoryName }
The reason that Get-ChildItem's -File doesn't work in your case is that you're combining it with a -Path value that is an actual wildcard expression rather than a literal path:
The wildcard expression is resolved first and - perhaps surprisingly - if directories are among the matching paths, they are returned as themselves, rather than listing their children (what is inside them); the latter only happens with literal input paths.
In your case, only directories match and the -File switch is therefore applied to them, not to their children. Since directories don't match the -File switch (by definition aren't files), there is no output.
Whether or not the -Recurse switch is also present then makes no difference, given that there's nothing to recurse into.
As an aside:
If you wanted to make your command work without -Recurse, -File can be made to work, but only if you append \* to your input wildcard (the positionally implied -Path argument), so as to force enumeration of the children to match the -File and the -Filter against:
# Without recursion and a trailing /*, -File works
Get-ChildItem C:\Users\*\AppData\Roaming\* -File -Filter '*.json'
However, since PowerShell then needs to enumerate all children first, it is then simpler and more efficient to omit -Filter and append its pattern directly to the input wildcard:
# No -Filter, pattern appended to input path
Get-ChildItem C:\Users\*\AppData\Roaming\*.json -File
Could you help me again with a powershell script?
I want to check if multiple folders exist, if they exist then delete the complete folder.
Also provide information if the folder has been deleted or information if the folder does not exist.
I now use the script below for multiple files. (thanks to good help)
I want to use the same script for 1 or more folders.
For example, delete folder c:\test1\ and c:test2
Folders may be deleted, even if they still contain files.
$paths = "c:\test\1.txt", "c:\test\2.txt", "c:\test\3.txt"
foreach($filePath in $paths)
{
if (Test-Path $filePath) {
Remove-Item $filePath -verbose
} else {
Write-Host "Path doesn't exits"
}
}
I'm not super handy with powershell, hope you can help me with this again.
Thanks
Tom
To remove a directory (folder) that has content, you must use the -Recurse switch with Remove-Item - otherwise, an interactive confirmation prompt is presented.
A given path existing doesn't necessarily mean that it is a directory - it may be a file. To specifically test if a given path is a directory / file, use -PathType Container / -PathType Leaf with Test-Path.
While only strictly necessary when paths happen to contain [ characters, the robust way to pass literal paths is via the -LiteralPath parameter that file-processing cmdlets support - by contrast, the first positional argument typically binds to the -Path parameter (e.g., Test-Path foo is the same as Test-Path -Path foo), which interprets its argument(s) as wildcard expressions.
Applied to your use case (note that no attempt is made to distinguish files from directories):
# Input directory paths.
$paths = 'c:\test1', 'c:\test2', 'c:\test3'
foreach ($path in $paths) {
if (Test-Path -LiteralPath $path) {
Remove-Item -LiteralPath $path -Verbose -Recurse -WhatIf
} else {
"Path doesn't exist: $path"
}
}
Note: The -WhatIf common parameter in the command above previews the operation. Remove -WhatIf once you're sure the operation will do what you want.
Another, more efficient option is to use Get-Item to get objects representing the file-system items, if they exist, and pipe them to Remove-Item:
$paths = 'c:\test1', 'c:\test2', 'c:\test3'
Get-Item -LiteralPath $paths -ErrorAction SilentlyContinue -ErrorVariable errs |
Remove-Item -Recurse -Verbose -WhatIf
if ($errs) {
"The following path(s) do not exist: $($errs.TargetObject)"
}
Note the use of -ErrorAction SilentlyContinue to silence errors resulting from nonexistent paths, and -ErrorVariable errs in order to collect these errors in self-chosen variable $errs.
The .TargetObject property of the [System.Management.Automation.ErrorRecord] instances collected in $errs then contains the path that triggered the error, resolved to a full path.
I am a beginner and do not know where the error is in my PowerShell script. After a long Google search and a lot of trial and error, I haven't found a solution.
Without LiteralPath the script works. However, I need LiteralPath so that I can use German letters like "ä" in paths.
$d = Get-ChildItem -LiteralPath "G:\ä\*.jpg" | resolve-path -LiteralPath | get-random -count 200
Move-Item -LiteralPath $d -destination G:\ä\ä
G:\ä\*.jpg is a wildcard pattern, so by definition it won't work with -LiteralPath, which uses its arguments literally (verbatim).
Non-ASCII-range characters such as ä should always work in file paths in PowerShell (with PowerShell-native commands), whether or not you use -Path with a wildcard pattern or -LiteralPath with a literal path.
The following streamlined version of your command should work; if it doesn't, the problem is most likely unrelated to non-ASCII-range characters in the paths:
Get-ChildItem -LiteralPath G:\ä -Filter *.jpg |
Get-Random -Count 200 |
Move-Item -Destination G:\ä\ä -WhatIf
Note: The -WhatIf common parameter in the command above previews the operation. Remove -WhatIf once you're sure the operation will do what you want.
Function Zip
{
Param
(
[string]$zipFile
,
[string[]]$toBeZipped
)
$CurDir = Get-Location
Set-Location "C:\Program Files\7-Zip"
.\7z.exe A -tzip $zipFile $toBeZipped | Out-Null
Set-Location $CurDir
}
$Now = Get-Date
$Days = "60"
$TargetFolder = "C:\users\Admin\Downloads\*.*"
$LastWrite = $Now.AddDays(-$Days)
$Files = Get-Childitem $TargetFolder -Recurse | Where {$_.LastWriteTime -le "$LastWrite"}
$Files
Zip C:\Users\Admin\Desktop\TEST.zip $Files
I am testing out this script I found online. My problem is that instead of zipping the files in the target folder, it is copying and zipping the contents of the 7-zip program file folder. What could cause this? Thanks in advance
Pass the files as full paths to the Zip function, using their .FullName property (PSv3+ syntax):
Zip C:\Users\Admin\Desktop\TEST.zip $Files.FullName
The problem is that, in Windows PowerShell, the [System.IO.FileInfo] instances returned by Get-ChildItem situationally[1] stringify to their file names only, which is what happened in your case, so your Zip function then interpreted the $toBeZipped values as relative to the current location, which is C:\Program Files\7-Zip at that point.
That said, it's better not to use Set-Location in your function altogether, so that in the event that you do want to pass actual relative paths, they are correctly interpreted as relative to the current location:
Function Zip {
Param
(
[Parameter(Mandatory)] # make sure a value is passed
[string]$zipFile
,
[Parameter(Mandatory)] # make sure a value is passed
[string[]]$toBeZipped
)
# Don't change the location, use & to invoke 7z by its full path.
$null = & "C:\Program Files\7-Zip\7z.exe" A -tzip $zipFile $toBeZipped
# You may want to add error handling here.
}
[1] When Get-ChildItem output stringifies to file names only:
Note:
If Get-ChildItem output is to be passed to other file-processing cmdlets, say Rename-Item, the problem can be bypassed by providing input to them via the pipeline, which implicitly binds to the target cmdlet's -LiteralPath parameter by full path - see this answer for more information.
The related Get-Item cmdlet output always stringifies to the full path, fortunately.
In PowerShell (Core) v6.1+, Get-ChildItem too always stringifies to the full path, fortunately.
The following therefore only applies to Get-ChildItem in Windows PowerShell:
The problem is twofold:
Even PowerShell's built-in cmdlets bind file / directory arguments (parameter values - as opposed to input via the pipeline) not as objects, but as strings (changing this behavior is being discussed in GitHub issue #6057).
Therefore, for robust argument-passing, you need to ensure that your Get-ChildItem output consistently stringifies to full paths, which Get-ChildItem does not guarantee - and it's easy to forget when name-only stringification occurs or even that you need to pay attention to it at all.
Always passing the .FullName property values instead is the simplest workaround or, for reliable operation with any PowerShell provider, not just the filesystem, .PSPath.
[System.IO.FileInfo] and [System.IO.DirectoryInfo] instances output by a Get-ChildItem command stringify to their file names only, if and only if:
If one or more literal directory paths are passed to -LiteralPath or -Path (possibly as the 1st positional argument) or no path at all is passed (target the current location); that is, if the contents of directories are enumerated.
and does not also use the -Include / -Exclude parameters (whether -Filter is used makes no difference).
By contrast, whether or not the following are also present makes no difference:
-Filter (optionally as the 2nd positional argument, but note that specifying a wildcard expression such as *.txt as the 1st (and possibly only) positional argument binds to the -Path parameter)
-Recurse (by itself, but note that it is often combined with -Include / -Exclude)
Example commands:
# NAME-ONLY stringification:
Get-ChildItem | % ToString # no target path
Get-ChildItem . | % ToString # path is literal dir.
Get-ChildItem . *.txt | % ToString # path is literal dir., combined with -Filter
# FULL PATH stringification:
Get-ChildItem foo* | % ToString # non-literal path (wildcard)
Get-ChildItem -Recurse -Include *.txt | % ToString # use of -Include
Get-ChildItem file.txt | % ToString # *file* path
If you (temporarily) disable the |Out-Null you'll see what error msg pass along.
$Files contains objects not just an array of file names.
By default powershell tries to stringify this using the Name property which doesn't contain the path - so 7zip can't find the files as you also change the path to the 7zip folder (and -recurse collecting $files)
So change the line
$Files = Get-Childitem $TargetFolder -Recurse | Where {$_.LastWriteTime -le "$LastWrite"}
and append
| Select-Object -ExpandProperty FullName
A slightly reformatted verson ofyour source:
Function Zip{
Param (
[string]$zipFile,
[string[]]$toBeZipped
)
& "C:\Program Files\7-Zip\7z.exe" A -tzip $zipFile $toBeZipped | Out-Null
}
$Days = "60"
$LastWrite = (Get-Date).Date.AddDays(-$Days)
$TargetFolder = "$($ENV:USERPROFILE)\Downloads\*"
$Files = Get-Childitem $TargetFolder -Recurse |
Where {$_.LastWriteTime -le $LastWrite} |
Select-Object -ExpandProperty FullName
$Files
Zip "$($ENV:USERPROFILE)\Desktop\TEST.zip" $Files
I'm looking for a PowerShell script which can find the files (N30008xx.txt, N30005xx.txt) from the source directory and copy them to the destination directory by creating a folder with the same name of the file's modification date.
I'm able to run the below script which creates the folder by files modified date.
$p = "Filesourcepath"
Get-ChildItem -Path $p |
Where-Object { ! ($_.PSIsContainer) } |
ForEach-Object {
$newDir = Join-Path $p ($_.LastWriteTime).ToString("yyyy-MM-dd")
New-Item -Path $newDir -ItemType Directory -ErrorAction SilentlyContinue
$_ | Move-Item -Destination $newDir
}
Your code should work in principle.
(As of this writing, there's confusion over copying vs. moving, and the aspect of matching only select files is missing.)
Below is a streamlined version of your code, which however, does not explain your symptom - you need to provide more information for us to diagnose your problem.
The streamlined code below:
takes advantage of the PSv3+ -File Get-ChildItem parameter to limit matching to files (as opposed to directories) - this saves the need for Where-Object { ! $_.PSIsContainer }.
uses -LiteralPath to pass the literal $dir path; while -Path (which is also the positional default) often works fine too, it's important to note that it interprets is argument as a wildcard expression, which can have unexpected results).
uses -Filter to provide the file mask (wildcard expression); this is generally preferable to using the -Path parameter, because it filters at the source (Windows API call) and is therefore faster, which can make a noticeable difference when processing large directories.
Caveat: the wildcard language supported in the -Filter argument is more limited than PowerShell's and also burdened with legacy quirks; in short: sticking with * and ? should be fine; for the full story, see this well-researched answer.
uses -Force instead of -ErrorAction SilentlyContinue in order to either create a directory or use a preexisting one.
Note that New-Item -ItemType Directory -Force returns a [System.IO.DirectoryInfo] instance in both scenarios (either the newly created directory or the preexisting one), which the code takes advantage of.
# Create sample dir. with 2 sample files in it.
$tmpDir = New-Item -Force -Type Directory tmpDir
New-Item -Type File -Force -Path ('N30008xx.txt', 'N30005xx.txt' -replace '^', "$($tmpDir.FullName)/")
$dir = $tmpDir
$fileMask = 'N*.txt'
Get-ChildItem -File -LiteralPath $dir -Filter $fileMask | ForEach-Object {
$newDir = Join-Path $dir $_.LastWriteTime.ToString("yyyy-MM-dd")
$_ | Move-Item -Destination (New-Item -ItemType Directory -Force $newDir)
}
Caveat re generalization of this code:
You're creating the target subdirectories inside the source directory.
If you were to use Get-ChildItem -Recurse to process the source directory recursively, you'd end up processing matching files twice: first when moving them, and then again when finding them in their moved-to location.
(In this particular case this would only cause an inefficiency, however, because processing the already-moved files attempts to move them into the directory where they already are, which is a quiet no-op.)
Here is a modified version of your PowerShell that should work.
Note: Your destination directory cannot be located under the source directory otherwise you will have a forever recursive move.
$p = pwd
$dst = "c:/tmp/testdir"
Get-ChildItem -Path $p | Where-Object {
$_.PSIsContainer -eq $false
} | ForEach-Object {
$newdir = Join-Path -Path $dst -ChildPath ($_.LastWriteTime).ToString("yyyy-MM-dd")
if (!(Test-Path -Path $newdir)) {
Write-Host "Create directory $newdir"
New-Item -Path $newdir -ItemType Directory
}
Write-Host "Copy file $_"
Move-Item -Path $_ -Destination $newdir
}