Search code examples
powershellfile-renamedelay-bind

Using Powershell to change filenames


When did changing filenames become so complicated?? All I want to do is add '_a' to a directory of files (with no extension)

The following does not work:

Rename-Item -newname { $_BaseName + '_a' } -whatif

Rename-Item : Cannot evaluate parameter 'NewName' because its argument is specified as a script block and there is no
input. A script block cannot be evaluated without input.
At line:1 char:22
+ Rename-Item -newname { $_BaseName + '_a' } -whatif
+                      ~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : MetadataError: (:) [Rename-Item], ParameterBindingException
    + FullyQualifiedErrorId : ScriptBlockArgumentNoInput,Microsoft.PowerShell.Commands.RenameItemCommand

then:

Rename-Item -Path C:\users\r-waibel\Downloads\Mar723_pod3-m $_.basename -Newname ($_.basename + "_a") -whatif

Rename-Item : A positional parameter cannot be found that accepts argument '$null'.
At line:1 char:1
+ rename-item -Path C:\users\r-waibel\Downloads\Mar723_pod3-m $_.basena ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidArgument: (:) [Rename-Item], ParameterBindingException
    + FullyQualifiedErrorId : PositionalParameterNotFound,Microsoft.PowerShell.Commands.RenameItemCommand

Solution

  • Let me add some background information to Mathias R. Jessen's effective solution:

    • The automatic $_ variable only ever has a meaningful value inside script blocks ({ ... }), and most commonly refers to the current input object received via the pipeline.

    • The script block passed to -NewName in your
      Rename-Item -NewName { $_BaseName + '_a' } -WhatIf command is a so-called delay-bind script block, which therefore indeed requires pipeline input:

      • Note that delay-bind script blocks only work with parameters that are explicitly designed to bind to pipeline input (and that they mustn't be of type [script block] or [object]).

      • In effect, instead of providing a static value to -NewName you're passing a piece of code that gets evaluated for each pipeline input object and therefore allows the calculation of dynamic, per-input-object values.

      • If there is no pipeline input, the command cannot function, and you'll get the error that includes Cannot evaluate parameter 'NewName' because its argument is specified as a script block and there is no input.

    • By contrast, -NewName ($_.basename + "_a") passes an (ultimately) static value to -NewName; that is, the expression is evaluated once, up front, and its result is passed as a static value to the parameter.

      • For the reasons explained above, $_ has no meaningful value in this context; it is $null, so $_.basename is $null too, and the overall result is just _a.

      • However, because you also tried to use $_.basename as an additional, positional argument
        (rename-item -Path C:\users\r-waibel\Downloads\Mar723_pod3-m $_.basename -Newname ($_.basename + "_a") -whatif), the Rename-Item call broke fundamentally, due to that argument (whose value happens to be $null) not being recognized.

      • This is what the error message A positional parameter cannot be found that accepts argument '$null'. indicates.

      • To solve your task without a delay-bind script block (the latter being preferable), but without having to hard-code the input file name, you'd need an aux. custom variable; e.g.:

         $file = Get-Item -LiteralPath C:\users\r-waibel\Downloads\Mar723_pod3-m
         $file | Rename-Item -NewName ($file.BaseName + '_a') -WhatIf
        
        • The above is the - more cumbersome, single-file-only - equivalent to using a delay-bind script block as follows:

           Get-Item -LiteralPath C:\users\r-waibel\Downloads\Mar723_pod3-m | 
             Rename-Item -NewName { $_.BaseName + '_a' } -WhatIf
          

    As for:

    All I want to do is add '_a' to a directory of files (with no extension)

    Using the flexibility of delay-bind script blocks discussed above, this is then as simple as piping the output from a Get-ChildItem call that outputs all files to rename to your Rename-Item call:

    Get-ChildItem -File -LiteralPath C:\users\r-waibel\Downloads\ | 
      Rename-Item -NewName { $_.BaseName + '_a' } -WhatIf