Self-extracting script in sh shell

12k views Asked by At

How would I go about making a self extracting archive that can be executed on sh?

The closest I have come to is:

extract_archive () {
    printf '<archive_contents>' | tar -C "$extract_dir" -xvf -
}

Where <archive_contents> contains a tarball with null characters, %, ' and \ characters escaped and enclosed between single quotes.

Is there any better way to do this so that no escaping is required?

(Please don't point me to shar, makeself etc. I want to write it from scratch.)

5

There are 5 answers

1
loentar On BEST ANSWER

Alternative variant is to use marker for end of shell script and use sed to cut-out shell script itself.

Script selfextract.sh:

#!/bin/bash

sed '0,/^#EOF#$/d' $0 | tar zx; exit 0
#EOF#

How to use:

# create sfx
cat selfextract.sh data.tar.gz >example_sfx.sh

# unpack sfx
bash example_sfx.sh
2
l0b0 On

Since shell scripts are not compiled, but executed statement by statement, you can mix binary and text content using a pattern like this (untested):

#!/bin/sh
sed -e '1,/^exit$/d' "$0" | tar -C "${1-.}" -zxvf -
exit
<binary tar gzipped content here>

You can add those two lines to the top of pretty much any tar+gzip file to make it self extractable.

To test:

$ cat header.sh
#!/bin/sh
sed -e '1,/^exit$/d' "$0" | tar -C "${1-.}" -zxvf -
exit
$ tar -czf header.tgz header.sh
$ cat header.sh header.tgz > header.tgz.sh
$ sh header.tgz.sh
header.sh
0
user1742529 On

Yes, you can do it natively with xtar.

  1. Build xtar elf64 tar self-extractor header (you free to modify it to support elf32, pe and other executable formats), it is based on lightweight bsdtar untar and std elf lib.

    cc contrib/xtar.c -o ./xtar
    
  2. Copy xtar binary to yourTar.xtar

    cp ./xtar yourTar.xtar
    
  3. Append yourTar.tar archive to the end of yourTar.xtar

    cat yourTar.tar >> yourTar.xtar
    chmod +x yourTar.xtar
    
2
I. Marin On

Here's a dash script (should work out of the box on Linux) that can create self-extracting shell archives (you can call it with the --help flag to find out how to use it):

#!/bin/dash

ExtractFirstAndLastPathComponent () {
    eval current_path="\"\$$1\""
    
    first_path_component=""
    last_path_component=""
    
    if [ -n "$current_path" ]; then
        #Remove trailing '/' characters:
        while [ ! "${current_path%"/"}" = "$current_path" ]; do
            current_path="${current_path%"/"}"
        done
        
        if [ -z "$current_path" ]; then
            eval current_path=\"\$$1\"
        fi
        
        last_path_component="${current_path##*"/"}"
        first_path_component="${current_path%"$last_path_component"}"
    fi
    
    eval $2="\"\$first_path_component\""
    eval $3="\"\$last_path_component\""
}

ConvertToFullPath () {
    initial_dir_ctfp="$PWD"
    
    eval current_path="\"\$$1\""
    
    lpc_current_path=""
    
    ExtractFirstAndLastPathComponent current_path fpc_current_path lpc_current_path
    if [ -n "$fpc_current_path" ]; then
        if [ -d "$fpc_current_path" ]; then
            cd "$fpc_current_path"
            fpc_current_path="$PWD"
        fi
    fi
    if [ -n "$lpc_current_path" ]; then
        eval $2="\"\$fpc_current_path/\$lpc_current_path\""
    else
        eval $2=\"\/\"
    fi
    
    cd "$initial_dir_ctfp"
}

GetFileEncodingAndSizeInBytes () {
    eval file_to_test=\"\$$1\"
    
    GetFileSizeInBytes file_to_test file_to_test_size_in_bytes
    
    #Get file mime encoding:
    if [ -d "$file_to_test" ]; then
        result="directory"
        file_to_test_size_in_bytes="0"
    elif [ ! "$file_to_test_size_in_bytes" -eq "0" ]; then
        file_mime_type="$(file -bL --mime-encoding "$file_to_test" 2>/dev/null)" || { file_mime_type="undetermined"; }
        case "$file_mime_type" in
            *"binary"* )
                #Only binary files containing the NULL character (^@) are considered binaries in this script:
                (cat -v "$file_to_test" | sed "/\^@/i'\^@'\$NL2") | { grep -q "\^@"; } && result="binary" || result="ascii"
            ;;
            *"ascii"* )
                result="ascii"
            ;;
            *"utf"* )
                result="${file_mime_type##*" "}"
            ;;
            * )
                result="undetermined"
            ;;
        esac
    else
        result="ascii"
    fi
    eval $2=\"\$result\"
    eval $3=\"\$file_to_test_size_in_bytes\"
}

