1
1
mirror of https://github.com/wader/fq.git synced 2024-12-01 19:12:34 +03:00
fq/pkg/cli/cli.go

259 lines
5.4 KiB
Go
Raw Normal View History

2020-06-08 03:29:51 +03:00
package cli
import (
"bufio"
"context"
"errors"
"fmt"
"io"
"io/fs"
"log"
"os"
"os/signal"
"path/filepath"
"runtime"
2020-06-08 03:29:51 +03:00
"github.com/wader/fq/pkg/interp"
"github.com/wader/fq/pkg/registry"
"github.com/wader/readline"
2020-06-08 03:29:51 +03:00
)
func maybeLogFile() {
2021-08-25 14:47:33 +03:00
// used during dev to redirect log to file, useful when debugging repl etc
2020-06-08 03:29:51 +03:00
if lf := os.Getenv("LOGFILE"); lf != "" {
2021-08-25 14:47:33 +03:00
if f, err := os.Create(lf); err == nil {
log.SetOutput(f)
}
2020-06-08 03:29:51 +03:00
}
}
type autoCompleterFn func(line []rune, pos int) (newLine [][]rune, length int)
func (a autoCompleterFn) Do(line []rune, pos int) (newLine [][]rune, length int) {
return a(line, pos)
}
2021-09-01 16:01:13 +03:00
type stdOS struct {
2021-11-01 14:57:55 +03:00
rl *readline.Instance
closeChan chan struct{}
2021-11-01 14:57:55 +03:00
interruptChan chan struct{}
2020-06-08 03:29:51 +03:00
}
2021-09-01 16:01:13 +03:00
func newStandardOS() *stdOS {
closeChan := make(chan struct{})
2020-06-08 03:29:51 +03:00
interruptChan := make(chan struct{}, 1)
// this more or less converts a os signal chan to just a struct{} chan that
// ignores signals if forwarding it would block, also this makes sure interp
// does not know about os.
2020-06-08 03:29:51 +03:00
go func() {
interruptSignalChan := make(chan os.Signal, 1)
signal.Notify(interruptSignalChan, os.Interrupt)
2021-11-01 14:57:55 +03:00
defer func() {
signal.Stop(interruptSignalChan)
close(interruptSignalChan)
close(interruptChan)
2021-11-01 14:57:55 +03:00
}()
for {
2020-06-08 03:29:51 +03:00
select {
case <-interruptSignalChan:
// ignore if interruptChan is full
select {
case interruptChan <- struct{}{}:
default:
}
case <-closeChan:
return
2020-06-08 03:29:51 +03:00
}
}
}()
2021-09-01 16:01:13 +03:00
return &stdOS{
closeChan: closeChan,
2021-11-01 14:57:55 +03:00
interruptChan: interruptChan,
2020-06-08 03:29:51 +03:00
}
}
func (stdOS) Platform() interp.Platform {
return interp.Platform{
OS: runtime.GOOS,
Arch: runtime.GOARCH,
}
}
2021-09-01 16:01:13 +03:00
type fdTerminal uintptr
func (fd fdTerminal) Size() (int, int) {
w, h, _ := readline.GetSize(int(fd))
return w, h
}
func (fd fdTerminal) IsTerminal() bool {
return readline.IsTerminal(int(fd))
}
type stdinInput struct {
fdTerminal
fs.File
}
func (o *stdOS) Stdin() interp.Input {
return stdinInput{
fdTerminal: fdTerminal(os.Stdin.Fd()),
File: interp.FileReader{
R: os.Stdin,
FileInfo: interp.FixedFileInfo{
FName: "stdin",
FMode: fs.ModeIrregular,
},
2020-06-08 03:29:51 +03:00
},
}
}
2021-09-01 16:01:13 +03:00
type stdoutOutput struct {
fdTerminal
os *stdOS
2020-06-08 03:29:51 +03:00
}
2021-09-01 16:01:13 +03:00
func (o stdoutOutput) Write(p []byte) (n int, err error) {
// Let write go thru readline if it has been used. This to have ansi color emulation
// on windows thru readlins:s stdout rewriter
// TODO: check if tty instead? else only color when repl
2021-08-26 18:47:09 +03:00
if o.os.rl != nil {
return o.os.rl.Write(p)
2020-06-08 03:29:51 +03:00
}
return os.Stdout.Write(p)
}
2021-09-01 16:01:13 +03:00
func (o *stdOS) Stdout() interp.Output {
return stdoutOutput{fdTerminal: fdTerminal(os.Stdout.Fd()), os: o}
2020-06-08 03:29:51 +03:00
}
2021-09-01 16:01:13 +03:00
type stderrOutput struct {
fdTerminal
2020-06-08 03:29:51 +03:00
}
2021-09-01 16:01:13 +03:00
func (o stderrOutput) Write(p []byte) (n int, err error) { return os.Stderr.Write(p) }
2020-06-08 03:29:51 +03:00
2021-09-01 16:01:13 +03:00
func (o *stdOS) Stderr() interp.Output { return stderrOutput{fdTerminal: fdTerminal(os.Stderr.Fd())} }
2020-06-08 03:29:51 +03:00
2021-11-01 14:57:55 +03:00
func (o *stdOS) InterruptChan() chan struct{} { return o.interruptChan }
2020-06-08 03:29:51 +03:00
2021-09-01 16:01:13 +03:00
func (*stdOS) Args() []string { return os.Args }
2020-06-08 03:29:51 +03:00
2021-09-01 16:01:13 +03:00
func (*stdOS) Environ() []string { return os.Environ() }
2020-06-08 03:29:51 +03:00
2021-09-01 16:01:13 +03:00
func (*stdOS) ConfigDir() (string, error) {
2020-06-08 03:29:51 +03:00
p, err := os.UserConfigDir()
if err != nil {
return "", err
}
return filepath.Join(p, "fq"), nil
}
2021-09-01 16:01:13 +03:00
type stdOSFS struct{}
2020-06-08 03:29:51 +03:00
2021-09-01 16:01:13 +03:00
func (stdOSFS) Open(name string) (fs.File, error) { return os.Open(name) }
2020-06-08 03:29:51 +03:00
2021-09-01 16:01:13 +03:00
func (*stdOS) FS() fs.FS { return stdOSFS{} }
2020-06-08 03:29:51 +03:00
func (o *stdOS) Readline(opts interp.ReadlineOpts) (string, error) {
2020-06-08 03:29:51 +03:00
if o.rl == nil {
var err error
var historyFile string
cacheDir, err := os.UserCacheDir()
if err != nil {
return "", err
}
historyFile = filepath.Join(cacheDir, "fq/history")
2020-06-08 03:29:51 +03:00
_ = os.MkdirAll(filepath.Dir(historyFile), 0700)
o.rl, err = readline.NewEx(&readline.Config{
HistoryFile: historyFile,
HistorySearchFold: true,
})
if err != nil {
return "", err
}
}
if opts.CompleteFn != nil {
2020-06-08 03:29:51 +03:00
o.rl.Config.AutoComplete = autoCompleterFn(func(line []rune, pos int) (newLine [][]rune, length int) {
names, shared := opts.CompleteFn(string(line), pos)
2020-06-08 03:29:51 +03:00
var runeNames [][]rune
for _, name := range names {
runeNames = append(runeNames, []rune(name[shared:]))
}
return runeNames, shared
})
}
o.rl.SetPrompt(opts.Prompt)
2020-06-08 03:29:51 +03:00
line, err := o.rl.Readline()
if errors.Is(err, readline.ErrInterrupt) {
return "", interp.ErrInterrupt
} else if errors.Is(err, io.EOF) {
return "", interp.ErrEOF
} else if err != nil {
return "", err
}
return line, nil
}
2021-09-01 16:01:13 +03:00
func (o *stdOS) History() ([]string, error) {
2020-06-08 03:29:51 +03:00
// TODO: refactor history handling to use internal fs?
r, err := os.Open(o.rl.Config.HistoryFile)
if err != nil {
return nil, err
}
defer r.Close()
var hs []string
lineScanner := bufio.NewScanner(r)
for lineScanner.Scan() {
hs = append(hs, lineScanner.Text())
}
if err := lineScanner.Err(); err != nil {
return nil, err
}
return hs, nil
}
2021-09-01 16:01:13 +03:00
func (o *stdOS) Close() error {
2020-06-08 03:29:51 +03:00
// only close if is terminal otherwise ansi reset will write
// to stdout and mess up raw output
if o.rl != nil {
o.rl.Close()
}
close(o.closeChan)
2020-06-08 03:29:51 +03:00
return nil
}
func Main(r *registry.Registry, version string) {
2020-06-08 03:29:51 +03:00
os.Exit(func() int {
defer maybeProfile()()
maybeLogFile()
2020-06-08 03:29:51 +03:00
sos := newStandardOS()
defer sos.Close()
i, err := interp.New(sos, r)
defer i.Stop()
if err != nil {
fmt.Fprintln(sos.Stderr(), err)
return 1
}
if err := i.Main(context.Background(), sos.Stdout(), version); err != nil {
2020-06-08 03:29:51 +03:00
if ex, ok := err.(interp.Exiter); ok { //nolint:errorlint
return ex.ExitCode()
}
return 1
}
return 0
}())
}