mirror of
https://github.com/wader/fq.git
synced 2024-11-27 14:14:58 +03:00
1ddea1ada3
Move registry to interp and add support for functions and filesystems. This will be used later for allow formats to add own functions and fq code. Add gojqextra function helpers to have more comfortable API to add functions. Takes care of argument type casting and JQValue:s and some more things. Refactor interp package to use new function helper and registry. Probably fixes a bunch of JQValue bugs and other type errors. Refactor out some mpeg nal things to mpeg format. Refactor interp jq code into display.q and init.jq. Remove undocumented aes_ctr funciton, was a test. Hopefully will add more crypto things laster.
258 lines
5.4 KiB
Go
258 lines
5.4 KiB
Go
package cli
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"io/fs"
|
|
"log"
|
|
"os"
|
|
"os/signal"
|
|
"path/filepath"
|
|
"runtime"
|
|
|
|
"github.com/wader/fq/pkg/interp"
|
|
|
|
"github.com/wader/readline"
|
|
)
|
|
|
|
func maybeLogFile() {
|
|
// used during dev to redirect log to file, useful when debugging repl etc
|
|
if lf := os.Getenv("LOGFILE"); lf != "" {
|
|
if f, err := os.Create(lf); err == nil {
|
|
log.SetOutput(f)
|
|
}
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
type stdOS struct {
|
|
rl *readline.Instance
|
|
closeChan chan struct{}
|
|
interruptChan chan struct{}
|
|
}
|
|
|
|
func newStandardOS() *stdOS {
|
|
closeChan := make(chan struct{})
|
|
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.
|
|
go func() {
|
|
interruptSignalChan := make(chan os.Signal, 1)
|
|
signal.Notify(interruptSignalChan, os.Interrupt)
|
|
defer func() {
|
|
signal.Stop(interruptSignalChan)
|
|
close(interruptSignalChan)
|
|
close(interruptChan)
|
|
}()
|
|
|
|
for {
|
|
select {
|
|
case <-interruptSignalChan:
|
|
// ignore if interruptChan is full
|
|
select {
|
|
case interruptChan <- struct{}{}:
|
|
default:
|
|
}
|
|
case <-closeChan:
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
|
|
return &stdOS{
|
|
closeChan: closeChan,
|
|
interruptChan: interruptChan,
|
|
}
|
|
}
|
|
|
|
func (stdOS) Platform() interp.Platform {
|
|
return interp.Platform{
|
|
OS: runtime.GOOS,
|
|
Arch: runtime.GOARCH,
|
|
}
|
|
}
|
|
|
|
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,
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
type stdoutOutput struct {
|
|
fdTerminal
|
|
os *stdOS
|
|
}
|
|
|
|
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
|
|
if o.os.rl != nil {
|
|
return o.os.rl.Write(p)
|
|
}
|
|
return os.Stdout.Write(p)
|
|
}
|
|
|
|
func (o *stdOS) Stdout() interp.Output {
|
|
return stdoutOutput{fdTerminal: fdTerminal(os.Stdout.Fd()), os: o}
|
|
}
|
|
|
|
type stderrOutput struct {
|
|
fdTerminal
|
|
}
|
|
|
|
func (o stderrOutput) Write(p []byte) (n int, err error) { return os.Stderr.Write(p) }
|
|
|
|
func (o *stdOS) Stderr() interp.Output { return stderrOutput{fdTerminal: fdTerminal(os.Stderr.Fd())} }
|
|
|
|
func (o *stdOS) InterruptChan() chan struct{} { return o.interruptChan }
|
|
|
|
func (*stdOS) Args() []string { return os.Args }
|
|
|
|
func (*stdOS) Environ() []string { return os.Environ() }
|
|
|
|
func (*stdOS) ConfigDir() (string, error) {
|
|
p, err := os.UserConfigDir()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return filepath.Join(p, "fq"), nil
|
|
}
|
|
|
|
type stdOSFS struct{}
|
|
|
|
func (stdOSFS) Open(name string) (fs.File, error) { return os.Open(name) }
|
|
|
|
func (*stdOS) FS() fs.FS { return stdOSFS{} }
|
|
|
|
func (o *stdOS) Readline(opts interp.ReadlineOpts) (string, error) {
|
|
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")
|
|
_ = 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 {
|
|
o.rl.Config.AutoComplete = autoCompleterFn(func(line []rune, pos int) (newLine [][]rune, length int) {
|
|
names, shared := opts.CompleteFn(string(line), pos)
|
|
var runeNames [][]rune
|
|
for _, name := range names {
|
|
runeNames = append(runeNames, []rune(name[shared:]))
|
|
}
|
|
|
|
return runeNames, shared
|
|
})
|
|
}
|
|
|
|
o.rl.SetPrompt(opts.Prompt)
|
|
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
|
|
}
|
|
|
|
func (o *stdOS) History() ([]string, error) {
|
|
// 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
|
|
}
|
|
|
|
func (o *stdOS) Close() error {
|
|
// 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)
|
|
return nil
|
|
}
|
|
|
|
func Main(r *interp.Registry, version string) {
|
|
os.Exit(func() int {
|
|
defer maybeProfile()()
|
|
maybeLogFile()
|
|
|
|
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 {
|
|
if ex, ok := err.(interp.Exiter); ok { //nolint:errorlint
|
|
return ex.ExitCode()
|
|
}
|
|
return 1
|
|
}
|
|
|
|
return 0
|
|
}())
|
|
}
|