GetOSType () {
    case "$(uname -s)" in
    *"Darwin"* | *"BSD"* )
        eval $1="BSD-based"
        ;;
    *"Linux"* )
        eval $1="Linux"
        ;;
    * )
        eval $1="Other"
        ;;
    esac
}

GetFileSizeInBytes () {
    eval file="\"\$$1\""
    [ -z "$OS_TYPE" ] && GetOSType OS_TYPE
    if [ "$OS_TYPE" = "BSD-based" ]; then
        file_size_in_bytes="$(stat -Lf %z -- "$file")" 2>/dev/null || { file_size_in_bytes="-1"; }
    elif [ "$OS_TYPE" = "Linux" ] || [ "$OS_TYPE" = "Other" ]; then
        file_size_in_bytes="$(stat -c %s -- "$file")" 2>/dev/null || { file_size_in_bytes="-1"; }
    fi
    eval $2="$file_size_in_bytes"
}

PrintErrorExtra () {
    {
    if [ ! "$error" = "true" ]; then
        for cc in $(seq 1 $params_0); do
            eval current_param_func=\"\$params_$cc\"
            printf '\n%s\n' "Parameter $cc: '$current_param_func'"
        done
        
        if [ -n "$find_parameters" ]; then
            printf '\n%s\n\n' "Find parameters: $find_parameters"
        fi
    fi
    }>&2
}

PrintInTitle () {
    printf "\033]0;%s\007" "$1"
}

PrintJustInTitle () {
    PrintInTitle "$1">"$print_to_screen"
}

trap1 () {
    printf "\n""Archiving: Aborted.\n">"$print_to_screen"
    
    CleanUp
        
    #kill all children processes, suppressing "Terminated" message:
    kill -s PIPE -- -$$ 2>/dev/null
    
    exit
}

CleanUp () {
    
    #Restore "INTERRUPT" (CTRL-C) and "TERMINAL STOP" (CTRL-Z) signals:
    trap - INT
    trap - TSTP
    
    #Clear the title:
    printf "\033]0;%s\007" "">"$print_to_screen"
    
    #Restore initial IFS:
    unset IFS
    
    #Restore initial directory:
    cd "$initial_dir"
    
    CheckPause
}

CheckPause () {
    #For not running from command line: pause the script after finishing:
    if [ "$params_0" = "1" ] && [ "$flag_count" -eq "0" ]; then
        printf '%s\n' "Press Enter to exit...">"$print_to_screen"
        read temp
    fi
}

DisplayHelp () {
    printf '%s\n' " TEXT project ARCHiver - a dash shell based script to archive a folder and its content as a self extracting SHell script ARchive (SHAR) (plain text)"
    printf '%s\n' " "
    printf '%s\n' " Archiving:"
    printf '%s\n' "     - Parameters syntax: <input_folder_path> [ <archive_name> <output_folder_path> ] [ <flags> ]"
    printf '%s\n' "     "
    printf '%s\n' "     - what it does:"
    printf '%s\n' "         - archives <input_folder> as <archive_name>.<default_extension> in the specified <output_folder_path>"
    printf '%s\n' "             - if <archive_name> is an empty string (\"\") / empty: it is considered the name of the <input_folder>"
    printf '%s\n' "             - if <output_folder_path> is an empty string (\"\") / empty: it is considered the path of the parent directory of <input_folder>"
    printf '%s\n' "         - for archiving: <flags> can be:"
    printf '%s\n' "             - --text-mode"
    printf '%s\n' "                 - archive only text files"
    printf '%s\n' "             - --find-parameters / -f"
    printf '%s\n' "                 - all the parameters given after the '--find-parameters' flag, are considered 'find' parameters (denoted here as <find_parameters>)"
    printf '%s\n' "                 - <find_parameters> can be: any parameters that can be passed to the 'find' utility (which is used internally by this script) - such as: name/path filters"
    printf '%s\n' "     - usage:"
    printf '%s\n' "     "
    printf '%s\n' " Extraction:"
    printf '%s\n' "     - Parameters syntax: <input_archive_path> [ <extraction_output_name> <extraction_output_path> ] [ <flags> ]"
    printf '%s\n' "     "
    printf '%s\n' "     - what it does:"
    printf '%s\n' "         - extracts <input_archive> to the <extraction_output_path>/<extraction_output_name> path; if non-empty: <extraction_output_name> is created"
    printf '%s\n' "             - if <extraction_output_name>: is empty/not provided -> it is considered blank; is an empty string (\"\") -> it is considered the name of the <input_archive>"
    printf '%s\n' "             - if <extraction_output_path> is an empty string (\"\") / empty / not provided: it is considered the path of the parent directory of <input_archive>"
    printf '%s\n' " "
    printf '%s\n' " General <flags> can be:"
    printf '%s\n' "     -h / --help - displays this help information"
    printf '%s\n' "     -v / --verbose - displays the current file progress on the terminal screen (by default only errors are displayed)"
    printf "\n"
}

CheckUtilities () {
    #Check if any of the necessary utilities is missing:
    error="false"
    
    for utility; do
        which $utility>/dev/null 2>/dev/null || { printf "\n%s\n" "ERROR: the '$utility' utility is not installed!">&2; error="true"; }
    done
    
    if [ "$error" = "true" ]; then
        CleanUp; exit 1
    fi
}

ReadRaw () {
    initial_IFS="$IFS"
    
    IFS= read -r $1
    
    #Restore initial IFS:
    unset IFS
    
    eval value=\"\$$1\"
    
    if [ -n "$value" ]; then printf "\n"; fi
}

ReadArchiveParameters () {
    {
    printf '\n%s\n' "Archiving: Please provide the ouput archive name (default = Enter = archive's parent directory name):"
    ReadRaw archive_name
    
    printf '%s\n' "Archiving: Please provide the output archive folder path (default = Enter = the parent folder path of the archive):"
    ReadRaw output_path
    
    if [ -z "$find_parameters" ]; then
        printf '%s\n' "Archiving: Please provide find parameters (default = Enter = <none>):"
        ReadRaw find_parameters
    fi
    }>$print_to_screen
}

ErrorMessage () {
    if [ "$error" = "false" ]; then
        PrintErrorExtra
    fi
    printf '\n%s\n' "$1">&2
    error="true"
}

PrintFileStart () {
    printf "\n"
    printf '%s\n' "### >>> START: FILE $k ($1):"
    printf "\t\n"
    printf '\t%s\n' "current_error=\"false\";"
    printf "\t\n"
}

PrintFileEnd () {
    printf '\t%s\n' "[ \"\$current_error\" = \"true\" ] && { error=\"true\"; printf \"ERROR: Extraction: FILE $k: Could not write to: \\\"\$BASE_PATH/\$crt_file_or_dir_path_escaped\\\"\"; }>&2"
    printf "\n"
    printf '%s\n' "### <<< END: FILE $k ($1):"
    printf "\n"
}

PrintExtractingFileMessage () {
    printf '\t%s\n' "PrintJustInTitle \"Extracting file $k - size $current_file_size_in_bytes"" B...\""
    printf '\t%s\n' "[ \"\$verbose_flag\" = \"1\" ] && printf \"%s\\n\" \"Extracting file $k ($1): \\\"\$BASE_PATH/\$crt_file_or_dir_path_escaped\\\" - size: $current_file_size_in_bytes Bytes...\">\$print_to_screen"
}


print_to_screen="/dev/tty" #print to screen only

initial_dir="$PWD"

NL2=$(printf '%s' "\n\n") #Store New Line for use with sed

#Trap "INTERRUPT" (CTRL-C) and "TERMINAL STOP" (CTRL-Z) signals:
trap 'trap1' INT
trap 'trap1' TSTP

#For use with xxd: max number of lines stored at a time by the 'cat' utility:
MAX_LINES_NUMBER=500000 #approx. 30 MB

#The maximum amount of memory in bytes, that the 'cat' utility can use, when archiving a text file as plain text:
MAX_TEXT_FILE_SIZE_IN_BYTES="2000000000" #approx. 2 GB

#Define archive extension here:
archive_extension="textarch"


#Enable globbing (POSIX compliant):
set +f

eval ARCHIVE_SIGNATURE_STRING=\"\T\E\X\T\A\R\C\H\I\V\E\"

GetOSType OS_TYPE

if [ "$OS_TYPE" = "Linux" ] || [ "$OS_TYPE" = "Other" ]; then
    NEG='!'
elif [ "$OS_TYPE" = "BSD-based" ]; then
    NEG='-not'
fi


#Store and process parameters:

help_flag="0"
verbose_flag="0"
text_flag="0"
find_parameters_flag="0"

params=""
find_parameters=""

selected_params_count=0

flag_count="0"

