1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
-- System functions
 
module ("dist.sys", package.seeall)
 
local cfg = require "dist.config"
local utils = require "dist.utils"
local lfs = require "lfs"
 
-- Return the path separator according to the platform.
function path_separator()
    if cfg.arch == "Windows" then
        return "\\"
    else
        return "/"
    end
end
 
-- Return path with wrong separators replaced with the right ones.
function check_separators(path)
    assert(type(path) == "string", "sys.check_separators: Argument 'path' is not a string.")
    if cfg.arch == "Windows" then
        return path:gsub("/", "\\")
    else
        return path
    end
end
 
-- Return the path with the unnecessary trailing separator removed.
function remove_trailing(path)
    assert(type(path) == "string", "sys.remove_trailing: Argument 'path' is not a string.")
    if path:sub(-1) == path_separator() and not is_root(path) then path = path:sub(1,-2) end
    return path
end
 
-- Return the path with the all occurences of '/.' or '\.' (representing
-- the current directory) removed.
function remove_curr_dir_dots(path)
    assert(type(path) == "string", "sys.remove_curr_dir_dots: Argument 'path' is not a string.")
    while path:match(path_separator() .. "%." .. path_separator()) do                       -- match("/%./")
        path = path:gsub(path_separator() .. "%." .. path_separator(), path_separator())    -- gsub("/%./", "/")
    end
    return path:gsub(path_separator() .. "%.$", "")                                         -- gsub("/%.$", "")
end
 
-- Return string argument quoted for a command line usage.
function quote(argument)
    assert(type(argument) == "string", "sys.quote: Argument 'argument' is not a string.")
 
    -- TODO: This seems like a not very nice hack. Why is it needed?
    --       Wouldn't it be better to fix the problem where it originates?
    -- replace '/' path separators for '\' on Windows
    if cfg.arch == "Windows" and argument:match("^[%u%U.]?:?[/\\].*") then
        argument = argument:gsub("//","\\"):gsub("/","\\")
    end
 
    -- Windows doesn't recognize paths starting with two slashes or backslashes
    -- so we double every backslash except for the first one
    if cfg.arch == "Windows" and argument:match("^[/\\].*") then
        local prefix = argument:sub(1,1)
        argument = argument:sub(2):gsub("\\",  "\\\\")
        argument = prefix .. argument
    else
        argument = argument:gsub("\\",  "\\\\")
    end
    argument = argument:gsub('"',  '\\"')
 
    return '"' .. argument .. '"'
end
 
-- Run the system command (in current directory).
-- Return true on success, nil on fail and log string.
-- When optional 'force_verbose' parameter is true, then the output will be shown
-- even when not in debug or verbose mode.
function exec(command, force_verbose)
    force_verbose = force_verbose or false
    assert(type(command) == "string", "sys.exec: Argument 'command' is not a string.")
    assert(type(force_verbose) == "boolean", "sys.exec: Argument 'force_verbose' is not a boolean.")
 
    if not (cfg.verbose or cfg.debug or force_verbose) then
        if cfg.arch == "Windows" then
            command = command .. " > NUL 2>&1"
        else
            command = command .. " > /dev/null 2>&1"
        end
    end
 
    if cfg.debug then print("Executing the command: " .. command) end
    local ok, str, status  = os.execute(command)
 
    -- os.execute returned values on failure are:
    --  nil or true, "exit", n or true, "signal", n for lua >= 5.2
    --  status ~= 0 for lua 5.x < 5.2
    if ok == nil or (str == "exit" and status ~= 0) or str == "signal" or (ok ~= 0 and ok ~= true) then
        return nil, "Error when running the command: " .. command
    else
        return true, "Sucessfully executed the command: " .. command
    end
end
 
-- Execute the 'command' and returns its output as a string.
function capture_output(command)
    assert(type(command) == "string", "sys.exec: Argument 'command' is not a string.")
 
    local executed, err = io.popen(command, "r")
    if not executed then return nil, "Error running the command '" .. command .. "':" .. err end
 
    local captured, err = executed:read("*a")
    if not captured then return nil, "Error reading the output of command '" .. command .. "':" .. err end
 
    executed:close()
    return captured
end
 
-- Return whether the path is a root.
function is_root(path)
    assert(type(path) == "string", "sys.is_root: Argument 'path' is not a string.")
    return utils.to_boolean(path:find("^[a-zA-Z]:[/\\]$") or path:find("^[/\\]$"))
end
 
-- Return whether the path is absolute.
function is_abs(path)
    assert(type(path) == "string", "sys.is_abs: Argument 'path' is not a string.")
    return utils.to_boolean(path:find("^[a-zA-Z]:[/\\].*$") or path:find("^[/\\].*$"))
