github.com/mvdan/u-root-coreutils@v0.0.0-20230122170626-c2eef2898555/cmds/exp/ed/commands.go (about)

     1  // Copyright 2019 the u-root Authors. All rights reserved
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // commands.go - defines editor commands
     6  package main
     7  
     8  import (
     9  	"bufio"
    10  	"bytes"
    11  	"fmt"
    12  	"io"
    13  	"os"
    14  	"regexp"
    15  	"strconv"
    16  	"strings"
    17  )
    18  
    19  var errExit = fmt.Errorf("exit")
    20  
    21  // A Context is passed to an invoked command
    22  type Context struct {
    23  	cmd       string // full command string
    24  	cmdOffset int    // start of the command after address resolution
    25  	addrs     []int  // resolved addresses
    26  	out       io.Writer
    27  }
    28  
    29  // A Command can be run with a Context and returns an error
    30  type Command func(*Context) error
    31  
    32  // The cmds map maps single byte commands to their handler functions.
    33  // This is also a good way to check what commands are implemented.
    34  var cmds = map[byte]Command{
    35  	'q': cmdQuit,
    36  	'Q': cmdQuit,
    37  	'd': cmdDelete,
    38  	'l': cmdPrint,
    39  	'p': cmdPrint,
    40  	'n': cmdPrint,
    41  	'h': cmdErr,
    42  	'H': cmdErr,
    43  	'a': cmdInput,
    44  	'i': cmdInput,
    45  	'c': cmdInput,
    46  	'w': cmdWrite,
    47  	'W': cmdWrite,
    48  	'k': cmdMark,
    49  	'e': cmdEdit,
    50  	'E': cmdEdit,
    51  	'r': cmdEdit,
    52  	'f': cmdFile,
    53  	'=': cmdLine,
    54  	'j': cmdJoin,
    55  	'm': cmdMove,
    56  	't': cmdMove,
    57  	'y': cmdCopy,
    58  	'x': cmdPaste,
    59  	'P': cmdPrompt,
    60  	's': cmdSub,
    61  	'u': cmdUndo,
    62  	'D': cmdDump, // var dump the buffer for debug
    63  	'z': cmdScroll,
    64  	'!': cmdCommand,
    65  	'#': func(*Context) (e error) { return },
    66  }
    67  
    68  //////////////////////
    69  // Command handlers /
    70  ////////////////////
    71  
    72  func cmdDelete(ctx *Context) (e error) {
    73  	var r [2]int
    74  	if r, e = buffer.AddrRangeOrLine(ctx.addrs); e != nil {
    75  		return
    76  	}
    77  	e = buffer.Delete(r)
    78  	return
    79  }
    80  
    81  func cmdQuit(ctx *Context) (e error) {
    82  	if ctx.cmd[ctx.cmdOffset] == 'q' && buffer.Dirty() {
    83  		return fmt.Errorf("warning: file modified")
    84  	}
    85  	return errExit
    86  }
    87  
    88  func cmdPrint(ctx *Context) (e error) {
    89  	var r [2]int
    90  	if r, e = buffer.AddrRangeOrLine(ctx.addrs); e != nil {
    91  		return
    92  	}
    93  	for l := r[0]; l <= r[1]; l++ {
    94  		if ctx.cmd[ctx.cmdOffset] == 'n' {
    95  			fmt.Fprintf(ctx.out, "%d\t", l+1)
    96  		}
    97  		line := buffer.GetMust(l, true)
    98  		if ctx.cmd[ctx.cmdOffset] == 'l' {
    99  			line += "$" // TODO: the man pages describes more escaping, but it's not clear what GNU ed actually does.
   100  		}
   101  		fmt.Fprintf(ctx.out, "%s\n", line)
   102  	}
   103  	return
   104  }
   105  
   106  func cmdScroll(ctx *Context) (e error) {
   107  	start, e := buffer.AddrValue(ctx.addrs)
   108  	if e != nil {
   109  		return
   110  	}
   111  	// parse win size (if there)
   112  	winStr := ctx.cmd[ctx.cmdOffset+1:]
   113  	if len(winStr) > 0 {
   114  		var win int
   115  		if win, e = strconv.Atoi(winStr); e != nil {
   116  			return fmt.Errorf("invalid window size: %s", winStr)
   117  		}
   118  		state.winSize = win
   119  	}
   120  	end := start + state.winSize - 1
   121  	if end > buffer.Len()-1 {
   122  		end = buffer.Len() - 1
   123  	}
   124  	var ls []string
   125  	if ls, e = buffer.Get([2]int{start, end}); e != nil {
   126  		return
   127  	}
   128  	for _, l := range ls {
   129  		fmt.Fprintf(ctx.out, "%s\n", l)
   130  	}
   131  	return
   132  }
   133  
   134  func cmdErr(ctx *Context) (e error) {
   135  	if ctx.cmd[ctx.cmdOffset] == 'h' {
   136  		if state.lastErr != nil {
   137  			fmt.Fprintf(ctx.out, "%s\n", state.lastErr)
   138  			return
   139  		}
   140  	}
   141  	if ctx.cmd[ctx.cmdOffset] == 'H' {
   142  		if state.printErr {
   143  			state.printErr = false
   144  			return
   145  		}
   146  		state.printErr = true
   147  	}
   148  	return
   149  }
   150  
   151  func cmdInput(ctx *Context) (e error) {
   152  	scan := bufio.NewScanner(os.Stdin)
   153  	nbuf := []string{}
   154  	if len(ctx.cmd[ctx.cmdOffset+1:]) != 0 && ctx.cmd[ctx.cmdOffset] != 'c' {
   155  		return fmt.Errorf("%c only takes a single line addres", ctx.cmd[ctx.cmdOffset])
   156  	}
   157  	for scan.Scan() {
   158  		line := scan.Text()
   159  		if line == "." {
   160  			break
   161  		}
   162  		nbuf = append(nbuf, line)
   163  	}
   164  	if len(nbuf) == 0 {
   165  		return
   166  	}
   167  	switch ctx.cmd[ctx.cmdOffset] {
   168  	case 'i':
   169  		var line int
   170  		if line, e = buffer.AddrValue(ctx.addrs); e != nil {
   171  			return
   172  		}
   173  		e = buffer.Insert(line, nbuf)
   174  	case 'a':
   175  		var line int
   176  		if line, e = buffer.AddrValue(ctx.addrs); e != nil {
   177  			return
   178  		}
   179  		e = buffer.Insert(line+1, nbuf)
   180  	case 'c':
   181  		var r [2]int
   182  		if r, e = buffer.AddrRange(ctx.addrs); e != nil {
   183  			return
   184  		}
   185  		if e = buffer.Delete(r); e != nil {
   186  			return
   187  		}
   188  		e = buffer.Insert(r[0], nbuf)
   189  	}
   190  	return
   191  }
   192  
   193  var rxWrite = regexp.MustCompile(`^(q)?(?: )?(!)?(.*)`)
   194  
   195  func cmdWrite(ctx *Context) (e error) {
   196  	file := state.fileName
   197  	quit := false
   198  	run := false
   199  	var r [2]int
   200  	if ctx.cmdOffset == 0 {
   201  		r[0] = 0
   202  		r[1] = buffer.Len() - 1
   203  	} else {
   204  		if r, e = buffer.AddrRange(ctx.addrs); e != nil {
   205  			return
   206  		}
   207  	}
   208  	m := rxWrite.FindAllStringSubmatch(ctx.cmd[ctx.cmdOffset+1:], -1)
   209  	if m[0][1] == "q" {
   210  		quit = true
   211  	}
   212  	if m[0][2] == "!" {
   213  		run = true
   214  	}
   215  	if len(m[0][3]) > 0 {
   216  		file = m[0][3]
   217  	}
   218  	var lstr []string
   219  	lstr, e = buffer.Get(r)
   220  	if e != nil {
   221  		return
   222  	}
   223  	if run {
   224  		s := System{
   225  			Cmd:    m[0][3],
   226  			Stdin:  bytes.NewBuffer(nil),
   227  			Stdout: os.Stdout,
   228  			Stderr: os.Stderr,
   229  		}
   230  		go func() {
   231  			for _, str := range lstr {
   232  				if _, e = fmt.Fprintf(s.Stdin.(*bytes.Buffer), "%s\n", str); e != nil {
   233  					return
   234  				}
   235  			}
   236  		}()
   237  		return s.Run()
   238  	}
   239  
   240  	var f *os.File
   241  	oFlag := os.O_TRUNC
   242  	if ctx.cmd[ctx.cmdOffset] == 'W' {
   243  		oFlag = os.O_APPEND
   244  	}
   245  	if f, e = os.OpenFile(file, os.O_WRONLY|os.O_CREATE|oFlag, 0o666); e != nil {
   246  		return e
   247  	}
   248  	defer f.Close()
   249  
   250  	for _, s := range lstr {
   251  		_, e = fmt.Fprintf(f, "%s\n", s)
   252  		if e != nil {
   253  			return
   254  		}
   255  	}
   256  	if quit {
   257  		if e = cmdQuit(ctx); e != nil {
   258  			return
   259  		}
   260  	}
   261  	buffer.Clean()
   262  	return
   263  }
   264  
   265  func cmdMark(ctx *Context) (e error) {
   266  	if len(ctx.cmd)-1 <= ctx.cmdOffset {
   267  		e = fmt.Errorf("no mark character supplied")
   268  		return
   269  	}
   270  	c := ctx.cmd[ctx.cmdOffset+1]
   271  	var l int
   272  	if l, e = buffer.AddrValue(ctx.addrs); e != nil {
   273  		return
   274  	}
   275  	e = buffer.SetMark(c, l)
   276  	return
   277  }
   278  
   279  func cmdEdit(ctx *Context) (e error) {
   280  	var addr int
   281  	// we do this manually because we allow addr 0
   282  	if len(ctx.addrs) == 0 {
   283  		return ErrINV
   284  	}
   285  	addr = ctx.addrs[len(ctx.addrs)-1]
   286  	if addr != 0 && buffer.OOB(addr) {
   287  		return ErrOOB
   288  	}
   289  	// cmd or filename?
   290  	cmd := ctx.cmd[ctx.cmdOffset]
   291  	force := false
   292  	if cmd == 'E' || cmd == 'r' {
   293  		force = true
   294  	} // else == 'e'
   295  	if buffer.Dirty() && !force {
   296  		return fmt.Errorf("warning: file modified")
   297  	}
   298  	filename := ctx.cmd[ctx.cmdOffset+1:]
   299  	filename = filename[wsOffset(filename):]
   300  	var fh io.Reader
   301  	if len(filename) == 0 {
   302  		filename = state.fileName
   303  	}
   304  	if filename[0] == '!' { // command, not filename
   305  		s := System{
   306  			Cmd:    filename[1:],
   307  			Stdout: bytes.NewBuffer(nil),
   308  			Stdin:  os.Stdin,
   309  			Stderr: os.Stderr,
   310  		}
   311  		if e = s.Run(); e != nil {
   312  			return
   313  		}
   314  		fh = s.Stdout.(io.Reader)
   315  	} else { // filename
   316  		if _, e = os.Stat(filename); os.IsNotExist(e) && !fsuppress {
   317  			return fmt.Errorf("%s: No such file or directory", filename)
   318  			// this is not fatal, we just start with an empty buffer
   319  		}
   320  		if fh, e = os.Open(filename); e != nil {
   321  			e = fmt.Errorf("could not read file: %v", e)
   322  			return
   323  		}
   324  		state.fileName = filename
   325  	}
   326  
   327  	if cmd != 'r' { // other commands replace
   328  		buffer = NewFileBuffer(nil)
   329  		if e = buffer.Read(0, fh); e != nil {
   330  			return
   331  		}
   332  	} else {
   333  		e = buffer.Read(addr, fh)
   334  	}
   335  	if !fsuppress {
   336  		fmt.Fprintf(ctx.out, "%d\n", buffer.Size())
   337  	}
   338  	return
   339  }
   340  
   341  func cmdFile(ctx *Context) (e error) {
   342  	newFile := ctx.cmd[ctx.cmdOffset:]
   343  	newFile = newFile[wsOffset(newFile):]
   344  	if len(newFile) > 0 {
   345  		state.fileName = newFile
   346  		return
   347  	}
   348  	fmt.Fprintf(ctx.out, "%s\n", state.fileName)
   349  	return
   350  }
   351  
   352  func cmdLine(ctx *Context) (e error) {
   353  	addr, e := buffer.AddrValue(ctx.addrs)
   354  	if e == nil {
   355  		fmt.Fprintf(ctx.out, "%d\n", addr+1)
   356  	}
   357  	return
   358  }
   359  
   360  func cmdJoin(ctx *Context) (e error) {
   361  	var r [2]int
   362  	if r, e = buffer.AddrRangeOrLine(ctx.addrs); e != nil {
   363  		return
   364  	}
   365  	// Technically only a range works, but a line isn't an error
   366  	if r[0] == r[1] {
   367  		return
   368  	}
   369  
   370  	joined := ""
   371  	for l := r[0]; l <= r[1]; l++ {
   372  		joined += buffer.GetMust(l, false)
   373  	}
   374  	if e = buffer.Delete(r); e != nil {
   375  		return
   376  	}
   377  	e = buffer.Insert(r[0], []string{joined})
   378  	return
   379  }
   380  
   381  func cmdMove(ctx *Context) (e error) {
   382  	var r [2]int
   383  	var dest int
   384  	var lines []string
   385  	cmd := ctx.cmd[ctx.cmdOffset]
   386  	if r, e = buffer.AddrRangeOrLine(ctx.addrs); e != nil {
   387  		return
   388  	}
   389  	// must parse the destination
   390  	destStr := ctx.cmd[ctx.cmdOffset+1:]
   391  	var nctx Context
   392  	if nctx.addrs, nctx.cmdOffset, e = buffer.ResolveAddrs(destStr); e != nil {
   393  		return
   394  	}
   395  	// this is a bit hacky, but we're supposed to allow 0
   396  	append := 1
   397  	last := len(nctx.addrs) - 1
   398  	if nctx.addrs[last] == -1 {
   399  		nctx.addrs[last] = 0
   400  		append = 0
   401  	}
   402  	if dest, e = buffer.AddrValue(nctx.addrs); e != nil {
   403  		return
   404  	}
   405  
   406  	if lines, e = buffer.Get(r); e != nil {
   407  		return
   408  	}
   409  	delt := r[1] - r[0] + 1
   410  	if dest < r[0] {
   411  		r[0] += delt
   412  		r[1] += delt
   413  	} else if dest > r[1] {
   414  		// NOP
   415  	} else {
   416  		return fmt.Errorf("cannot move lines to within their own range")
   417  	}
   418  
   419  	// Should we throw an error if there's trailing stuff?
   420  	if e = buffer.Insert(dest+append, lines); e != nil {
   421  		return
   422  	}
   423  	if cmd == 'm' {
   424  		e = buffer.Delete(r)
   425  	} // else 't'
   426  	return
   427  }
   428  
   429  func cmdCopy(ctx *Context) (e error) {
   430  	var r [2]int
   431  	if r, e = buffer.AddrRangeOrLine(ctx.addrs); e != nil {
   432  		return
   433  	}
   434  	return buffer.Copy(r)
   435  }
   436  
   437  func cmdPaste(ctx *Context) (e error) {
   438  	var addr int
   439  	// this is a bit hacky, but we're supposed to allow 0
   440  	append := 1
   441  	last := len(ctx.addrs) - 1
   442  	if ctx.addrs[last] == -1 {
   443  		ctx.addrs[last] = 0
   444  		append = 0
   445  	}
   446  	if addr, e = buffer.AddrValue(ctx.addrs); e != nil {
   447  		return
   448  	}
   449  	return buffer.Paste(addr + append)
   450  }
   451  
   452  func cmdPrompt(ctx *Context) (e error) {
   453  	if state.prompt {
   454  		state.prompt = false
   455  	} else if len(fprompt) > 0 {
   456  		state.prompt = true
   457  	}
   458  	return
   459  }
   460  
   461  var (
   462  	rxSanitize        = regexp.MustCompile(`\\.`)
   463  	rxBackrefSanitize = regexp.MustCompile(`\\\\`)
   464  	rxBackref         = regexp.MustCompile(`\\([0-9]+)|&`)
   465  	rxSubArgs         = regexp.MustCompile(`g|l|n|p|\d+`)
   466  )
   467  
   468  // FIXME: this is probably more convoluted than it needs to be
   469  func cmdSub(ctx *Context) (e error) {
   470  	cmd := ctx.cmd[ctx.cmdOffset+1:]
   471  	if len(cmd) == 0 {
   472  		if len(state.lastSub) == 0 {
   473  			return fmt.Errorf("invalid substitution")
   474  		}
   475  		cmd = state.lastSub
   476  	}
   477  	state.lastSub = cmd
   478  	del := cmd[0]
   479  	switch del {
   480  	case ' ':
   481  		fallthrough
   482  	case '\n':
   483  		fallthrough
   484  	case 'm':
   485  		fallthrough
   486  	case 'g':
   487  		return fmt.Errorf("invalid pattern delimiter")
   488  	}
   489  	// we replace escapes and their escaped characters with spaces to keep indexing
   490  	sane := rxSanitize.ReplaceAllString(cmd, "  ")
   491  
   492  	idx := [2]int{-1, -1}
   493  	idx[0] = strings.Index(sane[1:], string(del)) + 1
   494  	if idx[0] != -1 {
   495  		idx[1] = strings.Index(sane[idx[0]+1:], string(del)) + idx[0] + 1
   496  	}
   497  	if idx[1] == -1 {
   498  		idx[1] = len(cmd) - 1
   499  	}
   500  
   501  	mat := cmd[1:idx[0]]
   502  	rep := cmd[idx[0]+1 : idx[1]]
   503  	if rep == "%" {
   504  		rep = state.lastRep
   505  	}
   506  	state.lastRep = rep
   507  	arg := cmd[idx[1]+1:]
   508  
   509  	// arg processing
   510  	count := 1
   511  	var printP, printL, printN, global bool
   512  
   513  	parsedArgs := rxSubArgs.FindAllStringSubmatch(arg, -1)
   514  	for _, m := range parsedArgs {
   515  		switch m[0] {
   516  		case "g":
   517  			global = true
   518  		case "p":
   519  			printP = true
   520  		case "l":
   521  			printL = true
   522  		case "n":
   523  			printN = true
   524  		default:
   525  			if count, e = strconv.Atoi(m[0]); e != nil || count < 1 {
   526  				return fmt.Errorf("invalid substitution argument")
   527  			}
   528  		}
   529  	}
   530  
   531  	repSane := rxBackrefSanitize.ReplaceAllString(rep, "  ")
   532  	refs := rxBackref.FindAllStringSubmatchIndex(repSane, -1)
   533  
   534  	var r [2]int
   535  	if r, e = buffer.AddrRangeOrLine(ctx.addrs); e != nil {
   536  		return
   537  	}
   538  
   539  	var rx *regexp.Regexp
   540  	if rx, e = regexp.Compile(mat); e != nil {
   541  		return
   542  	}
   543  
   544  	last := ""
   545  	lastN := 0
   546  	nMatch := 0
   547  	b, _ := buffer.Get(r)
   548  	// we have to do things a bit manually because we we only have ReplaceAll, and we don't necessarily want that
   549  	for ln, l := range b {
   550  		matches := rx.FindAllStringSubmatchIndex(l, -1)
   551  		if !(len(matches) > 0) {
   552  			continue // skip the rest if we don't have matches
   553  		}
   554  		if !global {
   555  			if len(matches) >= count {
   556  				matches = [][]int{matches[count-1]}
   557  			} else {
   558  				matches = [][]int{}
   559  			}
   560  		}
   561  		// we have matches, deal with them
   562  		fLin := ""
   563  		oLin := 0
   564  		for _, m := range matches {
   565  			nMatch++
   566  			// fRep := rep
   567  			// offset := 0
   568  
   569  			// Fill backrefs
   570  			oRep := 0
   571  			fRep := ""
   572  			for _, r := range refs {
   573  				if rep[r[0]:r[1]] == "&" {
   574  					fRep += rep[oRep:r[0]]
   575  					fRep += l[m[0]:m[1]]
   576  					oRep = r[1]
   577  				} else {
   578  					i, _ := strconv.Atoi(rep[r[2]:r[3]])
   579  					if i > len(m)/2-1 { // not enough submatches for backref
   580  						return fmt.Errorf("invalid backref")
   581  					}
   582  					fRep += rep[oRep:r[0]]
   583  					fRep += l[m[2*i]:m[2*i+1]]
   584  					oRep = r[1]
   585  				}
   586  			}
   587  			fRep += rep[oRep:]
   588  
   589  			fLin += l[oLin:m[0]]
   590  			fLin += fRep
   591  			oLin = m[1]
   592  		}
   593  		fLin += l[oLin:]
   594  		if e = buffer.Delete([2]int{ln, ln}); e != nil {
   595  			return
   596  		}
   597  		if e = buffer.Insert(ln, []string{fLin}); e != nil {
   598  			return
   599  		}
   600  		last = fLin
   601  		lastN = ln
   602  	}
   603  	if nMatch == 0 {
   604  		e = fmt.Errorf("no match")
   605  	} else {
   606  		if printP {
   607  			fmt.Fprintf(ctx.out, "%s\n", last)
   608  		}
   609  		if printL {
   610  			fmt.Fprintf(ctx.out, "%s$\n", last)
   611  		}
   612  		if printN {
   613  			fmt.Fprintf(ctx.out, "%d\t%s\n", lastN+1, last)
   614  		}
   615  	}
   616  	return
   617  }
   618  
   619  func cmdUndo(ctx *Context) (e error) {
   620  	buffer.Rewind()
   621  	return
   622  }
   623  
   624  func cmdDump(ctx *Context) (e error) {
   625  	fmt.Fprintf(ctx.out, "%v\n", buffer)
   626  	return
   627  }
   628  
   629  var rxCmdSub = regexp.MustCompile(`%`)
   630  
   631  func cmdCommand(ctx *Context) (e error) {
   632  	s := System{
   633  		Cmd:    ctx.cmd[ctx.cmdOffset+1:],
   634  		Stdin:  os.Stdin,
   635  		Stdout: ctx.out,
   636  		Stderr: os.Stderr,
   637  	}
   638  	e = s.Run()
   639  	if e != nil {
   640  		return
   641  	}
   642  	fmt.Fprintf(ctx.out, "!")
   643  	return
   644  }