Oil Language Idioms

This is an informal, lightly-organized list of recommended idioms for the Oil language. Each section has snippets labeled No and Yes.

Table of Contents
Use Simple Word Evaluation to Avoid "Quoting Hell"
Substitute Variables
Splice Arrays
Explicitly Split, Glob, and Omit Empty Args
Iterate a Number of Times (Split Command Sub)
Avoid Ad Hoc Parsing and Splitting
Use New I/O Builtins
More Strategies For Structured Data
The write Builtin Is Simpler Than printf and echo
Write an Arbitrary Line
Write Without a Newline
Write an Array of Lines
Use Long Flags on the read builtin
Read a Line
Read a Whole File
Use Blocks to Save and Restore Context
Do Something In Another Directory
Temporarily Set Shell Options
Use the forkwait builtin for Subshells, not ()
Use the fork builtin for async, not &
Use Procs (Better Shell Functions)
Named Parameters
Variable Number of Arguments
"Out" Params as Return Values
Curly Braces Fix Semantic Problems
Procs Don't Have Dynamic Scope
If and errexit
Use Oil Expressions, Initializations, and Assignments (var, setvar)
Initialize and Assign Strings and Integers
Expressions on Integers
Mutate Integers
Initialize and Assign Arrays
Expressions on Arrays
Conditions and Comparisons
Substituting Expressions in Words
Use Egg Expressions instead of Regexes
Test for a Match
Extract Submatches
Glob Matching
TODO
Consider Using --long-flags for builtins
Don't use &&
Use catch to avoid Broken errexit When Using || and !
Source Files and Namespaces
Related Documents

Use Simple Word Evaluation to Avoid "Quoting Hell"

Substitute Variables

No:

local x='my song.mp3'
ls "$x"  # quotes required to avoid mangling

Yes:

var x = 'my song.mp3'
ls $x  # no quotes needed

Splice Arrays

No:

local myflags=( --all --long )
ls "${myflags[@]}" "$@"

Yes:

var myflags = %( --all --long )
ls @myflags @ARGV

Explicitly Split, Glob, and Omit Empty Args

Oil doesn't split arguments after variable expansion.

No:

local packages='python-dev gawk'
apt install $packages

Yes:

var packages = 'python-dev gawk'
apt install @split(packages)

Even better:

var packages = %(python-dev gawk)  # array literal
apt install @packages              # splice array

Oil doesn't glob after variable expansion.

No:

local pat='*.py'
echo $pat

Yes:

var pat = '*.py'
echo @glob(pat)   # explicit call

Oil doesn't omit unquoted words that evaluate to the empty string.

No:

local e=''
cp $e other $dest            # cp gets 2 args, not 3, in sh

Yes:

var e = ''
cp @maybe(e) other $dest     # explicit call

Iterate a Number of Times (Split Command Sub)

No:

local n=3
for x in $(seq $n); do  # No implicit splitting of unquoted words in Oil
  echo $x
done

Yes:

var n = 3
for x in @(seq $n) {   # Explicit splitting
  echo $x
}

Note that {1..3} works in bash and Oil, but the numbers must be constant.

Avoid Ad Hoc Parsing and Splitting

In other words, avoid groveling through backslashes and spaces in shell.

Instead, emit and consume the QSN and QTSV interchange formats.

Custom parsing and serializing should be limited to "the edges" of your Oil programs.

Use New I/O Builtins

These are discussed in the next two sections, but here's a summary.

write --qsn        # also -q
read --qsn :mystr  # also -q

read --line --qsn :myline     # read a single line
read --lines --qsn :myarray   # read many lines

That is, take advantage of the the invariants that the IO builtins respect. (doc in progress)

TODO: Implement and test these.

More Strategies For Structured Data

The write Builtin Is Simpler Than printf and echo

Write an Arbitrary Line

No:

printf '%s\n' "$mystr"

Yes:

write -- $mystr

The write builtin accepts -- so it doesn't confuse flags and args.

Write Without a Newline

No:

echo -n "$mystr"  # breaks if mystr is -e

Yes:

write --end '' -- $mystr
write -n -- $mystr  # -n is an alias for --end ''

Write an Array of Lines

var myarray = %(one two three)
write -- @myarray

Use Long Flags on the read builtin

Read a Line

No:

read line     # Bad because it mangles your backslashes!
read -r line  # Better, but easy to forget

Yes:

read --line   # also faster because it's a buffered read

TODO: implement this.

Read a Whole File

No:

mapfile -d ''
read -d ''

Yes:

read --all :mystr

TODO: implement this.

Use Blocks to Save and Restore Context

Do Something In Another Directory

No:

( cd /tmp; echo $PWD )  # subshell is unnecessary (and limited)

No:

pushd /tmp
echo $PWD
popd

Yes:

cd /tmp {
  echo $PWD
}

Temporarily Set Shell Options

No:

set +o errexit
myfunc  # without error checking
set -o errexit

Yes:

shopt --unset errexit {
  myfunc
}

TODO: Implement this.

Use the forkwait builtin for Subshells, not ()

No:

( not_mutated=foo )
echo $not_mutated

Yes:

forkwait {
  setvar not_mutated = 'foo'
}
echo $not_mutated

TODO: Implement this.

Use the fork builtin for async, not &

No:

myproc &

{ sleep 1; echo one; sleep 2; } &

Yes:

fork myproc

fork { sleep 1; echo one; sleep 2 }

TODO: Implement this.

Use Procs (Better Shell Functions)

Named Parameters

No:

f() {
  local src=$1
  local dest=${2:-/tmp}

  cp "$src" "$dest"
}

Yes:

proc f(src, dest='/tmp') {   # Python-like default values
  cp $src $dest
}