for param; do
    if [ "$find_parameters_flag" = "0" ]; then
        case "$param" in
            "-v" | "--verbose" | "--help" | "--text-mode" | "-t" | "--find-parameters" | "-f" )
                case "$param" in
                    "-v" | "--verbose" )
                        verbose_flag="1"
                    ;;
                    "--help" )
                        help_flag="1"
                    ;;
                    "--text-mode" | "-t" )
                        text_flag="1"
                    ;;
                    "--find-parameters" | "-f" )
                        find_parameters_flag="1"
                    ;;
                esac
                flag_count=$(($flag_count + 1))
            ;;
            * )
                selected_params_count=$(($selected_params_count+1))
                eval selected_params_$selected_params_count=\"\$param\"
            ;;
        esac
    else
        if [ "$find_parameters_flag" = "1" ]; then
            if [ -z "$find_parameters" ]; then
                find_parameters="$param"
            else
                if [ ! "$param" = "${param%*"*"*}" ] || [ ! "$param" = "${param%*"?"*}" ]; then # * and ? characters are treated literaly
                    find_parameters="$find_parameters ""'""$param""'"
                else
                    find_parameters="$find_parameters ""$param"
                fi
            fi
        fi
    fi
done
selected_params_0="$selected_params_count"

if [ -z "$find_parameters" ]; then
    find_parameters='-name "*"'
fi

if [ "$help_flag" = "1" ] || [ "$selected_params_0" = "0" ]; then
    DisplayHelp
    exit 0
fi

for i in $(seq 1 $selected_params_0); do
    eval params_$i=\"\$selected_params_$i\" #Store parameters other than flags
done
params_0=$selected_params_0

CheckUtilities find sed cat file sort chmod grep mkdir iconv stat seq kill rm

#Process parameters/flags and check for errors:

eval find /dev/null $find_parameters>/dev/null || {
    PrintErrorExtra
    printf "%s\n\n" "ERROR: Invalid find parameters provided after the \"--find-parameters\" flag!">&2
    CleanUp; exit 1
}

if [ "$params_0" -ge "1" ]; then
    {
    
    error="false"
    
    if [ "$params_0" -gt "3" ]; then
        ErrorMessage "ERROR: Archiving: Expected 1 or 3 parameters: <input_folder_path> [ <archive_name> <output_folder_path> ]!"
    else
        
        input_folder_path="$params_1"
        
        if [ "$params_0" -eq "3" ]; then
            archive_name="$params_2"
            output_path="$params_3"
        elif [ "$params_0" -eq "2" ]; then
            ErrorMessage "ERROR: Archiving: Expected 1 or 3 parameters: <input_folder_path> [ <archive_name> <output_folder_path> ]!"
        elif [ "$params_0" -eq "1" ]; then
            ReadArchiveParameters
        fi
        
        if [ -n "$archive_name" ] && [ ! "${archive_name%*"/"*}" = "$archive_name" ]; then
            ErrorMessage "ERROR: Archiving: Second provided non-flag parameter must be the name of the archive (must not contain '/')!"
        fi
        
        if [ -n "$output_path" ] && [ "${output_path%*"/"*}" = "$output_path" ]; then
            ErrorMessage "ERROR: Archiving: Third provided non-flag parameter must be the output path of the archive (must either be blank or contain '/')!"
        fi
    fi
    
    if [ "$error" = "true" ]; then
        CleanUp; exit 1
    fi
    
    ConvertToFullPath input_folder_path input_folder_full_path
    
    ExtractFirstAndLastPathComponent input_folder_full_path fpc_input_folder_path lpc_input_folder_path
    
    if [ -z "$archive_name" ] || [ "$archive_name" = "\"\"" ]; then
        archive_name="$lpc_input_folder_path"
    fi
    
    if [ -z "$output_path" ]; then
        output_path="$fpc_input_folder_path"
        if [ -z "$fpc_input_folder_path" ]; then
            output_path='/'
        fi
    fi
    
    archive_name_plus_ext="$archive_name"."$archive_extension"
    
    if [ ! "$input_folder_full_path" = "/" ]; then
        input_folder_parent_dir_path="$fpc_input_folder_path"
    fi
    
    if [ ! -d "$input_folder_path" ]; then
        ErrorMessage "ERROR: Archiving: The path provided as the first non-flag parameter: \"$input_folder_path\" is not a valid directory path or is not accessible!"
    fi
    
    if [ ! -d "$output_path" ]; then
        ErrorMessage "ERROR: Archiving: Output path: \"$output_path\" is not a valid directory path or is not accessible!"
    fi
    
    if [ "$error" = "true" ]; then
        CleanUp; exit 1
    fi
    }>&2
    
    ConvertToFullPath output_path output_path
    #Remove trailing '/' from output_path:
    ExtractFirstAndLastPathComponent output_path fpc_output_path lpc_output_path
    if [ ! "$output_path" = '/' ]; then
        output_path="$fpc_output_path/$lpc_output_path"
    fi
    
    cd "$initial_dir"
