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
8 # Note: must work with bash version 3.2 (macOS)
10 # Copyright 2017 Roger Luethi
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
16 # http://www.apache.org/licenses/LICENSE-2.0
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.
24 set -o errexit -o nounset
26 # Sanitize environment (for instance, standard sorting of glob matches)
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.
39 PYTHON_CREATE_FILTER=$(cat << 'PCF'
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)
53 PYTHON_EXTRACT_FILTER=$(cat << 'PEF'
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)
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."
79 #------------------------------------------------------------------------------
82 bname=$(basename "$0")
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)
89 -C <DIR> (change directory)
92 Example: Change to sysfs directory, create ttar file from fixtures directory
93 $bname -C sysfs -c -f sysfs/fixtures.ttar fixtures/
99 if [ "${VERBOSE:-}" == "yes" ]; then
105 if [ -n "$CMD" ]; then
106 echo "ERROR: more than one command given"
115 while getopts :cf:htxvC: opt; do
140 echo >&2 "ERROR: invalid option -$OPTARG"
147 # Remove processed options from arguments
148 shift $(( OPTIND - 1 ));
150 if [ "${CMD:-}" == "" ]; then
151 echo >&2 "ERROR: no command given"
154 elif [ "${ARCHIVE:-}" == "" ]; then
155 echo >&2 "ERROR: no archive name given"
165 if [ -n "${2:-}" ]; then
166 echo >&2 "ERROR: too many arguments."
170 if [ ! -e "$ttar_file" ]; then
171 echo >&2 "ERROR: file not found ($ttar_file)"
175 while read -r line; do
176 line_no=$(( line_no + 1 ))
177 if [ $size -gt 0 ]; then
181 if [[ $line =~ ^Path:\ (.*)$ ]]; then
182 path=${BASH_REMATCH[1]}
183 elif [[ $line =~ ^Lines:\ (.*)$ ]]; then
184 size=${BASH_REMATCH[1]}
186 elif [[ $line =~ ^Directory:\ (.*)$ ]]; then
187 path=${BASH_REMATCH[1]}
189 elif [[ $line =~ ^SymlinkTo:\ (.*)$ ]]; then
190 echo "$path -> ${BASH_REMATCH[1]}"
200 if [ -n "${2:-}" ]; then
201 echo >&2 "ERROR: too many arguments."
205 if [ ! -e "$ttar_file" ]; then
206 echo >&2 "ERROR: file not found ($ttar_file)"
210 while IFS= read -r line; do
211 line_no=$(( line_no + 1 ))
212 local eof_without_newline
213 if [ "$size" -gt 0 ]; then
214 if [[ "$line" =~ [^\\]EOF ]]; then
215 # An EOF not preceeded by a backslash indicates that the line
216 # does not end with a newline
217 eof_without_newline=1
219 eof_without_newline=0
221 # Replace NULLBYTE with null byte if at beginning of line
222 # Replace NULLBYTE with null byte unless preceeded by backslash
223 # Remove one backslash in front of NULLBYTE (if any)
224 # Remove EOF unless preceeded by backslash
225 # Remove one backslash in front of EOF
226 if [ $USE_PYTHON -eq 1 ]; then
227 echo -n "$line" | python -c "$PYTHON_EXTRACT_FILTER" >> "$path"
229 # The repeated pattern makes up for sed's lack of negative
230 # lookbehind assertions (for consecutive null bytes).
232 sed -e 's/^NULLBYTE/\x0/g;
233 s/\([^\\]\)NULLBYTE/\1\x0/g;
234 s/\([^\\]\)NULLBYTE/\1\x0/g;
235 s/\\NULLBYTE/NULLBYTE/g;
240 if [[ "$eof_without_newline" -eq 0 ]]; then
246 if [[ $line =~ ^Path:\ (.*)$ ]]; then
247 path=${BASH_REMATCH[1]}
248 if [ -e "$path" ] || [ -L "$path" ]; then
251 elif [[ $line =~ ^Lines:\ (.*)$ ]]; then
252 size=${BASH_REMATCH[1]}
253 # Create file even if it is zero-length.
256 elif [[ $line =~ ^Mode:\ (.*)$ ]]; then
257 mode=${BASH_REMATCH[1]}
258 chmod "$mode" "$path"
260 elif [[ $line =~ ^Directory:\ (.*)$ ]]; then
261 path=${BASH_REMATCH[1]}
264 elif [[ $line =~ ^SymlinkTo:\ (.*)$ ]]; then
265 ln -s "${BASH_REMATCH[1]}" "$path"
266 vecho " $path -> ${BASH_REMATCH[1]}"
267 elif [[ $line =~ ^# ]]; then
268 # Ignore comments between files
271 echo >&2 "ERROR: Unknown keyword on line $line_no: $line"
278 echo "# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -" \
284 if [ -z "${STAT_OPTION:-}" ]; then
285 if stat -c '%a' "$mfile" >/dev/null 2>&1; then
292 # Octal output, user/group/other (omit file type, sticky bit)
296 stat "${STAT_OPTION}" "${STAT_FORMAT}" "$mfile"
302 local eof_without_newline
305 if [ -L "$file" ]; then
307 symlinkTo=$(readlink "$file")
308 echo "SymlinkTo: $symlinkTo"
309 vecho " $file -> $symlinkTo"
311 elif [ -d "$file" ]; then
312 # Strip trailing slash (if there is one)
314 echo "Directory: $file"
315 mode=$(get_mode "$file")
319 # Find all files and dirs, including hidden/dot files
320 for x in "$file/"{*,.[^.]*}; do
323 elif [ -f "$file" ]; then
325 lines=$(wc -l "$file"|awk '{print $1}')
326 eof_without_newline=0
327 if [[ "$(wc -c "$file"|awk '{print $1}')" -gt 0 ]] && \
328 [[ "$(tail -c 1 "$file" | wc -l)" -eq 0 ]]; then
329 eof_without_newline=1
333 # Add backslash in front of EOF
334 # Add backslash in front of NULLBYTE
335 # Replace null byte with NULLBYTE
336 if [ $USE_PYTHON -eq 1 ]; then
337 < "$file" python -c "$PYTHON_CREATE_FILTER"
341 s/NULLBYTE/\\NULLBYTE/g;
345 if [[ "$eof_without_newline" -eq 1 ]]; then
346 # Finish line with EOF to indicate that the original line did
347 # not end with a linefeed
350 mode=$(get_mode "$file")
355 echo >&2 "ERROR: file not found ($file in $(pwd))"
365 if [ -z "${1:-}" ]; then
366 echo >&2 "ERROR: missing arguments."
370 if [ -e "$ttar_file" ]; then
374 echo "# Archive created by ttar $ARG_STRING"
380 if [ -n "${CDIR:-}" ]; then
381 if [[ "$ARCHIVE" != /* ]]; then
382 # Relative path: preserve the archive's location before changing
384 ARCHIVE="$(pwd)/$ARCHIVE"
389 "$CMD" "$ARCHIVE" "$@"