DictMinder: Setup Project ~ Parsing commands

DictMinder DIY is CLI english dictionary and word reminder.

Why am I making this?

As a Korean, I am struggling with being familiar with English.

The hardest part is memorizing words—when I learn something new, I forget it in a few days.

One day, I thought “Wouldn’t it be nice if something reminded me of the words repeatedly and unconsciously?”

So, I started this project.

Features

  • Store word data

    • Can be added by interactions(manually with standard I/O)

    • Can be addad by external API

    • Can be added by JSON file

    • Can be added by CSV file

  • Delete word data

  • Show word(s) with definitions (+ randomly)

    • By appending this command in the end of ~/.zshrc, we can see random words when terminal is initialized.
  • And so on..

Database Table

I made two tables: words and definitions.

Words can have 0~N definitions.

While definition must have one word.

All columns has ‘NOT NULL’ constraint.

Connecting to Database & Creating Table

Here’s the package I imported:

package db

import (
    "database/sql"
    "log"

    _ "modernc.org/sqlite"
)

type Database struct {
    DB *sql.DB
}

func NewDatabase() (*Database, error) {
    DB, err := sql.Open("sqlite", "data.db?_foreign_keys=on")
    if err != nil {
        return nil, err
    }

    return &Database{DB: DB}, nil
}

func (d *Database) Close() {
    if d.DB != nil {
        err := d.DB.Close()
        if err != nil {
            log.Fatal("Failed to close DB:\n", err.Error())
        }
    }
}

func (d *Database) InitTable() error {
    tx, err := d.DB.Begin()
    if err != nil {
        return err
    }
    defer tx.Rollback()

    _, err = tx.Exec(`
    CREATE TABLE IF NOT EXISTS words (
      word_id INTEGER PRIMARY KEY AUTOINCREMENT,
      content VARCHAR(255) NOT NULL
    );
  `)
    if err != nil {
        return err
    }

    _, err = tx.Exec(`
    CREATE TABLE IF NOT EXISTS definitions (
      definition_id INTEGER PRIMARY KEY AUTOINCREMENT,
      word_id INTEGER NOT NULL,
      pos VARCHAR(255) NOT NULL,
      def text NOT NULL,
      FOREIGN KEY(word_id) REFERENCES words(word_id) ON DELETE CASCADE
    );
    `)
    if err != nil {
        return err
    }

    return tx.Commit()
}

In main:

    // Connect To Database(SQLite)
    database, err := db.NewDatabase()
    if err != nil {
        log.Println("[DictMinder] DB connection Failed: \n", err.Error())
        return
    }
    defer database.Close()

    err = database.InitTable()
    if err != nil {
        log.Println("[DictMinder] DB table initialization failed: \n", err.Error())
        return
    }

Defining Commands

  • dictminder -v: show current version

  • dictminder -r <n>: randomly show word <n> times.

  • dictminder -ai <word>: add <word> with standard I/O interaction.

  • dictminder -aa <word>: add <word> via external api

  • dictminder -aj <json file>: add words from json file

  • dictminder -ac <dictdata.csv>: add words from csv file

  • dictminder —license: show license

  • dictminder -q <word> search about <word>.

  • dictminder -e <word> delete <word> in database.

  • dictminder -l <n> list <n> words in date order(oldest to newest)

  • dictminder --enable-autoload → enable random word show when terminal session created.

  • dictminder --disable-autoload → disable it.

Parsing flags

Imported package flag to parsing flag arguments.

The number of flags were 11, So flags can be managed by one integer with bitmask.

// Set Const Bit Flags..
    const (
        FlagVersion = 1 << iota
        FlagRandom
        FlagInteractive
        FlagAPI
        FlagJSON
        FlagCSV
        FlagLicense
        FlagQuery
        FlagErase
        FlagList
        FlagEnableAutoload
        FlagDisableAutoload
    )
// flag variables
    var (
        version              bool
        randomWord           int
        appendWithIntraction bool
        appendWithAPI        string
        appendWithJSON       string
        appendWithCSV        string
        license              bool
        query                string
        erase                string
        list                 int
        enableAutoload       bool
        disableAutoload      bool

        total int = 0 // total flags
    )

Attach flags with variables with default value and help messages..

Check Each flags whether is different from its default value.

If total is not power of 2, multiple flags were turned on.

So, print error message and exit.

    flag.BoolVar(&version, "v", false, "Print current version of this program.")
    flag.IntVar(&randomWord, "r", 0, "Print random word with definition `<x>` times")
    flag.StringVar(&appendWithIntraction, "ai", "", "Add word `<word>` to DB via interaction")
    flag.StringVar(&appendWithAPI, "aa", "", "Add word `<word>` to DB via Dictionary API.")
    flag.StringVar(&appendWithJSON, "aj", "", "Add word(s) to DB via JSON File `<path>`")
    flag.StringVar(&appendWithCSV, "ac", "", "Add word(s) to DB via CSV File `<path>`")
    flag.BoolVar(&license, "license", false, "Print license of this program")
    flag.StringVar(&query, "q", "", "Search `<word>` in DB.")
    flag.StringVar(&erase, "e", "", "Erase a `<word>` from the DB.")
    flag.IntVar(&list, "l", 0, "Print list of last-appended words with `<x>` rows")
    flag.BoolVar(&enableAutoload, "enable-autoload", false, "Enable to print a single random word when terminal session is created.")
    flag.BoolVar(&disableAutoload, "disable-autoload", false, "Disable to print a single random word when terminal session is created")
    flag.Parse()

    // Flag check wiht bitmask..
    if version {
        total |= FlagVersion
    }
    if randomWord > 0 {
        total |= FlagRandom
    }
    if appendWithIntraction != "" {
        total |= FlagInteractive
    }
    if appendWithAPI != "" {
        total |= FlagAPI
    }
    if appendWithJSON != "" {
        total |= FlagJSON
    }
    if appendWithCSV != "" {
        total |= FlagCSV
    }
    if license {
        total |= FlagLicense
    }
    if query != "" {
        total |= FlagQuery
    }
    if erase != "" {
        total |= FlagErase
    }
    if list > 0 {
        total |= FlagList
    }
    if enableAutoload {
        total |= FlagEnableAutoload
    }
    if disableAutoload {
        total |= FlagDisableAutoload
    }

    // If multiple flags are used, then error
    if total&(total-1) != 0 {
        log.Println("[DictMinder] You can use only one flag for a command.")
        exit
    }

This is the help message.

❯ go run main.go -h

Usage of ....-d/main:
  -aa <word>
        Add word <word> to DB via Dictionary API.
  -ac <path>
        Add word(s) to DB via CSV File <path>
  -ai <word>
        Add word <word> to DB via interaction
  -aj <path>
        Add word(s) to DB via JSON File <path>
  -disable-autoload
        Disable to print a single random word when terminal session is created
  -e <word>
        Erase a <word> from the DB.
  -enable-autoload
        Enable to print a single random word when terminal session is created.
  -l <x>
        Print list of last-appended words with <x> rows
  -license
        Print license of this program
  -q <word>
        Search <word> in DB.
  -r <x>
        Print random word with definition <x> times
  -v    Print current version of this program.

To do next:

  • Connect to Database

  • Parsing Flags

  • Flag-to-command Routing

  • Implement each command

  • Test

  • Version tagging

  • Build tools

  • Deploy