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 --falsebind builtin (work in progress)trap with an integer arg removes the trap (POSIX compatibility)_build/oils.shsetvar 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./installThis release has tons of changes, and we're keeping the docs updated.
ENV.Str, BashArray, BashAssocStr, 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.
ENVThis 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 -> redirI 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 --falseThis 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 $PWDstr() 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 FrameNow 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 $mystrparseCommand() 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.yshList[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 ENVFor 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:
configureconfigure
configureAgain, our measurements have noise when comparing OSH to bash:
configure.cpythonconfigure.util-linuxBut 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: