514 lines
16 KiB
Go
514 lines
16 KiB
Go
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||
// SPDX-License-Identifier: BSD-3-Clause
|
||
|
||
package asm
|
||
|
||
import (
|
||
"bytes"
|
||
"encoding/binary"
|
||
"os"
|
||
"os/exec"
|
||
"path/filepath"
|
||
"strings"
|
||
"testing"
|
||
|
||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||
)
|
||
|
||
// goobjView is a minimal parsed view of a GOOBJ payload, enough to check
|
||
// the emitter's output block by block.
|
||
type goobjView struct {
|
||
t *testing.T
|
||
b []byte
|
||
offs [blkEnd + 1]uint32
|
||
strOff uint32
|
||
}
|
||
|
||
func openGoobj(t *testing.T, data []byte) *goobjView {
|
||
t.Helper()
|
||
i := bytes.Index(data, []byte(goobjMagic))
|
||
if i < 0 {
|
||
t.Fatal("no GOOBJ magic in output")
|
||
}
|
||
v := &goobjView{t: t, b: data[i:], strOff: uint32(i + 96)}
|
||
for j := 0; j <= blkEnd; j++ {
|
||
v.offs[j] = binary.LittleEndian.Uint32(v.b[20+4*j:])
|
||
}
|
||
return v
|
||
}
|
||
|
||
func (v *goobjView) blk(i int) []byte { return v.b[v.offs[i]:v.offs[i+1]] }
|
||
|
||
func (v *goobjView) str(off, ln uint32) string {
|
||
return string(v.b[off : off+ln])
|
||
}
|
||
|
||
type goobjSymView struct {
|
||
name string
|
||
abi uint16
|
||
typ uint8
|
||
flag uint8
|
||
flag2 uint8
|
||
size uint32
|
||
align uint32
|
||
}
|
||
|
||
func (v *goobjView) syms(i int) []goobjSymView {
|
||
var out []goobjSymView
|
||
for x := v.blk(i); len(x) >= 21; x = x[21:] {
|
||
le := binary.LittleEndian
|
||
out = append(out, goobjSymView{
|
||
name: v.str(le.Uint32(x[4:]), le.Uint32(x[0:])),
|
||
abi: le.Uint16(x[8:]),
|
||
typ: x[10],
|
||
flag: x[11],
|
||
flag2: x[12],
|
||
size: le.Uint32(x[13:]),
|
||
align: le.Uint32(x[17:]),
|
||
})
|
||
}
|
||
return out
|
||
}
|
||
|
||
// TestGOObjectStructure checks the emitted object's blocks against the
|
||
// ground truth captured from go tool asm: the symbol tables, the FuncInfo
|
||
// contents, the pc-value tables, the relocation and the aux wiring.
|
||
func TestGOObjectStructure(t *testing.T) {
|
||
f, errs := parser.Parse("t_amd64.s", `
|
||
#include "textflag.h"
|
||
|
||
TEXT ·addq(SB), NOSPLIT, $0-24
|
||
MOVQ a+0(FP), AX
|
||
MOVQ b+8(FP), CX
|
||
ADDQ CX, AX
|
||
MOVQ AX, ret+16(FP)
|
||
RET
|
||
|
||
TEXT ·loadmask(SB), NOSPLIT, $0-8
|
||
VMOVDQU mask<>(SB), X0
|
||
VPMOVMSKB X0, AX
|
||
MOVQ AX, ret+0(FP)
|
||
RET
|
||
|
||
GLOBL mask<>(SB), RODATA, $16
|
||
DATA mask<>+0(SB)/8, $0x0807060504030201
|
||
DATA mask<>+8(SB)/8, $0x800f0e0d0c0b0a09
|
||
`)
|
||
if len(errs) > 0 {
|
||
t.Fatalf("parse: %v", errs)
|
||
}
|
||
img, err := AssembleFile(f)
|
||
if err != nil {
|
||
t.Fatalf("AssembleFile: %v", err)
|
||
}
|
||
obj, err := img.GOObject("testpkg", "t_amd64.s")
|
||
if err != nil {
|
||
t.Fatalf("GOObject: %v", err)
|
||
}
|
||
v := openGoobj(t, obj)
|
||
|
||
if flags := binary.LittleEndian.Uint32(v.b[16:]); flags != 4 {
|
||
t.Errorf("flags = %#x, want ObjFlagFromAssembly (4)", flags)
|
||
}
|
||
|
||
// Package defs: the static GLOBL, then per function the FuncInfo and the
|
||
// two DWARF symbols (debug_line program, subprogram DIE).
|
||
defs := v.syms(blkSymdef)
|
||
if len(defs) != 7 {
|
||
t.Fatalf("symdefs = %d, want 7", len(defs))
|
||
}
|
||
if defs[0].name != "mask" || defs[0].abi != 0xffff || defs[0].typ != kindSRODATA || defs[0].size != 16 || defs[0].flag2 != symFlag2Link {
|
||
t.Errorf("mask symbol = %+v", defs[0])
|
||
}
|
||
if defs[1].name != "" || defs[1].typ != kindSDATA || defs[1].size != 28 {
|
||
t.Errorf("addq funcinfo symbol = %+v", defs[1])
|
||
}
|
||
if defs[2].name != "" || defs[2].typ != kindSDWARFLINES || defs[2].size == 0 {
|
||
t.Errorf("addq lines symbol = %+v", defs[2])
|
||
}
|
||
if defs[3].name != "" || defs[3].typ != kindSDWARFFCN || defs[3].size == 0 {
|
||
t.Errorf("addq DIE symbol = %+v", defs[3])
|
||
}
|
||
if defs[4].name != "" || defs[4].typ != kindSDATA || defs[4].size != 28 {
|
||
t.Errorf("loadmask funcinfo symbol = %+v", defs[4])
|
||
}
|
||
|
||
// Non-package defs: four pc tables and the function, per function.
|
||
nps := v.syms(blkNonpkgdef)
|
||
if len(nps) != 10 {
|
||
t.Fatalf("nonpkgdefs = %d, want 10", len(nps))
|
||
}
|
||
fn := nps[4]
|
||
if fn.name != "testpkg.addq" || fn.typ != kindSTEXT || fn.flag != symFlagNoSplit || fn.size != 19 {
|
||
t.Errorf("addq symbol = %+v", fn)
|
||
}
|
||
for i, s := range []int{0, 1, 2, 3, 5, 6, 7, 8} {
|
||
if nps[s].typ != kindSRODATA || nps[s].align != 1 || nps[s].name != "" {
|
||
t.Errorf("pc table %d = %+v", i, nps[s])
|
||
}
|
||
}
|
||
|
||
// FuncInfo: args 24, FuncFlag Asm, one file, no inline tree.
|
||
le := binary.LittleEndian
|
||
data := v.blk(blkData)
|
||
didx := v.blk(blkDataIdx)
|
||
fi := data[16:44]
|
||
if le.Uint32(fi[0:]) != 24 || le.Uint32(fi[4:]) != 0 || fi[8] != 0 || fi[9] != funcFlagAsm ||
|
||
le.Uint32(fi[16:]) != 1 || le.Uint32(fi[20:]) != 0 || le.Uint32(fi[24:]) != 0 {
|
||
t.Errorf("funcinfo bytes %x", fi)
|
||
}
|
||
|
||
// The pc-value tables of addq (non-package indices 0–3, so global
|
||
// indices 7–10): pcsp a flat zero over the whole function, pcinline a
|
||
// flat -1, both with the pc delta in MinLC (1) units.
|
||
pcsp := data[le.Uint32(didx[4*7:]):]
|
||
if got := pcsp[:3]; !bytes.Equal(got, []byte{0x02, 19, 0x00}) {
|
||
t.Errorf("pcsp = %x, want 021300", got)
|
||
}
|
||
pcinl := data[le.Uint32(didx[4*10:]):]
|
||
if got := pcinl[:3]; !bytes.Equal(got, []byte{0x00, 19, 0x00}) {
|
||
t.Errorf("pcinline = %x, want 001300", got)
|
||
}
|
||
|
||
// Relocations: the four DWARF address references (two per function, in
|
||
// definition order), then the loadmask code's R_PCREL against the
|
||
// GLOBL, with the field in the function code left zero. The loadmask
|
||
// code's offset comes from the data index (7 defs + 9 non-package).
|
||
relocs := v.blk(blkReloc)
|
||
if len(relocs) != 5*23 {
|
||
t.Fatalf("relocs = %d bytes, want 5 entries", len(relocs))
|
||
}
|
||
// addq's DWARF references (defs 2 and 3) against the function, which
|
||
// is non-package index 4.
|
||
lr := relocs[:23]
|
||
if int32(le.Uint32(lr[0:])) != 3 || lr[4] != 8 || le.Uint16(lr[5:]) != relocAddr ||
|
||
le.Uint32(lr[15:]) != pkgIdxNone || le.Uint32(lr[19:]) != 4 {
|
||
t.Errorf("addq lines reloc = %x", lr)
|
||
}
|
||
dr := relocs[23:46]
|
||
if dr[4] != 4 || le.Uint16(dr[5:]) != relocDWTXTADDRU4() ||
|
||
le.Uint32(dr[15:]) != pkgIdxNone || le.Uint32(dr[19:]) != 4 {
|
||
t.Errorf("addq DIE reloc = %x", dr)
|
||
}
|
||
cr := relocs[4*23:]
|
||
off := int32(le.Uint32(cr[0:]))
|
||
if off != 4 || cr[4] != 4 || le.Uint16(cr[5:]) != relocPCRel ||
|
||
le.Uint64(cr[7:]) != 0 || le.Uint32(cr[15:]) != pkgIdxSelf || le.Uint32(cr[19:]) != 0 {
|
||
t.Errorf("loadmask reloc = %x", cr)
|
||
}
|
||
lm := le.Uint32(didx[4*16:])
|
||
code := data[lm : lm+18]
|
||
if !bytes.Equal(code[4:8], []byte{0, 0, 0, 0}) {
|
||
t.Errorf("relocated field = %x, want zeroed", code[4:8])
|
||
}
|
||
|
||
// Aux wiring: FuncInfo, the two DWARF symbols (package symbols), then
|
||
// the four pc tables (non-package symbols).
|
||
auxs := v.blk(blkAux)
|
||
if len(auxs) != 2*7*9 {
|
||
t.Fatalf("aux = %d bytes, want 14 entries", len(auxs))
|
||
}
|
||
wantAux := []struct {
|
||
typ uint8
|
||
pkg uint32
|
||
idx uint32
|
||
}{
|
||
{auxFuncInfo, pkgIdxSelf, 1},
|
||
{auxDwarfInfo, pkgIdxSelf, 3},
|
||
{auxDwarfLines, pkgIdxSelf, 2},
|
||
{auxPcsp, pkgIdxNone, 0},
|
||
{auxPcfile, pkgIdxNone, 1},
|
||
{auxPcline, pkgIdxNone, 2},
|
||
{auxPcinline, pkgIdxNone, 3},
|
||
{auxFuncInfo, pkgIdxSelf, 4},
|
||
{auxDwarfInfo, pkgIdxSelf, 6},
|
||
{auxDwarfLines, pkgIdxSelf, 5},
|
||
{auxPcsp, pkgIdxNone, 5},
|
||
{auxPcfile, pkgIdxNone, 6},
|
||
{auxPcline, pkgIdxNone, 7},
|
||
{auxPcinline, pkgIdxNone, 8},
|
||
}
|
||
for i, w := range wantAux {
|
||
e := auxs[i*9:]
|
||
if e[0] != w.typ || le.Uint32(e[1:]) != w.pkg || le.Uint32(e[5:]) != w.idx {
|
||
t.Errorf("aux[%d] = {%d,%d,%d}, want {%d,%d,%d}", i, e[0], le.Uint32(e[1:]), le.Uint32(e[5:]), w.typ, w.pkg, w.idx)
|
||
}
|
||
}
|
||
}
|
||
|
||
// decodePCValues decodes a pc-value table into (pc, value) steps. The
|
||
// table ends with a final unsigned pc delta covering the rest of the
|
||
// function, followed by a zero byte that carries no value delta.
|
||
func decodePCValues(b []byte) (pcs, vals []int64) {
|
||
val, n := binary.Varint(b)
|
||
b = b[n:]
|
||
val-- // the first delta is against the implicit -1
|
||
var pc int64
|
||
pcs = append(pcs, pc)
|
||
vals = append(vals, val)
|
||
for {
|
||
pcd, n := binary.Uvarint(b)
|
||
b = b[n:]
|
||
if pcd == 0 { // zero pc delta terminates the table
|
||
break
|
||
}
|
||
pc += int64(pcd)
|
||
if len(b) == 1 && b[0] == 0 { // final coverage, no value change
|
||
break
|
||
}
|
||
vd, n := binary.Varint(b)
|
||
b = b[n:]
|
||
val += vd
|
||
pcs = append(pcs, pc)
|
||
vals = append(vals, val)
|
||
}
|
||
return pcs, vals
|
||
}
|
||
|
||
// TestGOObjectPcspFrame checks the pcsp table of a frame-pointer function:
|
||
// the prologue raises the stack delta to 8+frame, the RET's epilogue
|
||
// restores it to zero.
|
||
func TestGOObjectPcspFrame(t *testing.T) {
|
||
f, errs := parser.Parse("frame_amd64.s", `
|
||
#include "textflag.h"
|
||
TEXT ·framed(SB), NOSPLIT, $8-0
|
||
MOVQ BP, AX
|
||
RET
|
||
`)
|
||
if len(errs) > 0 {
|
||
t.Fatalf("parse: %v", errs)
|
||
}
|
||
img, err := AssembleFile(f)
|
||
if err != nil {
|
||
t.Fatalf("AssembleFile: %v", err)
|
||
}
|
||
fn := img.Funcs[0]
|
||
pcs, vals := decodePCValues(pcspTable(fn, 1))
|
||
// Prologue: PUSHQ BP (1 byte, +8), MOVQ SP, BP (3 bytes, no change),
|
||
// SUBQ $8, SP (4 bytes, +16 in total); the RET's epilogue unwinds
|
||
// ADDQ $8, SP (+8) then POPQ BP (0).
|
||
wantPCs := []int64{0, 1, 8}
|
||
wantVals := []int64{0, 8, 16}
|
||
if len(pcs) < len(wantPCs) {
|
||
t.Fatalf("pcsp pcs = %v vals = %v", pcs, vals)
|
||
}
|
||
for i := range wantPCs {
|
||
if pcs[i] != wantPCs[i] || vals[i] != wantVals[i] {
|
||
t.Errorf("pcsp[%d] = (%d,%d), want (%d,%d) — all: %v %v", i, pcs[i], vals[i], wantPCs[i], wantVals[i], pcs, vals)
|
||
}
|
||
}
|
||
// The last two steps unwind the epilogue to zero.
|
||
n := len(pcs)
|
||
if vals[n-1] != 0 || vals[n-2] != 8 {
|
||
t.Errorf("epilogue steps = %v %v, want …8, 0", pcs, vals)
|
||
}
|
||
// The table covers the whole function.
|
||
if last := pcs[n-1]; last >= int64(fn.Size) {
|
||
t.Errorf("last pc %d beyond function size %d", last, fn.Size)
|
||
}
|
||
}
|
||
|
||
// TestGOObjectExternalRejected checks that a reference to a symbol no GLOBL
|
||
// defines is reported: GOOBJ emission resolves only file-local symbols so
|
||
// far.
|
||
func TestGOObjectExternalRejected(t *testing.T) {
|
||
f, errs := parser.Parse("ext_amd64.s", `
|
||
#include "textflag.h"
|
||
TEXT ·useext(SB), NOSPLIT, $0-8
|
||
MOVQ elsewhere(SB), AX
|
||
MOVQ AX, ret+0(FP)
|
||
RET
|
||
`)
|
||
if len(errs) > 0 {
|
||
t.Fatalf("parse: %v", errs)
|
||
}
|
||
img, err := AssembleFile(f)
|
||
if err != nil {
|
||
t.Fatalf("AssembleFile: %v", err)
|
||
}
|
||
if _, err := img.GOObject("p", "ext_amd64.s"); err == nil || !strings.Contains(err.Error(), "external") {
|
||
t.Errorf("error = %v, want an external-symbol error", err)
|
||
}
|
||
}
|
||
|
||
// TestGOObjectLinkAndRun is the end-to-end check: assemble the test
|
||
// functions to a GOOBJ, swap it into a go build in place of the toolchain's
|
||
// assembly object, link, and run — the output must match the baseline
|
||
// binary the Go assembler produced. Skipped when no Go toolchain is
|
||
// available.
|
||
func TestGOObjectLinkAndRun(t *testing.T) {
|
||
goBin, err := exec.LookPath("go")
|
||
if err != nil {
|
||
t.Skip("no Go toolchain available")
|
||
}
|
||
dir := t.TempDir()
|
||
|
||
const asmSrc = `
|
||
#include "textflag.h"
|
||
|
||
TEXT ·addq(SB), NOSPLIT, $0-24
|
||
MOVQ a+0(FP), AX
|
||
MOVQ b+8(FP), CX
|
||
ADDQ CX, AX
|
||
MOVQ AX, ret+16(FP)
|
||
RET
|
||
|
||
TEXT ·loadmask(SB), NOSPLIT, $0-8
|
||
VMOVDQU mask<>(SB), X0
|
||
VPMOVMSKB X0, AX
|
||
MOVQ AX, ret+0(FP)
|
||
RET
|
||
|
||
GLOBL mask<>(SB), RODATA, $16
|
||
DATA mask<>+0(SB)/8, $0x0807060504030201
|
||
DATA mask<>+8(SB)/8, $0x800f0e0d0c0b0a09
|
||
`
|
||
const mainSrc = `package main
|
||
|
||
func addq(a, b int64) int64
|
||
func loadmask() int64
|
||
|
||
func main() {
|
||
println(addq(41, 1))
|
||
println(loadmask())
|
||
}
|
||
`
|
||
if err := os.WriteFile(filepath.Join(dir, "main_amd64.s"), []byte(asmSrc), 0o644); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := os.WriteFile(filepath.Join(dir, "main.go"), []byte(mainSrc), 0o644); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module goobjtest\n\ngo 1.27\n"), 0o644); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
// Baseline build with the toolchain's assembler; keep the work
|
||
// directory and the commands the build used.
|
||
cmd := exec.Command(goBin, "build", "-x", "-work", "-o", "app", ".")
|
||
cmd.Dir = dir
|
||
buildLog, err := cmd.CombinedOutput()
|
||
if err != nil {
|
||
t.Fatalf("baseline build: %v\n%s", err, buildLog)
|
||
}
|
||
var work string
|
||
var asmObj, pkgArch, linkLine string
|
||
for line := range strings.SplitSeq(string(buildLog), "\n") {
|
||
switch {
|
||
case strings.HasPrefix(line, "WORK="):
|
||
work = strings.TrimPrefix(line, "WORK=")
|
||
case strings.Contains(line, "/asm ") && strings.Contains(line, "-o ") && strings.Contains(line, "main_amd64.s") && !strings.Contains(line, "-gensymabis"):
|
||
asmObj = fieldAfter(line, "-o")
|
||
case strings.Contains(line, "pack r") && strings.Contains(line, "_pkg_.a"):
|
||
pkgArch = strings.TrimSpace(strings.SplitN(line, "pack r", 2)[1])
|
||
pkgArch = strings.Fields(strings.SplitN(pkgArch, "#", 2)[0])[0]
|
||
case strings.Contains(line, "/link ") && strings.Contains(line, "-importcfg"):
|
||
linkLine = line
|
||
}
|
||
}
|
||
if work == "" || asmObj == "" || pkgArch == "" || linkLine == "" {
|
||
t.Fatalf("could not locate the build steps:\n%s", buildLog)
|
||
}
|
||
asmObj = strings.ReplaceAll(asmObj, "$WORK", work)
|
||
pkgArch = strings.ReplaceAll(pkgArch, "$WORK", work)
|
||
|
||
// The baseline's answer.
|
||
baseOut, err := exec.Command(filepath.Join(dir, "app")).CombinedOutput()
|
||
if err != nil {
|
||
t.Fatalf("run baseline: %v\n%s", err, baseOut)
|
||
}
|
||
|
||
// Assemble the same source with gasm and swap the object in.
|
||
pf, perrs := parser.Parse(filepath.Join(dir, "main_amd64.s"), asmSrc)
|
||
if len(perrs) > 0 {
|
||
t.Fatalf("parse: %v", perrs)
|
||
}
|
||
img, err := AssembleFile(pf)
|
||
if err != nil {
|
||
t.Fatalf("AssembleFile: %v", err)
|
||
}
|
||
obj, err := img.GOObject("main", filepath.Join(dir, "main_amd64.s"))
|
||
if err != nil {
|
||
t.Fatalf("GOObject: %v", err)
|
||
}
|
||
|
||
// Rebuild the package archive with our object in place of the
|
||
// toolchain's (go tool pack has no replace-in-place that dedupes, so
|
||
// extract, substitute and repack). The archive member holding the
|
||
// assembler's output is named after the asm object file, e.g.
|
||
// main_amd64.o.
|
||
extract := exec.Command(goBin, "tool", "pack", "x", pkgArch)
|
||
membersDir := filepath.Join(dir, "members")
|
||
if err := os.MkdirAll(membersDir, 0o755); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
extract.Dir = membersDir
|
||
if out, err := extract.CombinedOutput(); err != nil {
|
||
t.Fatalf("pack x: %v\n%s", err, out)
|
||
}
|
||
member := filepath.Join(membersDir, filepath.Base(asmObj))
|
||
if err := os.Chmod(member, 0o644); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := os.WriteFile(member, obj, 0o644); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
listCmd := exec.Command(goBin, "tool", "pack", "t", pkgArch)
|
||
listOut, err := listCmd.CombinedOutput()
|
||
if err != nil {
|
||
t.Fatalf("pack t: %v\n%s", err, listOut)
|
||
}
|
||
newArch := filepath.Join(dir, "pkg.a")
|
||
args := []string{"tool", "pack", "c", newArch}
|
||
seen := map[string]bool{}
|
||
for m := range strings.FieldsSeq(string(listOut)) {
|
||
if seen[m] {
|
||
continue
|
||
}
|
||
seen[m] = true
|
||
if err := os.Chmod(filepath.Join(membersDir, m), 0o644); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
args = append(args, filepath.Join(membersDir, m))
|
||
}
|
||
pack := exec.Command(goBin, args...)
|
||
pack.Dir = membersDir
|
||
if out, err := pack.CombinedOutput(); err != nil {
|
||
t.Fatalf("pack c: %v\n%s", err, out)
|
||
}
|
||
|
||
// Link with our archive. The link line carries a GOROOT assignment
|
||
// and $WORK placeholders; run it through the shell with the
|
||
// GOEXPERIMENT the toolchain expects (the linker compares the object
|
||
// header against its own, experiments included).
|
||
goExp, _ := exec.Command(goBin, "env", "GOEXPERIMENT").Output()
|
||
linkLine = strings.ReplaceAll(linkLine, "$WORK", work)
|
||
linkLine = strings.ReplaceAll(linkLine, filepath.Join(work, "b001", "_pkg_.a"), newArch)
|
||
linkLine = strings.ReplaceAll(linkLine, filepath.Join(work, "b001", "exe", "a.out"), filepath.Join(dir, "app2"))
|
||
link := exec.Command("sh", "-c", linkLine)
|
||
link.Dir = dir
|
||
link.Env = append(os.Environ(), "GOEXPERIMENT="+strings.TrimSpace(string(goExp)))
|
||
if out, err := link.CombinedOutput(); err != nil {
|
||
t.Fatalf("link with gasm object: %v\n%s", err, out)
|
||
}
|
||
got, err := exec.Command(filepath.Join(dir, "app2")).CombinedOutput()
|
||
if err != nil {
|
||
t.Fatalf("run gasm-linked binary: %v\n%s", err, got)
|
||
}
|
||
if !bytes.Equal(got, baseOut) {
|
||
t.Errorf("gasm-linked output %q, want baseline %q", got, baseOut)
|
||
}
|
||
}
|
||
|
||
// fieldAfter returns the whitespace-delimited field following the first
|
||
// occurrence of flag in line.
|
||
func fieldAfter(line, flag string) string {
|
||
fields := strings.Fields(line)
|
||
for i, f := range fields {
|
||
if f == flag && i+1 < len(fields) {
|
||
return fields[i+1]
|
||
}
|
||
}
|
||
return ""
|
||
}
|