Systems
Administration with Scsh
Evan Sarmiento
Shell scripting languages provide systems administrators with
powerful tools to automate mundane tasks, saving hours of work.
Most of these languages, however, do not provide a well-designed
object system, nor are they optimized for recursion and abstraction.
Scheme, a dialect of LISP, incorporates all the features of a good
programming language as listed above. Because Scheme cannot interact
with UNIX primitives alone, there is a Scheme software package called
scsh (the scheme shell). Scsh provides a set of macros for the Scheme
programming language, adding the ability to call on all standard
UNIX system calls and even adds regular expressions.
In this article, I will briefly introduce and describe the important
features of the Scheme language and present some of my own scripts
written in scsh, which exhibit many of the listed features. This
article presumes that you know how to program using Scheme. If you
are new to Scheme, read the Notes section at the end of this article,
which provides numerous learning resources.
You can download scsh from:
http://www.swiss.ai.mit.edu/ftpdir/scsh/
There are easy-to-follow installation instructions on the site. For
FreeBSD users, scsh is present in the ports collection, /usr/ports/shells/scsh.
Scheme comes in many variants, such as MIT Scheme, Scheme 48,
MzScheme, and others. Scsh is based on Scheme 48. However, I find
MIT Scheme to be the most complete. You can download MIT Scheme
from:
http://www.swiss.ai.mit.edu/projects/scheme/
Scheme tutorials and the book Structure and Interpretation of Programs
(Second Edition by Harold Abelson and Gerald Jay Sussman, with Julie
Sussman, The MIT Press) are provided online at:
http://sicp.ai.mit.edu
You can also find out more about each variant by looking at:
http://www.Schemers.org
Running Scsh
When you execute scsh, you're running in interactive mode,
which is similar to the interactive mode for Python. You can type
in individual commands, and the interpreter will evaluate them line-by-line.
To use scsh for batch processing, append this header to the top
of your scsh script files:
#! /usr/local/bin/scsh -s
!#
Benefits of Scsh
Programming in scsh provides numerous benefits. You will have
access to both a scripting language and a systems-programming language
when using scsh, because it is just a set of macros for Scheme.
scsh uses ''process notation'', which allows
easy access to the command line. Scsh also provides low-level access
to the operating system. The current release of scsh provides full
access to Posix, which includes: fork, exec, wait, sockets, write,
open, close, seek, tell, chmod, chmod, chgrp, chown, locking, and
more. Another benefit of using scsh is that it is very portable.
It is based on Scheme and, like Java, Scheme code is executed through
a byte-code interpreter. There are Scheme 48 virtual machines for
numerous operating systems, including Windows, which allow you to
run scsh in almost any environment.
The Scheme Philosophy
Before discussing the specifics of scsh, it is useful to understand
the peculiar syntax and fundamental data structures of Scheme. Scheme
was derived from Lisp, improving upon many of the Lisp features.
The Lisp programming language was mainly used in two fields: math
and artificial intelligence. Scheme is a dialect of Lisp that stresses
elegance and simplicity. It is a stripped-down version of Common
Lisp. In fact, the language specification is about 50 pages, while
the Common Lisp's specification is about 1,300 pages. Scheme
is often used in computer science courses to teach abstraction and
programming concepts. Even though it is used primarily as a teaching
language, Scheme has numerous applications -- the GIMP allows
users to write GIMP-loadable modules in Guile, a GNU Scheme variant,
and Scwm, a Window manager, allows users to write their own extensions
in Scheme.
The elegance and readability of Scheme is what prompted Olin Shivers
to write scsh, a set of macros that provide access to low-level
operating system functions and usual shell commands. Sh provides
a terse, inflexible, and hard to read syntax, which does not provide
access to lower level functions like sockets or signals, unlike
scsh, which encompasses both a shell scripting language and a systems
programming language.
Prefix Notation
Scheme may be hard to learn at first, but learning scheme will
certainly increase your programming skills. What separates Scheme
from other languages is its use of Prefix Notation. In Scheme, operations
are placed before operands. For example, you would normally write
this equation as such:
Regular: 5 / 3 * 10
Prefix Notation: (* (/ 5 3) 10)
Even simple arithmetic is changed. For example, 1 + 1 + 1 + 1
becomes:
(+ 1 1 1 1)
The concept of Prefix Notation is important to understand in order
to use scsh. All operations in Scheme are done through Prefix Notation
syntax, which may seem cumbersome at first, but simplifies coding.
Lists
Lists are an integral part of the Scheme programming languages.
A list is just a data structure. The most obvious application of
a List would be in a loop, where your program needs to take an element
from the front of the list and perform some evaluations on it. When
I program in scsh, I commonly use lists to parse output from programs.
I can also specify that the program output be translated into a
list of strings. Another benefit of Lists is that you can have functions
return more than one value using a list. This is how the (pipe)
system call works -- it returns a list consisting of two new
ports.
Lists are manipulated through the functions: list, cons, car,
cdr, and append. Scheme itself is based on the idea of a list. When
the Scheme interpreter tries to execute the command (+ 1 1 1
1), the interpreter thinks of it as a list, where the first
element of the list is the operator. You create a list by using
the list function.
The following examples are performed in interactive mode:
(define q (list 1 2 3 4 5))
This creates a list assigned to the name q. You do not have to assign
lists to names; you can pass them anonymously to functions.
You can retrieve the item off the front of the list by using car:
(car q)
1
You can remove one item from the list by using cdr. Using cdr
on a list creates a temporary copy of the list in memory with the
first item removed. The original list is still intact:
(cdr q)
'(2 3 4 5)
cons adds an item to the beginning of a list, while append
adds a list to the end of a list:
(cons 5 q)
'(5 1 2 3 4 5)
(append q (list 5))
'(1 2 3 4 5 5)
What is Scsh?
As stated before, scsh is a set of Scheme macros that allow you
to interact with the operating system, even providing access to
system calls. Scsh is different from other shell scripting languages
because it works with Scheme and uses a syntax that many people
are unfamiliar with. However, Scheme does provide recursion, abstraction,
object orientation, and readability features, which make it perfect
to use for systems programming or shell scripting.
Keep in mind that scsh is not a "command language";
it is a systems programming language, meaning that it encompasses
both a way to interact with the shell, and a way to interact with
low-level functions of the operating system. A programming language
is just a notation for expressing computation. Scsh has its own
way of expressing commands, called S-expressions, which will be
described in the next section.
Ports
A port, which appears throughout all scsh documentation, is like
a file descriptor, from which you can read and write data. In scsh,
there are certain functions that either require ports as arguments,
or return ports. You will probably be using functions that return
ports.
When I'm programming in scsh, I usually need to parse output
from programs or text-files. There is a specific function in scsh
that allows you to run a program and have all output returned on
a certain port. You can then read all the data from the port and
turn it into any sort of data structure. I usually turn output into
a list.
Ports are also used when opening files. When a user calls the
(open-file) system call, a port is returned and you can perform
subsequent operations, such as reading and writing, on that port.
Extended Process Notation
Interaction with the operating system is done through S-expressions.
S-expressions themselves are just data structures for representing
complex data. They can be either byte strings or lists of S-expressions.
Usually, while coding in scsh, you will be dealing with lists of
S-expressions. An S-expression could look like this:
(belong (all (your "base")) (to "us"))
This S-expression has no meaning and is only an example of the syntax.
When analyzing this S-expression, you can see that belong is
a function that acts on the return value of (all (your "base"))
and (to "us")).
Scsh has a notation, taking the form of S-expressions, for controlling
UNIX processes, which are called process forms. An extended process
form is a specification of a UNIX process to run, in a particular
environment:
(pf redir_1 redir_n)
where pf is the process form and redir_i are arguments.
Listed below is a set of process forms that do the normal set of commands,
such as creating a pipe, running a process in the background, redirecting
output into the calling process or out to a file:
| -- fork/pipe.
& -- Run a process in background.
begin -- Runs code in a fork.
< -- Open a file for read.
> -- Open a file for crease/truncate.
The list below provides scsh functions and examples that are commonly
used in shell scripting. Note that ">" means
that we are working at the interactive scsh prompt:
(run . EPF) -- Runs a process; returns an exit code.
For example:
(run (echo Hello))
Hello
(run/sexps . EPF) -- Run a process and return its
output as a list of strings. This is very useful as shown later.
This example executes ps | tail -1, transforming the output
to a list for further parsing:
> (define output (run/sexps (| (ps) (tail -1))))
> output
'(61661 v0 S 0:00.45 aterm -tr -tn xterm-color -bg black -fg white -sh 30 -font nexus)
> (car output)
61661
(argv n) -> string -- Returns the Nth argument
given to the program.
(begin . EPF) -- Runs code within a fork. For example:
(begin (run (ls)))
This forks a new process to execute ls.
(with-cwd directory . EPF) -- Changes the current
directory to directory and executes the other arguments.
For example:
(with-cwd "/usr/src"
(run (ls)))
(& . EPF) -- Runs a process in the background.
For example, compare these two commands. The first is a command
written in Sh, the second is written in scsh. It is a simple operation
and both commands get the last ten lines of /var/log/smb.log
and grep for the string File.
Sh: tail -10 /var/log/log.smb | grep File
Scsh: (run (| (tail -10 /var/log/log.smb) (grep File)))
System Calls
As noted before, scsh provides access to low-level system calls,
like dup, sockets, open, close, write, read, etc. All of these operations
are done through the ports. Below is a table of system calls supported
in scsh. I took a sampling from each section (not all of the system
calls are listed), and you can find the rest in the documentation.
UNIX I/O
(dup fd/port) -> fd/port -- This function duplicates
a file descriptor or a port, returning the newly created descriptor
or port:
> (define port (open-file "/etc/inetd.conf" open/read))
> (define port-dup (dup port))
(open-file fname flags [perms]) -> port -- This
function opens the filename referenced by fname and returns a
port. Perms is an optional argument, allowing you to set permissions
on the file you are opening.
The are numerous flags you can use, which are listed in the scsh reference
or in cheat.txt, however, the most commonly used flags are:
open/read
open/write
open/append
> (define port (open-file "/etc/inetd.conf" open/read))
(open-input-file fname [flags]) -> port
(open-output-file fname [flag perms]) -> port
(open-fdes fname flags [perms]) -> integer
This returns a true file descriptor; ports are more useful and do
all the same things.
(set-fdes-flags fd/port flags) -- Allows you to
set flags on a file descriptor or a port. You can find the flags
in the reference manual:
> (define port (open-file "/etc/inetd.conf" open/read))
> (set-fdes-flags port open/async)
open/async -- Enables Asynchronous writing on the
port.
(pipe) -> [rport wport] -- This returns two unused
ports, rport and a wport. All output on rport
is sent to wport.
(read-line [fd/port retain-newline?]) -> string or eof-object
-- When a file descriptor or port is specified, it returns
the first line waiting on the port:
> (define port (open-file "/etc/inetd.conf" open/read))
> (read-line port)
"# $FreeBSD: src/etc/inetd.conf,v 1.44.2.1 2000/03/25 22:09:59 jhb Exp $"
> (read-line port)
"#"
> (read-line port)
"# Internet server configuration database"
(read-string nbytes [fd/port]) -> string or #f --
Returns a string from a specified port:
> (read-string 30 port)
"# $FreeBSD: src/etc/inetd.conf"
(write-string string [fd/port start end]) -- Writes
a string to a specified port:
> (define port (open-file "/home/evms/.zshrc" open/append))
> (write-string "echo Boo" port)
Locking
(make-lock-region exclusive? start len [whence]) -> lock-region
-- Returns a lock-region record. Records are similar to a
struct in C:
> (define q (make-lock-region 0 30 50))
(lock-region fdes lock) -- Locks a file descriptor
according to the lock region created with make-lock-region.
File System
(create-directory fname [perms override?]) -- Creates
a directory fname.
(delete-directory fname) -- Deletes a directory
fname.
(delete-file fname) -- Deletes a file fname.
Processes
(exec prog arg1 ...) -- Executes prog with
args:
> (exec "/bin/ls" "-al")
(fork) -> proc -- Forks a new process and returns
a process object.
Process State
(umask) -> fixnum -- Returns the current umask:
> (umask)
18
(set-uid uid) -- Sets the processes UID:
> (set-uid 0)
Signals
(signal-process proc/pid sig) -- Signals a process
whose pid is pid with a signal specified by sig.
You can use almost any signal, but the most common ones are:
signal/kill
signal/stop
signal/alrm
signal/hup
(signal-process (cadr (run/sexps (| (ps aux) (grep inetd) (tail -1)))) signal/hup)
The (cadr ...) evaluates to the pid of the inetd
process. As you recall, run/sexps turns the output into a list
and cadr takes the second element from the list and returns
it.
These system calls are the ones I use most. There are socket system
calls in scsh, but they are complicated and won't be explained
in this article. The scsh documentation, however, has great examples
on sockets.
OO in Scheme
Object oriented programming is becoming a dominant trend for systems
programming, and it can also be used for administration tasks. For
example, you could create a computer object, which has the locally
defined functions ping, restore, reboot, and
local variables that indicate the IP address and hostname. When
used in a network environment, this script would be modular and
powerful. I would use Scheme to write this because it has the capacity
for OO.
OO in Scheme is mainly done through the use of state variables
and lambda. All variables in Scheme maintain state. Here, I will
use the example of a piggy bank object. You can deposit, withdraw,
and display the amount of change within the piggy bank. Of course,
you can modify this code to do something that is actually useful:
(define (create-piggybank)
(let ((balance 0))
(define (deposit amount)
(set! balance (+ balance amount)))
(define (withdraw amount)
(if (<= (- balance amount) 0)
(display "Error: you have no balance!")
(set! balance (- balance amount))))
(define (print_balance)
(display balance))
(define (dispatch operation)
(if (eq? operation 'deposit)
deposit
(if (eq? operation 'withdraw)
withdraw
(if (eq? operation 'print_balance)
print_balance))))
dispatch))
See Figure 1.
System Automation
Scsh is especially useful for system automation. Below are some
of the shell scripts I've created using scsh, comparing them
to a script written in sh that performs the same function.
A jail is similar to chroot, but with more restrictions. Jail
is implemented within FreeBSD. This script initializes all of the
jails within /jail. It is used on bootup. Within the /jail
directory, note that the jails are named by their IP addresses rather
than their hostnames.
Bourne Shell:
#! /bin/sh
JAIL="/usr/sbin/jail"
for dir in $(ls /export); do
echo $dir
$JAIL /export/$dir $dir $dir /bin/sh /etc/rc
done
Scsh:
#! /usr/local/bin/scsh -s
!#
(with-cwd "/jail"
(define (jail dir host ip)
(run (/usr/sbin/jail ,dir ,host ,ip /bin/sh /etc/rc)))
(for-each (lambda (j)
(jail j j j))
(directory-files)))
Comparison
The sh version of the script looks substantially shorter and easier
to read, but, there are substantial benefits in the scsh version.
One benefit is the ability to define functions within regions like
(with-cwd), which allowed me to keep the (jail) function
hidden from users of the script and I think it makes it easier to
read. (directory-files) is also much shorter than writing
for dir in $(ls /export); do....
(lambda) is used to create anonymous functions in Scheme.
As shown, I am passing a function as an argument to (for-each)
to be applied to every item of the list (directory-files).
Lambda is very powerful and can be used for a variety of purposes.
Using lambda, you can write a scsh script that returns functions
or creates functions from a template.
This second shell script, similar to one in ''Useful
Scripts for Overworked Administrators'' by Mark Prager
(Sys Admin, June 2001) pings a list of servers to test their
availability. If the servers are down, the administrator is contacted.
Bourne Shell:
#!/bin/sh
hosts="next-station postfix ns router"
for i in $hosts; do
if [ "$(ping -c 1 $i | grep "100% packet loss")" != "" ]; then
/home/evms/sms "$i is down."
echo "$i is down" | /usr/bin/mail kaworu
fi
done
scsh:
#! /usr/local/bin/scsh -s
!#
(for-each (lambda (host)
(define (alive?)
(run/strings (| (/sbin/ping -c 1 ,host) (grep "100% packet loss"))))
(if (alive?)
'()
(begin
(run (/home/evms/sms ", host is down."))
(run (| (echo ", host is down") (/usr/bin/mail kaworu))))))
(list "next-station" "postfix" "ns"
"router" "teqnix" "next" "logsrv"))
This example shows that I am also employing abstraction by defining
a function within lambda. This example is a bit larger than the sh
script, but it is certainly more readable.
Through the vnconfig program in FreeBSD, you can create
mountable partitions through disk images. This scsh shell script
accepts two arguments: the directory that you want to back up, and
the path to where the disk image should be stored:
#! /usr/local/bin/scsh -s
!#
(begin
(if (eq? (length (command-line)) 1)
(run (echo
"Usage: ./createbackup <tree> <file>
For example, ./createbackup /usr creates a self-mounting image <file>
containing all files within /usr"))
(let ((totalsize (car (run/sexps (| (du -c ,(argv 1)) (grep "total"))))))
(run (echo "It appears that" ,(argv 1) "is" ,totalsize "K. Starting the backup process.."))
(begin
(&& (dd if=/dev/zero of=,(argv 2) bs=1k count=,totalsize)
(vnconfig -s labels -c vn0 ,(argv 2))
(disklabel -r -w vn0 auto)
(newfs vn0c)
(rmdir /backup_mount)
(mkdir /backup_mount)
(mount /dev/vn0c /backup_mount)
(cp -R -f ,(argv 1) /backup_mount)
(umount /backup_mount)
(rmdir /backup_mount))))
(run (echo "Backup complete."))))
This example illustrates the advantage of Prefix Notation. Instead
of having to include a && after each command, I only
have to include it once, which makes the script easier to read. Also,
let is a very important constructor that allows you to define
variables within a region so that it can not be altered by the programmer.
This is another example of abstraction.
Conclusion
There are many shells available for the various *NIX operating
systems. Each one encompasses both an interactive prompt and a batch
mode where more complicated commands can be written. Most shell
scripting languages are archaic and have limited access to the operating
system. Scsh merges shell scripting and systems programming to produce
easy to read, portable, and fast code. When comparing the scsh shell
scripts to their sh counterparts, you can see the difference scsh
makes.
References
Shivers, Olin, "A Scheme Shell". 1994. MIT Laboratory
for Computer Science.
Shivers, Olin, and Brian D. Carlstrom. "Scsh Reference Manual".
1994. SCSH 0.5.2 Documentation.
Kelsey, R. and J. Reeves and M. Sperber. "The Incomplete
Scheme 48 Reference Manual for release 0.57", Scheme 48 0.57
Documentation.
Abelson, H., and G. J. Sussman and J. Sussman. "Structure
and Interpretation of Computer Programs 2nd edition". 1996.
MIT Press.
IEEE Working Group. "IEEE Standard for the Scheme Programming
Language". 1991.
Evan Sarmiento is a tenth-grade student at Boston University
Academy. He enjoys FreeBSD kernel hacking and network administration.
He can be contacted at: evms@bu.edu.
|