Creating a Self-installing Executable Shell Script
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
uuencodeand
uudecodeThese 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.shThen, 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
- Login or register to post comments
- 405 reads
Printer-friendly version


