Skip to content

Commit

Permalink
Various structure tweaks in prep. for subqueries
Browse files Browse the repository at this point in the history
- Extract input read into separate method
- Remove expensive "reduce sources" method in favour of simple map to track
  which directories have been seen.
- Move tilde expansion to parser (will need to run this on subqueries as well).
- Revert wildcarded-LIKE transform (introduced in c438848).
- Escape more forgotten characters in README examples. 😑
  • Loading branch information
kashav committed May 17, 2017
1 parent a6c3339 commit e19bd3a
Show file tree
Hide file tree
Showing 5 changed files with 106 additions and 113 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,8 @@ $ fsql SELECT all FROM ~ WHERE file IS dir
List the name, size, and modification time of JavaScript files in the current directory that were modified after April 1st 2017 (try running this on a `node_modules` directory, it's fast :sunglasses:).

```sh
$ fsql name, size, time FROM . WHERE name LIKE %.js AND time > 'Apr 01 2017 00 00'
$ fsql name, size, time FROM . WHERE name LIKE %.js AND time \> \'Apr 01 2017 00 00\'
$ fsql "name, size, time FROM . WHERE name LIKE %.js AND time > 'Apr 01 2017 00 00'"
```

List all files named `main.go` in `$GOPATH` which are larger than 10.5 kilobytes or smaller than 100 bytes (note the escaped parentheses and redirection symbols, to avoid this, wrap the query in quotes).
Expand Down
12 changes: 12 additions & 0 deletions compare/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,18 @@ func Alpha(comp query.TokenType, a, b string) bool {
case query.NotEquals:
return a != b
case query.Like:
if b[0] == '%' && b[len(b)-1] == '%' {
return strings.Contains(a, b[1:len(b)-1])
}

if b[0] == '%' {
return strings.HasSuffix(a, b[1:])
}

if b[len(b)-1] == '%' {
return strings.HasPrefix(a, b[:len(b)-1])
}

return strings.Contains(a, b)
case query.RLike:
return regexp.MustCompile(b).MatchString(a)
Expand Down
87 changes: 45 additions & 42 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"io"
"log"
"os"
"os/user"
"path/filepath"
"strconv"
"strings"
Expand All @@ -22,6 +21,19 @@ const (
uGIGABYTE = 1024 * uMEGABYTE
)

// Used to track which paths we've seen to avoid revisiting a directory.
var seen = make(map[string]bool, 0)

// Read the command line arguments for the query.
func readInput() string {
if len(os.Args) > 2 {
return strings.Join(os.Args[1:], " ")
}

return os.Args[1]
}

// Runs the appropriate cmp method for the provided condition.
func compare(condition query.Condition, file os.FileInfo) bool {
var retval bool

Expand Down Expand Up @@ -72,6 +84,7 @@ func compare(condition query.Condition, file os.FileInfo) bool {
return retval
}

// Return true iff path contains a substring of any element of exclusions.
func containsAny(exclusions []string, path string) bool {
for _, exclusion := range exclusions {
if strings.Contains(path, exclusion) {
Expand All @@ -83,70 +96,60 @@ func containsAny(exclusions []string, path string) bool {
}

func main() {
var input string
if len(os.Args) == 2 {
input = os.Args[1]
} else {
input = strings.Join(os.Args[1:], " ")
}

usr, err := user.Current()
if err != nil {
log.Fatal(err)
}
input := readInput()

q, err := query.RunParser(input)
if err != nil {
if err == io.ErrUnexpectedEOF {
log.Fatal("Unexpected end of line.")
log.Fatal("Unexpected end of line")
}
log.Fatal(err)
}

// Replace ~ with the home directory in each source directory.
for _, sourceType := range []string{"include", "exclude"} {
for i, src := range q.Sources[sourceType] {
if strings.Contains(src, "~") {
q.Sources[sourceType][i] = filepath.Join(usr.HomeDir, src[1:])
}
}
}

err = q.ReduceInclusions()
if err != nil {
log.Fatal(err)
}

for _, src := range q.Sources["include"] {
filepath.Walk(src, func(path string, info os.FileInfo, err error) error {
if path == "." || path == ".." || err != nil ||
containsAny(q.Sources["exclude"], path) {
if path == "." || path == ".." || err != nil {
return nil
}

if q.ConditionTree.Evaluate(info, compare) {
if q.HasAttribute("mode") {
fmt.Printf("%s\t", info.Mode())
}
if _, ok := seen[path]; ok {
return nil
}
seen[path] = true

if q.HasAttribute("size") {
fmt.Printf("%d\t", info.Size())
// If this path is excluded or the condition is false, return.
if containsAny(q.Sources["exclude"], path) ||
!q.ConditionTree.Evaluate(info, compare) {
return nil
}

if q.HasAttribute("mode") {
fmt.Printf("%s", info.Mode())
if q.HasAttribute("size", "time", "name") {
fmt.Print("\t")
}
}

if q.HasAttribute("time") {
fmt.Printf("%s\t", info.ModTime().Format(time.Stamp))
if q.HasAttribute("size") {
fmt.Printf("%d", info.Size())
if q.HasAttribute("time", "name") {
fmt.Print("\t")
}
}

if q.HasAttribute("time") {
fmt.Printf("%s", info.ModTime().Format(time.Stamp))
if q.HasAttribute("name") {
if strings.Contains(path, usr.HomeDir) {
path = filepath.Join("~", path[len(usr.HomeDir):])
}
fmt.Printf("%s", path)
fmt.Print("\t")
}
}

fmt.Printf("\n")
if q.HasAttribute("name") {
// TODO: Only show file name, instead of the full path?
fmt.Printf("%s", path)
}

fmt.Printf("\n")
return nil
})
}
Expand Down
52 changes: 40 additions & 12 deletions query/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"errors"
"fmt"
"io"
"os/user"
"path/filepath"
"strings"
)

Expand All @@ -12,21 +14,27 @@ func RunParser(input string) (*Query, error) {
return (&parser{}).parse(input)
}

type parser struct {
tokenizer *Tokenizer
current *Token
expected TokenType
}

var allAttributes = map[string]bool{
"mode": true,
"name": true,
"size": true,
"time": true,
}

type parser struct {
tokenizer *Tokenizer
current *Token
expected TokenType
}

// Return true when no attributes are provided (regardless of if the SELECT
// keyword is provided). Returns false otherwise.
func (p *parser) showAllAttributes() (bool, error) {
if p.expect(Select) == nil {
if p.current == nil {
return false, nil
}

if p.current.Type == From || p.current.Type == Where {
return true, nil
}
Expand All @@ -47,6 +55,7 @@ func (p *parser) showAllAttributes() (bool, error) {
return true, nil
}

// Parse each of the clauses in the input string.
func (p *parser) parse(input string) (*Query, error) {
p.tokenizer = NewTokenizer(input)
q := new(Query)
Expand Down Expand Up @@ -80,6 +89,21 @@ func (p *parser) parse(input string) (*Query, error) {
if err != nil {
return nil, err
}

// Replace the tilde with the home directory in each source directory. This
// is only required when the query is wrapped in quotes, since the shell
// will automatically expand tildes otherwise.
usr, err := user.Current()
if err != nil {
return nil, err
}
for _, sourceType := range []string{"include", "exclude"} {
for i, src := range q.Sources[sourceType] {
if strings.Contains(src, "~") {
q.Sources[sourceType][i] = filepath.Join(usr.HomeDir, src[1:])
}
}
}
}

if p.expect(Where) == nil {
Expand All @@ -98,6 +122,7 @@ func (p *parser) parse(input string) (*Query, error) {
return q, nil
}

// Parse the list of attributes provided to the SELECT clause.
func (p *parser) parseAttributes(attributes *map[string]bool) error {
attribute := p.expect(Identifier)
if attribute == nil {
Expand All @@ -118,6 +143,8 @@ func (p *parser) parseAttributes(attributes *map[string]bool) error {
return p.parseAttributes(attributes)
}

// Parse the list of directories passed to the FROM clause. Expects that
// the sources input has an "include" and "exclude" key.
func (p *parser) parseSources(sources *map[string][]string) error {
sourceType := "include"
if p.expect(Minus) != nil {
Expand All @@ -128,6 +155,7 @@ func (p *parser) parseSources(sources *map[string][]string) error {
if source == nil {
return p.currentError()
}

(*sources)[sourceType] = append((*sources)[sourceType], source.Raw)

if p.expect(Comma) == nil {
Expand All @@ -137,6 +165,7 @@ func (p *parser) parseSources(sources *map[string][]string) error {
return p.parseSources(sources)
}

// Parse the condition passed to the WHERE clause.
func (p *parser) parseConditionTree() (*ConditionNode, error) {
s := new(stack)

Expand Down Expand Up @@ -199,6 +228,8 @@ func (p *parser) parseConditionTree() (*ConditionNode, error) {
return s.pop(), nil
}

// Parse a single condition, made up of the negation, identifier (attribute),
// comparator, and value.
func (p *parser) parseNextCondition() (*Condition, error) {
negate := false
if p.expect(Not) != nil {
Expand All @@ -222,12 +253,6 @@ func (p *parser) parseNextCondition() (*Condition, error) {
return nil, p.currentError()
}

// Use regexp to evaluate wildcard (%) in LIKE conditions.
if comp == Like && strings.Contains(value.Raw, "%") {
comp = RLike
value.Raw = strings.Replace(value.Raw, "%", ".*", -1)
}

return &Condition{
Attribute: attr.Raw,
Comparator: comp,
Expand All @@ -236,6 +261,7 @@ func (p *parser) parseNextCondition() (*Condition, error) {
}, nil
}

// Returns the next token if it matches the expectation, nil otherwise.
func (p *parser) expect(t TokenType) *Token {
p.expected = t

Expand All @@ -252,6 +278,8 @@ func (p *parser) expect(t TokenType) *Token {
return nil
}

// Returns the current error, based on the parser's current Token and the
// previously expected TokenType (set in expect).
func (p *parser) currentError() error {
if p.current == nil {
return io.ErrUnexpectedEOF
Expand Down
65 changes: 7 additions & 58 deletions query/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ package query
import (
"fmt"
"os"
"path/filepath"
"strings"
)

// Query represents an input query.
Expand All @@ -14,64 +12,15 @@ type Query struct {
ConditionTree *ConditionNode // Root node of this query's condition tree.
}

// ReduceInclusions reduces this query's sources by removing any source
// which is a subdirectory of another source.
func (q *Query) ReduceInclusions() error {
redundants := make(map[int]bool, len(q.Sources["include"])-1)

for i, base := range q.Sources["include"] {
for j, target := range q.Sources["include"][i+1:] {
if i == (i + j + 1) {
break
}

if base == target {
// Duplicate source entry.
redundants[i+j+1] = true
continue
}

rel, err := filepath.Rel(base, target)
if err != nil || (rel[:2] == ".." && rel[len(rel)-1] != '.') {
// filepath.Rel only returns error when can't make target relative to
// base, i.e. they're disjoint (which is what we want).
continue
} else if strings.Contains(rel, "..") {
// Base directory is redundant, we can exit the inner loop.
redundants[i] = true
break
} else {
// Target directory is redundant.
redundants[i+j+1] = true
}
}
}

sources := make([]string, 0)
for i := 0; i < len(q.Sources["include"]); i++ {
// Skip all redundant directories.
if _, ok := redundants[i]; ok {
continue
}

// Return error iff directory doesn't exist. Should we just ignore
// nonexistent directories instead?
path := q.Sources["include"][i]
_, err := os.Stat(path)
if os.IsNotExist(err) {
return fmt.Errorf("no such file or directory: %s", path)
}
sources = append(sources, q.Sources["include"][i])
}
q.Sources["include"] = sources
return nil
}

// HasAttribute checks if the query's attribute map contains the provided
// attribute.
func (q *Query) HasAttribute(attribute string) bool {
_, found := q.Attributes[attribute]
return found
func (q *Query) HasAttribute(attributes ...string) bool {
for _, attribute := range attributes {
if _, found := q.Attributes[attribute]; found {
return true
}
}
return false
}

// ConditionNode represents a single node of a query's WHERE clause tree.
Expand Down

0 comments on commit e19bd3a

Please sign in to comment.