DEV Community

Boluwatife Adewusi
Boluwatife Adewusi

Posted on

File Organizer, Part 3: Making It Interactive with Cobra! 🐍⚡

Welcome to the third part of our File Organizer journey! So far, we’ve built a tool that organizes files into categories and made it flexible enough for users to modify those categories—without writing code! But what’s the fun in a tool that’s not interactive? Let’s take it up a notch by adding a command-line interface (CLI) so users can interact with it directly! 🎯

How do we do that? Enter Cobra—a powerful library used by tools like Docker and Kubernetes. If you’re ready to make your Go project interactive and shareable, buckle up! 🚀


Step 1: Installing Cobra

First, open your terminal and install Cobra with:

go get -u github.com/spf13/cobra@latest
Enter fullscreen mode Exit fullscreen mode

With that, Cobra is locked and loaded! Now, let’s organize our project structure.


Step 2: Setting Up Project Structure

Create a folder named cmd inside your project directory. This is where we’ll define commands and their handlers. Inside cmd, create two files:

  • root.go – Defines the CLI commands
  • handlers.go – Calls the right functions based on the commands

Step 3: Displaying Categories with handlers.go

In handlers.go, our first mission is to display the saved categories beautifully using Go's built-in text/tabwriter package. Think of it as adding that little touch of elegance to the terminal output. 💅

// handlers.go

package cmd

import (
    "your-project-path/models"
    "errors"
    "fmt"
    "os"
    "path/filepath"
    "strings"
    "text/tabwriter"
)

var store = models.NewFileDataStore("")

// DisplayCategories lists all categories in a pretty format
func DisplayCategories(verbose bool) {
    categories, err := store.GetCategories()

    if err != nil {
        fmt.Println(err)
    }

    const padding = 3
    w := tabwriter.NewWriter(os.Stdout, 0, 0, padding, ' ', tabwriter.Debug)

    defer w.Flush()

    if verbose {
        _, _ = fmt.Fprintln(w, "Category\t Folder Name\t Extensions\t")
        for _, category := range categories {
            _, _ = fmt.Fprintln(
                w,
                fmt.Sprintf("%s\t %s\t %s\t",
                    category.FileType,
                    category.FolderName,
                    strings.Join(category.Extensions, ","),
                ),
            )
        }
    } else {
        _, _ = fmt.Fprintln(w, "Category\t Folder Name\t")
        for _, category := range categories {
            _, _ = fmt.Fprintln(w, fmt.Sprintf("%s\t %s\t", category.FileType, category.FolderName))
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Next, we’ll add functions for adding and removing categories to keep things dynamic and easy to manage. 🛠️


func addCategory(fileType, folderName string, extensions []string) {
    if fileType == "" {
        fmt.Println(errors.New("category name not supplied"))
        return
    }

    var extensionsToAdd []string
    var extensionsToRemove []string

    for _, extension := range extensions {
        if strings.HasPrefix(extension, "-") {
            rawExtension := strings.ReplaceAll(extension, "-", "")
            if strings.HasPrefix(rawExtension, ".") {
                extensionsToRemove = append(extensionsToRemove, rawExtension)
            } else {
                extensionsToRemove = append(extensionsToRemove, "."+extension)
            }
        } else {
            if strings.HasPrefix(extension, ".") {
                extensionsToAdd = append(extensionsToAdd, extension)
            } else {
                extensionsToAdd = append(extensionsToAdd, "."+extension)
            }
        }
    }

    err := store.AddCategory(fileType, folderName, extensionsToAdd)

    if err != nil {
        fmt.Println(err)
        return
    }

    err = store.RemoveExtensionsFromCategory(fileType, extensionsToRemove)

    if err != nil {
        fmt.Println(err)
        return
    }

    DisplayCategories(true)
}

func removeCategory(fileType string) {
    err := store.RemoveCategory(fileType)

    if err != nil {
        fmt.Println(err)
    }

    fmt.Println("Successfully removed")
    DisplayCategories(false)
}

func organizeFiles(dir string) {
    files, err := os.ReadDir(dir)

    if err != nil {
        fmt.Println(err)
    }

    for _, file := range files {
        if file.IsDir() {
            continue
        }

        fileName := file.Name()

        category, err := store.GetType(fileName)

        if err != nil {
            fmt.Println(err)
            continue
        }

        err = category.MoveToFolder(filepath.Join(dir, fileName))

        if err != nil {
            fmt.Println(err)
            continue
        }

        fmt.Printf("%s moved to %s\n", fileName, filepath.Join(dir, category.FolderName, fileName))
    }
}

Enter fullscreen mode Exit fullscreen mode

Step 4: Setting Up Commands with root.go

Now comes the fun part! Let’s define our interactive commands in root.go. For example, here’s how you can set up the display command:

// root.go

package cmd

import (
    "github.com/spf13/cobra"
)

// RootCmd is the base command
var rootCmd = &cobra.Command{
    Use:     "org",
    Short:   "Org is a file organizer",
    Long:    `Org is a file organizer that categorizes and moves files into folders based on their extensions.`,
    Version: "0.1",
}

// Initialize the CLI commands
func init() {
        categoryListCmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "Displays the extensions associated with each category")
    rootCmd.AddCommand(categoryListCmd)
}

// Execute the CLI tool
func Execute() {
    if err := rootCmd.Execute(); err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
}

// categoryListCmd prints all categories
var categoryListCmd = &cobra.Command{
    Use:   "category",
    Short: "Lists all file categories",
    Long:  "Displays a list of all categories used for organizing files, optionally with extensions.",
    Example: `org category 
org category --verbose`,
    Run: func(cmd *cobra.Command, args []string) {
        DisplayCategories(verbose)
    },
}
Enter fullscreen mode Exit fullscreen mode

Step 5: Bringing It All Together with main.go

Here’s how everything ties together in main.go:

// main.go

package main

import (
    "your-project-path/cmd"
)

func main() {
    cmd.Execute()
}
Enter fullscreen mode Exit fullscreen mode

Step 6: Running Your Tool and Testing Commands

Let’s give our tool a test drive! 🏎️ Open your terminal and run:

go run main.go display
Enter fullscreen mode Exit fullscreen mode

If everything is set up correctly, you’ll see your categories displayed in a neat table. Now, you can easily add, remove, and display categories with just a few terminal commands. 🎉


Step 7: Building an Executable

Ready to share your creation with others? Let’s make it shareable by building an executable:

go build -o build/main
Enter fullscreen mode Exit fullscreen mode

This command builds an executable that works on your device type. But what if you want your tool to run on multiple platforms? 🌐 Check out this DigitalOcean guide for instructions on cross-platform builds.


Bonus: Explore the Full Command Set

I’ve walked you through the first command, but there’s so much more you can do! 🧑‍💻 For the full set of commands, check out the GitHub repository here.


Conclusion

With Cobra powering our CLI, our File Organizer tool is now fully interactive and ready to take on the world! 🌍 Whether you’re organizing files for personal use or sharing the tool with friends, it’s now easier than ever to manage file categories.

So, what are you waiting for? Try it out, explore the commands, and build something awesome!


Let me know what you think in the comments or hit me up with feedback. Stay tuned for more cool projects with Golang! 🚀

Top comments (0)