├── config.lua
├── readme.md
├── luaish.lua
└── lua.lua
/config.lua:
--------------------------------------------------------------------------------
1 | --- Reads configuration files into a Lua table.
2 | -- Understands INI files, classic Unix config files, and simple
3 | -- delimited columns of values.
4 | --
5 | -- # test.config
6 | -- # Read timeout in seconds
7 | -- read.timeout=10
8 | -- # Write timeout in seconds
9 | -- write.timeout=5
10 | -- #acceptable ports
11 | -- ports = 1002,1003,1004
12 | --
13 | -- -- readconfig.lua
14 | -- require 'pl'
15 | -- local t = config.read 'test.config'
16 | -- print(pretty.write(t))
17 | --
18 | -- ### output #####
19 | -- {
20 | -- ports = {
21 | -- 1002,
22 | -- 1003,
23 | -- 1004
24 | -- },
25 | -- write_timeout = 5,
26 | -- read_timeout = 10
27 | -- }
28 | --
29 | -- See the Guide for further @{06-data.md.Reading_Configuration_Files|discussion}
30 | --
31 | -- Dependencies: none
32 | -- @module pl.config
33 |
34 | local type,tonumber,ipairs,io, table = _G.type,_G.tonumber,_G.ipairs,_G.io,_G.table
35 |
36 | local function split(s,re)
37 | local res = {}
38 | local t_insert = table.insert
39 | re = '[^'..re..']+'
40 | for k in s:gmatch(re) do t_insert(res,k) end
41 | return res
42 | end
43 |
44 | local function strip(s)
45 | return s:gsub('^%s+',''):gsub('%s+$','')
46 | end
47 |
48 | local function strip_quotes (s)
49 | return s:gsub("['\"](.*)['\"]",'%1')
50 | end
51 |
52 | local config = {}
53 |
54 | --- like io.lines(), but allows for lines to be continued with '\'.
55 | -- @param file a file-like object (anything where read() returns the next line) or a filename.
56 | -- Defaults to stardard input.
57 | -- @return an iterator over the lines, or nil
58 | -- @return error 'not a file-like object' or 'file is nil'
59 | function config.lines(file)
60 | local f,openf,err
61 | local line = ''
62 | if type(file) == 'string' then
63 | f,err = io.open(file,'r')
64 | if not f then return nil,err end
65 | openf = true
66 | else
67 | f = file or io.stdin
68 | if not file.read then return nil, 'not a file-like object' end
69 | end
70 | if not f then return nil, 'file is nil' end
71 | return function()
72 | local l = f:read()
73 | while l do
74 | -- does the line end with '\'?
75 | local i = l:find '\\%s*$'
76 | if i then -- if so,
77 | line = line..l:sub(1,i-1)
78 | elseif line == '' then
79 | return l
80 | else
81 | l = line..l
82 | line = ''
83 | return l
84 | end
85 | l = f:read()
86 | end
87 | if openf then f:close() end
88 | end
89 | end
90 |
91 | --- read a configuration file into a table
92 | -- @param file either a file-like object or a string, which must be a filename
93 | -- @param cnfg a configuration table that may contain these fields:
94 | --
95 | -- - variablilize make names into valid Lua identifiers (default true)
96 | -- - convert_numbers try to convert values into numbers (default true)
97 | -- - trim_space ensure that there is no starting or trailing whitespace with values (default true)
98 | -- - trim_quotes remove quotes from strings (default false)
99 | -- - list_delim delimiter to use when separating columns (default ',')
100 | --
101 | -- @return a table containing items, or nil
102 | -- @return error message (same as @{config.lines}
103 | function config.read(file,cnfg)
104 | local f,openf,err
105 | cnfg = cnfg or {}
106 | local function check_cnfg (var,def)
107 | local val = cnfg[var]
108 | if val == nil then return def else return val end
109 | end
110 | local t = {}
111 | local top_t = t
112 | local variablilize = check_cnfg ('variabilize',true)
113 | local list_delim = check_cnfg('list_delim',',')
114 | local convert_numbers = check_cnfg('convert_numbers',true)
115 | local trim_space = check_cnfg('trim_space',true)
116 | local trim_quotes = check_cnfg('trim_quotes',false)
117 | local ignore_assign = check_cnfg('ignore_assign',false)
118 | local keysep = check_cnfg('keysep','=')
119 | local keypat = keysep == ' ' and '%s+' or '%s*'..keysep..'%s*'
120 |
121 | local function process_name(key)
122 | if variablilize then
123 | key = key:gsub('[^%w]','_')
124 | end
125 | return key
126 | end
127 |
128 | local function process_value(value)
129 | if list_delim and value:find(list_delim) then
130 | value = split(value,list_delim)
131 | for i,v in ipairs(value) do
132 | value[i] = process_value(v)
133 | end
134 | elseif convert_numbers and value:find('^[%d%+%-]') then
135 | local val = tonumber(value)
136 | if val then value = val end
137 | end
138 | if type(value) == 'string' then
139 | if trim_space then value = strip(value) end
140 | if trim_quotes then value = strip_quotes(value) end
141 | end
142 | return value
143 | end
144 |
145 | local iter,err = config.lines(file)
146 | if not iter then return nil,err end
147 | for line in iter do
148 | -- strips comments
149 | local ci = line:find('%s*[#;]')
150 | if ci then line = line:sub(1,ci-1) end
151 | -- and ignore blank lines
152 | if line:find('^%s*$') then
153 | elseif line:find('^%[') then -- section!
154 | local section = process_name(line:match('%[([^%]]+)%]'))
155 | t = top_t
156 | t[section] = {}
157 | t = t[section]
158 | else
159 | line = line:gsub('^%s*','')
160 | local i1,i2 = line:find(keypat)
161 | if i1 and not ignore_assign then -- key,value assignment
162 | local key = process_name(line:sub(1,i1-1))
163 | local value = process_value(line:sub(i2+1))
164 | t[key] = value
165 | else -- a plain list of values...
166 | t[#t+1] = process_value(line)
167 | end
168 | end
169 | end
170 | return top_t
171 | end
172 |
173 | return config
174 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | ## A better REPL for Lua
2 |
3 | luaish is based on [lua.lua](http://lua-users.org/wiki/LuaInterpreterInLua) which is a Lua interpreter front-end written in Lua by David Manura.
4 |
5 | /mnt/extra/luaish$ lua52 lua.lua -h
6 | usage: lua.lua [options] [script [args]].
7 | Available options are:
8 | -e stat execute string 'stat'
9 | -l name require library 'name'
10 | -i enter interactive mode after executing 'script'
11 | -v show version information
12 | -- stop handling options
13 | - execute stdin and stop handling options
14 |
15 | Starting from this good working point, I first modified `lua.lua` to be 5.2 compatible, and added 'readline-like' support using [lua-linenoise](https://github.com/hoelzro/lua-linenoise) which is a linenoise binding by Rob Hoelz. Although only a few hundred lines of C, linenoise is more than capable for straightforward line editing and history. And it has enough tab completion support for our purposes.
16 |
17 | So, say if you have typed 'st' then will give you the only matching Lua global, which is 'string'. If you now enter '.' , will cycle through all the available `string` table functions.
18 |
19 | This also works with objects (such as strings). After 's:r' will complete 's:rep' for us:
20 |
21 | > s = '#'
22 | > = s:rep(10)
23 | "##########"
24 |
25 | There is also a few shortcuts defined, so 'fn' gives 'function', and 'rt' gives 'return'.
26 |
27 | luaish makes the command history available in the usual way, and saves it in the `~/luai-history` file. Anything you put in the `~/luairc.lua` file will be loaded at startup.
28 |
29 | luaish uses [luaposix](https://github.com/rrthomas/luaposix) for directory iteration and setting the process environment.
30 |
31 | There is an _optional_ dependency on [Microlight](https://github.com/stevedonovan/Microlight), which is only used to provide some table-dumping abilities to the REPL:
32 |
33 | > = {1,2;one=1}
34 | {1,2,one=1}
35 |
36 | ## Shell Mode
37 |
38 | It can be irritating to have to switch between the Lua interactive prompt and the shell, as separate programs. However, Lua would make a bad shell, in the same way (arguably) that Bash makes a poor programming language.
39 |
40 | Any line begining with '.' is assumed to be a shell command:
41 |
42 | > .tail luaish.lua
43 | local luarc = home..'/.luairc.lua'
44 | local f = io.open(luarc,'r')
45 | if f then
46 | f:close()
47 | dofile(luarc)
48 | else
49 | print 'no ~/.luairc.lua found'
50 | end
51 |
52 | return lsh
53 | > .ls
54 | luaish.lua lua.lua readme.md
55 |
56 | In this shell sub-mode, tab completion switches to _working with paths_. In the above case, I typed '.tail l' and tabbed twice to get '.tail luaish.lua'.
57 |
58 | `cd` is available, but is a _pseudo-command_. It changes the current working directory for the whole session, and updates the title bar of the terminal window. It acts rather like the `pushd` command, so that the pseudo-command `back` goes back to the directory you came from.
59 |
60 | > .cd ../lua/Penlight
61 | ../lua/Penlight
62 | > .ls
63 | github Penlight stevedonovan.github.com
64 | > .back
65 | /mnt/extra/luaish
66 | > .cd ../lua
67 | > .lua hello.lua
68 | hello dolly!
69 | > .l hello.lua
70 | hello dolly!
71 |
72 | 'l' is another pseudo-command, which is equivalent to the Lua call `dofile 'hello.lua`; thereafter '.l' will load the last named file.
73 |
74 | Note that this works as expected:
75 |
76 | > .export P=$(pwd)
77 | > .echo $P
78 | /mnt/extra/luaish
79 |
80 | But, given that luaish is just creating a subshell for commands, how can this command modify the environment of luaish? How this is done is discussed next.
81 |
82 | ## Shell and Lua Mode communication
83 |
84 | If a shell command ends with a '| - ' then 'fun' is assumed to be a function in the global table 'luaish'. The predefined '>' function sets the global with the given name to the output, as a Lua table:
85 |
86 | > .ls -1 | -> ls
87 | > = ls
88 | {"luaish.lua","lua.lua","readme.md"}
89 |
90 | Another built-in function is 'lf', which presents numbered output lines, You can then refer to the line as '$n'
91 |
92 | > .ls -1 | -lf
93 | 1 luaish.lua
94 | 2 lua.lua
95 | 3 readme.md
96 | > .head -n 2 $3
97 | ## A better REPL for Lua
98 |
99 | Any Lua globals are also expanded:
100 |
101 | > P = 'hello'
102 | > .echo $P $(pwd)
103 | hello /mnt/extra/luaish
104 |
105 | The 'ls -1 |-lf' pattern is common enough that an _alias_ is provided:
106 |
107 | > .dir *.lua
108 | 1 luaish.lua
109 | 2 lua.lua
110 |
111 | which is defined so:
112 |
113 | luaish.add_alias('dir','ls -1 %s |-lf')
114 |
115 | Nothing fancy goes on here; any arguments to the alias are passed to the command directly.
116 |
117 | Now the implementation of 'export' can be explained. There is a built-in function which uses [luaposix](https://github.com/rrthomas/luaposix)'s `setenv` function:
118 |
119 | function luaish.lsetenv (f)
120 | local line = f:read()
121 | local var, value = line:match '^(%S+) "(.-)"$'
122 | posix.setenv(var,value)
123 | end
124 |
125 | (Note that these functions are passed a file object for reading from the shell process)
126 |
127 | Here is the long way of using 'lsetenv':
128 |
129 | >.export P=$(pwd) && echo P "$P" | -lsetenv
130 |
131 | And that's exactly what the builtin-command 'export' outputs when you say:
132 |
133 | >.export P=$(pwd)
134 |
135 | Lua string values can be passed to the shell as expanded globals, but there's also an equivalent '| -' mechanism for pumping data into a shell command:
136 |
137 | > t = {'one','two','three'}
138 | > .-print t | sort
139 | one
140 | three
141 | two
142 |
143 | Again, 'print' is a function in the 'luaish' table; these functions work exactly like the others, except they write to their file object. Here is a simplified implementation:
144 |
145 | function luaish.print (f,name)
146 | for _,line in ipairs(_G[name]) do
147 | f:write(line,'\n')
148 | end
149 | end
150 |
151 | The purpose of `~/.luairc.lua` is to let people define their own Lua filters and aliases, as well as preloading useful libraries.
152 |
153 | ## More Possibilities
154 |
155 | luaish is currently in the 'executable proposal' stage of development, for people to try out and play with the possibilities. It could do with some refactoring, so that a person may use it only as a linenoise-equiped Lua prompt, or even use that old dog `readline` itself. Rob Hoelz and myself will be looking at how to build a more generalized and extendable framework.
156 |
157 | Currently, you may _either_ use the 'push input' or 'pop output' forms, but not together. Since `popen2` can be implemented using luaposix, this restriction can be lifted, and we _can_ have a general mechanism for pumping Lua data through a shell filter:
158 |
159 | > . -print idata | sort | -> sdata
160 |
161 |
--------------------------------------------------------------------------------
/luaish.lua:
--------------------------------------------------------------------------------
1 | local have_ml,ml = pcall(require, 'ml')
2 | local posix = require 'posix'
3 | local linenoise = require 'linenoise'
4 | local lsh = { tostring = tostring }
5 | local append = table.insert
6 |
7 | io.write "luaish (c) Steve Donovan 2012\n"
8 |
9 | local our_completions, our_history = {}
10 | local our_line_handlers, our_shortcuts = {} ,{}
11 |
12 | local function is_pair_iterable(t)
13 | local mt = getmetatable(t)
14 | return type(t) == 'table' or (mt and mt.__pairs)
15 | end
16 |
17 | local function lua_candidates(line)
18 | -- identify the expression!
19 | local i1,i2 = line:find('[.:%w_]+$')
20 | if not i1 then return end
21 | local front,partial = line:sub(1,i1-1), line:sub(i1)
22 | local prefix, last = partial:match '(.-)([^.:]*)$'
23 | local t, all = _G
24 | if #prefix > 0 then
25 | local P = prefix:sub(1,-2)
26 | all = last == ''
27 | for w in P:gmatch '[^.:]+' do
28 | t = t[w]
29 | if not t then return end
30 | end
31 | end
32 | prefix = front .. prefix
33 | local res = {}
34 | local function append_candidates(t)
35 | for k,v in pairs(t) do
36 | if all or k:sub(1,#last) == last then
37 | append(res,prefix..k)
38 | end
39 | end
40 | end
41 | local mt = getmetatable(t)
42 | if is_pair_iterable(t) then
43 | append_candidates(t)
44 | end
45 | if mt and is_pair_iterable(mt.__index) then
46 | append_candidates(mt.__index)
47 | end
48 | return res
49 | end
50 |
51 | local function completion_handler(c,s)
52 | local cc
53 | for pat, cf in pairs(our_completions) do
54 | if s:match(pat) then
55 | cc = cf(s)
56 | if not cc then return end
57 | end
58 | end
59 | for sc,value in pairs(our_shortcuts) do
60 | local idx = #s - #sc + 1
61 | if s:sub(idx)==sc then
62 | cc = {s:sub(1,idx-1)..value}
63 | break
64 | end
65 | end
66 | if not cc then
67 | cc = lua_candidates(s)
68 | end
69 | if cc then
70 | for _,name in ipairs(cc) do
71 | linenoise.addcompletion(c,name)
72 | end
73 | end
74 | end
75 |
76 | function lsh.set_tostring(ts)
77 | local old_tostring = lsh.tostring
78 | lsh.tostring = ts
79 | return old_tostring
80 | end
81 |
82 | function lsh.set_shortcuts(shortcuts)
83 | our_shortcuts = shortcuts
84 | end
85 |
86 | function lsh.add_line_handler (h)
87 | append(our_line_handlers, h)
88 | end
89 |
90 | function lsh.add_completion(pat,cf)
91 | our_completions[pat] = cf
92 | end
93 |
94 | local file_list = {}
95 |
96 | function lsh.add_to_file_list(file)
97 | append(file_list,file)
98 | print(('%2d %s'):format(#file_list,file))
99 | end
100 |
101 | function lsh.reset_file_list()
102 | file_list = {}
103 | end
104 |
105 | function lsh.saveline (s)
106 | linenoise.historyadd(s)
107 | linenoise.historysave(our_history)
108 | end
109 |
110 | function lsh.readline(prmt)
111 | local line, err = linenoise.linenoise(prmt)
112 | if not line then
113 | if err == 'cancel' then
114 | print ''
115 | line = linenoise.linenoise(prmt)
116 | else
117 | return nil
118 | end
119 | end
120 | if #file_list > 0 and line then
121 | line = line:gsub('%$(%d+)',function(num)
122 | num = tonumber(num)
123 | return file_list[num] or 'que?'
124 | end)
125 | lsh.lines = file_list
126 | end
127 | return line
128 | end
129 |
130 | function lsh.checkline(b)
131 | for _,h in ipairs(our_line_handlers) do
132 | local res = h(b)
133 | if res then
134 | lsh.saveline(b)
135 | return false -- we handled this, keep reading
136 | end
137 | end
138 | return true -- line for Lua interpreter
139 | end
140 |
141 | -------- Lua output filters --------
142 | -- a shell command like '.ls | sort |=name'
143 | -- will be put through a Lua function called 'name' in the luaish global table.
144 | -- This function is passed the file object from popen
145 |
146 | -- this filter prints lines with line numbers, and the lines can
147 | -- subsequently be accessed with $n from the prompt (all modes)
148 | function lsh.lf (f)
149 | lsh.reset_file_list()
150 | for line in f:lines() do
151 | lsh.add_to_file_list(line)
152 | end
153 | end
154 |
155 | -- this filter is used to implement the built-in command export
156 | function lsh.lsetenv (f)
157 | local line = f:read()
158 | local var, value = line:match '^(%S+) "(.-)"$'
159 | posix.setenv(var,value)
160 | end
161 |
162 | lsh ['>'] = function(f,name)
163 | local res = {}
164 | for line in f:lines() do
165 | append(res,line)
166 | end
167 | _G[name] = res
168 | end
169 |
170 | lsh.print = function(f,name)
171 | local val = _G[name]
172 | if not val then
173 | io.write(("'%s' is not a Lua global\n"):format(name))
174 | return
175 | end
176 | for _,line in ipairs(_G[name]) do
177 | f:write(line,'\n')
178 | end
179 | end
180 |
181 | local function at(s,i)
182 | return s:sub(i,i)
183 | end
184 |
185 | local push, pop = append, table.remove
186 | local dirstack = {}
187 |
188 | local function is_directory(path)
189 | return posix.stat(path,'type') == 'directory'
190 | end
191 |
192 | function path_candidates(line)
193 | local i1,front,path,name
194 | i1 = line:find '%S+$'
195 | if not i1 then return end
196 | front, path = line:sub(1,i1-1), line:sub(i1)
197 | i1 = path:find '[.%w%-]*$'
198 | if not i1 then return end
199 | path,name = path:sub(1,i1-1), path:sub(i1)
200 | local fullpath, sc, dpath = path,at(path,1),path
201 | if sc == '~' then
202 | path = os.getenv 'HOME'..'/'..path:sub(3)
203 | fullpath = path
204 | elseif sc ~= '/' then
205 | fullpath = posix.getcwd()
206 | if path ~= '' then
207 | fullpath = fullpath ..'/'..path
208 | else
209 | path = '.'
210 | dpath = ''
211 | end
212 | end
213 | if not is_directory(fullpath) then return end
214 | local res = {}
215 | local all = name == ''
216 | for _,f in ipairs(posix.dir(path)) do
217 | if all or f:sub(1,#name)==name then
218 | push(res,front..dpath..f)
219 | end
220 | end
221 | table.sort(res,function(a,b)
222 | return #a < #b
223 | end)
224 | return res
225 | end
226 |
227 | local function set_title(msg)
228 | msg = msg or posix.getcwd()
229 | io.write("\027]2;luai "..msg.."\007")
230 | end
231 |
232 | local function change_directory(dir)
233 | posix.chdir(dir)
234 | set_title(posix.getcwd())
235 | print(dir)
236 | end
237 |
238 | local function back()
239 | local odir = pop(dirstack)
240 | if odir then
241 | change_directory(odir)
242 | else
243 | print 'dir stack is empty'
244 | end
245 | end
246 |
247 | local last_command
248 |
249 | local function exec (line)
250 | local i1,_,filter,at_start
251 | i1,_,lfilter = line:find '|%s*%-(.+)$'
252 | if not i1 then
253 | at_start = true
254 | _,i1,lfilter = line:find '%s*%-([^|]+)|'
255 | end
256 | if i1 then
257 | if at_start then
258 | line = line:sub(i1+1)
259 | else
260 | line = line:sub(1,i1-1)
261 | end
262 | local lfun,arg = lfilter:match '(%S+)%s+(.-)%s*$'
263 | if not lfun then
264 | lfun = lfilter
265 | end
266 | if not lsh[lfun] then
267 | io.write (lfun,' is not a Lua filter\n')
268 | return true
269 | end
270 | local f = io.popen(line,at_start and 'w' or 'r')
271 | local ok, res = pcall(lsh[lfun],f,arg)
272 | f:close()
273 | if not ok then
274 | io.write(res,'\n')
275 | end
276 | return true
277 | else
278 | os.execute(line)
279 | end
280 | return true
281 | end
282 |
283 | local alias = {}
284 |
285 | function lsh.add_alias(name,cmd)
286 | alias[name] = cmd
287 | end
288 |
289 | local function expand_alias(cmd,args)
290 | return alias[cmd]:format(args)
291 | end
292 |
293 | local function expand_lua_globals(line)
294 | return line:gsub('%$(%a+)',function(name)
295 | local val = _G[name]
296 | if val then return tostring(val)
297 | else return '$'..name
298 | end
299 | end)
300 | end
301 |
302 | function shell_command_handler (line)
303 | if at(line,1) == '.' then
304 | line = line:gsub('^%.%s*','')
305 | line = expand_lua_globals(line)
306 | local cmd,args = line:match '^(%S+)(.*)$'
307 | if not args then
308 | io.write 'bad command syntax\n'
309 | return true
310 | end
311 | if alias[cmd] then
312 | line = expand_alias(cmd,args)
313 | cmd,args = line:match '^(%a+)(.*)$'
314 | end
315 | args = args:gsub('^%s*','')
316 | if cmd == 'cd' then
317 | local arg = args:match '^(%S+)'
318 | local _,k = arg:find '^%-+$'
319 | if k then
320 | arg = arg:gsub('%-','../',k)
321 | end
322 | arg = arg:gsub('^~',os.getenv 'HOME')
323 | if not is_directory(arg) then
324 | arg = posix.dirname(arg)
325 | end
326 | push(dirstack,posix.getcwd())
327 | change_directory(arg)
328 | elseif cmd == 'l' then
329 | if args == '' then
330 | args = last_command
331 | end
332 | if not args then
333 | print 'no script file specified'
334 | return true
335 | end
336 | dofile(args)
337 | last_command = args
338 | elseif cmd == 'back' then
339 | back()
340 | elseif cmd == 'h' then
341 | return exec(('tail %s |-lf'):format(our_history))
342 | elseif cmd == 'export' then
343 | local var = args:match '^(.-)='
344 | local cmd = (('%s && echo %s \\"$%s\\" |-lsetenv'):format(args,var,var))
345 | return exec(cmd)
346 | else -- plain shell command
347 | return exec(line)
348 | end
349 | return true
350 | end
351 | end
352 |
353 | local home = os.getenv 'HOME'
354 | linenoise.setcompletion(completion_handler)
355 | our_history = home..'/.luai-history'
356 | linenoise.historyload(our_history)
357 | lsh.add_line_handler(shell_command_handler)
358 | lsh.add_completion('^%.',path_candidates)
359 |
360 | -- microlight isn't essential, but it gives you better
361 | -- output; tables will be printed out
362 | if have_ml then
363 | lsh.set_tostring(ml.tstring)
364 | _G.ml = ml
365 | end
366 | local ok
367 | ok,_G.config = pcall(require,'config')
368 |
369 | lsh.set_shortcuts {
370 | fn = "function ",
371 | rt = "return ",
372 | }
373 |
374 | _G.posix = posix
375 | _G.luaish = lsh -- global for rc file
376 |
377 | lsh.add_alias('dir','ls -1 %s |-lf')
378 | lsh.add_alias('locate','locate %s |-lf')
379 |
380 | local luarc = home..'/.luairc.lua'
381 | local f = io.open(luarc,'r')
382 | if f then
383 | f:close()
384 | dofile(luarc)
385 | else
386 | --print 'no ~/.luairc.lua found'
387 | end
388 |
389 | return lsh
390 |
--------------------------------------------------------------------------------
/lua.lua:
--------------------------------------------------------------------------------
1 | -- lua.lua - Lua 5.1 interpreter (lua.c) reimplemented in Lua.
2 | --
3 | -- WARNING: This is not completed but was quickly done just an experiment.
4 | -- Fix omissions/bugs and test if you want to use this in production.
5 | -- Particularly pay attention to error handling.
6 | --
7 | -- (c) David Manura, 2008-08
8 | -- Licensed under the same terms as Lua itself.
9 | -- Based on lua.c from Lua 5.1.3.
10 | -- Improvements by Shmuel Zeigerman.
11 |
12 | -- Variables analogous to those in luaconf.h
13 | local LUA_INIT = "LUA_INIT"
14 | local LUA_PROGNAME = "lua"
15 | local LUA_PROMPT = "> "
16 | local LUA_PROMPT2 = ">> "
17 | local function LUA_QL(x) return "'" .. x .. "'" end
18 |
19 | local lua51 = _VERSION:match '5%.1$'
20 | -- Variables analogous to those in lua.h
21 | local LUA_RELEASE, LUA_COPYRIGHT, eof_ender
22 | if lua51 then
23 | LUA_RELEASE = "Lua 5.1.4"
24 | LUA_COPYRIGHT = "Copyright (C) 1994-2008 Lua.org, PUC-Rio"
25 | eof_ender = LUA_QL("")
26 | else
27 | LUA_RELEASE = "Lua 5.2.0"
28 | LUA_COPYRIGHT = "Copyright (C) 1994-2011 Lua.org, PUC-Rio"
29 | eof_ender = ''
30 | end
31 | local EXTRA_COPYRIGHT = "lua.lua (c) David Manura, 2008-08"
32 |
33 | -- Note: don't allow user scripts to change implementation.
34 | -- Check for globals with "cat lua.lua | luac -p -l - | grep ETGLOBAL"
35 |
36 | local _G = _G
37 | local assert = assert
38 | local collectgarbage = collectgarbage
39 | local loadfile = loadfile
40 | local loadstring = loadstring or load
41 | local pcall = pcall
42 | local rawget = rawget
43 | local select = select
44 | local tostring = tostring
45 | local type = type
46 | local unpack = unpack or table.unpack
47 | local xpcall = xpcall
48 | local io_stderr = io.stderr
49 | local io_stdout = io.stdout
50 | local io_stdin = io.stdin
51 | local string_format = string.format
52 | local string_sub = string.sub
53 | local os_getenv = os.getenv
54 | local os_exit = os.exit
55 |
56 |
57 | local progname = LUA_PROGNAME
58 |
59 | -- Use external functions, if available
60 | local lua_stdin_is_tty = function() return true end
61 | local setsignal = function() end
62 |
63 | local function print_usage()
64 | io_stderr:write(string_format(
65 | "usage: %s [options] [script [args]].\n" ..
66 | "Available options are:\n" ..
67 | " -e stat execute string " .. LUA_QL("stat") .. "\n" ..
68 | " -l name require library " .. LUA_QL("name") .. "\n" ..
69 | " -i enter interactive mode after executing " ..
70 | LUA_QL("script") .. "\n" ..
71 | " -v show version information\n" ..
72 | " -- stop handling options\n" ..
73 | " - execute stdin and stop handling options\n"
74 | ,
75 | progname))
76 | io_stderr:flush()
77 | end
78 |
79 | local our_tostring = tostring
80 |
81 | local tuple = table.pack or function(...)
82 | return {n=select('#', ...), ...}
83 | end
84 |
85 | local using_lsh,lsh
86 |
87 | local function our_print (...)
88 | local args = tuple(...)
89 | for i = 1,args.n do
90 | io.write(our_tostring(args[i]),'\t')
91 | end
92 | _G._ = args[1]
93 | io.write '\n'
94 | end
95 |
96 | local function saveline(s)
97 | if using_lsh then
98 | lsh.saveline(s)
99 | end
100 | end
101 |
102 | local function getline(prmt)
103 | if using_lsh then
104 | return lsh.readline(prmt)
105 | else
106 | io_stdout:write(prmt)
107 | io_stdout:flush()
108 | return io_stdin:read'*l'
109 | end
110 | end
111 |
112 | local function l_message (pname, msg)
113 | if pname then io_stderr:write(string_format("%s: ", pname)) end
114 | io_stderr:write(string_format("%s\n", msg))
115 | io_stderr:flush()
116 | end
117 |
118 | local function report(status, msg)
119 | if not status and msg ~= nil then
120 | msg = (type(msg) == 'string' or type(msg) == 'number') and tostring(msg)
121 | or "(error object is not a string)"
122 | l_message(progname, msg);
123 | end
124 | return status
125 | end
126 |
127 | local function traceback (message)
128 | local tp = type(message)
129 | if tp ~= "string" and tp ~= "number" then return message end
130 | local debug = _G.debug
131 | if type(debug) ~= "table" then return message end
132 | local tb = debug.traceback
133 | if type(tb) ~= "function" then return message end
134 | return tb(message, 2)
135 | end
136 |
137 | local function docall(f, ...)
138 | local tp = {...} -- no need in tuple (string arguments only)
139 | local F = function() return f(unpack(tp)) end
140 | setsignal(true)
141 | local result = tuple(xpcall(F, traceback))
142 | setsignal(false)
143 | -- force a complete garbage collection in case of errors
144 | if not result[1] then collectgarbage("collect") end
145 | return unpack(result, 1, result.n)
146 | end
147 |
148 | function dofile(name)
149 | local f, msg = loadfile(name)
150 | if f then f, msg = docall(f) end
151 | return report(f, msg)
152 | end
153 |
154 | local function dostring(s, name)
155 | local f, msg = loadstring(s, name)
156 | if f then f, msg = docall(f) end
157 | return report(f, msg)
158 | end
159 |
160 | local function dolibrary (name)
161 | return report(docall(_G.require, name))
162 | end
163 |
164 | local function print_version()
165 | l_message(nil, LUA_RELEASE .. " " .. LUA_COPYRIGHT.."\n"..EXTRA_COPYRIGHT)
166 | end
167 |
168 | local function getargs (argv, n)
169 | local arg = {}
170 | for i=1,#argv do arg[i - n] = argv[i] end
171 | if _G.arg then
172 | local i = 0
173 | while _G.arg[i] do
174 | arg[i - n] = _G.arg[i]
175 | i = i - 1
176 | end
177 | end
178 | return arg
179 | end
180 |
181 | local function get_prompt (firstline)
182 | -- use rawget to play fine with require 'strict'
183 | local pmt = rawget(_G, firstline and "_PROMPT" or "_PROMPT2")
184 | local tp = type(pmt)
185 | if tp == "string" or tp == "number" then
186 | return tostring(pmt)
187 | end
188 | return firstline and LUA_PROMPT or LUA_PROMPT2
189 | end
190 |
191 | local function fetchline(firstline)
192 | return getline(get_prompt(firstline))
193 | end
194 |
195 | local function incomplete (msg)
196 | if msg then
197 | if string_sub(msg, -#eof_ender) == eof_ender then
198 | return true
199 | end
200 | end
201 | return false
202 | end
203 |
204 |
205 | local function pushline (firstline)
206 | local b, fine = true
207 | repeat
208 | b = fetchline(firstline)
209 | if not b then return end -- no input
210 | if using_lsh then
211 | fine = lsh.checkline(b)
212 | end
213 | until fine
214 | if firstline and string_sub(b, 1, 1) == '=' then
215 | return "return " .. string_sub(b, 2) -- change '=' to `return'
216 | else
217 | return b
218 | end
219 | end
220 |
221 |
222 | local function loadline ()
223 | local b = pushline(true)
224 | if not b then return -1 end -- no input
225 | local f, msg
226 | while true do -- repeat until gets a complete line
227 | f, msg = loadstring(b, "=stdin")
228 | if not incomplete(msg) then break end -- cannot try to add lines?
229 | local b2 = pushline(false)
230 | if not b2 then -- no more input?
231 | return -1
232 | end
233 | b = b .. "\n" .. b2 -- join them
234 | end
235 |
236 | saveline(b)
237 |
238 | return f, msg
239 | end
240 |
241 |
242 | local function dotty ()
243 | local oldprogname = progname
244 | progname = nil
245 | using_lsh,lsh = pcall(require, 'luaish')
246 | if using_lsh then
247 | our_tostring = lsh.tostring
248 | else
249 | print('problem loading luaish:',lsh)
250 | end
251 | while true do
252 | local result
253 | local status, msg = loadline()
254 | if status == -1 then break end
255 | if status then
256 | result = tuple(docall(status))
257 | status, msg = result[1], result[2]
258 | end
259 | report(status, msg)
260 | if status and result.n > 1 then -- any result to print?
261 | status, msg = pcall(our_print, unpack(result, 2, result.n))
262 | if not status then
263 | l_message(progname, string_format(
264 | "error calling %s (%s)",
265 | LUA_QL("print"), msg))
266 | end
267 | end
268 | end
269 | io_stdout:write"\n"
270 | io_stdout:flush()
271 | progname = oldprogname
272 | end
273 |
274 |
275 | local function handle_script(argv, n)
276 | _G.arg = getargs(argv, n) -- collect arguments
277 | local fname = argv[n]
278 | if fname == "-" and argv[n-1] ~= "--" then
279 | fname = nil -- stdin
280 | end
281 | local status, msg = loadfile(fname)
282 | if status then
283 | status, msg = docall(status, unpack(_G.arg))
284 | end
285 | return report(status, msg)
286 | end
287 |
288 |
289 | local function collectargs (argv, p)
290 | local i = 1
291 | while i <= #argv do
292 | if string_sub(argv[i], 1, 1) ~= '-' then -- not an option?
293 | return i
294 | end
295 | local prefix = string_sub(argv[i], 1, 2)
296 | if prefix == '--' then
297 | if #argv[i] > 2 then return -1 end
298 | return argv[i+1] and i+1 or 0
299 | elseif prefix == '-' then
300 | return i
301 | elseif prefix == '-i' then
302 | if #argv[i] > 2 then return -1 end
303 | p.i = true
304 | p.v = true
305 | elseif prefix == '-v' then
306 | if #argv[i] > 2 then return -1 end
307 | p.v = true
308 | elseif prefix == '-e' then
309 | p.e = true
310 | if #argv[i] == 2 then
311 | i = i + 1
312 | if argv[i] == nil then return -1 end
313 | end
314 | elseif prefix == '-l' then
315 | if #argv[i] == 2 then
316 | i = i + 1
317 | if argv[i] == nil then return -1 end
318 | end
319 | else
320 | return -1 -- invalid option
321 | end
322 | i = i + 1
323 | end
324 | return 0
325 | end
326 |
327 |
328 | local function runargs(argv, n)
329 | local i = 1
330 | while i <= n do if argv[i] then
331 | assert(string_sub(argv[i], 1, 1) == '-')
332 | local c = string_sub(argv[i], 2, 2) -- option
333 | if c == 'e' then
334 | local chunk = string_sub(argv[i], 3)
335 | if chunk == '' then i = i + 1; chunk = argv[i] end
336 | assert(chunk)
337 | if not dostring(chunk, "=(command line)") then return false end
338 | elseif c == 'l' then
339 | local filename = string_sub(argv[i], 3)
340 | if filename == '' then i = i + 1; filename = argv[i] end
341 | assert(filename)
342 | if not dolibrary(filename) then return false end
343 | end
344 | i = i + 1
345 | end end
346 | return true
347 | end
348 |
349 |
350 | local function handle_luainit()
351 | local init = os_getenv(LUA_INIT)
352 | if init == nil then
353 | return -- status OK
354 | elseif string_sub(init, 1, 1) == '@' then
355 | dofile(string_sub(init, 2))
356 | else
357 | dostring(init, "=" .. LUA_INIT)
358 | end
359 | end
360 |
361 |
362 | local import = _G.import
363 | if import then
364 | lua_stdin_is_tty = import.lua_stdin_is_tty or lua_stdin_is_tty
365 | setsignal = import.setsignal or setsignal
366 | LUA_RELEASE = import.LUA_RELEASE or LUA_RELEASE
367 | LUA_COPYRIGHT = import.LUA_COPYRIGHT or LUA_COPYRIGHT
368 | _G.import = nil
369 | end
370 |
371 | if _G.arg and _G.arg[0] and #_G.arg[0] > 0 then progname = _G.arg[0] end
372 | local argv = {...}
373 | handle_luainit()
374 | local has = {i=false, v=false, e=false}
375 | local script = collectargs(argv, has)
376 | if script < 0 then -- invalid args?
377 | print_usage()
378 | os_exit(1)
379 | end
380 | if has.v then print_version() end
381 | local status = runargs(argv, (script > 0) and script-1 or #argv)
382 | if not status then os_exit(1) end
383 | if script ~= 0 then
384 | status = handle_script(argv, script)
385 | if not status then os_exit(1) end
386 | else
387 | _G.arg = nil
388 | end
389 | if has.i then
390 | dotty()
391 | elseif script == 0 and not has.e and not has.v then
392 | if lua_stdin_is_tty() then
393 | print_version()
394 | dotty()
395 | else dofile(nil) -- executes stdin as a file
396 | end
397 | end
398 |
--------------------------------------------------------------------------------