Skip to content

Shell scripting

edos-sh is the shell, installed as /bin/sh. It is a real scripting language rather than a command dispatcher: functions, control flow, arithmetic, globbing, heredocs and job control.

One thing separates it from POSIX sh on sight. Every block ends with end — there is no fi, no done, no esac, and no then or do to open one either.

#!/bin/sh
for file in $(ls /bin)
if [ -f /bin/$file ]
echo "program: $file"
end
end

Control flow works in scripts, not interactively.

Terminal window
sh script.sh # run a file
sh script.sh a b c # with positional arguments
sh -c "echo hello" # a single command
sh -e script.sh # exit on the first failure
./script.sh # needs a #!/bin/sh line

Assignment needs no keyword. export puts a variable in the environment children inherit.

Terminal window
NAME=value
export PATH=/bin
unset NAME
$? exit status of the last command
$0 script name, or sh when interactive
$1–$9 positional arguments
$# how many there are
$@ all of them

Double quotes expand, single quotes do not. ~ expands to $HOME.

Terminal window
echo "$HOME" # /
echo '$HOME' # $HOME

$(...) captures a command’s output, and nests.

Terminal window
FILES=$(ls /bin)
echo "count: $(echo $(ls /bin | wc -w))"

$(( ... )) evaluates integer arithmetic: + - * / %, comparisons < <= > >= == !=, !, parentheses, and assignment. Variables inside need no $.

Terminal window
echo $(( (2 + 3) * 4 )) # 20
i=0
i=$(( i + 1 ))

test, also spelled [, sets the exit status. Any command works as a condition: zero is true.

Terminal window
if [ -f /bin/sh ]
echo exists
elif [ -d /bin ]
echo directory
else
echo neither
end
Test True when
-f PATH a regular file
-d PATH a directory
-e PATH exists at all
-z STR / -n STR empty / non-empty
STR = STR / STR != STR strings match, or do not
A -eq B also -ne, -lt, -gt, -le, -ge
! EXPR negation

With [, the brackets are arguments and need spaces around them.

Terminal window
while [ -f /tmp/lock ]
sleep 1
end
for f in one two three
echo "item: $f"
end

break and continue do what they look like.

Terminal window
function greet
echo "hello, $1"
return 0
end
greet world

Arguments arrive as $1–$9 and $@, the same as a script’s. return sets the exit status and leaves the function.

Terminal window
cmd1 && cmd2 # only if cmd1 succeeded
cmd1 || cmd2 # only if it failed
cmd1 ; cmd2 # regardless
ls /bin | grep sh

Redirection takes an explicit descriptor and can duplicate one onto another, per pipeline stage.

Terminal window
echo hi > file # truncate
echo more >> file # append
cat < file # read
cmd 2> errors # stderr to a file
cmd 2>&1 # stderr onto stdout
cmd &> everything # both

Heredocs feed a block to stdin. Quoting the marker suppresses expansion.

Terminal window
cat > note << EOF
home is $HOME
EOF
cat << 'EOF'
this $HOME stays literal
EOF

*, ? and [...] expand against the filesystem. A pattern matching nothing is passed through unchanged rather than erroring, and a quoted one is left alone.

Terminal window
ls /bin/e*
rm /tmp/*.log

Each pipeline gets its own process group, and the terminal is handed to whichever group is in the foreground — which is why Ctrl+C on sleep 30 | cat kills both stages instead of one.

Terminal window
sleep 60 & # background
jobs # what is running or suspended
fg 1 # bring it back
bg 1 # resume it in the background
wait # block until children finish

Ctrl+Z suspends the foreground job.

exit, help, pwd, cd, clear, echo, export, unset, env, history, test, [, break, continue, set, return, kill, jobs, fg, bg, wait, ifconfig, ip.

set -e exits on the first failure; set +e turns that back off.

#!/bin/sh
# greet.sh -- greet everyone named on the command line
set -e
if [ $# -eq 0 ]
echo "usage: greet.sh NAME..."
exit 1
end
function greet
echo "Hello, $1!"
end
count=0
for name in $@
greet $name
count=$(( count + 1 ))
end
echo "Greeted $count people."
$ sh greet.sh Alice Bob Charlie
Hello, Alice!
Hello, Bob!
Hello, Charlie!
Greeted 3 people.