Editing with Bash

From CLUG Wiki

Jump to: navigation, search

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.