| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389 | #!/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=Cpath=""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=0PYTHON_CREATE_FILTER=$(cat << 'PCF'#!/usr/bin/env pythonimport reimport sysfor 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 pythonimport reimport sysfor 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 << USAGEUsage:   $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)Example: Change to sysfs directory, create ttar file from fixtures directory         $bname -C sysfs -c -f sysfs/fixtures.ttar fixtures/USAGEexit "$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 VERBOSEwhile 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            ;;        *)            echo >&2 "ERROR: invalid option -$OPTARG"            echo            usage 1            ;;    esacdone# Remove processed options from argumentsshift $(( OPTIND - 1 ));if [ "${CMD:-}" == "" ]; then    echo >&2 "ERROR: no command given"    echo    usage 1elif [ "${ARCHIVE:-}" == "" ]; then    echo >&2 "ERROR: no archive name given"    echo    usage 1fifunction 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 preceeded 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 preceeded by backslash            # Remove one backslash in front of NULLBYTE (if any)            # Remove EOF unless preceeded 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 [ -e "$path" ] || [ -L "$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_environmentif [ -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" "$@"
 |