source: trunk/lua/cli.lua @ 55

Revision 55, 11.6 KB checked in by reyalp, 2 years ago (diff)

improve error checking for upload/download arguments, use clean up path handling for downloaddir and deletefiles

  • Property svn:eol-style set to native
Line 
1--[[
2 Copyright (C) 2010-2011 <reyalp (at) gmail dot com>
3
4  This program is free software; you can redistribute it and/or modify
5  it under the terms of the GNU General Public License version 2 as
6  published by the Free Software Foundation.
7
8  This program is distributed in the hope that it will be useful,
9  but WITHOUT ANY WARRANTY; without even the implied warranty of
10  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11  GNU General Public License for more details.
12
13  You should have received a copy of the GNU General Public License
14  along with this program; if not, write to the Free Software
15  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16--]]
17
18local cli = {
19        cmds={},
20        names={},
21        finished = false,
22}
23
24cli.cmd_proto = {
25        get_help = function(self)
26                local namestr = self.names[1]
27                if #self.names > 1 then
28                        namestr = namestr .. " (" .. self.names[2]
29                        for i=3,#self.names do
30                                namestr = namestr .. "," .. self.names[i]
31                        end
32                        namestr = namestr .. ")"
33                end
34                return string.format("%-12s %-12s: - %s\n",namestr,self.arghelp,self.help)
35        end,
36}
37
38cli.cmd_meta = {
39        __index = function(cmd, key)
40                return cli.cmd_proto[key]
41        end,
42        __call = function(cmd,...)
43                return cmd:func(...)
44        end,
45}
46
47function cli:add_commands(cmds)
48        for i = 1, #cmds do
49                cmd = cmds[i]
50                table.insert(self.cmds,cmd)
51                if not cmd.arghelp then
52                        cmd.arghelp = ''
53                end
54                for _,name in ipairs(cmd.names) do
55                        if self.names[name] then
56                                warnf("duplicate command name %s\n",name)
57                        else
58                                self.names[name] = cmd
59                        end
60                end
61                setmetatable(cmd,cli.cmd_meta)
62        end
63end
64
65function cli:prompt()
66        if chdk.is_connected() then
67                local script_id = chdk.get_script_id()
68                if script_id then
69                        printf("con %d> ",script_id)
70                else
71                        printf("con> ")
72                end
73        else
74                printf("___> ")
75        end
76end
77
78-- execute command given by a single line
79-- returns status,message
80-- message is an error message or printable value
81function cli:execute(line)
82        -- single char shortcuts
83        local s,e,cmd = string.find(line,'^[%c%s]*([!?.#=])[%c%s]*')
84        if not cmd then
85                s,e,cmd = string.find(line,'^[%c%s]*([%w_]+)[%c%s]*')
86        end
87        if s then
88                local args = string.sub(line,e+1)
89                if self.names[cmd] then
90                        local status,msg = self.names[cmd](args)
91                        if not status and not msg then
92                                msg=cmd .. " failed"
93                        end
94                        return status,msg
95                else
96                        return false,string.format("unknown command '%s'\n",cmd)
97                end
98        elseif string.find(line,'[^%c%s]') then
99                return false, string.format("bad input '%s'\n",line)
100        end
101        -- blank input is OK
102        return true,""
103end
104
105function cli:run()
106        self:prompt()
107        for line in io.lines() do
108                local status,msg = self:execute(line)
109                if not status then
110                        errf("%s\n",tostring(msg))
111                elseif msg and string.len(msg) ~= 0 then
112                        printf("%s",msg)
113                        if string.sub(msg,-1,-1) ~= '\n' then
114                                printf("\n")
115                        end
116                end
117                if self.finished then
118                        break
119                end
120                self:prompt()
121        end
122end
123
124-- add/correct A/ as needed, replace \ with /
125function cli:make_camera_path(path)
126        if not path then
127                return 'A/'
128        end
129        -- fix slashes
130        path = string.gsub(path,'\\','/')
131        local pfx = string.sub(path,1,2)
132        if pfx == 'A/' then
133                return path
134        end
135        if pfx == 'a/' then
136                return 'A' .. string.sub(path,2,-1)
137        end
138        return 'A/' .. path
139end
140
141-- returns <str>, <remaining arg string>
142-- accepts quoted strings or space delimited
143function cli:get_string_arg(arg)
144        if type(arg) ~= 'string' then
145                return
146        end
147        local path
148        -- trim leading spaces
149        local s, e = string.find(arg,'^[%c%s]*')
150        arg = string.sub(arg,e+1)
151        -- check for quotes
152        s, e, str = string.find(arg,'^["]([^"]+)["]')
153        if s then
154                return str, string.sub(arg,e+1)
155        end
156        -- try without quotes
157        s, e, str = string.find(arg,'^([^%c%s]+)')
158        if s then
159                return str, string.sub(arg,e+1)
160        end
161        return nil
162end
163
164--[[
165t,args=cli:get_opts(args,optspec)
166optspect is an array of option letters
167returns table of option values
168plus arg string with recognized opts removed
169TODO should unify command line processing with main.lua args
170]]
171function cli:get_opts(arg,optspec)
172        local r={}
173        for i,v in ipairs(optspec) do
174                arg = string.gsub(arg,'-'..v,function()
175                        r[v]=true
176                        return ''
177                end)
178        end
179        return r,arg
180end
181
182-- returns num, <remaining arg string>
183-- num can be signed hex or decimal
184function cli:get_num_arg(arg)
185        if type(arg) ~= 'string' then
186                return
187        end
188        local hex,num
189        local s, e, neg=string.find(arg,'^[%c%s]*(-?)')
190        if not s then
191                neg = ''
192        end
193        arg = string.sub(arg,e+1)
194        s, e, hex=string.find(arg,'^(0[Xx])')
195        if s then
196                arg = string.sub(arg,e+1)
197                s, e, num=string.find(arg,'^([%x]+)')
198        else
199                hex = ''
200                s, e, num=string.find(arg,'^([%d]+)')
201        end
202        if s then
203                return tonumber(neg..hex..num), string.sub(arg,e+1)
204        end
205end
206
207cli:add_commands{
208        {
209                names={'help','h','?'},
210                arghelp = '[command]';
211                help='help on [command] or all commands',
212                func=function(self,args)
213                        if cli.names[args] then
214                                return true, cli.names[args]:get_help()
215                        end
216                        if args and args ~= "" then
217                                return false, string.format("unknown command '%s'\n",args)
218                        end
219                        msg = ""
220                        for i,c in ipairs(cli.cmds) do
221                                msg = msg .. c:get_help()
222                        end
223                        return true, msg
224                end,
225        },
226        {
227                names={'#'},
228                help='comment',
229                func=function(self,args)
230                        return true
231                end,
232        },
233        {
234                names={'exec','!'},
235                help='execute local lua',
236                arghelp='<lua code>',
237                func=function(self,args)
238                        local f,r = loadstring(args)
239                        if f then
240                                r={pcall(f)};
241                                if not r[1] then
242                                        return false, string.format("call failed:%s\n",r[2])
243                                end
244                                local s
245                                if #r > 1 then
246                                        s='=' .. serialize(r[2],{pretty=true,err_type=false,err_cycle=false})
247                                        for i = 3, #r do
248                                                s = s .. ',' .. serialize(r[i],{pretty=true,err_type=false,err_cycle=false})
249                                        end
250                                end
251                                return true, s
252                        else
253                                return false, string.format("compile failed:%s\n",r)
254                        end
255                end,
256        },
257        {
258                names={'quit','q'},
259                help='quit program',
260                func=function()
261                        cli.finished = true
262                        return true,"bye"
263                end,
264        },
265        {
266                names={'lua','l','.'},
267                help='execute remote lua',
268                arghelp='<lua code>',
269                func=function(self,args)
270                        return chdku.exec(args)
271                end,
272        },
273        {
274                names={'getm'},
275                help='get messages',
276                func=function(self,args)
277                        local msgs=''
278                        local msg,err
279                        while true do
280                                msg,err=chdk.read_msg()
281                                if type(msg) ~= 'table' then
282                                        return false,msgs..err
283                                end
284                                if msg.type == 'none' then
285                                        return true,msgs
286                                end
287                                msgs = msgs .. chdku.format_script_msg(msg) .. "\n"
288                        end
289                end,
290        },
291        {
292                names={'putm'},
293                help='send message',
294                arghelp='<msg string>',
295                func=function(self,args)
296                        return chdk.write_msg(args)
297                end,
298        },
299        {
300                names={'luar','='},
301                help='execute remote lua, wait for result',
302                arghelp='<lua code>',
303                func=function(self,args)
304                        local rets={}
305                        local msgs={}
306                        local status,err = chdku.execwait(args,{rets=rets,msgs=msgs})
307                        if not status then
308                                return false,err
309                        end
310                        local r=''
311                        for i=1,#msgs do
312                                r=r .. chdku.format_script_msg(msgs[i]) .. '\n'
313                        end
314                        for i=1,table.maxn(rets) do
315                                r=r .. chdku.format_script_msg(rets[i]) .. '\n'
316                        end
317                        return true,r
318                end,
319        },
320        {
321                -- TODO support display as words
322                names={'rmem'},
323                help=' read memory',
324                arghelp='<address> [count]',
325                func=function(self,args)
326                        local addr
327                        addr,args = cli:get_num_arg(args)
328                        local count = cli:get_num_arg(args)
329                        if not addr then
330                                return false, "bad args"
331                        end
332                        if not count then
333                                count = 1
334                        end
335                        printf("0x%x %u\n",addr,count)
336                        r,msg = chdk.getmem(addr,count)
337                        if not r then
338                                return false,msg
339                        end
340                        return true,hexdump(r,addr)
341                end,
342        },
343        {
344                names={'list'},
345                help='list devices',
346                func=function()
347                        if chdk.is_connected() then
348                                return false,"cannot yet list while connected :("
349                        end
350                        local msg = ''
351                        for num,d in ipairs(chdk.list_devices()) do
352                                msg = msg .. string.format("%d: %s %s/%s vendor:%x pid:%x\n",num,d.model,d.bus,d.dev,d.vendor_id,d.product_id)
353                        end
354                        return true,msg
355                end,
356        },
357        {
358                names={'upload','u'},
359                help='upload a file to the camera',
360                arghelp="<local> [remote]",
361                func=function(self,args)
362                        local src,args = cli:get_string_arg(args)
363                        if not src then
364                                return false, "missing source"
365                        end
366                        local dst = cli:get_string_arg(args)
367                        -- no dst, use filename of source
368                        if not dst then
369                                dst = util.basename(src)
370                        -- trailing slash, append filename of source
371                        elseif string.find(dst,'[\\/]$') then
372                                dst = dst .. util.basename(src)
373                        end
374                        if not (src and dst) then
375                                return false, "bad/missing args ?"
376                        end
377                        dst = cli:make_camera_path(dst)
378                        local msg=string.format("%s->%s\n",src,dst)
379                        local r, msg2 = chdk.upload(src,dst)
380                        if msg2 then
381                                msg = msg .. msg2
382                        end
383                        return r, msg
384                end,
385        },
386        {
387                names={'download','d'},
388                help='download a file from the camera',
389                arghelp="<remote> [local]",
390                func=function(self,args)
391                        local src,args = cli:get_string_arg(args)
392                        if not src then
393                                return false, "missing source"
394                        end
395                        local dst = cli:get_string_arg(args)
396                        -- use final component
397                        if not dst then
398                                dst = util.basename(src)
399                        -- trailing slash, append filename of source
400                        -- TODO should use stat to figure out if target is a directory
401                        elseif string.find(dst,'[\\/]$') then
402                                dst = dst .. util.basename(src)
403                        end
404                        if not dst then
405                                return false, "bad/missing args ?"
406                        end
407                        src = cli:make_camera_path(src)
408                        local msg=string.format("%s->%s\n",src,dst)
409                        local r, msg2 = chdk.download(src,dst)
410                        if msg2 then
411                                msg = msg .. msg2
412                        end
413                        return r, msg
414                end,
415        },
416        {
417                names={'version','ver'},
418                help='print API versions',
419                func=function(self,args)
420                        local host_ver = string.format("host:%d.%d cam:",chdk.host_api_version())
421                        if chdk.is_connected() then
422                                cam_major, cam_minor = chdk.camera_api_version()
423                                if not cam_major then
424                                        return false, host_ver .. string.format("error %s",cam_minor)
425                                end
426                                return true, host_ver .. string.format("%d.%d",cam_major,cam_minor)
427                        else
428                                return true, host_ver .. "not connected"
429                        end
430                end,
431        },
432        {
433                names={'connect','c'},
434                help='(re)connect to device',
435                -- TODO support device selection
436                func=function(self,args)
437                        return chdk.connect()
438                end,
439        },
440        {
441                names={'disconnect','dis'},
442                help='disconnect from device',
443                func=function(self,args)
444                        return chdk.disconnect()
445                end,
446        },
447        {
448                names={'ls'},
449                help='list files/directories on camera',
450                arghelp="[-l] [path]",
451                func=function(self,args)
452                        local opts,listops
453                        opts,args=cli:get_opts(args,{'l'})
454                        local path=cli:get_string_arg(args)
455                        path = cli:make_camera_path(path)
456                        if opts.l then
457                                listopts = { stat='*' }
458                        else
459                                listopts = { stat='/' }
460                        end
461                        local list,msg = chdku.listdir(path,listopts)
462                        if type(list) == 'table' then
463                                local r = ''
464                                if opts.l then
465                                        -- alphabetic sort TODO sorting/grouping options
466                                        chdku.sortdir_stat(list)
467                                        for i,st in ipairs(list) do
468                                                if st.is_dir then
469                                                        r = r .. string.format("%s/\n",st.name)
470                                                else
471                                                        r = r .. string.format("%-13s %10d\n",st.name,st.size)
472                                                end
473                                        end
474                                else
475                                        table.sort(list)
476                                        for i,name in ipairs(list) do
477                                                r = r .. name .. '\n'
478                                        end
479                                end
480
481                                return true,r
482                        end
483                        return false,msg
484                end,
485        },
486        {
487                names={'reboot'},
488                help='reboot the camera',
489                arghelp="[file]",
490                func=function(self,args)
491                        local bootfile=cli:get_string_arg(args)
492                        if bootfile then
493                                bootfile = cli:make_camera_path(bootfile)
494                                bootfile = string.format("'%s'",bootfile)
495                        else
496                                bootfile = ''
497                        end
498                        -- sleep and disconnect to avoid later connection problems on some cameras
499                        -- clobber because we don't care about memory leaks
500                        local status,err=chdku.exec('sleep(1000);reboot('..bootfile..')',{clobber=true})
501                        if not status then
502                                return false,err
503                        end
504                        chdk.disconnect()
505                        -- sleep locally to avoid clobbering the reboot, and allow time for the camera to come up before trying to connect
506                        sys.sleep(3000)
507                       
508                        return chdk.connect()
509                end,
510        },
511};
512
513return cli;
Note: See TracBrowser for help on using the repository browser.