Batch copy and rename files with PowerShell - powershell

I'm trying to use PowerShell to batch copy and rename files.
The original files are named AAA001A.jpg, AAB002A.jpg, AAB003A.jpg, etc.
I'd like to copy the files with new names, by stripping the first four characters from the filenames, and the character before the period, so that the copied files are named 01.jpg, 02.jpg, 03.jpg, etc.
I have experience with Bash scripts on Linux, but I'm stumped on how to do this with PowerShell. After a couple of hours of trial-and-error, this is as close as I've gotten:
Get-ChildItem AAB002A.jpg | foreach { copy-item $_ "$_.name.replace ("AAB","")" }
(it doesn't work)

Note:
* While perhaps slightly more complex than abelenky's answer, it (a) is more robust in that it ensures that only *.jpg files that fit the desired pattern are processed, (b) shows some advanced regex techniques, (c) provides background information and explains the problem with the OP's approach.
* This answer uses PSv3+ syntax.
Get-ChildItem *.jpg |
Where-Object Name -match '^.{4}(.+).\.(.+)$' |
Copy-Item -Destination { $Matches.1 + '.' + $Matches.2 } -WhatIf
To keep the command short, the destination directory is not explicitly controlled, so the copies will be placed in the current dir. To ensure placement in the same dir. as the input files, use
Join-Path $_.PSParentPath ($Matches.1 + '.' + $Matches.2) inside { ... }.
-WhatIf previews what files would be copied to; remove it to perform actual copying.
Get-ChildItem *.jpg outputs all *.jpg files - whether or not they fit the pattern of files to be renamed.
Where-Object Name -match '^.{4}(.*).\.(.+)$' then narrows the matches down to those that fit the pattern, using a regex (regular expression):
^...$ anchors the regular expression to ensure that it matches the whole input (^ matches the start of the input, and $ its end).
.{4} matches the first 4 characters (.), whatever they may be.
(.+) matches any nonempty sequence of characters and, due to being enclosed in (...), captures that sequence in a capture group, which is reflected in the automatic $Matches variable, accessible as $Matches.1 (due to being the first capture group).
. matches the character just before the filename extension.
\. matches a literal ., due to being escaped with \ - i.e., the start of the extension.
(.+) is the 2nd capture group that captures the filename extension (without the preceding . literal), accessible as $Matches.2.
Copy-Item -Destination { $Matches.1 + '.' + $Matches.2 } then renames each input file based on the capture-group values extracted from the input filenames.
Generally, directly piping to a cmdlet, if feasible, is always preferable to piping to the Foreach-Object cmdlet (whose built-in alias is foreach), for performance reasons.
In the Copy-Item command above, the target path is specified via a script-block argument, which is evaluated for each input path with $_ bound to the input file at hand.
Note: The above assumes that the copies should be placed in the current directory, because the script block outputs a mere filename, not a path.
To control the target path explicitly, use Join-Path inside the -Destination script block.
For instance, to ensure that the copies are always placed in the same folder as the input files - irrespective of what the current dir. is - use:
Join-Path $_.PSParentPath ($Matches.1 + '.' + $Matches.2)
As for what you've tried:
Inside "..." (double-quoted strings), you must use $(...), the subexpression operator, in order to embed expressions that should be replaced with their value.
Irrespective of that, .replace ("AAB", "") (a) breaks syntactically due to the space char. before ( (did you confuse the [string] type's .Replace() method with PowerShell's -replace operator?), (b) hard-codes the prefix to remove, (c) is limited to 3 characters, and (d) doesn't remove the character before the period.
The destination-location caveat applies as well: If your expression worked, it would only evaluate to a filename, which would place the resulting file in the current directory rather than the same directory as the input file (though that wouldn't be a problem, if you ran the command from the current dir. or if that is your actual intent).

In Powershell:
(without nasty regexs. We hates the regexs! We does!)
Get-ChildItem *.jpg | Copy-Item -Destination {($_.BaseName.Substring(4) -replace ".$")+$_.Extension} -WhatIf
Details on the expression:
$_.BaseName.Substring(4) :: Chop the first 4 letters of the filename.
-replace ".$" :: Chop the last letter.
+$_.Extension :: Append the Extension

Not Powershell, but Batch File:
(since someone wants to be ultra-pedantic about comments)
#echo off
setlocal enabledelayedexpansion
for %%a in (*.jpg) do (
::Save the Extension
set EXT=%%~xa
::Get the Source Filename (no extension)
set SRC_FILE=%%~na
::Chop the first 4 chars
set DST_FILE=!SRC_FILE:~4!
::Chop the last 1 char.
set DST_FILE=!DST_FILE:~,-1!
:: Copy the file
copy !SRC_FILE!!EXT! !DST_FILE!!EXT! )

try this:
Get-ChildItem "C:\temp\Test" -file -filter "*.jpg" | where BaseName -match '.{4,}' |
%{ Copy-Item $_.FullName (Join-Path $_.Directory ("{0}{1}" -f $_.BaseName.Substring(4, $_.BaseName.Length - 5), $_.Extension)) }

Related

Powershell - Add extension to files containing dots

I have a script that addes the extension .xml to files without an extension.
Get-ChildItem "C:\TEST" -file "*." | rename-item -NewName {"{0}.xml" -f $_.fullname}
This works perfectly for a file such as:
BC Logistics SPA con Socio Duo - 2022-03-31 - FT 123456VESE
However, it does not work for a file such as:
A.B.C. Mini MAGAZZINI - 2022-02-25 - FT MM9 000000123
This is because of the dots. The directory also contains files that already have .xml as extension, as well as .pdf-files.
How can I add the extenion .xml to files without an extension but with dots in them?
Excluding .pdf and .xml files is not an option as the directory also contains other files that are deleted in the process.
The challenge is that nowadays filename extensions are a convention, and that most APIs, including .NET's, consider anything after the last ., if any, to be the extension, even if it isn't meant to be one. E.g., in A.B.C. Mini, . Mini (sic) is considered the extension, whereas -Filter *. in effect only matches names that contain no . at all.
If you're willing to assume that any file that doesn't end in . followed by 3 characters (e.g., .pdf) is an extension-less file, you can use the following:
# Note: Works with 3-character extensions only, doesn't limit what those chars.
# may be.
Get-ChildItem -File C:\TEST |
Where-Object Extension -NotLike '.???' |
Rename-Item -NewName { '{0}.xml' -f $_.FullName } -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.
If you need to match what characters are considered part of an extension more specifically and want to consider a range of character counts after the . - say 1 through 4 - so that, say, .1 and .html would be considered extensions too:
# Works with 1-4 character extensions that are letters, digits, or "_"
Get-ChildItem -File C:\TEST |
Where-Object Extension -NotMatch '^\.\w{1,4}$' |
Rename-Item -NewName { '{0}.xml' -f $_.FullName } -WhatIf
Note the use of a regex with the -notmatch operator.
Regex ^\.\w{1,4}$ in essence means: match any extension that has between 1 and 4 word characters (\w), where a word characters is defined as either a letter, a digit, or an underscore (_).
See this regex101.com page for a detailed explanation and the ability to experiment.

How do I run test-path on all paths in the system variable PATH using powershell?

I would like to run the Test-Path, or something similar the completes my purpose to find the invalid paths in my path variable.
The main thing I have done is search for
test path system variable for invalid entries
This did not find anything.
This example is just to show I have tried something, but I don't really know what the best command it.
Test-Path -Path %Path% -PathType Any
Update
These scripts enabled my to find a couple bad paths and fix them
Building on Mathias R. Jessen's great solution in a comment:
# Output those PATH entries that refer to nonexistent dirs.
# Works on both Windows and Unix-like platforms.
$env:PATH -split [IO.Path]::PathSeparator -ne '' |
Where-Object { -not (Test-Path $_) }
Using the all uppercase form PATH of the variable name and [IO.Path]::PathSeparator as the separator to -split by makes the command cross-platform:
On Unix-like platforms environment variable names are case-sensitive, so using $env:PATH (all-upercase) is required; by contrast, Windows is not case-sensitive, so $env:PATH works there too, even though the actual case of the name is Path.
On Unix-like platforms, : separates the entries in $env:PATH, whereas it is ; on Windows - [IO.Path]::PathSeparator returns the platform-appropriate character.
-ne '' filters out any empty tokens resulting from the -split operation, which could result from directly adjacent separators in the variable value (e.g., ;;) - such empty entries have no effect and can be ignored.
Note: With a an array as the LHS, such as returned by -split, PowerShell comparison operators such as -eq and -ne act as filters and return an array of matching items rather than a Boolean - see about_Comparison_Operators.
The Where-Object call filters the input directory paths down to those that do not exist, and outputs them (which prints to the display by default).
Note that, strictly speaking, Test-Path's first positional parameter is -Path, which interprets its argument as a wildcard expression.
For full robustness, Test-Path -LiteralPath $_ is needed, to rule out inadvertent interpretation of literal paths that happen to contain [ as wildcards - though with entries in $env:PATH that seems unlikely.

Expanding Environmental Variable using CSV file into Powershell script

i am trying to create a backup script with csv file which will contain all the local (where backup will be stored) and backup (folder to backup) loaction and run robocopy to copy files from backup to local folder. i want to import environmental variable from csv file that are used for default folders (e.g. env:APPDATA) as location to be use for backing up some files and folders in user documents or pulbic documents. When i use the env variable directly or use full path address in the script the copy action works fine.
robocopy "&env:localappdata\steam" "$backup"
but when import from csv, the script does not see it as env variable. robocopy shows the error because it picks up the locations like this
Source : C:\WINDOWS\system32\$env:LOCALAPPDATA\steam\
Dest : D:\backup\steam\
Below is the full code i am using.
$path = "$PSScriptRoot\backup\"
$locations = import-csv "$PSScriptRoot\backup\local.csv" -Delimiter "," -Header 'Local','Backup','Display' | Select-Object Local,Backup,display
foreach($location in $locations){
$source = $location.Local
$source = $source.ToString()
$destination = $location.Backup
$destination = $destination.tostring()
$Name = $location.Display
$Name = $name.tostring()
Write-host "Copying $Name, please wait...." -foregroundColor Yellow
robocopy "$destination" "$path$source" \s \e
}
And my CSV file looks like this
Steam, $env:LOCALAPPDATA\steam, Steam files Backup
As you have added a path in the csv as $env:LOCALAPPDATA\steam, and knowing that whatever you read from file using Import-Csv is a string, you need to use regex to convert the $env:VARNAME into %VARNAME% in order to be able to resolve that environment variable into a path.
Instead of
$source = $source.ToString()
do this
$source = [Environment]::ExpandEnvironmentVariables(($source -replace '\$env:(\w+)', '%$1%'))
$source wil now have a valid path like C:\Users\Aman\AppData\Local\Steam
Do the same with $destination
Regex details:
\$ Match the character “$” literally
env: Match the characters “env:” literally
( Match the regular expression below and capture its match into backreference number 1
\w Match a single character that is a “word character” (letters, digits, etc.)
+ Between one and unlimited times, as many times as possible, giving back as needed (greedy)
)
P.S. Instead of combining a path with string concatenation like you do in "$path$source", it is much safer to use the Join-Path cmdlet for that

How to recursively append to file name in powershell?

I have multiple .txt files in folders/their sub-folders.
I want to append _old to their file names.
I tried:
Get-ChildItem -Recurse | Rename-Item -NewName {$_.name -replace '.txt','_old.txt' }
This results in:
Some files get updated correctly
Some files get updated incorrectly - they get _old twice - example: .._old_old.txt
There are few errors: Rename-Item : Source and destination path must be different.
To prevent already renamed files from accidentally reentering the file enumeration and therefore getting renamed multiple times, enclose your Get-ChildItem call in (), the grouping operator, which ensures that all output is collected first[1], before sending the results through the pipeline:
(Get-ChildItem -Recurse) |
Rename-Item -NewName { $_.name -replace '\.txt$', '_old.txt' }
Note that I've used \.txt$ as the regex[2], so as to ensure that only a literal . (\.) followed by string txt at the end ($) of the file name is matched, so as to prevent false positives (e.g., a file named Atxt.csv or even a directory named AtxtB would accidentally match your original regex).
Note: The need to collect all Get-ChildItem output first arises from how the PowerShell pipeline fundamentally works: objects are (by default) sent to the pipeline one by one, and processed by a receiving command as they're being received. This means that, without (...) around Get-ChildItem, Rename-Item starts renaming files before Get-ChildItem has finished enumerating files, which causes problems. See this answer for more information about how the PowerShell pipeline works.
Tip of the hat to Matthew for suggesting inclusion of this information.
However, I suggest optimizing your command as follows:
(Get-ChildItem -Recurse -File -Filter *.txt) |
Rename-Item -NewName { $_.BaseName + '_old' + $_.Extension }
-File limits the the output to files (doesn't also return directories).
-Filter is the fastest way to limit results to a given wildcard pattern.
$_.BaseName + '_old' + $_.Extension uses simple string concatenation via the sub-components of a file name.
An alternative is to stick with -replace:
$_.Name -replace '\.[^.]+$', '_old$&'
Note that if you wanted to run this repeatedly and needed to exclude files renamed in a previous run, add -Exclude *_old.txt to the Get-ChildItem call.
[1] Due to a change in how Get-ChildItem is implemented in PowerShell [Core] 6+ (it now internally sorts the results, which invariably requires collecting them all first), the (...) enclosure is no longer strictly necessary, but this could be considered an implementation detail, so for conceptual clarity it's better to continue to use (...).
[2] PowerShell's -replace operator operates on regexes (regular expressions); it doesn't perform literal substring searches the way that the [string] type's .Replace() method does.
The below command will return ALL files from the current folder and sub-folders within the current directory the command is executed from.
Get-ChildItem -Recurse
Because of this you are also re-turning all the files you have already updated to have the _old suffix.
What you need to do is use the -Include -Exclude paramters of the Get-Childitem Cmdlet in order to ignore files that already have the _old suffix, and meet your include criteria, for example.
Get-ChildItem -Recure -Include "*.txt" -Exclude "*_old"
Then pipe the results into your re-name item command
Get-ChildItem cmdlet explanation can be found here.
https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.management/get-childitem?view=powershell-7

Using PowerShell to add an extension to files

I have a directory of files that I'd like to append file extension to as long as they don't have an existing, specified extension. So add .txt to all file names that don't end in .xyz. PowerShell seems like a good candidate for this, but I don't know anything about it. How would I go about it?
Here is the Powershell way:
gci -ex "*.xyz" | ?{!$_.PsIsContainer} | ren -new {$_.name + ".txt"}
Or to make it a little more verbose and easier to understand:
Get-ChildItem -exclude "*.xyz"
| WHere-Object{!$_.PsIsContainer}
| Rename-Item -newname {$_.name + ".txt"}
EDIT: There is of course nothing wrong with the DOS way either. :)
EDIT2: Powershell does support implicit (and explicit for that matter) line continuation and as Matt Hamilton's post shows it does make thing's easier to read.
+1 to EBGreen, except that (at least on XP) the "-exclude" parameter to get-childitem doesn't seem to work. The help text (gci -?) actually says "this parameter does not work properly in this cmdlet"!
So you can filter manually like this:
gci
| ?{ !$_.PSIsContainer -and !$_.Name.EndsWith(".xyz") }
| %{ ren -new ($_.Name + ".txt") }
Consider the DOS command FOR in a standard shell.
C:\Documents and Settings\Kenny>help for
Runs a specified command for each file in a set of files.
FOR %variable IN (set) DO command [command-parameters]
%variable Specifies a single letter replaceable parameter.
(set) Specifies a set of one or more files. Wildcards may be used.
command Specifies the command to carry out for each file.
command-parameters
Specifies parameters or switches for the specified command.
...
In addition, substitution of FOR variable references has been enhanced.
You can now use the following optional syntax:
%~I - expands %I removing any surrounding quotes (")
%~fI - expands %I to a fully qualified path name
%~dI - expands %I to a drive letter only
%~pI - expands %I to a path only
%~nI - expands %I to a file name only
%~xI - expands %I to a file extension only
%~sI - expanded path contains short names only
%~aI - expands %I to file attributes of file
%~tI - expands %I to date/time of file
%~zI - expands %I to size of file
%~$PATH:I - searches the directories listed in the PATH
environment variable and expands %I to the
fully qualified name of the first one found.
If the environment variable name is not
defined or the file is not found by the
search, then this modifier expands to the
empty string
Found this helpful while using PowerShell v4.
Get-ChildItem -Path "C:\temp" -Filter "*.config" -File |
Rename-Item -NewName { $PSItem.Name + ".disabled" }