Developing a Command-Line Recipe Manager in Go

Table of Contents

  1. Introduction
  2. Prerequisites
  3. Setup
  4. Creating the Recipe Manager - Defining the Data Structures - Loading Recipes from a File - Displaying the Recipe List - Adding a New Recipe - Searching for Recipes - Deleting a Recipe - Saving Changes to a File

  5. Conclusion

Introduction

In this tutorial, we will develop a command-line recipe manager using Go. The recipe manager will allow users to load, display, add, search, and delete recipes. By the end of this tutorial, you will have a working recipe manager implemented in Go.

Prerequisites

To follow along with this tutorial, you should have basic knowledge of the Go programming language. You should also have Go installed on your machine. If you need help installing Go, refer to the official Go documentation.

Setup

Before we start building our recipe manager, let’s set up the project structure and import the necessary packages.

Create a new directory for your project and initialize a Go module:

mkdir recipe-manager
cd recipe-manager
go mod init github.com/your-username/recipe-manager

Next, create a new Go file named main.go:

touch main.go

Open the main.go file in your preferred text editor.

Import the required packages:

package main

import (
    "bufio"
    "fmt"
    "os"
    "strings"
)

We have imported the bufio, fmt, os, and strings packages, which will be used for reading user input, formatting output, accessing the file system, and manipulating strings, respectively.

Now, let’s start building our recipe manager.

Creating the Recipe Manager

Defining the Data Structures

First, we need to define the data structures to represent a recipe. Each recipe will have a name, ingredients, and instructions.

Add the following struct definition to the main.go file:

type Recipe struct {
    Name        string
    Ingredients []string
    Instructions string
}

This Recipe struct will serve as our recipe data type.

Loading Recipes from a File

To allow users to load recipes from a file, let’s implement a function that reads recipes from a given file path and returns a slice of Recipe structs.

Add the following function to the main.go file:

func loadRecipesFromFile(filepath string) ([]Recipe, error) {
    file, err := os.Open(filepath)
    if err != nil {
        return nil, err
    }
    defer file.Close()

    var recipes []Recipe
    scanner := bufio.NewScanner(file)
    var currentRecipe Recipe
    for scanner.Scan() {
        line := scanner.Text()
        if line == "" {
            // Blank line; add completed recipe to the list
            if currentRecipe.Name != "" {
                recipes = append(recipes, currentRecipe)
                currentRecipe = Recipe{}
            }
        } else {
            // Non-blank line; populate recipe fields
            if currentRecipe.Name == "" {
                currentRecipe.Name = line
            } else if currentRecipe.Instructions == "" {
                currentRecipe.Instructions = line
            } else {
                currentRecipe.Ingredients = append(currentRecipe.Ingredients, line)
            }
        }
    }

    if err := scanner.Err(); err != nil {
        return nil, err
    }

    if currentRecipe.Name != "" {
        recipes = append(recipes, currentRecipe)
    }

    return recipes, nil
}

The loadRecipesFromFile function takes a file path as an argument and uses the os.Open function to open the file. It utilizes a bufio.Scanner to read the file line by line. We parse the lines and populate the Recipe struct accordingly. Whenever we encounter a blank line, we add the completed Recipe to the list of recipes. Finally, we return the slice of recipes.

Displaying the Recipe List

Next, let’s implement a function that displays the list of loaded recipes.

Add the following function to the main.go file:

func displayRecipeList(recipes []Recipe) {
    fmt.Println("Recipe List:")
    fmt.Println("-------------------------------------")
    for i, recipe := range recipes {
        fmt.Printf("%d. %s\n", i+1, recipe.Name)
    }
    fmt.Println("-------------------------------------")
}

The displayRecipeList function takes a slice of recipes as an argument and prints each recipe’s name with a corresponding number.

Adding a New Recipe

To enable users to add new recipes, let’s implement a function that prompts the user for recipe details and adds the new recipe to the recipe list.

Add the following function to the main.go file:

