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