1#!/usr/bin/env ruby
2# -*- ruby -*-
3#
4# Copyright (c) 2001-2004 Akinori MUSHA
5#
6# All rights reserved.
7#
8# Redistribution and use in source and binary forms, with or without
9# modification, are permitted provided that the following conditions
10# are met:
11# 1. Redistributions of source code must retain the above copyright
12#    notice, this list of conditions and the following disclaimer.
13# 2. Redistributions in binary form must reproduce the above copyright
14#    notice, this list of conditions and the following disclaimer in the
15#    documentation and/or other materials provided with the distribution.
16#
17# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
18# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
21# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
22# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
23# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
24# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
26# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
27# SUCH DAMAGE.
28#
29
30RCS_ID = %q$Idaemons: /home/cvs/sunshar/sunshar.rb,v 1.13 2004/02/28 14:15:47 knu Exp $
31RCS_REVISION = RCS_ID.split[2]
32MYNAME = File.basename($0)
33
34require 'optparse'
35require 'fileutils'
36require 'shellwords'
37require 'stringio'
38
39$USAGE = 'usage'
40
41$strip_level = 0
42$force = false
43$dryrun = false
44$quiet = false
45$dir = nil
46
47def info(*s)
48  puts(*s) unless $quiet
49end
50
51def usage
52  print <<-EOF
53#{MYNAME} rev.#{RCS_REVISION}
54
55usage:  #{MYNAME} [-fnq] [-p level] [file]
56        #{MYNAME} -h
57  -d dir  chdir   -- chdir to dir before extracting files
58  -f      force   -- allow overwriting, ignore errors
59  -h      help    -- show this help
60  -n      dry run -- show what would have been extracted
61  -p N    strip   -- strip N levels from pathnames (cf. patch(1)\'s -p)
62  -q      quiet   -- be quiet
63  EOF
64end
65
66def main
67  params = ARGV.getopts("fhnq", "d:", "p:")
68
69  if params['h']
70    usage
71    exit 0
72  end
73
74  if params['f']
75    $force = true
76  end
77
78  if params['n']
79    $dryrun = true
80  end
81
82  if params['q']
83    $quiet = true
84  end
85
86  $dir = params['d'] || '.'
87
88  if not params['p'].nil?
89    $strip_level = params['p'].to_i rescue -1
90
91    if $strip_level < 0
92      STDERR.puts "negative value ignored: #{params['p']}"
93    end
94  end
95
96  nerrors = 0
97
98  if ARGV.empty?
99    info "extracting files from stdin into #{$dir}"
100
101    begin
102      Dir.chdir($dir) {
103	unshar_stream(STDIN)
104      }
105
106      info "done."
107    rescue => e
108      STDERR.puts "error in extracting stdin: #{e.message}"
109      nerrors += 1
110    end
111  else
112    for file in ARGV
113      info "extracting files from #{file} into #{$dir}"
114
115      begin
116	File.open(file) do |f|
117	  Dir.chdir($dir) {
118	    unshar_stream(f)
119	  }
120	end
121
122	info "done."
123      rescue => e
124	STDERR.puts "error in extracting #{file}: #{e.message}"
125	nerrors += 1
126      end
127    end
128  end
129
130  exit nerrors
131end
132
133def unshar_stream(io)
134  e = nil
135
136  while line = io.gets
137    if /^(\s*)\# This is a shell archive/ =~ line
138      indent = $1.length
139      break
140    end
141  end
142
143  if io.eof?
144    raise "not a shell archive."
145  end
146
147  f = nil
148  prefix = nil
149  file = nil
150  boundary = nil
151
152  while line = io.gets
153    line.slice!(0, indent)
154
155    if f
156      if line.strip == boundary
157	f.close
158	f = nil
159	next
160      end
161
162      if line.sub!(/^#{Regexp.quote(prefix)}/, '')
163	f.print line
164      else
165	raise "line #{io.lineno}: broken archive: #{line}"
166      end
167
168      next
169    end
170
171    case line
172    when /^exit\s*$/
173      break
174    when /^echo\s+(.+)$/
175      # info $1
176    when /^mkdir\s+(?:-p\s+)?(.+)$/
177      dir = nil
178
179      Shellwords.shellwords($1).each do |word|
180	if /^[^\-]/ =~ word
181	  dir = word
182	  break
183	end
184      end
185
186      next if dir.nil?
187
188      dir = strip_filename(dir.strip + '/')
189      if dir.chomp('/').empty?
190	next
191      end
192
193      begin
194	FileUtils.mkdir_p(dir) unless $dryrun
195	info "c - #{dir}"
196      rescue => e
197	info "c - #{dir} ... failed: #{e.message}"
198	raise e
199      end
200    when /sed\s+(.+)>(.+)<<(.+)/
201      prefix = Shellwords.shellwords($1).first
202      file = Shellwords.shellwords($2).first
203      boundary = Shellwords.shellwords($3).first
204
205      next unless prefix && file && boundary
206
207      if /s(.)\^(.*)\1\1/ =~ prefix
208	prefix = $2
209      else
210	next
211      end
212
213      file = strip_filename(file)
214
215      next if file.empty? || boundary.empty?
216
217      overwrite = false
218
219      if File.exist?(file)
220	if $force
221	  overwrite = true
222	else
223	  info "x - #{file} ... skipped"
224	  next
225	end
226      end
227
228      dir = File.dirname(file)
229
230      if !File.directory?(dir + "/.")
231	begin
232	  FileUtils.mkdir_p(dir) unless $dryrun
233	  info "d - #{dir}"
234	rescue => e
235	  info "d - #{dir} ... failed: #{e.message}"
236	  raise e
237	end
238      end
239
240      begin
241        f = $dryrun ? StringIO.new : File.open(file, 'w')
242	if overwrite
243	  info "x - #{file} ... overwritten"
244	else
245	  info "x - #{file}"
246	end
247      rescue => e
248	info "x - #{file} ... failed! (#{e.message})"
249
250	if $force
251	  f = nil
252	  next
253	else
254	  raise e
255	end
256      end
257    end
258  end
259
260  raise e if e
261end
262
263def strip_filename(file)
264  sfile = file.gsub(%r"/{2,}", "/")
265
266  if 0 < $strip_level
267    sfile.sub!(%r"^([^/]*/){1,#{$strip_level}}", '')
268  end
269
270  case sfile
271  when %r"^[~/]"
272    raise "reference to absolute directory: #{file} (use -p N)"
273  when %r"(^|/)\.\.(?:/|$)"
274    raise "reference to parent directory: #{file} (use -p N)"
275  end
276
277  sfile
278end
279
280def signal_handler(sig)
281  info "\nInterrupted."
282
283  exit 255
284end
285
286if $0 == __FILE__
287  for sig in [2, 3, 15]
288    trap(sig) do
289      signal_handler(sig)
290    end
291  end
292
293  main
294end
295