func addRecipe(recipes []Recipe) []Recipe {
    var recipe Recipe

    fmt.Println("Add a New Recipe")
    fmt.Println("-------------------------------------")
    fmt.Print("Recipe Name: ")
    reader := bufio.NewReader(os.Stdin)
    recipe.Name, _ = reader.ReadString('\n')
    recipe.Name = strings.TrimSpace(recipe.Name)

    fmt.Println("Ingredients (one ingredient per line, enter an empty line to finish):")
    for {
        line, _ := reader.ReadString('\n')
        line = strings.TrimSpace(line)
        if line == "" {
            break
        }
        recipe.Ingredients = append(recipe.Ingredients, line)
    }

    fmt.Println("Instructions:")
    for {
        line, _ := reader.ReadString('\n')
        line = strings.TrimSpace(line)
        if line == "" {
            break
        }
        recipe.Instructions += line + "\n"
    }

    recipes = append(recipes, recipe)
    fmt.Println("Recipe added successfully!")

    return recipes
}

The addRecipe function prompts the user for the recipe name, ingredients (entered one per line), and instructions. It adds the new recipe to the slice of recipes and returns the updated list.

Searching for Recipes

Let’s implement a function that allows users to search for recipes by name.

Add the following function to the main.go file:

func searchRecipes(recipes []Recipe, searchTerm string) []Recipe {
    var matchingRecipes []Recipe

    for _, recipe := range recipes {
        if strings.Contains(strings.ToLower(recipe.Name), strings.ToLower(searchTerm)) {
            matchingRecipes = append(matchingRecipes, recipe)
        }
    }

    return matchingRecipes
}

The searchRecipes function takes a slice of recipes and a search term as arguments. It searches for recipes whose name contains the search term (case-insensitive) and returns a new slice containing the matching recipes.

Deleting a Recipe

To allow users to delete a recipe from the recipe list, let’s implement a function that removes a recipe based on its index.

Add the following function to the main.go file:

func deleteRecipe(recipes []Recipe, index int) []Recipe {
    if index < 0 || index >= len(recipes) {
        fmt.Println("Invalid recipe index!")
        return recipes
    }

    fmt.Printf("Are you sure you want to delete '%s'? (y/n): ", recipes[index].Name)
    reader := bufio.NewReader(os.Stdin)
    confirmation, _ := reader.ReadString('\n')
    confirmation = strings.TrimSpace(confirmation)
    if confirmation != "y" {
        fmt.Println("Deletion canceled.")
        return recipes
    }

    copy(recipes[index:], recipes[index+1:])
    recipes[len(recipes)-1] = Recipe{}
    recipes = recipes[:len(recipes)-1]

    fmt.Println("Recipe deleted successfully!")

    return recipes
}

The deleteRecipe function takes a slice of recipes and an index as arguments. It removes the recipe at the specified index from the slice.

Saving Changes to a File

To allow users to save their changes to a file, let’s implement a function that writes the updated recipe list to a given file path.

Add the following function to the main.go file:

func saveRecipesToFile(filepath string, recipes []Recipe) error {
    file, err := os.Create(filepath)
    if err != nil {
        return err
    }
    defer file.Close()

    writer := bufio.NewWriter(file)
    for _, recipe := range recipes {
        fmt.Fprintln(writer, recipe.Name)
        fmt.Fprintln(writer, recipe.Instructions)
        for _, ingredient := range recipe.Ingredients {
            fmt.Fprintln(writer, ingredient)
        }
        fmt.Fprintln(writer)
    }

    return writer.Flush()
}

The saveRecipesToFile function takes a file path and a slice of recipes as arguments. It opens the file for writing, writes each recipe’s details to the file, and flushes the writer to ensure all data is written.

Conclusion

In this tutorial, we have developed a command-line recipe manager using Go. We learned how to load recipes from a file, display the recipe list, add new recipes, search for recipes, delete recipes, and save changes to a file. Feel free to enhance this recipe manager further by adding additional features such as editing recipes or improving the user interface.

Happy cooking with your command-line recipe manager in Go!