package main import ( "encoding/json" "fmt" "io" "os" "sort" "strings" "time" "unicode" ) // Database record format. Time stamp and name are required. // Tags and notes are optional. type Item struct { Stamp time.Time Name string Tags []string `json:",omitempty"` Notes string `json:",omitempty"` } // Item implements stringer interface func (i *Item) String() string { s := i.Stamp.Format(time.ANSIC) + "\n Name: " + i.Name if len(i.Tags) > 0 { s = fmt.Sprintf("%s\n Tags: %v", s, i.Tags) } if i.Notes > "" { s += "\n Notes: " + i.Notes } return s } // collection of Items type db []*Item // db implements sort.Interface func (d db) Len() int { return len(d) } func (d db) Swap(i, j int) { d[i], d[j] = d[j], d[i] } func (d db) Less(i, j int) bool { return d[i].Stamp.Before(d[j].Stamp) } // hard coded database file name const fn = "sdb.json" func main() { if len(os.Args) == 1 { latest() return } switch os.Args[1] { case "add": add() case "latest": latest() case "tags": tags() case "all": all() case "help": help() default: usage("unrecognized command") } } func usage(err string) { if err > "" { fmt.Println(err) } fmt.Println(`usage: sdb [command] [data] where command is one of add, latest, tags, all, or help.`) } func help() { usage("") fmt.Println(` Commands must be in lower case. If no command is specified, the default command is latest. Latest prints the latest item. All prints all items in chronological order. Tags prints the lastest item for each tag. Help prints this message. Add adds data as a new record. The format is, name [tags] [notes] Name is the name of the item and is required for the add command. Tags are optional. A tag is a single word. A single tag can be specified without enclosing brackets. Multiple tags can be specified by enclosing them in square brackets. Text remaining after tags is taken as notes. Notes do not have to be enclosed in quotes or brackets. The brackets above are only showing that notes are optional. Quotes may be useful however--as recognized by your operating system shell or command line--to allow entry of arbitrary text. In particular, quotes or escape characters may be needed to prevent the shell from trying to interpret brackets or other special characters. Examples: sdb add Bookends // no tags, no notes sdb add Bookends rock my favorite // tag: rock, notes: my favorite sdb add Bookends [rock folk] // two tags sdb add Bookends [] "Simon & Garfunkel" // notes, no tags sdb add "Simon&Garfunkel [artist]" // name: Simon&Garfunkel, tag: artist As shown in the last example, if you use features of your shell to pass all data as a single string, the item name and tags will still be identified by separating whitespace. The database is stored in JSON format in the file "sdb.json" `) } // load data for read only purposes. func load() (db, bool) { d, f, ok := open() if ok { f.Close() if len(d) == 0 { fmt.Println("no items") ok = false } } return d, ok } // open database, leave open func open() (d db, f *os.File, ok bool) { var err error f, err = os.OpenFile(fn, os.O_RDWR|os.O_CREATE, 0666) if err != nil { fmt.Println("cant open??") fmt.Println(err) return } jd := json.NewDecoder(f) err = jd.Decode(&d) // EOF just means file was empty. That's okay with us. if err != nil && err != io.EOF { fmt.Println(err) f.Close() return } ok = true return } // handle latest command func latest() { d, ok := load() if !ok { return } sort.Sort(d) fmt.Println(d[len(d)-1]) } // handle all command func all() { d, ok := load() if !ok { return } sort.Sort(d) for _, i := range d { fmt.Println("-----------------------------------") fmt.Println(i) } fmt.Println("-----------------------------------") } // handle tags command func tags() { d, ok := load() if !ok { return } // we have to traverse the entire list to collect tags so there // is no point in sorting at this point. // collect set of unique tags associated with latest item for each latest := make(map[string]*Item) for _, item := range d { for _, tag := range item.Tags { li, ok := latest[tag] if !ok || item.Stamp.After(li.Stamp) { latest[tag] = item } } } // invert to set of unique items, associated with subset of tags // for which the item is the latest. type itemTags struct { item *Item tags []string } inv := make(map[*Item][]string) for tag, item := range latest { inv[item] = append(inv[item], tag) } // now we sort just the items we will output li := make(db, len(inv)) i := 0 for item := range inv { li[i] = item i++ } sort.Sort(li) // finally ready to print for _, item := range li { tags := inv[item] fmt.Println("-----------------------------------") fmt.Println("Latest item with tags", tags) fmt.Println(item) } fmt.Println("-----------------------------------") } // handle add command func add() { if len(os.Args) < 3 { usage("add command requires data") return } else if len(os.Args) == 3 { add1() } else { add4() } } // add command with one data string. look for ws as separators. func add1() { data := strings.TrimLeftFunc(os.Args[2], unicode.IsSpace) if data == "" { // data must have at least some non-whitespace usage("invalid name") return } sep := strings.IndexFunc(data, unicode.IsSpace) if sep < 0 { // data consists only of a name addItem(data, nil, "") return } name := data[:sep] data = strings.TrimLeftFunc(data[sep:], unicode.IsSpace) if data == "" { // nevermind trailing ws, it's still only a name addItem(name, nil, "") return } if data[0] == '[' { sep = strings.Index(data, "]") if sep < 0 { // close bracketed list for the user. no notes. addItem(name, strings.Fields(data[1:]), "") } else { // brackets make things easy addItem(name, strings.Fields(data[1:sep]), strings.TrimLeftFunc(data[sep+1:], unicode.IsSpace)) } return } sep = strings.IndexFunc(data, unicode.IsSpace) if sep < 0 { // remaining word is a tag addItem(name, []string{data}, "") } else { // there's a tag and some data addItem(name, []string{data[:sep]}, strings.TrimLeftFunc(data[sep+1:], unicode.IsSpace)) } } // add command with multiple strings remaining on command line func add4() { name := os.Args[2] tag1 := os.Args[3] if tag1[0] != '[' { // no brackets makes things easy addItem(name, []string{tag1}, strings.Join(os.Args[4:], " ")) return } if tag1[len(tag1)-1] == ']' { // tags all in one os.Arg is easy too addItem(name, strings.Fields(tag1[1:len(tag1)-1]), strings.Join(os.Args[4:], " ")) return } // start a list for tags var tags []string if tag1 > "[" { tags = []string{tag1[1:]} } for x, tag := range os.Args[4:] { if tag[len(tag)-1] != ']' { tags = append(tags, tag) } else { // found end of tag list if tag > "]" { tags = append(tags, tag[:len(tag)-1]) } addItem(name, tags, strings.Join(os.Args[5+x:], " ")) return } } // close bracketed list for the user. no notes. addItem(name, tags, "") } // complete the add command func addItem(name string, tags []string, notes string) { db, f, ok := open() if !ok { return } defer f.Close() // add the item and format JSON db = append(db, &Item{time.Now(), name, tags, notes}) sort.Sort(db) js, err := json.MarshalIndent(db, "", " ") if err != nil { fmt.Println(err) return } // time to overwrite the file if _, err = f.Seek(0, 0); err != nil { fmt.Println(err) return } f.Truncate(0) if _, err = f.Write(js); err != nil { fmt.Println(err) } }