Undo copy (cp) with Python and Golang

The lesson of this post: It is okay to make mistakes, as long as you learn from them.

The first thing I want to point out is that macOS and linux treat cp differently. On both OS, the -R option is to copy a directory recursively(including subtree and everything). On macOS if the source argument(a directory) has a /, all its contents will be copied. If the / is omitted, then the directory itself will be copied.

I messed up. Instead of copying a directory, I copied the entire content on my Desktop. Now it’s mixed up with a bunch of other stuff.

I’m gonna replay the scenario on a test directory. The content of source and target:

MacBook-Pro:test kavish$ ls -a source/
.                   .anothertest        directory_in_source file2.txt           file4.txt
..                  .test               file1.txt           file3.txt           file5.txt
MacBook-Pro:test kavish$
MacBook-Pro:test kavish$ ls -a target/
.                   directory_in_source file3.txt           not_so_secret.yml   secret_4.txt
..                  directory_in_target file4.txt           secret_1.txt        secret_5.txt
.anothertest        file1.txt           file5.txt           secret_2.txt
.test               file2.txt           .i_am_hidden         secret_3.txt

Files and Directories from source was copied in the target directory. The target directory contains other files that I need to keep, which makes it impossible to simply remove all files.

The script below will look in the source directory, and keep a list of the filenames. Then it will concatenate the target directory with those filenames and remove them.


import os, shutil

source = "/tmp/test/source" # sys.argv[1]
target = "/tmp/test/target" # sys.argv[2]

files = os.listdir(source) # returns a list of top level directory contents

for file in files:
    to_del = os.path.join(target, file) # join target with source file/directory
    if os.path.isdir(to_del):
        print(f"Removing Directory: {to_del}")
        try:
            shutil.rmtree(to_del) # remove directories and sub-directories
        except FileNotFoundError:
            pass
    else:
        print(f"Removing File: {to_del}")
        try:
            os.remove(to_del) # remove files
        except FileNotFoundError:
            pass

print("Done")

The FileNotFoundError is very important here, because once shutil.rmtree() encounters a directory, it will remove the whole tree.

Note: If cp overwrote some files in the destination with same names as the source, those files will be removed! You won’t be able to get the original.

Here’s the same thing written in Go:

package main

import (
	"fmt"
	"os"
	path "path/filepath"
)

func check(err error) {
	if err != nil {
		panic(err)
	}
}

func main() {
	source := "/tmp/test/source" // os.Argv[1]
	target := "/tmp/test/target" // os.Argv[1]
	files, err := os.ReadDir(source)
	check(err)

	for _, file := range files {
		to_del := path.Join(target, file.Name())
		if file.IsDir() {
			fmt.Printf("Removing Directory: %v\n", to_del)
			err := os.RemoveAll(to_del)
			check(err)
		} else {
			fmt.Printf("Removing File: %v\n", to_del)
			err := os.Remove(to_del)
			check(err)
		}

	}
}

Running the script:

MacBook-Pro:goworkshop kavish$ go run dirwalk.go
Removing File: /tmp/test/target/.anothertest
Removing Directory: /tmp/test/target/.test
Removing Directory: /tmp/test/target/directory_in_source
Removing File: /tmp/test/target/file1.txt
Removing File: /tmp/test/target/file2.txt
Removing File: /tmp/test/target/file3.txt
Removing File: /tmp/test/target/file4.txt
Removing File: /tmp/test/target/file5.txt
MacBook-Pro:goworkshop kavish$
MacBook-Pro:goworkshop kavish$ ls -a /tmp/test/target/
.                   directory_in_target not_so_secret.yml   secret_2.txt        secret_4.txt
..                  .i_am_hidden         secret_1.txt        secret_3.txt        secret_5.txt

Codes on Github: Go and Python

“When you ask God for a gift, Be thankful if he sends, Not diamonds, pearls or riches, but the love of real true friends.”Helen Steiner Rice