github.com/teddydd/sh@v2.6.4+incompatible/cmd/shfmt/main.go (about)

     1  // Copyright (c) 2016, Daniel Martí <mvdan@mvdan.cc>
     2  // See LICENSE for licensing information
     3  
     4  package main
     5  
     6  import (
     7  	"bytes"
     8  	"flag"
     9  	"fmt"
    10  	"io"
    11  	"io/ioutil"
    12  	"os"
    13  	"os/exec"
    14  	"path/filepath"
    15  	"regexp"
    16  
    17  	"mvdan.cc/sh/fileutil"
    18  	"mvdan.cc/sh/syntax"
    19  )
    20  
    21  var (
    22  	showVersion = flag.Bool("version", false, "")
    23  
    24  	list   = flag.Bool("l", false, "")
    25  	write  = flag.Bool("w", false, "")
    26  	simple = flag.Bool("s", false, "")
    27  	find   = flag.Bool("f", false, "")
    28  	diff   = flag.Bool("d", false, "")
    29  
    30  	langStr = flag.String("ln", "", "")
    31  	posix   = flag.Bool("p", false, "")
    32  
    33  	indent      = flag.Uint("i", 0, "")
    34  	binNext     = flag.Bool("bn", false, "")
    35  	caseIndent  = flag.Bool("ci", false, "")
    36  	spaceRedirs = flag.Bool("sr", false, "")
    37  	keepPadding = flag.Bool("kp", false, "")
    38  	minify      = flag.Bool("mn", false, "")
    39  
    40  	toJSON = flag.Bool("tojson", false, "")
    41  
    42  	parser            *syntax.Parser
    43  	printer           *syntax.Printer
    44  	readBuf, writeBuf bytes.Buffer
    45  
    46  	copyBuf = make([]byte, 32*1024)
    47  
    48  	in  io.Reader = os.Stdin
    49  	out io.Writer = os.Stdout
    50  
    51  	version = "v2.6.4"
    52  )
    53  
    54  func main() {
    55  	flag.Usage = func() {
    56  		fmt.Fprint(os.Stderr, `usage: shfmt [flags] [path ...]
    57  
    58  If no arguments are given, standard input will be used. If a given path
    59  is a directory, it will be recursively searched for shell files - both
    60  by filename extension and by shebang.
    61  
    62    -version  show version and exit
    63  
    64    -l        list files whose formatting differs from shfmt's
    65    -w        write result to file instead of stdout
    66    -d        error with a diff when the formatting differs
    67    -s        simplify the code
    68  
    69  Parser options:
    70  
    71    -ln str   language variant to parse (bash/posix/mksh, default "bash")
    72    -p        shorthand for -ln=posix
    73  
    74  Printer options:
    75  
    76    -i uint   indent: 0 for tabs (default), >0 for number of spaces
    77    -bn       binary ops like && and | may start a line
    78    -ci       switch cases will be indented
    79    -sr       redirect operators will be followed by a space
    80    -kp       keep column alignment paddings
    81    -mn       minify program to reduce its size (implies -s)
    82  
    83  Utilities:
    84  
    85    -f        recursively find all shell files and print the paths
    86    -tojson   print syntax tree to stdout as a typed JSON
    87  `)
    88  	}
    89  	flag.Parse()
    90  
    91  	if *showVersion {
    92  		fmt.Println(version)
    93  		return
    94  	}
    95  	if *posix && *langStr != "" {
    96  		fmt.Fprintf(os.Stderr, "-p and -ln=lang cannot coexist\n")
    97  		os.Exit(1)
    98  	}
    99  	lang := syntax.LangBash
   100  	switch *langStr {
   101  	case "bash", "":
   102  	case "posix":
   103  		lang = syntax.LangPOSIX
   104  	case "mksh":
   105  		lang = syntax.LangMirBSDKorn
   106  	default:
   107  		fmt.Fprintf(os.Stderr, "unknown shell language: %s\n", *langStr)
   108  		os.Exit(1)
   109  	}
   110  	if *posix {
   111  		lang = syntax.LangPOSIX
   112  	}
   113  	if *minify {
   114  		*simple = true
   115  	}
   116  	parser = syntax.NewParser(syntax.KeepComments, syntax.Variant(lang))
   117  	printer = syntax.NewPrinter(func(p *syntax.Printer) {
   118  		syntax.Indent(*indent)(p)
   119  		if *binNext {
   120  			syntax.BinaryNextLine(p)
   121  		}
   122  		if *caseIndent {
   123  			syntax.SwitchCaseIndent(p)
   124  		}
   125  		if *spaceRedirs {
   126  			syntax.SpaceRedirects(p)
   127  		}
   128  		if *keepPadding {
   129  			syntax.KeepPadding(p)
   130  		}
   131  		if *minify {
   132  			syntax.Minify(p)
   133  		}
   134  	})
   135  	if flag.NArg() == 0 {
   136  		if err := formatStdin(); err != nil {
   137  			if err != errChangedWithDiff {
   138  				fmt.Fprintln(os.Stderr, err)
   139  			}
   140  			os.Exit(1)
   141  		}
   142  		return
   143  	}
   144  	if *toJSON {
   145  		fmt.Fprintln(os.Stderr, "-tojson can only be used with stdin/out")
   146  		os.Exit(1)
   147  	}
   148  	anyErr := false
   149  	for _, path := range flag.Args() {
   150  		walk(path, func(err error) {
   151  			if err != errChangedWithDiff {
   152  				fmt.Fprintln(os.Stderr, err)
   153  			}
   154  			anyErr = true
   155  		})
   156  	}
   157  	if anyErr {
   158  		os.Exit(1)
   159  	}
   160  }
   161  
   162  var errChangedWithDiff = fmt.Errorf("")
   163  
   164  func formatStdin() error {
   165  	if *write {
   166  		return fmt.Errorf("-w cannot be used on standard input")
   167  	}
   168  	src, err := ioutil.ReadAll(in)
   169  	if err != nil {
   170  		return err
   171  	}
   172  	return formatBytes(src, "<standard input>")
   173  }
   174  
   175  var vcsDir = regexp.MustCompile(`^\.(git|svn|hg)$`)
   176  
   177  func walk(path string, onError func(error)) {
   178  	info, err := os.Stat(path)
   179  	if err != nil {
   180  		onError(err)
   181  		return
   182  	}
   183  	if !info.IsDir() {
   184  		if err := formatPath(path, false); err != nil {
   185  			onError(err)
   186  		}
   187  		return
   188  	}
   189  	filepath.Walk(path, func(path string, info os.FileInfo, err error) error {
   190  		if info.IsDir() && vcsDir.MatchString(info.Name()) {
   191  			return filepath.SkipDir
   192  		}
   193  		if err != nil {
   194  			onError(err)
   195  			return nil
   196  		}
   197  		conf := fileutil.CouldBeScript(info)
   198  		if conf == fileutil.ConfNotScript {
   199  			return nil
   200  		}
   201  		err = formatPath(path, conf == fileutil.ConfIfShebang)
   202  		if err != nil && !os.IsNotExist(err) {
   203  			onError(err)
   204  		}
   205  		return nil
   206  	})
   207  }
   208  
   209  func formatPath(path string, checkShebang bool) error {
   210  	f, err := os.Open(path)
   211  	if err != nil {
   212  		return err
   213  	}
   214  	defer f.Close()
   215  	readBuf.Reset()
   216  	if checkShebang {
   217  		n, err := f.Read(copyBuf[:32])
   218  		if err != nil {
   219  			return err
   220  		}
   221  		if !fileutil.HasShebang(copyBuf[:n]) {
   222  			return nil
   223  		}
   224  		readBuf.Write(copyBuf[:n])
   225  	}
   226  	if *find {
   227  		fmt.Fprintln(out, path)
   228  		return nil
   229  	}
   230  	if _, err := io.CopyBuffer(&readBuf, f, copyBuf); err != nil {
   231  		return err
   232  	}
   233  	f.Close()
   234  	return formatBytes(readBuf.Bytes(), path)
   235  }
   236  
   237  func formatBytes(src []byte, path string) error {
   238  	prog, err := parser.Parse(bytes.NewReader(src), path)
   239  	if err != nil {
   240  		return err
   241  	}
   242  	if *simple {
   243  		syntax.Simplify(prog)
   244  	}
   245  	if *toJSON {
   246  		// must be standard input; fine to return
   247  		return writeJSON(out, prog, true)
   248  	}
   249  	writeBuf.Reset()
   250  	printer.Print(&writeBuf, prog)
   251  	res := writeBuf.Bytes()
   252  	if !bytes.Equal(src, res) {
   253  		if *list {
   254  			if _, err := fmt.Fprintln(out, path); err != nil {
   255  				return err
   256  			}
   257  		}
   258  		if *write {
   259  			f, err := os.OpenFile(path, os.O_WRONLY|os.O_TRUNC, 0)
   260  			if err != nil {
   261  				return err
   262  			}
   263  			if _, err := f.Write(res); err != nil {
   264  				return err
   265  			}
   266  			if err := f.Close(); err != nil {
   267  				return err
   268  			}
   269  		}
   270  		if *diff {
   271  			data, err := diffBytes(src, res, path)
   272  			if err != nil {
   273  				return fmt.Errorf("computing diff: %s", err)
   274  			}
   275  			out.Write(data)
   276  			return errChangedWithDiff
   277  		}
   278  	}
   279  	if !*list && !*write && !*diff {
   280  		if _, err := out.Write(res); err != nil {
   281  			return err
   282  		}
   283  	}
   284  	return nil
   285  }
   286  
   287  func writeTempFile(dir, prefix string, data []byte) (string, error) {
   288  	file, err := ioutil.TempFile(dir, prefix)
   289  	if err != nil {
   290  		return "", err
   291  	}
   292  	_, err = file.Write(data)
   293  	if err1 := file.Close(); err == nil {
   294  		err = err1
   295  	}
   296  	if err != nil {
   297  		os.Remove(file.Name())
   298  		return "", err
   299  	}
   300  	return file.Name(), nil
   301  }
   302  
   303  func diffBytes(b1, b2 []byte, path string) ([]byte, error) {
   304  	fmt.Fprintf(out, "diff -u %s %s\n",
   305  		filepath.ToSlash(path+".orig"),
   306  		filepath.ToSlash(path))
   307  	f1, err := writeTempFile("", "shfmt", b1)
   308  	if err != nil {
   309  		return nil, err
   310  	}
   311  	defer os.Remove(f1)
   312  
   313  	f2, err := writeTempFile("", "shfmt", b2)
   314  	if err != nil {
   315  		return nil, err
   316  	}
   317  	defer os.Remove(f2)
   318  
   319  	data, err := exec.Command("diff", "-u", f1, f2).Output()
   320  	if len(data) == 0 {
   321  		// No diff, or something went wrong; don't check for err
   322  		// as diff will return non-zero if the files differ.
   323  		return nil, err
   324  	}
   325  	// We already print the filename, so remove the
   326  	// temporary filenames printed by diff.
   327  	lines := bytes.Split(data, []byte("\n"))
   328  	for i, line := range lines {
   329  		switch {
   330  		case bytes.HasPrefix(line, []byte("---")):
   331  		case bytes.HasPrefix(line, []byte("+++")):
   332  		default:
   333  			return bytes.Join(lines[i:], []byte("\n")), nil
   334  		}
   335  	}
   336  	return data, nil
   337  }