1--- Module implementing the LuaRocks "config" command.
2-- Queries information about the LuaRocks configuration.
3local config_cmd = {}
4
5local persist = require("luarocks.persist")
6local cfg = require("luarocks.core.cfg")
7local util = require("luarocks.util")
8local deps = require("luarocks.deps")
9local dir = require("luarocks.dir")
10local fs = require("luarocks.fs")
11
12function config_cmd.add_to_parser(parser)
13   local cmd = parser:command("config", [[
14Query information about the LuaRocks configuration.
15
16* When given a configuration key, it prints the value of that key according to
17  the currently active configuration (taking into account all config files and
18  any command-line flags passed)
19
20  Examples:
21     luarocks config lua_interpreter
22     luarocks config variables.LUA_INCDIR
23     luarocks config lua_version
24
25* When given a configuration key and a value, it overwrites the config file (see
26  the --scope option below to determine which) and replaces the value of the
27  given key with the given value.
28
29  * `lua_dir` is a special key as it checks for a valid Lua installation
30    (equivalent to --lua-dir) and sets several keys at once.
31  * `lua_version` is a special key as it changes the default Lua version
32    used by LuaRocks commands (equivalent to passing --lua-version).
33
34  Examples:
35     luarocks config variables.OPENSSL_DIR /usr/local/openssl
36     luarocks config lua_dir /usr/local
37     luarocks config lua_version 5.3
38
39* When given a configuration key and --unset, it overwrites the config file (see
40  the --scope option below to determine which) and deletes that key from the
41  file.
42
43  Example: luarocks config variables.OPENSSL_DIR --unset
44
45* When given no arguments, it prints the entire currently active configuration,
46  resulting from reading the config files from all scopes.
47
48  Example: luarocks config]], util.see_also([[
49   https://github.com/luarocks/luarocks/wiki/Config-file-format
50   for detailed information on the LuaRocks config file format.
51]]))
52      :summary("Query information about the LuaRocks configuration.")
53
54   cmd:argument("key", "The configuration key.")
55      :args("?")
56   cmd:argument("value", "The configuration value.")
57      :args("?")
58
59   cmd:option("--scope", "The scope indicates which config file should be rewritten.\n"..
60      '* Using a wrapper created with `luarocks init`, the default is "project".\n'..
61      '* Using --local (or when `local_by_default` is `true`), the default is "user".\n'..
62      '* Otherwise, the default is "system".')
63      :choices({"system", "user", "project"})
64   cmd:flag("--unset", "Delete the key from the configuration file.")
65   cmd:flag("--json", "Output as JSON.")
66
67   -- Deprecated flags
68   cmd:flag("--lua-incdir"):hidden(true)
69   cmd:flag("--lua-libdir"):hidden(true)
70   cmd:flag("--lua-ver"):hidden(true)
71   cmd:flag("--system-config"):hidden(true)
72   cmd:flag("--user-config"):hidden(true)
73   cmd:flag("--rock-trees"):hidden(true)
74end
75
76local function config_file(conf)
77   print(dir.normalize(conf.file))
78   if conf.found then
79      return true
80   else
81      return nil, "file not found"
82   end
83end
84
85local cfg_skip = {
86   errorcodes = true,
87   flags = true,
88   platforms = true,
89   root_dir = true,
90   upload_servers = true,
91}
92
93local function should_skip(k, v)
94   return type(v) == "function" or cfg_skip[k]
95end
96
97local function cleanup(tbl)
98   local copy = {}
99   for k, v in pairs(tbl) do
100      if not should_skip(k, v) then
101         copy[k] = v
102      end
103   end
104   return copy
105end
106
107local function traverse_varstring(var, tbl, fn, missing_parent)
108   local k, r = var:match("^%[([0-9]+)%]%.(.*)$")
109   if k then
110      k = tonumber(k)
111   else
112      k, r = var:match("^([^.[]+)%.(.*)$")
113      if not k then
114         k, r = var:match("^([^[]+)(%[.*)$")
115      end
116   end
117
118   if k then
119      if not tbl[k] and missing_parent then
120         missing_parent(tbl, k)
121      end
122
123      if tbl[k] then
124         return traverse_varstring(r, tbl[k], fn, missing_parent)
125      else
126         return nil, "Unknown entry " .. k
127      end
128   end
129
130   local i = var:match("^%[([0-9]+)%]$")
131   if i then
132      var = tonumber(i)
133   end
134
135   return fn(tbl, var)
136end
137
138local function print_json(value)
139   local json_ok, json = util.require_json()
140   if not json_ok then
141      return nil, "A JSON library is required for this command. "..json
142   end
143
144   print(json.encode(value))
145   return true
146end
147
148local function print_entry(var, tbl, is_json)
149   return traverse_varstring(var, tbl, function(t, k)
150      if not t[k] then
151         return nil, "Unknown entry " .. k
152      end
153      local val = t[k]
154
155      if not should_skip(var, val) then
156         if is_json then
157            return print_json(val)
158         elseif type(val) == "string" then
159            print(val)
160         else
161            persist.write_value(io.stdout, val)
162         end
163      end
164      return true
165   end)
166end
167
168local function infer_type(var)
169   local typ
170   traverse_varstring(var, cfg, function(t, k)
171      if t[k] ~= nil then
172         typ = type(t[k])
173      end
174   end)
175   return typ
176end
177
178local function write_entries(keys, scope, do_unset)
179   if scope == "project" and not cfg.config_files.project then
180      return nil, "Current directory is not part of a project. You may want to run `luarocks init`."
181   end
182
183   local tbl, err = persist.load_config_file_if_basic(cfg.config_files[scope].file, cfg)
184   if not tbl then
185      return nil, err
186   end
187
188   for var, val in util.sortedpairs(keys) do
189      traverse_varstring(var, tbl, function(t, k)
190         if do_unset then
191            t[k] = nil
192         else
193            local typ = infer_type(var)
194            local v
195            if typ == "number" and tonumber(val) then
196               v = tonumber(val)
197            elseif typ == "boolean" and val == "true" then
198               v = true
199            elseif typ == "boolean" and val == "false" then
200               v = false
201            else
202               v = val
203            end
204            t[k] = v
205            keys[var] = v
206         end
207         return true
208      end, function(p, k)
209         p[k] = {}
210      end)
211   end
212
213   local ok, err = persist.save_from_table(cfg.config_files[scope].file, tbl)
214   if ok then
215      print(do_unset and "Removed" or "Wrote")
216      for var, val in util.sortedpairs(keys) do
217         if do_unset then
218            print(("\t%s"):format(var))
219         else
220            print(("\t%s = %q"):format(var, val))
221         end
222      end
223      print(do_unset and "from" or "to")
224      print("\t" .. cfg.config_files[scope].file)
225      return true
226   else
227      return nil, err
228   end
229end
230
231local function get_scope(args)
232   return args.scope
233          or (args["local"] and "user")
234          or (args.project_tree and "project")
235          or (cfg.local_by_default and "user")
236          or (fs.is_writable(cfg.config_files["system"].file and "system"))
237          or "user"
238end
239
240--- Driver function for "config" command.
241-- @return boolean: True if succeeded, nil on errors.
242function config_cmd.command(args)
243   deps.check_lua_incdir(cfg.variables, args.lua_version or cfg.lua_version)
244   deps.check_lua_libdir(cfg.variables, args.lua_version or cfg.lua_version)
245
246   -- deprecated flags
247   if args.lua_incdir then
248      print(cfg.variables.LUA_INCDIR)
249      return true
250   end
251   if args.lua_libdir then
252      print(cfg.variables.LUA_LIBDIR)
253      return true
254   end
255   if args.lua_ver then
256      print(cfg.lua_version)
257      return true
258   end
259   if args.system_config then
260      return config_file(cfg.config_files.system)
261   end
262   if args.user_config then
263      return config_file(cfg.config_files.user)
264   end
265   if args.rock_trees then
266      for _, tree in ipairs(cfg.rocks_trees) do
267      	if type(tree) == "string" then
268      	   util.printout(dir.normalize(tree))
269      	else
270      	   local name = tree.name and "\t"..tree.name or ""
271      	   util.printout(dir.normalize(tree.root)..name)
272      	end
273      end
274      return true
275   end
276
277   if args.key == "lua_version" and args.value then
278      local scope = get_scope(args)
279      if scope == "project" and not cfg.config_files.project then
280         return nil, "Current directory is not part of a project. You may want to run `luarocks init`."
281      end
282
283      local prefix = dir.dir_name(cfg.config_files[scope].file)
284      local ok, err = persist.save_default_lua_version(prefix, args.value)
285      if not ok then
286         return nil, "could not set default Lua version: " .. err
287      end
288      print("Lua version will default to " .. args.value .. " in " .. prefix)
289   end
290
291   if args.key == "lua_dir" and args.value then
292      local scope = get_scope(args)
293      local keys = {
294         ["variables.LUA_DIR"] = cfg.variables.LUA_DIR,
295         ["variables.LUA_BINDIR"] = cfg.variables.LUA_BINDIR,
296         ["variables.LUA_INCDIR"] = cfg.variables.LUA_INCDIR,
297         ["variables.LUA_LIBDIR"] = cfg.variables.LUA_LIBDIR,
298         ["lua_interpreter"] = cfg.lua_interpreter,
299      }
300      if args.lua_version then
301         local prefix = dir.dir_name(cfg.config_files[scope].file)
302         persist.save_default_lua_version(prefix, args.lua_version)
303      end
304      return write_entries(keys, scope, args.unset)
305   end
306
307   if args.key then
308      if args.value or args.unset then
309         local scope = get_scope(args)
310         return write_entries({ [args.key] = args.value or args.unset }, scope, args.unset)
311      else
312         return print_entry(args.key, cfg, args.json)
313      end
314   end
315
316   local cleancfg = cleanup(cfg)
317
318   if args.json then
319      return print_json(cleancfg)
320   else
321      print(persist.save_from_table_to_string(cleancfg))
322      return true
323   end
324end
325
326return config_cmd
327