tb 0.2 → 0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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