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