end
 
-- Return whether the specified file or directory exists.
function exists(path)
    assert(type(path) == "string", "sys.exists: Argument 'path' is not a string.")
    local attr, err = lfs.attributes(path)
    return utils.to_boolean(attr), err
end
 
-- Return whether the 'file' exists and is a file.
function is_file(file)
    assert(type(file) == "string", "sys.is_file: Argument 'file' is not a string.")
    return lfs.attributes(file, "mode") == "file"
end
 
-- Return whether the 'dir' exists and is a directory.
function is_dir(dir)
    assert(type(dir) == "string", "sys.is_dir: Argument 'dir' is not a string.")
    return lfs.attributes(dir, "mode") == "directory"
end
 
-- Return the current working directory
function current_dir()
    local dir, err = lfs.currentdir()
    if not dir then return nil, err end
    return dir
end
 
-- Return an iterator over the directory 'dir'.
-- If 'dir' doesn't exist or is not a directory, return nil and error message.
function get_directory(dir)
    dir = dir or current_dir()
    assert(type(dir) == "string", "sys.get_directory: Argument 'dir' is not a string.")
    if is_dir(dir) then
        return lfs.dir(dir)
    else
        return nil, "Error: '".. dir .. "' is not a directory."
    end
end
 
-- Extract file or directory name from its path.
function extract_name(path)
    assert(type(path) == "string", "sys.extract_name: Argument 'path' is not a string.")
    if is_root(path) then return path end
 
    path = remove_trailing(path)
    path = path:gsub("^.*" .. path_separator(), "")
    return path
end
 
-- Return parent directory of the 'path' or nil if there's no parent directory.
-- If 'path' is a path to file, return the directory the file is in.
function parent_dir(path)
    assert(type(path) == "string", "sys.parent_dir: Argument 'path' is not a string.")
    path = remove_curr_dir_dots(path)
    path = remove_trailing(path)
 
    local dir = path:gsub(utils.escape_magic(extract_name(path)) .. "$", "")
    if dir == "" then
        return nil
    else
        return make_path(dir)
    end
end
 
-- Returns the table of all parent directories of 'path' up to the directory
-- specified by 'boundary_path' (exclusive).
function parents_up_to(path, boundary_path)
    assert(type(path) == "string", "sys.parents_up_to: Argument 'path' is not a string.")
    assert(type(boundary_path) == "string", "sys.parents_up_to: Argument 'boundary_path' is not a string.")
    boundary_path = remove_trailing(boundary_path)
 
    -- helper function to recursively collect the parent directories
    local function collect_parents(_path, _parents)
        local _parent = parent_dir(_path)
        if _parent and _parent ~= boundary_path then
            table.insert(_parents, _parent)
            return collect_parents(_parent, _parents)
        else
            return _parents
        end
    end
 
    return collect_parents(path, {})
end
 
-- Compose path composed from specified parts or current
-- working directory when no part specified.
function make_path(...)
    -- arg is deprecated in lua 5.2 in favor of table.pack we mimic here
    local arg = {n=select('#',...),...}
    local parts = arg
    assert(type(parts) == "table", "sys.make_path: Argument 'parts' is not a table.")
 
    local path, err
    if parts.n == 0 then
        path, err = current_dir()
    else
        path, err = table.concat(parts, path_separator())
    end
    if not path then return nil, err end
 
    -- squeeze repeated occurences of a file separator
    path = path:gsub(path_separator() .. "+", path_separator())
 
    -- remove unnecessary trailing path separator
    path = remove_trailing(path)
 
    return path
end
 
-- Return absolute path from 'path'
function abs_path(path)
    assert(type(path) == "string", "sys.get_abs_path: Argument 'path' is not a string.")
    if is_abs(path) then return path end
 
    local cur_dir, err = current_dir()
    if not cur_dir then return nil, err end
 
    return make_path(cur_dir, path)
end
 
-- Returns path to the temporary directory of OS.
function tmp_dir()
    return os.getenv("TMPDIR") or os.getenv("TEMP") or os.getenv("TMP") or "/tmp"
end
 
-- Returns temporary file (or directory) path (with optional prefix).
function tmp_name(prefix)
    prefix = prefix or ""
    assert(type(prefix) == "string", "sys.tmp_name: Argument 'prefix' is not a string.")
    return make_path(tmp_dir(), prefix .. "luadist_" .. utils.rand(10000000000))
end
 
