| // 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) |
| } |