github.com/benhoyt/goawk@v1.8.1/goawk.go (about)

     1  // Package goawk is an implementation of AWK written in Go.
     2  //
     3  // You can use the command-line "goawk" command or run AWK from your
     4  // Go programs using the "interp" package. The command-line program
     5  // has the same interface as regular awk:
     6  //
     7  //     goawk [-F fs] [-v var=value] [-f progfile | 'prog'] [file ...]
     8  //
     9  // The -F flag specifies the field separator (the default is to split
    10  // on whitespace). The -v flag allows you to set a variable to a
    11  // given value (multiple -v flags allowed). The -f flag allows you to
    12  // read AWK source from a file instead of the 'prog' command-line
    13  // argument. The rest of the arguments are input filenames (default
    14  // is to read from stdin).
    15  //
    16  // A simple example (prints the sum of the numbers in the file's
    17  // second column):
    18  //
    19  //     $ echo 'foo 12
    20  //     > bar 34
    21  //     > baz 56' >file.txt
    22  //     $ goawk '{ sum += $2 } END { print sum }' file.txt
    23  //     102
    24  //
    25  // To use GoAWK in your Go programs, see README.md or the "interp"
    26  // docs.
    27  //
    28  package main
    29  
    30  import (
    31  	"bytes"
    32  	"fmt"
    33  	"os"
    34  	"path/filepath"
    35  	"runtime"
    36  	"runtime/pprof"
    37  	"strings"
    38  	"unicode/utf8"
    39  
    40  	"github.com/benhoyt/goawk/interp"
    41  	"github.com/benhoyt/goawk/lexer"
    42  	"github.com/benhoyt/goawk/parser"
    43  )
    44  
    45  const (
    46  	version    = "v1.8.1"
    47  	copyright  = "GoAWK " + version + " - Copyright (c) 2019 Ben Hoyt"
    48  	shortUsage = "usage: goawk [-F fs] [-v var=value] [-f progfile | 'prog'] [file ...]"
    49  	longUsage  = `Standard AWK arguments:
    50    -F separator
    51          field separator (default " ")
    52    -v assignment
    53          name=value variable assignment (multiple allowed)
    54    -f progfile
    55          load AWK source from progfile (multiple allowed)
    56  
    57  Additional GoAWK arguments:
    58    -cpuprofile file
    59          write CPU profile to file
    60    -d    debug mode (print parsed AST to stderr)
    61    -dt   show variable types debug info
    62    -h    show this usage message
    63    -version
    64          show GoAWK version and exit
    65  `
    66  )
    67  
    68  func main() {
    69  	// Parse command line arguments manually rather than using the
    70  	// "flag" package so we can support flags with no space between
    71  	// flag and argument, like '-F:' (allowed by POSIX)
    72  	var progFiles []string
    73  	var vars []string
    74  	fieldSep := " "
    75  	cpuprofile := ""
    76  	debug := false
    77  	debugTypes := false
    78  	memprofile := ""
    79  
    80  	var i int
    81  	for i = 1; i < len(os.Args); i++ {
    82  		// Stop on explicit end of args or first arg not prefixed with "-"
    83  		arg := os.Args[i]
    84  		if arg == "--" {
    85  			i++
    86  			break
    87  		}
    88  		if !strings.HasPrefix(arg, "-") {
    89  			break
    90  		}
    91  
    92  		switch arg {
    93  		case "-F":
    94  			if i+1 >= len(os.Args) {
    95  				errorExitf("flag needs an argument: -F")
    96  			}
    97  			i++
    98  			fieldSep = os.Args[i]
    99  		case "-f":
   100  			if i+1 >= len(os.Args) {
   101  				errorExitf("flag needs an argument: -f")
   102  			}
   103  			i++
   104  			progFiles = append(progFiles, os.Args[i])
   105  		case "-v":
   106  			if i+1 >= len(os.Args) {
   107  				errorExitf("flag needs an argument: -v")
   108  			}
   109  			i++
   110  			vars = append(vars, os.Args[i])
   111  		case "-cpuprofile":
   112  			if i+1 >= len(os.Args) {
   113  				errorExitf("flag needs an argument: -cpuprofile")
   114  			}
   115  			i++
   116  			cpuprofile = os.Args[i]
   117  		case "-d":
   118  			debug = true
   119  		case "-dt":
   120  			debugTypes = true
   121  		case "-h", "--help":
   122  			fmt.Printf("%s\n\n%s\n\n%s", copyright, shortUsage, longUsage)
   123  			os.Exit(0)
   124  		case "-memprofile":
   125  			if i+1 >= len(os.Args) {
   126  				errorExitf("flag needs an argument: -memprofile")
   127  			}
   128  			i++
   129  			memprofile = os.Args[i]
   130  		case "-version", "--version":
   131  			fmt.Println(version)
   132  			os.Exit(0)
   133  		default:
   134  			switch {
   135  			case strings.HasPrefix(arg, "-F"):
   136  				fieldSep = arg[2:]
   137  			case strings.HasPrefix(arg, "-f"):
   138  				progFiles = append(progFiles, arg[2:])
   139  			case strings.HasPrefix(arg, "-v"):
   140  				vars = append(vars, arg[2:])
   141  			case strings.HasPrefix(arg, "-cpuprofile="):
   142  				cpuprofile = arg[12:]
   143  			case strings.HasPrefix(arg, "-memprofile="):
   144  				memprofile = arg[12:]
   145  			default:
   146  				errorExitf("flag provided but not defined: %s", arg)
   147  			}
   148  		}
   149  	}
   150  
   151  	// Any remaining args are program and input files
   152  	args := os.Args[i:]
   153  
   154  	var src []byte
   155  	if len(progFiles) > 0 {
   156  		// Read source: the concatenation of all source files specified
   157  		buf := &bytes.Buffer{}
   158  		progFiles = expandWildcardsOnWindows(progFiles)
   159  		for _, progFile := range progFiles {
   160  			if progFile == "-" {
   161  				_, err := buf.ReadFrom(os.Stdin)
   162  				if err != nil {
   163  					errorExit(err)
   164  				}
   165  			} else {
   166  				f, err := os.Open(progFile)
   167  				if err != nil {
   168  					errorExit(err)
   169  				}
   170  				_, err = buf.ReadFrom(f)
   171  				if err != nil {
   172  					_ = f.Close()
   173  					errorExit(err)
   174  				}
   175  				_ = f.Close()
   176  			}
   177  			// Append newline to file in case it doesn't end with one
   178  			_ = buf.WriteByte('\n')
   179  		}
   180  		src = buf.Bytes()
   181  	} else {
   182  		if len(args) < 1 {
   183  			errorExitf(shortUsage)
   184  		}
   185  		src = []byte(args[0])
   186  		args = args[1:]
   187  	}
   188  
   189  	// Parse source code and setup interpreter
   190  	parserConfig := &parser.ParserConfig{
   191  		DebugTypes:  debugTypes,
   192  		DebugWriter: os.Stderr,
   193  	}
   194  	prog, err := parser.ParseProgram(src, parserConfig)
   195  	if err != nil {
   196  		errMsg := fmt.Sprintf("%s", err)
   197  		if err, ok := err.(*parser.ParseError); ok {
   198  			showSourceLine(src, err.Position, len(errMsg))
   199  		}
   200  		errorExitf("%s", errMsg)
   201  	}
   202  	if debug {
   203  		fmt.Fprintln(os.Stderr, prog)
   204  	}
   205  	config := &interp.Config{
   206  		Argv0: filepath.Base(os.Args[0]),
   207  		Args:  expandWildcardsOnWindows(args),
   208  		Vars:  []string{"FS", fieldSep},
   209  	}
   210  	for _, v := range vars {
   211  		parts := strings.SplitN(v, "=", 2)
   212  		if len(parts) != 2 {
   213  			errorExitf("-v flag must be in format name=value")
   214  		}
   215  		config.Vars = append(config.Vars, parts[0], parts[1])
   216  	}
   217  
   218  	if cpuprofile != "" {
   219  		f, err := os.Create(cpuprofile)
   220  		if err != nil {
   221  			errorExitf("could not create CPU profile: %v", err)
   222  		}
   223  		if err := pprof.StartCPUProfile(f); err != nil {
   224  			errorExitf("could not start CPU profile: %v", err)
   225  		}
   226  	}
   227  
   228  	// Run the program!
   229  	status, err := interp.ExecProgram(prog, config)
   230  	if err != nil {
   231  		errorExit(err)
   232  	}
   233  
   234  	if cpuprofile != "" {
   235  		pprof.StopCPUProfile()
   236  	}
   237  	if memprofile != "" {
   238  		f, err := os.Create(memprofile)
   239  		if err != nil {
   240  			errorExitf("could not create memory profile: %v", err)
   241  		}
   242  		runtime.GC() // get up-to-date statistics
   243  		if err := pprof.WriteHeapProfile(f); err != nil {
   244  			errorExitf("could not write memory profile: %v", err)
   245  		}
   246  		_ = f.Close()
   247  	}
   248  
   249  	os.Exit(status)
   250  }
   251  
   252  // For parse errors, show source line and position of error, eg:
   253  //
   254  // -----------------------------------------------------
   255  // BEGIN { x*; }
   256  //           ^
   257  // -----------------------------------------------------
   258  // parse error at 1:11: expected expression instead of ;
   259  //
   260  func showSourceLine(src []byte, pos lexer.Position, dividerLen int) {
   261  	divider := strings.Repeat("-", dividerLen)
   262  	if divider != "" {
   263  		fmt.Fprintln(os.Stderr, divider)
   264  	}
   265  	lines := bytes.Split(src, []byte{'\n'})
   266  	srcLine := string(lines[pos.Line-1])
   267  	numTabs := strings.Count(srcLine[:pos.Column-1], "\t")
   268  	runeColumn := utf8.RuneCountInString(srcLine[:pos.Column-1])
   269  	fmt.Fprintln(os.Stderr, strings.Replace(srcLine, "\t", "    ", -1))
   270  	fmt.Fprintln(os.Stderr, strings.Repeat(" ", runeColumn)+strings.Repeat("   ", numTabs)+"^")
   271  	if divider != "" {
   272  		fmt.Fprintln(os.Stderr, divider)
   273  	}
   274  }
   275  
   276  func errorExit(err error) {
   277  	pathErr, ok := err.(*os.PathError)
   278  	if ok && os.IsNotExist(err) {
   279  		errorExitf("file %q not found", pathErr.Path)
   280  	}
   281  	errorExitf("%s", err)
   282  }
   283  
   284  func errorExitf(format string, args ...interface{}) {
   285  	fmt.Fprintf(os.Stderr, format+"\n", args...)
   286  	os.Exit(1)
   287  }
   288  
   289  func expandWildcardsOnWindows(args []string) []string {
   290  	if runtime.GOOS != "windows" {
   291  		return args
   292  	}
   293  	return expandWildcards(args)
   294  }
   295  
   296  // Originally from https://github.com/mattn/getwild (compatible LICENSE).
   297  func expandWildcards(args []string) []string {
   298  	result := make([]string, 0, len(args))
   299  	for _, arg := range args {
   300  		matches, err := filepath.Glob(arg)
   301  		if err == nil && len(matches) > 0 {
   302  			result = append(result, matches...)
   303  		} else {
   304  			result = append(result, arg)
   305  		}
   306  	}
   307  	return result
   308  }