Commits

Paul Ruane committed e3ec63f

Issue #55: Add support for --database global option.

Refactoring: simplified how CLI commands are represented.
Refactoring: moved entity types from storage/database to entities.
Added support for --database global option.
Added partial support for --database to Zsh completion (does not honour the database specified yet).

  • Participants
  • Parent commits 1099e8e

Comments (0)

Files changed (60)

 ^src/tmsu/common/version.gen.go$
 ^src/main/_testmain.go$
 ^bin/
+^dist/
     directory under the 'queries' directory.
   * Added script to allow the virtual filesystem to be mounted via the
     system mount command or on startup via the fstab.
+  * Added global option --database for specifying database location.
 
 v0.2.2
 ------
 HIGH
 
-R Move to common Command struct rather than struct type per command.
 E Issue #55: Add support for --database option to replace TMSU_DB.
+E Zsh completion for the --database global option.
+T Work out why Sqlite3 is suddenly so slow to build.
 E Issue #55: Zsh completion to honour --database.
-T Document mounting via fstab
-T Document VFS queries.
+T Doc: Mounting via fstab
+T Doc: VFS queries.
+B Long options should use equals rather than space.
 E Additional VFS operations.
 E Issue #33: provide binary for 64 bit Linux.
 E Ensure unit-tests cover all command options.

File misc/zsh/_tmsu

 	local cmd ret=1
 
 	_arguments -C \
-	    ''{--verbose,-v}'[show verbose messages]' \
-	    ''{--version,-V}'[show version information and exit]' \
-	    ''{--help,-h}'[show help and exit]' \
+	    {--verbose,-v}'[show verbose messages]' \
+	    {--version,-V}'[show version information and exit]' \
+	    {--database,-d+}'[use the specified database]:database_path:_files' \
+	    {--help,-h}'[show help and exit]' \
 		'1: :_tmsu_commands' \
 		'*::arg:->args' \
 		&& ret=0

File src/tmsu/cli/cli.go

 import (
 	"os"
 	"tmsu/log"
+	"tmsu/storage/database"
 )
 
