Warning: Work in progress! Leave feedback on Zulip or Github if you'd like this doc to be updated.

Command Language

This chapter in the Oils Reference describes the command language for both OSH and YSH.

Table of Contents
Quick Sketch: What's a Command?
Commands
simple-command
semicolon ;
Conditional
case
if
true
false
colon :
bang !
and &&
or ||
Iteration
while
until
for
for-expr-sh
Control Flow
break
continue
return
exit
Grouping
sh-func
sh-block
subshell
Concurrency
pipe
ampersand
Redirects
redir-file
redir-desc
here-doc
Other Command
dparen ((
time
YSH Command
proc
ysh-if
ysh-case
ysh-while
ysh-for
equal
call
typed-arg
lazy-expr-arg
block-arg

Quick Sketch: What's a Command?

OSH:

print-files() {
  for name in *.py; do
    if test -x "$name"; then
      echo "$name is executable"
    fi
  done
}

YSH:

proc print-files {
  for name in *.py {
    if test -x $name {  # no quotes needed
      echo "$name is executable"
    }
  }
}

Commands

simple-command

Commands are composed of words. The first word may by the name of a shell builtin, an Oil proc / shell "function", an external command, or an alias:

echo hi               # a shell builtin doesn't start a process
ls /usr/bin ~/src     # starts a new process
myproc "hello $name"
myshellfunc "hello $name"
myalias -l

Redirects are also allowed in any part of the command:

echo 'to stderr' >&2
echo >&2 'to stderr'

echo 'to file' > out.txt
echo > out.txt 'to file'

semicolon ;

Run two commands in sequence like this:

echo one; echo two

or this:

echo one
echo two

Conditional

case

Match a string against a series of glob patterns. Execute code in the section below the matching pattern.

path='foo.py'
case "$path" in
  *.py)
    echo 'python'
    ;;
  *.sh)
    echo 'shell'
    ;;
esac

if

Test if a command exited with status zero (true). If so, execute the corresponding block of code.

Shell:

if test -d foo; then
  echo 'foo is a directory'
elif test -f foo; then
  echo 'foo is a file'
else
  echo 'neither'
fi

Oil:

if test -d foo {
  echo 'foo is a directory'
} elif test -f foo {
  echo 'foo is a file'
} else {
  echo 'neither'
}

true

Do nothing and return status 0.

if true; then
  echo hello
fi

false

Do nothing and return status 1.

if false; then
  echo 'not reached'
else
  echo hello
fi

colon :

Like true: do nothing and return status 0.

bang !

Invert an exit code:

if ! test -d /tmp; then   
  echo "No temp directory
fi

and &&

mkdir -p /tmp && cp foo /tmp

or ||

ls || die "failed"

Iteration

while

POSIX

until

POSIX

for

For loops iterate over words.

Oil style:

var mystr = 'one'
var myarray = :| two three |

for i in $mystr @myarray *.py {
  echo $i
}

Shell style:

local mystr='one'
local myarray=(two three)

for i in "mystr" "${myarray[@]}" *.py; do
  echo $i
done

Both fragments output 3 lines and then Python files on remaining lines.

for-expr-sh

A bash/ksh construct:

for (( i = 0; i < 5; ++i )); do
  echo $i
done

Control Flow

These are keywords in Oil, not builtins!

break

Break out of a loop. (Not used for case statements!)

continue

Continue to the next iteration of a loop.

return

Return from a function.

exit

Exit the shell process with the given status:

exit 2

Grouping

sh-func

POSIX:

f() {
  echo args "$@"
}
f 1 2 3

sh-block

POSIX:

{ echo one; echo two; }

Note the trailing ; -- which isn't necessary in Oil.

subshell

( echo one; echo two )

Use forkwait in Oil instead.

Concurrency

pipe

ampersand

CMD &

The & language construct runs CMD in the background as a job, immediately returning control to the shell.

The resulting PID is recorded in the $! variable.

Redirects

redir-file

Examples of redirecting the stdout of a command:

echo foo > out.txt   # overwrite out.txt
date >> stamp.txt    # append to stamp.txt

Redirect to the stdin of a command:

cat < in.txt

Redirects are compatible with POSIX and bash, so they take descriptor numbers on the left:

make 2> stderr.txt   # '2>' is valid, but '2 >' is not

Note that the word argument to file redirects is evaluated like bash, which is different than other arguments to other redirects:

tar -x -z < Python*  # glob must expand to exactly 1 file
tar -x -z < $myvar   # $myvar is split because it's unquoted

In other words, it's evaluated as a sequence of 1 word, which produces zero to N strings. But redirects are only valid when it produces exactly 1 string.

(Related: YSH uses shopt --set simple_word_eval, which means that globs that match nothing evaluate to zero strings, not themselves.)

redir-desc

Redirect to a file descriptor:

echo 'to stderr' >&2

here-doc

TODO: unbalanced HTML if we use <<?

cat <<EOF
here doc with $double ${quoted} substitution
EOF

myfunc() {
        cat <<-EOF
        here doc with one tab leading tab stripped
        EOF
}

cat <<< 'here string'

Other Command

dparen ((

time

time [-p] pipeline

Measures the time taken by a command / pipeline. It uses the getrusage() function from libc.

Note that time is a KEYWORD, not a builtin!

YSH Command

proc

Procs are shell-like functions, but with named parameters, and without dynamic scope (TODO):

proc copy(src, dest) {
  cp --verbose --verbose $src $dest
}

Compare with sh-func.

ysh-if

Command or expression:

if (x > 0) {
  echo 'positive'
}

ysh-case

case $x {
  # balanced parens around patterns
  (*.py)     echo 'Python' ;;
  ('README') echo 'README' ;;  # consatnt words must be quoted
  (*)        echo 'Other'  ;;
}

ysh-while

Command or expression:

var x = 5
while (x < 0) {
  setvar x -= 1
}

ysh-for

Two forms for shell-style loops:

for name in *.py {
  echo "$name"
}

for i, name in *.py {
  echo "$i $name"
}

Two forms for expressions that evaluate to a List:

for item in (mylist) {
  echo "$item"
}

for i, item in (mylist) {
  echo "$i $item"
}

Three forms for expressions that evaluate to a Dict:

for key in (mydict) {
  echo "$key"
}

for key, value in (mydict) {
  echo "$key $value"
}

for i, key, value in (mydict) {
  echo "$i $key $value"
}

equal

The = keyword evaluates an expression and shows the result:

oil$ = 1 + 2*3
(Int)   7

It's meant to be used interactively. Think of it as an assignment with no variable on the left.

call

The call keyword evaluates an expression and throws away the result:

var x = :| one two |
call x->append('three')
call x->append(['typed', 'data'])

typed-arg

Internal commands (procs and builtins) accept typed arguments.

json write (myobj)

Block literals have a special syntax:

cd /tmp {
  echo $PWD
}

This is equivalent to:

var cmd = ^(echo $PWD)  # unevaluated command

cd /tmp (cmd)  # pass typed arg

lazy-expr-arg

Expressions in brackets like this:

assert [42 === x]

Are syntactic sugar for:

assert (^[42 === x])

That is, it's single arg of type value.Expr.

block-arg

Blocks can be passed to builtins (and procs eventually):

cd /tmp {
  echo $PWD  # prints /tmp
}
echo $PWD

Compare with sh-block.

vim: sw=2


Generated on Wed, 06 Dec 2023 01:04:01 -0500