Parse complex rules
diff --git a/eval.go b/eval.go
index 33fb00b..75009a8 100644
--- a/eval.go
+++ b/eval.go
@@ -174,7 +174,9 @@
 		lineno:    ast.lineno,
 		cmdLineno: ast.cmdLineno,
 	}
-	ev.curRule.parse(line)
+	if err := ev.curRule.parse(line); err != "" {
+		Error(ast.filename, ast.lineno, err)
+	}
 	// It seems rules with no outputs are siliently ignored.
 	if len(ev.curRule.outputs) == 0 {
 		ev.curRule = nil
diff --git a/parser_test.go b/parser_test.go
index 140ae1e..bdc94fb 100644
--- a/parser_test.go
+++ b/parser_test.go
@@ -47,8 +47,7 @@
 		}
 		if got, want := args, tc.args; !reflect.DeepEqual(got, want) {
 			t.Errorf(`parseExpr(%q)=%q, _, _; want %q, _, _`, tc.in, got, want)
-		}
-		if got, want := tc.in[rest:], tc.rest; got != want {
+		} else if got, want := tc.in[rest:], tc.rest; got != want {
 			t.Errorf(`parseExpr(%q)=_, %q, _; want _, %q, _`, tc.in, got, want)
 		}
 	}
diff --git a/rule_parser.go b/rule_parser.go
index ce74878..7ce218e 100644
--- a/rule_parser.go
+++ b/rule_parser.go
@@ -5,20 +5,71 @@
 )
 
 type Rule struct {
-	outputs   []string
-	inputs    []string
-	cmds      []string
-	filename  string
-	lineno    int
-	cmdLineno int
+	outputs        []string
+	inputs         []string
+	outputPatterns []string
+	isDoubleColon  bool
+	cmds           []string
+	filename       string
+	lineno         int
+	cmdLineno      int
 }
 
-func (r *Rule) parse(line string) {
-	colonIndex := strings.IndexByte(line, ':')
-	if colonIndex < 0 {
-		Error(r.filename, r.lineno, "*** missing separator.")
+func isPatternRule(s string) bool {
+	return strings.IndexByte(s, '%') >= 0
+}
+
+func (r *Rule) parse(line string) string {
+	index := strings.IndexByte(line, ':')
+	if index < 0 {
+		return "*** missing separator."
 	}
 
-	r.outputs = splitSpaces(line[:colonIndex])
-	r.inputs = splitSpaces(line[colonIndex+1:])
+	first := line[:index]
+	outputs := splitSpaces(first)
+	isFirstPattern := isPatternRule(first)
+	if isFirstPattern {
+		if len(outputs) > 1 {
+			return "*** mixed implicit and normal rules: deprecated syntax"
+		}
+		r.outputPatterns = outputs
+	} else {
+		r.outputs = outputs
+	}
+
+	index++
+	if index < len(line) && line[index] == ':' {
+		r.isDoubleColon = true
+		index++
+	}
+
+	rest := line[index:]
+	index = strings.IndexByte(rest, ':')
+	if index < 0 {
+		r.inputs = splitSpaces(rest)
+		return ""
+	}
+
+	// %.x: %.y: %.z
+	if isFirstPattern {
+		return "*** mixed implicit and normal rules: deprecated syntax"
+	}
+
+	second := rest[:index]
+	third := rest[index+1:]
+
+	r.outputs = outputs
+	r.outputPatterns = splitSpaces(second)
+	if len(r.outputPatterns) == 0 {
+		return "*** missing target pattern."
+	}
+	if len(r.outputPatterns) > 1 {
+		return "*** multiple target patterns."
+	}
+	if !isPatternRule(r.outputPatterns[0]) {
+		return "*** target pattern contains no '%'."
+	}
+	r.inputs = splitSpaces(third)
+
+	return ""
 }
diff --git a/rule_parser_test.go b/rule_parser_test.go
index 4c76c71..dbc166e 100644
--- a/rule_parser_test.go
+++ b/rule_parser_test.go
@@ -9,6 +9,7 @@
 	for _, tc := range []struct {
 		in   string
 		want Rule
+		err  string
 	} {
 		{
 			in:   "foo: bar",
@@ -24,11 +25,67 @@
 				inputs:  []string{"bar", "baz"},
 			},
 		},
+		{
+			in:   "foo:: bar",
+			want: Rule{
+				outputs: []string{"foo"},
+				inputs:  []string{"bar"},
+				isDoubleColon: true,
+			},
+		},
+		{
+			in:  "foo",
+			err: "*** missing separator.",
+		},
+		{
+			in:  "%.o: %.c",
+			want: Rule{
+				outputPatterns: []string{"%.o"},
+				inputs:         []string{"%.c"},
+			},
+		},
+		{
+			in:  "foo %.o: %.c",
+			err: "*** mixed implicit and normal rules: deprecated syntax",
+		},
+		{
+			in:  "foo.o: %.o: %.c %.h",
+			want: Rule{
+				outputs:        []string{"foo.o"},
+				outputPatterns: []string{"%.o"},
+				inputs:         []string{"%.c", "%.h"},
+			},
+		},
+		{
+			in:  "%.x: %.y: %.z",
+			err: "*** mixed implicit and normal rules: deprecated syntax",
+		},
+		{
+			in:  "foo.o: : %.c",
+			err: "*** missing target pattern.",
+		},
+		{
+			in:  "foo.o: %.o %.o: %.c",
+			err: "*** multiple target patterns.",
+		},
+		{
+			in:  "foo.o: foo.o: %.c",
+			err: "*** target pattern contains no '%'.",
+		},
+		/* TODO
+		{
+			in:  "foo.o: %.c: %.c",
+			err: "*** target 'foo.o' doesn't match the target pattern",
+		},
+		*/
 	} {
 		got := &Rule{}
-		got.parse(tc.in)
-		if !reflect.DeepEqual(tc.want, *got) {
-			t.Errorf(`r.parse(%q)=%q, want %q`, tc.in, tc.want, *got)
+		err := got.parse(tc.in)
+		if err != tc.err {
+			t.Errorf(`r.parse(%q)=%s, want %s`, tc.in, err, tc.err)
+		}
+		if err == "" && !reflect.DeepEqual(*got, tc.want) {
+			t.Errorf(`r.parse(%q); r=%q, want %q`, tc.in, *got, tc.want)
 		}
 	}
 }