Rash for me seemed like the perfect blend of lisp (racket) and external commands.
But it's always nice to hear when someone uses it and likes it despite that!
Rash has several limitations, sometimes due to design choices, that schemesh solves:
1. no job control
2. multi-line editing is limited
3. from what I understand, shell syntax is available only at REPL top level. Once you switch to Lisp syntax with `(`, you can return to shell syntax only with `)`. Thus means you cannot embed shell syntax inside Lisp syntax, i.e. you cannot do `(define j {find -type f | less})`
4. shell commands are Lisp functions, not Lisp objects. Inspecting and redirecting them after they have been created is difficult
5. Rash is written in Racket, which has larger RAM footprint than schemesh running on vanilla Chez Scheme: at startup, ~160MB vs. ~32MB
6. Racket/Rash support for multi-language at REPL is limited: once you do `#lang racket`, you cannot go back to `#lang rash`
It's possible I misunderstand what you mean because I'm not sure what piping to less is supposed to accomplish here, but this is not true. The following program works just fine:
#lang rash
(require racket/port
racket/string)
(define (echo!!! message)
(define output {echo $message |> port->string |> string-trim})
(string-append output "!!!"))
(echo!!! "Hello")>1. no job control
Racket is missing a feature in its rktio library needed to do job control with its process API, which Rash uses. At one point I added one or two other minor features needed for job control, but I ran out of steam and never finished the final one. It's a small feature, even, though now I don't remember much of the context. I hope I wrote enough notes to go back and finish this some day.
>2. multi-line editing is limited
I always intended to write a nice line editor that would do this properly. But, again, I never got around to it. I would still like to, and I will probably take a serious look at your line editor some time.
The design was intended as something to use interactively as well as for scripting. But since I never improved the line editing situation, even I only use it for scripting. After documentation issues, this is the most pressing thing that I would fix.
>3. from what I understand, shell syntax is available only at REPL top level. Once you switch to Lisp syntax with `(`, you can return to shell syntax only with `)`. Thus means you cannot embed shell syntax inside Lisp syntax, i.e. you cannot do `(define j {find -type f | less})`
As mentioned is not correct, you can recursively switch between shell and lisp.
>4. shell commands are Lisp functions, not Lisp objects. Inspecting and redirecting them after they have been created is difficult
This one is a design flaw. I've meant to go back and fix it (eg. just retrofitting a new pipe operator that returns the subprocess pipeline segment as an object rather than its ports or outputs), but, of course, haven't gotten around to it.
>5. Rash is written in Racket, which has larger RAM footprint than schemesh running on vanilla Chez Scheme: at startup, ~160MB vs. ~32MB
Yep.
>6. Racket/Rash support for multi-language at REPL is limited: once you do `#lang racket`, you cannot go back to `#lang rash`
So actually `#lang` is not supported at all in the REPL. It's neither supported in the Racket REPL nor the rash repl. In practice, what `#lang` does is (1) set the reader for a module, and (2) set the base import for the module, IE what symbol definitions are available. With the REPL you have to do this more manually. The repl in Racket is sort of second class in various ways, in part due to the “the top level is hopeless” problems for macros. (Search for that phrase and you can find many issues with repls and macros discussed over the years in the Racket mailing list.) Defining a new `#lang` in Racket includes various pieces about setting up modules specifically, and since the top level repl is not a module, it would need some different support that is currently not there, and would need to be retrofitted for various `#lang`s. But you can start a repl with an arbitrary reader, and use `eval` with arbitrary modules loaded or symbols defined. My intention with a rash line editor would also have been to make some infrastructure for better language-specific repls in racket generally. But, well, obviously I never actually did it. If I do make time for rash repl improvements in the near future, it will just as likely be ways for using it more nicely with emacs rather than actually writing a new line editor... we'll see.
I'm always sad when I think about how I've left Rash to languish. In grad school I was always stressed about publication (which I ultimately did poorly at), which sapped a lot of my desire and energy to actually get into the code and make improvements. Since graduating and going into industry, and with kids, I've rarely felt like I have the time or energy after all of my other responsibilities to spend time on hobby projects. Some day I would like to get back into it, fix its issues, polish it up, document it properly, etc. Alas, maybe some day.
I'd just like to share my joy in using the Emacs shell (Eshell), which I find to be a wonderful fusion of the Unix shell and a Lisp REPL. You can enter commands with or without parentheses. For example:
~ $ concat "foo" "bar"
foobar
~ $ (concat "foo" "bar")
foobar
Or ~ $ * 123 456
56088
~ $ (* 123 456)
56088
Eshell comes with several built-in commands implemented as Elisp functions. For example: ~ $ which cd echo ls which
eshell/cd is a byte-compiled Lisp function in ‘em-dirs.el’.
eshell/echo is a byte-compiled Lisp function in ‘em-basic.el’.
eshell/ls is a byte-compiled Lisp function in ‘em-ls.el’.
eshell/which is a byte-compiled Lisp function in ‘esh-cmd.el’.
~ $ ls -l /etc/h*
-rw-r--r-- 1 root wheel 446 2025-02-15 20:43 /etc/hosts
-rw-r--r-- 1 root wheel 0 2024-10-01 2024 /etc/ hosts.equiv
~ $ (eshell/ls "-l" (eshell-extended-glob "/etc/h*"))
-rw-r--r-- 1 root wheel 446 2025-02-15 20:43 /etc/hosts
-rw-r--r-- 1 root wheel 0 2024-10-01 2024 /etc/hosts.equiv
Of course, you can still run external commands like usual: ~ $ which curl jq python3 rustc
/usr/bin/curl
/opt/homebrew/bin/jq
/opt/homebrew/bin/python3
/Users/susam/.cargo/bin/rustc
~ $ python3 -c "print('hello')"
hello
~ $ curl -sS https://hacker-news.firebaseio.com/v0/item/43061183.json | jq -r .title
Schemesh: Fusion between Unix shell and Lisp REPL
Since TRAMP is an integral part of Emacs, you can switch between the local shell and remote shells transparently with simple 'cd' commands. For example: ~ $ echo local > /tmp/foo.txt
~ $ echo remote > /ssh:susam@susam.net:/tmp/foo.txt
~ $ cd /tmp/
/tmp $ cat foo.txt
local
/tmp $ hostname
mac.local
~ $ cd /ssh:susam@susam.net:/tmp/
/ssh:susam@susam.net:/tmp $ hostname
susam.net
/ssh:susam@susam.net:/tmp $ cat foo.txt
remote
In the second command, I redirected a file to a remote file system with the usual '>' redirection operator.Notice how, in the sixth command, I switched from my local shell to a remote shell with a simple 'cd' command. With Eshell and TRAMP, working across multiple remote systems becomes transparent, seamless, and effortless! Best of all, I still have the full power of Emacs at my fingertips, making Eshell an incredibly smooth and powerful experience!
I thought it was the latter (but it's been a while since I looked at it).
Tcl's exec gets it right. R Keene's pipethread extension for tcl gets it even more right.
Just perusing the schemesh docs (haven't tried it yet), it looks like he got it right, as well
Does it also have job control, and jobs as first-class objects?
In schemesh, you can do things like
find / -xdev -type f | ls -l --sort=size
CTRL+Z
bg 1
and also (define j {git log})
(display j)
(display (sh-run/string j))> A command invocation followed by an ampersand (&) will be run in the background. Eshell has no job control, so you can not suspend or background the current process, or bring a background process into the foreground. That said, background processes invoked from Eshell can be controlled the same way as any other background process in Emacs.
For things like this, we would have to switch to something like M-x shell or even M-x ansi-term. In Emacs, we have an assortment of shells and terminal implementations. As a long time Emacs user, I know when to use which, so it does not bother me. However, I can imagine how this might feel cumbersome for newer Emacs users.
In fact, this is one of the reasons I think your project is fantastic. It offers some of the Eshell-like experience, and more, to non-Emacs users, which is very compelling!
Schemesh is intended as an interactive shell and REPL: it supports line editing, autocompletion, searchable history, aliases, builtins, a customizable prompt, and automatic loading of `~/.config/schemesh/repl_init.ss`.
Most importantly, it has job control (CTRL+Z, `fg`, `bg` etc.) and recognizes and extends Unix shell syntax for starting, redirecting and composing jobs. An example:
find (lisp-expression-returning-some-string) -type f | xargs ls 2>/dev/null >> ls.log &
Scsh has none of the above features. As stated in https://scsh.net/docu/html/man-Z-H-2.html#node_sec_1.4
> Scsh, in the current release, is primarily designed for the writing of shell scripts -- programming. It is not a very comfortable system for interactive command use: the current release lacks job control, command-line editing, a terse, convenient command syntax, and it does not read in an initialisation file analogous to .login or .profile
Rather than the tclsh way of saying “we’ll just make the Lisp seem really shelly” which is a dud to anyone who is not a lisper.
Now, it’d be really cool if schemesh had a TUI library at the maturity level of Ratatui.
So... it sacrifices sub-shell syntax with parentheses being hijacked for Scheme. Have you also lost $(...) shell interpolation as the saner alternative to `...`?
It'd be really great if you could put your answer in the readme. It was the first question that came to my mind when looking at your project.
I'm looking forward to trying out schemesh!
For example, if you type:
ls
in schemesh you will execute the "ls" command and get a directory listing, whereas in scsh you will get the value of a variable named "ls".[UPDATE] Also, as cosmos0072 notes in a sibling comment, schemesh has shell-like features like line editing, autocompletion, searchable history, aliases, builtins, etc.
(lisp expression) | <unix command> | (lisp expression)
Yes, although the current syntax is cumbersome - I am thinking how to improve it.
The first part is easy. If you want to run something like
(lisp-expr1-produces-a-string) | grep foo
the current solution is echo (lisp-expr1-produces-a-string) | grep foo
The second part, i.e. feeding a command's output into a Scheme function, is more cumbersome.If you want to run
echo (lisp-expr1) | grep foo | (lisp-expr2)
the current solution requires (sh-run/string job), namely: (lisp-expr2-accepts-string-arg
(sh-run/string
{echo (lisp-expr1) | grep foo}))
If instead you have a (lisp-expr2...) that reads from an integer file descriptor passed as argument - not a Scheme I/O port - you can write (lisp-expr2-accepts-integer-file-descriptor
(sh-start/fd-stdout
{echo (lisp-expr1) | grep foo}))
[UPDATE] There is also a function (sh-redirect job redirection-args ...) - it can add arbitrary redirections to a job, including pipes, but it's quite low-level and verbose to useThe functions (sh-fd-stdin) (sh-fd-stdout) and (sh-fd-stderr) return the integer file descriptors that a schemesh builtin should use to perform I/O.
With them, you can do
true (lambda () (lisp-expr-writes-to-fd (sh-fd-stdout))) | grep foo | true (lambda () (lisp-expr-reads-from-fd (sh-fd-stdin)))
It should work :)In my opinion, the outmost parens (even when invoking Lisp functions), as well as all the elaborate glue function names, kinda kill it for interactive use. If you think about it, it's leaking the implementation details into the syntax, and makes for poor idioms. Not very Lispy.
My idea is something like:
>>> + 1 2 3
6
(And you would never know if it's /bin/+ or (define (+ ...))) >>> seq 1 10 | sum
Let's assume seq is an executable, and sum is a Scheme function. Each "token" seq produces (by default delimited by all whitespace, maybe you could override the rules for a local context, parameterize?) is buffered by the shell, and at the end the whole thing turned into a list of strings. The result is passed to sum as a parameter. (Of course this would break if sum expects a list of integers, but it could also parse the strings as it goes.)The other way around would also work. If seq produces a list of integers, it's turned into a list of strings and fed into sum as input lines.
The shell could scan $PATH and create a simple function wrapper for each executable.
Now to avoid unnecessary buffering or type conversion, a typed variant of Scheme could be used, possibly with multiple dispatch (per argument/return type). E.g. if the next function in the pipeline accepts an input port or a lazy string iterator, the preceding shell command wrapper could return an output port.
The tricky case with syntax is what to do with tokens like "-9", "3.14", etc. The lexer could store both the parsed value (if it is valid), and the original string. Depending on the context, it could be resolved to either, but retain strong (dynamic) typing when interacting with a Scheme function, so "3.14.15" wouldn't work if a typed function only accepts numbers.
Reminds me of Tcl a bit.
- Elixir: data |> process(12) puts data as the FIRST arg of process (before 12).
- Gleam: data |> process(12, _) puts data as the "hole" arg ("_") of process.
So far so good, but these approaches are mainly just more convenient function calls - i.e., they don't have fancy error checking in them. Then you have Haskell:
- Haskell: >>= "binds" actions to guarantee execution order (even for actions that don't depend on the previous action's output!). This is more fancy because it uses monads to encapsulate the computations at each step, and can shortcircuit on errors.
Clojure does either first or last position depending on the operator, and it offers lightweight lambdas similar to your second option
The natural choice for a language like Haskell is final position: the rhs of the |> will be partially applied an |> has type a -> (a -> b) -> b
In R, things go in the last slot I think but most arguments on the right hand side would be passed as keywords so the ‘last’ slot would often be the first argument.
The whole point of Unix pipes is that execution is parallel so I’m not totally sure I get your point about guaranteeing execution order.
Threading functions together is basically about being able to write
read -> eval -> print -> loop
rather than: (loop (print (eval (read))))For creating a single `schemesh` executable with the usual shell-compatible options and arguments, the Scheme implementation also needs to be linkable as a library from C:
Chez Scheme provides a `kernel.o` or `libkernel.a` library that you can link into C code, then call the C functions Sscheme_init(), Sregister_boot_file() and finally Scall0(some_scheme_repl_procedure) or Sscheme_start()
Similar to [ and [[
It would be quite limited:
internal status would not persist between invocations,
and it would only be able to exchange unstructured data (a stream of bytes) with the shell
It’s one of the more impressive and genuinely interesting shells I’ve seen for a while.
Babashka is another amazing tool for interacting with a shell with clojure (or a very close dialect thereof).
Where's my book The Little Schemer? :D
Being able to switch back/forth without leaving the prompt is nice.
And I do an awful lot of shelling out, usually as a poor person's FFI or concurrency, or to just interact with a chain of shell pipes.
I got pretty far, I abandoned the project when the computer I was working on was destroyed, and I hadn't committed and pushed the majority of the work.
I'm glad that someone is actually doing it though; a Scheme shell always seemed like it could have a lot of potential for scripting.