diff --git a/format/format.go b/format/format.go index ea3d892..cd2f570 100755 --- a/format/format.go +++ b/format/format.go @@ -6,6 +6,8 @@ import ( "bytes" "fmt" "os" + "path/filepath" + "runtime" "strings" "github.com/therxwold/elu/ast" @@ -31,17 +33,61 @@ func Bytes(path string, src []byte) ([]byte, error) { return []byte(out), nil } -// Path formats the file at path in place. Overwrites the original. +// Path formats the file at path in place. Overwrites the original. It creates a temp file first, then renames it to the original file. func Path(path string) error { + success := false + // create a temp file + temp, err := os.CreateTemp(filepath.Dir(path), "elu-format-*") + if err != nil { + return err + } + // deleting the temp file in case of crash + defer func() { + if !success { + os.Remove(temp.Name()) + } + }() + // read the original file b, err := os.ReadFile(path) if err != nil { return err } - out, err := Bytes(path, b) + // write the formatted file + out, err := Bytes(temp.Name(), b) + if err != nil { + return err + } + _, err = temp.Write(out) if err != nil { return err } - return os.WriteFile(path, out, 0o644) + // close the temp file before renaming it to the original file + temp.Close() + // rename the temp file to the original file + err = os.Rename(temp.Name(), path) + if err != nil { + // Windows fallback; + // try to delete the original file and rename the temp file to it + if runtime.GOOS == "windows" { + // creating a bak file + bak := path + ".bak" + if err = os.Rename(path, bak); err != nil { + return err + } + // renaming the temp file to the original file + err = os.Rename(temp.Name(), path) + if err != nil { + // restoring the bak file + os.Rename(bak, path) + return err + } + os.Remove(bak) + return nil + } + return err + } + success = true + return nil } // File renders a parsed ELU AST back to canonical formatted text. @@ -50,10 +96,13 @@ func File(f *ast.File) string { var b bytes.Buffer fmt.Fprintf(&b, "pack %q version %d\n", f.PackID, f.Version) fmt.Fprintf(&b, "type = %q\n", f.Type) + // Render all nodes in the file. for _, n := range f.Nodes { b.WriteByte('\n') writeNode(&b, n, 0) } + // Add a newline if the file didn't end with one. + // This is important for tools that expect a newline at the end of files. if !bytes.HasSuffix(b.Bytes(), []byte("\n")) { b.WriteByte('\n') } @@ -65,18 +114,22 @@ func writeNode(b *bytes.Buffer, n *ast.Node, indent int) { pad := strings.Repeat(" ", indent) switch n.Kind { case ast.NodeBlock: + // Blocks are rendered as key: name\n{ children }. fmt.Fprintf(b, "%s%s %q:\n", pad, n.Key, n.Name) for _, c := range n.Children { writeNode(b, c, indent+2) } case ast.NodeSection: + // Sections are rendered as key:\n{ children }. fmt.Fprintf(b, "%s%s:\n", pad, sectionKey(n.Key)) for _, c := range n.Children { writeNode(b, c, indent+2) } case ast.NodeAssign: + // Assignments are rendered as key = value\n. fmt.Fprintf(b, "%s%s = %s\n", pad, n.Key, renderValue(n.Value)) case ast.NodeListItem: + // List items are rendered as either an expression, a value, or a section. writeListItem(b, n, indent) } } @@ -132,13 +185,16 @@ func isBareKey(s string) bool { if s == "" { return false } + // Check first rune separately, since it can't be a digit or dot. for i, r := range s { if i == 0 { + // Check if the rune is a valid first rune. if !(r == '_' || (r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z')) { return false } continue } + // Check if the rune is a valid identifier rune. if !(r == '_' || r == '.' || r == '-' || (r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9')) { return false }