tb 0.2 → 0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (95) hide show
  1. data/README +62 -50
  2. data/bin/tb +22 -18
  3. data/lib/tb.rb +35 -19
  4. data/lib/tb/basic.rb +85 -86
  5. data/lib/tb/catreader.rb +33 -116
  6. data/lib/tb/cmd_cat.rb +31 -27
  7. data/lib/tb/cmd_consecutive.rb +45 -35
  8. data/lib/tb/cmd_crop.rb +86 -52
  9. data/lib/tb/cmd_cross.rb +113 -71
  10. data/lib/tb/cmd_cut.rb +49 -44
  11. data/lib/tb/cmd_git_log.rb +193 -0
  12. data/lib/tb/cmd_grep.rb +43 -32
  13. data/lib/tb/cmd_group.rb +63 -39
  14. data/lib/tb/cmd_gsub.rb +53 -43
  15. data/lib/tb/cmd_help.rb +51 -24
  16. data/lib/tb/cmd_join.rb +32 -35
  17. data/lib/tb/cmd_ls.rb +233 -205
  18. data/lib/tb/cmd_mheader.rb +47 -37
  19. data/lib/tb/cmd_nest.rb +94 -0
  20. data/lib/tb/cmd_newfield.rb +29 -33
  21. data/lib/tb/cmd_rename.rb +40 -32
  22. data/lib/tb/cmd_shape.rb +31 -24
  23. data/lib/tb/cmd_sort.rb +46 -25
  24. data/lib/tb/cmd_svn_log.rb +47 -28
  25. data/lib/tb/cmd_tar_tvf.rb +447 -0
  26. data/lib/tb/cmd_to_csv.rb +60 -0
  27. data/lib/tb/cmd_to_json.rb +60 -0
  28. data/lib/tb/cmd_to_pnm.rb +48 -0
  29. data/lib/tb/cmd_to_pp.rb +71 -0
  30. data/lib/tb/cmd_to_tsv.rb +48 -0
  31. data/lib/tb/cmd_to_yaml.rb +52 -0
  32. data/lib/tb/cmd_unnest.rb +118 -0
  33. data/lib/tb/cmdmain.rb +24 -20
  34. data/lib/tb/cmdtop.rb +33 -25
  35. data/lib/tb/cmdutil.rb +26 -66
  36. data/lib/tb/csv.rb +46 -34
  37. data/lib/tb/enum.rb +294 -0
  38. data/lib/tb/enumerable.rb +198 -7
  39. data/lib/tb/enumerator.rb +73 -0
  40. data/lib/tb/fieldset.rb +27 -19
  41. data/lib/tb/fileenumerator.rb +365 -0
  42. data/lib/tb/json.rb +50 -0
  43. data/lib/tb/pager.rb +6 -6
  44. data/lib/tb/pairs.rb +227 -0
  45. data/lib/tb/pnm.rb +23 -22
  46. data/lib/tb/reader.rb +52 -49
  47. data/lib/tb/record.rb +48 -19
  48. data/lib/tb/revcmp.rb +38 -0
  49. data/lib/tb/ropen.rb +74 -57
  50. data/lib/tb/search.rb +25 -21
  51. data/lib/tb/tsv.rb +31 -34
  52. data/sample/excel2csv +24 -20
  53. data/sample/poi-xls2csv.rb +24 -20
  54. data/sample/poi-xls2csv.sh +22 -18
  55. data/sample/tbplot +185 -127
  56. data/test-all-cov.rb +3 -3
  57. data/test-all.rb +1 -1
  58. data/test/test_basic.rb +26 -10
  59. data/test/test_catreader.rb +7 -6
  60. data/test/test_cmd_cat.rb +32 -0
  61. data/test/test_cmd_consecutive.rb +10 -0
  62. data/test/test_cmd_crop.rb +4 -4
  63. data/test/test_cmd_cross.rb +16 -4
  64. data/test/test_cmd_git_log.rb +46 -0
  65. data/test/test_cmd_help.rb +17 -12
  66. data/test/test_cmd_join.rb +21 -1
  67. data/test/test_cmd_ls.rb +3 -4
  68. data/test/test_cmd_mheader.rb +17 -11
  69. data/test/test_cmd_nest.rb +49 -0
  70. data/test/test_cmd_sort.rb +15 -0
  71. data/test/test_cmd_tar_tvf.rb +281 -0
  72. data/test/{test_cmd_csv.rb → test_cmd_to_csv.rb} +35 -21
  73. data/test/{test_cmd_json.rb → test_cmd_to_json.rb} +31 -3
  74. data/test/{test_cmd_pnm.rb → test_cmd_to_pnm.rb} +2 -2
  75. data/test/{test_cmd_pp.rb → test_cmd_to_pp.rb} +4 -4
  76. data/test/{test_cmd_tsv.rb → test_cmd_to_tsv.rb} +4 -4
  77. data/test/{test_cmd_yaml.rb → test_cmd_to_yaml.rb} +3 -3
  78. data/test/test_cmd_unnest.rb +89 -0
  79. data/test/test_cmdtty.rb +19 -13
  80. data/test/test_enumerable.rb +83 -1
  81. data/test/test_fileenumerator.rb +265 -0
  82. data/test/test_json.rb +15 -0
  83. data/test/test_pager.rb +3 -4
  84. data/test/test_pairs.rb +122 -0
  85. data/test/test_pnm.rb +24 -24
  86. data/test/test_reader.rb +35 -13
  87. data/test/test_revcmp.rb +10 -0
  88. data/test/test_tbenum.rb +173 -0
  89. metadata +51 -23
  90. data/lib/tb/cmd_csv.rb +0 -42
  91. data/lib/tb/cmd_json.rb +0 -60
  92. data/lib/tb/cmd_pnm.rb +0 -43
  93. data/lib/tb/cmd_pp.rb +0 -70
  94. data/lib/tb/cmd_tsv.rb +0 -43
  95. data/lib/tb/cmd_yaml.rb +0 -47
data/lib/tb/cmd_sort.rb CHANGED
@@ -1,36 +1,43 @@
1
- # Copyright (C) 2011 Tanaka Akira <akr@fsij.org>
1
+ # Copyright (C) 2011-2012 Tanaka Akira <akr@fsij.org>
2
2
  #
3
3
  # Redistribution and use in source and binary forms, with or without
4
- # modification, are permitted provided that the following conditions are met:
4
+ # modification, are permitted provided that the following conditions
5
+ # are met:
5
6
  #
6
- # 1. Redistributions of source code must retain the above copyright notice, this
7
- # list of conditions and the following disclaimer.
8
- # 2. Redistributions in binary form must reproduce the above copyright notice,
9
- # this list of conditions and the following disclaimer in the documentation
10
- # and/or other materials provided with the distribution.
11
- # 3. The name of the author may not be used to endorse or promote products
12
- # derived from this software without specific prior written permission.
7
+ # 1. Redistributions of source code must retain the above copyright
8
+ # notice, this list of conditions and the following disclaimer.
9
+ # 2. Redistributions in binary form must reproduce the above
10
+ # copyright notice, this list of conditions and the following
11
+ # disclaimer in the documentation and/or other materials provided
12
+ # with the distribution.
13
+ # 3. The name of the author may not be used to endorse or promote
14
+ # products derived from this software without specific prior
15
+ # written permission.
13
16
  #
14
- # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
15
- # WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
16
- # MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
17
- # EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
18
- # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
19
- # OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20
- # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21
- # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
22
- # IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
23
- # OF SUCH DAMAGE.
17
+ # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS
18
+ # OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19
+ # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
21
+ # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
22
+ # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
23
+ # GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
24
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
25
+ # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
26
+ # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
27
+ # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24
28
 
25
29
  Tb::Cmd.subcommands << 'sort'
26
30
 
27
31
  Tb::Cmd.default_option[:opt_sort_f] = nil
32
+ Tb::Cmd.default_option[:opt_sort_r] = nil
28
33
 
29
34
  def (Tb::Cmd).op_sort
30
35
  op = OptionParser.new
31
- op.banner = 'Usage: tb sort [OPTS] [TABLE]'
36
+ op.banner = "Usage: tb sort [OPTS] [TABLE]\n" +
37
+ "Sort rows."
32
38
  define_common_option(op, "hNo", "--no-pager")
33
39
  op.def_option('-f FIELD,...', 'specify sort keys') {|fs| Tb::Cmd.opt_sort_f = fs }
40
+ op.def_option('-r', '--reverse', 'reverse order') { Tb::Cmd.opt_sort_r = true }
34
41
  op
35
42
  end
36
43
 
@@ -43,15 +50,29 @@ def (Tb::Cmd).main_sort(argv)
43
50
  else
44
51
  fs = nil
45
52
  end
46
- tbl = Tb::CatReader.open(argv, Tb::Cmd.opt_N) {|reader| build_table(reader) }
53
+ creader = Tb::CatReader.open(argv, Tb::Cmd.opt_N)
54
+ header = []
47
55
  if fs
48
- blk = lambda {|rec| fs.map {|f| smart_cmp_value(rec[f]) } }
56
+ blk = lambda {|pairs| fs.map {|f| smart_cmp_value(pairs[f]) } }
49
57
  else