Variable Number of Arguments

No:

f() {
  local first=$1
  shift

  echo $first
  echo "$@"
}

Yes:

proc f(first, @rest) {  # @ means "the rest of the arguments"
  write -- $first
  write -- @rest        # @ means "splice this array"
}

"Out" Params as Return Values

No:

f() {
  local in=$1
  local -n out=$2

  out=PREFIX-$in
}

myvar='zzz'
f zzz myvar  # assigns myvar to 'PREFIX-zzz'

Yes:

proc f(in, :out) {  # : means accept a string "reference"
  setref out = "PREFIX-$in"
}

var myvar = 'zzz'
f zzz :myvar        # : means pass a string "reference" (optional)

TODO: Implement this

Curly Braces Fix Semantic Problems

Procs Don't Have Dynamic Scope

Shell functions can access variables in their caller:

foo() {
  foo_var=x;
  bar
}

bar() {
  echo $foo_var  # looks up the stack
}

foo

In Oil, you have to pass params explicitly:

proc bar {
  echo $foo_var  # error
}

TODO: Implement this.

If and errexit

Bug in POSIX shell:

if myfunc; then  # oops, errors not checked in myfunc
  echo hi
fi

Suggested workaround:

if $0 myfunc; then  # invoke a new shell
  echo hi
fi

"$@"

Oil extension, without an extra process:

if catch myfunc; then
  echo hi
fi

Even better:

if myfunc {  # implicit 'catch', equivalent to the above
  echo hi
}

Note that && and || and ! require an explicit catch:

No:

myfunc || fail
myfunc && echo 'success'
! myfunc

Yes:

catch myfunc || fail
catch myfunc && echo 'success'
! catch myfunc

This explicit syntax avoids breaking POSIX shell. You have to opt in to the better behavior.

TODO: Implement 'catch' and implicit catch

Use Oil Expressions, Initializations, and Assignments (var, setvar)

Initialize and Assign Strings and Integers

No:

local mystr=foo
mystr='new value'

local myint=42  # still a string in shell

Yes:

var mystr = 'foo'
setvar mystr = 'new value'

var myint = 42  # a real integer

Expressions on Integers

No:

x=$(( 1 + 2*3 ))
(( x = 1 + 2*3 ))

Yes:

setvar x = 1 + 2*3

Mutate Integers

No:

(( i++ ))  # interacts poorly with errexit
i=$(( i+1 ))

Yes:

setvar i += 1  # like Python, with a keyword

Initialize and Assign Arrays

Container literals in Oil look like %(one two) and %{key: 'value'}.

No:

local -a myarray=(one two three)
myarray[3]='THREE'

Yes:

var myarray = %(one two three)
setvar myarray[3] = 'THREE'

No:

local -A myassoc=(['key']=value ['k2']=v2)
myassoc['key']=V

Yes:

# keys don't need to be quoted
var myassoc = %{key: 'value', k2: 'v2'}
setvar myassoc['key'] = 'V'

Expressions on Arrays

No:

local x=${a[i-1]}
x=${a[i]}

local y=${A['key']}

Yes:

var x = a[i-1]
setvar x = a[i]

var y = A['key']

Conditions and Comparisons

No:

if (( x > 0 )); then
  echo positive
fi

Yes:

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

Substituting Expressions in Words

No:

echo flag=$((1 + a[i] * 3))  # C-like arithmetic

Yes:

echo flag=$[1 + a[i] * 3]    # Arbitrary Oil expressions

# Possible, but a local var might be more readable
echo flag=$['1' if x else '0']

Use Egg Expressions instead of Regexes

Test for a Match

No:

local pat='[[:digit:]]+'
if [[ $x =~ $pat ]]; then
  echo 'number'
fi

Yes:

if (x ~ /digit+/) {
  echo 'number'
}

Or extract the pattern:

var pat = / digit+ /
if (x ~ pat) {
  echo 'number'
}

Extract Submatches

No:

if [[ $x =~ ([[:digit:]]+) ]] {
  echo "${BASH_REMATCH[@]}"
}

Yes:

if (x ~ / <d+> /) {  # <> is capture
  argv.py @M         # special M variable
}

Glob Matching

No:

if [[ $x == *.py ]]; then
  echo Python
fi

TODO: Implement the ~~ operator.

Yes:

if (x ~~ '*.py') {
  echo 'Python'
}

No:

case $x in
  *.py)
    echo Python
    ;;
  *.sh)
    echo Shell
    ;;
esac

Yes (purely a style preference):

case $x {          # curly braces
  (*.py)           # balanced parens
    echo 'Python'
    ;;
  (*.sh)
    echo 'Shell'
    ;;
}

TODO

Consider Using --long-flags for builtins

Easier to write:

test -d /tmp
test -d / -a -f /vmlinuz

shopt -u extglob

Easier to read:

test --dir /tmp
test --dir / && test --file /vmlinuz

shopt --unset extglob

Style note: Prefer test to [, because idiomatic Oil code doesn't use "puns".

TODO: implement this.

Don't use &&

Because errexit is on in Oil, it's implicit.

No:

mkdir /tmp/dest && cp foo /tmp/destj

Yes:

mkdir /tmp/dest
cp foo /tmp/dest

Use catch to avoid Broken errexit When Using || and !

No:

# Bad POSIX behavior results in ignored errors
set -o errexit

myfunc || die "failed"  # Oil's strict_errexit disallows this
! myfunc                # and this

Yes:

# Oil respects errors, but catches at top level
set -o errexit

catch myfunc || die "failed"
! catch myfunc

TODO: Implement this.

Source Files and Namespaces

TODO

Related Documents


Generated on Thu Oct 8 13:33:44 PDT 2020