diff --git a/eval/builtin_fn.go b/eval/builtin_fn.go index 8b9d7d50..582b7107 100644 --- a/eval/builtin_fn.go +++ b/eval/builtin_fn.go @@ -3,33 +3,18 @@ package eval // Builtin functions. import ( - "bytes" - "encoding/json" "errors" "fmt" - "io" - "io/ioutil" - "math" "math/rand" "net" - "os" - "os/exec" - "path/filepath" - "regexp" "runtime" - "strconv" - "strings" - "sync" "time" "unsafe" - "github.com/elves/elvish/store/storedefs" "github.com/elves/elvish/util" "github.com/xiaq/persistent/hash" ) -var builtinFns []*BuiltinFn - // BuiltinFn is a builtin function. type BuiltinFn struct { Name string @@ -64,172 +49,33 @@ func (b *BuiltinFn) Call(ec *EvalCtx, args []Value, opts map[string]Value) { b.Impl(ec, args, opts) } +var builtinFns []*BuiltinFn + +func addToBuiltinFns(moreFns []*BuiltinFn) { + builtinFns = append(builtinFns, moreFns...) +} + +// Builtins that have not been put into their own groups go here. + +var ErrArgs = errors.New("args error") + func init() { - // Needed to work around init loop. - builtinFns = []*BuiltinFn{ - // Trivial builtin + addToBuiltinFns([]*BuiltinFn{ {"nop", nop}, - // Introspection {"kind-of", kindOf}, - // Generic identity and equality {"is", is}, {"eq", eq}, {"not-eq", notEq}, - // Value output - {"put", put}, - - // Bytes output - {"print", print}, - {"echo", echo}, - {"pprint", pprint}, - {"repr", repr}, - - // Bytes to value - {"slurp", slurp}, - {"from-lines", fromLines}, - {"from-json", fromJSON}, - - // Value to bytes - {"to-lines", toLines}, - {"to-json", toJSON}, - - // Execution control - {"run-parallel", runParallel}, - - // Exception and control - {"fail", fail}, - {"multi-error", multiErrorFn}, - {"return", returnFn}, - {"break", breakFn}, - {"continue", continueFn}, - - // Misc functional {"constantly", constantly}, - // Misc shell basic {"-source", source}, - // Iterations. - {"each", each}, - {"peach", peach}, - {"repeat", repeat}, - - // Container primitives. - {"assoc", assoc}, - {"dissoc", dissoc}, - - // Sequence primitives - {"explode", explode}, - {"all", all}, - {"take", take}, - {"drop", drop}, - {"range", rangeFn}, - {"count", count}, - {"has-key", hasKey}, - {"has-value", hasValue}, - - // String - {"to-string", toString}, - {"joins", joins}, - {"splits", splits}, - {"replaces", replaces}, - - // String operations - {"ord", ord}, - {"base", base}, - {"wcswidth", wcswidth}, - {"-override-wcwidth", overrideWcwidth}, - - // Map operations - {"keys", keys}, - - // String predicates - {"has-prefix", hasPrefix}, - {"has-suffix", hasSuffix}, - - // String comparison - {"s", - wrapStrCompare(func(a, b string) bool { return a > b })}, - {">=s", - wrapStrCompare(func(a, b string) bool { return a >= b })}, - - // eawk - {"eawk", eawk}, - - // Directory - {"cd", cd}, - {"dir-history", dirs}, - - // Path - {"path-abs", WrapStringToStringError(filepath.Abs)}, - {"path-base", WrapStringToString(filepath.Base)}, - {"path-clean", WrapStringToString(filepath.Clean)}, - {"path-dir", WrapStringToString(filepath.Dir)}, - {"path-ext", WrapStringToString(filepath.Ext)}, - {"eval-symlinks", WrapStringToStringError(filepath.EvalSymlinks)}, - {"tilde-abbr", tildeAbbr}, - - // File types - {"-is-dir", isDir}, - - // Boolean operations {"bool", boolFn}, {"not", not}, - // Arithmetics - {"+", plus}, - {"-", minus}, - {"*", times}, - {"/", slash}, - {"^", pow}, - {"%", mod}, - - // Random - {"rand", randFn}, - {"randint", randint}, - - // Numerical comparison - {"<", - wrapNumCompare(func(a, b float64) bool { return a < b })}, - {"<=", - wrapNumCompare(func(a, b float64) bool { return a <= b })}, - {"==", - wrapNumCompare(func(a, b float64) bool { return a == b })}, - {"!=", - wrapNumCompare(func(a, b float64) bool { return a != b })}, - {">", - wrapNumCompare(func(a, b float64) bool { return a > b })}, - {">=", - wrapNumCompare(func(a, b float64) bool { return a >= b })}, - - // Command resolution - {"resolve", resolveFn}, - {"has-external", hasExternal}, - {"search-external", searchExternal}, - - // File and pipe - {"fopen", fopen}, - {"fclose", fclose}, - {"pipe", pipe}, - {"prclose", prclose}, - {"pwclose", pwclose}, - - // Process control - {"fg", fg}, - {"exec", execFn}, - {"exit", exit}, - // Time {"esleep", sleep}, {"-time", _time}, @@ -244,90 +90,11 @@ func init() { {"-src-name", _getSrcName}, {"-ifaddrs", _ifaddrs}, - } + }) // For rand and randint. rand.Seed(time.Now().UTC().UnixNano()) } -// Errors thrown by builtins. -var ( - ErrArgs = errors.New("args error") - ErrInput = errors.New("input error") - ErrStoreNotConnected = errors.New("store not connected") - ErrNoMatchingDir = errors.New("no matching directory") - ErrNotInSameGroup = errors.New("not in the same process group") -) - -func WrapStringToString(f func(string) string) BuiltinFnImpl { - return func(ec *EvalCtx, args []Value, opts map[string]Value) { - TakeNoOpt(opts) - s := mustGetOneString(args) - ec.ports[1].Chan <- String(f(s)) - } -} - -func WrapStringToStringError(f func(string) (string, error)) BuiltinFnImpl { - return func(ec *EvalCtx, args []Value, opts map[string]Value) { - TakeNoOpt(opts) - s := mustGetOneString(args) - result, err := f(s) - maybeThrow(err) - ec.ports[1].Chan <- String(result) - } -} - -func wrapStrCompare(cmp func(a, b string) bool) BuiltinFnImpl { - return func(ec *EvalCtx, args []Value, opts map[string]Value) { - TakeNoOpt(opts) - for _, a := range args { - if _, ok := a.(String); !ok { - throw(ErrArgs) - } - } - result := true - for i := 0; i < len(args)-1; i++ { - if !cmp(string(args[i].(String)), string(args[i+1].(String))) { - result = false - break - } - } - ec.OutputChan() <- Bool(result) - } -} - -func wrapNumCompare(cmp func(a, b float64) bool) BuiltinFnImpl { - return func(ec *EvalCtx, args []Value, opts map[string]Value) { - TakeNoOpt(opts) - floats := make([]float64, len(args)) - for i, a := range args { - f, err := toFloat(a) - maybeThrow(err) - floats[i] = f - } - result := true - for i := 0; i < len(floats)-1; i++ { - if !cmp(floats[i], floats[i+1]) { - result = false - break - } - } - ec.OutputChan() <- Bool(result) - } -} - -var errMustBeOneString = errors.New("must be one string argument") - -func mustGetOneString(args []Value) string { - if len(args) != 1 { - throw(errMustBeOneString) - } - s, ok := args[0].(String) - if !ok { - throw(errMustBeOneString) - } - return string(s) -} - func nop(ec *EvalCtx, args []Value, opts map[string]Value) { } @@ -375,182 +142,6 @@ func notEq(ec *EvalCtx, args []Value, opts map[string]Value) { ec.OutputChan() <- Bool(result) } -func put(ec *EvalCtx, args []Value, opts map[string]Value) { - TakeNoOpt(opts) - out := ec.ports[1].Chan - for _, a := range args { - out <- a - } -} - -func print(ec *EvalCtx, args []Value, opts map[string]Value) { - var sepv String - ScanOpts(opts, OptToScan{"sep", &sepv, String(" ")}) - - out := ec.ports[1].File - sep := string(sepv) - for i, arg := range args { - if i > 0 { - out.WriteString(sep) - } - out.WriteString(ToString(arg)) - } -} - -func echo(ec *EvalCtx, args []Value, opts map[string]Value) { - print(ec, args, opts) - ec.ports[1].File.WriteString("\n") -} - -func pprint(ec *EvalCtx, args []Value, opts map[string]Value) { - TakeNoOpt(opts) - out := ec.ports[1].File - for _, arg := range args { - out.WriteString(arg.Repr(0)) - out.WriteString("\n") - } -} - -func repr(ec *EvalCtx, args []Value, opts map[string]Value) { - TakeNoOpt(opts) - out := ec.ports[1].File - for i, arg := range args { - if i > 0 { - out.WriteString(" ") - } - out.WriteString(arg.Repr(NoPretty)) - } - out.WriteString("\n") -} - -func slurp(ec *EvalCtx, args []Value, opts map[string]Value) { - TakeNoArg(args) - TakeNoOpt(opts) - - in := ec.ports[0].File - out := ec.ports[1].Chan - - all, err := ioutil.ReadAll(in) - maybeThrow(err) - out <- String(string(all)) -} - -func fromLines(ec *EvalCtx, args []Value, opts map[string]Value) { - TakeNoArg(args) - TakeNoOpt(opts) - - in := ec.ports[0].File - out := ec.ports[1].Chan - - linesToChan(in, out) -} - -// fromJSON parses a stream of JSON data into Value's. -func fromJSON(ec *EvalCtx, args []Value, opts map[string]Value) { - TakeNoArg(args) - TakeNoOpt(opts) - - in := ec.ports[0].File - out := ec.ports[1].Chan - - dec := json.NewDecoder(in) - var v interface{} - for { - err := dec.Decode(&v) - if err != nil { - if err == io.EOF { - return - } - throw(err) - } - out <- FromJSONInterface(v) - } -} - -func toLines(ec *EvalCtx, args []Value, opts map[string]Value) { - iterate := ScanArgsOptionalInput(ec, args) - TakeNoOpt(opts) - - out := ec.ports[1].File - - iterate(func(v Value) { - fmt.Fprintln(out, ToString(v)) - }) -} - -// toJSON converts a stream of Value's to JSON data. -func toJSON(ec *EvalCtx, args []Value, opts map[string]Value) { - iterate := ScanArgsOptionalInput(ec, args) - TakeNoOpt(opts) - - out := ec.ports[1].File - - enc := json.NewEncoder(out) - iterate(func(v Value) { - err := enc.Encode(v) - maybeThrow(err) - }) -} - -func runParallel(ec *EvalCtx, args []Value, opts map[string]Value) { - var functions []CallableValue - ScanArgsVariadic(args, &functions) - TakeNoOpt(opts) - - var waitg sync.WaitGroup - waitg.Add(len(functions)) - exceptions := make([]*Exception, len(functions)) - for i, function := range functions { - go func(ec *EvalCtx, function CallableValue, exception **Exception) { - err := ec.PCall(function, NoArgs, NoOpts) - if err != nil { - *exception = err.(*Exception) - } - waitg.Done() - }(ec.fork("[run-parallel function]"), function, &exceptions[i]) - } - - waitg.Wait() - maybeThrow(ComposeExceptionsFromPipeline(exceptions)) -} - -func fail(ec *EvalCtx, args []Value, opts map[string]Value) { - var msg String - ScanArgs(args, &msg) - TakeNoOpt(opts) - - throw(errors.New(string(msg))) -} - -func multiErrorFn(ec *EvalCtx, args []Value, opts map[string]Value) { - var excs []*Exception - ScanArgsVariadic(args, &excs) - TakeNoOpt(opts) - - throw(PipelineError{excs}) -} - -func returnFn(ec *EvalCtx, args []Value, opts map[string]Value) { - TakeNoArg(args) - TakeNoOpt(opts) - - throw(Return) -} - -func breakFn(ec *EvalCtx, args []Value, opts map[string]Value) { - TakeNoArg(args) - TakeNoOpt(opts) - - throw(Break) -} - -func continueFn(ec *EvalCtx, args []Value, opts map[string]Value) { - TakeNoArg(args) - TakeNoOpt(opts) - - throw(Continue) -} - func constantly(ec *EvalCtx, args []Value, opts map[string]Value) { TakeNoOpt(opts) @@ -579,528 +170,6 @@ func source(ec *EvalCtx, args []Value, opts map[string]Value) { maybeThrow(ec.Source(string(fname))) } -// each takes a single closure and applies it to all input values. -func each(ec *EvalCtx, args []Value, opts map[string]Value) { - var f CallableValue - iterate := ScanArgsOptionalInput(ec, args, &f) - TakeNoOpt(opts) - - broken := false - iterate(func(v Value) { - if broken { - return - } - // NOTE We don't have the position range of the closure in the source. - // Ideally, it should be kept in the Closure itself. - newec := ec.fork("closure of each") - newec.ports[0] = DevNullClosedChan - ex := newec.PCall(f, []Value{v}, NoOpts) - ClosePorts(newec.ports) - - if ex != nil { - switch ex.(*Exception).Cause { - case nil, Continue: - // nop - case Break: - broken = true - default: - throw(ex) - } - } - }) -} - -// peach takes a single closure and applies it to all input values in parallel. -func peach(ec *EvalCtx, args []Value, opts map[string]Value) { - var f CallableValue - iterate := ScanArgsOptionalInput(ec, args, &f) - TakeNoOpt(opts) - - var w sync.WaitGroup - broken := false - var err error - iterate(func(v Value) { - if broken || err != nil { - return - } - w.Add(1) - go func() { - // NOTE We don't have the position range of the closure in the source. - // Ideally, it should be kept in the Closure itself. - newec := ec.fork("closure of each") - newec.ports[0] = DevNullClosedChan - ex := newec.PCall(f, []Value{v}, NoOpts) - ClosePorts(newec.ports) - - if ex != nil { - switch ex.(*Exception).Cause { - case nil, Continue: - // nop - case Break: - broken = true - default: - err = ex - } - } - w.Done() - }() - }) - w.Wait() - maybeThrow(err) -} - -func repeat(ec *EvalCtx, args []Value, opts map[string]Value) { - var ( - n int - v Value - ) - ScanArgs(args, &n, &v) - TakeNoOpt(opts) - - out := ec.OutputChan() - for i := 0; i < n; i++ { - out <- v - } -} - -func assoc(ec *EvalCtx, args []Value, opts map[string]Value) { - var ( - a Assocer - k, v Value - ) - ScanArgs(args, &a, &k, &v) - TakeNoOpt(opts) - ec.OutputChan() <- a.Assoc(k, v) -} - -func dissoc(ec *EvalCtx, args []Value, opts map[string]Value) { - var ( - a Dissocer - k Value - ) - ScanArgs(args, &a, &k) - TakeNoOpt(opts) - ec.OutputChan() <- a.Dissoc(k) -} - -// explode puts each element of the argument. -func explode(ec *EvalCtx, args []Value, opts map[string]Value) { - var v IterableValue - ScanArgs(args, &v) - TakeNoOpt(opts) - - out := ec.ports[1].Chan - v.Iterate(func(e Value) bool { - out <- e - return true - }) -} - -const allBufferSize = 4096 - -func all(ec *EvalCtx, args []Value, opts map[string]Value) { - TakeNoArg(args) - TakeNoOpt(opts) - - valuesDone := make(chan struct{}) - go func() { - for input := range ec.ports[0].Chan { - ec.ports[1].Chan <- input - } - close(valuesDone) - }() - _, err := io.Copy(ec.ports[1].File, ec.ports[0].File) - <-valuesDone - if err != nil { - throwf("cannot copy byte input: %s", err) - } -} - -func take(ec *EvalCtx, args []Value, opts map[string]Value) { - var n int - iterate := ScanArgsOptionalInput(ec, args, &n) - TakeNoOpt(opts) - - out := ec.ports[1].Chan - i := 0 - iterate(func(v Value) { - if i < n { - out <- v - } - i++ - }) -} - -func drop(ec *EvalCtx, args []Value, opts map[string]Value) { - var n int - iterate := ScanArgsOptionalInput(ec, args, &n) - TakeNoOpt(opts) - - out := ec.ports[1].Chan - i := 0 - iterate(func(v Value) { - if i >= n { - out <- v - } - i++ - }) -} - -func rangeFn(ec *EvalCtx, args []Value, opts map[string]Value) { - var step float64 - ScanOpts(opts, OptToScan{"step", &step, String("1")}) - - var lower, upper float64 - var err error - - switch len(args) { - case 1: - upper, err = toFloat(args[0]) - maybeThrow(err) - case 2: - lower, err = toFloat(args[0]) - maybeThrow(err) - upper, err = toFloat(args[1]) - maybeThrow(err) - default: - throw(ErrArgs) - } - - out := ec.ports[1].Chan - for f := lower; f < upper; f += step { - out <- floatToString(f) - } -} - -func hasValue(ec *EvalCtx, args []Value, opts map[string]Value) { - TakeNoOpt(opts) - - var container, value Value - var found bool - - ScanArgs(args, &container, &value) - - switch container := container.(type) { - case Iterable: - container.Iterate(func(v Value) bool { - found = (v == value) - return !found - }) - case MapLike: - container.IterateKey(func(v Value) bool { - found = (container.IndexOne(v) == value) - return !found - }) - default: - throw(fmt.Errorf("argument of type '%s' is not iterable", container.Kind())) - } - - ec.ports[1].Chan <- Bool(found) -} - -func hasKey(ec *EvalCtx, args []Value, opts map[string]Value) { - TakeNoOpt(opts) - - var container, key Value - var found bool - - ScanArgs(args, &container, &key) - - switch container := container.(type) { - case HasKeyer: - found = container.HasKey(key) - case Lener: - // XXX(xiaq): Not all types that implement Lener have numerical indices - err := util.PCall(func() { - ParseAndFixListIndex(ToString(key), container.Len()) - }) - found = (err == nil) - default: - throw(fmt.Errorf("couldn't get key or index of type '%s'", container.Kind())) - } - - ec.ports[1].Chan <- Bool(found) -} - -func count(ec *EvalCtx, args []Value, opts map[string]Value) { - TakeNoOpt(opts) - - var n int - switch len(args) { - case 0: - // Count inputs. - ec.IterateInputs(func(Value) { - n++ - }) - case 1: - // Get length of argument. - v := args[0] - if lener, ok := v.(Lener); ok { - n = lener.Len() - } else if iterator, ok := v.(Iterable); ok { - iterator.Iterate(func(Value) bool { - n++ - return true - }) - } else { - throw(fmt.Errorf("cannot get length of a %s", v.Kind())) - } - default: - throw(errors.New("want 0 or 1 argument")) - } - ec.ports[1].Chan <- String(strconv.Itoa(n)) -} - -// toString converts all arguments to strings. -func toString(ec *EvalCtx, args []Value, opts map[string]Value) { - TakeNoOpt(opts) - out := ec.OutputChan() - for _, a := range args { - out <- String(ToString(a)) - } -} - -// joins joins all input strings with a delimiter. -func joins(ec *EvalCtx, args []Value, opts map[string]Value) { - var sepv String - iterate := ScanArgsOptionalInput(ec, args, &sepv) - sep := string(sepv) - TakeNoOpt(opts) - - var buf bytes.Buffer - iterate(func(v Value) { - if s, ok := v.(String); ok { - if buf.Len() > 0 { - buf.WriteString(sep) - } - buf.WriteString(string(s)) - } else { - throwf("join wants string input, got %s", v.Kind()) - } - }) - out := ec.ports[1].Chan - out <- String(buf.String()) -} - -// splits splits an argument strings by a delimiter and writes all pieces. -func splits(ec *EvalCtx, args []Value, opts map[string]Value) { - var s, sep String - ScanArgs(args, &sep, &s) - TakeNoOpt(opts) - - out := ec.ports[1].Chan - parts := strings.Split(string(s), string(sep)) - for _, p := range parts { - out <- String(p) - } -} - -func replaces(ec *EvalCtx, args []Value, opts map[string]Value) { - var ( - old, repl, s String - optMax int - ) - ScanArgs(args, &old, &repl, &s) - ScanOpts(opts, OptToScan{"max", &optMax, String("-1")}) - - ec.ports[1].Chan <- String(strings.Replace(string(s), string(old), string(repl), optMax)) -} - -func ord(ec *EvalCtx, args []Value, opts map[string]Value) { - var s String - ScanArgs(args, &s) - TakeNoOpt(opts) - - out := ec.ports[1].Chan - for _, r := range s { - out <- String("0x" + strconv.FormatInt(int64(r), 16)) - } -} - -// ErrBadBase is thrown by the "base" builtin if the base is smaller than 2 or -// greater than 36. -var ErrBadBase = errors.New("bad base") - -func base(ec *EvalCtx, args []Value, opts map[string]Value) { - var ( - b int - nums []int - ) - ScanArgsVariadic(args, &b, &nums) - TakeNoOpt(opts) - - if b < 2 || b > 36 { - throw(ErrBadBase) - } - - out := ec.ports[1].Chan - - for _, num := range nums { - out <- String(strconv.FormatInt(int64(num), b)) - } -} - -func wcswidth(ec *EvalCtx, args []Value, opts map[string]Value) { - var s String - ScanArgs(args, &s) - TakeNoOpt(opts) - - out := ec.ports[1].Chan - out <- String(strconv.Itoa(util.Wcswidth(string(s)))) -} - -func overrideWcwidth(ec *EvalCtx, args []Value, opts map[string]Value) { - var ( - s String - w int - ) - ScanArgs(args, &s, &w) - TakeNoOpt(opts) - - r, err := toRune(s) - maybeThrow(err) - util.OverrideWcwidth(r, w) -} - -func keys(ec *EvalCtx, args []Value, opts map[string]Value) { - TakeNoOpt(opts) - - var iter IterateKeyer - ScanArgs(args, &iter) - - out := ec.ports[1].Chan - - iter.IterateKey(func(v Value) bool { - out <- v - return true - }) -} - -func hasPrefix(ec *EvalCtx, args []Value, opts map[string]Value) { - var s, prefix String - ScanArgs(args, &s, &prefix) - TakeNoOpt(opts) - - ec.OutputChan() <- Bool(strings.HasPrefix(string(s), string(prefix))) -} - -func hasSuffix(ec *EvalCtx, args []Value, opts map[string]Value) { - var s, suffix String - ScanArgs(args, &s, &suffix) - TakeNoOpt(opts) - - ec.OutputChan() <- Bool(strings.HasSuffix(string(s), string(suffix))) -} - -var eawkWordSep = regexp.MustCompile("[ \t]+") - -// eawk takes a function. For each line in the input stream, it calls the -// function with the line and the words in the line. The words are found by -// stripping the line and splitting the line by whitespaces. The function may -// call break and continue. Overall this provides a similar functionality to -// awk, hence the name. -func eawk(ec *EvalCtx, args []Value, opts map[string]Value) { - var f CallableValue - iterate := ScanArgsOptionalInput(ec, args, &f) - TakeNoOpt(opts) - - broken := false - iterate(func(v Value) { - if broken { - return - } - line, ok := v.(String) - if !ok { - throw(ErrInput) - } - args := []Value{line} - for _, field := range eawkWordSep.Split(strings.Trim(string(line), " \t"), -1) { - args = append(args, String(field)) - } - - newec := ec.fork("fn of eawk") - // TODO: Close port 0 of newec. - ex := newec.PCall(f, args, NoOpts) - ClosePorts(newec.ports) - - if ex != nil { - switch ex.(*Exception).Cause { - case nil, Continue: - // nop - case Break: - broken = true - default: - throw(ex) - } - } - }) -} - -func cd(ec *EvalCtx, args []Value, opts map[string]Value) { - TakeNoOpt(opts) - - var dir string - if len(args) == 0 { - dir = mustGetHome("") - } else if len(args) == 1 { - dir = ToString(args[0]) - } else { - throw(ErrArgs) - } - - cdInner(dir, ec) -} - -func cdInner(dir string, ec *EvalCtx) { - maybeThrow(Chdir(dir, ec.Daemon)) -} - -var dirDescriptor = NewStructDescriptor("path", "score") - -func dirs(ec *EvalCtx, args []Value, opts map[string]Value) { - TakeNoArg(args) - TakeNoOpt(opts) - - if ec.Daemon == nil { - throw(ErrStoreNotConnected) - } - dirs, err := ec.Daemon.Dirs(storedefs.NoBlacklist) - if err != nil { - throw(errors.New("store error: " + err.Error())) - } - out := ec.ports[1].Chan - for _, dir := range dirs { - out <- &Struct{dirDescriptor, []Value{ - String(dir.Path), - floatToString(dir.Score), - }} - } -} - -func tildeAbbr(ec *EvalCtx, args []Value, opts map[string]Value) { - var pathv String - ScanArgs(args, &pathv) - path := string(pathv) - TakeNoOpt(opts) - - out := ec.ports[1].Chan - out <- String(util.TildeAbbr(path)) -} - -func isDir(ec *EvalCtx, args []Value, opts map[string]Value) { - var pathv String - ScanArgs(args, &pathv) - path := string(pathv) - TakeNoOpt(opts) - - ec.OutputChan() <- Bool(isDirInner(path)) -} - -func isDirInner(path string) bool { - fi, err := os.Stat(path) - return err == nil && fi.Mode().IsDir() -} - func boolFn(ec *EvalCtx, args []Value, opts map[string]Value) { var v Value ScanArgs(args, &v) @@ -1117,213 +186,6 @@ func not(ec *EvalCtx, args []Value, opts map[string]Value) { ec.OutputChan() <- Bool(!ToBool(v)) } -func plus(ec *EvalCtx, args []Value, opts map[string]Value) { - var nums []float64 - ScanArgsVariadic(args, &nums) - TakeNoOpt(opts) - - out := ec.ports[1].Chan - sum := 0.0 - for _, f := range nums { - sum += f - } - out <- floatToString(sum) -} - -func minus(ec *EvalCtx, args []Value, opts map[string]Value) { - var ( - sum float64 - nums []float64 - ) - ScanArgsVariadic(args, &sum, &nums) - TakeNoOpt(opts) - - out := ec.ports[1].Chan - if len(nums) == 0 { - // Unary - - sum = -sum - } else { - for _, f := range nums { - sum -= f - } - } - out <- floatToString(sum) -} - -func times(ec *EvalCtx, args []Value, opts map[string]Value) { - var nums []float64 - ScanArgsVariadic(args, &nums) - TakeNoOpt(opts) - - out := ec.ports[1].Chan - prod := 1.0 - for _, f := range nums { - prod *= f - } - out <- floatToString(prod) -} - -func slash(ec *EvalCtx, args []Value, opts map[string]Value) { - TakeNoOpt(opts) - if len(args) == 0 { - // cd / - cdInner("/", ec) - return - } - // Division - divide(ec, args, opts) -} - -func divide(ec *EvalCtx, args []Value, opts map[string]Value) { - var ( - prod float64 - nums []float64 - ) - ScanArgsVariadic(args, &prod, &nums) - TakeNoOpt(opts) - - out := ec.ports[1].Chan - for _, f := range nums { - prod /= f - } - out <- floatToString(prod) -} - -func pow(ec *EvalCtx, args []Value, opts map[string]Value) { - var b, p float64 - ScanArgs(args, &b, &p) - TakeNoOpt(opts) - - out := ec.ports[1].Chan - out <- floatToString(math.Pow(b, p)) -} - -func mod(ec *EvalCtx, args []Value, opts map[string]Value) { - var a, b int - ScanArgs(args, &a, &b) - TakeNoOpt(opts) - - out := ec.ports[1].Chan - out <- String(strconv.Itoa(a % b)) -} - -func randFn(ec *EvalCtx, args []Value, opts map[string]Value) { - TakeNoArg(args) - TakeNoOpt(opts) - - out := ec.ports[1].Chan - out <- floatToString(rand.Float64()) -} - -func randint(ec *EvalCtx, args []Value, opts map[string]Value) { - var low, high int - ScanArgs(args, &low, &high) - TakeNoOpt(opts) - - if low >= high { - throw(ErrArgs) - } - out := ec.ports[1].Chan - i := low + rand.Intn(high-low) - out <- String(strconv.Itoa(i)) -} - -func resolveFn(ec *EvalCtx, args []Value, opts map[string]Value) { - var cmd String - ScanArgs(args, &cmd) - TakeNoOpt(opts) - - out := ec.ports[1].Chan - out <- resolve(string(cmd), ec) -} - -func hasExternal(ec *EvalCtx, args []Value, opts map[string]Value) { - var cmd String - ScanArgs(args, &cmd) - TakeNoOpt(opts) - - _, err := exec.LookPath(string(cmd)) - ec.OutputChan() <- Bool(err == nil) -} - -func searchExternal(ec *EvalCtx, args []Value, opts map[string]Value) { - var cmd String - ScanArgs(args, &cmd) - TakeNoOpt(opts) - - path, err := exec.LookPath(string(cmd)) - maybeThrow(err) - - out := ec.ports[1].Chan - out <- String(path) -} - -func fopen(ec *EvalCtx, args []Value, opts map[string]Value) { - var namev String - ScanArgs(args, &namev) - name := string(namev) - TakeNoOpt(opts) - - // TODO support opening files for writing etc as well. - out := ec.ports[1].Chan - f, err := os.Open(name) - maybeThrow(err) - out <- File{f} -} - -func fclose(ec *EvalCtx, args []Value, opts map[string]Value) { - var f File - ScanArgs(args, &f) - TakeNoOpt(opts) - - maybeThrow(f.inner.Close()) -} - -func pipe(ec *EvalCtx, args []Value, opts map[string]Value) { - TakeNoArg(args) - TakeNoOpt(opts) - - r, w, err := os.Pipe() - out := ec.ports[1].Chan - maybeThrow(err) - out <- Pipe{r, w} -} - -func prclose(ec *EvalCtx, args []Value, opts map[string]Value) { - var p Pipe - ScanArgs(args, &p) - TakeNoOpt(opts) - - maybeThrow(p.r.Close()) -} - -func pwclose(ec *EvalCtx, args []Value, opts map[string]Value) { - var p Pipe - ScanArgs(args, &p) - TakeNoOpt(opts) - - maybeThrow(p.w.Close()) -} - -func exit(ec *EvalCtx, args []Value, opts map[string]Value) { - var codes []int - ScanArgsVariadic(args, &codes) - TakeNoOpt(opts) - - doexit := func(i int) { - preExit(ec) - os.Exit(i) - } - switch len(codes) { - case 0: - doexit(0) - case 1: - doexit(codes[0]) - default: - throw(ErrArgs) - } -} - func sleep(ec *EvalCtx, args []Value, opts map[string]Value) { var t float64 ScanArgs(args, &t) @@ -1397,10 +259,3 @@ func _ifaddrs(ec *EvalCtx, args []Value, opts map[string]Value) { out <- String(addr.String()) } } - -func preExit(ec *EvalCtx) { - err := ec.Daemon.Close() - if err != nil { - fmt.Fprintln(os.Stderr, err) - } -} diff --git a/eval/builtin_fn_cmd.go b/eval/builtin_fn_cmd.go new file mode 100644 index 00000000..c32125c3 --- /dev/null +++ b/eval/builtin_fn_cmd.go @@ -0,0 +1,82 @@ +package eval + +import ( + "errors" + "fmt" + "os" + "os/exec" +) + +// Command and process control. + +var ErrNotInSameGroup = errors.New("not in the same process group") + +func init() { + addToBuiltinFns([]*BuiltinFn{ + // Command resolution + {"resolve", resolveFn}, + {"has-external", hasExternal}, + {"search-external", searchExternal}, + + // Process control + {"fg", fg}, + {"exec", execFn}, + {"exit", exit}, + }) +} + +func resolveFn(ec *EvalCtx, args []Value, opts map[string]Value) { + var cmd String + ScanArgs(args, &cmd) + TakeNoOpt(opts) + + out := ec.ports[1].Chan + out <- resolve(string(cmd), ec) +} + +func hasExternal(ec *EvalCtx, args []Value, opts map[string]Value) { + var cmd String + ScanArgs(args, &cmd) + TakeNoOpt(opts) + + _, err := exec.LookPath(string(cmd)) + ec.OutputChan() <- Bool(err == nil) +} + +func searchExternal(ec *EvalCtx, args []Value, opts map[string]Value) { + var cmd String + ScanArgs(args, &cmd) + TakeNoOpt(opts) + + path, err := exec.LookPath(string(cmd)) + maybeThrow(err) + + out := ec.ports[1].Chan + out <- String(path) +} + +func exit(ec *EvalCtx, args []Value, opts map[string]Value) { + var codes []int + ScanArgsVariadic(args, &codes) + TakeNoOpt(opts) + + doexit := func(i int) { + preExit(ec) + os.Exit(i) + } + switch len(codes) { + case 0: + doexit(0) + case 1: + doexit(codes[0]) + default: + throw(ErrArgs) + } +} + +func preExit(ec *EvalCtx) { + err := ec.Daemon.Close() + if err != nil { + fmt.Fprintln(os.Stderr, err) + } +} diff --git a/eval/builtin_fn_cmd_test.go b/eval/builtin_fn_cmd_test.go new file mode 100644 index 00000000..3131d10a --- /dev/null +++ b/eval/builtin_fn_cmd_test.go @@ -0,0 +1,5 @@ +package eval + +func init() { + addToEvalTests([]evalTest{}) +} diff --git a/eval/builtin_fn_unix.go b/eval/builtin_fn_cmd_unix.go similarity index 100% rename from eval/builtin_fn_unix.go rename to eval/builtin_fn_cmd_unix.go diff --git a/eval/builtin_fn_windows.go b/eval/builtin_fn_cmd_windows.go similarity index 100% rename from eval/builtin_fn_windows.go rename to eval/builtin_fn_cmd_windows.go diff --git a/eval/builtin_fn_container.go b/eval/builtin_fn_container.go new file mode 100644 index 00000000..f29ee986 --- /dev/null +++ b/eval/builtin_fn_container.go @@ -0,0 +1,250 @@ +package eval + +import ( + "errors" + "fmt" + "io" + "strconv" + + "github.com/elves/elvish/util" +) + +// Sequence, list and maps. + +func init() { + addToBuiltinFns([]*BuiltinFn{ + {"range", rangeFn}, + {"repeat", repeat}, + {"explode", explode}, + + {"assoc", assoc}, + {"dissoc", dissoc}, + + {"all", all}, + {"take", take}, + {"drop", drop}, + + {"has-key", hasKey}, + {"has-value", hasValue}, + + {"count", count}, + + {"keys", keys}, + }) +} + +func rangeFn(ec *EvalCtx, args []Value, opts map[string]Value) { + var step float64 + ScanOpts(opts, OptToScan{"step", &step, String("1")}) + + var lower, upper float64 + var err error + + switch len(args) { + case 1: + upper, err = toFloat(args[0]) + maybeThrow(err) + case 2: + lower, err = toFloat(args[0]) + maybeThrow(err) + upper, err = toFloat(args[1]) + maybeThrow(err) + default: + throw(ErrArgs) + } + + out := ec.ports[1].Chan + for f := lower; f < upper; f += step { + out <- floatToString(f) + } +} + +func repeat(ec *EvalCtx, args []Value, opts map[string]Value) { + var ( + n int + v Value + ) + ScanArgs(args, &n, &v) + TakeNoOpt(opts) + + out := ec.OutputChan() + for i := 0; i < n; i++ { + out <- v + } +} + +// explode puts each element of the argument. +func explode(ec *EvalCtx, args []Value, opts map[string]Value) { + var v IterableValue + ScanArgs(args, &v) + TakeNoOpt(opts) + + out := ec.ports[1].Chan + v.Iterate(func(e Value) bool { + out <- e + return true + }) +} + +func assoc(ec *EvalCtx, args []Value, opts map[string]Value) { + var ( + a Assocer + k, v Value + ) + ScanArgs(args, &a, &k, &v) + TakeNoOpt(opts) + ec.OutputChan() <- a.Assoc(k, v) +} + +func dissoc(ec *EvalCtx, args []Value, opts map[string]Value) { + var ( + a Dissocer + k Value + ) + ScanArgs(args, &a, &k) + TakeNoOpt(opts) + ec.OutputChan() <- a.Dissoc(k) +} + +const allBufferSize = 4096 + +func all(ec *EvalCtx, args []Value, opts map[string]Value) { + TakeNoArg(args) + TakeNoOpt(opts) + + valuesDone := make(chan struct{}) + go func() { + for input := range ec.ports[0].Chan { + ec.ports[1].Chan <- input + } + close(valuesDone) + }() + _, err := io.Copy(ec.ports[1].File, ec.ports[0].File) + <-valuesDone + if err != nil { + throwf("cannot copy byte input: %s", err) + } +} + +func take(ec *EvalCtx, args []Value, opts map[string]Value) { + var n int + iterate := ScanArgsOptionalInput(ec, args, &n) + TakeNoOpt(opts) + + out := ec.ports[1].Chan + i := 0 + iterate(func(v Value) { + if i < n { + out <- v + } + i++ + }) +} + +func drop(ec *EvalCtx, args []Value, opts map[string]Value) { + var n int + iterate := ScanArgsOptionalInput(ec, args, &n) + TakeNoOpt(opts) + + out := ec.ports[1].Chan + i := 0 + iterate(func(v Value) { + if i >= n { + out <- v + } + i++ + }) +} + +func hasValue(ec *EvalCtx, args []Value, opts map[string]Value) { + TakeNoOpt(opts) + + var container, value Value + var found bool + + ScanArgs(args, &container, &value) + + switch container := container.(type) { + case Iterable: + container.Iterate(func(v Value) bool { + found = (v == value) + return !found + }) + case MapLike: + container.IterateKey(func(v Value) bool { + found = (container.IndexOne(v) == value) + return !found + }) + default: + throw(fmt.Errorf("argument of type '%s' is not iterable", container.Kind())) + } + + ec.ports[1].Chan <- Bool(found) +} + +func hasKey(ec *EvalCtx, args []Value, opts map[string]Value) { + TakeNoOpt(opts) + + var container, key Value + var found bool + + ScanArgs(args, &container, &key) + + switch container := container.(type) { + case HasKeyer: + found = container.HasKey(key) + case Lener: + // XXX(xiaq): Not all types that implement Lener have numerical indices + err := util.PCall(func() { + ParseAndFixListIndex(ToString(key), container.Len()) + }) + found = (err == nil) + default: + throw(fmt.Errorf("couldn't get key or index of type '%s'", container.Kind())) + } + + ec.ports[1].Chan <- Bool(found) +} + +func count(ec *EvalCtx, args []Value, opts map[string]Value) { + TakeNoOpt(opts) + + var n int + switch len(args) { + case 0: + // Count inputs. + ec.IterateInputs(func(Value) { + n++ + }) + case 1: + // Get length of argument. + v := args[0] + if lener, ok := v.(Lener); ok { + n = lener.Len() + } else if iterator, ok := v.(Iterable); ok { + iterator.Iterate(func(Value) bool { + n++ + return true + }) + } else { + throw(fmt.Errorf("cannot get length of a %s", v.Kind())) + } + default: + throw(errors.New("want 0 or 1 argument")) + } + ec.ports[1].Chan <- String(strconv.Itoa(n)) +} + +func keys(ec *EvalCtx, args []Value, opts map[string]Value) { + TakeNoOpt(opts) + + var iter IterateKeyer + ScanArgs(args, &iter) + + out := ec.ports[1].Chan + + iter.IterateKey(func(v Value) bool { + out <- v + return true + }) +} diff --git a/eval/builtin_fn_container_test.go b/eval/builtin_fn_container_test.go new file mode 100644 index 00000000..ae016dc5 --- /dev/null +++ b/eval/builtin_fn_container_test.go @@ -0,0 +1,44 @@ +package eval + +func init() { + addToEvalTests([]evalTest{ + {`range 3`, want{out: strs("0", "1", "2")}}, + {`range 1 3`, want{out: strs("1", "2")}}, + {`range 0 10 &step=3`, want{out: strs("0", "3", "6", "9")}}, + {`repeat 4 foo`, want{out: strs("foo", "foo", "foo", "foo")}}, + {`explode [foo bar]`, want{out: strs("foo", "bar")}}, + + {`put (assoc [0] 0 zero)[0]`, want{out: strs("zero")}}, + {`put (assoc [&] k v)[k]`, want{out: strs("v")}}, + {`put (assoc [&k=v] k v2)[k]`, want{out: strs("v2")}}, + {`has-key (dissoc [&k=v] k) k`, want{out: bools(false)}}, + + {`put foo bar | all`, want{out: strs("foo", "bar")}}, + {`echo foobar | all`, want{bytesOut: []byte("foobar\n")}}, + {`{ put foo bar; echo foobar } | all`, + want{out: strs("foo", "bar"), bytesOut: []byte("foobar\n")}}, + {`range 100 | take 2`, want{out: strs("0", "1")}}, + {`range 100 | drop 98`, want{out: strs("98", "99")}}, + + {`has-key [foo bar] 0`, want{out: bools(true)}}, + {`has-key [foo bar] 0:1`, want{out: bools(true)}}, + {`has-key [foo bar] 0:20`, want{out: bools(false)}}, + {`has-key [&lorem=ipsum &foo=bar] lorem`, want{out: bools(true)}}, + {`has-key [&lorem=ipsum &foo=bar] loremwsq`, want{out: bools(false)}}, + {`has-value [&lorem=ipsum &foo=bar] lorem`, want{out: bools(false)}}, + {`has-value [&lorem=ipsum &foo=bar] bar`, want{out: bools(true)}}, + {`has-value [foo bar] bar`, want{out: bools(true)}}, + {`has-value [foo bar] badehose`, want{out: bools(false)}}, + {`has-value "foo" o`, want{out: bools(true)}}, + {`has-value "foo" d`, want{out: bools(false)}}, + + {`range 100 | count`, want{out: strs("100")}}, + {`count [(range 100)]`, want{out: strs("100")}}, + + {`keys [&]`, wantNothing}, + {`keys [&a=foo]`, want{out: strs("a")}}, + // Windows does not have an external sort command. Disabled until we have a + // builtin sort command. + // {`keys [&a=foo &b=bar] | each echo | sort | each put`, want{out: strs("a", "b")}}, + }) +} diff --git a/eval/builtin_fn_flow.go b/eval/builtin_fn_flow.go new file mode 100644 index 00000000..752b4c8c --- /dev/null +++ b/eval/builtin_fn_flow.go @@ -0,0 +1,154 @@ +package eval + +import ( + "errors" + "sync" +) + +// Flow control. + +func init() { + addToBuiltinFns([]*BuiltinFn{ + {"run-parallel", runParallel}, + + // Iterations. + {"each", each}, + {"peach", peach}, + + // Exception and control + {"fail", fail}, + {"multi-error", multiErrorFn}, + {"return", returnFn}, + {"break", breakFn}, + {"continue", continueFn}, + }) +} + +func runParallel(ec *EvalCtx, args []Value, opts map[string]Value) { + var functions []CallableValue + ScanArgsVariadic(args, &functions) + TakeNoOpt(opts) + + var waitg sync.WaitGroup + waitg.Add(len(functions)) + exceptions := make([]*Exception, len(functions)) + for i, function := range functions { + go func(ec *EvalCtx, function CallableValue, exception **Exception) { + err := ec.PCall(function, NoArgs, NoOpts) + if err != nil { + *exception = err.(*Exception) + } + waitg.Done() + }(ec.fork("[run-parallel function]"), function, &exceptions[i]) + } + + waitg.Wait() + maybeThrow(ComposeExceptionsFromPipeline(exceptions)) +} + +// each takes a single closure and applies it to all input values. +func each(ec *EvalCtx, args []Value, opts map[string]Value) { + var f CallableValue + iterate := ScanArgsOptionalInput(ec, args, &f) + TakeNoOpt(opts) + + broken := false + iterate(func(v Value) { + if broken { + return + } + // NOTE We don't have the position range of the closure in the source. + // Ideally, it should be kept in the Closure itself. + newec := ec.fork("closure of each") + newec.ports[0] = DevNullClosedChan + ex := newec.PCall(f, []Value{v}, NoOpts) + ClosePorts(newec.ports) + + if ex != nil { + switch ex.(*Exception).Cause { + case nil, Continue: + // nop + case Break: + broken = true + default: + throw(ex) + } + } + }) +} + +// peach takes a single closure and applies it to all input values in parallel. +func peach(ec *EvalCtx, args []Value, opts map[string]Value) { + var f CallableValue + iterate := ScanArgsOptionalInput(ec, args, &f) + TakeNoOpt(opts) + + var w sync.WaitGroup + broken := false + var err error + iterate(func(v Value) { + if broken || err != nil { + return + } + w.Add(1) + go func() { + // NOTE We don't have the position range of the closure in the source. + // Ideally, it should be kept in the Closure itself. + newec := ec.fork("closure of each") + newec.ports[0] = DevNullClosedChan + ex := newec.PCall(f, []Value{v}, NoOpts) + ClosePorts(newec.ports) + + if ex != nil { + switch ex.(*Exception).Cause { + case nil, Continue: + // nop + case Break: + broken = true + default: + err = ex + } + } + w.Done() + }() + }) + w.Wait() + maybeThrow(err) +} + +func fail(ec *EvalCtx, args []Value, opts map[string]Value) { + var msg String + ScanArgs(args, &msg) + TakeNoOpt(opts) + + throw(errors.New(string(msg))) +} + +func multiErrorFn(ec *EvalCtx, args []Value, opts map[string]Value) { + var excs []*Exception + ScanArgsVariadic(args, &excs) + TakeNoOpt(opts) + + throw(PipelineError{excs}) +} + +func returnFn(ec *EvalCtx, args []Value, opts map[string]Value) { + TakeNoArg(args) + TakeNoOpt(opts) + + throw(Return) +} + +func breakFn(ec *EvalCtx, args []Value, opts map[string]Value) { + TakeNoArg(args) + TakeNoOpt(opts) + + throw(Break) +} + +func continueFn(ec *EvalCtx, args []Value, opts map[string]Value) { + TakeNoArg(args) + TakeNoOpt(opts) + + throw(Continue) +} diff --git a/eval/builtin_fn_flow_test.go b/eval/builtin_fn_flow_test.go new file mode 100644 index 00000000..dda7e353 --- /dev/null +++ b/eval/builtin_fn_flow_test.go @@ -0,0 +1,20 @@ +package eval + +func init() { + addToEvalTests([]evalTest{ + {`run-parallel { put lorem } { echo ipsum }`, + want{out: strs("lorem"), bytesOut: []byte("ipsum\n")}}, + + {`put 1 233 | each put`, want{out: strs("1", "233")}}, + {`echo "1\n233" | each put`, want{out: strs("1", "233")}}, + {`each put [1 233]`, want{out: strs("1", "233")}}, + {`range 10 | each [x]{ if (== $x 4) { break }; put $x }`, + want{out: strs("0", "1", "2", "3")}}, + {`range 10 | each [x]{ if (== $x 4) { fail haha }; put $x }`, + want{out: strs("0", "1", "2", "3"), err: errAny}}, + // TODO: test peach + + {`fail haha`, want{err: errAny}}, + {`return`, want{err: Return}}, + }) +} diff --git a/eval/builtin_fn_fs.go b/eval/builtin_fn_fs.go new file mode 100644 index 00000000..2dbfe340 --- /dev/null +++ b/eval/builtin_fn_fs.go @@ -0,0 +1,130 @@ +package eval + +import ( + "errors" + "os" + "path/filepath" + + "github.com/elves/elvish/store/storedefs" + "github.com/elves/elvish/util" +) + +// Filesystem. + +var ErrStoreNotConnected = errors.New("store not connected") + +func init() { + addToBuiltinFns([]*BuiltinFn{ + // Directory + {"cd", cd}, + {"dir-history", dirs}, + + // Path + {"path-abs", WrapStringToStringError(filepath.Abs)}, + {"path-base", WrapStringToString(filepath.Base)}, + {"path-clean", WrapStringToString(filepath.Clean)}, + {"path-dir", WrapStringToString(filepath.Dir)}, + {"path-ext", WrapStringToString(filepath.Ext)}, + {"eval-symlinks", WrapStringToStringError(filepath.EvalSymlinks)}, + {"tilde-abbr", tildeAbbr}, + + // File types + {"-is-dir", isDir}, + }) +} + +func WrapStringToString(f func(string) string) BuiltinFnImpl { + return func(ec *EvalCtx, args []Value, opts map[string]Value) { + TakeNoOpt(opts) + s := mustGetOneString(args) + ec.ports[1].Chan <- String(f(s)) + } +} + +func WrapStringToStringError(f func(string) (string, error)) BuiltinFnImpl { + return func(ec *EvalCtx, args []Value, opts map[string]Value) { + TakeNoOpt(opts) + s := mustGetOneString(args) + result, err := f(s) + maybeThrow(err) + ec.ports[1].Chan <- String(result) + } +} + +var errMustBeOneString = errors.New("must be one string argument") + +func mustGetOneString(args []Value) string { + if len(args) != 1 { + throw(errMustBeOneString) + } + s, ok := args[0].(String) + if !ok { + throw(errMustBeOneString) + } + return string(s) +} + +func cd(ec *EvalCtx, args []Value, opts map[string]Value) { + TakeNoOpt(opts) + + var dir string + if len(args) == 0 { + dir = mustGetHome("") + } else if len(args) == 1 { + dir = ToString(args[0]) + } else { + throw(ErrArgs) + } + + cdInner(dir, ec) +} + +func cdInner(dir string, ec *EvalCtx) { + maybeThrow(Chdir(dir, ec.Daemon)) +} + +var dirDescriptor = NewStructDescriptor("path", "score") + +func dirs(ec *EvalCtx, args []Value, opts map[string]Value) { + TakeNoArg(args) + TakeNoOpt(opts) + + if ec.Daemon == nil { + throw(ErrStoreNotConnected) + } + dirs, err := ec.Daemon.Dirs(storedefs.NoBlacklist) + if err != nil { + throw(errors.New("store error: " + err.Error())) + } + out := ec.ports[1].Chan + for _, dir := range dirs { + out <- &Struct{dirDescriptor, []Value{ + String(dir.Path), + floatToString(dir.Score), + }} + } +} + +func tildeAbbr(ec *EvalCtx, args []Value, opts map[string]Value) { + var pathv String + ScanArgs(args, &pathv) + path := string(pathv) + TakeNoOpt(opts) + + out := ec.ports[1].Chan + out <- String(util.TildeAbbr(path)) +} + +func isDir(ec *EvalCtx, args []Value, opts map[string]Value) { + var pathv String + ScanArgs(args, &pathv) + path := string(pathv) + TakeNoOpt(opts) + + ec.OutputChan() <- Bool(isDirInner(path)) +} + +func isDirInner(path string) bool { + fi, err := os.Stat(path) + return err == nil && fi.Mode().IsDir() +} diff --git a/eval/builtin_fn_fs_test.go b/eval/builtin_fn_fs_test.go new file mode 100644 index 00000000..f3339673 --- /dev/null +++ b/eval/builtin_fn_fs_test.go @@ -0,0 +1,7 @@ +package eval + +func init() { + addToEvalTests([]evalTest{ + {`path-base a/b/c.png`, want{out: strs("c.png")}}, + }) +} diff --git a/eval/builtin_fn_io.go b/eval/builtin_fn_io.go new file mode 100644 index 00000000..134bf2be --- /dev/null +++ b/eval/builtin_fn_io.go @@ -0,0 +1,204 @@ +package eval + +import ( + "encoding/json" + "fmt" + "io" + "io/ioutil" + "os" +) + +// Input and output. + +func init() { + addToBuiltinFns([]*BuiltinFn{ + // Value output + {"put", put}, + + // Bytes output + {"print", print}, + {"echo", echo}, + {"pprint", pprint}, + {"repr", repr}, + + // Bytes to value + {"slurp", slurp}, + {"from-lines", fromLines}, + {"from-json", fromJSON}, + + // Value to bytes + {"to-lines", toLines}, + {"to-json", toJSON}, + + // File and pipe + {"fopen", fopen}, + {"fclose", fclose}, + {"pipe", pipe}, + {"prclose", prclose}, + {"pwclose", pwclose}, + }) +} + +func put(ec *EvalCtx, args []Value, opts map[string]Value) { + TakeNoOpt(opts) + out := ec.ports[1].Chan + for _, a := range args { + out <- a + } +} + +func print(ec *EvalCtx, args []Value, opts map[string]Value) { + var sepv String + ScanOpts(opts, OptToScan{"sep", &sepv, String(" ")}) + + out := ec.ports[1].File + sep := string(sepv) + for i, arg := range args { + if i > 0 { + out.WriteString(sep) + } + out.WriteString(ToString(arg)) + } +} + +func echo(ec *EvalCtx, args []Value, opts map[string]Value) { + print(ec, args, opts) + ec.ports[1].File.WriteString("\n") +} + +func pprint(ec *EvalCtx, args []Value, opts map[string]Value) { + TakeNoOpt(opts) + out := ec.ports[1].File + for _, arg := range args { + out.WriteString(arg.Repr(0)) + out.WriteString("\n") + } +} + +func repr(ec *EvalCtx, args []Value, opts map[string]Value) { + TakeNoOpt(opts) + out := ec.ports[1].File + for i, arg := range args { + if i > 0 { + out.WriteString(" ") + } + out.WriteString(arg.Repr(NoPretty)) + } + out.WriteString("\n") +} + +func slurp(ec *EvalCtx, args []Value, opts map[string]Value) { + TakeNoArg(args) + TakeNoOpt(opts) + + in := ec.ports[0].File + out := ec.ports[1].Chan + + all, err := ioutil.ReadAll(in) + maybeThrow(err) + out <- String(string(all)) +} + +func fromLines(ec *EvalCtx, args []Value, opts map[string]Value) { + TakeNoArg(args) + TakeNoOpt(opts) + + in := ec.ports[0].File + out := ec.ports[1].Chan + + linesToChan(in, out) +} + +// fromJSON parses a stream of JSON data into Value's. +func fromJSON(ec *EvalCtx, args []Value, opts map[string]Value) { + TakeNoArg(args) + TakeNoOpt(opts) + + in := ec.ports[0].File + out := ec.ports[1].Chan + + dec := json.NewDecoder(in) + var v interface{} + for { + err := dec.Decode(&v) + if err != nil { + if err == io.EOF { + return + } + throw(err) + } + out <- FromJSONInterface(v) + } +} + +func toLines(ec *EvalCtx, args []Value, opts map[string]Value) { + iterate := ScanArgsOptionalInput(ec, args) + TakeNoOpt(opts) + + out := ec.ports[1].File + + iterate(func(v Value) { + fmt.Fprintln(out, ToString(v)) + }) +} + +// toJSON converts a stream of Value's to JSON data. +func toJSON(ec *EvalCtx, args []Value, opts map[string]Value) { + iterate := ScanArgsOptionalInput(ec, args) + TakeNoOpt(opts) + + out := ec.ports[1].File + + enc := json.NewEncoder(out) + iterate(func(v Value) { + err := enc.Encode(v) + maybeThrow(err) + }) +} + +func fopen(ec *EvalCtx, args []Value, opts map[string]Value) { + var namev String + ScanArgs(args, &namev) + name := string(namev) + TakeNoOpt(opts) + + // TODO support opening files for writing etc as well. + out := ec.ports[1].Chan + f, err := os.Open(name) + maybeThrow(err) + out <- File{f} +} + +func fclose(ec *EvalCtx, args []Value, opts map[string]Value) { + var f File + ScanArgs(args, &f) + TakeNoOpt(opts) + + maybeThrow(f.inner.Close()) +} + +func pipe(ec *EvalCtx, args []Value, opts map[string]Value) { + TakeNoArg(args) + TakeNoOpt(opts) + + r, w, err := os.Pipe() + out := ec.ports[1].Chan + maybeThrow(err) + out <- Pipe{r, w} +} + +func prclose(ec *EvalCtx, args []Value, opts map[string]Value) { + var p Pipe + ScanArgs(args, &p) + TakeNoOpt(opts) + + maybeThrow(p.r.Close()) +} + +func pwclose(ec *EvalCtx, args []Value, opts map[string]Value) { + var p Pipe + ScanArgs(args, &p) + TakeNoOpt(opts) + + maybeThrow(p.w.Close()) +} diff --git a/eval/builtin_fn_io_test.go b/eval/builtin_fn_io_test.go new file mode 100644 index 00000000..87377789 --- /dev/null +++ b/eval/builtin_fn_io_test.go @@ -0,0 +1,30 @@ +package eval + +func init() { + addToEvalTests([]evalTest{ + {`put foo bar`, want{out: strs("foo", "bar")}}, + + {`print [foo bar]`, want{bytesOut: []byte("[foo bar]")}}, + {`echo [foo bar]`, want{bytesOut: []byte("[foo bar]\n")}}, + {`pprint [foo bar]`, want{bytesOut: []byte("[\n foo\n bar\n]\n")}}, + + {`print "a\nb" | slurp`, want{out: strs("a\nb")}}, + {`print "a\nb" | from-lines`, want{out: strs("a", "b")}}, + {`print "a\nb\n" | from-lines`, want{out: strs("a", "b")}}, + {`echo '{"k": "v", "a": [1, 2]}' '"foo"' | from-json`, + want{out: []Value{ + ConvertToMap(map[Value]Value{ + String("k"): String("v"), + String("a"): NewList(strs("1", "2")...)}), + String("foo"), + }}}, + {`echo 'invalid' | from-json`, want{err: errAny}}, + + {`put "l\norem" ipsum | to-lines`, + want{bytesOut: []byte("l\norem\nipsum\n")}}, + {`put [&k=v &a=[1 2]] foo | to-json`, + want{bytesOut: []byte(`{"a":["1","2"],"k":"v"} +"foo" +`)}}, + }) +} diff --git a/eval/builtin_fn_num.go b/eval/builtin_fn_num.go new file mode 100644 index 00000000..22300206 --- /dev/null +++ b/eval/builtin_fn_num.go @@ -0,0 +1,170 @@ +package eval + +import ( + "math" + "math/rand" + "strconv" +) + +// Numerical operations. + +func init() { + addToBuiltinFns([]*BuiltinFn{ + // Comparison + {"<", + wrapNumCompare(func(a, b float64) bool { return a < b })}, + {"<=", + wrapNumCompare(func(a, b float64) bool { return a <= b })}, + {"==", + wrapNumCompare(func(a, b float64) bool { return a == b })}, + {"!=", + wrapNumCompare(func(a, b float64) bool { return a != b })}, + {">", + wrapNumCompare(func(a, b float64) bool { return a > b })}, + {">=", + wrapNumCompare(func(a, b float64) bool { return a >= b })}, + + // Arithmetics + {"+", plus}, + {"-", minus}, + {"*", times}, + {"/", slash}, + {"^", pow}, + {"%", mod}, + + // Random + {"rand", randFn}, + {"randint", randint}, + }) +} + +func wrapNumCompare(cmp func(a, b float64) bool) BuiltinFnImpl { + return func(ec *EvalCtx, args []Value, opts map[string]Value) { + TakeNoOpt(opts) + floats := make([]float64, len(args)) + for i, a := range args { + f, err := toFloat(a) + maybeThrow(err) + floats[i] = f + } + result := true + for i := 0; i < len(floats)-1; i++ { + if !cmp(floats[i], floats[i+1]) { + result = false + break + } + } + ec.OutputChan() <- Bool(result) + } +} + +func plus(ec *EvalCtx, args []Value, opts map[string]Value) { + var nums []float64 + ScanArgsVariadic(args, &nums) + TakeNoOpt(opts) + + out := ec.ports[1].Chan + sum := 0.0 + for _, f := range nums { + sum += f + } + out <- floatToString(sum) +} + +func minus(ec *EvalCtx, args []Value, opts map[string]Value) { + var ( + sum float64 + nums []float64 + ) + ScanArgsVariadic(args, &sum, &nums) + TakeNoOpt(opts) + + out := ec.ports[1].Chan + if len(nums) == 0 { + // Unary - + sum = -sum + } else { + for _, f := range nums { + sum -= f + } + } + out <- floatToString(sum) +} + +func times(ec *EvalCtx, args []Value, opts map[string]Value) { + var nums []float64 + ScanArgsVariadic(args, &nums) + TakeNoOpt(opts) + + out := ec.ports[1].Chan + prod := 1.0 + for _, f := range nums { + prod *= f + } + out <- floatToString(prod) +} + +func slash(ec *EvalCtx, args []Value, opts map[string]Value) { + TakeNoOpt(opts) + if len(args) == 0 { + // cd / + cdInner("/", ec) + return + } + // Division + divide(ec, args, opts) +} + +func divide(ec *EvalCtx, args []Value, opts map[string]Value) { + var ( + prod float64 + nums []float64 + ) + ScanArgsVariadic(args, &prod, &nums) + TakeNoOpt(opts) + + out := ec.ports[1].Chan + for _, f := range nums { + prod /= f + } + out <- floatToString(prod) +} + +func pow(ec *EvalCtx, args []Value, opts map[string]Value) { + var b, p float64 + ScanArgs(args, &b, &p) + TakeNoOpt(opts) + + out := ec.ports[1].Chan + out <- floatToString(math.Pow(b, p)) +} + +func mod(ec *EvalCtx, args []Value, opts map[string]Value) { + var a, b int + ScanArgs(args, &a, &b) + TakeNoOpt(opts) + + out := ec.ports[1].Chan + out <- String(strconv.Itoa(a % b)) +} + +func randFn(ec *EvalCtx, args []Value, opts map[string]Value) { + TakeNoArg(args) + TakeNoOpt(opts) + + out := ec.ports[1].Chan + out <- floatToString(rand.Float64()) +} + +func randint(ec *EvalCtx, args []Value, opts map[string]Value) { + var low, high int + ScanArgs(args, &low, &high) + TakeNoOpt(opts) + + if low >= high { + throw(ErrArgs) + } + out := ec.ports[1].Chan + i := low + rand.Intn(high-low) + out <- String(strconv.Itoa(i)) +} diff --git a/eval/builtin_fn_num_test.go b/eval/builtin_fn_num_test.go new file mode 100644 index 00000000..29274ee5 --- /dev/null +++ b/eval/builtin_fn_num_test.go @@ -0,0 +1,20 @@ +package eval + +func init() { + addToEvalTests([]evalTest{ + {`== 1 1.0`, want{out: bools(true)}}, + {`== 10 0xa`, want{out: bools(true)}}, + {`== a a`, want{err: errAny}}, + {`> 0x10 1`, want{out: bools(true)}}, + + // TODO test more edge cases + {"+ 233100 233", want{out: strs("233333")}}, + {"- 233333 233100", want{out: strs("233")}}, + {"- 233", want{out: strs("-233")}}, + {"* 353 661", want{out: strs("233333")}}, + {"/ 233333 353", want{out: strs("661")}}, + {"/ 1 0", want{out: strs("+Inf")}}, + {"^ 16 2", want{out: strs("256")}}, + {"% 23 7", want{out: strs("2")}}, + }) +} diff --git a/eval/builtin_fn_str.go b/eval/builtin_fn_str.go new file mode 100644 index 00000000..705dd0f6 --- /dev/null +++ b/eval/builtin_fn_str.go @@ -0,0 +1,239 @@ +package eval + +import ( + "bytes" + "errors" + "regexp" + "strconv" + "strings" + + "github.com/elves/elvish/util" +) + +// String operations. + +var ErrInput = errors.New("input error") + +func init() { + addToBuiltinFns([]*BuiltinFn{ + {"s", + wrapStrCompare(func(a, b string) bool { return a > b })}, + {">=s", + wrapStrCompare(func(a, b string) bool { return a >= b })}, + + {"to-string", toString}, + + {"joins", joins}, + {"splits", splits}, + {"replaces", replaces}, + + {"ord", ord}, + {"base", base}, + + {"wcswidth", wcswidth}, + {"-override-wcwidth", overrideWcwidth}, + + {"has-prefix", hasPrefix}, + {"has-suffix", hasSuffix}, + + {"eawk", eawk}, + }) +} + +func wrapStrCompare(cmp func(a, b string) bool) BuiltinFnImpl { + return func(ec *EvalCtx, args []Value, opts map[string]Value) { + TakeNoOpt(opts) + for _, a := range args { + if _, ok := a.(String); !ok { + throw(ErrArgs) + } + } + result := true + for i := 0; i < len(args)-1; i++ { + if !cmp(string(args[i].(String)), string(args[i+1].(String))) { + result = false + break + } + } + ec.OutputChan() <- Bool(result) + } +} + +// toString converts all arguments to strings. +func toString(ec *EvalCtx, args []Value, opts map[string]Value) { + TakeNoOpt(opts) + out := ec.OutputChan() + for _, a := range args { + out <- String(ToString(a)) + } +} + +// joins joins all input strings with a delimiter. +func joins(ec *EvalCtx, args []Value, opts map[string]Value) { + var sepv String + iterate := ScanArgsOptionalInput(ec, args, &sepv) + sep := string(sepv) + TakeNoOpt(opts) + + var buf bytes.Buffer + iterate(func(v Value) { + if s, ok := v.(String); ok { + if buf.Len() > 0 { + buf.WriteString(sep) + } + buf.WriteString(string(s)) + } else { + throwf("join wants string input, got %s", v.Kind()) + } + }) + out := ec.ports[1].Chan + out <- String(buf.String()) +} + +// splits splits an argument strings by a delimiter and writes all pieces. +func splits(ec *EvalCtx, args []Value, opts map[string]Value) { + var s, sep String + ScanArgs(args, &sep, &s) + TakeNoOpt(opts) + + out := ec.ports[1].Chan + parts := strings.Split(string(s), string(sep)) + for _, p := range parts { + out <- String(p) + } +} + +func replaces(ec *EvalCtx, args []Value, opts map[string]Value) { + var ( + old, repl, s String + optMax int + ) + ScanArgs(args, &old, &repl, &s) + ScanOpts(opts, OptToScan{"max", &optMax, String("-1")}) + + ec.ports[1].Chan <- String(strings.Replace(string(s), string(old), string(repl), optMax)) +} + +func ord(ec *EvalCtx, args []Value, opts map[string]Value) { + var s String + ScanArgs(args, &s) + TakeNoOpt(opts) + + out := ec.ports[1].Chan + for _, r := range s { + out <- String("0x" + strconv.FormatInt(int64(r), 16)) + } +} + +// ErrBadBase is thrown by the "base" builtin if the base is smaller than 2 or +// greater than 36. +var ErrBadBase = errors.New("bad base") + +func base(ec *EvalCtx, args []Value, opts map[string]Value) { + var ( + b int + nums []int + ) + ScanArgsVariadic(args, &b, &nums) + TakeNoOpt(opts) + + if b < 2 || b > 36 { + throw(ErrBadBase) + } + + out := ec.ports[1].Chan + + for _, num := range nums { + out <- String(strconv.FormatInt(int64(num), b)) + } +} + +func wcswidth(ec *EvalCtx, args []Value, opts map[string]Value) { + var s String + ScanArgs(args, &s) + TakeNoOpt(opts) + + out := ec.ports[1].Chan + out <- String(strconv.Itoa(util.Wcswidth(string(s)))) +} + +func overrideWcwidth(ec *EvalCtx, args []Value, opts map[string]Value) { + var ( + s String + w int + ) + ScanArgs(args, &s, &w) + TakeNoOpt(opts) + + r, err := toRune(s) + maybeThrow(err) + util.OverrideWcwidth(r, w) +} + +func hasPrefix(ec *EvalCtx, args []Value, opts map[string]Value) { + var s, prefix String + ScanArgs(args, &s, &prefix) + TakeNoOpt(opts) + + ec.OutputChan() <- Bool(strings.HasPrefix(string(s), string(prefix))) +} + +func hasSuffix(ec *EvalCtx, args []Value, opts map[string]Value) { + var s, suffix String + ScanArgs(args, &s, &suffix) + TakeNoOpt(opts) + + ec.OutputChan() <- Bool(strings.HasSuffix(string(s), string(suffix))) +} + +var eawkWordSep = regexp.MustCompile("[ \t]+") + +// eawk takes a function. For each line in the input stream, it calls the +// function with the line and the words in the line. The words are found by +// stripping the line and splitting the line by whitespaces. The function may +// call break and continue. Overall this provides a similar functionality to +// awk, hence the name. +func eawk(ec *EvalCtx, args []Value, opts map[string]Value) { + var f CallableValue + iterate := ScanArgsOptionalInput(ec, args, &f) + TakeNoOpt(opts) + + broken := false + iterate(func(v Value) { + if broken { + return + } + line, ok := v.(String) + if !ok { + throw(ErrInput) + } + args := []Value{line} + for _, field := range eawkWordSep.Split(strings.Trim(string(line), " \t"), -1) { + args = append(args, String(field)) + } + + newec := ec.fork("fn of eawk") + // TODO: Close port 0 of newec. + ex := newec.PCall(f, args, NoOpts) + ClosePorts(newec.ports) + + if ex != nil { + switch ex.(*Exception).Cause { + case nil, Continue: + // nop + case Break: + broken = true + default: + throw(ex) + } + } + }) +} diff --git a/eval/builtin_fn_str_test.go b/eval/builtin_fn_str_test.go new file mode 100644 index 00000000..40edce86 --- /dev/null +++ b/eval/builtin_fn_str_test.go @@ -0,0 +1,26 @@ +package eval + +func init() { + addToEvalTests([]evalTest{ + {`==s haha haha`, want{out: bools(true)}}, + {`==s 10 10.0`, want{out: bools(false)}}, + {` 0x10 1`, want{out: bools(true)}}, - {`is 1 1`, want{out: bools(true)}}, {`is [] []`, want{out: bools(true)}}, {`is [1] [1]`, want{out: bools(false)}}, {`eq 1 1`, want{out: bools(true)}}, {`eq [] []`, want{out: bools(true)}}, - {`ord a`, want{out: strs("0x61")}}, - {`base 16 42 233`, want{out: strs("2a", "e9")}}, - {`wcswidth 你好`, want{out: strs("4")}}, - {`has-key [foo bar] 0`, want{out: bools(true)}}, - {`has-key [foo bar] 0:1`, want{out: bools(true)}}, - {`has-key [foo bar] 0:20`, want{out: bools(false)}}, - {`has-key [&lorem=ipsum &foo=bar] lorem`, want{out: bools(true)}}, - {`has-key [&lorem=ipsum &foo=bar] loremwsq`, want{out: bools(false)}}, - {`has-value [&lorem=ipsum &foo=bar] lorem`, want{out: bools(false)}}, - {`has-value [&lorem=ipsum &foo=bar] bar`, want{out: bools(true)}}, - {`has-value [foo bar] bar`, want{out: bools(true)}}, - {`has-value [foo bar] badehose`, want{out: bools(false)}}, - {`has-value "foo" o`, want{out: bools(true)}}, - {`has-value "foo" d`, want{out: bools(false)}}, - - {`put (assoc [0] 0 zero)[0]`, want{out: strs("zero")}}, - {`put (assoc [&] k v)[k]`, want{out: strs("v")}}, - {`put (assoc [&k=v] k v2)[k]`, want{out: strs("v2")}}, - {`has-key (dissoc [&k=v] k) k`, want{out: bools(false)}}, + {`f=(constantly foo); $f; $f`, want{out: strs("foo", "foo")}}, + {`(constantly foo) bad`, want{err: errAny}}, } func init() {