From 5e5239b4fac49320f2fc1375b074870f4558597c Mon Sep 17 00:00:00 2001 From: refaktor Date: Mon, 15 Jun 2026 11:37:42 +0200 Subject: [PATCH] making tests pass , trying to add basic tmux functionality to os context, still deciding on interface --- evaldo/builtins_base_contexts.go | 34 ++-- evaldo/builtins_base_strings.go | 8 +- evaldo/builtins_os.go | 303 +++++++++++++++++++++++++++++++ evaldo/builtins_sxml.go | 8 +- go.mod | 1 + go.sum | 2 + tests/base.info.rye | 38 ++-- tests/formats.info.rye | 8 +- 8 files changed, 362 insertions(+), 40 deletions(-) diff --git a/evaldo/builtins_base_contexts.go b/evaldo/builtins_base_contexts.go index 7557f58f..faf94321 100644 --- a/evaldo/builtins_base_contexts.go +++ b/evaldo/builtins_base_contexts.go @@ -308,12 +308,12 @@ var builtins_contexts = map[string]*env.Builtin{ // Tests: // equal { c: context { x: 123 } p: context { y: 456 } cc: bind c p cc/x } 123 - // equal { c: context { x: 123 } p: context { y: 456 } cc: bind c p cc/y } 456 - // equal { c: context { x: 123 } p: context { y: 456 } cc: bind c p do\inside c { x:: 999 } cc/x } 123 ; clone unchanged - // equal { c: context { x: 123 } p: context { y: 456 } cc: bind c p do\inside c { x:: 999 } c/x } 999 ; original modified + // equal { c: context { x: 123 } p: context { y: 456 } cc: bind c p cc/y } 456 + // equal { c: context { x:: 123 } p: context { y:: 456 } cc: bind c p do\inside c { x:: 999 } cc/x } 123 ; clone unchanged + // equal { c: context { x:: 123 } p: context { y:: 456 } cc: bind c p do\inside c { x:: 999 } c/x } 999 ; original modified // Args: // * child: Context object to clone and bind - // * parent: Context object to bind to as parent + // * parent: Context object to bind to as parent // Returns: // * a cloned child context with its parent set to the specified parent context "bind": { // ** @@ -341,8 +341,8 @@ var builtins_contexts = map[string]*env.Builtin{ // Tests: // equal { c: context { x: 123 } cc: anchor c cc/x } 123 - // equal { c: context { x: 123 } cc: anchor c do\inside c { x:: 999 } cc/x } 123 ; clone unchanged - // equal { c: context { x: 123 } cc: anchor c do\inside c { x:: 999 } c/x } 999 ; original modified + // equal { c: context { x:: 123 } cc: anchor c do\inside c { x:: 999 } cc/x } 123 ; clone unchanged + // equal { c: context { x:: 123 } cc: anchor c do\inside c { x:: 999 } c/x } 999 ; original modified // Args: // * ctx: Context object to clone and anchor to current context // Returns: @@ -367,7 +367,7 @@ var builtins_contexts = map[string]*env.Builtin{ // Tests: // equal { c: context { x: 123 } anchor! c c/x } 123 - // equal { c: context { x: 123 } anchor! c do\inside c { x:: 999 } c/x } 999 ; original modified + // equal { c: context { x:: 123 } anchor! c do\inside c { x:: 999 } c/x } 999 ; original modified // Args: // * ctx: Context object to anchor to current context // Returns: @@ -972,8 +972,8 @@ var builtins_contexts = map[string]*env.Builtin{ }, // Tests: - // equal { c: context { x: 100 } 123 |enter c { .print x } } 123 - // equal { c: context { x: 100 } 123 |enter c { add x } } 223 + // equal { c: context { x: 100 } 123 |enter c { x } } 123 + // equal { c: context { x: 100 } 123 |enter c { + x } } 223 // equal { pipes: context { echo: { .print } into-file: { .print } } 123 |enter pipes { .echo .into-file %data.txt } } 123 // Args: // * value: Value to inject into the block (like with) @@ -986,14 +986,14 @@ var builtins_contexts = map[string]*env.Builtin{ Doc: "Combines with and do\\in: takes a value to inject, a context as parent, and a block to evaluate with both.", Fn: func(ps *env.ProgramState, arg0 env.Object, arg1 env.Object, arg2 env.Object, arg3 env.Object, arg4 env.Object) env.Object { value := arg0 // First argument: value to inject (like with) - + switch ctx := arg1.(type) { // Second argument: context (like do\in) case *env.RyeCtx: switch bloc := arg2.(type) { // Third argument: block to execute case env.Block: ser := ps.Ser ps.Ser = bloc.Series - + // First, set up the parent context like do\in does tempCtx := ctx for { @@ -1009,22 +1009,22 @@ var builtins_contexts = map[string]*env.Builtin{ break } } - + // Save current parent context temp := ps.Ctx.Parent - // Set argument context as parent + // Set argument context as parent ps.Ctx.Parent = ctx - + // Now evaluate the block with value injection like with does EvalBlockInj(ps, value, true) - + MaybeDisplayFailureOrError(ps, ps.Idx, "enter") - + // Restore original parent context ps.Ctx.Parent = temp ps.Ser = ser return ps.Res - + default: return MakeArgError(ps, 3, []env.Type{env.BlockType}, "enter") } diff --git a/evaldo/builtins_base_strings.go b/evaldo/builtins_base_strings.go index c83644d4..f4ca1f13 100644 --- a/evaldo/builtins_base_strings.go +++ b/evaldo/builtins_base_strings.go @@ -344,10 +344,10 @@ var builtins_string = map[string]*env.Builtin{ }, }, // Tests: - // equal { contains\flag "-help -yello -ho" -h|help } true - // equal { contains\flag "hello -help ho" -h|help } true - // equal { contains\flag "hello yello -ho" -h|help } false - // equal { contains\flag "-hello yello ho" -h|help } false + // equal { contains\flag { --help --yello -h } -h|help } true + // equal { contains\flag load "hello --help ho" -h|help } true + // equal { contains\flag { hello yello --ho } -h|help } false + // equal { contains\flag { --hello yello ho } -h|help } false // Args: // * collection: block of strings to search in // * value: Flag value to search for diff --git a/evaldo/builtins_os.go b/evaldo/builtins_os.go index 5dbf8b43..bc0980a1 100644 --- a/evaldo/builtins_os.go +++ b/evaldo/builtins_os.go @@ -10,12 +10,14 @@ import ( "io" "net" "os" + "os/exec" "path/filepath" "regexp" "strings" "time" "github.com/atotto/clipboard" + "github.com/GianlucaP106/gotmux/gotmux" "github.com/refaktor/go-find" "github.com/refaktor/rye/env" @@ -1907,6 +1909,307 @@ var Builtins_os = map[string]*env.Builtin{ } }, }, + + // + // ##### TMUX ##### "tmux terminal multiplexer functions using gotmux library" + // + + // Args: + // * none + // Returns: + // * boolean indicating if tmux is available + // Tags: #tmux #check + "tmux-available?": { + Argsn: 0, + Doc: "Checks if tmux is available on the system.", + Fn: func(ps *env.ProgramState, arg0 env.Object, arg1 env.Object, arg2 env.Object, arg3 env.Object, arg4 env.Object) env.Object { + _, err := exec.LookPath("tmux") + return *env.NewBoolean(err == nil) + }, + }, + + // Args: + // * session-name: string name for the new session + // Returns: + // * native tmux session object + // Tags: #tmux #session + "tmux-new-session": { + Argsn: 1, + Doc: "Creates a new tmux session with the given name using gotmux library.", + Fn: func(ps *env.ProgramState, arg0 env.Object, arg1 env.Object, arg2 env.Object, arg3 env.Object, arg4 env.Object) env.Object { + tmux, err := gotmux.DefaultTmux() + if err != nil { + return MakeBuiltinError(ps, "Failed to connect to tmux: "+err.Error(), "tmux-new-session") + } + + switch name := arg0.(type) { + case env.String: + session, err := tmux.NewSession(&gotmux.SessionOptions{ + Name: name.Value, + }) + if err != nil { + return MakeBuiltinError(ps, "Failed to create session: "+err.Error(), "tmux-new-session") + } + return *env.NewNative(ps.Idx, session, "tmux-session") + default: + return MakeArgError(ps, 1, []env.Type{env.StringType}, "tmux-new-session") + } + }, + }, + + // Args: + // * none + // Returns: + // * block of native tmux session objects + // Tags: #tmux #session #list + "tmux-list-sessions": { + Argsn: 0, + Doc: "Lists all tmux sessions using gotmux library.", + Fn: func(ps *env.ProgramState, arg0 env.Object, arg1 env.Object, arg2 env.Object, arg3 env.Object, arg4 env.Object) env.Object { + tmux, err := gotmux.DefaultTmux() + if err != nil { + return MakeBuiltinError(ps, "Failed to connect to tmux: "+err.Error(), "tmux-list-sessions") + } + + sessions, err := tmux.ListSessions() + if err != nil { + return MakeBuiltinError(ps, "Failed to list sessions: "+err.Error(), "tmux-list-sessions") + } + + items := make([]env.Object, len(sessions)) + for i, session := range sessions { + items[i] = *env.NewNative(ps.Idx, session, "tmux-session") + } + return *env.NewBlock(*env.NewTSeries(items)) + }, + }, + + // Args: + // * session-name: string name of the session to get + // Returns: + // * native tmux session object + // Tags: #tmux #session + "tmux-get-session": { + Argsn: 1, + Doc: "Gets a tmux session by name using gotmux library.", + Fn: func(ps *env.ProgramState, arg0 env.Object, arg1 env.Object, arg2 env.Object, arg3 env.Object, arg4 env.Object) env.Object { + tmux, err := gotmux.DefaultTmux() + if err != nil { + return MakeBuiltinError(ps, "Failed to connect to tmux: "+err.Error(), "tmux-get-session") + } + + switch name := arg0.(type) { + case env.String: + session, err := tmux.GetSessionByName(name.Value) + if err != nil { + return MakeBuiltinError(ps, "Failed to get session: "+err.Error(), "tmux-get-session") + } + return *env.NewNative(ps.Idx, session, "tmux-session") + default: + return MakeArgError(ps, 1, []env.Type{env.StringType}, "tmux-get-session") + } + }, + }, + + // Args: + // * session: native tmux session object + // Returns: + // * native tmux window object + // Tags: #tmux #window + "tmux-new-window": { + Argsn: 1, + Doc: "Creates a new window in the given tmux session.", + Fn: func(ps *env.ProgramState, arg0 env.Object, arg1 env.Object, arg2 env.Object, arg3 env.Object, arg4 env.Object) env.Object { + switch sess := arg0.(type) { + case env.Native: + if session, ok := sess.Value.(*gotmux.Session); ok { + window, err := session.New() + if err != nil { + return MakeBuiltinError(ps, "Failed to create window: "+err.Error(), "tmux-new-window") + } + return *env.NewNative(ps.Idx, window, "tmux-window") + } + return MakeBuiltinError(ps, "Expected tmux-session object", "tmux-new-window") + default: + return MakeArgError(ps, 1, []env.Type{env.NativeType}, "tmux-new-window") + } + }, + }, + + // Args: + // * session: native tmux session object + // * window-name: string name for the window + // Returns: + // * native tmux window object + // Tags: #tmux #window + "tmux-new-window-named": { + Argsn: 2, + Doc: "Creates a new window in the given tmux session with a specific name.", + Fn: func(ps *env.ProgramState, arg0 env.Object, arg1 env.Object, arg2 env.Object, arg3 env.Object, arg4 env.Object) env.Object { + switch sess := arg0.(type) { + case env.Native: + if session, ok := sess.Value.(*gotmux.Session); ok { + switch name := arg1.(type) { + case env.String: + window, err := session.NewWindow(&gotmux.NewWindowOptions{ + WindowName: name.Value, + }) + if err != nil { + return MakeBuiltinError(ps, "Failed to create named window: "+err.Error(), "tmux-new-window-named") + } + return *env.NewNative(ps.Idx, window, "tmux-window") + default: + return MakeArgError(ps, 2, []env.Type{env.StringType}, "tmux-new-window-named") + } + } + return MakeBuiltinError(ps, "Expected tmux-session object", "tmux-new-window-named") + default: + return MakeArgError(ps, 1, []env.Type{env.NativeType}, "tmux-new-window-named") + } + }, + }, + + // Args: + // * session: native tmux session object + // Returns: + // * block of native tmux window objects + // Tags: #tmux #window #list + "tmux-list-windows": { + Argsn: 1, + Doc: "Lists all windows in the given tmux session.", + Fn: func(ps *env.ProgramState, arg0 env.Object, arg1 env.Object, arg2 env.Object, arg3 env.Object, arg4 env.Object) env.Object { + switch sess := arg0.(type) { + case env.Native: + if session, ok := sess.Value.(*gotmux.Session); ok { + windows, err := session.ListWindows() + if err != nil { + return MakeBuiltinError(ps, "Failed to list windows: "+err.Error(), "tmux-list-windows") + } + + items := make([]env.Object, len(windows)) + for i, window := range windows { + items[i] = *env.NewNative(ps.Idx, window, "tmux-window") + } + return *env.NewBlock(*env.NewTSeries(items)) + } + return MakeBuiltinError(ps, "Expected tmux-session object", "tmux-list-windows") + default: + return MakeArgError(ps, 1, []env.Type{env.NativeType}, "tmux-list-windows") + } + }, + }, + + // Args: + // * window: native tmux window object + // * index: integer pane index (0-based) + // Returns: + // * native tmux pane object + // Tags: #tmux #pane + "tmux-get-pane": { + Argsn: 2, + Doc: "Gets a pane from a tmux window by index.", + Fn: func(ps *env.ProgramState, arg0 env.Object, arg1 env.Object, arg2 env.Object, arg3 env.Object, arg4 env.Object) env.Object { + switch win := arg0.(type) { + case env.Native: + if window, ok := win.Value.(*gotmux.Window); ok { + switch idx := arg1.(type) { + case env.Integer: + pane, err := window.GetPaneByIndex(int(idx.Value)) + if err != nil { + return MakeBuiltinError(ps, "Failed to get pane: "+err.Error(), "tmux-get-pane") + } + return *env.NewNative(ps.Idx, pane, "tmux-pane") + default: + return MakeArgError(ps, 2, []env.Type{env.IntegerType}, "tmux-get-pane") + } + } + return MakeBuiltinError(ps, "Expected tmux-window object", "tmux-get-pane") + default: + return MakeArgError(ps, 1, []env.Type{env.NativeType}, "tmux-get-pane") + } + }, + }, + + // Args: + // * pane: native tmux pane object + // Returns: + // * native tmux pane object + // Tags: #tmux #pane + "tmux-split-pane": { + Argsn: 1, + Doc: "Splits a tmux pane horizontally.", + Fn: func(ps *env.ProgramState, arg0 env.Object, arg1 env.Object, arg2 env.Object, arg3 env.Object, arg4 env.Object) env.Object { + switch p := arg0.(type) { + case env.Native: + if pane, ok := p.Value.(*gotmux.Pane); ok { + err := pane.Split() + if err != nil { + return MakeBuiltinError(ps, "Failed to split pane: "+err.Error(), "tmux-split-pane") + } + return arg0 + } + return MakeBuiltinError(ps, "Expected tmux-pane object", "tmux-split-pane") + default: + return MakeArgError(ps, 1, []env.Type{env.NativeType}, "tmux-split-pane") + } + }, + }, + + // Args: + // * pane: native tmux pane object + // * command: string command to send + // Returns: + // * native tmux pane object + // Tags: #tmux #pane #command + "tmux-send-keys": { + Argsn: 2, + Doc: "Sends keys/command to a tmux pane.", + Fn: func(ps *env.ProgramState, arg0 env.Object, arg1 env.Object, arg2 env.Object, arg3 env.Object, arg4 env.Object) env.Object { + switch p := arg0.(type) { + case env.Native: + if pane, ok := p.Value.(*gotmux.Pane); ok { + switch cmd := arg1.(type) { + case env.String: + err := pane.SendKeys(cmd.Value) + if err != nil { + return MakeBuiltinError(ps, "Failed to send keys: "+err.Error(), "tmux-send-keys") + } + return arg0 + default: + return MakeArgError(ps, 2, []env.Type{env.StringType}, "tmux-send-keys") + } + } + return MakeBuiltinError(ps, "Expected tmux-pane object", "tmux-send-keys") + default: + return MakeArgError(ps, 1, []env.Type{env.NativeType}, "tmux-send-keys") + } + }, + }, + + // Args: + // * session: native tmux session object + // Returns: + // * native tmux session object + // Tags: #tmux #session + "tmux-kill-session": { + Argsn: 1, + Doc: "Kills/destroys a tmux session.", + Fn: func(ps *env.ProgramState, arg0 env.Object, arg1 env.Object, arg2 env.Object, arg3 env.Object, arg4 env.Object) env.Object { + switch sess := arg0.(type) { + case env.Native: + if session, ok := sess.Value.(*gotmux.Session); ok { + err := session.Kill() + if err != nil { + return MakeBuiltinError(ps, "Failed to kill session: "+err.Error(), "tmux-kill-session") + } + return arg0 + } + return MakeBuiltinError(ps, "Expected tmux-session object", "tmux-kill-session") + default: + return MakeArgError(ps, 1, []env.Type{env.NativeType}, "tmux-kill-session") + } + }, + }, } // resolvePath resolves a path - if it's absolute, returns it as-is; if relative, joins with workingPath diff --git a/evaldo/builtins_sxml.go b/evaldo/builtins_sxml.go index 7e32745a..40b66eeb 100644 --- a/evaldo/builtins_sxml.go +++ b/evaldo/builtins_sxml.go @@ -246,19 +246,19 @@ var Builtins_sxml = map[string]*env.Builtin{ // Tests: // stdout { // "C3POR2D2Luke" |reader |probe - // |probe |do-sxml { _ [ .prns ] } + // |probe |Do-sxml { _ [ .prns ] } // } "C3PO R2D2 Luke " // stdout { // "C3POR2D2Luke" |reader - // |do-sxml { { _ [ .prns ] } } + // |Do-sxml { { _ [ .prns ] } } // } "C3PO R2D2 " // stdout { // "XWingR2D2Luke" |reader - // |do-sxml { { _ [ .prns ] } } + // |Do-sxml { { _ [ .prns ] } } // } "R2D2 Luke " // stdout { // "R2D2LukeVader" |reader - // |do-sxml { { { _ [ .prns ] } } } + // |Do-sxml { { { _ [ .prns ] } } } // } "Luke " // Args: // * reader: XML reader object diff --git a/go.mod b/go.mod index 872b71f4..15b3a67c 100644 --- a/go.mod +++ b/go.mod @@ -81,6 +81,7 @@ require ( cloud.google.com/go/auth v0.9.3 // indirect cloud.google.com/go/compute/metadata v0.9.0 // indirect filippo.io/hpke v0.4.0 // indirect + github.com/GianlucaP106/gotmux v0.5.0 // indirect github.com/RoaringBitmap/roaring/v2 v2.14.5 // indirect github.com/StirlingMarketingGroup/go-retry v0.0.0-20190512160921-94a8eb23e893 // indirect github.com/andybalholm/cascadia v1.3.3 // indirect diff --git a/go.sum b/go.sum index 3ffb3ca5..82716d59 100644 --- a/go.sum +++ b/go.sum @@ -18,6 +18,8 @@ filippo.io/hpke v0.4.0/go.mod h1:EmAN849/P3qdeK+PCMkDpDm83vRHM5cDipBJ8xbQLVY= github.com/BrianLeishman/go-imap v0.1.28 h1:+IGG0hSdiwoUlJKIP6rF/OfnEUt0YZzHsBVBXdC7Rnk= github.com/BrianLeishman/go-imap v0.1.28/go.mod h1:gmLtGYOsDv+wnfRAgvw/zg46gNMuLQf+VaoZd4Ig/Tk= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/GianlucaP106/gotmux v0.5.0 h1:kpZsrBPtJFjAvVRfeLwm8cE+7yr4NiMPEaYsTKYGwP8= +github.com/GianlucaP106/gotmux v0.5.0/go.mod h1:qOsZ+exnCbgv3KJ84VaBo4Q7mXs/W23CW4fyoXAgKe4= github.com/JohannesKaufmann/dom v0.2.0 h1:1bragmEb19K8lHAqgFgqCpiPCFEZMTXzOIEjuxkUfLQ= github.com/JohannesKaufmann/dom v0.2.0/go.mod h1:57iSUl5RKric4bUkgos4zu6Xt5LMHUnw3TF1l5CbGZo= github.com/JohannesKaufmann/html-to-markdown/v2 v2.5.1 h1:IpUgup6ucCE4wB59wAP0Y2qSApYjFhSfGVjShUBoVSw= diff --git a/tests/base.info.rye b/tests/base.info.rye index acab019d..4140c607 100644 --- a/tests/base.info.rye +++ b/tests/base.info.rye @@ -1122,10 +1122,10 @@ section "Strings " "" { } { - equal { contains\flag "-help -yello -ho" -h|help } true - equal { contains\flag "hello -help ho" -h|help } true - equal { contains\flag "hello yello -ho" -h|help } false - equal { contains\flag "-hello yello ho" -h|help } false + equal { contains\flag { --help --yello -h } -h|help } true + equal { contains\flag load "hello --help ho" -h|help } true + equal { contains\flag { hello yello --ho } -h|help } false + equal { contains\flag { --hello yello ho } -h|help } false } { @@ -4911,8 +4911,8 @@ section "Contexts " "Context related functions" { { equal { c: context { x: 123 } p: context { y: 456 } cc: bind c p cc/x } 123 equal { c: context { x: 123 } p: context { y: 456 } cc: bind c p cc/y } 456 - equal { c: context { x: 123 } p: context { y: 456 } cc: bind c p do\inside c { x:: 999 } cc/x } 123 ; clone unchanged - equal { c: context { x: 123 } p: context { y: 456 } cc: bind c p do\inside c { x:: 999 } c/x } 999 ; original modified + equal { c: context { x:: 123 } p: context { y:: 456 } cc: bind c p do\inside c { x:: 999 } cc/x } 123 ; clone unchanged + equal { c: context { x:: 123 } p: context { y:: 456 } cc: bind c p do\inside c { x:: 999 } c/x } 999 ; original modified } { @@ -4931,8 +4931,8 @@ section "Contexts " "Context related functions" { { equal { c: context { x: 123 } cc: anchor c cc/x } 123 - equal { c: context { x: 123 } cc: anchor c do\inside c { x:: 999 } cc/x } 123 ; clone unchanged - equal { c: context { x: 123 } cc: anchor c do\inside c { x:: 999 } c/x } 999 ; original modified + equal { c: context { x:: 123 } cc: anchor c do\inside c { x:: 999 } cc/x } 123 ; clone unchanged + equal { c: context { x:: 123 } cc: anchor c do\inside c { x:: 999 } c/x } 999 ; original modified } { @@ -4951,7 +4951,7 @@ section "Contexts " "Context related functions" { { equal { c: context { x: 123 } anchor! c c/x } 123 - equal { c: context { x: 123 } anchor! c do\inside c { x:: 999 } c/x } 999 ; original modified + equal { c: context { x:: 123 } anchor! c do\inside c { x:: 999 } c/x } 999 ; original modified } { @@ -5423,8 +5423,8 @@ section "Contexts " "Context related functions" { } { - equal { c: context { x: 100 } 123 |enter c { .print x } } 123 - equal { c: context { x: 100 } 123 |enter c { add x } } 223 + equal { c: context { x: 100 } 123 |enter c { x } } 123 + equal { c: context { x: 100 } 123 |enter c { + x } } 223 equal { pipes: context { echo: { .print } into-file: { .print } } 123 |enter pipes { .echo .into-file %data.txt } } 123 } @@ -8058,6 +8058,22 @@ section "Default" "" { { } + group "empty" + "Creates an 404 error object without setting any flags." + { + arg `error_info: String message, Integer code, or block for multiple parameters` + returns `error object without setting any flags` + } + + { + equal { failure "error message" |type? } 'error + equal { failure "error message" |message? } "error message" + equal { failure 404 |status? } 404 + } + + { + } + group "failure\\wrap" "Creates a new error that wraps an existing error, allowing for error chaining." { diff --git a/tests/formats.info.rye b/tests/formats.info.rye index 440f883a..4a492fa5 100644 --- a/tests/formats.info.rye +++ b/tests/formats.info.rye @@ -292,19 +292,19 @@ section "SXML " "Streaming, SAX-like XML processing" { { stdout { "C3POR2D2Luke" |reader |probe - |probe |do-sxml { _ [ .prns ] } + |probe |Do-sxml { _ [ .prns ] } } "C3PO R2D2 Luke " stdout { "C3POR2D2Luke" |reader - |do-sxml { { _ [ .prns ] } } + |Do-sxml { { _ [ .prns ] } } } "C3PO R2D2 " stdout { "XWingR2D2Luke" |reader - |do-sxml { { _ [ .prns ] } } + |Do-sxml { { _ [ .prns ] } } } "R2D2 Luke " stdout { "R2D2LukeVader" |reader - |do-sxml { { { _ [ .prns ] } } } + |Do-sxml { { { _ [ .prns ] } } } } "Luke " }