[822] | 1 | #!/usr/bin/env bash
|
---|
| 2 |
|
---|
| 3 | # Purpose: plain text tar format
|
---|
| 4 | # Limitations: - only suitable for text files, directories, and symlinks
|
---|
| 5 | # - stores only filename, content, and mode
|
---|
| 6 | # - not designed for untrusted input
|
---|
| 7 | #
|
---|
| 8 | # Note: must work with bash version 3.2 (macOS)
|
---|
| 9 |
|
---|
| 10 | # Copyright 2017 Roger Luethi
|
---|
| 11 | #
|
---|
| 12 | # Licensed under the Apache License, Version 2.0 (the "License");
|
---|
| 13 | # you may not use this file except in compliance with the License.
|
---|
| 14 | # You may obtain a copy of the License at
|
---|
| 15 | #
|
---|
| 16 | # http://www.apache.org/licenses/LICENSE-2.0
|
---|
| 17 | #
|
---|
| 18 | # Unless required by applicable law or agreed to in writing, software
|
---|
| 19 | # distributed under the License is distributed on an "AS IS" BASIS,
|
---|
| 20 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
---|
| 21 | # See the License for the specific language governing permissions and
|
---|
| 22 | # limitations under the License.
|
---|
| 23 |
|
---|
| 24 | set -o errexit -o nounset
|
---|
| 25 |
|
---|
| 26 | # Sanitize environment (for instance, standard sorting of glob matches)
|
---|
| 27 | export LC_ALL=C
|
---|
| 28 |
|
---|
| 29 | path=""
|
---|
| 30 | CMD=""
|
---|
| 31 | ARG_STRING="$*"
|
---|
| 32 |
|
---|
| 33 | #------------------------------------------------------------------------------
|
---|
| 34 | # Not all sed implementations can work on null bytes. In order to make ttar
|
---|
| 35 | # work out of the box on macOS, use Python as a stream editor.
|
---|
| 36 |
|
---|
| 37 | USE_PYTHON=0
|
---|
| 38 |
|
---|
| 39 | PYTHON_CREATE_FILTER=$(cat << 'PCF'
|
---|
| 40 | #!/usr/bin/env python
|
---|
| 41 |
|
---|
| 42 | import re
|
---|
| 43 | import sys
|
---|
| 44 |
|
---|
| 45 | for line in sys.stdin:
|
---|
| 46 | line = re.sub(r'EOF', r'\EOF', line)
|
---|
| 47 | line = re.sub(r'NULLBYTE', r'\NULLBYTE', line)
|
---|
| 48 | line = re.sub('\x00', r'NULLBYTE', line)
|
---|
| 49 | sys.stdout.write(line)
|
---|
| 50 | PCF
|
---|
| 51 | )
|
---|
| 52 |
|
---|
| 53 | PYTHON_EXTRACT_FILTER=$(cat << 'PEF'
|
---|
| 54 | #!/usr/bin/env python
|
---|
| 55 |
|
---|
| 56 | import re
|
---|
| 57 | import sys
|
---|
| 58 |
|
---|
| 59 | for line in sys.stdin:
|
---|
| 60 | line = re.sub(r'(?<!\\)NULLBYTE', '\x00', line)
|
---|
| 61 | line = re.sub(r'\\NULLBYTE', 'NULLBYTE', line)
|
---|
| 62 | line = re.sub(r'([^\\])EOF', r'\1', line)
|
---|
| 63 | line = re.sub(r'\\EOF', 'EOF', line)
|
---|
| 64 | sys.stdout.write(line)
|
---|
| 65 | PEF
|
---|
| 66 | )
|
---|
| 67 |
|
---|
| 68 | function test_environment {
|
---|
| 69 | if [[ "$(echo "a" | sed 's/a/\x0/' | wc -c)" -ne 2 ]]; then
|
---|
| 70 | echo "WARNING sed unable to handle null bytes, using Python (slow)."
|
---|
| 71 | if ! which python >/dev/null; then
|
---|
| 72 | echo "ERROR Python not found. Aborting."
|
---|
| 73 | exit 2
|
---|
| 74 | fi
|
---|
| 75 | USE_PYTHON=1
|
---|
| 76 | fi
|
---|
| 77 | }
|
---|
| 78 |
|
---|
| 79 | #------------------------------------------------------------------------------
|
---|
| 80 |
|
---|
| 81 | function usage {
|
---|
| 82 | bname=$(basename "$0")
|
---|
| 83 | cat << USAGE
|
---|
| 84 | Usage: $bname [-C <DIR>] -c -f <ARCHIVE> <FILE...> (create archive)
|
---|
| 85 | $bname -t -f <ARCHIVE> (list archive contents)
|
---|
| 86 | $bname [-C <DIR>] -x -f <ARCHIVE> (extract archive)
|
---|
| 87 |
|
---|
| 88 | Options:
|
---|
| 89 | -C <DIR> (change directory)
|
---|
| 90 | -v (verbose)
|
---|
| 91 | --recursive-unlink (recursively delete existing directory if path
|
---|
| 92 | collides with file or directory to extract)
|
---|
| 93 |
|
---|
| 94 | Example: Change to sysfs directory, create ttar file from fixtures directory
|
---|
| 95 | $bname -C sysfs -c -f sysfs/fixtures.ttar fixtures/
|
---|
| 96 | USAGE
|
---|
| 97 | exit "$1"
|
---|
| 98 | }
|
---|
| 99 |
|
---|
| 100 | function vecho {
|
---|
| 101 | if [ "${VERBOSE:-}" == "yes" ]; then
|
---|
| 102 | echo >&7 "$@"
|
---|
| 103 | fi
|
---|
| 104 | }
|
---|
| 105 |
|
---|
| 106 | function set_cmd {
|
---|
| 107 | if [ -n "$CMD" ]; then
|
---|
| 108 | echo "ERROR: more than one command given"
|
---|
| 109 | echo
|
---|
| 110 | usage 2
|
---|
| 111 | fi
|
---|
| 112 | CMD=$1
|
---|
| 113 | }
|
---|
| 114 |
|
---|
| 115 | unset VERBOSE
|
---|
| 116 | unset RECURSIVE_UNLINK
|
---|
| 117 |
|
---|
| 118 | while getopts :cf:-:htxvC: opt; do
|
---|
| 119 | case $opt in
|
---|
| 120 | c)
|
---|
| 121 | set_cmd "create"
|
---|
| 122 | ;;
|
---|
| 123 | f)
|
---|
| 124 | ARCHIVE=$OPTARG
|
---|
| 125 | ;;
|
---|
| 126 | h)
|
---|
| 127 | usage 0
|
---|
| 128 | ;;
|
---|
| 129 | t)
|
---|
| 130 | set_cmd "list"
|
---|
| 131 | ;;
|
---|
| 132 | x)
|
---|
| 133 | set_cmd "extract"
|
---|
| 134 | ;;
|
---|
| 135 | v)
|
---|
| 136 | VERBOSE=yes
|
---|
| 137 | exec 7>&1
|
---|
| 138 | ;;
|
---|
| 139 | C)
|
---|
| 140 | CDIR=$OPTARG
|
---|
| 141 | ;;
|
---|
| 142 | -)
|
---|
| 143 | case $OPTARG in
|
---|
| 144 | recursive-unlink)
|
---|
| 145 | RECURSIVE_UNLINK="yes"
|
---|
| 146 | ;;
|
---|
| 147 | *)
|
---|
| 148 | echo -e "Error: invalid option -$OPTARG"
|
---|
| 149 | echo
|
---|
| 150 | usage 1
|
---|
| 151 | ;;
|
---|
| 152 | esac
|
---|
| 153 | ;;
|
---|
| 154 | *)
|
---|
| 155 | echo >&2 "ERROR: invalid option -$OPTARG"
|
---|
| 156 | echo
|
---|
| 157 | usage 1
|
---|
| 158 | ;;
|
---|
| 159 | esac
|
---|
| 160 | done
|
---|
| 161 |
|
---|
| 162 | # Remove processed options from arguments
|
---|
| 163 | shift $(( OPTIND - 1 ));
|
---|
| 164 |
|
---|
| 165 | if [ "${CMD:-}" == "" ]; then
|
---|
| 166 | echo >&2 "ERROR: no command given"
|
---|
| 167 | echo
|
---|
| 168 | usage 1
|
---|
| 169 | elif [ "${ARCHIVE:-}" == "" ]; then
|
---|
| 170 | echo >&2 "ERROR: no archive name given"
|
---|
| 171 | echo
|
---|
| 172 | usage 1
|
---|
| 173 | fi
|
---|
| 174 |
|
---|
| 175 | function list {
|
---|
| 176 | local path=""
|
---|
| 177 | local size=0
|
---|
| 178 | local line_no=0
|
---|
| 179 | local ttar_file=$1
|
---|
| 180 | if [ -n "${2:-}" ]; then
|
---|
| 181 | echo >&2 "ERROR: too many arguments."
|
---|
| 182 | echo
|
---|
| 183 | usage 1
|
---|
| 184 | fi
|
---|
| 185 | if [ ! -e "$ttar_file" ]; then
|
---|
| 186 | echo >&2 "ERROR: file not found ($ttar_file)"
|
---|
| 187 | echo
|
---|
| 188 | usage 1
|
---|
| 189 | fi
|
---|
| 190 | while read -r line; do
|
---|
| 191 | line_no=$(( line_no + 1 ))
|
---|
| 192 | if [ $size -gt 0 ]; then
|
---|
| 193 | size=$(( size - 1 ))
|
---|
| 194 | continue
|
---|
| 195 | fi
|
---|
| 196 | if [[ $line =~ ^Path:\ (.*)$ ]]; then
|
---|
| 197 | path=${BASH_REMATCH[1]}
|
---|
| 198 | elif [[ $line =~ ^Lines:\ (.*)$ ]]; then
|
---|
| 199 | size=${BASH_REMATCH[1]}
|
---|
| 200 | echo "$path"
|
---|
| 201 | elif [[ $line =~ ^Directory:\ (.*)$ ]]; then
|
---|
| 202 | path=${BASH_REMATCH[1]}
|
---|
| 203 | echo "$path/"
|
---|
| 204 | elif [[ $line =~ ^SymlinkTo:\ (.*)$ ]]; then
|
---|
| 205 | echo "$path -> ${BASH_REMATCH[1]}"
|
---|
| 206 | fi
|
---|
| 207 | done < "$ttar_file"
|
---|
| 208 | }
|
---|
| 209 |
|
---|
| 210 | function extract {
|
---|
| 211 | local path=""
|
---|
| 212 | local size=0
|
---|
| 213 | local line_no=0
|
---|
| 214 | local ttar_file=$1
|
---|
| 215 | if [ -n "${2:-}" ]; then
|
---|
| 216 | echo >&2 "ERROR: too many arguments."
|
---|
| 217 | echo
|
---|
| 218 | usage 1
|
---|
| 219 | fi
|
---|
| 220 | if [ ! -e "$ttar_file" ]; then
|
---|
| 221 | echo >&2 "ERROR: file not found ($ttar_file)"
|
---|
| 222 | echo
|
---|
| 223 | usage 1
|
---|
| 224 | fi
|
---|
| 225 | while IFS= read -r line; do
|
---|
| 226 | line_no=$(( line_no + 1 ))
|
---|
| 227 | local eof_without_newline
|
---|
| 228 | if [ "$size" -gt 0 ]; then
|
---|
| 229 | if [[ "$line" =~ [^\\]EOF ]]; then
|
---|
| 230 | # An EOF not preceded by a backslash indicates that the line
|
---|
| 231 | # does not end with a newline
|
---|
| 232 | eof_without_newline=1
|
---|
| 233 | else
|
---|
| 234 | eof_without_newline=0
|
---|
| 235 | fi
|
---|
| 236 | # Replace NULLBYTE with null byte if at beginning of line
|
---|
| 237 | # Replace NULLBYTE with null byte unless preceded by backslash
|
---|
| 238 | # Remove one backslash in front of NULLBYTE (if any)
|
---|
| 239 | # Remove EOF unless preceded by backslash
|
---|
| 240 | # Remove one backslash in front of EOF
|
---|
| 241 | if [ $USE_PYTHON -eq 1 ]; then
|
---|
| 242 | echo -n "$line" | python -c "$PYTHON_EXTRACT_FILTER" >> "$path"
|
---|
| 243 | else
|
---|
| 244 | # The repeated pattern makes up for sed's lack of negative
|
---|
| 245 | # lookbehind assertions (for consecutive null bytes).
|
---|
| 246 | echo -n "$line" | \
|
---|
| 247 | sed -e 's/^NULLBYTE/\x0/g;
|
---|
| 248 | s/\([^\\]\)NULLBYTE/\1\x0/g;
|
---|
| 249 | s/\([^\\]\)NULLBYTE/\1\x0/g;
|
---|
| 250 | s/\\NULLBYTE/NULLBYTE/g;
|
---|
| 251 | s/\([^\\]\)EOF/\1/g;
|
---|
| 252 | s/\\EOF/EOF/g;
|
---|
| 253 | ' >> "$path"
|
---|
| 254 | fi
|
---|
| 255 | if [[ "$eof_without_newline" -eq 0 ]]; then
|
---|
| 256 | echo >> "$path"
|
---|
| 257 | fi
|
---|
| 258 | size=$(( size - 1 ))
|
---|
| 259 | continue
|
---|
| 260 | fi
|
---|
| 261 | if [[ $line =~ ^Path:\ (.*)$ ]]; then
|
---|
| 262 | path=${BASH_REMATCH[1]}
|
---|
| 263 | if [ -L "$path" ]; then
|
---|
| 264 | rm "$path"
|
---|
| 265 | elif [ -d "$path" ]; then
|
---|
| 266 | if [ "${RECURSIVE_UNLINK:-}" == "yes" ]; then
|
---|
| 267 | rm -r "$path"
|
---|
| 268 | else
|
---|
| 269 | # Safe because symlinks to directories are dealt with above
|
---|
| 270 | rmdir "$path"
|
---|
| 271 | fi
|
---|
| 272 | elif [ -e "$path" ]; then
|
---|
| 273 | rm "$path"
|
---|
| 274 | fi
|
---|
| 275 | elif [[ $line =~ ^Lines:\ (.*)$ ]]; then
|
---|
| 276 | size=${BASH_REMATCH[1]}
|
---|
| 277 | # Create file even if it is zero-length.
|
---|
| 278 | touch "$path"
|
---|
| 279 | vecho " $path"
|
---|
| 280 | elif [[ $line =~ ^Mode:\ (.*)$ ]]; then
|
---|
| 281 | mode=${BASH_REMATCH[1]}
|
---|
| 282 | chmod "$mode" "$path"
|
---|
| 283 | vecho "$mode"
|
---|
| 284 | elif [[ $line =~ ^Directory:\ (.*)$ ]]; then
|
---|
| 285 | path=${BASH_REMATCH[1]}
|
---|
| 286 | mkdir -p "$path"
|
---|
| 287 | vecho " $path/"
|
---|
| 288 | elif [[ $line =~ ^SymlinkTo:\ (.*)$ ]]; then
|
---|
| 289 | ln -s "${BASH_REMATCH[1]}" "$path"
|
---|
| 290 | vecho " $path -> ${BASH_REMATCH[1]}"
|
---|
| 291 | elif [[ $line =~ ^# ]]; then
|
---|
| 292 | # Ignore comments between files
|
---|
| 293 | continue
|
---|
| 294 | else
|
---|
| 295 | echo >&2 "ERROR: Unknown keyword on line $line_no: $line"
|
---|
| 296 | exit 1
|
---|
| 297 | fi
|
---|
| 298 | done < "$ttar_file"
|
---|
| 299 | }
|
---|
| 300 |
|
---|
| 301 | function div {
|
---|
| 302 | echo "# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -" \
|
---|
| 303 | "- - - - - -"
|
---|
| 304 | }
|
---|
| 305 |
|
---|
| 306 | function get_mode {
|
---|
| 307 | local mfile=$1
|
---|
| 308 | if [ -z "${STAT_OPTION:-}" ]; then
|
---|
| 309 | if stat -c '%a' "$mfile" >/dev/null 2>&1; then
|
---|
| 310 | # GNU stat
|
---|
| 311 | STAT_OPTION='-c'
|
---|
| 312 | STAT_FORMAT='%a'
|
---|
| 313 | else
|
---|
| 314 | # BSD stat
|
---|
| 315 | STAT_OPTION='-f'
|
---|
| 316 | # Octal output, user/group/other (omit file type, sticky bit)
|
---|
| 317 | STAT_FORMAT='%OLp'
|
---|
| 318 | fi
|
---|
| 319 | fi
|
---|
| 320 | stat "${STAT_OPTION}" "${STAT_FORMAT}" "$mfile"
|
---|
| 321 | }
|
---|
| 322 |
|
---|
| 323 | function _create {
|
---|
| 324 | shopt -s nullglob
|
---|
| 325 | local mode
|
---|
| 326 | local eof_without_newline
|
---|
| 327 | while (( "$#" )); do
|
---|
| 328 | file=$1
|
---|
| 329 | if [ -L "$file" ]; then
|
---|
| 330 | echo "Path: $file"
|
---|
| 331 | symlinkTo=$(readlink "$file")
|
---|
| 332 | echo "SymlinkTo: $symlinkTo"
|
---|
| 333 | vecho " $file -> $symlinkTo"
|
---|
| 334 | div
|
---|
| 335 | elif [ -d "$file" ]; then
|
---|
| 336 | # Strip trailing slash (if there is one)
|
---|
| 337 | file=${file%/}
|
---|
| 338 | echo "Directory: $file"
|
---|
| 339 | mode=$(get_mode "$file")
|
---|
| 340 | echo "Mode: $mode"
|
---|
| 341 | vecho "$mode $file/"
|
---|
| 342 | div
|
---|
| 343 | # Find all files and dirs, including hidden/dot files
|
---|
| 344 | for x in "$file/"{*,.[^.]*}; do
|
---|
| 345 | _create "$x"
|
---|
| 346 | done
|
---|
| 347 | elif [ -f "$file" ]; then
|
---|
| 348 | echo "Path: $file"
|
---|
| 349 | lines=$(wc -l "$file"|awk '{print $1}')
|
---|
| 350 | eof_without_newline=0
|
---|
| 351 | if [[ "$(wc -c "$file"|awk '{print $1}')" -gt 0 ]] && \
|
---|
| 352 | [[ "$(tail -c 1 "$file" | wc -l)" -eq 0 ]]; then
|
---|
| 353 | eof_without_newline=1
|
---|
| 354 | lines=$((lines+1))
|
---|
| 355 | fi
|
---|
| 356 | echo "Lines: $lines"
|
---|
| 357 | # Add backslash in front of EOF
|
---|
| 358 | # Add backslash in front of NULLBYTE
|
---|
| 359 | # Replace null byte with NULLBYTE
|
---|
| 360 | if [ $USE_PYTHON -eq 1 ]; then
|
---|
| 361 | < "$file" python -c "$PYTHON_CREATE_FILTER"
|
---|
| 362 | else
|
---|
| 363 | < "$file" \
|
---|
| 364 | sed 's/EOF/\\EOF/g;
|
---|
| 365 | s/NULLBYTE/\\NULLBYTE/g;
|
---|
| 366 | s/\x0/NULLBYTE/g;
|
---|
| 367 | '
|
---|
| 368 | fi
|
---|
| 369 | if [[ "$eof_without_newline" -eq 1 ]]; then
|
---|
| 370 | # Finish line with EOF to indicate that the original line did
|
---|
| 371 | # not end with a linefeed
|
---|
| 372 | echo "EOF"
|
---|
| 373 | fi
|
---|
| 374 | mode=$(get_mode "$file")
|
---|
| 375 | echo "Mode: $mode"
|
---|
| 376 | vecho "$mode $file"
|
---|
| 377 | div
|
---|
| 378 | else
|
---|
| 379 | echo >&2 "ERROR: file not found ($file in $(pwd))"
|
---|
| 380 | exit 2
|
---|
| 381 | fi
|
---|
| 382 | shift
|
---|
| 383 | done
|
---|
| 384 | }
|
---|
| 385 |
|
---|
| 386 | function create {
|
---|
| 387 | ttar_file=$1
|
---|
| 388 | shift
|
---|
| 389 | if [ -z "${1:-}" ]; then
|
---|
| 390 | echo >&2 "ERROR: missing arguments."
|
---|
| 391 | echo
|
---|
| 392 | usage 1
|
---|
| 393 | fi
|
---|
| 394 | if [ -e "$ttar_file" ]; then
|
---|
| 395 | rm "$ttar_file"
|
---|
| 396 | fi
|
---|
| 397 | exec > "$ttar_file"
|
---|
| 398 | echo "# Archive created by ttar $ARG_STRING"
|
---|
| 399 | _create "$@"
|
---|
| 400 | }
|
---|
| 401 |
|
---|
| 402 | test_environment
|
---|
| 403 |
|
---|
| 404 | if [ -n "${CDIR:-}" ]; then
|
---|
| 405 | if [[ "$ARCHIVE" != /* ]]; then
|
---|
| 406 | # Relative path: preserve the archive's location before changing
|
---|
| 407 | # directory
|
---|
| 408 | ARCHIVE="$(pwd)/$ARCHIVE"
|
---|
| 409 | fi
|
---|
| 410 | cd "$CDIR"
|
---|
| 411 | fi
|
---|
| 412 |
|
---|
| 413 | "$CMD" "$ARCHIVE" "$@"
|
---|