|  | // Copyright 2020 The Wuffs Authors. | 
|  | // | 
|  | // Licensed under the Apache License, Version 2.0 (the "License"); | 
|  | // you may not use this file except in compliance with the License. | 
|  | // You may obtain a copy of the License at | 
|  | // | 
|  | //    https://www.apache.org/licenses/LICENSE-2.0 | 
|  | // | 
|  | // Unless required by applicable law or agreed to in writing, software | 
|  | // distributed under the License is distributed on an "AS IS" BASIS, | 
|  | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 
|  | // See the License for the specific language governing permissions and | 
|  | // limitations under the License. | 
|  |  | 
|  | // ---------------- | 
|  |  | 
|  | // dumbindent formats C (and C-like) programs. | 
|  | // | 
|  | // Without explicit paths, it rewrites the standard input to standard output. | 
|  | // Otherwise, the -l (list files that would change) or -w (write files in | 
|  | // place) or both flags must be given. Given a file path, it operates on that | 
|  | // file; given a directory path, it operates on all *.{c,h} files in that | 
|  | // directory, recursively. File paths starting with a period are ignored. | 
|  | // | 
|  | // Pass -spaces=N or -tabs to use N spaces or 1 tab per indent level. The | 
|  | // default is 2 spaces per indent level. | 
|  | // | 
|  | // It is similar in concept to pretty-printers like `indent` or `clang-format`. | 
|  | // It is much dumber (it will not add or remove line breaks or otherwise | 
|  | // re-flow lines of code just to fit within an 80 column limit) but it can | 
|  | // therefore be much faster at the basic task of automatically indenting nested | 
|  | // blocks. The output isn't 'perfect', but it's usually sufficiently readable | 
|  | // if the input already has sensible line breaks. | 
|  | // | 
|  | // To quantify "much faster", on this one C file, this program was 80 times | 
|  | // faster than `clang-format`, even without a column limit: | 
|  | // | 
|  | //     $ wc release/c/wuffs-v0.2.c | 
|  | //      11858  35980 431885 release/c/wuffs-v0.2.c | 
|  | //     $ time dumbindent                               < release/c/wuffs-v0.2.c > /dev/null | 
|  | //     real    0m0.008s | 
|  | //     user    0m0.005s | 
|  | //     sys     0m0.005s | 
|  | //     $ time clang-format-9                           < release/c/wuffs-v0.2.c > /dev/null | 
|  | //     real    0m0.668s | 
|  | //     user    0m0.618s | 
|  | //     sys     0m0.032s | 
|  | //     $ time clang-format-9 -style='{ColumnLimit: 0}' < release/c/wuffs-v0.2.c > /dev/null | 
|  | //     real    0m0.641s | 
|  | //     user    0m0.585s | 
|  | //     sys     0m0.037s | 
|  | // | 
|  | // More commentary is at: | 
|  | // https://godoc.org/github.com/google/wuffs/lib/dumbindent | 
|  | // https://nigeltao.github.io/blog/2020/dumbindent.html | 
|  | package main | 
|  |  | 
|  | import ( | 
|  | "bytes" | 
|  | "errors" | 
|  | "flag" | 
|  | "fmt" | 
|  | "io" | 
|  | "io/ioutil" | 
|  | "os" | 
|  | "path/filepath" | 
|  | "runtime" | 
|  | "strings" | 
|  |  | 
|  | "github.com/google/wuffs/lib/dumbindent" | 
|  | ) | 
|  |  | 
|  | var ( | 
|  | lFlag = flag.Bool("l", false, "list files whose formatting differs from dumbindent's") | 
|  | wFlag = flag.Bool("w", false, "write result to (source) file instead of stdout") | 
|  |  | 
|  | spacesFlag = flag.Int("spaces", 2, "the number of spaces per indent") | 
|  | tabsFlag   = flag.Bool("tabs", false, "indent with a tab instead of spaces") | 
|  | ) | 
|  |  | 
|  | func usage() { | 
|  | fmt.Fprintf(os.Stderr, "usage: dumbindent [flags] [path ...]\n") | 
|  | flag.PrintDefaults() | 
|  | } | 
|  |  | 
|  | func main() { | 
|  | if err := main1(); err != nil { | 
|  | os.Stderr.WriteString(err.Error() + "\n") | 
|  | os.Exit(1) | 
|  | } | 
|  | } | 
|  |  | 
|  | func main1() error { | 
|  | flag.Usage = usage | 
|  | flag.Parse() | 
|  |  | 
|  | if flag.NArg() == 0 { | 
|  | if *lFlag { | 
|  | return errors.New("cannot use -l with standard input") | 
|  | } | 
|  | if *wFlag { | 
|  | return errors.New("cannot use -w with standard input") | 
|  | } | 
|  | return do(os.Stdin, "<standard input>") | 
|  | } | 
|  |  | 
|  | if !*lFlag && !*wFlag { | 
|  | return errors.New("must use -l or -w if paths are given") | 
|  | } | 
|  |  | 
|  | for i := 0; i < flag.NArg(); i++ { | 
|  | arg := flag.Arg(i) | 
|  | switch dir, err := os.Stat(arg); { | 
|  | case err != nil: | 
|  | return err | 
|  | case dir.IsDir(): | 
|  | return filepath.Walk(arg, walk) | 
|  | default: | 
|  | if err := do(nil, arg); err != nil { | 
|  | return err | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | return nil | 
|  | } | 
|  |  | 
|  | func isCHFile(info os.FileInfo) bool { | 
|  | name := info.Name() | 
|  | return !info.IsDir() && !strings.HasPrefix(name, ".") && | 
|  | (strings.HasSuffix(name, ".c") || strings.HasSuffix(name, ".h")) | 
|  | } | 
|  |  | 
|  | func walk(filename string, info os.FileInfo, err error) error { | 
|  | if (err == nil) && isCHFile(info) { | 
|  | err = do(nil, filename) | 
|  | } | 
|  | // Don't complain if a file was deleted in the meantime (i.e. the directory | 
|  | // changed concurrently while running this program). | 
|  | if (err != nil) && !os.IsNotExist(err) { | 
|  | return err | 
|  | } | 
|  | return nil | 
|  | } | 
|  |  | 
|  | func do(r io.Reader, filename string) error { | 
|  | src, err := []byte(nil), error(nil) | 
|  | if r != nil { | 
|  | src, err = ioutil.ReadAll(r) | 
|  | } else { | 
|  | src, err = ioutil.ReadFile(filename) | 
|  | } | 
|  | if err != nil { | 
|  | return err | 
|  | } | 
|  |  | 
|  | dst := dumbindent.FormatBytes(nil, src, &dumbindent.Options{ | 
|  | Spaces: *spacesFlag, | 
|  | Tabs:   *tabsFlag, | 
|  | }) | 
|  |  | 
|  | if r != nil { | 
|  | if _, err := os.Stdout.Write(dst); err != nil { | 
|  | return err | 
|  | } | 
|  | } else if !bytes.Equal(dst, src) { | 
|  | if *lFlag { | 
|  | fmt.Println(filename) | 
|  | } | 
|  | if *wFlag { | 
|  | if err := writeFile(filename, dst); err != nil { | 
|  | return err | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | return nil | 
|  | } | 
|  |  | 
|  | const chmodSupported = runtime.GOOS != "windows" | 
|  |  | 
|  | func writeFile(filename string, b []byte) error { | 
|  | f, err := ioutil.TempFile(filepath.Dir(filename), filepath.Base(filename)) | 
|  | if err != nil { | 
|  | return err | 
|  | } | 
|  | if chmodSupported { | 
|  | if info, err := os.Stat(filename); err == nil { | 
|  | f.Chmod(info.Mode().Perm()) | 
|  | } | 
|  | } | 
|  | _, werr := f.Write(b) | 
|  | cerr := f.Close() | 
|  | if werr != nil { | 
|  | os.Remove(f.Name()) | 
|  | return werr | 
|  | } | 
|  | if cerr != nil { | 
|  | os.Remove(f.Name()) | 
|  | return cerr | 
|  | } | 
|  | return os.Rename(f.Name(), filename) | 
|  | } |