Powershell - How to replace dots on files exept the extension - powershell

I have a question about the replacement of dots on multiple files, i have the next code:
Dir | Rename-item -NewName{ $_.basename.replace(".","-") + $_.extension }
This code works, but i have folders with dots and the problem is when i run the code, the folders repeat the words after the point like a "file extension":
like this:
How can i resolve this problem, i need just replace the dot on folder name with another word or space or everything i like and the files on the folder just replace before the extension.
thanks!

Unfortunately, the .BaseName ETS (Extended Type System) property that PowerShell adds to System.IO.DirectoryInfo instances, i.e. directories, by - unfortunate - design, unconditionally reports the directory name as-is.
It is only for System.IO.FileInfo instances, i.e. files, that .BaseName strips the extension, i.e., the last .-separated component.[1]
You can work around the problem by calling the System.IO.Path.GetFileNameWithoutExtension .NET method, which does not make this distinction (similarly, the type-native .Extension property doesn't make this distinction either, so it can be used as-is).
Get-ChildItem | Rename-Item -NewName {
[IO.Path]::GetFileNameWithoutExtension($_.Name).Replace('.', '-') + $_.Extension
}
[1] You can verify this as follows:
(Get-TypeData System.IO.DirectoryInfo).Members.BaseName vs.
(Get-TypeData System.IO.FileInfo).Members.BaseName

Related

Moving files to folders based on a filename

Howdy all of you smart people! I am a bit out of my depth and thought I would ask a question to see if someone with a higher IQ could at least point me in the right direction.
I have a share with a lot of single lvl subfolders all with similar names in format (xxxxxx_x)
Inside of each folder there are several files:
half will start with the same prefix "xxxxxx_x" as the parent folder
other half has another prefix of 10 digits followed by the parent folder prefix:"yyyyyyyyyy-xxxxxx_x"
I was asked to move files to a new location where I have already scripted a deployment of same folder structure but of course the new ask is for the files to be split based on the above two naming conventions.
TL;DR:
So to summarize, how can I move a batch of files from multiple subfolders to other location based on a part of the filename? (prefix)
typical example:
c:\share\123456_2\123456_2-filename.xls should go to d:\share\123456_2
while
c:\share\123456_2\1234567890-123456_2-filename.xls should go to E:\share\123456_2
I could get the files moved and folders created (if missing) using below but how do I only target files that start with the same string (0,8)?
dir | %{
$id = $_.Name.SubString(0,8);
if(-not (Test-Path $id)) {mkdir $id};
mv $_ "D:\share\$_";}
Well, one way to do this is using the -Split operator, or method to get just the first part of the broken string and check to see if it contains the underscore character (_); since that seems to follow the naming scheme.
Get-ChildItem |
ForEach-Object -Process {
$destination = $_.Name.Split('-')[0]
if ($destination -match "_") {
Move-Item -LiteralPath $_.FullName -Destination "D:\share\$destination" -WhatIf
}
}
Given that you're not looking to move the other files, this will only move the files that have the same beginning.
Remove the -WhatIf safety common parameter once you've dictated the results are what you're after.

Remove "." in file name while retaining file extension

I am trying to use PS to rename a bunch files within a big share and one of the requirements is to remove a dot from the file name. I have tested a few things with my rather basic skills and of course the most basic of scripts zap the file extension.
I finally came up with something like this:
gci *.xlsx | rename-item -newname {$_.Name.replace(".","") + $_.extension }
But that adds the extension to the end of the filename (while keeping the file extension intact)
I thought I could zap the last four symbols using something like this:
gci *.xlsx | rename-item -newname { $_.basename.substring(0,$_.basename.length-4) + $_.extension }
Overall this seems like an overly complicated operation which could also mess up files without dots (unless I specify xlsx as only 4 symbols to be removed)
Would anyone be able to point me in the right direction to an easier solution? ;-)
You were on the right track with your second attempt: using the .BaseName and .Extension properties of the [System.IO.FileInfo] instances[1] output by Get-ChildItem allows you to modify the base name (the file name without its extension) separately, and then re-append the extension to form the full file name:
Get-ChildItem *.xlsx |
Rename-Item -NewName { ($_.BaseName -replace '\.') + $_.Extension } -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.
The above uses the regex-based -replace operator to remove all . instances from the base name; because . is a regex metacharacter (representing any single character), it must be escaped as \. in order to be used literally.
In this simple case, you could have used the [string] type's .Replace() method as well ($_.BaseName.Replace('.', '')), but -replace offers more features and has fewer surprises - see this answer for more information.
Case in point: Say you wanted to remove only the first . from the base name; -replace allows you to do that as follows (but you couldn't do it with .Replace()):
'foo.bar.baz' -replace '\.(.*)$', '$1' # -> 'foobar.baz'
[1] .BaseName isn't a native property of this type; instead, it is a property that PowerShell decorates instances of the type with, using its ETS (Extended Type System).

Powershell, rename-item doesn't work as expected

I have a bunch of jpg image files named in the following pattern:
0001-rand01_012.jpg
0002-rand03_034.jpg
I want to rename them by removing the first 5 characters to get the form:
rand01_012.jpg
etc..
I use the following command:
Get-ChildItem | Rename-Item -NewName {$_.name.Substring(5)}
When using this with -whatif flag i get the expected message saying:
Performing the operation "Rename File" on target "Item: C:\Users\xxxx\Documents\xx xx\temp2\0123-rand16_030.jpg Destination: C:\Users\
xxxx\Documents\xx xx\temp2\rand16_030.jpg".
But removing the whatif gives me errors of this type:
Rename-Item : The input to the script block for parameter 'NewName' failed. Exception calling "Substring" with "1" argument(s): "startIndex cannot be
larger than length of string.
followed by a whole bunch of:
Rename-Item : Cannot create a file when that file already exists.
The files themselves are renamed with random number of characters removed rather than 5 as was intended. So they have ended up like:
01.jpg
01.jpg
.
.
.
d14_001.jpg
etc.
I have used this command to rename such files in the past with success. The fact that I'm getting such random results is making me pull my hair out.
tl;dr
Make sure you only process the files of interest:
(Get-ChildItem -File [0-9][0-9][0-9][0-9]-*.jpg) |
Rename-Item -NewName {$_.name.Substring(5)} -WhatIf
The -WhatIf common parameter in the command above previews the operation. Remove -WhatIf once you're sure the operation will do what you want.
In PowerShell [Core] 6+, placing (...) around Get-ChildItem is no longer technically necessary, but advisable.[1]
That way:
You rule out unrelated files up front.
Even if something goes wrong, you can correct the problem and run the command again to reprocess only the failed files, without affecting the previously renamed files.
The most likely reason for something going wrong is more than 1 input file resulting in the same filename after removing the 5 first char.
It sounds like you've mistakenly run the command repeatedly, so you've cut off 5 chars. multiple times:
0001-rand01_01.jpg -> rand01_01.jpg -> _01.jpg
Once a filename has fewer than 5 chars., you'll get the the startIndex-related error, because the [string] class's .Substring() method doesn't accept an index beyond the length of the string (try 'ab'.Substring(3)).
That said, since you're running Get-ChildItem without a filter and therefore return all (non-hidden) child items, you may be processing unrelated files ore even directories whose names are too short.
The Cannot create a file when that file already exists. errors are just follow-on errors that result from the script block that normally returns the new name effectively returning the empty string, so Rename-Item is somewhat obscurely complaining that you can't rename a file to its current name.
That said, you can even get Cannot create a file when that file already exists errors during the first run, namely if more than 1 input file with its first 5 chars. chopped off results in the same filename.
E.g., 0001-rand01_012.jpg and 0002-rand01_012.jpg would both be renamed to rand01_012.jpg, which fails once the first one has been renamed.
That is, for your command to work as intended, all filenames that result from dropping the first 5 chars. must be unique.
Here's an MCVE (Minimal, Complete, and Verifiable Example):
Setup:
# Create and change to temp dir.
Set-Location (mkdir temp)
# Create sample input files named 0001-rand01_01.jpg, 0002-rand01_02.jpg, ...
# Note how the suffixes after the first 5 char. must be *unique*.
1..9 | %{ $null > "000${_}-rand01_0${_}.jpg" }
1st run:
# No errors
> (Get-ChildItem -File) | Rename-Item -NewName { $_.Name.Substring(5) }
# Show new names
> Get-ChildItem | Select Name
Name
----
rand01_01.jpg
rand01_02.jpg
rand01_03.jpg
rand01_04.jpg
rand01_05.jpg
rand01_06.jpg
rand01_07.jpg
rand01_08.jpg
rand01_09.jpg
A 2nd run yields:
Name
----
1_01.jpg
1_02.jpg
1_03.jpg
1_04.jpg
1_05.jpg
1_06.jpg
1_07.jpg
1_08.jpg
1_09.jpg
At the time of the 3rd run, all the names are too short, and all you'll get is Rename-Item : Cannot create a file when that file already exists. errors.
[1] Enclosing Get-ChildItem in (...) ensures that the matching files are collected in an array, up front, before Rename-Item is invoked.
This explicitly prevents already-renamed files from getting re-enumerated by Get-ChildItem and thus interfering with the iteration. Explicit use of (...) is technically no longer necessary in PowerShell [Core] 6+ (it is necessary in Windows PowerShell (5.1-)), because Get-ChildItem is implemented in a way that always internally collects info about all files up front, across platforms, because it sorts them by name, which is inherently only possible after all names have been collected.
In light of that, whether you use (...) or not should functionally amount to the same, although using (...) is advisable, because it doesn't rely on what amounts to an implementation detail (the documentation doesn't mention how the outputs are ordered).