elif [ "$params_0" -eq "0" ]; then
    DisplayHelp
    exit 0
fi


params_0=$selected_params_0
if [ "$error" = "true" ]; then
    printf "\n\n">&2
    CleanUp; exit 1
fi


cd "$initial_dir"


### DEFINE OUTPUT FILE: ###
if [ "$output_path" = '/' ]; then
    output_file="/$archive_name_plus_ext"
else
    output_file="$output_path/$archive_name_plus_ext"
fi

if [ -e "$output_file" ]; then
    ErrorMessage "ERROR: Archiving: Output file: \"$output_file\" already exists!"
    CleanUp; exit 1
fi

ConvertToFullPath output_file output_file_full_path


###     
{ cat /dev/null>>"$output_file_full_path"; } 2>/dev/null || {
    ErrorMessage "ERROR: Archiving: Could not write to output file: \"$output_file\"!"
    CleanUp; exit 1
}
{ {
    
    ExtractFirstAndLastPathComponent input_folder_full_path fpc_input_folder_full_path lpc_input_folder_full_path
    
    cat <<'EOF'
#!/bin/dash

EOF
    
    printf '%s\n' "### $ARCHIVE_SIGNATURE_STRING ARCHIVE ###"
    
    cat<<'EOF'

### >>> START: ARCHIVE HEADER

PrintInTitle () {
    printf "\033]0;%s\007" "$1"
}

PrintJustInTitle () {
    PrintInTitle "$1">"$print_to_screen"
}

ReadRaw () {
    initial_IFS="$IFS"
    
    IFS= read -r $1
    
    #Restore initial IFS:
    unset IFS
}

CheckUtilities () {
    #Check if any of the necessary utilities is missing:
    error="false"
    
    for utility; do
        which $utility >/dev/null 2>/dev/null || { ErrorMessage "ERROR: the '$utility' utility is not installed!"; }
    done>&2
    
    if [ "$error" = "true" ]; then
        CleanUp2; exit 1
    fi
}

trap2 () {
    CleanUp2
    
    printf "\n""Extraction: Aborted.\n">"$print_to_screen"
    
    printf '%s\n' "Press Enter to exit...">"$print_to_screen"
    read temp
    
    #kill all children processes, suppressing "Terminated" message:
    kill -s PIPE -- -$$ 2>/dev/null
}

CleanUp2 () {
    
    #Restore "INTERRUPT" (CTRL-C) and "TERMINAL STOP" (CTRL-Z) signals:
    trap - INT
    trap - TSTP
    
    cd "$initial_dir"
    
    CheckPause
}

CheckPause () {
    if [ -z "$params_1" ] && [ -z "$params_2" ]; then
        printf '%s\n' "Press Enter to exit...">"$print_to_screen"
        read temp
    fi
}

ReadExtractParameters () {
    {
    printf '\n%s\n' "Extraction: Please provide the name of the folder where to extract the archive (this will be created) (default = blank = <empty>):"
    ReadRaw extraction_output_name
    if [ -n "$extraction_output_name" ]; then printf "\n"; fi
    
    printf '%s\n' "Extraction: Please provide the parent directory path of the folder where to extract the archive (default = blank = <archive parent directory path>):"
    ReadRaw extraction_output_path
    if [ -n "$extraction_output_path" ]; then printf "\n"; fi
    }>$print_to_screen
}

ErrorMessage () {
    if [ "$error" = "false" ]; then
        PrintErrorExtra
    fi
    printf '\n%s\n' "$1">&2
    error="true"
}

PrintErrorExtra () {
    {
    if [ ! "$error" = "true" ]; then
        for cc in $(seq 1 $params_0); do
            eval current_param_func=\"\$params_$cc\"
            printf '\n%s\n' "Parameter $cc: '$current_param_func'"
        done
        printf "\n"
    fi
    }>&2
}

ArchivingOutputFileWriteError () {
    ErrorMessage "ERROR: Archiving: Could not write to output file: \"$output_file\"!"
    CleanUp2; exit 1
}


print_to_screen="/dev/tty"

initial_dir="$PWD"

#Trap "INTERRUPT" (CTRL-C) and "TERMINAL STOP" (CTRL-Z) signals:
trap 'trap2' INT
trap 'trap2' TSTP

stored_archive_size_in_bytes="000000000000000"

verbose_flag="0"
i=0
params_0=0
for param; do
    case "$param" in
        "-v" | "--verbose" )
            verbose_flag="1"
        ;;
        * )
            i=$(($i+1))
            eval params_$i=\"\$param\"
        ;;
    esac
done
params_0="$i"

CheckUtilities mkdir iconv sed rm

cd "${0%/*}" 2>/dev/null
current_script_path="$(pwd -P)/${0##*/}"
input_archive_path="$current_script_path"

cd "$initial_dir"

lpc_input_archive_path="${input_archive_path##*"/"}"
fpc_input_archive_path="${input_archive_path%"$lpc_input_archive_path"*}"

{

[ ! "$params_1" = "${params_1%*"/"*}" ] && params_1_is_full_path="false"
[ ! "$params_2" = "${params_2%*"/"*}" ] && params_2_is_full_path="false"

if [ ! "$params_0" -eq "0" ]; then
    if [ "$params_0" -le "2" ]; then
        if [ "$params_1_is_full_path" = "$params_2_is_full_path" ]; then
            ErrorMessage "ERROR: Extraction: If 2 parameters provided: one should be a name (should not contain '/') and the other should be a path (should contain '/')!"
            CleanUp2; exit 1
        elif [ "$params_1_is_full_path" = "false" ]; then
            extraction_output_name="$params_1"
            extraction_output_path="$params_2"
        else
            extraction_output_name="$params_2"
            extraction_output_path="$params_1"
        fi
    else
        ErrorMessage "ERROR: Extraction: Expected between 0 and 2 parameters!"
        CleanUp2; exit 1
    fi
else
    ReadExtractParameters
fi

if [ "$extraction_output_name" = \"\" ]; then
    extraction_output_name="${lpc_input_archive_path%"."*}"
fi

if [ "$extraction_output_path" = "\"\"" ] || [ -z "$extraction_output_path" ]; then
    extraction_output_path="$fpc_input_archive_path"
fi

error="false"
if [ -n "$extraction_output_path" ]; then
    cd "$extraction_output_path" 2>/dev/null && {
        if [ -e "$extraction_output_name" ]; then
            ErrorMessage "ERROR: Extraction: The provided folder name: \"$extraction_output_name\" already exists as a directory in the specified location: \"$extraction_output_path\"!"
            CleanUp2; exit 1
        elif [ -n "$extraction_output_name" ]; then
            mkdir "$extraction_output_name" 2>/dev/null || {
                ErrorMessage "ERROR: Extraction: Could not create directory: \"$extraction_output_name\" to the extraction output path: \"$extraction_output_path\"!"
                CleanUp2; exit 1
            }
            cd "$extraction_output_name"
        fi
    } || {
        ErrorMessage "ERROR: Extraction: Could not access extraction output path: \"$extraction_output_path\"!"
        CleanUp2; exit 1
    }
fi

BASE_PATH="$PWD"

if [ ! -w "$BASE_PATH" ]; then
    ErrorMessage "ERROR: Extraction: Cannot extract archive: BASE_PATH directory \"$BASE_PATH\" is not writable\"!"
    CleanUp2; exit 1
fi

}

EOF
}>"$output_file"; } 2>/dev/null || ArchivingOutputFileWriteError

ExtractFirstAndLastPathComponent output_file_full_path fpc_output_file_full_path lpc_output_file_full_path
ExtractFirstAndLastPathComponent input_folder_full_path fpc_input_folder_full_path lpc_input_folder_full_path

if [ "$verbose_flag" = "1" ]; then
    printf "\n"
fi

if [ ! "$input_folder_full_path" = "/" ]; then
    if [ "$fpc_input_folder_full_path" = "" ]; then
        input_folder_name="/$lpc_input_folder_full_path"
    else
        input_folder_name="./$lpc_input_folder_full_path"
    fi
else
    input_folder_name=""
fi

{ {
cat<<EOF
if [ -z "\$extraction_output_name" ] && [ -e "$input_folder_name" ]; then
    ErrorMessage "ERROR: Extraction: Could not create directory: \"$input_folder_name\" to the extraction output path: \"\$extraction_output_path\" - directory already exists!"
    CleanUp2; exit 1
fi

error="false"

Q="'"

EOF
}>>"$output_file"; } 2>/dev/null || ArchivingOutputFileWriteError


Q="'"

IFS='
'

j=0
k=0
error="false"

