#!/usr/bin/env bash BEG="{<{<{" END="}>}>}" FHEAD="===>>> " SUFFIX=( "" "# Local variables:" "# hide-local-variable-section: t" "# eval:(add-color-pattern" "# \"^\\n\\f\\n$FHEAD\\\\(.*\\\\)\\n$BEG\\n\\\\|^$END\\n\"" "# 'red/red4-scale50 0 t 'yellow/red4-bold 1 t)" "# End:") ############################################################################### # Archive format: # "top message" LF # " file/dir"... LF # For each file (filenames do not contain LFs): # LF FF LF $FHEAD LF $BEG LF # # LF $END LF # Assuming that the values of $BEG, $END, $FHEAD are regexp-safe ############################################################################### failwith() { echo "error: $*" 1>&2; exit 1; } # for weird file names encode() { local str="$1" s str="${str//%/%25}" while [[ "x$str" == "x"*[^[:print:]]* ]]; do s="${str%%[^[:print:]]*}" str="$s$(printf '%%%02x' "'${str:${#s}:1}")${str:${#s}+1}" done echo "$str" } decode() { local str="$1" ret="" s while [[ "x$str" == "x"*"%"??* ]]; do s="${str%%"%"??*}" ret+="$s$(printf '%b_' "\\x${str:${#s}+1:2}")"; ret="${ret:0:-1}" str="${str:${#s}+3}" done echo "$ret$str" } CMD="" MODE="" shopt -s nullglob usage() { echo "usage: $0 [-p/-u/-c] files..." echo " -p: pack files into archive" echo " -c: pack into archive, run command on it, unpack" echo " -u: unpack files from archive" exit } while ((1)); do case "x$1" in ( "x-p" | "x-u" ) if [[ "$MODE" = "" ]]; then MODE="${1:1}"; shift else failwith "multiple mode flags: $1"; fi ;; ( "x-c" ) if [[ "$MODE" = "" ]]; then MODE="p"; CMD="$2"; shift 2 else failwith "multiple command flags: $1 $2"; fi ;; ( "-h" | "--help" ) usage ;; ( "-"* ) failwith "unknown flag: $1" ;; ( * ) break ;; esac done ARCHIVE="$1"; shift ############################################################################### pack() { if [[ -e "$ARCHIVE" ]]; then failwith "archive file exists: $ARCHIVE"; fi exec 3>&1 1> "$ARCHIVE" echo "Files in this archive:" for p in "$@"; do if [[ -d "$p" ]]; then echo " $p/..." elif [[ -w "$p" ]]; then echo " $p" elif [[ -r "$p" ]]; then failwith "read-only file: $p" elif [[ -e "$p" ]]; then failwith "unreadable file: $p" else failwith "file not found: $p" fi done for p in "$@"; do show_file "$p"; done for s in "${SUFFIX[@]}"; do printf '%s\n' "$s"; done exec 1>&3 3>&- if [[ "x$CMD" != "x" ]]; then $CMD "$ARCHIVE"; unpack; fi } shown_files=$'\n' show_file() { if [[ -d "$1" ]]; then for x in "$1/"*; do show_file "$x"; done else local enc="$(encode "$1")" if [[ "$shown_files" = *$'\n'"$enc"$'\n'* ]]; then echo "skipping $1..." 1>&3; return fi echo "packing $1..." 1>&3 shown_files="$shown_files$enc"$'\n' printf '\n\f\n%s%s\n%s\n' "$FHEAD" "$enc" "$BEG" qbad="$(grep -c -e "^$BEG\$" -e "^$END\$" "$1")" if [[ "$qbad" != "0" ]]; then echo "warning: found $qbad occurrences of delimiters in $1, quoting" 1>&2 sed -e "s/^\\\\*\\($BEG\\|$END\\)$/\\\\\\0/g" < "$1" else cat "$1" fi printf '\n%s\n' "$END" fi } ############################################################################### unpack() { if [[ ! -r "$ARCHIVE" ]]; then failwith "archive file missing: $ARCHIVE"; fi bad() { failwith "bad archive, $*"; } skip() { head -c $1 0<&3 > /dev/null; } exec < "$ARCHIVE" 3< "$ARCHIVE" ofs0="$(grep -obam 1 -xe $'\f' "$ARCHIVE")" if [[ "x$ofs0" != "x"*":"* ]]; then bad "no file marker found"; fi ofs0="$((${ofs0%%:*} - 1))" head -c $ofs0 > /dev/null skip $ofs0 while ((1)); do chunk="$(grep -obam 1 -xe $'\f')" if [[ -z "$chunk" ]]; then break; fi if [[ "$chunk" != $'1:\f' ]]; then bad "FF found at a bad offset"; fi chunk="$(grep -obam 1 -xe "$BEG")" if [[ "$chunk" != *":$BEG" ]]; then bad "expecting \"$BEG\" marker"; fi chunk="${chunk%%:*}" # LF FF LF; $FHEAD; $BEG LF skip 3; file="$(head -1 0<&3)"; skip $((${#BEG} + 1)) if [[ "x$file" != "x$FHEAD"* || "${#file}" != "$((chunk-1))" ]]; then bad "malformed file header: $file" fi file="$(decode "${file#"$FHEAD"}")" if [[ ! -w "$file" ]]; then failwith "file not found: $file" elif [[ ! -r "$file" ]]; then failwith "unreadable file: $file" elif [[ ! -w "$file" ]]; then failwith "read-only file: $file" fi # tmp="$(dirname "$file")/.#$$#.$(basename "$file")" # touches directories tmp="/tmp/multifile-temp-$$" echo -n "extracting $file..." chunk="$(grep -obam 1 -xe "$END")" if [[ "$chunk" != *":$END" ]]; then bad "expecting \"$END\" marker"; fi chunk="${chunk%%:*}" head -c $((chunk-1)) 0<&3 > "$tmp" skip $((${#END} + 2)) # LF $END LF if grep -q -e "^\\\\$BEG\$" -e "^\\\\$END\$" "$tmp"; then echo -n " (unquoting)" sed -i -e "s/^\\\\\\(\\\\*\\($BEG\\|$END\\)\\)$/\\1/g" "$tmp" fi if diff -q "$tmp" "$file" > /dev/null; then echo " same" else echo " modified"; cat "$tmp" > "$file"; fi rm -f "$tmp" done rm "$ARCHIVE" } ############################################################################### case "$MODE" in ( "" ) failwith "missing mode specification" ;; ( "p" ) pack "$@" ;; ( "u" ) unpack "$@" ;; esac