Creating a Self-installing Executable Shell Script

Printer-friendly versionPrinter-friendly version

This How To will cover creating a shell script that contains your installable product inside of it and can be used to install your product on a target machine. This is essentially one file that contains both the files to install and the install logic within it. You'll need a few tools to start off with, namely

uuencode

and

uudecode

These are historical unix tools. From the wikipedia page, at http://en.wikipedia.org/wiki/Uuencode

Uuencoding is a form of binary-to-text encoding that originated in the Unix program uuencode, for encoding binary data for transmission over the uucp mail system. The name "uuencoding" is derived from "Unix-to-Unix encoding". Since uucp converted characters between various computers' character sets, uuencode was used to convert the data to fairly common characters that were unlikely to be "translated" and thereby destroy the file.

Basically, uuencode takes binary data and makes it into character data, and uudecode takes that data and makes it back into exactly the binary data it was. On debian systems, these tools can be found in the "sharutils" package. On many distributions they are still widely used, can often be found to be included in the default installation. To get started, then. Basically, we want to create a shell script that uses the 'here document' construct to embed a character encoded binary file that we can decode as we run the script. It looks something like:


#!/bin/bash

cd /tmp &&
{ uudecode <<-!
begin-base64 644 -
H4sIAFuOWEkAA+3SUWoCMRDG8Tz3FHMAK5nFXS/iBaKO3UBcZTPCHt+0D+JT
+ySl9P9jyAeZeZhA3Kp3a188vE5shs3mM3Xbx+f8ol0fVLUbNG5bhqg6aB8k
vnCnh1v1NIuE/ZzT9M3cT/0/ajfmKq2SePsJ7VhcTrnYSvwiRztfpupzchMf
TaqV03tuN6mUPH2ILXa4edqX1hqtFKmHOV/97bcfBQAAAAAAAAAAAAAAAAD/
wB3iCO/tACgAAA==
====
!
} | tar zx && echo "Un-tarred test2.txt in /tmp" || echo "Failure..."

I had a file, "test.tar.gz", which I uuencoded like so:

cat test.tar.gz | uuencode -m - > test.sh

Then, I opened "test.sh", which looked like this:


begin-base64 644 -
H4sIAFuOWEkAA+3SUWoCMRDG8Tz3FHMAK5nFXS/iBaKO3UBcZTPCHt+0D+JT
+ySl9P9jyAeZeZhA3Kp3a188vE5shs3mM3Xbx+f8ol0fVLUbNG5bhqg6aB8k
vnCnh1v1NIuE/ZzT9M3cT/0/ajfmKq2SePsJ7VhcTrnYSvwiRztfpupzchMf
TaqV03tuN6mUPH2ILXa4edqX1hqtFKmHOV/97bcfBQAAAAAAAAAAAAAAAAD/
wB3iCO/tACgAAA==
====

I then added the top and bottom, essentially embedding the uuencoded data inside a "here document", by placing it between

<<-!

and

!

The key to a proper here-document is that the '!' MUST be the last character on the line, both in the opening and closing of the document. The '-' in the opening tags tells bash to strip all leading tabs from the front of each line of the document, allowing you to indent it in a normal fashion. If you left it out, you'd have to be sure your document was starting in column 0 on every line. The '{}' brackets around the uudecode << run the uudecode on the document, and pass the output to the 'tar' command. Instead of '|', you could '>' the output to a tar.gz file, and then run an un-tar on it, but this is cleaner, and leaves less stuff to clean up. The only reason to do that is if you're encoding a really large file, and start running into out-of-memory errors.

You can expand on this principle by creating your shell script without the uuencoded data in it, and then running another script to parse the install script and insert data where needed. Here is a sample installer script, that is identical to the previous example, but without the data included. Instead, I've placed a specially formatted comment that my parser script will look for

 
#!/bin/bash
#
# test.sh
#
cd /tmp &&
#includefile test.txt

Then, we run the following script on it:


#!/bin/bash
#***********************************************************
# makeinstaller.sh                                         *
#   Make a patch script by substituting #include           *
#   directives in a shell script.                          *
#                                                          *
#   valid include statements are:                          *
#     #includedir                                          *
#     #includefile                                         *
#                                                          *
#   #includedir - makes a tarball of the dir and uuencodes *
#      it, then includes the uuencoded data in place of    *
#      the #includedir statement                           *
#   #includefile - takes the given file and uuencodes it,  *
#      then includes the uuencoded data in place of the    *
#      statement                                           *
#                                                          *
#  NOTE:                                                   *
#    It's best to supply a full path to both of these      *
#    directives, but they will try to assume relative      *
#    paths where possible.                                 *
#                                                          *
#***********************************************************

# needed shell options
shopt -s extglob        # extended pattern matching features of bash
shopt -s expand_aliases # expand aliases
shopt -s xpg_echo       # expand backslash sequences in echo lines -
                        # this way we don't have to supply '-e' to echo cmds

# Permit me the minor perl-ism...makes code so much cleaner
die () {
    local ex=$?
    echo "${1}"
    exit ${ex}
}

# initialize variables:
bzip=false
gzip=false
cnt=0