50
- blk = lambda {|rec| rec.map {|k, v| smart_cmp_value(v) } }
58
+ blk = lambda {|pairs| header.map {|f| smart_cmp_value(pairs[f]) } }
51
59
  end
52
- tbl2 = tbl.reorder_records_by(&blk)
60
+ if Tb::Cmd.opt_sort_r
61
+ blk1 = blk
62
+ blk = lambda {|pairs| Tb::RevCmp.new(blk1.call(pairs)) }
63
+ end
64
+ er = Tb::Enumerator.new {|y|
65
+ creader.with_cumulative_header {|header0|
66
+ if header0
67
+ y.set_header(header0)
68
+ end
69
+ }.each {|pairs, header1|
70
+ header = header1
71
+ y.yield pairs
72
+ }
73
+ }.extsort_by(&blk)
53
74
  with_output {|out|
54
- tbl_generate_csv(tbl2, out)
75
+ er.write_to_csv(out, !Tb::Cmd.opt_N)
55
76
  }
56
77
  end
57
78
 
@@ -1,26 +1,30 @@
1
- # Copyright (C) 2011 Tanaka Akira <akr@fsij.org>
1
+ # Copyright (C) 2011-2012 Tanaka Akira <akr@fsij.org>
2
2
  #
3
3
  # Redistribution and use in source and binary forms, with or without
4
- # modification, are permitted provided that the following conditions are met:
4
+ # modification, are permitted provided that the following conditions
5
+ # are met:
5
6
  #
6
- # 1. Redistributions of source code must retain the above copyright notice, this
7
- # list of conditions and the following disclaimer.
8
- # 2. Redistributions in binary form must reproduce the above copyright notice,
9
- # this list of conditions and the following disclaimer in the documentation
10
- # and/or other materials provided with the distribution.
11
- # 3. The name of the author may not be used to endorse or promote products
12
- # derived from this software without specific prior written permission.
7
+ # 1. Redistributions of source code must retain the above copyright
8
+ # notice, this list of conditions and the following disclaimer.
9
+ # 2. Redistributions in binary form must reproduce the above
10
+ # copyright notice, this list of conditions and the following
11
+ # disclaimer in the documentation and/or other materials provided
12
+ # with the distribution.
13
+ # 3. The name of the author may not be used to endorse or promote
14
+ # products derived from this software without specific prior
15
+ # written permission.
13
16
  #
14
- # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
15
- # WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
16
- # MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
17
- # EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
18
- # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
19
- # OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20
- # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21
- # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
22
- # IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
23
- # OF SUCH DAMAGE.
17
+ # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS
18
+ # OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19
+ # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
21
+ # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
22
+ # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
23
+ # GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
24
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
25
+ # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
26
+ # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
27
+ # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24
28
 
25
29
  require 'rexml/document'
26
30
 
@@ -31,16 +35,25 @@ Tb::Cmd.default_option[:opt_svn_log_xml] = nil
31
35
 
32
36
  def (Tb::Cmd).op_svn_log
33
37
  op = OptionParser.new
34
- op.banner = 'Usage: tb svn-log [OPTS] -- [SVN-LOG-ARGS]'
38
+ op.banner = "Usage: tb svn-log [OPTS] -- [SVN-LOG-ARGS]\n" +
39
+ "Show the SVN log as a table."
35
40
  define_common_option(op, "hNo", "--no-pager")
36
41
  op.def_option('--svn-command COMMAND', 'specify the svn command (default: svn)') {|command| Tb::Cmd.opt_svn_log_svn_command = command }
37
42
  op.def_option('--svn-log-xml FILE', 'specify the result svn log --xml') {|filename| Tb::Cmd.opt_svn_log_xml = filename }
38
43
  op
39
44
  end
40
45
 
46
+ Tb::Cmd.def_vhelp('svn-log', <<'End')
47
+ Example:
48
+
49
+ % tb svn-log
50
+ % tb svn-log -- -v
51
+ % tb svn-log -- -v http://svn.ruby-lang.org/repos/ruby/trunk
52
+ End
53
+
41
54
  class Tb::Cmd::SVNLOGListener
42
- def initialize(gen)
43
- @gen = gen
55
+ def initialize(y)
56
+ @y = y
44
57
  @header = nil
45
58
  @elt_stack = []
46
59
  @att_stack = []
@@ -82,14 +95,17 @@ class Tb::Cmd::SVNLOGListener
82
95
  else
83
96
  @header = %w[rev author date msg]
84
97
  end
85
- @gen.output_header @header
98
+ @y.set_header @header
86
99
  end
87
100
  if @log['paths']
88
101
  @log['paths'].each {|h|
89
- @gen << (@log.values_at(*%w[rev author date msg]) + h.values_at(*%w[kind action path]))
102
+ assoc = @log.to_a.reject {|f, v| !%w[rev author date msg].include?(f) }
103
+ assoc += h.to_a.reject {|f, v| !%w[kind action path].include?(f) }
104
+ @y.yield Tb::Pairs.new(assoc)
90
105
  }
91
106
  else
92
- @gen << @log.values_at(*%w[rev author date msg])
107
+ assoc = @log.to_a.reject {|f, v| !%w[rev author date msg].include?(f) }
108
+ @y.yield Tb::Pairs.new(assoc)
93
109
  end
94
110
  @log = nil
95
111
  end
@@ -139,7 +155,7 @@ def (Tb::Cmd).svn_log_with_svn_log(argv)
139
155
  }
140
156
  else
141
157
  svn = Tb::Cmd.opt_svn_log_svn_command || 'svn'
142
- IO.popen(['svn', 'log', '--xml', *argv]) {|f|
158
+ IO.popen([svn, 'log', '--xml', *argv]) {|f|
143
159
  yield f
144
160
  }
145
161
  end
@@ -148,11 +164,14 @@ end
148
164
  def (Tb::Cmd).main_svn_log(argv)
149
165
  op_svn_log.parse!(argv)
150
166
  exit_if_help('svn-log')
151
- with_table_stream_output {|gen|
167
+ er = Tb::Enumerator.new {|y|
152
168
  svn_log_with_svn_log(argv) {|f|
153
- listener = Tb::Cmd::SVNLOGListener.new(gen)
169
+ listener = Tb::Cmd::SVNLOGListener.new(y)
154
170
  REXML::Parsers::StreamParser.new(f, listener).parse
155
171
  }
156
172
  }
173
+ with_output {|out|
174
+ er.write_to_csv(out, !Tb::Cmd.opt_N)
175
+ }
157
176
  end
158
177
 
@@ -0,0 +1,447 @@
1
+ # Copyright (C) 2012 Tanaka Akira <akr@fsij.org>
2
+ #
3
+ # Redistribution and use in source and binary forms, with or without
4
+ # modification, are permitted provided that the following conditions
5
+ # are met:
6
+ #
7
+ # 1. Redistributions of source code must retain the above copyright
8
+ # notice, this list of conditions and the following disclaimer.
9
+ # 2. Redistributions in binary form must reproduce the above
10
+ # copyright notice, this list of conditions and the following
11
+ # disclaimer in the documentation and/or other materials provided
12
+ # with the distribution.
13
+ # 3. The name of the author may not be used to endorse or promote
14
+ # products derived from this software without specific prior
15
+ # written permission.
16
+ #
17
+ # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS
18
+ # OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19
+ # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
21
+ # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
22
+ # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
23
+ # GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
24
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
25
+ # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
26
+ # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
27
+ # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
+
29
+ Tb::Cmd.subcommands << 'tar-tvf'
30
+
31
+ Tb::Cmd.default_option[:opt_tar_tvf_l] = 0
32
+ Tb::Cmd.default_option[:opt_tar_tvf_ustar] = nil
33
+
34
+ def (Tb::Cmd).op_tar_tvf
35
+ op = OptionParser.new
36
+ op.banner = "Usage: tb tar-tvf [OPTS] [TAR-FILE ...]\n" +
37
+ "Show the file listing of tar file."
38
+ define_common_option(op, "hNo", "--no-pager")
39
+ op.def_option('-l', 'show more attributes.') {|fs| Tb::Cmd.opt_tar_tvf_l += 1 }
40
+ op.def_option('--ustar', 'ustar format (POSIX.1-1988). No GNU and POSIX.1-2001 extension.') {|fs| Tb::Cmd.opt_tar_tvf_ustar = true }
41
+ op
42
+ end
43
+
44
+ Tb::Cmd::TAR_RECORD_LENGTH = 512
45
+ Tb::Cmd::TAR_HEADER_STRUCTURE = [
46
+ [:name, "Z100"], # [POSIX] NUL-terminated character strings except when all characters in the array contain non-NUL characters including the last character.
47
+ [:mode, "A8"], # [POSIX] leading zero-filled octal numbers using digits which terminated by one or more <space> or NUL characters.
48
+ [:uid, "A8"], # [POSIX] leading zero-filled octal numbers using digits which terminated by one or more <space> or NUL characters.
49
+ [:gid, "A8"], # [POSIX] leading zero-filled octal numbers using digits which terminated by one or more <space> or NUL characters.
50
+ [:size, "A12"], # [POSIX] leading zero-filled octal numbers using digits which terminated by one or more <space> or NUL characters.
51
+ [:mtime, "A12"], # [POSIX] leading zero-filled octal numbers using digits which terminated by one or more <space> or NUL characters.
52
+ [:chksum, "A8"], # [POSIX] leading zero-filled octal numbers using digits which terminated by one or more <space> or NUL characters.
53
+ [:typeflag, "a1"], # [POSIX] a single character.
54
+ [:linkname, "Z100"], # [POSIX] NUL-terminated character strings except when all characters in the array contain non-NUL characters including the last character.
55
+ [:magic, "Z6"], # [POSIX] terminated by a NUL character.
56
+ [:version, "Z2"], # [POSIX] two octets containing the characters "00" (zero-zero)
57
+ [:uname, "Z32"], # [POSIX] terminated by a NUL character.
58
+ [:gname, "Z32"], # [POSIX] terminated by a NUL character.
59
+ [:devmajor, "A8"], # [POSIX] leading zero-filled octal numbers using digits which terminated by one or more <space> or NUL characters.
60
+ [:devminor, "A8"], # [POSIX] leading zero-filled octal numbers using digits which terminated by one or more <space> or NUL characters.
61
+ [:prefix, "Z155"], # [POSIX] NUL-terminated character strings except when all characters in the array contain non-NUL characters including the last character.
62
+ ]
63
+ Tb::Cmd::TAR_HEADER_TEPMLATE = Tb::Cmd::TAR_HEADER_STRUCTURE.map {|n, t| t }.join('')
64
+
65
+ Tb::Cmd::TAR_TYPEFLAG = {
66
+ "\0" => :regular, # [POSIX] For backwards-compatibility.
67
+ '0' => :regular, # [POSIX]
68
+ '1' => :link, # [POSIX]
69
+ '2' => :symlink, # [POSIX]
70
+ '5' => :directory, # [POSIX]
71
+ '3' => :character_special, # [POSIX]
72
+ '4' => :block_special, # [POSIX]
73
+ '6' => :fifo, # [POSIX]
74
+ '7' => :contiguous, # [POSIX] Reserved for high-performance file. (It is come from "contiguous file" (S_IFCTG) of Masscomp?)
75
+ }
76
+
77
+ def (Tb::Cmd).tar_tvf_parse_seconds_from_epoch(val)
78
+ if /\./ =~ val
79
+ num = ($` + $').to_i
80
+ den = 10 ** $'.length
81
+ t = Rational(num, den)
82
+ begin
83
+ Time.at(t)
84
+ rescue TypeError
85
+ ti = t.floor
86
+ Time.at(ti, ((t-ti) * 1000000).floor)
87
+ end
88
+ else
89
+ Time.at(val.to_i)
90
+ end
91
+ end
92
+
93
+ Tb::Cmd::TAR_PAX_KEYWORD_RECOGNIZERS = {
94
+ 'atime' => [:atime, lambda {|val| Tb::Cmd.tar_tvf_parse_seconds_from_epoch(val) }],
95
+ 'mtime' => [:mtime, lambda {|val| Tb::Cmd.tar_tvf_parse_seconds_from_epoch(val) }],
96
+ 'ctime' => [:ctime, lambda {|val| Tb::Cmd.tar_tvf_parse_seconds_from_epoch(val) }],
97
+ 'gid' => [:gid, lambda {|val| val.to_i }],
98
+ 'gname' => [:gname, lambda {|val| val }],
99
+ 'uid' => [:uid, lambda {|val| val.to_i }],
100
+ 'uname' => [:uname, lambda {|val| val }],
101
+ 'linkpath' => [:linkname, lambda {|val| val }],
102
+ 'path' => [:path, lambda {|val| val }],
103
+ 'size' => [:size, lambda {|val| val.to_i }],
104
+ }
105
+
106
+ Tb::Cmd::TAR_CSV_HEADER = %w[mode filemode uid user gid group devmajor devminor size mtime path linkname]
107
+ Tb::Cmd::TAR_CSV_LONG_HEADER = %w[mode filemode uid user gid group devmajor devminor size mtime atime ctime path linkname size_in_tar tar_typeflag tar_magic tar_version tar_chksum]
108
+
109
+ def (Tb::Cmd).tar_tvf_parse_header(header_record)
110
+ ary = header_record.unpack(Tb::Cmd::TAR_HEADER_TEPMLATE)
111
+ h = {}
112
+ Tb::Cmd::TAR_HEADER_STRUCTURE.each_with_index {|(k, _), i|
113
+ h[k] = ary[i]
114
+ }
115
+ [:mode, :uid, :gid, :size, :mtime, :chksum, :devmajor, :devminor].each {|k|
116
+ h[k] = h[k].to_i(8)
117
+ }
118
+ h[:mtime] = Time.at(h[:mtime])
119
+ if h[:prefix].empty?
120
+ h[:path] = h[:name]
121
+ else
122
+ h[:path] = h[:prefix] + '/' + h[:name]
123
+ end
124
+ header_record_for_chksum = header_record.dup
125
+ header_record_for_chksum[148, 8] = ' ' * 8
126
+ if header_record_for_chksum.sum(0) != h[:chksum]
127
+ warn "invalid checksum: #{h[:path].inspect}"
128
+ end
129
+ h
130
+ end
131
+
132
+ class Tb::Cmd::TarFormatError < StandardError
133
+ end
134
+
135
+ class Tb::Cmd::TarReader
136
+ def initialize(input)
137
+ @input = input
138
+ @offset = 0
139
+ end
140
+ attr_reader :offset
141
+
142
+ def get_single_record(kind)
143
+ record = @input.read(Tb::Cmd::TAR_RECORD_LENGTH)
144
+ if !record
145
+ return nil
146
+ end
147
+ if record.length != Tb::Cmd::TAR_RECORD_LENGTH
148
+ warn "premature end of tar archive (#{kind})"
149
+ raise Tb::Cmd::TarFormatError
150
+ end
151
+ @offset += Tb::Cmd::TAR_RECORD_LENGTH
152
+ record
153
+ end
154
+
155
+ def read_single_record(kind)
156
+ record = get_single_record(kind)
157
+ if !record
158
+ warn "premature end of tar archive (#{kind})"
159
+ raise Tb::Cmd::TarFormatError
160
+ end
161
+ record
162
+ end
163
+
164
+ def read_exactly(size, kind)
165
+ record = @input.read(size)
166
+ if !record
167
+ warn "premature end of tar archive (#{kind})"
168
+ raise Tb::Cmd::TarFormatError
169
+ end
170
+ if record.length != size
171
+ warn "premature end of tar archive (#{kind})"
172
+ raise Tb::Cmd::TarFormatError
173
+ end
174
+ @offset += size
175
+ record
176
+ end
177
+
178
+ def skip(size, kind)
179
+ begin
180
+ @input.seek(size, IO::SEEK_CUR)
181
+ rescue Errno::ESPIPE
182
+ rest = size
183
+ while 0 < rest
184
+ if rest < 4096
185
+ s = rest
186
+ else
187
+ s = 4096
188
+ end
189
+ ret = @input.read(s)
190
+ if !ret || ret.length != s
191
+ warn "premature end of tar archive content (#{kind})"
192
+ raise Tb::Cmd::TarFormatError
193
+ end
194
+ rest -= s
195
+ end
196
+ end
197
+ @offset += size
198
+ end
199
+ end
200
+
201
+ def (Tb::Cmd).tar_tvf_read_end_of_archive_indicator(reader)
202
+ # The end of archive indicator is two consecutive records of NULs.
203
+ # The first record is already read.
204
+ second_end_of_archive_indicator_record = reader.get_single_record("second record of the end of archive indicator")
205
+ if !second_end_of_archive_indicator_record
206
+ # some tarballs have only one record of NULs.
207
+ return
208
+ end
209
+ if /\A\0*\z/ !~ second_end_of_archive_indicator_record
210
+ warn "The second record of end of tar archive indicator is not zero"
211
+ raise Tb::Cmd::TarFormatError
212
+ end
213
+ # It is acceptable that there may be garbage after the end of tar
214
+ # archive indicator. ("ustar Interchange Format" in POSIX)
215
+ end
216
+
217
+ def (Tb::Cmd).tar_tvf_check_extension_record(reader, h, content_blocklength)
218
+ prefix_parameters = {}
219
+ case h[:typeflag]
220
+ when 'L' # GNU
221
+ content = reader.read_exactly(content_blocklength, 'GNU long file name')[0, h[:size]][/\A[^\0]*/]
222
+ prefix_parameters[:path] = content
223
+ return prefix_parameters
224
+ when 'K' # GNU
225
+ content = reader.read_exactly(content_blocklength, 'GNU long link name')[0, h[:size]][/\A[^\0]*/]
226
+ prefix_parameters[:linkname] = content
227
+ return prefix_parameters
228
+ when 'x' # pax (POSIX.1-2001)
229
+ content = reader.read_exactly(content_blocklength, 'pax Extended Header content')[0, h[:size]]
230
+ while /\A(\d+) / =~ content
231
+ lenlen = $&.length
232
+ len = $1.to_i
233
+ param = content[lenlen, len-lenlen]
234
+ content = content[len..-1]
235
+ if /\n\z/ =~ param
236
+ param.chomp!("\n")
237
+ else
238
+ warn "pax hearder record doesn't end with a newline: #{param.inspect}"
239
+ end
240
+ if /=/ !~ param
241
+ warn "pax hearder record doesn't contain a equal character: #{param.inspect}"
242
+ else
243
+ key = $`
244
+ val = $'
245
+ if Tb::Cmd::TAR_PAX_KEYWORD_RECOGNIZERS[key]
246
+ if val == ''
247
+ prefix_parameters[symkey] = nil
248
+ else
249
+ symkey, recognizer = Tb::Cmd::TAR_PAX_KEYWORD_RECOGNIZERS[key]
250
+ prefix_parameters[symkey] = recognizer.call(val)
251
+ end
252
+ end
253
+ end
254
+ end
255
+ return prefix_parameters
256
+ end
257
+ nil
258
+ end
259
+
260
+ def (Tb::Cmd).tar_tvf_each(f)
261
+ offset = 0
262
+ reader = Tb::Cmd::TarReader.new(f)
263
+ prefix_parameters = {}
264
+ while true
265
+ header_record = reader.get_single_record("file header")
266
+ if !header_record
267
+ break
268
+ end
269
+ if /\A\0*\z/ =~ header_record
270
+ tar_tvf_read_end_of_archive_indicator(reader)
271
+ break
272
+ end
273
+ h = tar_tvf_parse_header(header_record)
274
+ content_numrecords = (h[:size] + Tb::Cmd::TAR_RECORD_LENGTH - 1) / Tb::Cmd::TAR_RECORD_LENGTH
275
+ content_blocklength = content_numrecords * Tb::Cmd::TAR_RECORD_LENGTH
276
+ if !Tb::Cmd.opt_tar_tvf_ustar
277
+ extension_params = tar_tvf_check_extension_record(reader, h, content_blocklength)
278
+ if extension_params
279
+ prefix_parameters.update extension_params
280
+ next
281
+ end
282
+ end
283
+ prefix_parameters.each {|k, v|
284
+ if v.nil?
285
+ h.delete k
286
+ else
287
+ h[k] = v
288
+ end
289
+ }
290
+ case Tb::Cmd::TAR_TYPEFLAG[h[:typeflag]]
291
+ when :link, :symlink, :directory, :character_special, :block_special, :fifo
292
+ # xxx: hardlink may have contents for posix archive.
293
+ else
294
+ reader.skip(content_blocklength, 'file content')
295
+ end
296
+ h[:size_in_tar] = reader.offset - offset
297
+ yield h
298
+ offset = reader.offset
299
+ prefix_parameters = {}
300
+ end
301
+ end
302
+
303
+ def (Tb::Cmd).tar_tvf_open_with0(arg)
304
+ if arg == '-'
305
+ yield $stdin
306
+ else
307
+ open(arg, 'rb') {|f|
308
+ yield f
309
+ }
310
+ end
311
+ end
312
+
313
+ def (Tb::Cmd).tar_tvf_open_with(arg)
314
+ tar_tvf_open_with0(arg) {|f|
315
+ magic = f.read(8)
316
+ case magic
317
+ when /\A\x1f\x8b/, /\A\037\235/ # \x1f\x8b is gzip format. \037\235 is "compress" format of old Unix.
318
+ decompression = ['gzip', '-dc']
319
+ when /\ABZh/
320
+ decompression = ['bzip2', '-dc']
321
+ when /\A\xFD7zXZ\x00/
322
+ decompression = ['xz', '-dc']
323
+ end
324
+ begin
325
+ f.rewind
326
+ seek_success = true
327
+ rescue Errno::ESPIPE
328
+ seek_success = false
329
+ end
330
+ # Ruby 1.9 dependent.
331
+ if decompression
332
+ if seek_success
333
+ IO.popen(decompression + [{:in => f}], 'rb') {|pipe|
334
+ yield pipe
335
+ }
336
+ else
337
+ IO.pipe {|r, w|
338
+ w.binmode
339
+ IO.popen(decompression + [{:in => r}], 'rb') {|pipe|
340
+ w << magic
341
+ th = Thread.new {
342
+ IO.copy_stream(f, w)
343
+ w.close
344
+ }
345
+ begin
346
+ yield pipe
347
+ ensure
348
+ th.join
349
+ end
350
+ }
351
+ }
352
+ end
353
+ else
354
+ if seek_success
355
+ yield f
356
+ else
357
+ IO.pipe {|r, w|
358
+ w.binmode
359
+ w << magic
360
+ th = Thread.new {
361
+ IO.copy_stream(f, w)
362
+ w.close
363
+ }
364
+ begin
365
+ yield r
366
+ ensure
367
+ th.join
368
+ end
369
+ }
370
+ end
371
+ end
372
+ }
373
+ end
374
+
375
+ def (Tb::Cmd).tar_tvf_format_filemode(typeflag, mode)
376
+ entry_type =
377
+ case Tb::Cmd::TAR_TYPEFLAG[typeflag]
378
+ when :regular then '-'
379
+ when :directory then 'd'
380
+ when :character_special then 'c'
381
+ when :block_special then 'b'
382
+ when :fifo then 'p'
383
+ when :symlink then 'l'
384
+ when :link then 'h'
385
+ when :contiguous then 'C'
386
+ else '?'
387
+ end
388
+ m = mode
389
+ sprintf("%s%c%c%c%c%c%c%c%c%c",
390
+ entry_type,
391
+ (m & 0400 == 0 ? ?- : ?r),
392
+ (m & 0200 == 0 ? ?- : ?w),
393
+ (m & 0100 == 0 ? (m & 04000 == 0 ? ?- : ?S) :
394
+ (m & 04000 == 0 ? ?x : ?s)),
395
+ (m & 0040 == 0 ? ?- : ?r),
396
+ (m & 0020 == 0 ? ?- : ?w),
397
+ (m & 0010 == 0 ? (m & 02000 == 0 ? ?- : ?S) :
398
+ (m & 02000 == 0 ? ?x : ?s)),
399
+ (m & 0004 == 0 ? ?- : ?r),
400
+ (m & 0002 == 0 ? ?- : ?w),
401
+ (m & 0001 == 0 ? (m & 01000 == 0 ? ?- : ?T) :
402
+ (m & 01000 == 0 ? ?x : ?t)))
403
+ end
404
+
405
+ def (Tb::Cmd).main_tar_tvf(argv)
406
+ op_tar_tvf.parse!(argv)
407
+ exit_if_help('tar-tvf')
408
+ argv = ['-'] if argv.empty?
409
+ er = Tb::Enumerator.new {|y|
410
+ if Tb::Cmd.opt_tar_tvf_l == 0
411
+ header = Tb::Cmd::TAR_CSV_HEADER
412
+ else
413
+ header = Tb::Cmd::TAR_CSV_LONG_HEADER
414
+ end
415
+ y.set_header header
416
+ argv.each {|filename|
417
+ tar_tvf_open_with(filename) {|f|
418
+ tar_tvf_each(f) {|h|
419
+ formatted = {}
420
+ formatted["mode"] = sprintf("0%o", h[:mode])
421
+ formatted["filemode"] = tar_tvf_format_filemode(h[:typeflag], h[:mode])
422
+ formatted["uid"] = h[:uid].to_s
423
+ formatted["gid"] = h[:gid].to_s
424
+ formatted["size"] = h[:size].to_s
425
+ formatted["mtime"] = h[:mtime].iso8601(0 < Tb::Cmd.opt_tar_tvf_l ? 9 : 0)
426
+ formatted["atime"] = h[:atime].iso8601(0 < Tb::Cmd.opt_tar_tvf_l ? 9 : 0) if h[:atime]
427
+ formatted["ctime"] = h[:ctime].iso8601(0 < Tb::Cmd.opt_tar_tvf_l ? 9 : 0) if h[:ctime]
428
+ formatted["user"] = h[:uname]
429
+ formatted["group"] = h[:gname]
430
+ formatted["devmajor"] = h[:devmajor].to_s
431
+ formatted["devminor"] = h[:devminor].to_s
432
+ formatted["path"] = h[:path]
433
+ formatted["linkname"] = h[:linkname]
434
+ formatted["size_in_tar"] = h[:size_in_tar]
435
+ formatted["tar_chksum"] = h[:chksum]
436
+ formatted["tar_typeflag"] = h[:typeflag]
437
+ formatted["tar_magic"] = h[:magic]
438
+ formatted["tar_version"] = h[:version]
439
+ y.yield Tb::Pairs.new(header.map {|f2| [f2, formatted[f2]] })
440
+ }
441
+ }
442
+ }
443
+ }
444
+ with_output {|out|
445
+ er.write_to_csv(out, !Tb::Cmd.opt_N)
446
+ }
447
+ end