How can I move from Windows traditional command line to the modern PowerShell?

I was used to a few command line tricks in Windows that increased my productivity a lot.
Now I am told that I should move to PowerShell because it's more POWERful. It took me a while to get a little bit hang of it (objects, piping, etc.), and there are a lot of great tutorials on how to get a few things done.
However, some (relatively) basic trick still puzzle me. For instance, what is the equivalent of the FOR structure in PowerShell?
For example,
FOR %i IN (*.jpg) DO Convert %i -resize 800x300 resized/%i
The above line takes all of photos in a folder and uses the ImageMagick's Convert tool to resize the images and restores the resized imaged in a sub-folder called RESIZED.
In PowerShell I tried the command:
Dir ./ | foreach {convert $_.name -resize 800x300 resized/$_name}
This can't work despite all of the googling around I did. What is missing?
Note that / rather than \ is used as the path separator in this answer, which works on Windows too and makes the code compatible with the cross-platform PowerShell Core editions.
tl;dr:
$convertExe = './convert' # adjust path as necessary
Get-ChildItem -File -Filter *.jpg | ForEach-Object {
& $convertExe $_.Name -resize 800x300 resized/$($_.Name)
}
Read on for an explanation and background information.
The equivalent of:
FOR %i IN (*.jpg)
is:
Get-ChildItem -File -Filter *.jpg
or, with PowerShell's own wildcard expressions (slower, but more powerful):
Get-ChildItem -File -Path *.jpg # specifying parameter name -Path is optional
If you're not worried about restricting matches to files (as opposed to directories), Get-Item *.jpg will do too.
While dir works as a built-in alias for Get-ChildItem, I recommend getting used to PowerShell's own aliases, which follow a consistent naming convention; e.g., PowerShell's own alias for Get-ChildItem is gci
Also, in scripts it is better to always use the full command names - both for readability and robustness.
As you've discovered, to process the matching files in a loop you must pipe (|) the Get-ChildItem command's output to the ForEach-Object cmdlet, to which you pass a script block ({ ... }) that is executed for each input object, and in which $_ refers to the input object at hand.
(foreach is a built-in alias for ForEach-Object, but note that there's also a foreach statement, which works differently, and it's important not to confuse the two.)
There are 2 pitfalls for someone coming from the world of cmd.exe (batch files):
In PowerShell, referring to an executable by filename only (e.g., convert) does not execute an executable by that name located in the current directory, for security reasons.
Only executables in the PATH can be executed by filename only, and unless you've specifically placed ImageMagick's convert.exe in a directory that comes before the SYSTEM32 directory in the PATH, the standard Windows convert.exe utility (whose purpose is to convert FAT disk volumes to NTFS) will be invoked.
Use Get-Command convert to see what will actually execute when you submit convert; $env:PATH shows the current value of the PATH environment variable (equivalent of echo %PATH%).
If your custom convert.exe is indeed in the current directory, invoke it as ./convert - i.e., you must explicitly reference its location.
Otherwise (your convert.exe is either not in the PATH at all or is shadowed by a different utility) specify the path to the executable as needed, but note that if you reference that path in a variable or use a string that is single- or double-quoted (which is necessary if the path contains spaces, for instance), you must invoke with &, the call operator; e.g.,
& $convertExe ... or & "$HOME/ImageMagic 2/convert" ...
PowerShell sends objects through the pipeline, not strings (this innovation is at the heart of PowerShell's power). When you reference and object's property or an element by index as part of a larger string, you must enclose the expression in $(...), the subexpression operator:
resized/$($_.Name) - Correct: property reference enclosed in $(...)
resized/$_.Name - !! INCORRECT - $_ is stringified on its own, followed by literal .Name
However, note that a stand-alone property/index reference or even method call does not need $(...); e.g., $_.Name by itself, as used in the command in the question, does work, and retains its original type (is not stringified).
Note that a variable without property / index access - such as $_ by itself - does not need $(...), but in the case at hand $_ would expand to the full path. For the most part, unquoted tokens that include variable references are treated like implicitly double-quoted strings, whose interpolation rules are summarized in this answer of mine; however, many additional factors come into play, which are summarized here; edge cases are highlighted in this question.
At the end of the day, the safest choice is to double-quote strings that contain variable references or subexpressions:
"resized/$($_.Name)" - SAFEST
Use:
Get-ChildItem | foreach {convert $_.name -resize 800x300 resized/$($_.name)}
Or, perhaps, you need to pass the full name (with path), also showing a shorter syntax (using aliases):
gci | % {convert $_.fullname -resize 800x300 resized/$($_.name)}
Also, you might want to supply the full path to the executable.
Revised based on comments given below
There are many applications with the name "Convert". If I do
Get-Command Convert
on my computer. It shows me an app that is part of the Windows system. If PowerShell is running the wrong app on you, it's never going to work.
The solution will be to point PowerShell at the convert tool inside the ImageMagick program folder. A Google search on "ImageMagick PowerShell" will lead you to lots of people who have faced the same problem as you.

In function repeat an action for each entered parameter

My main script run once gci on a specified drive via -path parameter , then it does multiple different tables from this output. Here below is a part of my script which does a specific table from an directory specified via -folder parameter, for example :
my-globalfunction -path d:\ -folder d:\folder
It work fine, but only for one entered folder path, the goal of this script is that user can enter multiple folders path and get a tables for each entered -folder parameter value, like this :
This clause in your Where-Object would be the issue:
$_.FullName.StartsWith($folder, [System.StringComparison]::OrdinalIgnoreCase)
The array of folders passed are most likely being cast as one long string which would never match. I had a regex solution posted but remembered a simpler way after looking at what your logic was trying to do.
Simpler Way
Even easier way is to put this information right into Get-ChildItem since it accepts string arrays for -Path. This way I don't think you even need to have 2 parameters since you never again use the results from $fol anyway. Based on the assumption that you were looking for all subfolders of $folder
$gdfolders = Get-ChildItem -Path $folder -Recurse -Force | Where-Object{$_.psiscontainer}
That would return all subfolders of the paths provided. If you have PowerShell 3.0 or higher this would even be easier.
$gdfolders = Get-ChildItem -Path $folder -Recurse -Force -Directory
Update from comments
The code you have displayed is incomplete which is what lead me to the solution that you see above. If you do use the variable $fol somewhere else that you do not show lets go back to my earlier regex solution which would work better in place with what you already have.
$regex = "^($(($folder | ForEach-Object{[regex]::Escape($_)}) -join "|")).+"
....
$gdfolders = $fol | Where-Object{($_.Attributes -eq "Directory") -and ($_.FullName -match $regex)}
What this will do is build a regex compare string with what I will assume is the logic of locate folders that begin with either of paths passed.
Using your example input of "d:\folder1", "d:\folder2" the variable $regex would work out to ^(d:\\folder1|d:\\folder2). The proper characters, like \, are escaped automatically by the static method [regex]::Escape which is applied to each element. We then use -join to place a pipe which, in this regex capture group means match whats on the left OR on the right. For completeness sake we state that the match has to occur at the beginning of the path with the caret ^ although this is most likely redundant. It would match paths that start with either "d:\folder1" or "d:\folder2". At the end of the regex string we have .+ which means match 1 to more characters. This should ensure we dont match the actual folder "d:\folder1" but meerly its children
Side Note
The quotes in the line with ’Size (MB)’ are not the proper ones which are '. If you have issues around that code consider changing the quotes.