Attempting to Learn Go - Sorting and Moving Files by Extension
Tags: go, atlg, continuingeducation
Published: 2019-03-02 19:28:22

---
title: Attempting to Learn Go - Sorting and Moving Files by Extension
published: true
description: "This time we modify our previous code to be able to sort our files into a nice directory structure."
tags: go, atlg, continuing-education
cover_image: https://dev.shindakun.net/img/attemptinggo_w.png
series: "Attempting to Learn Go"
---

# Off the Rails

It turns out that we [went off the rails last week](https://dev.to/shindakun/attempting-to-learn-go---listing-files-by-extension-1n10). Which actually makes for a nice starting point this time around. Our goal last time was to write code that would sort files based on the files extensions. Again thanks to @ladydascalie for the ideas!

> - Write a program to sort files within a folder by their extension

When I read the original ask I assumed that we should sort and order the files in each extension "bucket". As it turns out that was not "required". My assumption caused me to write a good bit of extra code. So while in the end, we did get to what we wanted there was a bunch of extra code for doing a whole bunch of other things. Oh well.

I realized that I also am not leaning on the standard library as much as I should be. I spent a lot of time splitting a filename to get the extension when `filepath.Ext()` would have returned it for me. Oh well, again.

I am going to try to keep a better eye out for that as we go forward - that's what attempting to learn is all about after all. Though not as close as I should this week since I didn't leave myself enough time to do the coding practice I wanted to do.

## Carrying On

This time around we're going to tackle the second part of the ask, the "bonus section" if you will.

> - Later make it sort them in logical folders ex: .txt in Documents, .jpg in Images etc...

Alright, so let's think about it, we've got our map of extensions and filenames, what should we do with it? In this version, we're going to make a simple switch statement based on the extension "bucket"! OK, that's all well and good but are we going to do this to standard out or on the filesystem? I'm going to interpret this as on the filesystem. There I go assuming again! We'll range over the file extensions. Then for each one, we'll create the destination directory, if needed. After that, we'll loop through the actual files in that bucket and move them to their new home. Sounds easy enough let's start with a couple of small snippets which we'll apply in our new code.

***

## Creating A Target Directory

Thanks to `os.Mkdir()` we can create a directory. `MkdirAll()` could be an option if we wanted to be able to make a whole set of directories along a path. We don't want to, at least for now, to keep it simple.

```go
  err := os.Mkdir(destination, 0777)
  if err != nil {
    return err
  }
```

***

## Moving Files

Again the standard library has us covered! `os.Rename(src, dest)` will "move" files by renaming them with a different path.

```go
  err = os.Rename("./file.md", "./documents/file.go")
  if err != nil {
    return err
  }
```

So, that's our new features let's get down to modifying our previous code. We're going to remove the entire sections on printing to the screen as it's not needed. Then insert some new functions for creating the directory and then moving the file.

***

## Code Walkthrough

We'll skip over our imports and dive right in!

```go
package main

import (
  "encoding/json"
  "fmt"
  "io/ioutil"
  "os"
  "strings"
)
```

I suppose I didn't need to break this into its own function as it could have fit right in in the main function. Splitting out `createDestination()` makes the upcoming `prepAndMove()` a more readable in the end.

```go
func createDestination(s string) error {
  if _, err := os.Stat(s); os.IsNotExist(err) {
    err := os.Mkdir(s, 0777)
    if err != nil {
      return err
    }
  } else {
    // already exists, probably, maybe, hopefully
    return nil
  }
  return nil
}
```

`doMove()`, that sounds promising! This does exactly what it says on the tin, it attempts to move the files using `os.Rename()`.

```go
func doMove(file, dir string) error {
  err := os.Rename(file, dir+"/"+file)
  if err != nil {
    return err
  }
  return nil
}
```

`prepAndMove()` is the new "core" of our program. It receives our map of extensions and filename strings. Now I suppose we could get creative and have a second map as in `map[string][]string`. It could hold the directory to sort to along with the extension that goes in that directory. We could then loop through that and have a bunch of nested loops and craziness going on. But, for a small utility that might be overkill, let's stick with the switch statement. We could always add it later, let's stick with trying to solve the "ask" for now.

So, for each extension bucket, we'll `range` through and match on the extension. Once matched, we will attempt to create a destination directory. If all goes well we'll then range the filenames for that extension and pass them into `doMove()`. Done with one bucket we'll move on to the next until we've been through them all. For brevity, I'm only including the `documents` and `images` section.

```go
func prepAndMove(files map[string][]string) {
  for i, list := range files {
    switch i {
    case "text", "txt", "md":
      dir := "documents"
      err := createDestination(dir)
      if err != nil {
        msg := fmt.Sprintf("An error occured creating destiation.\n%s", err)
        fmt.Println(msg)
        os.Exit(1)
      }
      for j := range list {
        doMove(list[j], dir)
      }
    case "png", "jpg", "gif", "webp":
      dir := "images"
      err := createDestination(dir)
      if err != nil {
        msg := fmt.Sprintf("An error occured creating destiation.\n%s", err)
        fmt.Println(msg)
        os.Exit(1)
      }
      for j := range list {
        doMove(list[j], dir)
      }
    }
  }
}
```

The first bit of `main()` hasn't changed much at all. I am still considering pulling our "error then exit" code into a separate function. We are using similar code in so many places it would be worth it. There's one for when refactoring time comes!

```go
func main() {
  wd, err := os.Getwd()
  if err != nil {
    msg := fmt.Sprintf("An error occured getting the current working directory.\n%s", err)
    fmt.Println(msg)
    os.Exit(1)
  }

  dir, err := ioutil.ReadDir(wd)
  if err != nil {
    msg := fmt.Sprintf("An error occured reading the current working directory.\n%s", err)
    fmt.Println(msg)
    os.Exit(1)
  }
```

I could have swapped in the example done by Benjamin in the comments but that felt a bit like cheating. I do like the [use of `continue`](https://dev.to/ladydascalie/comment/92k2) in it and will have to keep that in mind it's much cleaner. We removed the code looking for files with no extension. It is not needed since we can't sort those anyway.

```go
  var m = make(map[string][]string)
  for _, file := range dir {
    if !file.IsDir() {
      fileName := file.Name()
      ext := strings.Split(fileName, ".")
      if len(ext) > 1 {
        m[ext[len(ext)-1]] = append(m[ext[len(ext)-1]], fileName)
      }
    }
  }

  prepAndMove(m)
}
```

## Next Time

It's a tad disjointed but gets the job done. It's not super useful since I didn't go back in and update it to take a source directory or a root destination. I'm happy with how it came out for the short amount of time I ended up giving myself to work on it.

Feel free to let me know what you would change or do differently in the comments below!

***

You can find the code for this and most of the other Attempting to Learn Go posts in the repo on GitHub.

{% github shindakun/atlg %}

***

| Enjoy this post? |
|:----:|
| [How about buying me a coffee?](https://ko-fi.com/shindakun) |

***