{
cat<<'EOF'
cd "$BASE_PATH"

### <<< END: ARCHIVE HEADER

EOF
}>>"$output_file_full_path"

if [ "$verbose_flag" = "1" ]; then
    printf "\n"
fi

cd "$initial_dir"

if [ "$error" = "true" ]; then printf "\n">&2 ; fi


#Proceed to Archiving files:

cd "$input_folder_full_path"

output_file_relative_path=".${output_file_full_path#$input_folder_full_path}"

j=0
k=0
for line in $(eval find . \\\( -type d -o -type f \\\) -a $NEG -path '.' -a $NEG -path './' -a $NEG -path "\"\$output_file_relative_path\"" $find_parameters 2>/dev/null|sort); do
    
    j=$(($j+1))
    
    current_file_or_dir="$input_folder_parent_dir_path/$input_folder_name/$line"
    
    GetFileEncodingAndSizeInBytes current_file_or_dir current_file_encoding current_file_size_in_bytes
    current_file_size_in_bytes_padded="$(printf '%012d' "$current_file_size_in_bytes")"
    PrintJustInTitle "Analyzing/Archiving: file path $j - size $current_file_size_in_bytes_padded"" B..."
    
    current_file_or_dir2="$input_folder_name/$line"
    crt_file_or_dir_path2_escaped="$(printf '%s\n' "$current_file_or_dir2"|sed "s/'/$Q\"\$Q\"$Q/g")"
    
    if [ ! -r "$current_file_or_dir" ]; then
        printf '%s\n' "ERROR: Archiving: File not accessible/readable: \"$current_file_or_dir2\"!">&2
        error="true"
    fi
    
    if [ -d "$current_file_or_dir" ]; then
        
        if [ "$verbose_flag" = "1" ]; then
            printf '%s\n' "Archiving file $j (dir): \"$current_file_or_dir2\"..."
        fi
        
        k=$(($k+1))
        
        { {
            PrintFileStart "DIR"
            
            printf "\t%s\n" "crt_file_or_dir_path_escaped='$crt_file_or_dir_path2_escaped'"
            printf "\t\n"
            
            PrintExtractingFileMessage dir
            
            printf '\t%s\n' "mkdir -p \"\$BASE_PATH/\$crt_file_or_dir_path_escaped\" 2>/dev/null || { current_error=\"true\"; }"
            
            PrintFileEnd "DIR"
        }>>"$output_file_full_path"; } 2>/dev/null || ArchivingOutputFileWriteError
    elif [ -f "$current_file_or_dir" ]; then
        
        if [ "$verbose_flag" = "1" ]; then
            printf '%s\n' "Archiving file $j (file): \"$current_file_or_dir2\" - size: $current_file_size_in_bytes Bytes..."
        fi
        
        if [ ! "$current_file_encoding" = "binary" ] && [ "$current_file_size_in_bytes" -le "$MAX_TEXT_FILE_SIZE_IN_BYTES" ]; then
            
            k=$(($k+1))
            
            { {
                
                PrintFileStart "TEXT FILE"
                
                printf "\t%s\n" "crt_file_or_dir_path_escaped='$crt_file_or_dir_path2_escaped'"
                printf "\t\n"
                
                PrintExtractingFileMessage file
                
                printf '\t%s\n' "mkdir -p \"\$BASE_PATH/\${crt_file_or_dir_path_escaped%\"/\"*}\" && {"
                printf '\t\t%s\n' "{"
                printf '\t\t\t%s\n' "{"
                
                printf '\t\t\t\t%s\n' "cat<<'EOFxxyyzzzzyyxx'"
                utf8="false"; utf="false"
                case "$current_file_encoding" in
                    *"utf-8"* )
                        utf8="true"
                    ;;
                    *"utf"* )
                        utf="true"
                    ;;
                esac
                if [ "$utf8" = "true" ]; then
                    cat "$current_file_or_dir"
                elif [ "$utf" = "true" ]; then 
                    iconv -f "$current_file_encoding" -t utf8 "$current_file_or_dir"
                else
                    cat "$current_file_or_dir"
                fi|sed 's/EOFxxyyzzzzyyxx/eEOFxxyyzzzzyyxx/g'
                printf '\n%s\n' 'EOFxxyyzzzzyyxx'
                printf '\t\t\t%s\n' "}|sed 's/eEOFxxyyzzzzyyxx/EOFxxyyzzzzyyxx/g'>>\"\$BASE_PATH/\$crt_file_or_dir_path_escaped\""
                printf '\t\t%s' "} 2>/dev/null"
                
                printf '%s\n' " && {"
                printf '\t\t\t%s\n' "perl -pi -e 'chomp if eof' \"\$BASE_PATH/\$crt_file_or_dir_path_escaped\" 2>/dev/null || sed -i -z 's/\(\n\)$//' \"\$BASE_PATH/\$crt_file_or_dir_path_escaped\" 2>/dev/null"
                printf '\t\t%s\n' "} || current_error=\"true\""
                
                if [ "$utf" = "true" ]; then
                    printf '\t\t%s\n' "iconv -f utf-8 -t \"$current_file_encoding\" \"\$BASE_PATH/\$crt_file_or_dir_path_escaped\" -o \"\$BASE_PATH/\$crt_file_or_dir_path_escaped\""
                fi
                
                printf '\t%s\n' "} || current_error=\"true\""
                
                PrintFileEnd "TEXT FILE"
            }>>"$output_file_full_path"; } 2>/dev/null || ArchivingOutputFileWriteError
        elif [ "$text_flag" = "0" ]; then
            k=$(($k+1))
            {
                PrintFileStart "HEX ENCODED FILE"
                
                printf "\t%s\n\n" "crt_file_or_dir_path_escaped='$crt_file_or_dir_path2_escaped'"
                
                PrintExtractingFileMessage file
                
                printf '\t%s\n' "mkdir -p \"\$BASE_PATH/\${crt_file_or_dir_path_escaped%\"/\"*}\" && {"
                
                printf '%s\n' "cat<<'EOFxxyyzzzzyyxx'"
                
                if [ "$current_file_size_in_bytes" -le "100000000000" ]; then
                    xxd -p "$current_file_or_dir"|{
                        awk '\
                            { print $0; }; \
                            (NR%'"$MAX_LINES_NUMBER"'==0){ \
                                print "EOFxxyyzzzzyyxx"; \
                                print "}\x3e\x3e\x22$BASE_PATH/$crt_file_or_dir_path_escaped"".tempxyzzyx\x22"; \
                                print "{ cat\x3c\x3c\x27EOFxxyyzzzzyyxx\x27"; \
                            }; \
                        '
                    }
                fi
                
                printf '%s\n' "EOFxxyyzzzzyyxx"
                printf "\t%s" "}>>\"\$BASE_PATH/\$crt_file_or_dir_path_escaped"".tempxyzzyx\""
                printf '%s\n' " || current_error=\"true\""
                
                printf '\t%s'"\n" "xxd -p -r \"\$BASE_PATH/\$crt_file_or_dir_path_escaped.tempxyzzyx\" \"\$BASE_PATH/\$crt_file_or_dir_path_escaped\" && {"
                printf '\t\t%s'"\n" "rm \"\$BASE_PATH/\$crt_file_or_dir_path_escaped.tempxyzzyx\""
                printf '\t%s'"\n" "}"
                
                PrintFileEnd "HEX ENCODED FILE"
            }>>"$output_file_full_path" || ArchivingOutputFileWriteError
        elif [ "$current_file_encoding" = "binary" ]; then
            printf '\n%s\n' "WARNING: Archiving: Skipping binary file: \"$current_file_or_dir2\"">&2
        fi
    fi
done
if [ ! "$file_or_dir_path_0" = "0" ]; then
    if [ "$verbose_flag" = "1" ]; then printf "\n"; fi
else
    printf '\n%s\n\n' "WARNING: Archiving: No file match - so no files where added to the archive...">&2
fi

{
cat<<EOF

### >>> START: ARCHIVE FOOTER
    
    ### TOTAL FILES COUNT: $k
    
    PrintJustInTitle "Extraction: Done."
    
    printf '\n%s' "Extraction: Done."
    
    if [ "\$error" = "true" ]; then
        printf '%s\n' " Errors were encountered!"
    elif [ "\$error" = "false" ]; then
        printf '%s\n' " No errors were encountered!"
    fi
    
    CleanUp2

### <<< END: ARCHIVE FOOTER
EOF
}>>"$output_file_full_path"

#Make the generated archive a read-only file for all users:
chmod a=r "$output_file"

#Make the generated archive an executable file for all users:
chmod a+x "$output_file"

PrintJustInTitle "Archiving: Done."
printf "\n"

printf '%s' "Archiving: Done."
if [ "$error" = "true" ]; then
    printf '%s\n' " Errors were encountered!"
elif [ "$error" = "false" ]; then
    printf '%s\n' " No errors were encountered!"
fi

printf "\n"

CleanUp
0
aBarocio80 On

Some good articles on how to do exactly that could be found at: