Forum Discussion

Kenneth Green's avatar
Kenneth Green
Brass Contributor
Mar 02, 2024

Powershell script move contents of child folder to parent, go to next folder and repeat

Windows 10

I am not very literate in powershell script but I trying to create a Power Shell script that does the following.

 

For this example I am using "C:\Test" as a testing folder and the file types are .flac, but in the finished script the files could be of any type.

 

Inside "C:\Test" are unknown number of multiple parent folders.

Each of these folders has a (1 deep) child folder.

Inside the (1 deep) child folders are multiple files and maybe other folders.


I need for the script to look at Some folder 1\Some sub folder 1\ and move or copy the contents "*.*" to Some folder 1 and then delete Some sub folder 1
next go to next folder in "C:\Test" and repeat,

then the next and repeat and so on.
and do this for all Folders in "C:\Test\"

 

As a visual example

As it is now

 

"C:\Test\"
	"Some folder 1\Some sub folder 1\*.*"
	"Some folder 2\Some sub folder 2\*.*"
	"Some folder 3\Some sub folder 3\*.*"
	"Some folder 4\Some sub folder 4\*.*"

 

 

What I am trying to achieve

 

"C:\Test\"
	"Some folder 1\*.*"
	"Some folder 2\*.*"
	"Some folder 3\*.*"
	"Some folder 4\*.*"

 

 

What I have tried so far...

 

Get-ChildItem 'C:\TEST' -Filter '*.*' -Directory -Recurse -Force | ForEach-Object {
    Move-Item "$($_.FullName)\*.*" -Destination $_.Parent.FullName
    Remove-Item $_.FullName -Force
}

 

 Which runs in powershell with out any errors but when I check in C:\Test it has not done anything.

 

I also tried

 

$flacs = Get-ChildItem -Path "C:\TEST" -Recurse -Filter "*.*"
    foreach ($flac in $flacs) {

        $Parent_Directory = Split-Path -Path $flac.FullName -Parent
        $Destination_Path = Split-Path -Path $Parent_Directory -Parent
        
        Move-Item -Path $flac.FullName -Destination $Destination_Path
            if ($null -eq (Get-ChildItem -Path $Parent_Directory)) {

                Remove-Item -Path $Parent_Directory

            }
    }

 

Which also runs with no errors but again it has not actually done anything.

 

I then also tried

 

$sourceFolder = "C:\Test" $fileType = ".flac"

$parentFolders = Get-ChildItem -Path $sourceFolder -Directory

foreach ($parentFolder in $parentFolders) { # Get the child folder (assuming only one exists) $childFolder = Get-ChildItem -Path $parentFolder.FullName -Directory

# Check if a child folder exists
if ($childFolder) {
    # Get the files in the child folder
    $files = Get-ChildItem -Path $childFolder.FullName -File -Filter $fileType

    # Move or copy the files to the parent folder
    foreach ($file in $files) {
        Move-Item -Path $file.FullName -Destination $parentFolder.FullName -Force  # Use Copy-Item instead of Move-Item if you want to copy instead of move
    }

    # Delete the child folder
    Remove-Item -Path $childFolder.FullName -Force -Recurse
}
}

 

Which also runs with no errors but again it has not actually done anything.

 

To take account that, when used for real, the child folders could contain many file types I also tried changing.

 

$sourceFolder = "C:\Test" $fileType = ".flac"

 

 

to

 

$sourceFolder = "C:\Test" $fileType = "*.*"

 

 

but this produced the error

 

At line:1 char:27
+ $sourceFolder = "C:\Test" $fileType = "*.*"
+                           ~~~~~~~~~
Unexpected token '$fileType' in expression or statement.
    + CategoryInfo          : ParserError: (:) [], ParentContainsErrorRecordException
    + FullyQualifiedErrorId : UnexpectedToken

 

Also, I am unable to find a way to take into account that the each child folder might, or might not have its own child folders and that they, and the files in them must also be copied to the parent.

 

Thanks

 

 

 

 

 

 

 

  • LainRobertson's avatar
    LainRobertson
    Mar 08, 2024

    I've reproduced the "process lock" scenario you've run into, and the outcome matches expectations on my side - keeping in mind none of this requires administrative access (I'm always operating as an unprivileged user in accordance with best practice).

     

    In the output example below, we see the "before" and "after" file layout, where I've deliberately locked the following path by virtue of changing the command prompt session to using it:

     

    D:\Data\Temp\Forum\Root\Parent1\Child1

     

    As a result, when I've then run the script, this path failed to be moved, hence the two files it contains remain in their original location - nothing was deleted.

     

    Because this specific version of "Child1" failed, it allowed the next iteration of "Child1" - located under Parent2 - to succeed in moving - all of which can be seen in the "after" directory listing.

     

    The key takeaway though is that nothing was deleted, so if you're seeing that, then I'm unsure how that's come about.

     

    Output

     

     

    Cheers,

    Lain

  • LainRobertson's avatar
    LainRobertson
    Silver Contributor

    Kenneth Green 

     

    Hey, Ken.

     

    You've had a good go at it there, so let's see if we can't knock it over.

     

    Firstly, let's quickly revisit the logical structure you've described.

     

    - Root
    --- Parent
    ----- Child

     

    Looking at your first few attempts, the one thing you don't need to be doing is worrying about the -Recurse parameter since it has no meaning when moving a file, while directory moves implicitly cause nested files and directories to move - barring where a file/directory of the same name already exists in the destination beforehand.

     

    So, what we want to do is iterate one level (i.e. not recursively) below each parent (where "parent" is a directory object, not a file object) and move that object up to the root level, and if that happens without any errors, then remove the parent directory.

     

    You've already touched two approaches that could be taken:

     

    1. Move; or
    2. Copy and delete.

     

    In the interested of safety and somewhat reduced coding, I've gone with the first option.

     

    This script has basic safety built in but it's nothing profound, as I'm leveraging the way in which Move-Item is designed. Specifically, I'm referring to the behaviour described above about not moving a file or directory if it already exists in the destination.

     

    So, the only additional safety check I've added is not removing the parent directory if any errors were thrown from the prior Move-Item command. That allows you to inspect the conflicting files and avoid data loss through unconditionally copying duplicate file(s) over the top of ones already within the destination.

     

    Anyhow, here's the specifics.

     

    Initial file/directory layout

     

     

    Example script

    $Root = "D:\Data\Temp\Forum\Root";
    
    Get-ChildItem -Path $Root -Directory -ErrorAction:Stop |
        ForEach-Object {
            $Parent = $_.FullName;
            $Success = $true;       # Used to determine if the "parent" directory should be removed. Any exception will set this to $false, preventing removal.
    
            try
            {
                Get-ChildItem -Path "$Parent\*" -ErrorAction:Stop |
                    ForEach-Object {
                        try
                        {
                            $_ | Move-Item -Destination $Root -ErrorAction:Stop;
                        }
                        catch
                        {
                            Write-Warning -Message "Failed to move/remove: $($_.CategoryInfo.TargetName).";
                            Write-Warning -Message ("   $($_.Exception.Message)");
                            $Success = $false;
    
                            # Output useable details to the pipeline so an failures can be operated on down the pipeline.
                            [PSCustomObject] @{
                                Status = "Failed";
                                Reason = $_.Exception.Message;
                                Path = $_.CategoryInfo.TargetName;
                            }
                        }
                    }
            }
            catch
            {
                Write-Warning -Message "Failed to enumerate: $Parent.";
                Write-Warning -Message ("   $($_.Exception.Message)");
                $Success = $false;
    
                            # Output useable details to the pipeline so an failures can be operated on down the pipeline.
                            [PSCustomObject] @{
                                Status = "Failed";
                                Reason = $_.Exception.Message;
                                Path = $_.CategoryInfo.TargetName;
                            }
            }
    
            if ($Success)
            {
                $_ | Remove-Item -Recurse -ErrorAction:Stop;
            }
        }

     

     

    Example output

     

    And the layout contents showing the Child directories (and contents) having moved up a level - excluding the second Child1 directory which has been left in situ as per the output above:

     

     

    In the output, I've written the issue to the warning channel, but I've also written a summary to the pipeline. This allows you to assign the failure results to a variable or simply pass them down another pipeline chain for further process (such as piping out to a CSV file using Export-Csv - just to list one option).

     

    Again, it's nothing fancy but should hopefully clear up any confusion about the looping structures and when to (and not to) use the -Recursive parameter on Get-ChildItem.

     

    Cheers,

    Lain

    • Kenneth Green's avatar
      Kenneth Green
      Brass Contributor

      LainRobertson 

       

      Hi Lain

       

      I must apoligize for not replying until now but I was called away from home for a few days.

       

      Thank you for your kind help which I really do appreciate.

       

      I have now tested your code suggestion but sadly, for me, it does not work as expected.

       

      For testing I created a Test folder in my C drive root.


      Inside the Test Folder I created 4 Parent folders.
      Inside Parent 1 I created 2 child folders (as a parent may have none or many child folders)
      Inside Parent 3 - 4 I created one child folder in each.

      Inside each child folder I placed 2 text files, one audio .flac file and a .jpg file

      I also used variations of the naming format and used characters such as (, ), [ and ] to reflect the real world folder names

       

      As a illistration I attach a screen shot


      I then modified your code to reflect the test folder...

      $Root = "C:\Test";

       

      I then opened Powershell changed to my C drive, pasted your code and hit enter

       

      Powershell returned the error as below

      Remove-Item : Cannot remove item C:\Test\01 Parent [For] a (Test) 1\Child 1 [For] a (Test): The process cannot access the file 'C:\Test\01 Parent [For] a (Test) 1\Child 1 [For] a (Test)' because it is being used by another process.
      At line:47 char:18
      + $_ | Remove-Item -Recurse -ErrorAction:Stop;
      + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
      + CategoryInfo : WriteError: (Child 1 [For] a (Test):DirectoryInfo) [Remove-Item], IOException
      + FullyQualifiedErrorId : RemoveFileSystemItemIOError,Microsoft.PowerShell.Commands.RemoveItemCommand
      PS C:\>
      

      When I checked the child folders, the code had not removed any of them.
      in
      01 Parent [For] a (Test) 1\Child 1 [For] a (Test)
      It had deleted all of the test files but it did not copy or move them up a level to the parent folder, it had simply deleted them.

       

      in
      02 Parent (For) a [Test] 2\Child 2 [For] a (Test)
      03 Parent (For) - a (Test) 3\Child 3 [For] a (Test)
      04 Parent + a [Test] 4\Child 4 + a [Test]

       

      And a image for good measure


      The code did not change anything with all of the test files still inside the Child folders

      I have tested this with Powershell as an Admin and without but the result is still the same.

      • LainRobertson's avatar
        LainRobertson
        Silver Contributor

        Kenneth Green 

         

        Hi, Ken.

         

        No problems with the delay. That's the norm in here and "real life" has to take priority for us all.

         

        Looking at that error, it makes sense nothing was moved.

         

        When a process has a lock on a directory or file, it cannot be moved. So, the key is to ensure that no processes have a lock on the test folder, including its contents.

         

        I've used for a long while a Microsoft tool called "handle" for chasing process locks on resources, which if you're interested can be found here:

         

         

        And here's an example of me using handle to find any references to the word "forum", since I'm interested in seeing what currently has access to the "forum" directory below D:\Data\Temp on my computer.

         

         

        If you're not interested in finding which specific process(s) has the lock, you can simply close everything - including Explorer windows like the one from your screenshot, then re-open PowerShell and start again.

         

        The key takeaway is that it's not the script that has the issue in this specific case, as nothing can move a resource locked by another process. In other words, the content not moving is an expected outcome for this specific error.

         

        Cheers,

        Lain

Resources