erbtex 0.2.0

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.
@@ -0,0 +1,31 @@
1
+ \documentclass{article}
2
+
3
+ \begin{document}
4
+
5
+ % This demonstrates that a local variable defined in one chunk of Ruby code
6
+ % can be picked up and used in a later chunk.
7
+
8
+ {: x = 99999 :}
9
+
10
+ \section{Some Pointed Questions}
11
+
12
+ {: 25.times do :}
13
+ \noindent Did you know that
14
+ {: new_x = Math.sqrt(x) :}
15
+ $\sqrt{{:= "%0.10f" % x :}} \approx {:= "%0.10f" % new_x :}$?\par
16
+ {: x = new_x :}
17
+ {: end :}
18
+
19
+ Maybe if you're Rain Man.
20
+
21
+ % The following shows that local variables don't persist across a 'require'
22
+ % statement. Local variables defined in required file disappear on return.
23
+ % Use a global variable $var to communicate between files.
24
+
25
+ {: require './testbind' :}
26
+
27
+ The `required' file set x to 1957, but it still reads {:= x :} in the \LaTeX\
28
+ doc. However, the global variable \$x set to 1957 in the required file is {:=
29
+ $x :} %$
30
+ in the \LaTeX\ file.
31
+ \end{document}
@@ -0,0 +1,160 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ module ErbTeX
4
+ class NoInputFile < StandardError; end
5
+
6
+ class CommandLine
7
+ attr_reader :command_line, :marked_command_line, :input_file
8
+ attr_reader :progname, :input_path, :output_dir, :run_dir
9
+
10
+ def initialize(command_line)
11
+ @command_line = command_line
12
+ @input_file = @marked_command_line = nil
13
+ @run_dir = Dir.pwd
14
+ find_output_dir
15
+ find_progname
16
+ find_input_file
17
+ find_input_path
18
+ mark_command_line
19
+ end
20
+
21
+ def find_progname
22
+ @progname = @command_line.split(' ')[0]
23
+ end
24
+
25
+ def find_output_dir
26
+ args = @command_line.split(' ')
27
+ # There is an -output-comment option, so -output-d is the shortest
28
+ # unambiguous way to write the -output-directory option. It can use
29
+ # one or two dashes at the beginning, and the argument can be
30
+ # seaparated from it with an '=' or white space.
31
+ have_out_dir = false
32
+ out_dir = nil
33
+ args.each do |a|
34
+ if have_out_dir
35
+ # Found -output-directory on last pass without an equals sign
36
+ out_dir = a
37
+ end
38
+ if a =~ /^--?output-d(irectory)?=(\S+)/
39
+ out_dir = $2
40
+ elsif a =~ /^--?output-d(irectory)?$/
41
+ # Next arg is the out_dir
42
+ have_out_dir = true
43
+ end
44
+ end
45
+ if out_dir.nil?
46
+ if File.writable?(Dir.pwd)
47
+ @output_dir = Dir.pwd
48
+ else
49
+ @output_dir = File.expand_path(ENV['TEXMFOUTPUT'])
50
+ end
51
+ else
52
+ @output_dir = File.expand_path(out_dir)
53
+ end
54
+ end
55
+
56
+ def find_input_file
57
+ # Remove the initial command from the command line
58
+ cmd = @command_line.split(/\s+/)[1..-1].join(' ')
59
+ cmd = cmd.gsub(/\s+--?[-a-zA-Z]+(=\S+)?/, ' ')
60
+ infile_re = %r{(\\input\s+)?(([-.~_/A-Za-z0-9]+)(\.[a-z]+)?)\s*$}
61
+ if cmd =~ infile_re
62
+ @input_file = "#{$2}"
63
+ if @input_file =~ /\.tex(\.erb)?$/
64
+ @input_file = @input_file
65
+ else
66
+ @input_file += ".tex"
67
+ end
68
+ elsif cmd =~ %r{(\\input\s+)?(["'])((?:\\?.)*?)\2} #"
69
+ # The re above captures single- or double-quoted strings with
70
+ # the insides in $3
71
+ @input_file = "#{$3}"
72
+ if @input_file !~ /\.tex$/
73
+ @input_file += ".tex#{$1}"
74
+ end
75
+ else
76
+ @input_file = nil
77
+ end
78
+ end
79
+
80
+ def find_input_path
81
+ # If input_file is absolute, don't look further
82
+ if @input_file =~ /^\//
83
+ @input_path = @input_file
84
+ elsif @input_file.nil?
85
+ @input_path = nil
86
+ else
87
+ # The following cribbed from kpathsea.rb
88
+ @progname.untaint
89
+ @input_file.untaint
90
+ kpsewhich = "kpsewhich -progname=\"#{@progname}\" -format=\"tex\" \"#{@input_file}\""
91
+ lines = ""
92
+ IO.popen(kpsewhich) do |io|
93
+ lines = io.readlines
94
+ end
95
+ if $? == 0
96
+ @input_path = lines[0].chomp.untaint
97
+ else
98
+ raise NoInputFile, "Can't find #{@input_file} in TeX search path; try kpsewhich -format=tex #{@input_file}."
99
+ end
100
+ end
101
+ end
102
+
103
+ def new_command_line(new_progname, new_infile)
104
+ ncl = @marked_command_line.sub('^p^', new_progname)
105
+ # Quote the new_infile in case it has spaces
106
+ if new_infile
107
+ ncl = ncl.sub('^f^', "'#{new_infile}'")
108
+ end
109
+ ncl
110
+ end
111
+
112
+ def mark_command_line
113
+ # Replace input file with '^f^'
114
+ infile_re = %r{(\\input\s+)?(([-.~_/A-Za-z0-9]+)(\.[a-z]+)?)\s*$}
115
+ quoted_infile_re = %r{(\\input\s+)?(["'])((?:\\?.)*?)\2} #"
116
+ if @input_file.nil?
117
+ @marked_command_line = @command_line
118
+ elsif @command_line =~ infile_re
119
+ @marked_command_line = @command_line.sub(infile_re, "#{$1}^f^")
120
+ elsif @command_line =~ quoted_infile_re
121
+ @marked_command_line = @command_line.sub(quoted_infile_re, "#{$1}^f^")
122
+ else
123
+ @marked_command_line = @command_line
124
+ end
125
+ # Replace progname with '^p^'
126
+ @marked_command_line = @marked_command_line.lstrip
127
+ @marked_command_line = @marked_command_line.sub(/\S+/, '^p^')
128
+ end
129
+ end
130
+ end
131
+
132
+ # NOTES:
133
+
134
+ # The following text is from the Web2C documentation at
135
+ # http://tug.org/texinfohtml/web2c.html#Output-file-location
136
+ #
137
+ # 3.4 Output file location
138
+ #
139
+ # All the programs generally follow the usual convention for output
140
+ # files. Namely, they are placed in the directory current when the
141
+ # program is run, regardless of any input file location; or, in a few
142
+ # cases, output is to standard output.
143
+
144
+ # For example, if you run ‘tex /tmp/foo’, for example, the output will
145
+ # be in ./foo.dvi and ./foo.log, not /tmp/foo.dvi and /tmp/foo.log.
146
+
147
+ # You can use the ‘-output-directory’ option to cause all output files
148
+ # that would normally be written in the current directory to be written
149
+ # in the specified directory instead. See Common options.
150
+
151
+ # If the current directory is not writable, and ‘-output-directory’ is
152
+ # not specified, the main programs (TeX, Metafont, MetaPost, and BibTeX)
153
+ # make an exception: if the config file or environment variable value
154
+ # TEXMFOUTPUT is set (it is not by default), output files are written to
155
+ # the directory specified.
156
+
157
+ # TEXMFOUTPUT is also checked for input files, as TeX often generates
158
+ # files that need to be subsequently read; for input, no suffixes (such
159
+ # as ‘.tex’) are added by default and no exhaustive path searching is
160
+ # done, the input name is simply checked as given.
@@ -0,0 +1,41 @@
1
+ module ErbTeX
2
+ # Find the first executable file in the PATH that is the same
3
+ # basename, but not the same absolute name as calling_prog. If this
4
+ # program has been linked to the name pdflatex, for example, and is
5
+ # located in ~/bin/pdflatex, this function will take '~/bin/pdflatex'
6
+ # as it parameter, expand it to /home/ded/pdflatex, then walk through
7
+ # the PATH looking for an executable with the same basename, pdflatex,
8
+ # but not the same absolute name /home/ded/bin/pdflatex.
9
+ #
10
+ # This allows us to make several symlinks to our erbtex program with
11
+ # the name of the actual program we want to invoke. So our link
12
+ # version of pdflatex will know to invoke the *real* pdflatex in
13
+ # /usr/bin/pdflatex after we've done the pre-processing. Also, other
14
+ # programs that want to invoke pdflatex will still work, except that
15
+ # we'll sneak in and do ruby pre-processing before invoking the real
16
+ # program.
17
+
18
+ # If the calling program is 'erbtex', treat it as 'pdflatex' just as
19
+ # if it were a pdflatex link to erbtex
20
+
21
+ def ErbTeX.find_executable(calling_prog)
22
+ calling_prog = File.absolute_path(calling_prog)
23
+ call_path = File.dirname(calling_prog)
24
+ call_base = File.basename(calling_prog).sub(/^erbtex$/, 'pdflatex')
25
+ executable = nil
26
+ ENV['PATH'].split(':').each do |p|
27
+ next unless File.directory?(p)
28
+ next if File.absolute_path(p) == call_path
29
+ Dir.chdir(p) do
30
+ Dir.glob(call_base).each do |f|
31
+ if system("file -L #{f} | grep -q ELF")
32
+ executable = File.join(p, f)
33
+ break
34
+ end
35
+ end
36
+ end
37
+ break if executable
38
+ end
39
+ executable
40
+ end
41
+ end
@@ -0,0 +1,108 @@
1
+ require 'tempfile'
2
+ require 'pathname'
3
+
4
+ module ErbTeX
5
+ # When we are handed a command line, it will be one that was
6
+ # originally intended for the real tex processor, e.g., pdflatex.
7
+ #
8
+ # We want to find the intended input file and the intended output
9
+ # directory using the ErbTeX::CommandLine object.
10
+ #
11
+ # We want to process the intended input file with Erubis and save the
12
+ # output in a temporary file with an .etx extension.
13
+ #
14
+ # Write the .etx file to the current directory unless it is not
15
+ # writable, in which case write it to /tmp.
16
+ #
17
+ # Perhaps change the Erubis pattern to something like .{ }. so that
18
+ # AucTeX does not get confused with the comment character used in
19
+ # Erubis by default (<%= %>). Erubis -p commandline would use the
20
+ # switch -p '\.{ }\.' But adapt if old pattern style is found in the
21
+ # input.
22
+ #
23
+ # If there are no Erubis patterns in the file, skip the Erubis phase
24
+ # and just pass the original command on to the system.
25
+ #
26
+ # But wait. What if there are \include{file} or \input file
27
+ # statements in the input and those have Erubis patterns in them? We
28
+ # have to invoke erbtex recursively on those, replacing the
29
+ # orginal with a processed temporary and patching up the
30
+ # \include{tmp-file}, and so on.
31
+ #
32
+ # If there is an error in the Erubis phase, we want the error message
33
+ # to make it clear what happened and exit without invoking the tex
34
+ # processor.
35
+ #
36
+ # We want to find the real tex processor with find_executable and run it
37
+ # on our processed .etx file and otherwise leave the commandline
38
+ # intact.
39
+ #
40
+ def ErbTeX.run(command)
41
+ cl = CommandLine.new(command)
42
+ Dir.chdir(cl.run_dir) do
43
+ if cl.input_file
44
+ new_infile = process(cl.input_file, cl.input_path)
45
+ else
46
+ new_infile = nil
47
+ end
48
+ if new_infile
49
+ new_infile = Pathname.new(new_infile).
50
+ relative_path_from(Pathname.new(cl.run_dir))
51
+ new_progname = ErbTeX.find_executable(command.lstrip.split(' ')[0])
52
+ cmd = cl.new_command_line(new_progname, new_infile)
53
+ cmd.sub!('\\', '\\\\\\')
54
+ puts "Executing: #{cmd}"
55
+ system(cmd)
56
+ end
57
+ end
58
+ end
59
+
60
+ # Run erbtex on the content of file_name, a String, and return the
61
+ # name of the file where the processed content can be found. This
62
+ # could be the orignal file name if no processing was needed, or a
63
+ # temporary file if the erubis pattern is found anywhere in the file.
64
+ def ErbTeX.process(file_name, dir)
65
+ puts "Input path: #{dir}"
66
+ contents = nil
67
+ File.open(file_name) do |f|
68
+ contents = f.read
69
+ end
70
+ # TODO: recurse through any \input or \include commands
71
+
72
+ # Add current directory to LOAD_PATH
73
+ $: << '.' unless $:.include?('.')
74
+
75
+ if ENV['ERBTEX_PATTERN']
76
+ pat = ENV['ERBTEX_PATTERN']
77
+ else
78
+ pat = '{: :}'
79
+ end
80
+
81
+ # Otherwise process the contents
82
+ # Find a writable directory, prefering the one the input file came
83
+ # from, or the current directory, and a temp file as a last resort.
84
+ file_absolute = File.absolute_path(File.expand_path(file_name))
85
+ file_dir = File.dirname(file_absolute)
86
+ if file_absolute =~ /\.tex\.erb$/
87
+ file_base = File.basename(file_absolute, '.tex.erb')
88
+ else
89
+ file_base = File.basename(file_absolute, '.tex')
90
+ end
91
+ of = nil
92
+ if File.writable?(file_dir)
93
+ out_file = file_dir + '/' + file_base + '.etx'
94
+ elsif File.writable?('.')
95
+ out_file = './' + file_base + '.etx'
96
+ else
97
+ of = Tempfile.new([File.basename(file_name), '.etx'])
98
+ out_file = of.path
99
+ end
100
+ unless of
101
+ of = File.open(out_file, 'w+')
102
+ end
103
+ er = Erubis::Eruby.new(contents, :pattern => pat)
104
+ of.write(er.result)
105
+ of.close
106
+ out_file
107
+ end
108
+ end
@@ -0,0 +1,3 @@
1
+ module ErbTeX
2
+ VERSION = "0.2.0"
3
+ end
data/lib/erbtex.rb ADDED
@@ -0,0 +1,10 @@
1
+ #! /usr/bin/env ruby
2
+
3
+ require 'erubis'
4
+
5
+ require 'erbtex/version'
6
+ require 'erbtex/command_line'
7
+ require 'erbtex/find_binary'
8
+ require 'erbtex/runner'
9
+
10
+ require 'byebug'
@@ -0,0 +1,181 @@
1
+ require 'test_helper'
2
+
3
+ class CommandLineTest < Test::Unit::TestCase
4
+ include ErbTeX
5
+
6
+ def setup
7
+ @test_dir = File.dirname(File.absolute_path(__FILE__))
8
+ @junk_tex = @test_dir + '/junk.tex'
9
+ FileUtils.touch(@junk_tex)
10
+
11
+ @tex_dir = @test_dir + '/tex_dir'
12
+ FileUtils.mkdir(@tex_dir) unless File.exists?(@tex_dir)
13
+ @junk2_tex = @tex_dir + '/junk2.tex'
14
+ FileUtils.touch(@junk2_tex)
15
+
16
+ @tex_dir_nw = @test_dir + '/tex_dir_nw'
17
+ FileUtils.mkdir(@tex_dir_nw) unless File.exists?(@tex_dir_nw)
18
+ @junk3_tex = @tex_dir_nw + '/junk3.tex'
19
+ FileUtils.touch(@junk3_tex)
20
+ FileUtils.chmod(0500, @tex_dir_nw)
21
+
22
+ @junk4_tex = File.expand_path('~/junk.tex')
23
+ FileUtils.touch(@junk4_tex)
24
+ end
25
+
26
+ def teardown
27
+ FileUtils.rm(@junk_tex)
28
+ FileUtils.rm(@junk4_tex)
29
+ FileUtils.rm_rf(@tex_dir)
30
+ FileUtils.chmod(0700, @tex_dir_nw)
31
+ FileUtils.rm_rf(@tex_dir_nw)
32
+ end
33
+
34
+ def test_find_ordinary_input_file
35
+ cl = 'pdflatex -ini --halt-on-error junk'
36
+ assert_equal("junk.tex",
37
+ CommandLine.new(cl).input_file)
38
+ end
39
+
40
+ def test_find_input_file_relative
41
+ cl = 'pdflatex -ini --halt-on-error ./junk.tex'
42
+ assert_equal("./junk.tex",
43
+ CommandLine.new(cl).input_file)
44
+ end
45
+
46
+ def test_find_input_file_relative_no_ext
47
+ cl = 'pdflatex -ini --halt-on-error ./junk'
48
+ assert_equal("./junk.tex",
49
+ CommandLine.new(cl).input_file)
50
+ end
51
+
52
+ def test_find_ordinary_input_file_with_ext
53
+ cl = 'pdflatex -ini --halt-on-error junk.tex'
54
+ assert_equal("junk.tex",
55
+ CommandLine.new(cl).input_file)
56
+ end
57
+
58
+ def test_find_ordinary_input_file_with_spaces
59
+ fn = 'A junk.tex'
60
+ FileUtils.touch(fn)
61
+ cl = "pdflatex -ini --halt-on-error \'#{fn}\'"
62
+ assert_equal("A junk.tex",
63
+ CommandLine.new(cl).input_file)
64
+ FileUtils.rm(fn)
65
+ end
66
+
67
+ def test_no_input_file
68
+ assert_raise ErbTeX::NoInputFile do
69
+ cl = 'pdflatex -ini'
70
+ CommandLine.new(cl)
71
+ end
72
+ end
73
+
74
+ def test_no_input_file_with_eq
75
+ assert_raise ErbTeX::NoInputFile do
76
+ cl = 'pdflatex -ini -output-directory=/tmp'
77
+ CommandLine.new(cl).input_file
78
+ end
79
+ end
80
+
81
+ def test_find_progname
82
+ cl = 'pdflatex -ini --halt-on-error junk.tex'
83
+ assert_equal("pdflatex",
84
+ CommandLine.new(cl).progname)
85
+ end
86
+
87
+ def test_mark_command_line
88
+ cl = 'pdflatex -ini --halt-on-error junk'
89
+ clm = '^p^ -ini --halt-on-error ^f^'
90
+ assert_equal(clm,
91
+ CommandLine.new(cl).marked_command_line)
92
+ end
93
+
94
+ def test_mark_command_line_with_ext
95
+ cl = 'pdflatex -ini --halt-on-error junk.tex'
96
+ clm = '^p^ -ini --halt-on-error ^f^'
97
+ assert_equal(clm,
98
+ CommandLine.new(cl).marked_command_line)
99
+ end
100
+
101
+ def test_mark_command_line_with_dir
102
+ cl = 'pdflatex -ini --halt-on-error ~/junk.tex'
103
+ clm = '^p^ -ini --halt-on-error ^f^'
104
+ assert_equal(clm,
105
+ CommandLine.new(cl).marked_command_line)
106
+ end
107
+
108
+ def test_mark_command_line_with_spaces
109
+ cl = 'pdflatex -ini --halt-on-error \'/home/ded/A junk.tex\''
110
+ clm = '^p^ -ini --halt-on-error ^f^'
111
+ assert_equal(clm,
112
+ CommandLine.new(cl).marked_command_line)
113
+ end
114
+
115
+ def test_find_embedded_input_file
116
+ cl = 'pdflatex -ini --halt-on-error \input junk'
117
+ assert_equal("junk.tex",
118
+ CommandLine.new(cl).input_file)
119
+ end
120
+
121
+ def test_find_embedded_input_file_with_ext
122
+ cl = 'pdflatex -ini --halt-on-error \input junk.tex'
123
+ assert_equal("junk.tex",
124
+ CommandLine.new(cl).input_file)
125
+ end
126
+
127
+ def test_find_input_file_with_relative
128
+ cl = 'pdflatex -ini --halt-on-error \input tex_dir/junk2.tex'
129
+ assert_equal("tex_dir/junk2.tex",
130
+ CommandLine.new(cl).input_file)
131
+ end
132
+
133
+ def test_find_input_file_with_spaces
134
+ fn = "my junk2.tex"
135
+ FileUtils.touch(fn)
136
+ cl = "pdflatex -ini --halt-on-error \input \"#{fn}\""
137
+ assert_equal(fn,
138
+ CommandLine.new(cl).input_file)
139
+ FileUtils.rm(fn)
140
+ end
141
+
142
+ def test_find_input_path_existing
143
+ cl = 'pdflatex -ini --halt-on-error \input junk.tex'
144
+ assert_equal("./junk.tex",
145
+ CommandLine.new(cl).input_path)
146
+ end
147
+
148
+ def test_dont_find_input_path_non_existing
149
+ cl = 'pdflatex -ini --halt-on-error \input junk3.tex'
150
+ assert_raise NoInputFile do
151
+ CommandLine.new(cl).input_path
152
+ end
153
+ end
154
+
155
+ def test_find_full_path_with_env
156
+ save_env = ENV['TEXINPUTS']
157
+ ENV['TEXINPUTS'] = File.dirname(__FILE__) + '/tex_dir'
158
+ cl = 'pdflatex -ini --halt-on-error \input junk2.tex'
159
+ assert_equal("./tex_dir/junk2.tex",
160
+ CommandLine.new(cl).input_path)
161
+ ENV['TEXINPUTS'] = save_env
162
+ end
163
+
164
+ def test_find_output_dir
165
+ cl = 'pdflatex -ini --halt-on-error \input junk.tex'
166
+ assert_equal(File.expand_path('./'), CommandLine.new(cl).output_dir)
167
+ end
168
+
169
+ def test_find_output_dir_with_options
170
+ cl = 'pdflatex -ini -output-d=~/tmp \input junk.tex'
171
+ assert_equal(File.expand_path('~/tmp'), CommandLine.new(cl).output_dir)
172
+ end
173
+
174
+ def test_find_output_dir_with_non_writable_pwd
175
+ cl = 'pdflatex -ini \input junk3.tex'
176
+ ENV['TEXMFOUTPUT'] = File.expand_path(File.dirname(__FILE__) + '/tex_dir')
177
+ Dir.chdir('tex_dir_nw') do
178
+ assert_equal(File.expand_path('../tex_dir'), CommandLine.new(cl).output_dir)
179
+ end
180
+ end
181
+ end
@@ -0,0 +1,44 @@
1
+ require 'test_helper'
2
+
3
+ class FindBinaryTest < Test::Unit::TestCase
4
+ include ErbTeX
5
+
6
+ # Here we set up the situation as we expect it to be after
7
+ # installation. There is a "real" pdflatex executable binary and
8
+ # there is one that is just a link to our script, the "fake" binary.
9
+ # The fake binary is earlier in PATH than the real binary, and we want
10
+ # this function, when fed the name of the fake binary to deduce the
11
+ # name of the real binary.
12
+ def setup
13
+ # Create a "fake" ruby script named pdflatex
14
+ @fake_dir = File.dirname(File.absolute_path(__FILE__)) + '/fake_bin'
15
+ FileUtils.mkdir(@fake_dir) unless File.exist?(@fake_dir)
16
+ @fake_binary = @fake_dir + '/pdflatex'
17
+ @erbtex = @fake_dir + '/erbtex'
18
+ FileUtils.touch(@erbtex)
19
+ FileUtils.chmod(0700, @erbtex)
20
+ FileUtils.rm_rf(@fake_binary) if File.exists?(@fake_binary)
21
+ FileUtils.ln_s(@erbtex, @fake_binary)
22
+
23
+ # Point to "real" pdflatex to find
24
+ @real_binary = '/usr/bin/pdflatex'
25
+ @real_dir = '/usr/bin'
26
+
27
+ # Put the fake dir on the PATH before the real dir
28
+ ENV['PATH'] = @fake_dir + ':' + @real_dir + ':' + ENV['PATH']
29
+ end
30
+
31
+ def teardown
32
+ FileUtils.rm_rf(@fake_dir)
33
+ end
34
+
35
+ def test_find_pdflatex
36
+ assert_equal(@real_binary,
37
+ ErbTeX.find_executable(@fake_binary))
38
+ end
39
+
40
+ def test_find_pdflatex_with_erbtex
41
+ assert_equal(@real_binary,
42
+ ErbTeX.find_executable(@erbtex))
43
+ end
44
+ end
@@ -0,0 +1,10 @@
1
+
2
+ # Set up load path for tests.
3
+
4
+ lib_dir = File.dirname(__FILE__) + '/../lib'
5
+ $:.unshift lib_dir unless $:.include?(lib_dir)
6
+
7
+ require 'test/unit'
8
+ require 'erbtex'
9
+ require 'fileutils'
10
+