Sketching out the parser.
This commit is contained in:
parent
9ba796161d
commit
d129ff285c
5 changed files with 258 additions and 18 deletions
55
README.md
Normal file
55
README.md
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
|
||||||
|
# Mk
|
||||||
|
|
||||||
|
Mk is a reboot of the Plan 9 mk command, which itself is a replacement for make.
|
||||||
|
This tool is for anyone who loves make, but hates all its stupid bullshit.
|
||||||
|
|
||||||
|
# Why Plan 9 mk is better than make
|
||||||
|
|
||||||
|
Plan 9 mk blows make out of the water. Yet tragically, few use or have even heard
|
||||||
|
of it. Put simply, mk takes make, keeps its simple direct syntax, but fixes
|
||||||
|
basically everything that's annoyed you over the years. To name a few things:
|
||||||
|
|
||||||
|
1. Recipes are delimited by any indentation, not tab characters in particular.
|
||||||
|
2. Phony targets are handled separately from file targets. Your mkfile won't
|
||||||
|
be broken by having a file named 'clean'.
|
||||||
|
2. Attributes instead of weird special targets like `.SECONDARY:`.
|
||||||
|
5. Special variables like `$target`, `$prereq`, and `$stem` in place of
|
||||||
|
make's pointlessly cryptic `$@`, `$^`, and `$*`.
|
||||||
|
3. In addition to suffix rules (e.g. `%.o: %.c`), mk has more powerful regular
|
||||||
|
expression rules.
|
||||||
|
4. Sane handling of rules with multiple targets.
|
||||||
|
5. An optional attribute to delete targets when a recipe fails, so you aren't
|
||||||
|
left with corrupt output.
|
||||||
|
6. Plan 9 mkfiles can not only include other mkfiles, but pipe in the output of
|
||||||
|
recipes. Your mkfile can configure itself by doing something like
|
||||||
|
`<|sh config.sh`.
|
||||||
|
7. A generalized mechanism to determine if a target is out of date, for when
|
||||||
|
timestamps won't cut it.
|
||||||
|
|
||||||
|
And much more! For more, read the original mk paper: ["Mk: a successor to
|
||||||
|
make"](#).
|
||||||
|
|
||||||
|
# Improvements over Plan 9 mk
|
||||||
|
|
||||||
|
This mk stays mostly faithful to Plan 9, but makes a few minor (in my opinion)
|
||||||
|
improvements.
|
||||||
|
|
||||||
|
1. Allow blank lines in recipes. A recipe is any indented block of text, and
|
||||||
|
continues until a non-indented character or the end of the file.
|
||||||
|
2. Add an 'S' attribute to execute recipes with programs other than sh. This
|
||||||
|
way, you don't have to separate your six line python script into its own
|
||||||
|
file. Just stick it in the mkfile.
|
||||||
|
3. Use a perl-compatible regular expressions. The original mk used plan9
|
||||||
|
regex, which few people know or care to learn.
|
||||||
|
4. A clean, modern implementation in go, that doesn't depend on the whole plan
|
||||||
|
9 for userspace stack.
|
||||||
|
|
||||||
|
Most Plan 9 mkfiles should remain backwards compatible, but strict backwards
|
||||||
|
compatibility isn't the goal.
|
||||||
|
|
||||||
|
# Current State
|
||||||
|
|
||||||
|
Totally non-functional. Check back later!
|
||||||
|
|
||||||
|
|
||||||
29
lex.go
29
lex.go
|
|
@ -1,4 +1,6 @@
|
||||||
|
|
||||||
|
// TODO: Backquoted strings.
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|
@ -15,7 +17,8 @@ const (
|
||||||
tokenError tokenType = iota
|
tokenError tokenType = iota
|
||||||
tokenBareString
|
tokenBareString
|
||||||
tokenQuotedString
|
tokenQuotedString
|
||||||
tokenInclude
|
tokenPipeInclude
|
||||||
|
tokenRedirInclude
|
||||||
tokenColon
|
tokenColon
|
||||||
tokenAssign
|
tokenAssign
|
||||||
tokenRecipe
|
tokenRecipe
|
||||||
|
|
@ -27,7 +30,8 @@ func (typ tokenType) String() string {
|
||||||
case tokenError: return "[Error]"
|
case tokenError: return "[Error]"
|
||||||
case tokenBareString: return "[BareString]"
|
case tokenBareString: return "[BareString]"
|
||||||
case tokenQuotedString: return "[QuotedString]"
|
case tokenQuotedString: return "[QuotedString]"
|
||||||
case tokenInclude: return "[Include]"
|
case tokenPipeInclude: return "[PipeInclude]"
|
||||||
|
case tokenRedirInclude: return "[RedirInclude]"
|
||||||
case tokenColon: return "[Colon]"
|
case tokenColon: return "[Colon]"
|
||||||
case tokenAssign: return "[Assign]"
|
case tokenAssign: return "[Assign]"
|
||||||
case tokenRecipe: return "[Recipe]"
|
case tokenRecipe: return "[Recipe]"
|
||||||
|
|
@ -144,6 +148,17 @@ func (l *lexer) accept(valid string) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Skip the next rune if it is in the valid string. Return true if it was
|
||||||
|
// skipped.
|
||||||
|
func (l *lexer) ignore(valid string) bool {
|
||||||
|
if strings.IndexRune(valid, l.peek()) >= 0 {
|
||||||
|
l.skip()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Consume characters from the valid string until the next is not.
|
// Consume characters from the valid string until the next is not.
|
||||||
func (l *lexer) acceptRun(valid string) int {
|
func (l *lexer) acceptRun(valid string) int {
|
||||||
prevpos := l.pos
|
prevpos := l.pos
|
||||||
|
|
@ -256,9 +271,15 @@ func lexComment (l* lexer) lexerStateFun {
|
||||||
|
|
||||||
func lexInclude (l* lexer) lexerStateFun {
|
func lexInclude (l* lexer) lexerStateFun {
|
||||||
l.skip() // '<'
|
l.skip() // '<'
|
||||||
|
var typ tokenType
|
||||||
|
if l.ignore("|") {
|
||||||
|
typ = tokenPipeInclude
|
||||||
|
} else {
|
||||||
|
typ = tokenRedirInclude
|
||||||
|
}
|
||||||
|
|
||||||
l.skipRun(" \t\n\r")
|
l.skipRun(" \t\n\r")
|
||||||
l.acceptUntil("\n\r")
|
l.emit(typ)
|
||||||
l.emit(tokenInclude)
|
|
||||||
return lexTopLevel
|
return lexTopLevel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
14
mk.go
14
mk.go
|
|
@ -2,23 +2,9 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"io/ioutil"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
input, _ := ioutil.ReadAll(os.Stdin)
|
|
||||||
l, tokens := lex(string(input))
|
|
||||||
|
|
||||||
for t := range tokens {
|
|
||||||
if t.typ == tokenError {
|
|
||||||
fmt.Printf("Error: %s", l.errmsg)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println(t.String())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
121
parse.go
Normal file
121
parse.go
Normal file
|
|
@ -0,0 +1,121 @@
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
)
|
||||||
|
|
||||||
|
/* Grammar, to the best of my knowledge:
|
||||||
|
|
||||||
|
Should we deviate at all from mk?
|
||||||
|
|
||||||
|
Yes! I want to simplify things by saying recipes have nonzero indentation and
|
||||||
|
everything else has zero.
|
||||||
|
|
||||||
|
rule ::= targets ':' attributes ':' prereqs NEWLINE RECIPE |
|
||||||
|
targets ':' prereqs NEWLINE RECIPE
|
||||||
|
|
||||||
|
targets ::= string | string "," targets
|
||||||
|
|
||||||
|
attributes ::= SCALAR | SCALAR attributes
|
||||||
|
|
||||||
|
prereqs ::= string | string "," prereqs
|
||||||
|
|
||||||
|
include ::= '<' string NEWLINE
|
||||||
|
|
||||||
|
string ::= SCALAR | QSTRING
|
||||||
|
|
||||||
|
assignment ::= SCALAR '=' string
|
||||||
|
|
||||||
|
How do we handle escaping new lines?
|
||||||
|
Is newline a token that's emitted?
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
// The parser for mk files is terribly simple. There are only three sorts of
|
||||||
|
// statements in mkfiles: variable assignments, rules (possibly with
|
||||||
|
// accompanying recipes), and includes.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//
|
||||||
|
// Maybe this is the wrong way to organize things.
|
||||||
|
// We should perhaps have a type for a parsed mkfile that includes every
|
||||||
|
// assignment as well as every rule.
|
||||||
|
//
|
||||||
|
// Rule order should not matter.
|
||||||
|
//
|
||||||
|
// Includes are tricky. If they were straight up includes, the could be
|
||||||
|
// evaluated in place, but they could contain shell script, etc.
|
||||||
|
//
|
||||||
|
// No...we still have to evaluate them in place. That means figuring out how to
|
||||||
|
// spawn shells from go.
|
||||||
|
//
|
||||||
|
|
||||||
|
|
||||||
|
type parser struct {
|
||||||
|
l *lexer // underlying lexer
|
||||||
|
tokenbuf []token // tokens consumed on the current statement
|
||||||
|
rules *ruleSet // current ruleSet
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// A parser state function takes a parser and the next token and returns a new
|
||||||
|
// state function, or nil if there was a parse error.
|
||||||
|
type parserStateFun func (*parser, token) parserStateFun
|
||||||
|
|
||||||
|
|
||||||
|
// Parse a mkfile, returning a new ruleSet.
|
||||||
|
func parse(input string) *ruleSet {
|
||||||
|
rules := &ruleSet{}
|
||||||
|
parseInto(input, rules)
|
||||||
|
return rules
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Parse a mkfile inserting rules and variables into a given ruleSet.
|
||||||
|
func parseInto(input string, rules *ruleSet) {
|
||||||
|
l, tokens := lex(input)
|
||||||
|
p := &parser{l, []token{}, rules}
|
||||||
|
state := parseTopLevel
|
||||||
|
for t := range tokens {
|
||||||
|
if t.typ == tokenError {
|
||||||
|
// TODO: fancier error messages
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: %s", l.errmsg)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
state = state(p, t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Handle the case when state is not top level.
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func parseTopLevel(p *parser, t token) parserStateFun {
|
||||||
|
switch t.typ {
|
||||||
|
case tokenPipeInclude: return parsePipeInclude(p, t)
|
||||||
|
// TODO: all others
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseTopLevel
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func parsePipeInclude(p *parser, t token) parserStateFun {
|
||||||
|
// TODO: We need to split this up into arguments so we can feed it into
|
||||||
|
// executeRecipe.
|
||||||
|
return parseTopLevel
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func parseRedirInclude(p *parser, t token) parserStateFun {
|
||||||
|
// TODO: Open the file, read its context, call parseInto recursively.
|
||||||
|
return parseTopLevel
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
57
recipe.go
Normal file
57
recipe.go
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os/exec"
|
||||||
|
"os"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
// A monolithic function for executing recipes.
|
||||||
|
func executeRecipe(program string,
|
||||||
|
args []string,
|
||||||
|
input string,
|
||||||
|
echo_out bool,
|
||||||
|
echo_err bool,
|
||||||
|
capture_out bool) string {
|
||||||
|
cmd := exec.Command(program, args...)
|
||||||
|
|
||||||
|
if echo_out {
|
||||||
|
cmdout, err := cmd.StdoutPipe()
|
||||||
|
if err != nil {
|
||||||
|
go io.Copy(os.Stdout, cmdout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if echo_err {
|
||||||
|
cmderr, err := cmd.StdoutPipe()
|
||||||
|
if err != nil {
|
||||||
|
go io.Copy(os.Stderr, cmderr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(input) > 0 {
|
||||||
|
cmdin, err := cmd.StdinPipe()
|
||||||
|
go func () { cmdin.Write([]byte(input)) }()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
output := ""
|
||||||
|
var err error
|
||||||
|
if capture_out {
|
||||||
|
output, err = cmd.Output()
|
||||||
|
} else {
|
||||||
|
err = cmd.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
// TODO: better error output
|
||||||
|
log.Fatal("Recipe failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue