| 
							- #!/usr/bin/env bash
 - 
 - # Purpose: plain text tar format
 - # Limitations: - only suitable for text files, directories, and symlinks
 - #              - stores only filename, content, and mode
 - #              - not designed for untrusted input
 - #
 - # Note: must work with bash version 3.2 (macOS)
 - 
 - # Copyright 2017 Roger Luethi
 - #
 - # Licensed under the Apache License, Version 2.0 (the "License");
 - # you may not use this file except in compliance with the License.
 - # You may obtain a copy of the License at
 - #
 - # http://www.apache.org/licenses/LICENSE-2.0
 - #
 - # Unless required by applicable law or agreed to in writing, software
 - # distributed under the License is distributed on an "AS IS" BASIS,
 - # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 - # See the License for the specific language governing permissions and
 - # limitations under the License.
 - 
 - set -o errexit -o nounset
 - 
 - # Sanitize environment (for instance, standard sorting of glob matches)
 - export LC_ALL=C
 - 
 - path=""
 - CMD=""
 - ARG_STRING="$*"
 - 
 - #------------------------------------------------------------------------------
 - # Not all sed implementations can work on null bytes. In order to make ttar
 - # work out of the box on macOS, use Python as a stream editor.
 - 
 - USE_PYTHON=0
 - 
 - PYTHON_CREATE_FILTER=$(cat << 'PCF'
 - #!/usr/bin/env python
 - 
 - import re
 - import sys
 - 
 - for line in sys.stdin:
 -     line = re.sub(r'EOF', r'\EOF', line)
 -     line = re.sub(r'NULLBYTE', r'\NULLBYTE', line)
 -     line = re.sub('\x00', r'NULLBYTE', line)
 -     sys.stdout.write(line)
 - PCF
 - )
 - 
 - PYTHON_EXTRACT_FILTER=$(cat << 'PEF'
 - #!/usr/bin/env python
 - 
 - import re
 - import sys
 - 
 - for line in sys.stdin:
 -     line = re.sub(r'(?<!\\)NULLBYTE', '\x00', line)
 -     line = re.sub(r'\\NULLBYTE', 'NULLBYTE', line)
 -     line = re.sub(r'([^\\])EOF', r'\1', line)
 -     line = re.sub(r'\\EOF', 'EOF', line)
 -     sys.stdout.write(line)
 - PEF
 - )
 - 
 - function test_environment {
 -     if [[ "$(echo "a" | sed 's/a/\x0/' | wc -c)" -ne 2 ]]; then
 -         echo "WARNING sed unable to handle null bytes, using Python (slow)."
 -         if ! which python >/dev/null; then
 -             echo "ERROR Python not found. Aborting."
 -             exit 2
 -         fi
 -         USE_PYTHON=1
 -     fi
 - }
 - 
 - #------------------------------------------------------------------------------
 - 
 - function usage {
 -     bname=$(basename "$0")
 -     cat << USAGE
 - Usage:   $bname [-C <DIR>] -c -f <ARCHIVE> <FILE...> (create archive)
 -          $bname            -t -f <ARCHIVE>           (list archive contents)
 -          $bname [-C <DIR>] -x -f <ARCHIVE>           (extract archive)
 - 
 - Options:
 -          -C <DIR>           (change directory)
 -          -v                 (verbose)
 -          --recursive-unlink (recursively delete existing directory if path
 -                              collides with file or directory to extract)
 - 
 - Example: Change to sysfs directory, create ttar file from fixtures directory
 -          $bname -C sysfs -c -f sysfs/fixtures.ttar fixtures/
 - USAGE
 - exit "$1"
 - }
 - 
 - function vecho {
 -     if [ "${VERBOSE:-}" == "yes" ]; then
 -         echo >&7 "$@"
 -     fi
 - }
 - 
 - function set_cmd {
 -     if [ -n "$CMD" ]; then
 -         echo "ERROR: more than one command given"
 -         echo
 -         usage 2
 -     fi
 -     CMD=$1
 - }
 - 
 - unset VERBOSE
 - unset RECURSIVE_UNLINK
 - 
 - while getopts :cf:-:htxvC: opt; do
 -     case $opt in
 -         c)
 -             set_cmd "create"
 -             ;;
 -         f)
 -             ARCHIVE=$OPTARG
 -             ;;
 -         h)
 -             usage 0
 -             ;;
 -         t)
 -             set_cmd "list"
 -             ;;
 -         x)
 -             set_cmd "extract"
 -             ;;
 -         v)
 -             VERBOSE=yes
 -             exec 7>&1
 -             ;;
 -         C)
 -             CDIR=$OPTARG
 -             ;;
 -         -)
 -             case $OPTARG in
 -                 recursive-unlink)
 -                     RECURSIVE_UNLINK="yes"
 -                     ;;
 -                 *)
 -                     echo -e "Error: invalid option -$OPTARG"
 -                     echo
 -                     usage 1
 -                     ;;
 -             esac
 -             ;;
 -         *)
 -             echo >&2 "ERROR: invalid option -$OPTARG"
 -             echo
 -             usage 1
 -             ;;
 -     esac
 - done
 - 
 - # Remove processed options from arguments
 - shift $(( OPTIND - 1 ));
 - 
 - if [ "${CMD:-}" == "" ]; then
 -     echo >&2 "ERROR: no command given"
 -     echo
 -     usage 1
 - elif [ "${ARCHIVE:-}" == "" ]; then
 -     echo >&2 "ERROR: no archive name given"
 -     echo
 -     usage 1
 - fi
 - 
 - function list {
 -     local path=""
 -     local size=0
 -     local line_no=0
 -     local ttar_file=$1
 -     if [ -n "${2:-}" ]; then
 -         echo >&2 "ERROR: too many arguments."
 -         echo
 -         usage 1
 -     fi
 -     if [ ! -e "$ttar_file" ]; then
 -         echo >&2 "ERROR: file not found ($ttar_file)"
 -         echo
 -         usage 1
 -     fi
 -     while read -r line; do
 -         line_no=$(( line_no + 1 ))
 -         if [ $size -gt 0 ]; then
 -             size=$(( size - 1 ))
 -             continue
 -         fi
 -         if [[ $line =~ ^Path:\ (.*)$ ]]; then
 -             path=${BASH_REMATCH[1]}
 -         elif [[ $line =~ ^Lines:\ (.*)$ ]]; then
 -             size=${BASH_REMATCH[1]}
 -             echo "$path"
 -         elif [[ $line =~ ^Directory:\ (.*)$ ]]; then
 -             path=${BASH_REMATCH[1]}
 -             echo "$path/"
 -         elif [[ $line =~ ^SymlinkTo:\ (.*)$ ]]; then
 -             echo  "$path -> ${BASH_REMATCH[1]}"
 -         fi
 -     done < "$ttar_file"
 - }
 - 
 - function extract {
 -     local path=""
 -     local size=0
 -     local line_no=0
 -     local ttar_file=$1
 -     if [ -n "${2:-}" ]; then
 -         echo >&2 "ERROR: too many arguments."
 -         echo
 -         usage 1
 -     fi
 -     if [ ! -e "$ttar_file" ]; then
 -         echo >&2 "ERROR: file not found ($ttar_file)"
 -         echo
 -         usage 1
 -     fi
 -     while IFS= read -r line; do
 -         line_no=$(( line_no + 1 ))
 -         local eof_without_newline
 -         if [ "$size" -gt 0 ]; then
 -             if [[ "$line" =~ [^\\]EOF ]]; then
 -                 # An EOF not preceded by a backslash indicates that the line
 -                 # does not end with a newline
 -                 eof_without_newline=1
 -             else
 -                 eof_without_newline=0
 -             fi
 -             # Replace NULLBYTE with null byte if at beginning of line
 -             # Replace NULLBYTE with null byte unless preceded by backslash
 -             # Remove one backslash in front of NULLBYTE (if any)
 -             # Remove EOF unless preceded by backslash
 -             # Remove one backslash in front of EOF
 -             if [ $USE_PYTHON -eq 1 ]; then
 -                 echo -n "$line" | python -c "$PYTHON_EXTRACT_FILTER" >> "$path"
 -             else
 -                 # The repeated pattern makes up for sed's lack of negative
 -                 # lookbehind assertions (for consecutive null bytes).
 -                 echo -n "$line" | \
 -                     sed -e 's/^NULLBYTE/\x0/g;
 -                             s/\([^\\]\)NULLBYTE/\1\x0/g;
 -                             s/\([^\\]\)NULLBYTE/\1\x0/g;
 -                             s/\\NULLBYTE/NULLBYTE/g;
 -                             s/\([^\\]\)EOF/\1/g;
 -                             s/\\EOF/EOF/g;
 -                     ' >> "$path"
 -             fi
 -             if [[ "$eof_without_newline" -eq 0 ]]; then
 -                 echo >> "$path"
 -             fi
 -             size=$(( size - 1 ))
 -             continue
 -         fi
 -         if [[ $line =~ ^Path:\ (.*)$ ]]; then
 -             path=${BASH_REMATCH[1]}
 -             if [ -L "$path" ]; then
 -                 rm "$path"
 -             elif [ -d "$path" ]; then
 -                 if [ "${RECURSIVE_UNLINK:-}" == "yes" ]; then
 -                     rm -r "$path"
 -                 else
 -                     # Safe because symlinks to directories are dealt with above
 -                     rmdir "$path"
 -                 fi
 -             elif [ -e "$path" ]; then
 -                 rm "$path"
 -             fi
 -         elif [[ $line =~ ^Lines:\ (.*)$ ]]; then
 -             size=${BASH_REMATCH[1]}
 -             # Create file even if it is zero-length.
 -             touch "$path"
 -             vecho "    $path"
 -         elif [[ $line =~ ^Mode:\ (.*)$ ]]; then
 -             mode=${BASH_REMATCH[1]}
 -             chmod "$mode" "$path"
 -             vecho "$mode"
 -         elif [[ $line =~ ^Directory:\ (.*)$ ]]; then
 -             path=${BASH_REMATCH[1]}
 -             mkdir -p "$path"
 -             vecho "    $path/"
 -         elif [[ $line =~ ^SymlinkTo:\ (.*)$ ]]; then
 -             ln -s "${BASH_REMATCH[1]}" "$path"
 -             vecho "    $path -> ${BASH_REMATCH[1]}"
 -         elif [[ $line =~ ^# ]]; then
 -             # Ignore comments between files
 -             continue
 -         else
 -             echo >&2 "ERROR: Unknown keyword on line $line_no: $line"
 -             exit 1
 -         fi
 -     done < "$ttar_file"
 - }
 - 
 - function div {
 -     echo "# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -" \
 -          "- - - - - -"
 - }
 - 
 - function get_mode {
 -     local mfile=$1
 -     if [ -z "${STAT_OPTION:-}" ]; then
 -         if stat -c '%a' "$mfile" >/dev/null 2>&1; then
 -             # GNU stat
 -             STAT_OPTION='-c'
 -             STAT_FORMAT='%a'
 -         else
 -             # BSD stat
 -             STAT_OPTION='-f'
 -             # Octal output, user/group/other (omit file type, sticky bit)
 -             STAT_FORMAT='%OLp'
 -         fi
 -     fi
 -     stat "${STAT_OPTION}" "${STAT_FORMAT}" "$mfile"
 - }
 - 
 - function _create {
 -     shopt -s nullglob
 -     local mode
 -     local eof_without_newline
 -     while (( "$#" )); do
 -         file=$1
 -         if [ -L "$file" ]; then
 -             echo "Path: $file"
 -             symlinkTo=$(readlink "$file")
 -             echo "SymlinkTo: $symlinkTo"
 -             vecho "    $file -> $symlinkTo"
 -             div
 -         elif [ -d "$file" ]; then
 -             # Strip trailing slash (if there is one)
 -             file=${file%/}
 -             echo "Directory: $file"
 -             mode=$(get_mode "$file")
 -             echo "Mode: $mode"
 -             vecho "$mode $file/"
 -             div
 -             # Find all files and dirs, including hidden/dot files
 -             for x in "$file/"{*,.[^.]*}; do
 -                 _create "$x"
 -             done
 -         elif [ -f "$file" ]; then
 -             echo "Path: $file"
 -             lines=$(wc -l "$file"|awk '{print $1}')
 -             eof_without_newline=0
 -             if [[ "$(wc -c "$file"|awk '{print $1}')" -gt 0 ]] && \
 -                     [[ "$(tail -c 1 "$file" | wc -l)" -eq 0 ]]; then
 -                 eof_without_newline=1
 -                 lines=$((lines+1))
 -             fi
 -             echo "Lines: $lines"
 -             # Add backslash in front of EOF
 -             # Add backslash in front of NULLBYTE
 -             # Replace null byte with NULLBYTE
 -             if [ $USE_PYTHON -eq 1 ]; then
 -                 < "$file" python -c "$PYTHON_CREATE_FILTER"
 -             else
 -                 < "$file" \
 -                     sed 's/EOF/\\EOF/g;
 -                             s/NULLBYTE/\\NULLBYTE/g;
 -                             s/\x0/NULLBYTE/g;
 -                     '
 -             fi
 -             if [[ "$eof_without_newline" -eq 1 ]]; then
 -                 # Finish line with EOF to indicate that the original line did
 -                 # not end with a linefeed
 -                 echo "EOF"
 -             fi
 -             mode=$(get_mode "$file")
 -             echo "Mode: $mode"
 -             vecho "$mode $file"
 -             div
 -         else
 -             echo >&2 "ERROR: file not found ($file in $(pwd))"
 -             exit 2
 -         fi
 -         shift
 -     done
 - }
 - 
 - function create {
 -     ttar_file=$1
 -     shift
 -     if [ -z "${1:-}" ]; then
 -         echo >&2 "ERROR: missing arguments."
 -         echo
 -         usage 1
 -     fi
 -     if [ -e "$ttar_file" ]; then
 -         rm "$ttar_file"
 -     fi
 -     exec > "$ttar_file"
 -     echo "# Archive created by ttar $ARG_STRING"
 -     _create "$@"
 - }
 - 
 - test_environment
 - 
 - if [ -n "${CDIR:-}" ]; then
 -     if [[ "$ARCHIVE" != /* ]]; then
 -         # Relative path: preserve the archive's location before changing
 -         # directory
 -         ARCHIVE="$(pwd)/$ARCHIVE"
 -     fi
 -     cd "$CDIR"
 - fi
 - 
 - "$CMD" "$ARCHIVE" "$@"
 
 
  |