blob: ee3bf256a6e70f12d40f7d54e69c9dd7a19d44b7 [file] [log] [blame]
// 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)
}