Editing with Bash
From CLUG Wiki
These are the notes for the CLUG talk of the 2005-06-28 expanded slightly to make up for the absence of yours truly standing at the podium and mumbling something.
Contents |
Implementing ed with bash builtins only
Introduction
Sooner or later you are likely to lose concentration and delete or overwrite important bits of your filesystem. Alternatively a hardware failure may make filesystem access really difficult. Or maybe your are running out of an initrd or other space constrained system.
Regardless, sometimes you are faced with the challenge of working on or salvaging a system with a really minimal toolset.
In the worst case you may only have access to those processes already running and maybe some which are still cached - in other words, those which have been run recently.
Of these process bash, your shell, is the most interesting - it tends to be the only interactive process left running after an ill considered rm -rf. So I will use bash to construct a very basic set of utilities.
Commands
- The cd command is a builtin, so you can still navigate through whatever bits of the filesystem remain. kill is also a builtin.
- Listing the content of a directory is more difficult, as /bin/ls is an external program and may be unavailable. In the place of /bin/ls one can use the echo builtin. Echo displays its parameters, and the shell will expand * to all the files in the current directory, so
echo *is sufficient to list your current directory.
- /bin/cat can be approximated by redirecting a file a bash subprocess which uses the read builtin to read in a line, and echo to display this line. The below example uses this to display /etc/passwd
(while read line ; do echo "$line" ; done) < /etc/passwd
- Constructing a simple version of /bin/cp using the same approach used to replace /bin/cat is left as an exercise to the reader.
- A replacement for ps can be jury rigged by stepping through proc and displaying the first part of the file stat for each process. The bash for loop comes in handy here (to examine each of the stat files), as does bash variable expansion (to limit output to the most interesting part of stat).
for name in /proc/[1-9]*/stat ; do read line < $name ; echo ${line%%\)*}\) ; done
- A replacement text editor is the most interesting challenge. I will use bash to construct a very basic /bin/ed approximation. You are invited to expand on the initial unpolished, untested outline. Conceptually there is little preventing you from implementing a reasonably complete ed substitute. If you have not used ed before, it is probably a good idea to read its manual page now. http://www.geocities.com/kensanata/ed.html contains a succinct but nonstandard manual page as well as example use, of sorts. When you absolutely need to edit the likes of lilo.conf, fstab or passwd to salvage a system, ed does the job. So just type man ed already.
# (GPL) 2005: Marc Welz. Crummy implementation of ed in bash, using builtins only
unset merge
unset exise
unset commit
unset med
bufferlength=0
newlength=0
function merge(){
base=$1
if [ $newlength -le 0 ] ; then
return
fi
i=$bufferlength
while [ $i -gt $base ] ; do
i=$[i-1]
buffer[$[i+newlength]]=${buffer[$i]}
done
i=0;
while [ $i -lt $newlength ] ; do
buffer[$[i+base]]=${new[$i]}
i=$[i+1];
done
bufferlength=$[bufferlength+newlength]
newlength=0
}
function exise(){
base=$1
if [ $base -ge $bufferlength ] ; then
return
fi
i=$base
while [ $i -lt $bufferlength ] ; do
buffer[i]=${buffer[$[i+1]]}
i=$[i+1]
done
buffer[$i]=""
bufferlength=$[bufferlength-1]
}
function receive(){
unset new
newlength=0
while read line ; do
if [ "$line" == "." ] ; then
return
fi
if [ "${new[$newlength]}" == ".." ] ; then
new[$newlength]="."
else
new[$newlength]=$line
fi
newlength=$[newlength+1]
done
}
function commit(){
: > $file
i=0;
bytes=0;
while [ $i -lt $bufferlength ] ; do
bytes=$[bytes+1+${#buffer[$i]}]
echo ${buffer[$i]} >> $file
i=$[i+1]
done
echo $bytes
}
function med(){
if [ $# -lt 1 ] ; then
echo "require a filename"
return 1
fi
file=$1
bufferlength=0
{ while read line; do buffer[$bufferlength]=$line; bufferlength=$[bufferlength+1]; done ; } < $file
if [ $bufferlength -gt 0 ] ; then
position=$[bufferlength-1]
else
position=0
fi
while read command ; do
case $command in
("")
if [ $[position+1] -lt $bufferlength ] ; then
position=$[position+1]
echo ${buffer[$position]}
else
echo "? (at end of file)"
fi
;;
(q*)
return ;;
(d)
if [ $position -ge 0 ] ; then
exise $position
if $position -gt 0 ] ; then
position=$[position-1]
fi
fi
;;
(a)
receive
tmp=$[position+1]
position=$[position+newlength]
merge $tmp
;;
(f)
echo $file ;;
(p)
echo ${buffer[$position]} ;;
(w)
commit ;;
([0-9]*)
tmp=$[command-1]
if [ $tmp -ge $bufferlength -o $tmp -lt 0 ] ; then
echo "? (out of range)"
else
position=$tmp
echo ${buffer[$position]}
fi
;;
(*)
echo "? (unimplemented command)" ;;
esac
done
}
Notes
- All above examples can be made functions or aliases, making their invocation less painful.
- If devices fail, a read from that device may block indefinitely. In such cases the background signal (generated with ^Z) may help you recover the terminal. Even better use the background facilities (&) or a subshell (parentheses) to begin with.
- If you are well prepared, you will have a static copy of busybox on your system. But Murphy's Law means that you will only really need busybox on systems where it is not available.
