Memra

Running a script written for another shell

◈ 10 cards

Why a Bourne script fails from a C-shell login, and the three ways to fix it — naming the interpreter, the #! line, and the execute bit.

The problem, concretely

Your login shell is csh. A colleague hands you a script called backup written in Bourne syntax — it uses VAR=value, if [ -d "$1" ]; then, export, and $? for the exit status. You make it executable and run ./backup. It produces a burst of syntax errors and does nothing useful.

Nothing is wrong with the script. The problem is the previous lesson's mechanism. When your csh runs a script it forks a child, and a fork produces a copy of the process that called it — so the child is another csh. That child opens backup and starts reading it as C-shell input. VAR=value is not an assignment in csh (it wants set VAR = value), [ -d ] is not csh test syntax, and $? in csh is not the exit status at all — csh spells that $status. The file is fine; the interpreter is wrong.

Fix 1 — name the interpreter yourself

sh backup /home/you/notes

This runs sh as an ordinary external command and hands it backup as an argument. Your csh forks, the child execs /bin/sh, and that Bourne shell opens the file and reads it. The script's own permissions barely matter here — sh only needs to be able to read the file, so this works even with no execute bit set at all. It is the fastest fix and the one to reach for when you are just testing something someone sent you.

Its weakness is that the knowledge lives in your fingers. Anyone who runs the script the obvious way still gets the errors, and six months later that includes you.

Fix 2 — put the choice inside the file: #!

Make the very first line of the file:

#!/bin/sh

The two characters #! at the start of a file are a magic number, and it is the kernel, not any shell, that acts on them. When the kernel is asked to exec a file and finds #! in the first two bytes, it reads the pathname that follows, execs that program instead, and passes the original file to it as an argument. So ./backup becomes, in effect, /bin/sh ./backup — and it does so no matter which shell you were typing in, which shell your colleague uses, or which shell cron will use at three in the morning.

The pathname after #! must be absolute. The kernel does no PATH search; #!sh simply fails.

Fix 3 — the historical : idiom

Before #! existed, the C shell used a cruder rule: if the first character of a script was #, csh read it itself; otherwise it handed the file to a Bourne shell. Scripts therefore began with a bare : line — a Bourne no-op — to steer csh away. You will still meet this in old code. It is worth recognising and not worth writing.

Worked example — the complete fix, in order

file backup                 # what am I even holding?
head -1 backup              # is there a #! line already?

There is no #! line, so add one as the first line, then:

chmod u+x backup
ls -l backup                # confirm the x bit is now set
./backup /home/you/notes

The ./ is not decoration. When you type a name with no slash in it, the shell searches the directories in PATH — and for good security reasons the current directory is normally not on PATH. Without ./, the shell looks everywhere except where the file is and reports command not found. Writing ./backup supplies a pathname, so no search happens.

One Linux caveat the textbook cannot give you: /bin/sh on many Linux distributions is dash, a small strict POSIX shell, not bash. If the script uses bash-only syntax — arrays, [[ … ]], local in the bash sense — then #!/bin/sh will fail on Linux and #!/bin/bash is the honest header.

kernel finds no magic numberkernel reads bytes 0-1./backuptyped at a csh promptno #! linecsh forks a child csha fork copies the callersyntax errorscsh cannot parse Bourne syntax#!/bin/shkernel execs /bin/shscript passed as an argumentscript runs
One command, two outcomes. The only difference is two bytes at the front of the file.
NORMAL ~/memra/learn/comp-325/running-a-script-under-another-shell utf-8 LF