+type Command struct {
+	Name        string
+	Synopsis    string
+	Description string
+	Options     Options
+	Exec        func(options Options, args []string) error
+}
+
 var commands = map[string]*Command{
-	"copy":    CopyCommand,
-	"delete":  DeleteCommand,
-	"dupes":   DupesCommand,
-	"files":   FilesCommand,
-	"help":    HelpCommand,
-	"imply":   ImplyCommand,
-	"merge":   MergeCommand,
-	"mount":   MountCommand,
-	"rename":  RenameCommand,
-	"repair":  RepairCommand,
-	"stats":   StatsCommand,
-	"status":  StatusCommand,
-	"tag":     TagCommand,
-	"tags":    TagsCommand,
-	"unmount": UnmountCommand,
-	"untag":   UntagCommand,
-	"version": VersionCommand,
-	"vfs":     VfsCommand}
+	"copy":    &CopyCommand,
+	"delete":  &DeleteCommand,
+	"dupes":   &DupesCommand,
+	"files":   &FilesCommand,
+	"help":    &HelpCommand,
+	"imply":   &ImplyCommand,
+	"merge":   &MergeCommand,
+	"mount":   &MountCommand,
+	"rename":  &RenameCommand,
+	"repair":  &RepairCommand,
+	"stats":   &StatsCommand,
+	"status":  &StatusCommand,
+	"tag":     &TagCommand,
+	"tags":    &TagsCommand,
+	"unmount": &UnmountCommand,
+	"untag":   &UntagCommand,
+	"version": &VersionCommand,
+	"vfs":     &VfsCommand}
 
 var globalOptions = Options{Option{"--verbose", "-v", "show verbose messages", false, ""},
 	Option{"--help", "-h", "show help and exit", false, ""},
-	Option{"--version", "-V", "show version information and exit", false, ""}}
+	Option{"--version", "-V", "show version information and exit", false, ""},
+	Option{"--database", "-d", "use the specified database", true, ""}}
 
 func Run() {
+	helpCommands = commands
+
 	parser := NewOptionParser(globalOptions, commands)
 	commandName, options, arguments, err := parser.Parse(os.Args[1:])
 	if err != nil {
 	switch {
 	case options.HasOption("--version"):
 		commandName = "version"
-	case options.HasOption("--help"):
-		commandName = "help"
-	case commandName == "":
+	case options.HasOption("--help"), commandName == "":
 		commandName = "help"
 	}
 
 	if options.HasOption("--verbose") {
 		log.Verbose = true
 	}
+	if dbOption := options.Get("--database"); dbOption != nil && dbOption.Argument != "" {
+		database.Path = dbOption.Argument
+	}
 
 	command := commands[commandName]
 	if command == nil {

File src/tmsu/cli/command.go

-/*
-Copyright 2011-2013 Paul Ruane.
-
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program.  If not, see <http://www.gnu.org/licenses/>.
-*/
-
-package cli
-
-type Command struct {
-	Name        string
-	Synopsis    string
-	Description string
-	Options     Options
-	Exec        func(options Options, args []string) error
-}

File src/tmsu/cli/common_test.go

 	"path/filepath"
 	"strings"
 	"testing"
+	"tmsu/entities"
 	"tmsu/log"
 	"tmsu/storage"
-	"tmsu/storage/entities"
 )
 
 func configureOutput() (string, string, error) {

File src/tmsu/cli/copy.go

 	"tmsu/storage"
 )
 
-var CopyCommand = &Command{
+var CopyCommand = Command{
 	Name:     "copy",
 	Synopsis: "Create a copy of a tag",
 	Description: `tmsu copy TAG NEW

File src/tmsu/cli/copy_test.go

 
 	// test
 
-	if err := copyCommand.Exec(Options{}, []string{"source", "dest"}); err != nil {
+	if err := CopyCommand.Exec(Options{}, []string{"source", "dest"}); err != nil {
 		test.Fatal(err)
 	}
 
 	databasePath := configureDatabase()
 	defer os.Remove(databasePath)
 
-	command := CopyCommand{false}
-
 	// test
 
-	err := command.Exec(Options{}, []string{"source", "dest"})
+	err := CopyCommand.Exec(Options{}, []string{"source", "dest"})
 
 	// validate
 
 	databasePath := configureDatabase()
 	defer os.Remove(databasePath)
 
-	command := CopyCommand{false}
-
 	// test
 
-	err := command.Exec(Options{}, []string{"source", "slash/invalid"})
+	err := CopyCommand.Exec(Options{}, []string{"source", "slash/invalid"})
 
 	// validate
 
 		test.Fatal(err)
 	}
 
-	command := CopyCommand{false}
-
 	// test
 
-	err = command.Exec(Options{}, []string{"source", "dest"})
+	err = CopyCommand.Exec(Options{}, []string{"source", "dest"})
 
 	// validate
 

File src/tmsu/cli/delete.go

 	"tmsu/storage"
 )
 
-var DeleteCommand = &Command{
+var DeleteCommand = Command{
 	Name:     "delete",
 	Synopsis: "Delete one or more tags",
 	Description: `tmsu delete TAG...

File src/tmsu/cli/delete_test.go

 		test.Fatal(err)
 	}
 
-	command := deleteCommand{false}
-
 	// test
 
-	if err := command.Exec(Options{}, []string{"deathrow"}); err != nil {
+	if err := DeleteCommand.Exec(Options{}, []string{"deathrow"}); err != nil {
 		test.Fatal(err)
 	}
 
 	databasePath := configureDatabase()
 	defer os.Remove(databasePath)
 
-	command := deleteCommand{false}
-
 	// test
 
-	err := command.Exec(cli.Options{}, []string{"deleteme"})
+	err := DeleteCommand.Exec(Options{}, []string{"deleteme"})
 
 	// validate
 

File src/tmsu/cli/dupes.go

 import (
 	"fmt"
 	"path/filepath"
+	"tmsu/entities"
 	"tmsu/fingerprint"
 	"tmsu/log"
 	_path "tmsu/path"
 	"tmsu/storage"
-	"tmsu/storage/entities"
 )
 
-var DupesCommand = &Command{
+var DupesCommand = Command{
 	Name:     "dupes",
 	Synopsis: "Identify duplicate files",
 	Description: `tmsu dupes [FILE]...

File src/tmsu/cli/dupes_test.go

 		test.Fatal(err)
 	}
 
-	command := dupesCommand{false, false}
-
 	// test
 
-	if err := command.Exec(Options{}, []string{}); err != nil {
+	if err := DupesCommand.Exec(Options{}, []string{}); err != nil {
 		test.Fatal(err)
 	}
 
 		test.Fatal(err)
 	}
 
-	command := dupesCommand{false, false}
-
 	// test
 
-	if err := command.Exec(Options{}, []string{}); err != nil {
+	if err := DupesCommand.Exec(Options{}, []string{}); err != nil {
 		test.Fatal(err)
 	}
 
 		test.Fatal(err)
 	}
 
-	command := dupesCommand{false, false}
-
 	// test
 
-	if err := command.Exec(Options{}, []string{}); err != nil {
+	if err := DupesCommand.Exec(Options{}, []string{}); err != nil {
 		test.Fatal(err)
 	}
 
 		test.Fatal(err)
 	}
 
-	command := dupesCommand{false, false}
-
 	// test
 
-	if err := command.Exec(Options{}, []string{path}); err != nil {
+	if err := DupesCommand.Exec(Options{}, []string{path}); err != nil {
 		test.Fatal(err)
 	}
 
 		test.Fatal(err)
 	}
 
-	command := dupesCommand{false, false}
-
 	// test
 
-	if err := command.Exec(Options{}, []string{path}); err != nil {
+	if err := DupesCommand.Exec(Options{}, []string{path}); err != nil {
 		test.Fatal(err)
 	}
 
 		test.Fatal(err)
 	}
 
-	command := DupesCommand{false, false}
-
 	// test
 
-	if err := command.Exec(Options{}, []string{path}); err != nil {
+	if err := DupesCommand.Exec(Options{}, []string{path}); err != nil {
 		test.Fatal(err)
 	}
 

File src/tmsu/cli/files.go

 import (
 	"fmt"
 	"strings"
+	"tmsu/entities"
 	"tmsu/log"
 	"tmsu/path"
 	"tmsu/query"
 	"tmsu/storage"
-	"tmsu/storage/entities"
 )
 
-var FilesCommand = &Command{
+var FilesCommand = Command{
 	Name:     "files",
 	Synopsis: "List files with particular tags",
 	Description: `tmsu files [OPTION]... QUERY 

File src/tmsu/cli/files_test.go

 		test.Fatal(err)
 	}
 
-	command := filesCommand{}
-
 	// test
 
-	if err := command.Exec(Options{Option{"-a", "--all", "", false, ""}}, []string{}); err != nil {
+	if err := FilesCommand.Exec(Options{Option{"-a", "--all", "", false, ""}}, []string{}); err != nil {
 		test.Fatal(err)
 	}
 
 		test.Fatal(err)
 	}
 
-	command := filesCommand{}
-
 	// test
 
-	if err := command.Exec(Options{}, []string{"b"}); err != nil {
+	if err := FilesCommand.Exec(Options{}, []string{"b"}); err != nil {
 		test.Fatal(err)
 	}
 
 		test.Fatal(err)
 	}
 
-	command := filesCommand{}
-
 	// test
 
-	if err := command.Exec(Options{}, []string{"not", "b"}); err != nil {
+	if err := FilesCommand.Exec(Options{}, []string{"not", "b"}); err != nil {
 		test.Fatal(err)
 	}
 
 		test.Fatal(err)
 	}
 
-	command := filesCommand{}
-
 	// test
 
-	if err := command.Exec(Options{}, []string{"b", "c"}); err != nil {
+	if err := FilesCommand.Exec(Options{}, []string{"b", "c"}); err != nil {
 		test.Fatal(err)
 	}
 
 		test.Fatal(err)
 	}
 
-	command := filesCommand{}
-
 	// test
 
-	if err := command.Exec(Options{}, []string{"b", "and", "c"}); err != nil {
+	if err := FilesCommand.Exec(Options{}, []string{"b", "and", "c"}); err != nil {
 		test.Fatal(err)
 	}
 
 		test.Fatal(err)
 	}
 
-	command := filesCommand{}
-
 	// test
 
-	if err := command.Exec(Options{}, []string{"b", "not", "c"}); err != nil {
+	if err := FilesCommand.Exec(Options{}, []string{"b", "not", "c"}); err != nil {
 		test.Fatal(err)
 	}
 
 		test.Fatal(err)
 	}
 
-	command := filesCommand{}
-
 	// test
 
-	if err := command.Exec(Options{}, []string{"b", "and", "not", "c"}); err != nil {
+	if err := FilesCommand.Exec(Options{}, []string{"b", "and", "not", "c"}); err != nil {
 		test.Fatal(err)
 	}
 
 		test.Fatal(err)
 	}
 
-	command := filesCommand{}
-
 	// test
 
-	if err := command.Exec(Options{}, []string{"b", "or", "c"}); err != nil {
+	if err := FilesCommand.Exec(Options{}, []string{"b", "or", "c"}); err != nil {
 		test.Fatal(err)
 	}
 

File src/tmsu/cli/help.go

 	"tmsu/log"
 )
 
-var HelpCommand = &Command{
+var HelpCommand = Command{
 	Name:     "help",
 	Synopsis: "List commands or show help for a particular command",
 	Description: `tmsu help [OPTION]... [COMMAND]
 	Exec:    helpExec,
 }
 
+var helpCommands map[string]*Command
+
 func helpExec(options Options, args []string) error {
 	if options.HasOption("--list") {
 		listCommands()
 	log.Print()
 
 	var maxWidth int = 0
-	commandNames := make([]string, 0, len(commands))
-	for _, command := range commands {
-		commandName := command.Name()
+	commandNames := make([]string, 0, len(helpCommands))
+	for _, command := range helpCommands {
+		commandName := command.Name
 		maxWidth = int(math.Max(float64(maxWidth), float64(len(commandName))))
 		commandNames = append(commandNames, commandName)
 	}
 
-	sort.Sort(commandNames)
+	sort.Strings(commandNames)
 
 	for _, commandName := range commandNames {
-		command, _ := commands[commandName]
+		command, _ := helpCommands[commandName]
 
-		commandSummary := command.Synopsis()
+		commandSummary := command.Synopsis
 		if commandSummary == "" {
 			continue
 		}
 
-		log.Printf("  %-"+strconv.Itoa(maxWidth)+"v  %v", command.Name(), commandSummary)
+		log.Printf("  %-"+strconv.Itoa(maxWidth)+"v  %v", command.Name, commandSummary)
+	}
+
+	log.Print()
+
+	log.Print("Global options:")
+	log.Print()
+
+	for _, option := range globalOptions {
+		log.Printf("  %v, %v: %v", option.ShortName, option.LongName, option.Description)
 	}
 
 	log.Print()
 }
 
 func listCommands() {
-	commandNames := make([]string, 0, len(commands))
+	commandNames := make([]string, 0, len(helpCommands))
 
-	for _, command := range commands {
-		if command.Synopsis() == "" {
+	for _, command := range helpCommands {
+		if command.Synopsis == "" {
 			continue
 		}
 
-		commandNames = append(commandNames, command.Name())
+		commandNames = append(commandNames, command.Name)
 	}
 
-	sort.Sort(commandNames)
+	sort.Strings(commandNames)
 
 	for _, commandName := range commandNames {
 		log.Print(commandName)
 }
 
 func describeCommand(commandName string) {
-	command := commands[commandName]
+	command := helpCommands[commandName]
 	if command == nil {
 		log.Printf("No such command '%v'.", commandName)
 		return
 	}
 
-	log.Print(command.Description())
+	log.Print(command.Description)
 
-	if len(command.Options()) > 0 {
+	if len(command.Options) > 0 {
 		log.Print()
 
 		log.Print("Options:")
 		log.Print()
 
-		for _, option := range command.Options() {
+		for _, option := range command.Options {
 			log.Printf("  %v, %v: %v", option.ShortName, option.LongName, option.Description)
 		}
 	}

File src/tmsu/cli/imply.go

 	"tmsu/storage"
 )
 
-var ImplyCommand = &Command{
+var ImplyCommand = Command{
 	Name:     "imply",
 	Synopsis: "Creates a tag implication",
 	Description: `tmsu [OPTION] imply TAG1 TAG2

File src/tmsu/cli/merge.go

 	"tmsu/storage"
 )
 
-var MergeCommand = &Command{
+var MergeCommand = Command{
 	Name:     "merge",
 	Synopsis: "Merge tags",
 	Description: `tmsu merge TAG... DEST

File src/tmsu/cli/merge_test.go

 		test.Fatal(err)
 	}
 
-	command := MergeCommand{false}
-
 	// test
 
-	if err := command.Exec(Options{}, []string{"a", "b"}); err != nil {
+	if err := MergeCommand.Exec(Options{}, []string{"a", "b"}); err != nil {
 		test.Fatal(err)
 	}
 
 		test.Fatal(err)
 	}
 
-	command := mergeCommand{false}
-
 	// test
 
-	if err := command.Exec(Options{}, []string{"a", "b", "c"}); err != nil {
+	if err := MergeCommand.Exec(Options{}, []string{"a", "b", "c"}); err != nil {
 		test.Fatal(err)
 	}
 
 		test.Fatal(err)
 	}
 
-	command := mergeCommand{false}
-
 	// test
 
-	if err := command.Exec(Options{}, []string{"a", "b"}); err == nil {
+	if err := MergeCommand.Exec(Options{}, []string{"a", "b"}); err == nil {
 		test.Fatal("Expected non-existent source tag to be identified.")
 	}
 }
 		test.Fatal(err)
 	}
 
-	command := mergeCommand{false}
-
 	// test
 
-	if err := command.Exec(Options{}, []string{"a", "b"}); err == nil {
+	if err := MergeCommand.Exec(Options{}, []string{"a", "b"}); err == nil {
 		test.Fatal("Expected non-existent destination tag to be identified.")
 	}
 }
 		test.Fatal(err)
 	}
 
-	command := MergeCommand{false}
-
 	// test
 
-	if err := command.Exec(Options{}, []string{"a", "a"}); err == nil {
+	if err := MergeCommand.Exec(Options{}, []string{"a", "a"}); err == nil {
 		test.Fatal("Expected source and destination the same tag to be identified.")
 	}
 }

File src/tmsu/cli/mount.go

 	"os/exec"
 	"syscall"
 	"time"
-	"tmsu/common"
 	"tmsu/log"
+	"tmsu/storage/database"
 	"tmsu/vfs"
 )
 
-var MountCommand = &Command{
+var MountCommand = Command{
 	Name:     "mount",
 	Synopsis: "Mount the virtual filesystem",
 	Description: `tmsu mount
 	case 1:
 		mountPath := args[0]
 
-		err := mountSelected(mountPath, allowOther)
+		err := mountDefault(mountPath, allowOther)
 		if err != nil {
 			return fmt.Errorf("could not mount database at '%v': %v", mountPath, err)
 		}
 	return nil
 }
 
-func mountSelected(mountPath string, allowOther bool) error {
-	databasePath, err := common.GetDatabasePath()
-	if err != nil {
-		return fmt.Errorf("could not get selected database configuration: %v", err)
-	}
-
-	if err = mountExplicit(databasePath, mountPath, allowOther); err != nil {
+func mountDefault(mountPath string, allowOther bool) error {
+	if err := mountExplicit(database.Path, mountPath, allowOther); err != nil {
 		return err
 	}
 

File src/tmsu/cli/option_test.go

 )
 
 func TestParseVanillaArguments(test *testing.T) {
-	parser := NewOptionParser(Options{}, make(map[CommandName]Command))
+	parser := NewOptionParser(Options{}, make(map[string]*Command))
 
 	commandName, options, arguments, err := parser.Parse([]string{"a", "b", "c"})
 	if err != nil {
 }
 
 func TestParseGlobalOptions(test *testing.T) {
-	parser := NewOptionParser(Options{Option{"--verbose", "-v", "verbose", false, ""}}, make(map[CommandName]Command))
+	parser := NewOptionParser(Options{Option{"--verbose", "-v", "verbose", false, ""}}, make(map[string]*Command))
 
 	commandName, options, arguments, err := parser.Parse([]string{"--verbose", "a", "b"})
 	if err != nil {
 }
 
 func TestInvalidGlobalOption(test *testing.T) {
-	parser := NewOptionParser(Options{}, make(map[CommandName]Command))
+	parser := NewOptionParser(Options{}, make(map[string]*Command))
 
 	_, _, _, err := parser.Parse([]string{"--invalid", "a", "b"})
 

File src/tmsu/cli/rename.go

 	"tmsu/storage"
 )
 
-var RenameCommand = &Command{
+var RenameCommand = Command{
 	Name:     "rename",
 	Synopsis: "Rename a tag",
 	Description: `tmsu rename OLD NEW

File src/tmsu/cli/rename_test.go

 		test.Fatal(err)
 	}
 
-	command := renameCommand{false}
-
 	// test
 
-	if err := command.Exec(Options{}, []string{"source", "dest"}); err != nil {
+	if err := RenameCommand.Exec(Options{}, []string{"source", "dest"}); err != nil {
 		test.Fatal(err)
 	}
 
 	databasePath := configureDatabase()
 	defer os.Remove(databasePath)
 
-	command := renameCommand{false}
-
 	// test
 
-	err := command.Exec(Options{}, []string{"source", "dest"})
+	err := RenameCommand.Exec(Options{}, []string{"source", "dest"})
 
 	// validate
 
 	databasePath := configureDatabase()
 	defer os.Remove(databasePath)
 
-	command := renameCommand{false}
-
 	// test
 
-	err := command.Exec(Options{}, []string{"source", "slash/invalid"})
+	err := RenameCommand.Exec(Options{}, []string{"source", "slash/invalid"})
 
 	// validate
 
 		test.Fatal(err)
 	}
 
-	command := renameCommand{false}
-
 	// test
 
-	err = command.Exec(Options{}, []string{"source", "dest"})
+	err = RenameCommand.Exec(Options{}, []string{"source", "dest"})
 
 	// validate
 

File src/tmsu/cli/repair.go

 	"fmt"
 	"os"
 	"path/filepath"
+	"tmsu/entities"
 	"tmsu/fingerprint"
 	"tmsu/log"
 	_path "tmsu/path"
 	"tmsu/storage"
-	"tmsu/storage/entities"
 )
 
-var RepairCommand = &Command{
+var RepairCommand = Command{
 	Name:     "repair",
 	Synopsis: "Repair the database",
 	Description: `tmsu [OPTION]... repair [PATH]...

File src/tmsu/cli/repair_test.go

 	}
 	defer os.Remove("/tmp/tmsu/a")
 
-	tagCommand := TagCommand{false, false}
-	if err := tagCommand.Exec(Options{}, []string{"/tmp/tmsu/a", "a"}); err != nil {
+	if err := TagCommand.Exec(Options{}, []string{"/tmp/tmsu/a", "a"}); err != nil {
 		test.Fatal(err)
 	}
 
 		test.Fatal(err)
 	}
 
-	command := repairCommand{false, false, false}
-
 	// test
 
-	if err := command.Exec(Options{}, []string{"/tmp/tmsu"}); err != nil {
+	if err := RepairCommand.Exec(Options{}, []string{"/tmp/tmsu"}); err != nil {
 		test.Fatal(err)
 	}
 
 	}
 	defer os.Remove("/tmp/tmsu/a")
 
-	tagCommand := TagCommand{false, false}
-	if err := tagCommand.Exec(Options{}, []string{"/tmp/tmsu/a", "a"}); err != nil {
+	if err := TagCommand.Exec(Options{}, []string{"/tmp/tmsu/a", "a"}); err != nil {
 		test.Fatal(err)
 	}
 
 		test.Fatal(err)
 	}
 
-	command := repairCommand{false, false, false}
-
 	// test
 
-	if err := command.Exec(Options{}, []string{"/tmp/tmsu"}); err != nil {
+	if err := RepairCommand.Exec(Options{}, []string{"/tmp/tmsu"}); err != nil {
 		test.Fatal(err)
 	}
 
 		test.Fatal(err)
 	}
 
-	tagCommand := TagCommand{false, false}
-	if err := tagCommand.Exec(Options{}, []string{"/tmp/tmsu/a", "a"}); err != nil {
+	if err := TagCommand.Exec(Options{}, []string{"/tmp/tmsu/a", "a"}); err != nil {
 		test.Fatal(err)
 	}
 
 		test.Fatal(err)
 	}
 
-	command := repairCommand{false, false, false}
-
 	// test
 
-	if err := command.Exec(Options{}, []string{}); err != nil {
+	if err := RepairCommand.Exec(Options{}, []string{}); err != nil {
 		test.Fatal(err)
 	}
 

File src/tmsu/cli/stats.go

 	"tmsu/storage"
 )
 
-var StatsCommand = &Command{
+var StatsCommand = Command{
 	Name:     "stats",
 	Synopsis: "Show database statistics",
 	Description: `tmsu stats

File src/tmsu/cli/status.go

 	"os"
 	"path/filepath"
 	"strings"
+	"tmsu/entities"
 	"tmsu/log"
 	"tmsu/path"
 	"tmsu/storage"
-	"tmsu/storage/entities"
 )
 
-var StatusCommand = &Command{
+var StatusCommand = Command{
 	Name:     "status",
 	Synopsis: "List the file tagging status",
 	Description: `tmsu status [PATH]...

File src/tmsu/cli/status_test.go

 	}
 	defer os.Remove("/tmp/tmsu/d")
 
-	tagCommand := TagCommand{false, false}
-
-	if err := tagCommand.Exec(Options{}, []string{"/tmp/tmsu/a", "a"}); err != nil {
+	if err := TagCommand.Exec(Options{}, []string{"/tmp/tmsu/a", "a"}); err != nil {
 		test.Fatal(err)
 	}
 
-	if err := tagCommand.Exec(Options{}, []string{"/tmp/tmsu/b", "b"}); err != nil {
+	if err := TagCommand.Exec(Options{}, []string{"/tmp/tmsu/b", "b"}); err != nil {
 		test.Fatal(err)
 	}
 
-	if err := tagCommand.Exec(Options{}, []string{"/tmp/tmsu/d", "d"}); err != nil {
+	if err := TagCommand.Exec(Options{}, []string{"/tmp/tmsu/d", "d"}); err != nil {
 		test.Fatal(err)
 	}
 
 
 	// test
 
-	if err := statusCommand.Exec(Options{}, []string{"/tmp/tmsu/a", "/tmp/tmsu/b", "/tmp/tmsu/c", "/tmp/tmsu/d"}); err != nil {
+	if err := StatusCommand.Exec(Options{}, []string{"/tmp/tmsu/a", "/tmp/tmsu/b", "/tmp/tmsu/c", "/tmp/tmsu/d"}); err != nil {
 		test.Fatal(err)
 	}
 

File src/tmsu/cli/tag.go

 	"path/filepath"
 	"strings"
 	"time"
+	"tmsu/entities"
 	"tmsu/fingerprint"
 	"tmsu/log"
 	"tmsu/storage"
-	"tmsu/storage/entities"
 )
 
-var TagCommand = &Command{
+var TagCommand = Command{
 	Name:     "tag",
 	Synopsis: "Apply tags to files",
 	Description: `tmsu tag [OPTION]... FILE TAG...

File src/tmsu/cli/tag_test.go

 
 	// test
 
-	if err := tagCommand.Exec(Options{}, []string{"/tmp/tmsu/a", "apple"}); err != nil {
+	if err := TagCommand.Exec(Options{}, []string{"/tmp/tmsu/a", "apple"}); err != nil {
 		test.Fatal(err)
 	}
 
 
 	// test
 
-	if err := tagCommand.Exec(Options{}, []string{"/tmp/tmsu/a", "apple", "banana", "clementine"}); err != nil {
+	if err := TagCommand.Exec(Options{}, []string{"/tmp/tmsu/a", "apple", "banana", "clementine"}); err != nil {
 		test.Fatal(err)
 	}
 
 
 	// test
 
-	if err := tagCommand.Exec(Options{Option{"--tags", "-t", "", true, "apple banana"}}, []string{"/tmp/tmsu/a", "/tmp/tmsu/b"}); err != nil {
+	if err := TagCommand.Exec(Options{Option{"--tags", "-t", "", true, "apple banana"}}, []string{"/tmp/tmsu/a", "/tmp/tmsu/b"}); err != nil {
 		test.Fatal(err)
 	}
 

File src/tmsu/cli/tags.go

 	"sort"
 	"strconv"
 	"strings"
+	"tmsu/entities"
 	"tmsu/log"
 	"tmsu/storage"
-	"tmsu/storage/entities"
 )
 
-var TagsCommand = &Command{
+var TagsCommand = Command{
 	Name:     "tags",
 	Synopsis: "List tags",
 	Description: `tmsu tags [OPTION]... [FILE]...

File src/tmsu/cli/tags_test.go

 
 	// test
 
-	if err := tagsCommand.Exec(Options{}, []string{"/tmp/tmsu/a"}); err != nil {
+	if err := TagsCommand.Exec(Options{}, []string{"/tmp/tmsu/a"}); err != nil {
 		test.Fatal(err)
 	}
 
 
 	// test
 
-	if err := tagsCommand.Exec(Options{}, []string{"/tmp/tmsu/a", "/tmp/tmsu/b"}); err != nil {
+	if err := TagsCommand.Exec(Options{}, []string{"/tmp/tmsu/a", "/tmp/tmsu/b"}); err != nil {
 		test.Fatal(err)
 	}
 
 
 	// test
 
-	if err := tagsCommand.Exec(Options{Option{"--all", "-a", "", false, ""}}, []string{}); err != nil {
+	if err := TagsCommand.Exec(Options{Option{"--all", "-a", "", false, ""}}, []string{}); err != nil {
 		test.Fatal(err)
 	}
 

File src/tmsu/cli/unmount.go

 	"tmsu/vfs"
 )
 
-var UnmountCommand = &Command{
+var UnmountCommand = Command{
 	Name:     "unmount",
 	Synopsis: "Unmount the virtual file-system",
 	Description: `tmsu unmount MOUNTPOINT

File src/tmsu/cli/untag.go

 	"fmt"
 	"path/filepath"
 	"strings"
+	"tmsu/entities"
 	"tmsu/log"
 	"tmsu/storage"
-	"tmsu/storage/entities"
 )
 
-var UntagCommand = &Command{
+var UntagCommand = Command{
 	Name:     "untag",
 	Synopsis: "Remove tags from files",
 	Description: `tmsu untag [OPTION]... FILE TAG...

File src/tmsu/cli/untag_test.go

 
 	// test
 
-	if err := untagCommand.Exec(Options{}, []string{"/tmp/tmsu/a", "apple"}); err != nil {
+	if err := UntagCommand.Exec(Options{}, []string{"/tmp/tmsu/a", "apple"}); err != nil {
 		test.Fatal(err)
 	}
 
 
 	// test
 
-	if err := untagCommand.Exec(Options{}, []string{"/tmp/tmsu/a", "apple", "banana"}); err != nil {
+	if err := UntagCommand.Exec(Options{}, []string{"/tmp/tmsu/a", "apple", "banana"}); err != nil {
 		test.Fatal(err)
 	}
 
 
 	// test
 
-	if err := untagCommand.Exec(Options{Option{"--tags", "-t", "", true, "apple"}}, []string{"/tmp/tmsu/a", "/tmp/tmsu/b"}); err != nil {
+	if err := UntagCommand.Exec(Options{Option{"--tags", "-t", "", true, "apple"}}, []string{"/tmp/tmsu/a", "/tmp/tmsu/b"}); err != nil {
 		test.Fatal(err)
 	}
 
 
 	// test
 
-	if err := untagCommand.Exec(Options{Option{"--all", "-a", "", false, ""}}, []string{"/tmp/tmsu/a", "/tmp/tmsu/b"}); err != nil {
+	if err := UntagCommand.Exec(Options{Option{"--all", "-a", "", false, ""}}, []string{"/tmp/tmsu/a", "/tmp/tmsu/b"}); err != nil {
 		test.Fatal(err)
 	}
 

File src/tmsu/cli/version.go

 	"tmsu/log"
 )
 
-var VersionCommand = &Command{
+var VersionCommand = Command{
 	Name:     "version",
 	Synopsis: "Display version and copyright information",
 	Description: `tmsu version

File src/tmsu/cli/vfs.go

 	"tmsu/vfs"
 )
 
-var VfsCommand = &Command{
+var VfsCommand = Command{
 	Name:        "vfs",
 	Synopsis:    "",
 	Description: "",

File src/tmsu/common/config.go

-/*
-Copyright 2011-2013 Paul Ruane.
-
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program.  If not, see <http://www.gnu.org/licenses/>.
-*/
-
-package common
-
-import (
-	"fmt"
-	"os"
-	"os/user"
-	"path/filepath"
-)
-
-func GetDatabasePath() (string, error) {
-	if path := os.Getenv("TMSU_DB"); path != "" {
-		return path, nil
-	}
-
-	u, err := user.Current()
-	if err != nil {
-		return "", fmt.Errorf("could not retrieve current user: %v", err)
-	}
-
-	return filepath.Join(u.HomeDir, ".tmsu/default.db"), nil
-}

File src/tmsu/entities/file.go

+/*
+Copyright 2011-2013 Paul Ruane.
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+package entities
+
+import (
+	"path/filepath"
+	"time"
+	"tmsu/fingerprint"
+)
+
+type File struct {
+	Id          uint
+	Directory   string
+	Name        string
+	Fingerprint fingerprint.Fingerprint
+	ModTime     time.Time
+	Size        int64
+	IsDir       bool
+}
+
+func (file File) Path() string {
+	return filepath.Join(file.Directory, file.Name)
+}
+
+type Files []*File
+
+func (files Files) Where(predicate func(*File) bool) Files {
+	result := make(Files, 0, 10)
+
+	for _, file := range files {
+		if predicate(file) {
+			result = append(result, file)
+		}
+	}
+
+	return result
+}

File src/tmsu/entities/filetag.go

+/*
+Copyright 2011-2013 Paul Ruane.
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+package entities
+
+type FileTag struct {
+	FileId uint
+	TagId  uint
+}
+
+type FileTags []*FileTag

File src/tmsu/entities/implication.go

+/*
+Copyright 2011-2013 Paul Ruane.
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+package entities
+
+type Implication struct {
+	ImplyingTag Tag
+	ImpliedTag  Tag
+}
+
+type Implications []*Implication

File src/tmsu/entities/query.go

+/*
+Copyright 2011-2013 Paul Ruane.
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+package entities
+
+type Query struct {
+	Text string
+}
+
+type Queries []*Query

File src/tmsu/entities/tag.go

+/*
+Copyright 2011-2013 Paul Ruane.
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+package entities
+
+type Tag struct {
+	Id   uint
+	Name string
+}
+
+type Tags []*Tag
+
+func (tags Tags) Len() int {
+	return len(tags)
+}
+
+func (tags Tags) Swap(i, j int) {
+	tags[i], tags[j] = tags[j], tags[i]
+}
+
+func (tags Tags) Less(i, j int) bool {
+	return tags[i].Name < tags[j].Name
+}
+
+func (tags Tags) Contains(searchTag *Tag) bool {
+	for _, tag := range tags {
+		if tag.Id == searchTag.Id {
+			return true
+		}
+	}
+
+	return false
+}
+
+func (tags Tags) ContainsName(name string) bool {
+	for _, tag := range tags {
+		if tag.Name == name {
+			return true
+		}
+	}
+
+	return false
+}
+
+func (tags Tags) Any(predicate func(*Tag) bool) bool {
+	for _, tag := range tags {
+		if predicate(tag) {
+			return true
+		}
+	}
+
+	return false
+}

File src/tmsu/storage/database/database.go

 	"errors"
 	_ "github.com/mattn/go-sqlite3"
 	"os"
+	"os/user"
 	"path/filepath"
-	"tmsu/common"
 	"tmsu/log"
 )
 
+var Path string
+
+func init() {
+	if path := os.Getenv("TMSU_DB"); path != "" {
+		Path = path
+	} else {
+		u, err := user.Current()
+		if err != nil {
+			log.Fatalf("could not identify current user: %v", err)
+		}
+
+		Path = filepath.Join(u.HomeDir, ".tmsu", "default.db")
+	}
+}
+
 type Database struct {
 	connection *sql.DB
 }
 
 func Open() (*Database, error) {
-	databasePath, err := common.GetDatabasePath()
-	if err != nil {
-		return nil, err
-	}
-
 	// attempt to create database directory
-	dir := filepath.Dir(databasePath)
+	dir := filepath.Dir(Path)
 	os.MkdirAll(dir, os.ModeDir|0755)
 
-	return OpenAt(databasePath)
+	return OpenAt(Path)
 }
 
 func OpenAt(path string) (*Database, error) {

File src/tmsu/storage/database/file.go

 	"path/filepath"
 	"strconv"
 	"time"
+	"tmsu/entities"
 	"tmsu/fingerprint"
 	"tmsu/query"
-	"tmsu/storage/entities"
 )
 
 // Retrieves the total number of tracked files.

File src/tmsu/storage/database/filetag.go

 import (
 	"database/sql"
 	"errors"
-	"tmsu/storage/entities"
+	"tmsu/entities"
 )
 
 // Determines whether the specified file has the specified tag applied.

File src/tmsu/storage/database/implication.go

 import (
 	"database/sql"
 	"strings"
-	"tmsu/storage/entities"
+	"tmsu/entities"
 )
 
 // Retrieves the complete set of tag implications.

File src/tmsu/storage/database/query.go

 import (
 	"database/sql"
 	"errors"
-	"tmsu/storage/entities"
+	"tmsu/entities"
 )
 
 // The complete set of queries.

File src/tmsu/storage/database/tag.go

 	"database/sql"
 	"errors"
 	"strings"
-	"tmsu/storage/entities"
+	"tmsu/entities"
 )
 
 // The number of tags in the database.

File src/tmsu/storage/entities/file.go

-/*
-Copyright 2011-2013 Paul Ruane.
-
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program.  If not, see <http://www.gnu.org/licenses/>.
-*/
-
-package entities
-
-import (
-	"path/filepath"
-	"time"
-	"tmsu/fingerprint"
-)
-
-type File struct {
-	Id          uint
-	Directory   string
-	Name        string
-	Fingerprint fingerprint.Fingerprint
-	ModTime     time.Time
-	Size        int64
-	IsDir       bool
-}
-
-func (file File) Path() string {
-	return filepath.Join(file.Directory, file.Name)
-}
-
-type Files []*File
-
-func (files Files) Where(predicate func(*File) bool) Files {
-	result := make(Files, 0, 10)
-
-	for _, file := range files {
-		if predicate(file) {
-			result = append(result, file)
-		}
-	}
-
-	return result
-}

File src/tmsu/storage/entities/filetag.go

-/*
-Copyright 2011-2013 Paul Ruane.
-
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program.  If not, see <http://www.gnu.org/licenses/>.
-*/
-
-package entities
-
-type FileTag struct {
-	FileId uint
-	TagId  uint
-}
-
-type FileTags []*FileTag

File src/tmsu/storage/entities/implication.go

-/*
-Copyright 2011-2013 Paul Ruane.
-
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program.  If not, see <http://www.gnu.org/licenses/>.
-*/
-
-package entities
-
-type Implication struct {
-	ImplyingTag Tag
-	ImpliedTag  Tag
-}
-
-type Implications []*Implication

File src/tmsu/storage/entities/query.go

-/*
-Copyright 2011-2013 Paul Ruane.
-
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program.  If not, see <http://www.gnu.org/licenses/>.
-*/
-
-package entities
-
-type Query struct {
-	Text string
-}
-
-type Queries []*Query

File src/tmsu/storage/entities/tag.go

-/*
-Copyright 2011-2013 Paul Ruane.
-
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program.  If not, see <http://www.gnu.org/licenses/>.
-*/
-
-package entities
-
-type Tag struct {
-	Id   uint
-	Name string
-}
-
-type Tags []*Tag
-
-func (tags Tags) Len() int {
-	return len(tags)
-}
-
-func (tags Tags) Swap(i, j int) {
-	tags[i], tags[j] = tags[j], tags[i]
-}
-
-func (tags Tags) Less(i, j int) bool {
-	return tags[i].Name < tags[j].Name
-}
-
-func (tags Tags) Contains(searchTag *Tag) bool {
-	for _, tag := range tags {
-		if tag.Id == searchTag.Id {
-			return true
-		}
-	}
-
-	return false
-}
-
-func (tags Tags) ContainsName(name string) bool {
-	for _, tag := range tags {
-		if tag.Name == name {
-			return true
-		}
-	}
-
-	return false
-}
-
-func (tags Tags) Any(predicate func(*Tag) bool) bool {
-	for _, tag := range tags {
-		if predicate(tag) {
-			return true
-		}
-	}
-
-	return false
-}

File src/tmsu/storage/file.go

 import (
 	"fmt"
 	"time"
+	"tmsu/entities"
 	"tmsu/fingerprint"
 	"tmsu/query"
-	"tmsu/storage/entities"
 )
 
 // Retrieves the total number of tracked files.

File src/tmsu/storage/filetag.go

 package storage
 
 import (
-	"tmsu/storage/entities"
+	"tmsu/entities"
 )
 
 // Determines whether the specified file has the specified tag applied.

File src/tmsu/storage/implication.go

 package storage
 
 import (
-	"tmsu/storage/entities"
+	"tmsu/entities"
 )
 
 // Retrieves the complete set of tag implications.

File src/tmsu/storage/query.go

 package storage
 
 import (
-	"tmsu/storage/entities"
+	"tmsu/entities"
 )
 
 // The complete set of queries.

File src/tmsu/storage/tag.go

 	"errors"
 	"fmt"
 	"path/filepath"
-	"tmsu/storage/entities"
+	"tmsu/entities"
 )
 
 // The number of tags in the database.

File src/tmsu/vfs/fusevfs.go

 	"strconv"
 	"strings"
 	"time"
+	"tmsu/entities"
 	"tmsu/log"
 	"tmsu/query"
 	"tmsu/storage"
-	"tmsu/storage/entities"
 )
 
 const tagsDir = "tags"