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