· 4 years ago · Jul 04, 2021, 09:06 AM
1-----------------------------------------------------
2--name : bin/tar.lua
3--description: creating, viewing and extracting tar archives on disk and tape
4--author : mpmxyz
5--github page: https://github.com/mpmxyz/ocprograms
6--forum page : http://oc.cil.li/index.php?/topic/421-tar-for-opencomputers/
7-----------------------------------------------------
8--[[
9 tar archiver for OpenComputers
10 for further information check the usage text or man page
11
12 TODO: support non primary tape drives
13 TODO: detect symbolic link cycles (-> remember already visited, resolved paths)
14]]
15local shell = require 'shell'
16local fs = require 'filesystem'
17local component = require 'component'
18
19local BLOCK_SIZE = 512
20local NULL_BLOCK = ("\0"):rep(BLOCK_SIZE)
21local WORKING_DIRECTORY = fs.canonical(shell.getWorkingDirectory()):gsub("/$","")
22
23--load auto_progress library if possible
24local auto_progress
25if true then
26 local ok
27 ok, auto_progress = pcall(require, "mpm.auto_progress")
28 if not ok then
29 auto_progress = {}
30 function auto_progress.new()
31 --library not available, create stub
32 return {
33 update = function() end,
34 finish = function() end,
35 }
36 end
37 end
38end
39
40--error information
41local USAGE_TEXT = [[
42Usage:
43tar <function letter> [other options] FILES...
44function letter description
45-c --create creates a new archive
46-r --append appends to existing archive
47-t --list lists contents from archive
48-x --extract --get extracts from archive
49other options description
50-f --file FILE first FILE is archive, else:
51 uses primary tape drive
52-h --dereference follows symlinks
53--exclude=FILE;... excludes FILE from archive
54-v --verbose lists processed files, also
55 shows progress for large files
56]]
57local function addUsage(text)
58 return text .. "\n" .. USAGE_TEXT
59end
60local ERRORS = {
61 missingAction = addUsage("Error: Missing function letter!"),
62 multipleActions = addUsage("Error: Multiple function letters!"),
63 missingFiles = addUsage("Error: Missing file names!"),
64 invalidChecksum = "Error: Invalid checksum!",
65 noHeaderName = "Error: No file name in header!",
66 invalidTarget = "Error: Invalid target!",
67}
68
69
70--formats numbers and stringvalues to comply to the tar format
71local function formatValue(text, length, maxDigits)
72 if type(text) == "number" then
73 maxDigits = maxDigits or (length - 1) --that is default
74 text = ("%0"..maxDigits.."o"):format(text):sub(-maxDigits, -1)
75 elseif text == nil then
76 text = ""
77 end
78 return (text .. ("\0"):rep(length - #text)):sub(-length, -1)
79end
80
81--a utility to make accessing the header easier
82--Only one header is accessed at a time: no need to throw tables around.
83local header = {}
84--loads a header, uses table.concat on tables, strings are taken directly
85function header:init(block)
86 if type(block) == "table" then
87 --combine tokens to form one 512 byte long header
88 block = table.concat(block, "")
89 elseif block == nil then
90 --make this header a null header
91 block = NULL_BLOCK
92 end
93 if #block < BLOCK_SIZE then
94 --add "\0"s to reach 512 bytes
95 block = block .. ("\0"):rep(BLOCK_SIZE - #block)
96 end
97 --remember the current block
98 self.block = block
99end
100--takes the given data and creates a header from it
101--the resulting block can be retrieved via header:getBytes()
102function header:assemble(data)
103 if #data.name > 100 then
104 local longName = data.name
105 --split at slash
106 local minPrefixLength = #longName - 101 --(100x suffix + 1x slash)
107 local splittingSlashIndex = longName:find("/", minPrefixLength + 1, true)
108 if splittingSlashIndex then
109 --can split path in 2 parts separated by a slash
110 data.filePrefix = longName:sub(1, splittingSlashIndex - 1)
111 data.name = longName:sub(splittingSlashIndex + 1, -1)
112 else
113 --unable to split path; try to put path to the file prefix
114 data.filePrefix = longName
115 data.name = ""
116 end
117 --checking for maximum file prefix length
118 assert(#data.filePrefix <= 155, "File name '"..longName.."' is too long; unable to apply ustar splitting!")
119 --force ustar format
120 data.ustarIndicator = "ustar"
121 data.ustarVersion = "00"
122 end
123 local tokens = {
124 formatValue(data.name, 100), --1
125 formatValue(data.mode, 8), --2
126 formatValue(data.owner, 8), --3
127 formatValue(data.group, 8), --4
128 formatValue(data.size, 12), --5
129 formatValue(data.lastModified, 12), --6
130 " ",--8 spaces --7
131 formatValue(data.typeFlag, 1), --8
132 formatValue(data.linkName, 100), --9
133 }
134 --ustar extension?
135 if data.ustarIndicator then
136 table.insert(tokens, formatValue(data.ustarindicator, 6))
137 table.insert(tokens, formatValue(data.ustarversion, 2))
138 table.insert(tokens, formatValue(data.ownerUser, 32))
139 table.insert(tokens, formatValue(data.ownerGroup, 32))
140 table.insert(tokens, formatValue(data.deviceMajor, 8))
141 table.insert(tokens, formatValue(data.deviceMinor, 8))
142 table.insert(tokens, formatValue(data.filePrefix, 155))
143 end
144 --temporarily assemble header for checksum calculation
145 header:init(tokens)
146 --calculating checksum
147 tokens[7] = ("%06o\0\0"):format(header:checksum(0, BLOCK_SIZE))
148 --assemble final header
149 header:init(tokens)
150end
151--extracts the information from the given header
152function header:read()
153 local data = {}
154 data.name = self:extract (0 , 100)
155 data.mode = self:extract (100, 8)
156 data.owner = self:extractNumber(108, 8)
157 data.group = self:extractNumber(116, 8)
158 data.size = self:extractNumber(124, 12)
159 data.lastModified = self:extractNumber(136, 12)
160 data.checksum = self:extractNumber(148, 8)
161 data.typeFlag = self:extract (156, 1) or "0"
162 data.linkName = self:extract (157, 100)
163 data.ustarIndicator = self:extract (257, 6)
164
165 --There is an old format using "ustar \0" instead of "ustar\0".."00"?
166 if data.ustarIndicator and data.ustarIndicator:sub(1,5) == "ustar" then
167 data.ustarVersion = self:extractNumber(263, 2)
168 data.ownerUser = self:extract (265, 32)
169 data.ownerGroup = self:extract (297, 32)
170 data.deviceMajor = self:extractNumber(329, 8)
171 data.deviceMinor = self:extractNumber(337, 8)
172 data.filePrefix = self:extract (345, 155)
173 end
174
175 assert(self:verify(data.checksum), ERRORS.invalidChecksum)
176 --assemble raw file name, normally relative to working dir
177 if data.filePrefix then
178 data.name = data.filePrefix .. "/" .. data.name
179 data.filePrefix = nil
180 end
181 assert(data.name, ERRORS.noHeaderName)
182 return data
183end
184--returns the whole 512 bytes of the header
185function header:getBytes()
186 return header.block
187end
188--returns if the header is a null header
189function header:isNull()
190 return self.block == NULL_BLOCK
191end
192--extracts a 0 terminated string from the given area
193function header:extract(offset, size)
194 --extract size bytes from the given offset, strips every NULL character
195 --returns a string
196 return self.block:sub(1 + offset, size + offset):match("[^\0]+")
197end
198--extracts an octal number from the given area
199function header:extractNumber(offset, size)
200 --extract size bytes from the given offset
201 --returns the first series of octal digits converted to a number
202 return tonumber(self.block:sub(1 + offset, size + offset):match("[0-7]+") or "", 8)
203end
204--calculates the checksum for the given area
205function header:checksum(offset, size, signed)
206 --calculates the checksum of a given range
207 local sum = 0
208 --summarize byte for byte
209 for index = 1 + offset, size + offset do
210 if signed then
211 --interpretation of a signed byte: compatibility for bugged implementations
212 sum = sum + (self.block:byte(index) + 128) % 256 - 128
213 else
214 sum = sum + self.block:byte(index)
215 end
216 end
217 --modulo to take care of negative sums
218 --The whole reason for the signed addition is that some implementations
219 --used signed bytes instead of unsigned ones and therefore computed 'wrong' checksums.
220 return sum % 0x40000
221end
222--checks if the given checksum is valid for the loaded header
223function header:verify(checksum)
224 local checkedSums = {
225 [self:checksum(0, 148, false) + 256 + self:checksum(156, 356, false)] = true,
226 [self:checksum(0, 148, true ) + 256 + self:checksum(156, 356, true )] = true,
227 }
228 return checkedSums[checksum] or false
229end
230
231
232local function makeRelative(path, reference)
233 --The path and the reference directory must have a common reference. (e.g. root)
234 --The default reference is the current working directory.
235 reference = reference or WORKING_DIRECTORY
236 --1st: split paths into segments
237 local returnDirectory = path:sub(-1,-1) == "/" --?
238 path = fs.segments(path)
239 reference = fs.segments(reference)
240 --2nd: remove common directories
241 while path[1] and reference[1] and path[1] == reference[1] do
242 table.remove(path, 1)
243 table.remove(reference, 1)
244 end
245 --3rd: add ".."s to leave that what's left of the working directory
246 local path = ("../"):rep(#reference) .. table.concat(path, "/")
247 --4th: If there is nothing remaining, we are at the current directory.
248 if path == "" then
249 path = "."
250 end
251 return path
252end
253
254
255local function tarFiles(files, options, mode, ignoredObjects, isDirectoryContent)
256 --combines files[2], files[3], ... into files[1]
257 --prepare output stream
258 local target, closeAtExit
259 if type(files[1]) == "string" then
260 --mode = append -> overwrite trailing NULL headers
261 local targetFile = shell.resolve(files[1]):gsub("/$","")
262 ignoredObjects[targetFile] = ignoredObjects[targetFile] or true
263 target = assert(io.open(targetFile, mode))
264 closeAtExit = true
265 else
266 target = files[1]
267 closeAtExit = false
268 assert(target.write, ERRORS.invalidTarget)
269 end
270 if mode == "rb+" then --append: not working with files because io.read does not support mode "rb+"
271 --start from beginning of file
272 assert(target:seek("set", 0))
273 --loop over every block
274 --This loop implies that it is okay if there is nothing (!) after the last file block.
275 --It also ensures that trailing null blocks are overwritten.
276 for block in target:lines(BLOCK_SIZE) do
277 if #block < BLOCK_SIZE then
278 --reached end of file before block was finished
279 error("Missing "..(BLOCK_SIZE-#block).." bytes to finish block.")
280 end
281 --load header
282 header:init(block)
283 if header:isNull() then
284 --go back to the beginning of the block
285 assert(target:seek("cur", -BLOCK_SIZE))
286 --found null header -> finished with skipping
287 break
288 end
289 --extract size information from header
290 local data = header.read(header)
291 if data.size and data.size > 0 then
292 --skip file content
293 local skippedBytes = math.ceil(data.size / BLOCK_SIZE) * BLOCK_SIZE
294 assert(target:seek("cur", skippedBytes))
295 end
296 end
297 if options.verbose then
298 print("End of archive detected; appending...")
299 end
300 end
301 for i = 2, #files do
302 --prepare data
303 --remove trailing slashes that might come from fs.list
304 local file = shell.resolve(files[i]):gsub("/$","")
305 --determine object type, that determines how the object is handled
306 local isALink, linkTarget = fs.isLink(file)
307 local objectType
308 if isALink and not options.dereference then
309 objectType = "link" --It's a symbolic link.
310 else
311 if fs.isDirectory(file) then
312 objectType = "dir" --It's a directory.
313 else
314 objectType = "file" --It's a normal file.
315 end
316 end
317 --add directory contents before the directory
318 --(It makes sense if you consider that you could change the directories file permissions to be read only.)
319 if objectType == "dir" and ignoredObjects[file] ~= "strict" then
320 local list = {target}
321 local i = 2
322 for containedFile in fs.list(file) do
323 list[i] = fs.concat(file, containedFile)
324 i = i + 1
325 end
326 tarFiles(list, options, nil, ignoredObjects, true)
327 end
328 --Ignored objects are not added to the tar.
329 if not ignoredObjects[file] then
330 local data = {}
331 --get relative path to current directory
332 data.name = makeRelative(file)
333 --add object specific data
334 if objectType == "link" then
335 --It's a symbolic link.
336 data.typeFlag = "2"
337 data.linkName = makeRelative(linkTarget, fs.path(file)):gsub("/$","") --force relative links
338 else
339 data.lastModified = math.floor(fs.lastModified(file) / 1000) --Java returns milliseconds...
340 if objectType == "dir" then
341 --It's a directory.
342 data.typeFlag = "5"
343 data.mode = 448 --> 700 in octal -> rwx------
344 elseif objectType == "file" then
345 --It's a normal file.
346 data.typeFlag = "0"
347 data.size = fs.size(file)
348 data.mode = 384 --> 600 in octal -> rw-------
349 end
350 end
351
352 --tell user what is going on
353 if options.verbose then
354 print("Adding:", data.name)
355 end
356 --assemble header
357 header:assemble(data)
358 --write header
359 assert(target:write(header:getBytes()))
360 --copy file contents
361 if objectType == "file" then
362 --open source file
363 local source = assert(io.open(file, "rb"))
364 --keep track of what has to be copied
365 local bytesToCopy = data.size
366 --init progress bar
367 local progressBar = auto_progress.new(bytesToCopy)
368 --copy file contents
369 for block in source:lines(BLOCK_SIZE) do
370 assert(target:write(block))
371 bytesToCopy = bytesToCopy - #block
372 assert(bytesToCopy >= 0, "Error: File grew while copying! Is it the output file?")
373 if options.verbose then
374 --update progress bar
375 progressBar.update(#block)
376 end
377 if #block < BLOCK_SIZE then
378 assert(target:write(("\0"):rep(BLOCK_SIZE - #block)))
379 break
380 end
381 end
382 --close source file
383 source:close()
384 if options.verbose then
385 --draw full progress bar
386 progressBar.finish()
387 end
388 assert(bytesToCopy <= 0, "Error: Could not copy file!")
389 end
390 end
391 end
392 if not isDirectoryContent then
393 assert(target:write(NULL_BLOCK)) --Why wasting 0.5 KiB if you can waste a full KiB? xD
394 assert(target:write(NULL_BLOCK)) --(But that's the standard!)
395 end
396 if closeAtExit then
397 target:close()
398 end
399end
400
401
402
403local extractingExtractors = {
404 ["0"] = function(data, options) --file
405 --creates a file at data.file and fills it with data.size bytes
406 --ensure that the directory is existing
407 local dir = fs.path(data.file)
408 if not fs.exists(dir) then
409 fs.makeDirectory(dir)
410 end
411 --don't overwrite the file if true
412 local skip = false
413 --check for existing file
414 if fs.exists(data.file) then
415 if options.verbose then
416 print("File already exists!")
417 end
418 --check for options specifying what to do now...
419 if options["keep-old-files"] then
420 error("Error: Attempting to overwrite: '"..data.file.."'!")
421 elseif options["skip-old-files"] then
422 --don't overwrite
423 skip = true
424 elseif options["keep-newer-files"] and data.lastModified then
425 --don't overwrite when file on storage is newer
426 local lastModifiedOnDrive = math.floor(fs.lastModified(data.file) / 1000)
427 if lastModifiedOnDrive > data.lastModified then
428 skip = true
429 end
430 else
431 --default: overwrite
432 end
433 if options.verbose and not skip then
434 --verbose: tell user that we are overwriting
435 print("Overwriting...")
436 end
437 end
438 if skip then
439 --go to next header
440 return data.size
441 end
442
443 --open target file
444 local target = assert(io.open(data.file, "wb"))
445 --set file length
446 local bytesToCopy = data.size
447 --init progress bar
448 local progressBar = auto_progress.new(bytesToCopy)
449 --create extractor function, writes min(bytesToCopy, #block) bytes to target
450 local function extractor(block)
451 --shortcut for abortion
452 if block == nil then
453 target:close()
454 return nil
455 end
456 --adjust block size to missing number of bytes
457 if #block > bytesToCopy then
458 block = block:sub(1, bytesToCopy)
459 end
460 --write up to BLOCK_SIZE bytes
461 assert(target:write(block))
462 --subtract copied amount of bytes from bytesToCopy
463 bytesToCopy = bytesToCopy - #block
464 if bytesToCopy <= 0 then
465 --close target stream when done
466 target:close()
467 if options.verbose then
468 --draw full progress bar
469 progressBar.finish()
470 end
471 --return nil to finish
472 return nil
473 else
474 if options.verbose then
475 --update progress bar
476 progressBar.update(#block)
477 end
478 --continue
479 return extractor
480 end
481 end
482 if bytesToCopy > 0 then
483 return extractor
484 else
485 target:close()
486 end
487 end,
488 ["2"] = function(data, options) --symlink
489 --ensure that the directory is existing
490 local dir = fs.path(data.file)
491 if not fs.exists(dir) then
492 fs.makeDirectory(dir)
493 end
494 --check for existing file
495 if fs.exists(data.file) then
496 if options.verbose then
497 print("File already exists!")
498 end
499 if options["keep-old-files"] then
500 error("Error: Attempting to overwrite: '"..data.file.."'!")
501 elseif options["skip-old-files"] then
502 return
503 elseif options["keep-newer-files"] and data.lastModified then
504 --don't overwrite when file on storage is newer
505 local lastModifiedOnDrive = math.floor(fs.lastModified(data.file) / 1000)
506 if lastModifiedOnDrive > data.lastModified then
507 return
508 end
509 else
510 --default: overwrite file
511 end
512 --delete original file
513 if options.verbose then
514 print("Overwriting...")
515 end
516 assert(fs.remove(data.file))
517 end
518 assert(fs.link(data.linkName, data.file))
519 end,
520 ["5"] = function(data, options) --directory
521 if not fs.isDirectory(data.file) then
522 assert(fs.makeDirectory(data.file))
523 end
524 end,
525}
526local listingExtractors = {
527 ["0"] = function(data, options) --file
528 --output info
529 print("File:", data.name)
530 print("Size:", data.size)
531 --go to next header
532 return data.size
533 end,
534 ["1"] = function(data, options) --hard link: unsupported, but reported
535 print("Hard link (unsupported):", data.name)
536 print("Target:", data.linkName)
537 end,
538 ["2"] = function(data, options) --symlink
539 print("Symbolic link:", data.name)
540 print("Target:", data.linkName)
541 end,
542 ["3"] = function(data, options) --device file: unsupported, but reported
543 print("Device File (unsupported):", data.name)
544 end,
545 ["4"] = function(data, options) --device file: unsupported, but reported
546 print("Device File (unsupported):", data.name)
547 end,
548 ["5"] = function(data, options) --directory
549 print("Directory:", data.name)
550 end,
551}
552
553
554local function untarFiles(files, options, extractorList)
555 --extracts the contents of every tar file given
556 for _,file in ipairs(files) do
557 --prepare input stream
558 local source, closeAtExit
559 if type(file) == "string" then
560 source = assert(io.open(shell.resolve(file), "rb"))
561 closeAtExit = true
562 else
563 source = file
564 closeAtExit = false
565 assert(source.lines, "Unknown source type.")
566 end
567 local extractor = nil
568 local hasDoubleNull = false
569 for block in source:lines(BLOCK_SIZE) do
570 if #block < BLOCK_SIZE then
571 error("Error: Unfinished Block; missing "..(BLOCK_SIZE-#block).." bytes!")
572 end
573 if extractor == nil then
574 --load header
575 header:init(block)
576 if header:isNull() then
577 --check for second null block
578 if source:read(BLOCK_SIZE) == NULL_BLOCK then
579 hasDoubleNull = true
580 end
581 --exit/close file when there is a NULL header
582 break
583 else
584 --read block as header
585 local data = header:read()
586 if options.verbose then
587 --tell user what is going on
588 print("Extracting:", data.name)
589 end
590 --enforcing relative paths
591 data.file = shell.resolve(WORKING_DIRECTORY.."/"..data.name)
592 --get extractor
593 local extractorInit = extractorList[data.typeFlag]
594 assert(extractorInit, "Unknown type flag \""..tostring(data.typeFlag).."\"")
595 extractor = extractorInit(data, options)
596 end
597 else
598 extractor = extractor(block)
599 end
600 if type(extractor) == "number" then
601 if extractor > 0 then
602 --adjust extractorInit to block size
603 local bytesToSkip = math.ceil(extractor / BLOCK_SIZE) * BLOCK_SIZE
604 --skip (extractorInit) bytes
605 assert(source:seek("cur", bytesToSkip))
606 end
607 --expect next header
608 extractor = nil
609 end
610 end
611 assert(extractor == nil, "Error: Reached end of file but expecting more data!")
612 if closeAtExit then
613 source:close()
614 end
615 if not hasDoubleNull then
616 print("Warning: Archive does not end with two Null blocks!")
617 end
618 end
619end
620
621
622--connect function parameters with actions
623local actions = {
624 c = function(files, options, ignoredObjects) --create
625 tarFiles(files, options, "wb", ignoredObjects)
626 end,
627 r = function(files, options, ignoredObjects) --append
628 tarFiles(files, options, "rb+", ignoredObjects)
629 end,
630 x = function(files, options, ignoredObjects) --extract
631 untarFiles(files, options, extractingExtractors)
632 end,
633 t = function(files, options, ignoredObjects) --list
634 untarFiles(files, options, listingExtractors)
635 end,
636}
637--also add some aliases
638actions["create"] = actions.c
639actions["append"] = actions.r
640actions["list"] = actions.t
641actions["extract"] = actions.x
642actions["get"] = actions.x
643
644local debugEnabled = false
645
646local function main(...)
647 --variables containing the processed arguments
648 local action, files
649 --prepare arguments
650 local params, options = shell.parse(...)
651 --add stacktrace to output
652 debugEnabled = options.debug
653 --quick help
654 if options.help then
655 print(USAGE_TEXT)
656 return
657 end
658 --determine executed function and options
659 for option, value in pairs(options) do
660 local isAction = actions[option]
661 if isAction then
662 assert(action == nil, ERRORS.multipleActions)
663 action = isAction
664 options[option] = nil
665 end
666 end
667 assert(action ~= nil, ERRORS.missingAction)
668 --prepare file names
669 files = params
670 --process options
671 if options.v then
672 options.verbose = true
673 end
674 if options.dir then
675 assert(options.dir ~= true and options.dir ~= "", "Error: Invalid --dir value!")
676 WORKING_DIRECTORY = shell.resolve(options.dir) or options.dir
677 assert(WORKING_DIRECTORY ~= nil and WORKING_DIRECTORY ~= "", "Error: Invalid --dir value!")
678 end
679 if options.f or options.file then
680 --use file for archiving
681 --keep file names as they are
682 else
683 --use tape
684 local tapeFile = {drive = component.tape_drive, pos = 0}
685 do
686 --check for Computronics bug
687 local endInversionBug = false
688 --step 1: move to the end of the tape
689 local movedBy = assert(tapeFile.drive.seek(tapeFile.drive.getSize()))
690 --step 2: check output of isEnd
691 if tapeFile.drive.isEnd() ~= true then
692 endInversionBug = true
693 end
694 --step 3: restore previous position
695 assert(tapeFile.drive.seek(-movedBy) == -movedBy, "Error: Tape did not return to original position after checking for isEnd bug!")
696
697 if endInversionBug then
698 if options.verbose then
699 print("tape_drive.isEnd() bug detected; adjusting...")
700 end
701 function tapeFile:isEnd()
702 --This is a workaround for bugged versions of Computronics.
703 return (not self.drive.isEnd()) or (not self.drive.isReady())
704 end
705 else
706 function tapeFile:isEnd()
707 --This does not work in bugged versions of Computronics.
708 return self.drive.isEnd()
709 end
710 end
711 end
712 --create some kind of "tape stream" with limited buf sufficient functionality
713 function tapeFile:lines(byteCount)
714 return function()
715 return self:read(byteCount)
716 end
717 end
718 function tapeFile:read(byteCount)
719 if self:isEnd() then
720 return nil
721 end
722 local data = self.drive.read(byteCount)
723 self.pos = self.pos + #data
724 return data
725 end
726 function tapeFile:write(text)
727 self.drive.write(text)
728 self.pos = self.pos + #text
729 if self:isEnd() then
730 return nil, "Error: Reached end of tape!"
731 else
732 return self
733 end
734 end
735 function tapeFile:seek(typ, pos)
736 local toSeek
737 if typ == "set" then
738 toSeek = pos - self.pos
739 elseif typ == "cur" then
740 toSeek = pos
741 end
742 local movedBy = 0
743 if toSeek ~= 0 then
744 movedBy = self.drive.seek(toSeek)
745 self.pos = self.pos + movedBy
746 end
747 if movedBy == toSeek then
748 return self.pos
749 else
750 return nil, "Error: Unable to seek!"
751 end
752 end
753 --add tape before first file
754 table.insert(files, 1, tapeFile)
755 end
756 if options.h then
757 options.dereference = true
758 end
759
760 --prepare list of ignored objects, default is the current directory and the target file if applicable
761 local ignoredObjects = {}
762 ignoredObjects[WORKING_DIRECTORY] = true
763 if options.exclude then
764 --";" is used as a separator
765 for excluded in options.exclude:gmatch("[^%;]+") do
766 ignoredObjects[shell.resolve(excluded) or excluded] = "strict"
767 end
768 end
769
770 assert(#files > 0, ERRORS.missingFiles)
771 --And action!
772 action(files, options, ignoredObjects)
773end
774
775--adding stack trace when --debug is used
776local function errorFormatter(msg)
777 msg = msg:gsub("^[^%:]+%:[^%:]+%: ","")
778 if debugEnabled then
779 --add traceback when debugging
780 return debug.traceback(msg, 3)
781 end
782 return msg
783end
784
785local ok, msg = xpcall(main, errorFormatter, ...)
786if not ok then
787 io.stdout:write(msg)
788end