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 }