1--[[--
2 Additions to the core io module.
3
4 The module table returned by `std.io` also contains all of the entries from
5 the core `io` module table.  An hygienic way to import this module, then,
6 is simply to override core `io` locally:
7
8    local io = require "std.io"
9
10 @module std.io
11]]
12
13
14local base  = require "std.base"
15local debug = require "std.debug"
16
17local argerror = debug.argerror
18local catfile, dirsep, insert, len, leaves, split =
19  base.catfile, base.dirsep, base.insert, base.len, base.leaves, base.split
20local ipairs, pairs = base.ipairs, base.pairs
21local setmetatable  = debug.setmetatable
22
23
24
25local M, monkeys
26
27
28local function input_handle (h)
29  if h == nil then
30    return io.input ()
31  elseif type (h) == "string" then
32    return io.open (h)
33  end
34  return h
35end
36
37
38local function slurp (file)
39  local h, err = input_handle (file)
40  if h == nil then argerror ("std.io.slurp", 1, err, 2) end
41
42  if h then
43    local s = h:read ("*a")
44    h:close ()
45    return s
46  end
47end
48
49
50local function readlines (file)
51  local h, err = input_handle (file)
52  if h == nil then argerror ("std.io.readlines", 1, err, 2) end
53
54  local l = {}
55  for line in h:lines () do
56    l[#l + 1] = line
57  end
58  h:close ()
59  return l
60end
61
62
63local function writelines (h, ...)
64  if io.type (h) ~= "file" then
65    io.write (h, "\n")
66    h = io.output ()
67  end
68  for v in leaves (ipairs, {...}) do
69    h:write (v, "\n")
70  end
71end
72
73
74local function monkey_patch (namespace)
75  namespace = namespace or _G
76  namespace.io = base.copy (namespace.io or {}, monkeys)
77
78  if namespace.io.stdin then
79    local mt = getmetatable (namespace.io.stdin) or {}
80    mt.readlines  = M.readlines
81    mt.writelines = M.writelines
82    setmetatable (namespace.io.stdin, mt)
83  end
84
85  return M
86end
87
88
89local function process_files (fn)
90  -- N.B. "arg" below refers to the global array of command-line args
91  if len (arg) == 0 then
92    insert (arg, "-")
93  end
94  for i, v in ipairs (arg) do
95    if v == "-" then
96      io.input (io.stdin)
97    else
98      io.input (v)
99    end
100    fn (v, i)
101  end
102end
103
104
105local function warnfmt (msg, ...)
106  local prefix = ""
107  if (prog or {}).name then
108    prefix = prog.name .. ":"
109    if prog.line then
110      prefix = prefix .. tostring (prog.line) .. ":"
111    end
112  elseif (prog or {}).file then
113    prefix = prog.file .. ":"
114    if prog.line then
115      prefix = prefix .. tostring (prog.line) .. ":"
116    end
117  elseif (opts or {}).program then
118    prefix = opts.program .. ":"
119    if opts.line then
120      prefix = prefix .. tostring (opts.line) .. ":"
121    end
122  end
123  if #prefix > 0 then prefix = prefix .. " " end
124  return prefix .. string.format (msg, ...)
125end
126
127
128local function warn (msg, ...)
129  writelines (io.stderr, warnfmt (msg, ...))
130end
131
132
133
134--[[ ================= ]]--
135--[[ Public Interface. ]]--
136--[[ ================= ]]--
137
138
139local function X (decl, fn)
140  return debug.argscheck ("std.io." .. decl, fn)
141end
142
143
144M = {
145  --- Concatenate directory names into a path.
146  -- @function catdir
147  -- @string ... path components
148  -- @return path without trailing separator
149  -- @see catfile
150  -- @usage dirpath = catdir ("", "absolute", "directory")
151  catdir = X ("catdir (string...)", function (...)
152	        return (table.concat ({...}, dirsep):gsub("^$", dirsep))
153	      end),
154
155  --- Concatenate one or more directories and a filename into a path.
156  -- @function catfile
157  -- @string ... path components
158  -- @treturn string path
159  -- @see catdir
160  -- @see splitdir
161  -- @usage filepath = catfile ("relative", "path", "filename")
162  catfile = X ("catfile (string...)", base.catfile),
163
164  --- Die with error.
165  -- This function uses the same rules to build a message prefix
166  -- as @{warn}.
167  -- @function die
168  -- @string msg format string
169  -- @param ... additional arguments to plug format string specifiers
170  -- @see warn
171  -- @usage die ("oh noes! (%s)", tostring (obj))
172  die = X ("die (string, [any...])", function (...)
173	     error (warnfmt (...), 0)
174           end),
175
176  --- Remove the last dirsep delimited element from a path.
177  -- @function dirname
178  -- @string path file path
179  -- @treturn string a new path with the last dirsep and following
180  --   truncated
181  -- @usage dir = dirname "/base/subdir/filename"
182  dirname = X ("dirname (string)", function (path)
183                 return (path:gsub (catfile ("", "[^", "]*$"), ""))
184	       end),
185
186  --- Overwrite core `io` methods with `std` enhanced versions.
187  --
188  -- Also adds @{readlines} and @{writelines} metamethods to core file objects.
189  -- @function monkey_patch
190  -- @tparam[opt=_G] table namespace where to install global functions
191  -- @treturn table the `std.io` module table
192  -- @usage local io = require "std.io".monkey_patch ()
193  monkey_patch = X ("monkey_patch (?table)", monkey_patch),
194
195  --- Process files specified on the command-line.
196  -- Each filename is made the default input source with `io.input`, and
197  -- then the filename and argument number are passed to the callback
198  -- function. In list of filenames, `-` means `io.stdin`.  If no
199  -- filenames were given, behave as if a single `-` was passed.
200  -- @todo Make the file list an argument to the function.
201  -- @function process_files
202  -- @tparam fileprocessor fn function called for each file argument
203  -- @usage
204  -- #! /usr/bin/env lua
205  -- -- minimal cat command
206  -- local io = require "std.io"
207  -- io.process_files (function () io.write (io.slurp ()) end)
208  process_files = X ("process_files (function)", process_files),
209
210  --- Read a file or file handle into a list of lines.
211  -- The lines in the returned list are not `\n` terminated.
212  -- @function readlines
213  -- @tparam[opt=io.input()] file|string file file handle or name;
214  --   if file is a file handle, that file is closed after reading
215  -- @treturn list lines
216  -- @usage list = readlines "/etc/passwd"
217  readlines = X ("readlines (?file|string)", readlines),
218
219  --- Perform a shell command and return its output.
220  -- @function shell
221  -- @string c command
222  -- @treturn string output, or nil if error
223  -- @see os.execute
224  -- @usage users = shell [[cat /etc/passwd | awk -F: '{print $1;}']]
225  shell = X ("shell (string)", function (c) return slurp (io.popen (c)) end),
226
227  --- Slurp a file handle.
228  -- @function slurp
229  -- @tparam[opt=io.input()] file|string file file handle or name;
230  --   if file is a file handle, that file is closed after reading
231  -- @return contents of file or handle, or nil if error
232  -- @see process_files
233  -- @usage contents = slurp (filename)
234  slurp = X ("slurp (?file|string)", slurp),
235
236  --- Split a directory path into components.
237  -- Empty components are retained: the root directory becomes `{"", ""}`.
238  -- @function splitdir
239  -- @param path path
240  -- @return list of path components
241  -- @see catdir
242  -- @usage dir_components = splitdir (filepath)
243  splitdir = X ("splitdir (string)",
244                function (path) return split (path, dirsep) end),
245
246  --- Give warning with the name of program and file (if any).
247  -- If there is a global `prog` table, prefix the message with
248  -- `prog.name` or `prog.file`, and `prog.line` if any.  Otherwise
249  -- if there is a global `opts` table, prefix the message with
250  -- `opts.program` and `opts.line` if any.  @{std.optparse:parse}
251  -- returns an `opts` table that provides the required `program`
252  -- field, as long as you assign it back to `_G.opts`.
253  -- @function warn
254  -- @string msg format string
255  -- @param ... additional arguments to plug format string specifiers
256  -- @see std.optparse:parse
257  -- @see die
258  -- @usage
259  --   local OptionParser = require "std.optparse"
260  --   local parser = OptionParser "eg 0\nUsage: eg\n"
261  --   _G.arg, _G.opts = parser:parse (_G.arg)
262  --   if not _G.opts.keep_going then
263  --     require "std.io".warn "oh noes!"
264  --   end
265  warn = X ("warn (string, [any...])", warn),
266
267  --- Write values adding a newline after each.
268  -- @function writelines
269  -- @tparam[opt=io.output()] file h open writable file handle;
270  --   the file is **not** closed after writing
271  -- @tparam string|number ... values to write (as for write)
272  -- @usage writelines (io.stdout, "first line", "next line")
273  writelines = X ("writelines (?file|string|number, [string|number...])", writelines),
274}
275
276
277monkeys = base.copy ({}, M)  -- before deprecations and core merge
278
279
280return base.merge (M, io)
281
282
283
284--- Types
285-- @section Types
286
287--- Signature of @{process_files} callback function.
288-- @function fileprocessor
289-- @string filename filename
290-- @int i argument number of *filename*
291-- @usage
292-- local fileprocessor = function (filename, i)
293--   io.write (tostring (i) .. ":\n===\n" .. io.slurp (filename) .. "\n")
294-- end
295-- io.process_files (fileprocessor)
296