Minor fixes
[project/luci.git] / libs / sys / luasrc / sys.lua
1 --[[
2 LuCI - System library
3
4 Description:
5 Utilities for interaction with the Linux system
6
7 FileId:
8 $Id$
9
10 License:
11 Copyright 2008 Steven Barth <steven@midlink.org>
12
13 Licensed under the Apache License, Version 2.0 (the "License");
14 you may not use this file except in compliance with the License.
15 You may obtain a copy of the License at
16
17 http://www.apache.org/licenses/LICENSE-2.0
18
19 Unless required by applicable law or agreed to in writing, software
20 distributed under the License is distributed on an "AS IS" BASIS,
21 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
22 See the License for the specific language governing permissions and
23 limitations under the License.
24
25 ]]--
26
27
28 local io = require "io"
29 local os = require "os"
30 local table = require "table"
31 local nixio = require "nixio"
32 local fs = require "nixio.fs"
33 local iwinfo = require "iwinfo"
34
35 local luci = {}
36 luci.util = require "luci.util"
37 luci.ip = require "luci.ip"
38
39 local tonumber, ipairs, pairs, pcall, type, next, setmetatable =
40 tonumber, ipairs, pairs, pcall, type, next, setmetatable
41
42
43 --- LuCI Linux and POSIX system utilities.
44 module "luci.sys"
45
46 --- Execute a given shell command and return the error code
47 -- @class function
48 -- @name call
49 -- @param ... Command to call
50 -- @return Error code of the command
51 function call(...)
52 return os.execute(...) / 256
53 end
54
55 --- Execute a given shell command and capture its standard output
56 -- @class function
57 -- @name exec
58 -- @param command Command to call
59 -- @return String containg the return the output of the command
60 exec = luci.util.exec
61
62 --- Invoke the luci-flash executable to write an image to the flash memory.
63 -- @param image Local path or URL to image file
64 -- @param kpattern Pattern of files to keep over flash process
65 -- @return Return value of os.execute()
66 function flash(image, kpattern)
67 local cmd = "luci-flash "
68 if kpattern then
69 cmd = cmd .. "-k '" .. kpattern:gsub("'", "") .. "' "
70 end
71 cmd = cmd .. "'" .. image:gsub("'", "") .. "' >/dev/null 2>&1"
72
73 return os.execute(cmd)
74 end
75
76 --- Retrieve information about currently mounted file systems.
77 -- @return Table containing mount information
78 function mounts()
79 local data = {}
80 local k = {"fs", "blocks", "used", "available", "percent", "mountpoint"}
81 local ps = luci.util.execi("df")
82
83 if not ps then
84 return
85 else
86 ps()
87 end
88
89 for line in ps do
90 local row = {}
91
92 local j = 1
93 for value in line:gmatch("[^%s]+") do
94 row[k[j]] = value
95 j = j + 1
96 end
97
98 if row[k[1]] then
99
100 -- this is a rather ugly workaround to cope with wrapped lines in
101 -- the df output:
102 --
103 -- /dev/scsi/host0/bus0/target0/lun0/part3
104 -- 114382024 93566472 15005244 86% /mnt/usb
105 --
106
107 if not row[k[2]] then
108 j = 2
109 line = ps()
110 for value in line:gmatch("[^%s]+") do
111 row[k[j]] = value
112 j = j + 1
113 end
114 end
115
116 table.insert(data, row)
117 end
118 end
119
120 return data
121 end
122
123 --- Retrieve environment variables. If no variable is given then a table
124 -- containing the whole environment is returned otherwise this function returns
125 -- the corresponding string value for the given name or nil if no such variable
126 -- exists.
127 -- @class function
128 -- @name getenv
129 -- @param var Name of the environment variable to retrieve (optional)
130 -- @return String containg the value of the specified variable
131 -- @return Table containing all variables if no variable name is given
132 getenv = nixio.getenv
133
134 --- Get or set the current hostname.
135 -- @param String containing a new hostname to set (optional)
136 -- @return String containing the system hostname
137 function hostname(newname)
138 if type(newname) == "string" and #newname > 0 then
139 fs.writefile( "/proc/sys/kernel/hostname", newname )
140 return newname
141 else
142 return nixio.uname().nodename
143 end
144 end
145
146 --- Returns the contents of a documented referred by an URL.
147 -- @param url The URL to retrieve
148 -- @param stream Return a stream instead of a buffer
149 -- @param target Directly write to target file name
150 -- @return String containing the contents of given the URL
151 function httpget(url, stream, target)
152 if not target then
153 local source = stream and io.popen or luci.util.exec
154 return source("wget -qO- '"..url:gsub("'", "").."'")
155 else
156 return os.execute("wget -qO '%s' '%s'" %
157 {target:gsub("'", ""), url:gsub("'", "")})
158 end
159 end
160
161 --- Returns the system load average values.
162 -- @return String containing the average load value 1 minute ago
163 -- @return String containing the average load value 5 minutes ago
164 -- @return String containing the average load value 15 minutes ago
165 function loadavg()
166 local info = nixio.sysinfo()
167 return info.loads[1], info.loads[2], info.loads[3]
168 end
169
170 --- Initiate a system reboot.
171 -- @return Return value of os.execute()
172 function reboot()
173 return os.execute("reboot >/dev/null 2>&1")
174 end
175
176 --- Returns the system type, cpu name and installed physical memory.
177 -- @return String containing the system or platform identifier
178 -- @return String containing hardware model information
179 -- @return String containing the total memory amount in kB
180 -- @return String containing the memory used for caching in kB
181 -- @return String containing the memory used for buffering in kB
182 -- @return String containing the free memory amount in kB
183 function sysinfo()
184 local cpuinfo = fs.readfile("/proc/cpuinfo")
185 local meminfo = fs.readfile("/proc/meminfo")
186
187 local system = cpuinfo:match("system typ.-:%s*([^\n]+)")
188 local model = ""
189 local memtotal = tonumber(meminfo:match("MemTotal:%s*(%d+)"))
190 local memcached = tonumber(meminfo:match("\nCached:%s*(%d+)"))
191 local memfree = tonumber(meminfo:match("MemFree:%s*(%d+)"))
192 local membuffers = tonumber(meminfo:match("Buffers:%s*(%d+)"))
193
194 if not system then
195 system = nixio.uname().machine
196 model = cpuinfo:match("model name.-:%s*([^\n]+)")
197 if not model then
198 model = cpuinfo:match("Processor.-:%s*([^\n]+)")
199 end
200 else
201 model = cpuinfo:match("cpu model.-:%s*([^\n]+)")
202 end
203
204 return system, model, memtotal, memcached, membuffers, memfree
205 end
206
207 --- Retrieves the output of the "logread" command.
208 -- @return String containing the current log buffer
209 function syslog()
210 return luci.util.exec("logread")
211 end
212
213 --- Retrieves the output of the "dmesg" command.
214 -- @return String containing the current log buffer
215 function dmesg()
216 return luci.util.exec("dmesg")
217 end
218
219 --- Generates a random id with specified length.
220 -- @param bytes Number of bytes for the unique id
221 -- @return String containing hex encoded id
222 function uniqueid(bytes)
223 local rand = fs.readfile("/dev/urandom", bytes)
224 return rand and nixio.bin.hexlify(rand)
225 end
226
227 --- Returns the current system uptime stats.
228 -- @return String containing total uptime in seconds
229 function uptime()
230 return nixio.sysinfo().uptime
231 end
232
233
234 --- LuCI system utilities / network related functions.
235 -- @class module
236 -- @name luci.sys.net
237 net = {}
238
239 --- Returns the current arp-table entries as two-dimensional table.
240 -- @return Table of table containing the current arp entries.
241 -- The following fields are defined for arp entry objects:
242 -- { "IP address", "HW address", "HW type", "Flags", "Mask", "Device" }
243 function net.arptable(callback)
244 return _parse_delimited_table(io.lines("/proc/net/arp"), "%s%s+", callback)
245 end
246
247 --- Returns conntrack information
248 -- @return Table with the currently tracked IP connections
249 function net.conntrack(callback)
250 local connt = {}
251 if fs.access("/proc/net/nf_conntrack", "r") then
252 for line in io.lines("/proc/net/nf_conntrack") do
253 line = line:match "^(.-( [^ =]+=).-)%2"
254 local entry, flags = _parse_mixed_record(line, " +")
255 entry.layer3 = flags[1]
256 entry.layer4 = flags[3]
257 for i=1, #entry do
258 entry[i] = nil
259 end
260
261 if callback then
262 callback(entry)
263 else
264 connt[#connt+1] = entry
265 end
266 end
267 elseif fs.access("/proc/net/ip_conntrack", "r") then
268 for line in io.lines("/proc/net/ip_conntrack") do
269 line = line:match "^(.-( [^ =]+=).-)%2"
270 local entry, flags = _parse_mixed_record(line, " +")
271 entry.layer3 = "ipv4"
272 entry.layer4 = flags[1]
273 for i=1, #entry do
274 entry[i] = nil
275 end
276
277 if callback then
278 callback(entry)
279 else
280 connt[#connt+1] = entry
281 end
282 end
283 else
284 return nil
285 end
286 return connt
287 end
288
289 --- Determine the current IPv4 default route. If multiple default routes exist,
290 -- return the one with the lowest metric.
291 -- @return Table with the properties of the current default route.
292 -- The following fields are defined:
293 -- { "dest", "gateway", "metric", "refcount", "usecount", "irtt",
294 -- "flags", "device" }
295 function net.defaultroute()
296 local route
297
298 net.routes(function(rt)
299 if rt.dest:prefix() == 0 and (not route or route.metric > rt.metric) then
300 route = rt
301 end
302 end)
303
304 return route
305 end
306
307 --- Determine the current IPv6 default route. If multiple default routes exist,
308 -- return the one with the lowest metric.
309 -- @return Table with the properties of the current default route.
310 -- The following fields are defined:
311 -- { "source", "dest", "nexthop", "metric", "refcount", "usecount",
312 -- "flags", "device" }
313 function net.defaultroute6()
314 local route
315
316 net.routes6(function(rt)
317 if rt.dest:prefix() == 0 and (not route or route.metric > rt.metric) then
318 route = rt
319 end
320 end)
321
322 return route
323 end
324
325 --- Determine the names of available network interfaces.
326 -- @return Table containing all current interface names
327 function net.devices()
328 local devs = {}
329 for k, v in ipairs(nixio.getifaddrs()) do
330 if v.family == "packet" then
331 devs[#devs+1] = v.name
332 end
333 end
334 return devs
335 end
336
337
338 --- Return information about available network interfaces.
339 -- @return Table containing all current interface names and their information
340 function net.deviceinfo()
341 local devs = {}
342 for k, v in ipairs(nixio.getifaddrs()) do
343 if v.family == "packet" then
344 local d = v.data
345 d[1] = d.rx_bytes
346 d[2] = d.rx_packets
347 d[3] = d.rx_errors
348 d[4] = d.rx_dropped
349 d[5] = 0
350 d[6] = 0
351 d[7] = 0
352 d[8] = d.multicast
353 d[9] = d.tx_bytes
354 d[10] = d.tx_packets
355 d[11] = d.tx_errors
356 d[12] = d.tx_dropped
357 d[13] = 0
358 d[14] = d.collisions
359 d[15] = 0
360 d[16] = 0
361 devs[v.name] = d
362 end
363 end
364 return devs
365 end
366
367
368 -- Determine the MAC address belonging to the given IP address.
369 -- @param ip IPv4 address
370 -- @return String containing the MAC address or nil if it cannot be found
371 function net.ip4mac(ip)
372 local mac = nil
373 net.arptable(function(e)
374 if e["IP address"] == ip then
375 mac = e["HW address"]
376 end
377 end)
378 return mac
379 end
380
381 --- Returns the current kernel routing table entries.
382 -- @return Table of tables with properties of the corresponding routes.
383 -- The following fields are defined for route entry tables:
384 -- { "dest", "gateway", "metric", "refcount", "usecount", "irtt",
385 -- "flags", "device" }
386 function net.routes(callback)
387 local routes = { }
388
389 for line in io.lines("/proc/net/route") do
390
391 local dev, dst_ip, gateway, flags, refcnt, usecnt, metric,
392 dst_mask, mtu, win, irtt = line:match(
393 "([^%s]+)\t([A-F0-9]+)\t([A-F0-9]+)\t([A-F0-9]+)\t" ..
394 "(%d+)\t(%d+)\t(%d+)\t([A-F0-9]+)\t(%d+)\t(%d+)\t(%d+)"
395 )
396
397 if dev then
398 gateway = luci.ip.Hex( gateway, 32, luci.ip.FAMILY_INET4 )
399 dst_mask = luci.ip.Hex( dst_mask, 32, luci.ip.FAMILY_INET4 )
400 dst_ip = luci.ip.Hex(
401 dst_ip, dst_mask:prefix(dst_mask), luci.ip.FAMILY_INET4
402 )
403
404 local rt = {
405 dest = dst_ip,
406 gateway = gateway,
407 metric = tonumber(metric),
408 refcount = tonumber(refcnt),
409 usecount = tonumber(usecnt),
410 mtu = tonumber(mtu),
411 window = tonumber(window),
412 irtt = tonumber(irtt),
413 flags = tonumber(flags, 16),
414 device = dev
415 }
416
417 if callback then
418 callback(rt)
419 else
420 routes[#routes+1] = rt
421 end
422 end
423 end
424
425 return routes
426 end
427
428 --- Returns the current ipv6 kernel routing table entries.
429 -- @return Table of tables with properties of the corresponding routes.
430 -- The following fields are defined for route entry tables:
431 -- { "source", "dest", "nexthop", "metric", "refcount", "usecount",
432 -- "flags", "device" }
433 function net.routes6(callback)
434 if fs.access("/proc/net/ipv6_route", "r") then
435 local routes = { }
436
437 for line in io.lines("/proc/net/ipv6_route") do
438
439 local dst_ip, dst_prefix, src_ip, src_prefix, nexthop,
440 metric, refcnt, usecnt, flags, dev = line:match(
441 "([a-f0-9]+) ([a-f0-9]+) " ..
442 "([a-f0-9]+) ([a-f0-9]+) " ..
443 "([a-f0-9]+) ([a-f0-9]+) " ..
444 "([a-f0-9]+) ([a-f0-9]+) " ..
445 "([a-f0-9]+) +([^%s]+)"
446 )
447
448 src_ip = luci.ip.Hex(
449 src_ip, tonumber(src_prefix, 16), luci.ip.FAMILY_INET6, false
450 )
451
452 dst_ip = luci.ip.Hex(
453 dst_ip, tonumber(dst_prefix, 16), luci.ip.FAMILY_INET6, false
454 )
455
456 nexthop = luci.ip.Hex( nexthop, 128, luci.ip.FAMILY_INET6, false )
457
458 local rt = {
459 source = src_ip,
460 dest = dst_ip,
461 nexthop = nexthop,
462 metric = tonumber(metric, 16),
463 refcount = tonumber(refcnt, 16),
464 usecount = tonumber(usecnt, 16),
465 flags = tonumber(flags, 16),
466 device = dev
467 }
468
469 if callback then
470 callback(rt)
471 else
472 routes[#routes+1] = rt
473 end
474 end
475
476 return routes
477 end
478 end
479
480 --- Tests whether the given host responds to ping probes.
481 -- @param host String containing a hostname or IPv4 address
482 -- @return Number containing 0 on success and >= 1 on error
483 function net.pingtest(host)
484 return os.execute("ping -c1 '"..host:gsub("'", '').."' >/dev/null 2>&1")
485 end
486
487
488 --- LuCI system utilities / process related functions.
489 -- @class module
490 -- @name luci.sys.process
491 process = {}
492
493 --- Get the current process id.
494 -- @class function
495 -- @name process.info
496 -- @return Number containing the current pid
497 function process.info(key)
498 local s = {uid = nixio.getuid(), gid = nixio.getgid()}
499 return not key and s or s[key]
500 end
501
502 --- Retrieve information about currently running processes.
503 -- @return Table containing process information
504 function process.list()
505 local data = {}
506 local k
507 local ps = luci.util.execi("top -bn1")
508
509 if not ps then
510 return
511 end
512
513 while true do
514 local line = ps()
515 if not line then
516 return
517 end
518
519 k = luci.util.split(luci.util.trim(line), "%s+", nil, true)
520 if k[1] == "PID" then
521 break
522 end
523 end
524
525 for line in ps do
526 local row = {}
527
528 line = luci.util.trim(line)
529 for i, value in ipairs(luci.util.split(line, "%s+", #k-1, true)) do
530 row[k[i]] = value
531 end
532
533 local pid = tonumber(row[k[1]])
534 if pid then
535 data[pid] = row
536 end
537 end
538
539 return data
540 end
541
542 --- Set the gid of a process identified by given pid.
543 -- @param gid Number containing the Unix group id
544 -- @return Boolean indicating successful operation
545 -- @return String containing the error message if failed
546 -- @return Number containing the error code if failed
547 function process.setgroup(gid)
548 return nixio.setgid(gid)
549 end
550
551 --- Set the uid of a process identified by given pid.
552 -- @param uid Number containing the Unix user id
553 -- @return Boolean indicating successful operation
554 -- @return String containing the error message if failed
555 -- @return Number containing the error code if failed
556 function process.setuser(uid)
557 return nixio.setuid(uid)
558 end
559
560 --- Send a signal to a process identified by given pid.
561 -- @class function
562 -- @name process.signal
563 -- @param pid Number containing the process id
564 -- @param sig Signal to send (default: 15 [SIGTERM])
565 -- @return Boolean indicating successful operation
566 -- @return Number containing the error code if failed
567 process.signal = nixio.kill
568
569
570 --- LuCI system utilities / user related functions.
571 -- @class module
572 -- @name luci.sys.user
573 user = {}
574
575 --- Retrieve user informations for given uid.
576 -- @class function
577 -- @name getuser
578 -- @param uid Number containing the Unix user id
579 -- @return Table containing the following fields:
580 -- { "uid", "gid", "name", "passwd", "dir", "shell", "gecos" }
581 user.getuser = nixio.getpw
582
583 --- Test whether given string matches the password of a given system user.
584 -- @param username String containing the Unix user name
585 -- @param pass String containing the password to compare
586 -- @return Boolean indicating wheather the passwords are equal
587 function user.checkpasswd(username, pass)
588 local pwe = nixio.getsp and nixio.getsp(username) or nixio.getpw(username)
589 local pwh = pwe and (pwe.pwdp or pwe.passwd)
590 if not pwh or #pwh < 1 or pwh ~= "!" and nixio.crypt(pass, pwh) ~= pwh then
591 return false
592 else
593 return true
594 end
595 end
596
597 --- Change the password of given user.
598 -- @param username String containing the Unix user name
599 -- @param password String containing the password to compare
600 -- @return Number containing 0 on success and >= 1 on error
601 function user.setpasswd(username, password)
602 if password then
603 password = password:gsub("'", "")
604 end
605
606 if username then
607 username = username:gsub("'", "")
608 end
609
610 local cmd = "(echo '"..password.."';sleep 1;echo '"..password.."')|"
611 cmd = cmd .. "passwd '"..username.."' >/dev/null 2>&1"
612 return os.execute(cmd)
613 end
614
615
616 --- LuCI system utilities / wifi related functions.
617 -- @class module
618 -- @name luci.sys.wifi
619 wifi = {}
620
621 --- Get wireless information for given interface.
622 -- @param ifname String containing the interface name
623 -- @return A wrapped iwinfo object instance
624 function wifi.getiwinfo(ifname)
625 local t = iwinfo.type(ifname)
626 if t then
627 local x = iwinfo[t]
628 return setmetatable({}, {
629 __index = function(t, k)
630 if x[k] then return x[k](ifname) end
631 end
632 })
633 end
634 end
635
636 --- Get iwconfig output for all wireless devices.
637 -- @return Table of tables containing the iwconfing output for each wifi device
638 function wifi.getiwconfig()
639 local cnt = luci.util.exec("PATH=/sbin:/usr/sbin iwconfig 2>/dev/null")
640 local iwc = {}
641
642 for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n\n")) do
643 local k = l:match("^(.-) ")
644 l = l:gsub("^(.-) +", "", 1)
645 if k then
646 local entry, flags = _parse_mixed_record(l)
647 if entry then
648 entry.flags = flags
649 end
650 iwc[k] = entry
651 end
652 end
653
654 return iwc
655 end
656
657 --- Get iwlist scan output from all wireless devices.
658 -- @return Table of tables contaiing all scan results
659 function wifi.iwscan(iface)
660 local siface = iface or ""
661 local cnt = luci.util.exec("iwlist "..siface.." scan 2>/dev/null")
662 local iws = {}
663
664 for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n\n")) do
665 local k = l:match("^(.-) ")
666 l = l:gsub("^[^\n]+", "", 1)
667 l = luci.util.trim(l)
668 if k then
669 iws[k] = {}
670 for j, c in pairs(luci.util.split(l, "\n Cell")) do
671 c = c:gsub("^(.-)- ", "", 1)
672 c = luci.util.split(c, "\n", 7)
673 c = table.concat(c, "\n", 1)
674 local entry, flags = _parse_mixed_record(c)
675 if entry then
676 entry.flags = flags
677 end
678 table.insert(iws[k], entry)
679 end
680 end
681 end
682
683 return iface and (iws[iface] or {}) or iws
684 end
685
686 --- Get available channels from given wireless iface.
687 -- @param iface Wireless interface (optional)
688 -- @return Table of available channels
689 function wifi.channels(iface)
690 local t = iwinfo.type(iface or "")
691 local cns
692 if t and iwinfo[t] then
693 cns = iwinfo[t].freqlist(iface)
694 end
695
696 if not cns or #cns == 0 then
697 cns = {
698 {channel = 1, mhz = 2412},
699 {channel = 2, mhz = 2417},
700 {channel = 3, mhz = 2422},
701 {channel = 4, mhz = 2427},
702 {channel = 5, mhz = 2432},
703 {channel = 6, mhz = 2437},
704 {channel = 7, mhz = 2442},
705 {channel = 8, mhz = 2447},
706 {channel = 9, mhz = 2452},
707 {channel = 10, mhz = 2457},
708 {channel = 11, mhz = 2462}
709 }
710 end
711
712 return cns
713 end
714
715
716 --- LuCI system utilities / init related functions.
717 -- @class module
718 -- @name luci.sys.init
719 init = {}
720 init.dir = "/etc/init.d/"
721
722 --- Get the names of all installed init scripts
723 -- @return Table containing the names of all inistalled init scripts
724 function init.names()
725 local names = { }
726 for name in fs.glob(init.dir.."*") do
727 names[#names+1] = fs.basename(name)
728 end
729 return names
730 end
731
732 --- Test whether the given init script is enabled
733 -- @param name Name of the init script
734 -- @return Boolean indicating whether init is enabled
735 function init.enabled(name)
736 if fs.access(init.dir..name) then
737 return ( call(init.dir..name.." enabled") == 0 )
738 end
739 return false
740 end
741
742 --- Get the index of he given init script
743 -- @param name Name of the init script
744 -- @return Numeric index value
745 function init.index(name)
746 if fs.access(init.dir..name) then
747 return call("source "..init.dir..name.."; exit $START")
748 end
749 end
750
751 --- Enable the given init script
752 -- @param name Name of the init script
753 -- @return Boolean indicating success
754 function init.enable(name)
755 if fs.access(init.dir..name) then
756 return ( call(init.dir..name.." enable") == 1 )
757 end
758 end
759
760 --- Disable the given init script
761 -- @param name Name of the init script
762 -- @return Boolean indicating success
763 function init.disable(name)
764 if fs.access(init.dir..name) then
765 return ( call(init.dir..name.." disable") == 0 )
766 end
767 end
768
769
770 -- Internal functions
771
772 function _parse_delimited_table(iter, delimiter, callback)
773 delimiter = delimiter or "%s+"
774
775 local data = {}
776 local trim = luci.util.trim
777 local split = luci.util.split
778
779 local keys = split(trim(iter()), delimiter, nil, true)
780 for i, j in pairs(keys) do
781 keys[i] = trim(keys[i])
782 end
783
784 for line in iter do
785 local row = {}
786 line = trim(line)
787 if #line > 0 then
788 for i, j in pairs(split(line, delimiter, nil, true)) do
789 if keys[i] then
790 row[keys[i]] = j
791 end
792 end
793 end
794
795 if callback then
796 callback(row)
797 else
798 data[#data+1] = row
799 end
800 end
801
802 return data
803 end
804
805 function _parse_mixed_record(cnt, delimiter)
806 delimiter = delimiter or " "
807 local data = {}
808 local flags = {}
809
810 for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n")) do
811 for j, f in pairs(luci.util.split(luci.util.trim(l), delimiter, nil, true)) do
812 local k, x, v = f:match('([^%s][^:=]*) *([:=]*) *"*([^\n"]*)"*')
813
814 if k then
815 if x == "" then
816 table.insert(flags, k)
817 else
818 data[k] = v
819 end
820 end
821 end
822 end
823
824 return data, flags
825 end