Search code examples
powershellfilepath

Strange behavior of Function call for Path renaming


I am trying to write functions to rename Paths and Files.

I want to add or remove to the directory and file names (excluding the file extensions) at the end or start of the name certain identifiers.

So I wrote these functions:

##################################
# Rename Files and Directories
##################################

function Ensure-String-End ($string, $ending) {
    # Ensure that $string ends with $ending.
    if ($string -match $(-join('', ".*", $ending, '$'))) {
        return $string
    } else {
        return $(-join('', $string, $ending))
    }
}

function Ensure-String-Start ($string, $start) {
    # Ensure that $string starts with the $start string.
    if ($string -match $(-join('', '^', $start, '.*$'))) {
        return $string
    } else {
        return $(-join('', $start, $string))
    }
}

function Remove-String-End ($string, $ending) {
    # Remove $ending string from end of $string
    return $string -replace "$ending$", ''
}

function Remove-String-Start ($string, $start) {
    # Remove $start string from start of $string
    return $string -replace "^$start", ''
}



function Ensure-Filename-End ($filename, $ending) {
    # Similar to Ensure-String-End just for file names - The file extension should stay.
    # But at the end of the filename without extension the $ending should be ensured.
    $path = Get-Item $filename
    return -join('', $path.DirectoryName, '\', $(Ensure-String-End $path.Basename $ending), $path.Extension)
}

function Ensure-Filename-Start ($path, $start) {
    # Filename should start with $start string.
    $path = Get-Item $path
    return -join('', $path.DirectoryName, '\', $(Ensure-String-Start $path.Basename $start), $path.Extension)
}

function Remove-Filename-End ($filename, $ending) {
    # Remove from filename end the $ending (file extension should stay)
    $path = Get-Item $filename
    return -join('', $path.DirectoryName, '\', $(Remove-String-End $path.Basename $ending), $path.Extension) 
}

function Remove-Filename-Start ($path, $start) {
    # Remove from file name's start the $start string. Rest of the path should be invariant.
    $path = Get-Item $path
    return -join('', $path.DirectoryName, '\', $(Remove-String-Start $path.Basename $start), $path.Extension)
}




function Ensure-Directories-Ending ($path, $ending) {
    # Make directories end with $ending and rename (`mv`) the directories.
    Get-ChildItem -Path $path -Directory | %{$_.FullName} |
        ForEach-Object { 
            $new_name = Ensure-String-End $_ $ending
            if ($new_name -ne $_) {
                echo "Renaming $_ to $new_name"
                Rename-Item -Path $_ -NewName $new_name
            }
        }
}

function Ensure-Files-Ending ($path, $ending) {    
    # `mv` the file names, ensuring they end with $ending - while file extension is kept.
    Get-ChildItem -Path $path -Recurse | %{$_.FullName} |
        ForEach-Object {
            $new_name = Ensure-Filename-End $_ $ending
            if ($new_name -ne $_) {
                echo "Renaming $_ to $new_name"
                Rename-Item -Path $_ -NewName $new_name
            }
        }
}

function Ensure-Directories-and-Files-Ending ($path, $ending) {
    # Recursively add to all directory and file names the $ending if they don't end already by it.
    Ensure-Directories-Ending $path $ending
    Ensure-Files-Ending $path $ending
}

function Remove-Directories-Ending ($path, $ending) {
    # Rename directories so that if they end with $ending this $ending is removed.
    Get-ChildItem -Path $path -Directory | 
        ForEach-Object {
            $dir_path = %{$_.FullName}
            $new_name = Remove-String-End $dir_path $ending
            if ($new_name -ne $dir_path) {
                echo "Renaming $dir_path to $new_name"
                Rename-Item -Path $dir_path -NewName $new_name
            }
        }
}

function Remove-Files-Ending ($path, $ending) {
    # Remove $ending from File names - rename them in this system.
    Get-ChildItem -Path $path -Recurse | %{$_.FullName} |
        ForEach-Object {
            $new_name = Remove-Filename-End $_ $ending
            if ($new_name -ne $_) {
                echo "Renaming $_ to $new_name"
                Rename-Item -Path $_ -NewName $new_name
            }
        }
}

function Remove-Directories-and-Files-Ending ($path, $ending) {
    # Recursively remove from ending of all Folders and Files' names the $ending string.
    $(Remove-Directories-Ending $path $ending)
    $(Remove-Files-Ending $path $ending)
}

Generate a test folder:

New-Item .\Desktop\test\a -type Directory 
New-Item .\Desktop\test\b -type Directory 
New-Item .\Desktop\test\c -type Directory
New-Item   .\Desktop\test\a\a.txt -type File
New-Item   .\Desktop\test\a\b.txt -type File
New-Item   .\Desktop\test\a\c.txt -type File
New-Item   .\Desktop\test\b\a.txt -type File
New-Item   .\Desktop\test\b\b.txt -type File
New-Item   .\Desktop\test\b\c.txt -type File
New-Item   .\Desktop\test\c\a.txt -type File
New-Item   .\Desktop\test\c\b.txt -type File
New-Item   .\Desktop\test\c\c.txt -type File

Add to each end of folders and files a certain ending:

Ensure-Directories-and-Files-Ending $HOME\Desktop\test "_test"

And remove them again:

Remove-Directories-and-Files-Ending $HOME\Desktop\test "_test"

there appear:

Rename-Item : Cannot rename the specified target, because it represents a path or device name.

Errors/Warnings.

How can I avoid them?

My final solution

Thank you everybody for your inputs. Considering them, I came to the conclusion that writing my own little functions for path handling would be helpful for my matter.


function PathLast ($path) {
    return $path.split('\')[-1]
}

function PathExt ($path) {
    $last = PathLast $path
    if ($last.Contains('.')) {
        return ".$($last.split('.')[-1])"
    } else {
        return ''
    }
}

function PathBase ($path) {
    $last = PathLast $path
    return $last -replace "$(PathExt $path)$", ''
}

function PathDir ($path) {
    $last = PathLast $path
    return $($path -replace "$last$", '') -replace '\\$', ''
}

# Renaming Paths should test, whether the absolute names are identical
# if identical, no renaming is performed.

function Rename-Path ($path, $new_path) {
    if ($path -ne $new_path) {
        Rename-Item -Path $path -NewName $new_path
    }
}

# Using those, the renaming functions were written.
# Each of them work both for files and directories/folders as well.

function Ensure-End ($path, $ending) {
    $dir = PathDir $path
    $base = PathBase $path
    $ext = PathExt $path
    echo "$dir\\$base$ext"
    if (-not $base.EndsWith($ending)) {
        Rename-Path $path "$dir\\$base$ending$ext"
    }
}

function Ensure-Start ($path, $start) {
    $dir = PathDir $path
    $base = PathBase $path
    $ext = PathExt $path
    if (-not $base.StartsWith($start)) {
        Rename-Path $path "$dir\\$start$base$ext"
    }
}

function Remove-End ($path, $ending) {
    $dir = PathDir $path
    $base = PathBase $path
    $ext = PathExt $path
    if ($base.EndsWith($ending)) {
        Rename-Path $path "$dir\\$($base -replace "$ending$", '')$ext"
    }
}

function Remove-Start ($path, $start) {
    $dir = PathDir $path
    $base = PathBase $path
    $ext = PathExt $path
    if ($base.StartsWith($start)) {
        Rename-Path $path "$dir\\$($base -replace "^$start", '')$ext"
    }
}


# The following functions are like the previous ones,
# just recursively applying the renamings on all children
# in the path-tree.

function Ensure-End-All ($path, $ending) {
    Get-ChildItem -Path $path -Recurse | %{$_.FullName} |
        ForEach-Object {
            Ensure-End $_ $ending
        }
}


function Ensure-Start-All ($path, $start) {
    Get-ChildItem -Path $path -Recurse | %{$_.FullName} |
        ForEach-Object {
            Ensure-Start $_ $start
        }
}

function Remove-End-All ($path, $ending) {
    Get-ChildItem -Path $path -Recurse | %{$_.FullName} |
        ForEach-Object {
            Remove-End $_ $ending
        }
}

function Remove-Start-All ($path, $start) {
    Get-ChildItem -Path $path -Recurse | %{$_.FullName} |
        ForEach-Object {
            Remove-Start $_ $start
        }
}

# Ensure-End-All .\Desktop\test "_test"
# Remove-End-All .\Desktop\test "_test"
# Ensure-Start-All .\Desktop\test "test_"
# Remove-Start-All .\Desktop\test "test_"

Solution

  • The Rename-Item cmdlet's -NewName parameter truly only accepts a new name for the input file-system item, not a path:

    • By design and invariably, Rename-Item renames a file or directory (or other PowerShell provider item) in its current location.

    • If, by contrast, you want to rename and also move the item to a different location, you must use the Move-Item cmdlet.


    You can easily provoke the error as follows:

    PS> Get-Item $PROFILE | Rename-Item -NewName "$HOME\NewName" -WhatIf
    Rename-Item: Cannot rename the specified target, because it represents a path or device name.
    

    Note that, as a courtesy, Rename-Item does accept a path in two cases:

    • If the -NewName argument is prefixed by verbatim relative path .\ (or ./).

      • For instance, the following two calls are equivalent:

        Rename-Item -LiteralPath c:\path\to\foo.txt -NewName bar.txt
        Rename-Item -LiteralPath c:\path\to\foo.txt -NewName .\bar.txt
        
      • Do note that . in this context does not refer to the current directory, but to that of the input file.

    • Curiously, the input item's current, full path is also accepted, but, given that you cannot rename an item to itself, this has the following effect:

      • For input items that are files, the call is a quiet no-op.
      • For directories, you get an error that states, "Rename-Item: Source and destination path must be different."
      • This surprising inconsistency is the subject of GitHub issue #14903.