source | all docs for version 0.10.0 | all versions | oilshell.org
This is an informal, lightly-organized list of recommended idioms for the Oil language. Each section has snippets labeled No and Yes.
No:
local x='my song.mp3'
ls "$x" # quotes required to avoid mangling
Yes:
var x = 'my song.mp3'
ls $x # no quotes needed
No:
local myflags=( --all --long )
ls "${myflags[@]}" "$@"
Yes:
var myflags = %( --all --long )
ls @myflags @ARGV
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
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.
In other words, avoid groveling through backslashes and spaces in shell.
Instead, emit and consume the QSN and QTT interchange formats.
Custom parsing and serializing should be limited to "the edges" of your Oil programs.
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
That is, take advantage of the invariants that the IO builtins respect. (doc in progress)
proc
construct and flagspec
!ls
in Python with
little effort.write
Builtin Is Simpler Than printf
and echo
No:
printf '%s\n' "$mystr"
Yes:
write -- $mystr
The write
builtin accepts --
so it doesn't confuse flags and args.
No:
echo -n "$mystr" # breaks if mystr is -e
Yes:
write --end '' -- $mystr
write -n -- $mystr # -n is an alias for --end ''
var myarray = %(one two three)
write -- @myarray
read
builtinNo:
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
No:
read -d '' # harder to read, easy to forget -r
Yes:
read --all :mystr
\0
(consume find -print0
)No:
# Obscure syntax that bash accepts, but not other shells
read -r -d '' myvar
Yes:
read -0 :myvar
shopt
Instead of set
Using a single builtin for all options makes scripts easier to read:
Discouraged:
set -o errexit
shopt -s dotglob
Idiomatic:
shopt --set errexit
shopt --set dotglob
(As always, set
can be used when you care about compatibility with other
shells.)
:
When Mentioning Variable NamesOil accepts this optional "pseudo-sigil" to make code more explicit.
No:
read -0 record < file.bin
echo $record
Yes:
read -0 :record < file.bin
echo $record
--long-flags
Easier to write:
test -d /tmp
test -d / && test -f /vmlinuz
shopt -u extglob
Easier to read:
test --dir /tmp
test --dir / && test --file /vmlinuz
shopt --unset extglob
No:
( cd /tmp; echo $PWD ) # subshell is unnecessary (and limited)
No:
pushd /tmp
echo $PWD
popd
Yes:
cd /tmp {
echo $PWD
}
No:
set +o errexit
myfunc # without error checking
set -o errexit
Yes:
shopt --unset errexit {
myfunc
}
forkwait
builtin for Subshells, not ()
No:
( not_mutated=foo )
echo $not_mutated
Yes:
var not_mutated = 'bar'
forkwait {
setvar not_mutated = 'foo'
}
echo $not_mutated
fork
builtin for async, not &
No:
myfunc &
{ sleep 1; echo one; sleep 2; } &
Yes:
fork { myfunc }
fork { sleep 1; echo one; sleep 2 }
$1
, $2
, ...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
}
"$@"
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"
}
You can also use the implicit ARGV
variable:
proc p {
cp -- @ARGV /tmp
}
declare -n
Out params are one way to "return" values from a proc
.
No:
f() {
local in=$1
local -n out=$2
out=PREFIX-$in
}
myvar='init'
f zzz myvar # assigns myvar to 'PREFIX-zzz'
Yes:
proc f(in, :out) { # : is an out param, i.e. a string "reference"
setref out = "PREFIX-$in"
}
var myvar = 'init'
f zzz :myvar # assigns myvar to 'PREFIX-zzz'.
# colon is required
That is, dynamic scope is turned off when procs are invoked.
Here's an example of shell functions reading variables in their caller:
bar() {
echo $foo_var # looks up the stack
}
foo() {
foo_var=x
bar
}
foo
In Oil, you have to pass params explicitly:
proc bar {
echo $foo_var # error, not defined
}
Shell functions can also mutate variables in their caller! But procs can't do this, which makes code easier to reason about.
Oil has a few lightweight features that make it easier to organize code into files. It doesn't have "namespaces".
Suppose we are running bin/mytool
, and we want BASE_DIR
to be the root of
the repository so we can do a relative import of lib/foo.sh
.
No:
# All of these are common idioms, with caveats
BASE_DIR=$(dirname $0)/..
BASE_DIR=$(dirname ${BASH_SOURCE[0]})/..
BASE_DIR=$(cd $($dirname $0)/.. && pwd)
BASE_DIR=$(dirname (dirname $(readlink -f $0)))
source $BASE_DIR/lib/foo.sh
Yes:
const BASE_DIR = "$this_dir/.."
source $BASE_DIR/lib/foo.sh
# Or simply:
source $_this_dir/../lib/foo.sh
The value of _this_dir
is the directory that contains the currently executing
file.
No:
# libfoo.sh
if test -z "$__LIBFOO_SH"; then
return
fi
__LIBFOO_SH=1
Yes:
# libfoo.sh
module libfoo.sh || return 0
No:
deploy() {
echo ...
}
"$@"
Yes
proc deploy() {
echo ...
}
runproc @ARGV # gives better error messages
Oil Fixes Shell's Error Handling (errexit
) once and
for all! Here's a comprehensive list of error handling idioms.
&&
Outside of if
/ while
It's implicit because errexit
is on in Oil.
No:
mkdir /tmp/dest && cp foo /tmp/dest
Yes:
mkdir /tmp/dest
cp foo /tmp/dest
It also avoids the Trailing &&
Pitfall mentioned at the end of the error
handling doc.
No:
ls /bad || true # OK because ls is external
myfunc || true # suffers from the "Disabled errexit Quirk"
Yes:
try ls /bad
try myfunc
errexit
is OnNo:
# set -e is enabled earlier
set +e
mycommand # this ignores errors when mycommand is a function
status=$? # save it before it changes
set -e
echo $status
Yes:
try mycommand
echo $_status
These idioms are OK in both shell and Oil:
if ! cp foo /tmp {
echo 'error copying' # any non-zero status
}
if ! test -d /bin {
echo 'not a directory'
}
To be consistent with the idioms below, you can also write them like this:
try cp foo /tmp
if (_status !== 0) {
echo 'error copying'
}
When the command is a shell function, you shouldn't use if myfunc
directly.
This is because shell has the Disabled errexit
Quirk, which is detected by
Oil's strict_errexit
.
No:
if myfunc; then # errors not checked in body of myfunc
echo 'success'
fi
Yes. The $0
Dispatch Pattern is a workaround that works in all shells.
if $0 myfunc; then # invoke a new shell
echo 'success'
fi
"$@" # Run the function $1 with args $2, $3, ...
Yes. Oil's try builtin sets the special _status
variable and returns
0
.
try myfunc # doesn't abort
if (_status === 0) {
echo 'success'
fi
try
Also Takes a BlockA block arg is useful for multiple commands:
try { # stops at the first error
chmod +x myfile
cp myfile /bin
}
if (_status !== 0) {
echo 'error'
}
No:
if ps | grep python; then
echo 'fouund'
fi
This is technically correct when pipefail
is on, but it's impossible for
Oil's strict_errexit
to distinguish it from if myfunc | grep python
ahead
of time (the "meta" pitfall). If you
know what you're doing, you can disable strict_errexit
.
Yes:
try {
ps | grep python
}
if (_status === 0) {
echo 'found'
}
# You can also examine the status of each part of the pipeline
if (_pipeline_status[0] !== 0) {
echo 'ps failed'
}
Similar to the pipeline example above:
No:
if ! comm <(sort left.txt) <(sort right.txt); then
echo 'error'
fi
Yes:
try {
comm <(sort left.txt) <(sort right.txt)
}
if (_status !== 0) {
echo 'error'
}
# You can also examine the status of each process sub
if (_process_sub_status[0] !== 0) {
echo 'first process sub failed'
}
(I used comm
in this example because it doesn't have a true / false / error
status like diff
.)
try {
var x = 42 / 0
echo "result is $[42 / 0]"
}
if (_status !== 0) {
echo 'divide by zero'
}
grep
, diff
, test
Oil's boolstatus
builtin distinguishes error from false.
No, this is subtly wrong. grep
has 3 different return values.
if grep 'class' *.py {
echo 'found' # status 0 means found
} else {
echo 'not found OR ERROR' # any non-zero status
}
Yes. boolstatus
aborts the program if egrep
doesn't return 0 or 1.
if boolstatus grep 'class' *.py { # may abort
echo 'found' # status 0 means found
} else {
echo 'not found' # status 1 means not found
}
More flexible style:
try grep 'class' *.py
case $_status {
(0) echo 'found'
;;
(1) echo 'not found'
;;
(*) echo 'fatal'
exit $_status
;;
}
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
No:
x=$(( 1 + 2*3 ))
(( x = 1 + 2*3 ))
Yes:
setvar x = 1 + 2*3
No:
(( i++ )) # interacts poorly with errexit
i=$(( i+1 ))
Yes:
setvar i += 1 # like Python, with a keyword
Arrays in Oil look like %(my array)
and ['my', 'array']
.
No:
local -a myarray=(one two three)
myarray[3]='THREE'
Yes:
var myarray = %(one two three)
setvar myarray[3] = 'THREE'
var same = ['one', 'two', 'three']
var typed = [1, 2, true, false, null]
Dicts in Oil look like {key: 'value'}
.
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'
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']
No:
if (( x > 0 )); then
echo 'positive'
fi
Yes:
if (x > 0) {
echo 'positive'
}
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']
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'
}
No:
if [[ $x =~ foo-([[:digit:]]+) ]] {
echo "${BASH_REMATCH[1]}" # first submatch
}
Yes:
if (x ~ / 'foo-' <d+> /) { # <> is capture
echo $_match(1) # first submatch
}
No:
if [[ $x == *.py ]]; then
echo 'Python'
fi
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'
;;
}
$RANDOM
vs. random()
LANG=C
vs. shopt --setattr LANG=C
errexit
). Oil fixes the
flaky error handling in POSIX shell and bash.