Idio Shell¶
This is where Idio stops behaving like a programming language and starts behaving like a shell.
We already have the fundamental behaviour, an unbound symbol
evaluates to itself. That’s going to occur for any of the elements of
cmd args where we are particularly interested in
cmd.
We don’t get everything our own way, though, as there is a bit of a semantic discrepancy between a programming language and a shell. The programming language is going to win so we need to bring our own hoop to jump through for a corner case of the shell aspect to work.
If you have a symbol on its own, it looks much like, say, a value being returned from a block. It’s perfectly reasonable to return a symbol from a expression so that’s what happens.
Of course, as a shell, we want to execute the external program called
ls (or env or make or …) for which we are forced to wrap
the single symbol in parentheses.
If you pass any arguments or use an operator the right thing happens but a single element needs to be wrapped:
make ; gets the symbol make
(make) ; runs the command /usr/bin/make (or wherever)
make -k ; runs the command /usr/bin/make -k
make > "/dev/null" ; runs the command /usr/bin/make
Annoying.
Command Execution¶
If the VM sees a symbol (or a string) in functional position,
ie. cmd then it will try to find and execute the external
command.
cmd¶
If cmd contains a / then it is assumed to be the
executable path as is, otherwise cmd is searched for on
PATH.
args¶
Arguments are expanded into strings for execve(2) fairly simply. Most Idio types do not have a sensible string representation (think: functions, hash tables and arrays etc.) and it is an error to try to use them as external command arguments.
Those that do:
numbers (fixnums, bignums and C/ types) are printed
strings in their UTF-8 form
unicode in their UTF-8 form
symbols are checked for the globbing meta-characters
*,?and[.If they do contain such characters then the results of glob(3) are inserted into
argsotherwise the UTF-8 representation of the symbol is used.a pair is checked to see if it is a list of strings (nominally the result of calling glob) and if so is inserted into
argsotherwise is an error
The glob-expansion (or list of strings) are the only elements that will increase the number of arguments to the external command.
Variables¶
There is a downside for argument expansion where a casually passed argument happens to be an Idio variable which could produce an unexpected error.
If sort was a make target then:
make sort
will likely produce an ^rt-command-argv-type-error because
sort was evaluated as the Idio function sort and a closure can’t be converted to a string.
Quoting any potential clashes avoids the evaluation:
make 'sort
Return Value¶
External commands will return #t or #f depending on whether
they exited with 0 or not. Well, they will initiate a return of
#f if they fail.
You can use this in logical expressions (and, or, not) or conditional expressions (if, cond) like in the shell.
Idio> (true) or (false)
#t
Idio> (true) and (false)
#f
job 33041: (false): completed: (exit 1)
Idio> if (true) (printf "Y\n") (printf "N\n")
Y
#<unspec>
Idio> if (false) (printf "Y\n") (printf "N\n")
N
#<unspec>
job 33043: (false): completed: (exit 1)
Note
We get the job notification messages because this session is interactive. A script would not report job notifications.
Errors¶
It’s worth repeating that Idio is reluctant to let any external command errors go unnoticed. As part of the flow of the script, if something failed, especially an external command, Idio should stop.
Note
Idio is far more lenient in interactive sessions as it defaults to suppressing pipefail and exit-on-error and allows expressions to be aborted back to the top-level.
By default, any external command error, including those in pipelines will result in a fatal error.
An ^rt-command-status-error will be raised:
if any command in a pipeline is killed or exits non-zero and the dynamic variable
suppress-pipefail!is falsefalse | true | true
will be a fatal error
suppress-pipefail! = #t false | true | true
will succeed
Note
The equivalent of
suppress-pipefail! = #tis the default for most shells and an interactive Idio session.suppress-pipefail! = #fis the default for Idio scripts.if a standalone command or the rightmost command in a pipeline is killed or exits non-zero
If you handle ^rt-command-status-error conditions then you make
the decisions.
However, if your code eventually invokes the default-rcse-handler
then it consults the dynamic variable suppress-exit-on-error!. If
that is false, the default, Idio will exit or kill itself
to give the same status to its parent. Hence, both:
true | true | false
(false)
will be fatal, whereas:
suppress-exit-on-error! = #t
true | true | false
(false)
will both succeed.
Note
suppress-exit-on-error! = #t is equivalent to the shell’s set
-e.
Async Commands¶
There is a corner case for asynchronous commands, those that are part of Process Substitution, where they are not part of the flow, they are adjunct to it.
Here, the default is to be told that the command failed (but not exit because of it).
An ^rt-async-command-status-error will be raised under the same
broad conditions as for normal standalone commands or pipelines.
You can handle the condition yourself (if you want to force an exit)
or change the suppress-async-command-report! dynamic variable to a
non-false value if you don’t want to be told about it.
Operators¶
Much of the heavy lifting of the shell work is done with infix operators:
<,>etc. for I/O redirection|for pipelines
In fact, these operators are written in Idio itself demonstrating the ability to extend the language’s functionality.
I/O Operators¶
The I/O operators, in particular, aren’t as flexible as a normal shell largely because, as a programming language, Idio can carry file descriptors around, or libc/dup2 them or whatever, which a shell can’t. So you should be doing that, not trampling over file descriptor 3.
The I/O operators are slightly less flexible in terms of positioning, they can only be placed after the command to be run, partly because they are infix operators:
ls -l > "foo"
cat < "foo"
as, essentially, the infix I/O operation, here, > and <, is
taken to be separating the command from its source/target expression.
The I/O operators only handle redirection of stdin, stdout and stderr. To what, though, is a bit more interesting.
By and large, you would be using handles in Idio where Idio supports the notion of current input, output and error handles.
Those current handles aren’t restricted to file or pipe handles, they can be string handles too.
Which, by extension, means you can do I/O redirection of an external command to or from string handles, for example:
osh := (open-output-string)
cat "foo" > osh
str := get-output-string osh
Of course, this sort of indirection to get the contents of a file as a string is Command Substitution, in Bash parlance, for which there is:
str := collect-output cat "foo"
which works just as well for a pipeline:
str := collect-output zcat "foo.tgz" | tar tf \-
collect-output is one of the job meta-commands and, like the others, could do with a syntactic short
cut.
Syntax¶
The general form is ... op expr where the options for
expr vary by op:
<,>,>>,2>,2>>exprcan be:an (an appropriately directioned) FD handle (file or pipe)
an (an appropriately directioned) string handle
a string indicating a file name which will be opened in the appropriate direction
#nmeaning/dev/nullwill be opened in the appropriate direction
with the
>>variants meaning append<&,>&,2>&exprcan be:an (an appropriately directioned) FD handle (file or pipe)
an (an appropriately directioned) string handle
a
fixnumorC/intinteger:0 indicating the current input handle
1 indicating the current output handle
2 indicating the current error handle
Pipelines¶
Pipelines work as you would expect. The pipeline operator, |, has
a higher priority than the I/O redirection operators meaning that, in
effect, I/O redirection is more closely tied to the command:
ls > #n | wc
will mean wc will see no input.
Meta-Commands¶
There are several job meta-commands which affect the overall pipeline rather than any individual pipe within the pipeline. They are the first word(s) in a pipeline:
collect-outputwhich collects the output of the command and returns it as a stringhn := collect-output uname -n
hn will be a string from the output of
uname -nNote
collect-outputwill strip-string the output from the command of trailing newlines.fg-jobis the default and runs the job in the foregroundbg-jobruns the job in the backgroundbg-job sleep 60
bg-jobreturns#t(backgrounding a job is always successful)pipe-intoestablishes a pipe as the input for the job and returns an output pipe handleYou can write into the pipe handle to generate input for the job.
This is similar to Perl’s
open ("| ...").oph := pipe-into sed -e "s/^/boo! /" hprintf oph "%s\n" HOSTNAME close-handle oph
for which something like the following will be printed
boo! hostname
named-pipe-intoestablished a named pipe as the input for the job and the pipe’s name is returned.The caller is expected to open the returned pipe name for writing.
On many systems the pipe’s name will be
/dev/fd/nbut on some systems it will be a FIFO in the file system.This is the equivalent of Bash’s Process Substitution form
>(...).pipe-fromestablishes a pipe as the output for the job and returns an input pipe handleYou can read from the pipe handle to get the job’s output.
This is similar to Perl’s
open ("... |").named-pipe-fromestablished a named pipe as the output for the job and the pipe’s name is returned.The caller is expected to open the returned pipe name for reading.
On many systems the pipe’s name will be
/dev/fd/nbut on some systems it will be a FIFO in the file system.This is the equivalent of Bash’s Process Substitution form
<(...).timeflags that a report on the accumulated resources of the job should be produced when the job completesIdio> time sleep 2 Real 2.011 User 0.001 Syst 0.001 #t
Both of the named-pipe-* variants are asynchronous commands.
Not all of the meta-commands are compatible.
Last built at 2026-01-04T22:40:02Z+0000 from da47fd3 (dev)