ttar 11 KB

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