usage="\nUsage: `basename $0` [-h] [-v] [-(z|j)] [-f <filename>] <output file> \n\
  -v              Verbose Mode
  -f <filename>   The filename to take as input file\n\
  -z              use gzip for compression of tar'd dirs and files\n\
                  Otherwise, no compression is used
  -j              use bzip2 for compression of tar'd dirs and files\n\
                  Otherwise, no comression is used\n\
  -h              print this message\n"

# parse command line opts
while getopts :vf:zjh option
do
case "${option}" in
    v)
        verbose=true
    ;;
    f)  # Source file - script on which to do the replace functions
        test -f ${OPTARG} && infile=${OPTARG} || die "\n ${OPTARG} is not a valid file! \n"
    ;;
    z)  # use gzip for tar archives
		gzip=true
    ;;
    j)  # use bzip2 for tar archives
		bzip=true
    ;;
    h)  # help message
        echo "${usage}"
        exit 0
    ;;
    \?)  echo "${usage}"
        exit 1
    ;;
esac
done

test $# -lt 1 && die "$usage"
#shift the argument stack until we have the last one
until test $# -le 1; do shift; done 

#interpret our arguments:
$gzip && taropts="-z" && compress=gzip
$bzip && taropts="-j" && compress=bzip2

# sanity checkpoint:
test -n $infile || die "Need to specify a file to parse! (use '-f <filename>')"
test -n $outfile || die "Need to specify an output file!"
$bzip && $gzip && echo "Can't use gzip and bzip together...choose one!!!"
[[ "${1}" != @(-*) ]] && outfile=${1} || die "${1} is not a valid output filename"
test -f $infile || die "$infile does not exist!"
[[ "$outfile" != @(/*) ]] && outfile=$PWD/$outfile #if outfile is relative, make it not

# make sure output file is empty
> $outfile

li=0 #line counter
while read lineout; do
	let li++
	if [ "${lineout#\#include}" != "${lineout}" ]; then
		directive=${lineout% *}
		target=${lineout#* }
		case $directive in
		"#includedir")
			test -d ${target} || die "${target} is not a directory on line $li"
			echo "#beginning of ${directive} ${target}" >> ${outfile}
			echo "cd ${target%/*} &&" >> ${outfile}
			echo '{ uudecode <<!' >> ${outfile}
			test -d ${target%/*} && cd ${target%/*}; 
				tar -c ${taropts} ${target##*/} | uuencode -m /dev/stdout >> ${outfile}
			echo "!\n} | tar ${taropts} -x &>/dev/null" >> ${outfile}
			echo "#end of ${directive} ${target}" >> ${outfile}
		;;
		"#includefile")
			if [ "`file --mime $target`" == @($target: application/*) ]; then
				echo "#beginning of already compressed ${directive} ${target}" >> ${outfile}
				test -d ${target%/*} && echo "cd ${target%/*} &&" >> ${outfile}
				echo '{ uudecode <<!' >> ${outfile}
				test -d ${target%/*} && cd ${target%/*};  
					uuencode -m ${target##*/} /dev/stdout >> ${outfile}
				echo "!\n} >${target##*/}" >> ${outfile}
				echo "#end of already compressed ${directive} ${target}" >> ${outfile}
			else
				echo "#beginning of compressed ${directive} ${target}" >> ${outfile}
				test -d ${target%/*} && echo "cd ${target%/*} &&" >> ${outfile}
				echo '{ uudecode <<!' >> ${outfile}
				test -d ${target%/*} && cd ${target%/*}; 
					$compress -9c ${target##*/} | uuencode -m /dev/stdout >> ${outfile}
				echo "!\n} | $compress -d >${target##*/}" >> ${outfile}
				echo "#end of compressed ${directive} ${target}" >> ${outfile}
			fi
		;;
		esac
	else
		cat >> $outfile <<-!
		${lineout}
		!
	fi
done < <(sed 's!\\!\\\\!g' $infile)

# make the output file executable
chmod +x $outfile

It's a bit long-winded, but what it does is loop through the rows of the input file (presumably a script) and looks for lines matching '#includefile' or '#includedir', then replaces them with the compressed, tarred, uuencoded data that will naturally un-compress when the script runs. If you supply absolute paths to the '#include' directives, the script will put the files/directories in the same places on the target system. The resulting code, from running the above makeinstaller.sh, I've named "installer.sh", and it looks like this:


#!/bin/bash

cd /tmp &&
#beginning of compressed #includefile test.txt
{ uudecode <<!
begin-base64 644 /dev/stdout
H4sICEmOWEkCA3Rlc3QudHh0AA2JwQmAMBAE/1axBWg3NhDjxhycieRWsHwD
wzxm9mqBSYIYmvqEYs4V6jh59xYaSYQqEfSy2SzJ3doFfsyv0uFzVboj8rBH
yw8oqQpfVQAAAA==
====
!
} | gzip -d >test.txt
#end of compressed #includefile test.txt

As you can see, the makeinstaller.sh script has left '#beginning' and '#end' comments to denote where it has done the replacement.

Hope it helps!

Brian


Filename/TitleSize
makeinstaller.sh5.13 KB
installer.sh324 bytes
test.sh46 bytes

Search Engine Optimization