2

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)

oavaldezi
  • 77
  • 2
  • 6
  • 1
    You can see from the colours in your `"$_.name.replace ("AAB","")"` that it's not processing sensibly, it would need to be more like `$_.name.replace("AAB","")` - not inside string quotes, or `"${$_.name.replace("AAB","")}"` inside string quotes. – TessellatingHeckler Feb 09 '17 at 02:47
  • 2
    @TessellatingHeckler: Good point, except that you meant `$(...)` (subexpression operator) inside the double-quoted string rather than `${...}`. – mklement0 Feb 09 '17 at 03:20

4 Answers4

2

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
abelenky
  • 63,815
  • 23
  • 109
  • 159
  • Caveat (which may or may not be a problem): The `.Substring()` call will break with filenames whose base name has fewer than 4 characters. – mklement0 Feb 09 '17 at 07:11
  • 1
    That's the posters problem. His question implies filenames are long enough. – abelenky Feb 09 '17 at 07:13
  • 1
    I see you've made a small pact with the devil - a homeopathic dose of regex (`.$`). I'm glad the sparring resulted in better answers. Give my regards to your donkey. – mklement0 Feb 09 '17 at 07:17
2

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).

Community
  • 1
  • 1
mklement0
  • 382,024
  • 64
  • 607
  • 775
  • Thank you! Your answer worked. PowerShell's regular expressions aren't POSIX compliant. Can you recommend PowerShell learning resources? – oavaldezi Feb 09 '17 at 03:30
  • PowerShell is based on the .NET Framework, so its regular expressions are much more powerful than what POSIX mandates (which is generally a good thing). As for learning: perhaps [this](https://www.simple-talk.com/sysadmin/powershell/powershell-one-liners-variables,-parameters,-properties,-and-objects/) is helpful, but I also recommend familiarizing yourself with PowerShell's own help system - see [`Get-Help Get-Help`](https://msdn.microsoft.com/en-us/powershell/reference/5.1/microsoft.powershell.core/get-help). – mklement0 Feb 09 '17 at 03:40
  • That regex still look hideous compared to some simple SubString/Concatenation operations. – abelenky Feb 09 '17 at 04:26
  • You've still hardcoded the "4" part, the only thing you're flexible about is the "2". Thats a lot of complexity for a pretty small win. To each their own. – abelenky Feb 09 '17 at 04:34
  • I just generalized my batch answer; it will now chop the first 4 and the last 1 char, and keep any arbitrary amount in between. (inherently cannot be generalized, My Ass!) – abelenky Feb 09 '17 at 04:55
  • 2
    @abelenky: I'm glad I inspired you to find a better solution - well done. (But what does have your donkey have to do with this?). And your batch-language-fu unequivocally resulted in a much simpler, more readable solution. Kudos! – mklement0 Feb 09 '17 at 05:01
1

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! ) 
abelenky
  • 63,815
  • 23
  • 109
  • 159
  • Caveat (which may or may not be a problem): If there happen to be `*.jpg` files with fewer than 5 or fewer characters in the base name, they'll cause problems. It's tempting to try something like `???????*.jpg` to limit matches to `*.jpg` files with, e.g., 7+ char. in the base name, but that is _not_ a solution - surprisingly, this pattern also matches `*.jpg` files with _fewer_ chars. in the base name. – mklement0 Feb 09 '17 at 07:37
  • Just a heads-up for readers not intimately familiar with the syntax of batch files: Using `::` comments inside `(…)` blocks generally works only in limited scenarios, and [the rules](http://stackoverflow.com/a/42148114/45375) are hard to remember. By contrast, using `REM` is safe. – mklement0 Feb 09 '17 at 23:45
-1

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))    }
Esperento57
  • 16,521
  • 3
  • 39
  • 45