Why Sponsor Oils? | blog | oilshell.org
This is a delayed announcement of the November release of:
Oils version 0.24.0 - Source tarballs and documentation.
(The most recent release is on our new oils.pub
domain. More about that later.)
Why was it delayed? As I was writing it, it felt too dense, so I wrote a friendly introduction to the ideas introduced:
That is essential background for this announcement. (The reasons are likely not what you think!)
What's in this release? Important stuff first:
The themes in the title:
util.ysh
are objects used as namespacesENV
is an object, which separates it from global variables (unlike POSIX shell)__builtins__
and __defaults__
We'll also see:
With the addition of closures, objects, and namespaces, YSH is now closer to Python and JavaScript than it is to Awk.
Python and JavaScript have all those features, but shell and Awk have none of them!
YSH is also gaining reflective control over the interpreter. We're making a programmable programming language, which supports DSLs like Awk and Hay.
Ruby seems to do better at this than Python or JavaScript, and so far our APIs compare well with Ruby. I welcome Ruby users to challenge us!
With these features, I also feel like YSH is more complete. The remaining big feature is Hay: declarative and programmable configuration. So:
But I don't anticipate big new features in the YSH language. The main change will be an overhaul of Hay!
As mentioned, the objects post is essential background, and an appendix described the motivation for closures. If you want to read more, here are some rough Zulip threads (login required):
Thank you to all contributors!
Str.split()
now supports an eggex separatorStr.replace()
improvements and docs1 ..= 5
and 1 ..< 5
(discussed below)&&
and ||
by over-lexingObj
typeargs.ysh
supports Float
and Str
typesList[Str]
args.ysh
API privatetest --true $[myexpr]
, and likewisetest --false
bind
builtin (work in progress)trap
with an integer arg removes the trap (POSIX compatibility)_build/oils.sh
setvar L[i]
- issue 2104std::deque
. (Temporary parse tree nodes are not GC objects.)grep -e
. This is the bug Aidan fixed, mentioned above.ale5000
command -v
bug, issue 2093, now fixedprintf
overflow, issue 2107, now fixedyurivict
- FreeBSD bug reportThis list is incomplete — feel free to ping me if I left something out!
I did a bunch of work on issue #2080, with great feedback from Void Linux (meator
) and Fedora (tkb-github
).
./install
now accepts an arg for the build variant to install, rather than always assuming _bin/cxx-opt-sh/oils-for-unix
--help
for the 3 scripts packagers run:
./configure
_build/oils.sh
./install
This release has tons of changes, and we're keeping the docs updated.
ENV
.Str
, BashArray
, BashAssoc
Str
, Int
, ... List
, Dict
, ... Func
, Proc
(((
not being parsed like bash (Samuel hit this, discussed on the #nix
channel)->
and &
I realize that we need more blog posts to explain these features in a friendly way. For example, we have a nice design for string literals that I haven't gotten around to highlighting.
But we're testing YSH ourselves first. Feel free to join Zulip if you want to be a part of that process! Feedback and questions are welcome.
help
builtinWe're also updating the Oils Error Catalog, With Hints.
The shell sometimes display codes like OILS-ERR-12
, to lead you to more detail. Googling OILS-ERR-12
now finds these details, or you can use help
to print a direct link:
ysh-0.25.0$ help OILS-ERR-12
https://oils.pub/release/0.25.0/doc/error-catalog.html#oils-err-12
As usual, the breaking changes are in YSH only. OSH is very stable because most changes are bug fixes in bash features.
ENV
This is the most noticeable breaking change. Albin Otterhall and others ran into it, discussed on Zulip.
I mentioned the ENV
object in the objects post. I think of it as a new namespace, and it also uses an Obj
as a stack of dictionaries.
Feature | OSH / POSIX Shell | Breaking YSH Change |
---|---|---|
Read Env Var |
|
|
Permanently Set Env Var |
|
|
Temporarily Set Env Var |
|
(unchanged) |
You may notice that setglobal
is a bit verbose, and I agree.
But it's more explicit, and doesn't introduce new rules into the language. Feedback on this is still welcome.
You can also write setenv
or sh-env
in pure YSH, and Julian has done something like this.
This code now behaves as you expect:
read <<< '''
1
2
3
'''
It has three lines, not 4! Adding an extra \n
was inherited from bash and mksh, and doesn't make sense in YSH. This is technically a breaking change.
1 .. 3
replacedThere are now two operators that are more explicit:
Syntax | Values | Name |
---|---|---|
1 ..< 3 |
1 2 | half-open range |
1 ..= 3 |
1 2 3 | closed range |
This was implemented by Aidan, and motivated by feedback from bar-g
.
Using the old ..
operator will suggest ..<
or ..=
.
args.ysh
takes Type objects, not StringsI mentioned this in the post on objects. The API now looks like:
parser (&spec)
flag -c --count (Int) # no quotes around 'Int'
flag -c --source (List[Int]) # parameterized type object
}
Procs are now defined in the local scope. So this is valid:
proc p {
proc inner {
echo hi
}
pp (inner) # inspect the value
}
I also removed shopt -s redefine_proc_func
. It inhibits metaprogramming, and is no longer needed now that we have
modules with namespaces!
eval()
and evalExpr()
movedThey're now methods on the io
Object:
eval (b)
was removed, in favor of call io->eval(b)
call evalExpr(ex)
was removed, in favor of call io->evalExpr(ex)
Why are they both methods on io
? I updated the Oils reference with examples of expressions that have effects:
var e1 = ^[ myplace->setValue(42) ] # memory operation
var e2 = ^[ $(echo 42 > hi) ] # I/O operation
You can now break continue return
out of a loop when you're inside a block.
This is technically a breaking change: we used to break
from the block, not the surrounding loop. This was issue 2039.
fopen
builtin -> redir
I renamed the fopen
builtin to redir
, based on this use case:
redir 2>&1 {
call io->eval(b)
} | wc -l
fopen
is retained for backward compatibility, but will be removed eventually. (I think Samuel mentioned once that fopen
is not the best name.)
Note: Right now, you can't write
call io->eval (b) 2>&1
Instead, you have to use the redir { }
block. This is a known parsing issue: #language-design > Parsing issue with commands that end with expressions
a is b
and a is not b
can now compare values of different types.
args.ysh
~==
operator to accept strings that look like negative numbers.In arithmetic ops like x + y
, YSH has always converted strings to integers:
var x = '42' # string, not integer
var sum = s + 1 # integer 43
To be consistent, we now do the same thing for List
indices:
var s = mylist[x] # get value at index 42
setvar mylist[x] = '9' # set value at index 42
setvar mylist[x] += 5 # increment value at index 42
And for the operands to Slice
and Range
. That is, a[x:y]
and x ..< y
now have the same rule that mylist[x]
and x + y
do.
This came up a few times when writing YSH: #language-design > Lessons learned writing YSH code
printf
- issue 2107trap ulimit
, YSH, ...This is something that other shells don't do! They silently overflow, which means that their behavior depends on the underlying C compiler and platform. We still have more work to do here, but the plan is for all integer ops in YSH to be well-defined.
test --true
and test --false
This is a nicer way to combine commands and expressions in conditionals.
if test --file $name && test --true $[myfunc(name)] {
echo yes
}
This feature was based on feedback from Will Clardy and Julian Brown.
Aidan also added hints that detect when you use ||
instead of or
, or &&
instead of and
. (They use a simple "over-lexing" strategy.)
Note: some of our most common feedback shows that the distinction between YSH commands vs. expressions is not always natural. That is, mixing shell and Python/JS is natural for some people, but not for others. We'll continue to work on this issue.
test (42)
- thanks to Will Clardy for finding thisreturn [x]
was allowed; the right syntax is return (x)
pp [x]
-- it's now pp (x)
assert [42 === x]
. Compared with assert (42 === x)
, the square brackets means that the unevaluated expression can be "destructured" and inspected.value.Place
args.ysh
$PWD
variable
cd
no longer depends on $PWD
\w
prompt variable no longer depends on $PWD
str()
function now accepts the types Null
, Bool
, and Eggex
.
$[stringify]
and @[stringify_each_elem]
setVar()
for "dynamic binding"Improvements by Aidan:
Str.split()
method now accepts an eggex.Str.replace()
fix: avoid infinite loop on match of zero length, like we do in Str.split()
New vm
object:
vm.id(obj)
function for value identity, similar to Python
List Dict Obj
. This is because values of type Bool Int Float Str
may not be managed by the GC.vm.getFrame(0)
to retrieve a value of type Frame
Now we can pretty print the globals in both OSH and YSH:
osh$ = dict(vm.getFrame(0))
Try it! This is part of
Now let's talk about the themes in the title: Closures, Objects, and Namespaces.
Why does YSH need closures?
We ran into more use cases:
Str.replace()
looks like ^"match = $first"
. It's a value of type Expr
.first
variable should be captured, and now is.where
in my-ls | where [size > max]
is also a value of type Expr
.max
variable should be captured, and now is.The next sections might be cleaer if I clarify that there are many ways of talking about the same thing:
Now let's see what's changed.
Command
values Are ClosuresProcs can take block arguments, which are denoted by { }
, and they are of type Command
:
var x = 42
myproc {
echo $x # x refers to the variable above
}
Reminder: this is how you write a block expression that's not an argument:
var x = 42
var myblock = ^(echo $x) # whenever this block is evaluated,
# x refers to the variable above
(The ^(echo $x)
syntax is similar to $(echo $x)
.)
This is perhaps a bit confusing:
cd
are not of type Command
.CommandFrag
("unbound").So they are not closures. This is because we want to be able to reference variables created in the block later:
cd /tmp {
var listing = $(ls -x -y -z)
}
echo $listing # should refer to the variable in the block
I also think of cd
like an "inline proc", in that invoking it doesn't push and pop a new stack frame.
There may be a way to resolve this inconsistency, or we can just live with it. Again, feedback is welcome.
Expr
values are ClosuresSimilarly, expressions are closures:
var x = 42
var e1 = ^[x + 1] # value of type Expr
var e2 = ^"x = $x" # another value of type Expr
p [x + 1] # another one, equivalent to:
p (^[x + 1])
Another related design note:
What still needs to be done?
For example:
for x in a b c {
myproc { echo $x } # x should be captured!
when [size > x] { echo big } # ditto
}
It's worth mentioning that the material on closures in Crafting Interpreters was very helpful. This book helped us with garbage collection, hash tables (e.g. deletion/tombstones), and closures!
Now let's talk about objects. Objects and closures are both ways of bundling code and data.
Languages like Python, JavaScript, Lua, and Ruby all have both objects and closures.
I showed the new API in the objects post:
var obj = Obj.new({x: 42}, null)
var mydict = first(obj)
var parent = rest(obj)
I would like this shorter API:
var obj = Obj({x: 42}, null) # no .new
But that requires the special __call__
method, which we don't have yet.
__invoke__
- Objects can be invoked like procsYou can now invoke objects with the same syntax as procs:
my-object arg1 arg2
You do this by giving them an __invoke__
meta-method. Docs:
At first, this was motivated by the use case of generating procs dynamically, which Julian asked about. We had solutions based on:
eval $mystr
parseCommand()
and then io->eval()
And then I decided to experiment with invokable objects. It then played a crucial rule in the implementation of modules:
my-module my-proc
So it's here to stay. I anticipate many more uses of it:
ctx
builtin, used in args.ysh
List[Str]
I created type objects like List
and Dict
, and defined the [
operator on them.
So now List[Str]
and Dict[Str, Int]
evaluate to singleton objects. This was for the args.ysh
use case, mentioned above, and discussed in the objects post.
dir()
function"I use this slogan to explain the motivation.
I want users to be able to discover shell by typing — by interacting with the interpreter. Not by reading the manual!
Let's see what changed.
Breaking change: I added shopt --set no_init_globals
, which means that YSH doesn't initialize certain globals, like SHELLOPTS
. This is part of organizing globals into namespaces, which is still ongoing. Feedback is welcome.
__builtins__
objectWe moved functions like len()
and types like Float
to a __builtins__
object. It serves the same purpose as __builtins__
in Python.
Example:
ysh$ = len
<BuiltinFunc 0x7fa1e1842f50>
ysh$ = __builtins__
(Obj) <Obj 0x7fa1e1970d20>
ysh$ = __builtins__.len'
<BuiltinFunc 0x7fa1e1842f50>
In YSH, a typical variable lookup now has three steps:
__builtins__
So builtins no longer pollute the global namespace.
__defaults__
object, consulted after ENV
For example, we have __defaults__.PATH
and __defaults__.PS1
.
keys() values() get()
are Free FunctionsWe used to have d => keys()
, but now it's just keys(d)
.
Why? Method calls are now obj.method()
, not obj => method()
. And this causes a conflict for Dict
, which supports mydict.attr
.
The =>
syntax is for function chaining, though it's still allowed for method calls.
I demonstrated in the objects post. We did this because we use it in the YSH standard library!
Python-like modules are nice and convenient! (Both JavaScript and Lua lacked modules for a long time, and later added them.)
read -u
properly fails as unimplemented
meithecatte
for figuring this out!shopt -s ignore_shopt_not_impl
shopt -p
can exit non-zero, like bash$PS1
isn't setWhen $PS1
is not set, this is the default prompt:
ysh-0.23.0$
When it is, we want OSH versus YSH to look like this:
currentdir$
ysh currentdir$
A couple days ago, I announced that we're (finally) moving to the oils.pub domain. This is actually the last post on oilshell.org
! I put it here because the 0.24.0 tarball is also published on this domain.
In that post, I gave a sense for what's in Oils 0.25.0, which is already released: bash compatibility, and "under the hood" improvements to our metalanguages.
I published a skeleton for a Vim syntax plugin:
It needs to be fleshed out, and I want to make it easy to write syntax highlighters for SublimeText, TreeSitter, Helix, and more.
So I expect that the experience of finishing the Vim plugin will feed back into the YSH language design! We can make the syntax simpler, mainly by disallowing legacy shell syntax:
Here's some brainstorming for the rest of 2025:
Let me know what you think in the comments. Happy new year!
Some of these issues are mentioned above, and some are not.
#2118 | strict_errexit message missing code location |
#2114 | printf errors can cause status 1, rather than being fatal |
#2110 | bug in old version of dash shell causes _build/oils.sh to start too many compilers in parallel |
#2108 | Ctrl-C causes Interrupted system call |
#2107 | printf crashes with ValueError when integers are large |
#2104 | Crash with setvar on out-of-bounds list index |
#2096 | ysh breaking: Replace 1 .. 5 range syntax with 1 ..< 5 half open and 1 ..= 5 closed range |
#2094 | allow && || in YSH conditions and add test --true --false |
#2080 | install script may not match what distros want - Void Linux, stripped binary, binary location when cross-compiling, etc. |
#2078 | Crash with dict literal |
#2074 | members of context managers are uninitialized and rooted |
#2055 | Trap does not check for the first argument being an unsigned integer |
#2039 | executing blocks that contain return/break/continue/error is inconsistent with eval on strings |
These metrics help me keep track of the project. Let's compare this release with the previous one, version 0.23.0.
OSH continues to make progress, with 20 more tests passing:
Everything works in fast C++, even though we write typed Python:
vars-special
error as in the last release, which seems to be an artifact of the test harness. I just fixed it.YSH made more progress, with 87 more tests passing:
Likewise, everything still works in C++:
I don't recall why the parser got faster:
Oils is generally getting faster, which is good!
Not much change in parser memory usage:
parse.configure-coreutils
1.65 M objects comprising 41.1 MB, max RSS 46.6 MBparse.configure-coreutils
1.65 M objects comprising 41.8 MB, max RSS 47.5 MBWe got faster on a compute-bound workload:
fib
takes 27.6 million irefs, mut+alloc+free+gcfib
takes 25.8 million irefs, mut+alloc+free+gcI think this improvement was due to removing a duplicate hash lookup in Python, e.g. if x in dict: foo = d[x]
. (These release notes aren't always complete, and sometimes the benchmarks remind me of improvements we made!)
No change on a I/O bound workload:
configure
configure
configure
Again, our measurements have noise when comparing OSH to bash:
configure.cpython
configure.util-linux
But it's a good sign that, compared with a couple releases ago, our worst numbers are getting closer to bash.
YSH has the biggest delta in lines of code, but it's still small:
And generated C++:
And compiled binary size: