summary refs log tree commit diff stats
path: root/main.go
blob: 13971e78724ec85ab8248635a41f25fd63a4dca5 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
package main

import (
	"bufio"
	"fmt"
	"os"

	"tildegit.org/andinus/grus/lexical"
	"tildegit.org/andinus/lynx"
)

func main() {
	initGrus()

	if len(os.Args) == 1 {
		fmt.Println("Usage: grus <word> <dictionaries>")
		os.Exit(1)
	}

	version := "v0.3.1"

	// Print version if first argument is version.
	if os.Args[1] == "version" {
		fmt.Printf("Grus %s\n", version)
		os.Exit(0)
	}

	// Define default environment variables.
	envVar := make(map[string]bool)
	envVar["GRUS_SEARCH_ALL"] = false
	envVar["GRUS_ANAGRAMS"] = true
	envVar["GRUS_STRICT_UNJUMBLE"] = false
	envVar["GRUS_PRINT_PATH"] = false

	// Check environment variables.
	for k, _ := range envVar {
		env := os.Getenv(k)
		if env == "false" ||
			env == "0" {
			envVar[k] = false
		} else if env == "true" ||
			env == "1" {
			envVar[k] = true
		}
	}

	// Print environment variables if first argument is env.
	if os.Args[1] == "env" {
		for k, v := range envVar {
			fmt.Printf("%s: %t\n", k, v)
		}
		os.Exit(0)
	}

	// Define default dictionaries.
	dicts := []string{
		"/usr/local/share/dict/words",
		"/usr/local/share/dict/web2",
		"/usr/share/dict/words",
		"/usr/share/dict/web2",
		"/usr/share/dict/special/4bsd",
		"/usr/share/dict/special/math",
	}

	// User has specified dictionaries, prepend them to dicts
	// list.
	if len(os.Args) >= 3 {
		dicts = append(os.Args[2:], dicts...)
	}

	// We use this to record if the word was unjumbled.
	unjumbled := false

	for k, dict := range dicts {
		if _, err := os.Stat(dict); err != nil &&
			!os.IsNotExist(err) {
			// Error is not nil & also it's not path
			// doesn't exist error. We do it this way to
			// avoid another level of indentation.
			panic(err)
		} else if err != nil &&
			os.IsNotExist(err) {
			// If file doesn't exist then continue with
			// next dictionary.
			continue
		}

		// Print path to dictionary if printPath is true.
		if envVar["GRUS_PRINT_PATH"] {
			if k != 0 {
				fmt.Println()
			}
			fmt.Println(dict)
		}

		file, err := os.Open(dict)
		panicOnErr(err)

		scanner := bufio.NewScanner(file)
		for scanner.Scan() {
			// Filter words by comparing length first &
			// run lexical.Sort on it only if the length
			// is equal.
			if len(scanner.Text()) == len(os.Args[1]) &&
				lexical.Sort(scanner.Text()) == lexical.Sort(os.Args[1]) {
				fmt.Println(scanner.Text())
				// If the user doesn't want anagrams
				// then exit the program.
				if !envVar["GRUS_ANAGRAMS"] {
					os.Exit(0)
				}
				unjumbled = true
			}
		}
		panicOnErr(scanner.Err())
		file.Close()

		// If the user has asked to strictly unjumble then we
		// cannot exit till it's unjumbled.
		if envVar["GRUS_STRICT_UNJUMBLE"] &&
			!unjumbled {
			// If user has asked to strictly unjumble & we
			// haven't done that yet & this is the last
			// dictionary then we've failed to unjumble it
			// & the program must exit with a non-zero
			// exit code.
			if k == len(dicts)-1 {
				os.Exit(1)
			}

			// Cannot exit, must search next dictionary.
			continue
		}

		// If user hasn't asked to search all dictionaries
		// then exit the program.
		if !envVar["GRUS_SEARCH_ALL"] {
			os.Exit(0)
		}
	}
}

func initGrus() {
	// We need less permissions on these conditions.
	if len(os.Args) == 1 ||
		os.Args[1] == "version" ||
		os.Args[1] == "env" {
		err := lynx.PledgePromises("stdio")
		panicOnErr(err)
	} else {
		err := lynx.PledgePromises("unveil stdio rpath")
		panicOnErr(err)

		unveil()

		// Drop unveil from promises.
		err = lynx.PledgePromises("stdio rpath")
		panicOnErr(err)
	}
}

func unveil() {
	paths := make(map[string]string)

	paths["/usr/share/dict"] = "r"
	paths["/usr/local/share/dict"] = "r"

	// Unveil user defined dictionaries.
	if len(os.Args) >= 3 {
		for _, dict := range os.Args[2:] {
			paths[dict] = "r"
		}
	}
	// This will not return error if the file doesn't exist.
	err := lynx.UnveilPaths(paths)
	panicOnErr(err)

	// Block further unveil calls.
	err = lynx.UnveilBlock()
	panicOnErr(err)
}

func panicOnErr(err error) {
	if err != nil {
		panic(err)
	}
}