-- Return table of all paths in 'dir'
function get_file_list(dir)
    dir = dir or current_dir()
    assert(type(dir) == "string", "sys.get_directory: Argument 'dir' is not a string.")
    if not exists(dir) then return nil, "Error getting file list of '" .. dir .. "': directory doesn't exist." end
 
    local function collect(path, all_paths)
        for item in get_directory(path) do
 
            local item_path = make_path(path, item)
            local _, last = item_path:find(dir .. path_separator(), 1, true)
            local path_to_insert = item_path:sub(last + 1)
 
            if is_file(item_path) then
                table.insert(all_paths, path_to_insert)
            elseif is_dir(item_path) and item ~= "." and item ~= ".." then
                table.insert(all_paths, path_to_insert)
                collect(item_path, all_paths)
            end
        end
    end
 
    local all_paths = {}
    collect(dir, all_paths)
 
    return all_paths
end
 
-- Return time of the last modification of 'file'.
function last_modification_time(file)
    assert(type(file) == "string", "sys.last_modification_time: Argument 'file' is not a string.")
    return lfs.attributes(file, "modification")
end
 
-- Return the current time (in seconds since epoch).
function current_time()
    return os.time()
end
 
-- Change the current working directory and return 'true' and previous working
-- directory on success and 'nil' and error message on error.
function change_dir(dir_name)
    assert(type(dir_name) == "string", "sys.change_dir: Argument 'dir_name' is not a string.")
    local prev_dir = current_dir()
    local ok, err = lfs.chdir(dir_name)
    if ok then
        return ok, prev_dir
    else
        return nil, err
    end
end
 
-- Make a new directory, making also all of its parent directories that doesn't exist.
function make_dir(dir_name)
    assert(type(dir_name) == "string", "sys.make_dir: Argument 'dir_name' is not a string.")
    if exists(dir_name) then
        return true
    else
        local par_dir = parent_dir(dir_name)
        if par_dir then
            local ok, err = make_dir(par_dir)
            if not ok then return nil, err end
        end
        return lfs.mkdir(dir_name)
    end
end
 
-- Move file (or directory) to the destination directory
function move_to(file_or_dir, dest_dir)
    assert(type(file_or_dir) == "string", "sys.move_to: Argument 'file_or_dir' is not a string.")
    assert(type(dest_dir) == "string", "sys.move_to: Argument 'dest_dir' is not a string.")
    assert(is_dir(dest_dir), "sys.move_to: Destination '" .. dest_dir .."' is not a directory.")
 
    -- Extract file/dir name from its path
    local file_or_dir_name = extract_name(file_or_dir)
 
    return os.rename(file_or_dir, make_path(dest_dir, file_or_dir_name))
end
 
-- rename file (or directory) to the new name.
function rename(file, new_name)
    assert(type(file) == "string", "sys.rename: Argument 'file' is not a string.")
    assert(type(new_name) == "string", "sys.rename: Argument 'new_name' is not a string.")
    assert(not exists(new_name), "sys.rename: desired filename already exists.")
 
    return os.rename(file, new_name)
end
 
-- Copy 'source' to the destination directory 'dest_dir'.
-- If 'source' is a directory, then recursive copying is used.
-- For non-recursive copying of directories use the make_dir() function.
function copy(source, dest_dir)
    assert(type(source) == "string", "sys.copy: Argument 'file_or_dir' is not a string.")
    assert(type(dest_dir) == "string", "sys.copy: Argument 'dest_dir' is not a string.")
    assert(is_dir(dest_dir), "sys.copy: destination '" .. dest_dir .."' is not a directory.")
 
    if cfg.arch == "Windows" then
        if is_dir(source) then
            make_dir(make_path(dest_dir, extract_name(source)))
            return exec("xcopy /E /I /Y /Q " .. quote(source) .. " " .. quote(dest_dir .. "\\" .. extract_name(source)))
        else
            return exec("copy /Y " .. quote(source) .. " " .. quote(dest_dir))
        end
    else
        if is_dir(source) then
            return exec("cp -fRH " .. quote(source) .. " " .. quote(dest_dir))
        else
            return exec("cp -fH " .. quote(source) .. " " .. quote(dest_dir))
        end
    end
end
 
-- Delete the specified file or directory
function delete(path)
    assert(type(path) == "string", "sys.delete: Argument 'path' is not a string.")
    assert(is_abs(path), "sys.delete: Argument 'path' is not an absolute path.")
 
    if cfg.arch == "Windows" then
        if not exists(path) then
            return true
        elseif is_file(path) then
            return os.remove(path)
        else
            return exec("rd /S /Q " .. quote(path))
        end
    else
        return exec("rm -rf " .. quote(path))
    end
end