├── .busted ├── .gitignore ├── .luacheckrc ├── .travis.yml ├── Dockerfile ├── Dockerfile-lua51 ├── LICENSE ├── Makefile ├── README.md ├── bin ├── lua51test ├── resty-repl └── test_with_expect ├── docker-compose.yml ├── expect ├── lib │ └── helpers.tcl ├── run_from_coroutine.exp ├── run_from_history_on_fail.exp ├── run_from_history_on_success.exp ├── show_context.exp └── test_data │ └── example1.lua ├── lib └── resty │ ├── repl.lua │ └── repl │ ├── binding.lua │ ├── compiler.lua │ ├── completer.lua │ ├── formatter.lua │ ├── readline.lua │ ├── readline_stub.lua │ ├── readline_utils.lua │ ├── sources.lua │ └── ui.lua ├── lua-resty-repl-0.0.5-1.rockspec ├── lua-resty-repl-0.0.6-0.rockspec ├── lua-resty-repl-scm-0.rockspec ├── spec ├── lib │ └── resty │ │ ├── binding_spec.lua │ │ ├── compiler_spec.lua │ │ ├── completer_spec.lua │ │ ├── formatter_spec.lua │ │ ├── readline_spec.lua │ │ └── repl_spec.lua └── spec_helper.lua └── vendor └── inspect.lua /.busted: -------------------------------------------------------------------------------- 1 | return { 2 | _all = { 3 | verbose = true, 4 | ['shuffle-tests'] = true, 5 | ['shuffle-files'] = true, 6 | helper = 'spec/spec_helper.lua', 7 | }, 8 | default = { 9 | }, 10 | } 11 | 12 | -- vim: ft=lua 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | -------------------------------------------------------------------------------- /.luacheckrc: -------------------------------------------------------------------------------- 1 | std = '+luajit' 2 | files['spec'] = { std = '+busted' } 3 | 4 | -- vim: ft=lua 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: bash 2 | 3 | cache: 4 | directories: 5 | - /home/travis/docker/ 6 | 7 | env: 8 | global: 9 | - OPENRESTY_IMAGE=/home/travis/docker/openresty_image.tar.gz 10 | - LUA51_IMAGE=/home/travis/docker/LUA51_IMAGE.tar.gz 11 | 12 | services: 13 | - docker 14 | 15 | before_install: 16 | - if [ -f ${OPENRESTY_IMAGE} ]; then gunzip -c ${OPENRESTY_IMAGE} | docker load; else docker-compose pull app; docker save saksmlz/openresty-testsuite:latest | gzip > ${OPENRESTY_IMAGE}; fi 17 | - if [ -f ${LUA51_IMAGE} ]; then gunzip -c ${LUA51_IMAGE} | docker load; else docker-compose pull lua51; docker save saksmlz/lua51-testsuite:latest | gzip > ${LUA51_IMAGE}; fi 18 | 19 | script: 20 | - make 21 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM saksmlz/openresty-docker:1.11.2.1-slim 2 | 3 | RUN set -ex \ 4 | && apt-get update \ 5 | && apt-get install --yes --no-install-recommends perl expect tclsh \ 6 | && apt-get install --yes --no-install-recommends build-essential git curl unzip libssl-dev \ 7 | && luarocks install busted \ 8 | && luarocks install luacheck \ 9 | && echo "#!/usr/bin/env resty" > /usr/local/bin/resty-busted \ 10 | && echo "require 'busted.runner'({ standalone = false })" >> /usr/local/bin/resty-busted \ 11 | && chmod +x /usr/local/bin/resty-busted \ 12 | && apt-get purge --yes --auto-remove build-essential git curl unzip libssl-dev \ 13 | && rm -rf /var/lib/apt/lists/* 14 | -------------------------------------------------------------------------------- /Dockerfile-lua51: -------------------------------------------------------------------------------- 1 | FROM alpine:3.4 2 | 3 | RUN set -ex \ 4 | && sed -i -e 's/v3\.4/edge/g' /etc/apk/repositories \ 5 | && apk update \ 6 | && apk add lua5.1 luarocks \ 7 | && apk add build-base lua5.1-dev \ 8 | && luarocks-5.1 install busted \ 9 | && apk del build-base lua5.1-dev 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Aliaksandr Rahalevich 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: lint test 2 | 3 | lint: 4 | docker-compose run --rm app luacheck . 5 | 6 | build: 7 | docker build -t saksmlz/openresty-testsuite:latest -f Dockerfile . 8 | docker build -t saksmlz/lua51-testsuite:latest -f Dockerfile-lua51 . 9 | 10 | push_images: 11 | docker push saksmlz/openresty-testsuite:latest 12 | docker push saksmlz/lua51-testsuite:latest 13 | 14 | test: test_openresty test_luajit test_lua51 test_luajit_integrational 15 | 16 | test_openresty: 17 | @echo OPENRESTY: 18 | @docker-compose run --rm app resty-busted spec 19 | 20 | test_luajit: 21 | @echo LUAJIT: 22 | @docker-compose run --rm app busted spec 23 | 24 | test_luajit_integrational: 25 | @echo LUAJIT INTEGRATIONAL: 26 | @docker-compose run --rm app bin/test_with_expect 27 | 28 | test_lua51: 29 | @echo LUA5.1: 30 | @docker-compose run --rm lua51 bin/lua51test 31 | 32 | shell: 33 | docker-compose run --rm app 34 | 35 | repl: 36 | @docker-compose run --rm app \ 37 | resty \ 38 | -I lib/ \ 39 | -I vendor/ \ 40 | -e 'require("resty.repl").start()' 41 | 42 | .PHONY: lint test shell repl build push_images test_openresty \ 43 | test_luajit test_lua51 test_with_expect 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Welcome to Resty Repl 2 | 3 | ## Features 4 | 5 | Resty Repl is a powerful alternative to the standard [luajit](http://luajit.org/) shell ispired by [pry](https://github.com/pry/pry). It is written from scratch to provide a number of advanced features, including: 6 | * Full read/write access to locals, upvalues and global variables 7 | * Pretty print for objects 8 | * A Powerful and flexible command system 9 | * Ability to view and replay history 10 | * Ability to see a context and source of the place in code from where repl was started 11 | * Runtime invocation (use Resty Repl as a developer console or debugger) 12 | * Tab completion 13 | * Simple and easy way to debug lua running in the nginx ([openresty](http://openresty.org/en/)) 14 | 15 | ## Runtime invocation 16 | 17 | First install luarock 18 | ```bash 19 | luarocks install lua-resty-repl 20 | ``` 21 | 22 | Then just drop this snippet anywhere in your code: 23 | 24 | ```lua 25 | require('resty.repl').start() 26 | ``` 27 | 28 | or run as cli: 29 | ```bash 30 | resty-repl 31 | ``` 32 | 33 | ## Openresty debugger 34 | But what makes it really nice is that now you can debug your [openresty](http://openresty.org/en/) code right from running nginx! 35 | 36 | ```nginx 37 | master_process off; 38 | error_log stderr notice; 39 | daemon off; 40 | 41 | events { 42 | worker_connections 1024; 43 | } 44 | 45 | http { 46 | server { 47 | listen 8080; 48 | lua_code_cache off; 49 | 50 | location / { 51 | content_by_lua_block { 52 | require('resty.repl').start() 53 | } 54 | } 55 | } 56 | } 57 | ``` 58 | 59 | and start debugging: 60 | ```bash 61 | $ curl -H X-Header:buz 172.17.0.2:8080?foo=bar 62 | 63 | ``` 64 | 65 | ``` 66 | nginx -c /tmp/ngx.conf 67 | 2016/09/20 16:26:33 [alert] 2257#0: lua_code_cache is off; this will hurt performance in /tmp/ngx.conf:12 68 | nginx: [alert] lua_code_cache is off; this will hurt performance in /tmp/ngx.conf:12 69 | 2016/09/20 16:26:33 [notice] 2257#0: using the "epoll" event method 70 | 2016/09/20 16:26:33 [notice] 2257#0: openresty/1.11.2.1 71 | 2016/09/20 16:26:33 [notice] 2257#0: built by gcc 4.9.2 (Debian 4.9.2-10) 72 | 2016/09/20 16:26:33 [notice] 2257#0: OS: Linux 4.4.0-38-generic 73 | 2016/09/20 16:26:33 [notice] 2257#0: getrlimit(RLIMIT_NOFILE): 65536:65536 74 | 75 | From: content_by_lua(ngx.conf:17) @ line 2 76 | 77 | [1] ngx(content)> ngx.req.get_headers() 78 | => { 79 | accept = "*/*", 80 | host = "172.17.0.2:8080", 81 | ["user-agent"] = "curl/7.47.0", 82 | ["x-header"] = "buz", 83 | = { 84 | __index = 85 | } 86 | } 87 | [2] ngx(content)> ngx.req.get_uri_args() 88 | => { 89 | foo = "bar" 90 | } 91 | [3] ngx(content)> ngx.say 'it works!' 92 | => 1 93 | [4] ngx(content)> ngx.exit(ngx.OK) 94 | 172.17.0.1 - - [20/Sep/2016:16:26:50 +0000] "GET /?foo=bar HTTP/1.1" 200 20 "-" "curl/7.47.0" 95 | ``` 96 | 97 | ## Compatibility 98 | Right now it's only compatible with: 99 | - luajit 100 | - lua5.1 (no readline) 101 | 102 | ## Os Support 103 | - GNU/Linux 104 | - Mac OS 105 | 106 | ## Roadmap 107 | - colorized output 108 | - smarter completion 109 | - full readline support for lua (no ffi environments) 110 | - remote debugger 111 | - command for showing function source 112 | - test suite with [resty-cli](https://github.com/openresty/resty-cli), [luajit](http://luajit.org/) and different versions of lua 113 | - better inspect library 114 | 115 | ## Code Status 116 | 117 | [![Build Status](https://travis-ci.org/saks/lua-resty-repl.svg?branch=master)](https://travis-ci.org/saks/lua-resty-repl) 118 | 119 | ## License 120 | 121 | resty-repl is released under the [MIT License](http://www.opensource.org/licenses/MIT). 122 | -------------------------------------------------------------------------------- /bin/lua51test: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | eval $(luarocks-5.1 path --bin) 4 | exec lua5.1 -e "require \"busted.runner\"{standalone = false}" 5 | -------------------------------------------------------------------------------- /bin/resty-repl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env luajit 2 | 3 | local loadstring = loadstring or load -- for lua 5.2 compat 4 | 5 | -- Tracekback (error printout) 6 | local function traceback(message) 7 | local tp = type(message) 8 | if tp ~= 'string' and tp ~= 'number' then return message end 9 | local debug = _G.debug 10 | if type(debug) ~= 'table' then return message end 11 | local tb = debug.traceback 12 | if type(tb) ~= 'function' then return message end 13 | return tb(message) 14 | end 15 | 16 | -- help 17 | local help = [==[ 18 | Usage: resty-repl [options] [script.lua [arguments]] 19 | 20 | Options: 21 | -l name load library name 22 | -e statement execute statement 23 | -h, --help print this help ]==] 24 | 25 | -- parse arguments 26 | local run, progargs, statement, lib, _ 27 | local parg = arg 28 | local nextarg 29 | for _, arg in ipairs(parg) do 30 | -- nextarg set? 31 | if nextarg == 'exec' then 32 | statement = arg 33 | nextarg = nil 34 | elseif nextarg == 'lib' then 35 | lib = arg 36 | nextarg = nil 37 | elseif not progargs then 38 | _, _, lib = arg:find('^%-l(.*)') 39 | if lib == '' then lib = nil end 40 | end 41 | -- load libraries 42 | if lib then 43 | local ok, err = xpcall(function() require(lib) end, traceback) 44 | if not ok then 45 | print('could not load ' .. lib .. ', skipping') 46 | print(err) 47 | end 48 | elseif progargs then 49 | -- program args 50 | table.insert(progargs, arg) 51 | elseif not statement then 52 | -- option? 53 | local _, _, option = arg:find('^%-%-(.*)') 54 | local shortopt 55 | if not option then 56 | _, _, shortopt = arg:find('^%-(.*)') 57 | end 58 | if option or shortopt then 59 | -- help 60 | if shortopt == 'h' or option == 'help' then 61 | print(help) 62 | return 63 | elseif shortopt == 'e' then 64 | nextarg = 'exec' 65 | elseif shortopt == 'l' then 66 | nextarg = 'lib' 67 | else 68 | -- unknown 69 | print('Error: unrecognized flag --' .. (option ~= nil and option or shortopt)) 70 | print(help) 71 | return 72 | end 73 | else 74 | -- exec program 75 | run = arg 76 | progargs = {} 77 | for k, v in pairs(parg) do 78 | if k <= 0 then 79 | progargs[k] = v 80 | end 81 | end 82 | end 83 | end 84 | end 85 | 86 | -- statement 87 | if statement then 88 | -- exec statement: 89 | local s = loadstring(statement) 90 | local ok, res = pcall(s) 91 | if not ok then 92 | print(res) 93 | return 94 | end 95 | -- quit by default 96 | if not interactive then return end 97 | end 98 | 99 | -- run program 100 | if run then 101 | -- set prog args: 102 | arg = progargs 103 | -- run 104 | dofile(run) 105 | end 106 | 107 | require('resty.repl').start() 108 | -------------------------------------------------------------------------------- /bin/test_with_expect: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # install package 4 | luarocks make lua-resty-repl-scm-0.rockspec > /dev/null 5 | 6 | suffix=.exp 7 | 8 | RED='\033[0;31m' 9 | GREEN='\033[0;32m' 10 | NC='\033[0m' # No Color 11 | 12 | before_each() rm -f /root/.luahistory # cleanup history file 13 | fail() { 14 | printf "${RED}FAIL${NC}\n${RED}" 15 | expect -d $test_file 16 | printf "${NC}" 17 | printf '%80s\n' | tr ' ' - 18 | } 19 | ok() printf "${GREEN}OK${NC}\n" 20 | 21 | run_test() { 22 | test_file=$1 23 | 24 | echo -n `basename -s $suffix $test_file`' .. ' 25 | expect $test_file >> /dev/null 26 | [ $? = 0 ] && ok || fail $test_file 27 | } 28 | 29 | for fn in `ls ./expect/*$suffix`; do 30 | before_each 31 | run_test $fn 32 | done 33 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | app: 2 | image: "saksmlz/openresty-testsuite:latest" 3 | command: "bash" 4 | container_name: "lua-resty-repl-tests" 5 | volumes: 6 | - ".:/lua-resty-repl" 7 | working_dir: "/lua-resty-repl" 8 | 9 | lua51: 10 | image: "saksmlz/lua51-testsuite:latest" 11 | command: "sh" 12 | container_name: "lua-resty-repl-tests" 13 | volumes: 14 | - ".:/lua-resty-repl" 15 | working_dir: "/lua-resty-repl" 16 | -------------------------------------------------------------------------------- /expect/lib/helpers.tcl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env tclsh 2 | 3 | set timeout 1 4 | 5 | set force_conservative 0 ;# set to 1 to force conservative mode even if 6 | ;# script wasn't run conservatively originally 7 | if {$force_conservative} { 8 | set send_slow {1 .1} 9 | proc send {ignore arg} { 10 | sleep .1 11 | exp_send -s -- $arg 12 | } 13 | } 14 | 15 | # Hit Enter key 16 | proc press_enter_key {} { 17 | send -- "\r" 18 | } 19 | 20 | # Exit with code 1 if expactation failed with timeout. 21 | proc expect_or_fail {expectation} { 22 | expect { 23 | timeout { exit 1 } 24 | $expectation 25 | } 26 | } 27 | 28 | # Send text and hit Enter key. 29 | proc input {text} { 30 | send -- $text 31 | press_enter_key 32 | } 33 | 34 | # Send UP_ARROW key 35 | proc press_up_arrow_key {{times 1}} { 36 | for {set x 0} {$x<$times} {incr x} { 37 | send -- "\033OA" 38 | } 39 | } 40 | 41 | proc expect_prompt_line {line_number} { 42 | expect_or_fail "\\\[$line_number] lua(main)> " 43 | } 44 | 45 | proc exit_repl {} { 46 | input "exit" 47 | expect_or_fail eof 48 | } 49 | -------------------------------------------------------------------------------- /expect/run_from_coroutine.exp: -------------------------------------------------------------------------------- 1 | #!/usr/bin/expect -f 2 | 3 | source "expect/lib/helpers.tcl" 4 | 5 | spawn "resty-repl" 6 | 7 | expect_prompt_line 1 8 | 9 | input "c = coroutine.create(function(c_arg) require('resty.repl').start() end)" 10 | 11 | expect_prompt_line 2 12 | 13 | input "coroutine.resume(c, 'foo')" 14 | 15 | expect_prompt_line 1 16 | 17 | input "c_arg" 18 | 19 | expect_or_fail "=> foo" 20 | 21 | exit_repl 22 | -------------------------------------------------------------------------------- /expect/run_from_history_on_fail.exp: -------------------------------------------------------------------------------- 1 | #!/usr/bin/expect -f 2 | 3 | source "expect/lib/helpers.tcl" 4 | 5 | spawn "resty-repl" 6 | 7 | expect_prompt_line 1 8 | 9 | input "x + x" 10 | expect_or_fail {ERROR: \[string "stdin"]:1: attempt to perform arithmetic on global 'x' (a nil value)} 11 | 12 | input "x = 1" 13 | expect_or_fail "=> nil" 14 | 15 | # press UP Arrow key twice, get "x + x" from history 16 | press_up_arrow_key 2 17 | press_enter_key 18 | 19 | expect_or_fail "=> 2" 20 | 21 | exit_repl 22 | -------------------------------------------------------------------------------- /expect/run_from_history_on_success.exp: -------------------------------------------------------------------------------- 1 | #!/usr/bin/expect -f 2 | 3 | source "expect/lib/helpers.tcl" 4 | 5 | spawn "resty-repl" 6 | 7 | expect_prompt_line 1 8 | 9 | input "1 + 1" 10 | expect_or_fail "=> 2" 11 | 12 | expect_prompt_line 2 13 | 14 | press_up_arrow_key 15 | expect_or_fail "1 + 1" 16 | 17 | press_enter_key 18 | expect_or_fail "=> 2" 19 | 20 | expect_prompt_line 3 21 | 22 | exit_repl 23 | -------------------------------------------------------------------------------- /expect/show_context.exp: -------------------------------------------------------------------------------- 1 | #!/usr/bin/expect -f 2 | 3 | source "expect/lib/helpers.tcl" 4 | 5 | spawn luajit ./expect/test_data/example1.lua 6 | 7 | # show context and prompt: 8 | expect_or_fail "\r 9 | From: ./expect/test_data/example1.lua @ line 7\r 10 | \r 11 | 2: local b = 2\r 12 | 3: \r 13 | 4: assert(y ~= b)\r 14 | 5: \r 15 | 6: local function f(x)\r 16 | => 7: require('resty.repl').start()\r 17 | 8: print('x arg: ' .. tostring(x))\r 18 | 9: end\r 19 | 10: \r 20 | 11: f(123)\r 21 | \r\n" 22 | expect_prompt_line 1 23 | 24 | # show the value of argument "x": 25 | input "x" 26 | expect_or_fail "=> 123" 27 | expect_prompt_line 2 28 | 29 | # override the value of argument "x": 30 | input "x = 11" 31 | expect_or_fail "=> nil" 32 | expect_prompt_line 3 33 | 34 | exit_repl 35 | -------------------------------------------------------------------------------- /expect/test_data/example1.lua: -------------------------------------------------------------------------------- 1 | local y = 1 2 | local b = 2 3 | 4 | assert(y ~= b) 5 | 6 | local function f(x) 7 | require('resty.repl').start() 8 | print('x arg: ' .. tostring(x)) 9 | end 10 | 11 | f(123) 12 | -------------------------------------------------------------------------------- /lib/resty/repl.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env resty 2 | 3 | -- USAGE: 4 | -- PUT INTO CODE: 5 | -- require('resty.repl').start() 6 | -- 7 | -- OR: 8 | -- run: resty -e "require('resty.repl').start()" 9 | -- 10 | -- TODO: better completion 11 | -- TODO: color output 12 | -- TODO: add specs 13 | 14 | local new_binding = require('resty.repl.binding').new 15 | local new_ui = require('resty.repl.ui').new 16 | local formatter = require 'resty.repl.formatter' 17 | 18 | local running = false 19 | local binding, ui 20 | 21 | local function stop() 22 | running = false 23 | end 24 | 25 | local function exit(exit_code) 26 | stop() 27 | return os.exit(exit_code or 0) 28 | end 29 | 30 | local function handle_input(input) 31 | if input.exit then exit(0) end 32 | if input.stop then return stop() end 33 | 34 | if input.code then 35 | local result = binding:eval(input.code) 36 | formatter.print(result, #input.code) 37 | return result 38 | end 39 | end 40 | 41 | local function start() 42 | running = true 43 | 44 | local caller_info = debug.getinfo(2) 45 | 46 | binding = new_binding(caller_info) 47 | ui = new_ui(binding) 48 | 49 | while running do 50 | local input = ui:readline() 51 | local result = handle_input(input) 52 | 53 | if result then 54 | ui:add_to_history(input.code) 55 | end 56 | end 57 | end 58 | 59 | return { 60 | _VERSION = '0.1', 61 | start = start, 62 | stop = stop, 63 | handle_input = handle_input, 64 | running = running, 65 | ui = ui, 66 | binding = binding, 67 | } 68 | -------------------------------------------------------------------------------- /lib/resty/repl/binding.lua: -------------------------------------------------------------------------------- 1 | local _M = {} 2 | 3 | local compile = require('resty.repl.compiler').compile 4 | local InstanceMethods = {} 5 | 6 | local table_pack = function(...) 7 | return { n = select('#', ...), ... } 8 | end 9 | 10 | -- FIXME: DRY 11 | local function safe_match(str, re) 12 | local ok, res = pcall(function() 13 | return string.match(str, re) 14 | end) 15 | 16 | return ok and res 17 | end 18 | 19 | local result_mt = {} 20 | function result_mt:is_success() 21 | return true == self[1] 22 | end 23 | 24 | function result_mt:value() 25 | if not self:is_success() then return end 26 | 27 | if 3 > self.n then return self[2] end 28 | 29 | local res = {} 30 | for i = 2, self.n do 31 | table.insert(res, self[i]) 32 | end 33 | 34 | return res 35 | end 36 | 37 | function result_mt:err() 38 | if not self:is_success() then return self[2] end 39 | end 40 | 41 | function result_mt:has_return_value() 42 | return self.n > 1 43 | end 44 | 45 | _M.eval_result = { 46 | new = function (t) return setmetatable(t, { __index = result_mt }) end 47 | } 48 | 49 | local get_function_index = function(func) 50 | local caller_index = 1 51 | local i = caller_index 52 | 53 | while true do 54 | local info = debug.getinfo(i) 55 | if not info then break end 56 | 57 | if info.func == func then return i end 58 | 59 | i = i + 1 60 | end 61 | end 62 | 63 | function InstanceMethods:eval(code) 64 | local func, err = compile(code) 65 | 66 | local result 67 | 68 | if func and 'function' == type(func) then 69 | setfenv(func, self:get_fenv()) 70 | result = _M.eval_result.new(table_pack(pcall(func))) 71 | 72 | if result:is_success() then 73 | self.env._ = result:value() -- update last return result 74 | end 75 | else 76 | result = _M.eval_result.new { false, err, n = 2 } 77 | end 78 | 79 | return result 80 | end 81 | 82 | --- Local Vars: 83 | function InstanceMethods:find_local_var(name, match) 84 | local func = self.info.func 85 | if 'function' ~= type(func) then return end 86 | 87 | local function_index = get_function_index(func) 88 | 89 | -- Example: repl running from within coroutine (#26). 90 | if not function_index then return end 91 | 92 | local index = function_index - 1 93 | local i = 1 94 | local all_names = {} 95 | 96 | while true do 97 | local var_name, var_value = debug.getlocal(index, i) 98 | if not var_name then break end 99 | 100 | if match and safe_match(var_name, name) then 101 | table.insert(all_names, var_name) 102 | elseif name == var_name then 103 | -- "index - 1" is because stack will become deeper for a caller 104 | return true, var_name, var_value, index - 1, i 105 | end 106 | 107 | i = i + 1 108 | end 109 | 110 | if match then return all_names end 111 | end 112 | 113 | function InstanceMethods:set_local_var(name, value) 114 | local ok, _, _, index, var_index = self:find_local_var(name) 115 | 116 | if ok then 117 | debug.setlocal(index, var_index, value) 118 | return true 119 | end 120 | end 121 | 122 | --- Upvalues: 123 | function InstanceMethods:find_upvalue(name, match) 124 | local func = self.info.func 125 | if 'function' ~= type(func) then return end 126 | 127 | local i = 1 128 | local all_names = {} 129 | 130 | while true do 131 | local var_name, var_value = debug.getupvalue(func, i) 132 | if not var_name then break end 133 | 134 | if match and safe_match(var_name, name) then 135 | table.insert(all_names, var_name) 136 | elseif name == var_name then 137 | return true, var_name, var_value, i 138 | end 139 | 140 | i = i + 1 141 | end 142 | 143 | if match then return all_names end 144 | end 145 | 146 | function InstanceMethods:set_upvalue(name, new_value) 147 | local func = self.info.func 148 | local ok, _, _, var_index = self:find_upvalue(name) 149 | 150 | if ok then 151 | debug.setupvalue(func, var_index, new_value) 152 | return true 153 | end 154 | end 155 | 156 | function InstanceMethods:get_fenv() 157 | return setmetatable({}, { 158 | __index = function(_, key) 159 | local found_local, _, local_value = self:find_local_var(key) 160 | if found_local then return local_value end 161 | 162 | local found_upvalue, _, upvalue_value = self:find_upvalue(key) 163 | if found_upvalue then return upvalue_value end 164 | 165 | return self.env[key] 166 | end, 167 | __newindex = function(_, key, value) 168 | local set_local = self:set_local_var(key, value) 169 | if set_local then return value end 170 | 171 | local set_upvalue = self:set_upvalue(key, value) 172 | if set_upvalue then return value end 173 | 174 | self.env[key] = value 175 | 176 | return value 177 | end 178 | }) 179 | end 180 | 181 | function _M.new(caller_info) 182 | local binding = { info = caller_info, env = getfenv(caller_info.func) } 183 | 184 | return setmetatable(binding, { __index = InstanceMethods }) 185 | end 186 | 187 | return _M 188 | -------------------------------------------------------------------------------- /lib/resty/repl/compiler.lua: -------------------------------------------------------------------------------- 1 | local source = 'stdin' 2 | 3 | local function compile(code) 4 | if not code then code = '' end 5 | 6 | -- first, try to load function that returns value 7 | local code_function, err = loadstring('return ' .. code, source) 8 | 9 | -- if failed, load function that returns nil 10 | if not code_function then 11 | code_function, err = loadstring(code, source) 12 | end 13 | 14 | return code_function, err 15 | end 16 | 17 | 18 | return { compile = compile } 19 | -------------------------------------------------------------------------------- /lib/resty/repl/completer.lua: -------------------------------------------------------------------------------- 1 | local _M = {} 2 | 3 | local property_re = '^(.+)[:.]$' 4 | 5 | local function safe_match(str, re) 6 | local ok, res = pcall(function() 7 | return string.match(str, re) 8 | end) 9 | 10 | return ok and res 11 | end 12 | 13 | function _M:eval(text) 14 | local result = self.binding:eval(text) 15 | 16 | if result:is_success() then 17 | return result:value() 18 | end 19 | end 20 | 21 | function _M:smart_completion(result) 22 | if #result > 1 then 23 | return result 24 | else 25 | result = result[1] 26 | end 27 | 28 | local prop = self:eval(result) 29 | local prop_type = type(prop) 30 | 31 | if 'function' == prop_type then 32 | return { result .. '()' } 33 | else 34 | return { result } 35 | end 36 | end 37 | 38 | function _M:find_matches_var(word) 39 | local result = {} 40 | local re = '^' .. word 41 | 42 | -- locals 43 | for _, k in ipairs(self.binding:find_local_var(re, true)) do 44 | if safe_match(k, re) then table.insert(result, k) end 45 | end 46 | 47 | -- upvalues 48 | for _, k in ipairs(self.binding:find_upvalue(re, true)) do 49 | if safe_match(k, re) then table.insert(result, k) end 50 | end 51 | 52 | -- fenv 53 | for k, _ in pairs(self.binding.env) do 54 | if safe_match(k, re) then table.insert(result, k) end 55 | end 56 | 57 | -- _G 58 | for k, _ in pairs(_G) do 59 | if safe_match(k, re) then table.insert(result, k) end 60 | end 61 | 62 | -- _G metatable 63 | local _G_mt = getmetatable(_G) 64 | if 'table' == type(_G_mt) and 'table' == type(_G_mt.__index) then 65 | for k, _ in pairs(_G_mt.__index) do 66 | if safe_match(k, re) then table.insert(result, k) end 67 | end 68 | end 69 | 70 | -- search in commands 71 | for _, command in pairs(self.commands) do 72 | if safe_match(command, re) then table.insert(result, command) end 73 | end 74 | 75 | return self:smart_completion(result) 76 | end 77 | 78 | function _M.find_prop_in_object(object, options) 79 | local prop_prefix = options.prop_prefix 80 | local word = options.word 81 | local result = {} 82 | 83 | -- search for own methods 84 | for k, v in pairs(object) do 85 | local v_type = type(v) 86 | if 'function' == v_type then k = k .. '()' end 87 | if 'table' == v_type then k = k .. '.' end 88 | table.insert(result, { k, type(v) }) 89 | end 90 | 91 | -- search for meta methods 92 | local mt = getmetatable(object) 93 | if mt and 'table' == type(mt.__index) then 94 | for k, v in pairs(mt.__index) do 95 | local v_type = type(v) 96 | if 'function' == v_type then k = k .. '()' end 97 | if 'table' == v_type then k = k .. '.' end 98 | table.insert(result, { k, v_type }) 99 | end 100 | end 101 | 102 | -- filter by property prefix 103 | if prop_prefix then 104 | local not_filterd = result 105 | result = {} 106 | for _, key_value_pair in ipairs(not_filterd) do 107 | if safe_match(key_value_pair[1], '^' .. prop_prefix) then 108 | table.insert(result, key_value_pair) 109 | end 110 | end 111 | end 112 | 113 | -- filter by value type 114 | if word:match ':$' then -- completing method name 115 | local not_filterd = result 116 | result = {} 117 | for _, key_value_pair in ipairs(not_filterd) do 118 | if key_value_pair[2] == 'function' then 119 | table.insert(result, key_value_pair) 120 | end 121 | end 122 | end 123 | 124 | -- prepend with word 125 | for i, key_value_pair in ipairs(result) do 126 | result[i] = word .. key_value_pair[1] 127 | end 128 | 129 | return result 130 | end 131 | 132 | function _M:find_matches_prop(word, prop_prefix) 133 | if safe_match(word, property_re) then 134 | local base_obj_str = safe_match(word, property_re) 135 | local base_obj = self:eval(base_obj_str) 136 | if not base_obj then return end 137 | 138 | if 'function' == type(base_obj) then return { base_obj_str .. '()' } end 139 | 140 | -- we're trying to complete property name, so if base object 141 | -- is not a table, we just return it. 142 | if 'table' ~= type(base_obj) then return { base_obj_str } end 143 | 144 | local result = self.find_prop_in_object(base_obj, { 145 | prop_prefix = prop_prefix, 146 | word = word, 147 | }) 148 | 149 | return self:smart_completion(result) 150 | else 151 | local object, dot, prop = word:match('(.+)([.:])(.+)$') 152 | if (not object) or (not prop) then return end 153 | return self:find_matches_prop(object .. dot, prop) 154 | end 155 | end 156 | 157 | function _M:find_matches(word) 158 | -- don't compete from the function: some_func() 159 | if word:match('^[()]+$') then return end 160 | 161 | if word == '' or word:match('^[^.:]+$') then 162 | return self:find_matches_var(word) 163 | else 164 | return self:find_matches_prop(word) 165 | end 166 | end 167 | 168 | function _M.new(binding, commands) 169 | return setmetatable({ 170 | binding = binding, 171 | commands = commands, 172 | }, { __index = _M }) 173 | end 174 | 175 | return _M 176 | -------------------------------------------------------------------------------- /lib/resty/repl/formatter.lua: -------------------------------------------------------------------------------- 1 | local inspect = require 'inspect' 2 | local readline = require 'resty.repl.readline' 3 | 4 | local function output(result, code_len) 5 | local value = result:value() 6 | 7 | -- print an error if not success 8 | if not result:is_success() then 9 | local err = result:err() 10 | if 'table' == type(err) then err = inspect(err) end 11 | readline.puts('ERROR: ' .. err) 12 | return 13 | end 14 | 15 | -- don't print anything if there is just 16 | if not result:has_return_value() and 0 == code_len then return end 17 | 18 | if _G.ngx and (_G.ngx.null == value) then 19 | value = '' 20 | elseif 'table' == type(value) then 21 | value = inspect(value) 22 | end 23 | 24 | readline.puts('=> ' .. tostring(value)) 25 | end 26 | 27 | return { print = output } 28 | -------------------------------------------------------------------------------- /lib/resty/repl/readline.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env lua 2 | 3 | local readline_utils = require 'resty.repl.readline_utils' 4 | 5 | local no_readline_fallback = function() 6 | io.write 'No readline found. Fallback mode.\n' 7 | return require 'resty.repl.readline_stub' 8 | end 9 | 10 | local success, ffi = pcall(function() return require('ffi') end) 11 | if not success then return no_readline_fallback() end 12 | 13 | -- from unistd.h: 14 | local R_OK = 4 15 | local W_OK = 2 16 | local F_OK = 0 17 | 18 | ffi.cdef[[ 19 | /* libc definitions */ 20 | void* malloc(size_t bytes); 21 | void free(void *); 22 | 23 | /* stdio.h */ 24 | size_t fwrite(const void *, size_t, size_t, void*); 25 | 26 | /* unistd.h */ 27 | int access(const char *pathname, int mode); 28 | 29 | typedef void rl_vcpfunc_t (char *); 30 | 31 | char *readline (const char *prompt); 32 | 33 | /* basic history handling */ 34 | void add_history(const char *line); 35 | int write_history (const char *filename); 36 | int append_history (int nelements, const char *filename); 37 | int read_history (const char *filename); 38 | 39 | /* completion */ 40 | typedef char **rl_completion_func_t (const char *, int, int); 41 | typedef char *rl_compentry_func_t (const char *, int); 42 | typedef char **rl_hook_func_t (const char *, int); 43 | typedef char *rl_hook_func_t (const char *, int); 44 | 45 | char **rl_completion_matches (const char *, rl_compentry_func_t *); 46 | 47 | const char *rl_basic_word_break_characters; 48 | rl_completion_func_t *rl_attempted_completion_function; 49 | char *rl_line_buffer; 50 | int rl_completion_append_character; 51 | int rl_completion_suppress_append; 52 | int rl_attempted_completion_over; 53 | 54 | void rl_callback_handler_install (const char *prompt, rl_vcpfunc_t *lhandler); 55 | void rl_callback_read_char (void); 56 | void rl_callback_handler_remove (void); 57 | 58 | void* rl_outstream; 59 | 60 | int rl_set_prompt (const char *prompt); 61 | int rl_clear_message (void); 62 | void rl_replace_line (const char *text); 63 | int rl_message (const char *); 64 | int rl_delete_text (int start, int end); 65 | int rl_insert_text (const char *); 66 | int rl_forced_update_display (void); 67 | void rl_redisplay (void); 68 | int rl_point; 69 | 70 | rl_hook_func_t *rl_startup_hook; 71 | int rl_readline_state; 72 | ]] 73 | 74 | -- for builds with separate libhistory: 75 | pcall(ffi.load, 'libhistory.so.6') 76 | pcall(ffi.load, 'libhistory.so.7') 77 | pcall(ffi.load, 'libhistory') 78 | 79 | local readline_available, clib 80 | 81 | -- for linux with libreadline 6.x: 82 | readline_available, clib = pcall(ffi.load, 'libreadline.so.6') 83 | 84 | -- for linux with libreadline 7.x: 85 | if not readline_available then 86 | readline_available, clib = pcall(ffi.load, 'libreadline.so.7') 87 | end 88 | 89 | -- for mac: 90 | if not readline_available then 91 | readline_available, clib = pcall(ffi.load, 'libreadline') 92 | end 93 | 94 | if not readline_available then return no_readline_fallback() end 95 | 96 | local function history_file_is_writable() 97 | local history_fn = readline_utils.history_fn() 98 | local rw_access = bit.bor(R_OK, W_OK) 99 | local file_exist = 0 == clib.access(history_fn, F_OK) 100 | 101 | if file_exist then 102 | return 0 == clib.access(history_fn, rw_access) 103 | else -- check dir 104 | return 0 == clib.access(readline_utils.home_dir, rw_access) 105 | end 106 | end 107 | 108 | if not history_file_is_writable() then readline_utils.home_dir = '/tmp' end 109 | 110 | -- read history from file 111 | clib.read_history(readline_utils.history_fn()) 112 | 113 | local write = function(text) 114 | return clib.fwrite(text, #text, 1, clib.rl_outstream) 115 | end 116 | 117 | local puts = function(text) 118 | if nil == text then text = '' else text = tostring(text) end 119 | return write(text .. '\n') 120 | end 121 | 122 | local add_to_history = function(text) 123 | clib.add_history(text) 124 | if history_file_is_writable() then 125 | clib.write_history(readline_utils.history_fn()) 126 | end 127 | end 128 | 129 | local teardown = function() 130 | clib.rl_callback_handler_remove() 131 | end 132 | 133 | local function set_attempted_completion_function(callback) 134 | function clib.rl_attempted_completion_function(word) 135 | local strword = ffi.string(word) 136 | local buffer = ffi.string(clib.rl_line_buffer) 137 | 138 | local matches = callback(strword, buffer) 139 | 140 | if not matches then return nil end 141 | 142 | -- if matches is an empty array, tell readline to not call default completion (file) 143 | clib.rl_attempted_completion_over = 1 144 | pcall(function() clib.rl_completion_suppress_append = 1 end) 145 | 146 | -- translate matches table to C strings 147 | -- (there is probably more efficient ways to do it) 148 | return clib.rl_completion_matches(word, function(_, i) 149 | local match = matches[i + 1] 150 | 151 | if match then 152 | -- readline will free the C string by itself, so create copies of them 153 | local buf = ffi.C.malloc(#match + 1) 154 | ffi.copy(buf, match, #match + 1) 155 | return buf 156 | else 157 | return ffi.new('void*', nil) 158 | end 159 | end) 160 | end 161 | end 162 | 163 | local chars_to_string = function(chars) 164 | local result = ffi.string(chars) 165 | ffi.C.free(chars) 166 | return result 167 | end 168 | 169 | local readline = function(...) 170 | local chars = clib.readline(...) 171 | local line 172 | 173 | if chars ~= nil then 174 | line = chars_to_string(chars) 175 | end 176 | 177 | return line 178 | end 179 | 180 | local set_startup_hook = function(f) clib.rl_startup_hook = f end 181 | 182 | local _M = setmetatable({ 183 | set_startup_hook = set_startup_hook, 184 | teardown = teardown, 185 | puts = puts, 186 | set_attempted_completion_function = set_attempted_completion_function, 187 | add_to_history = add_to_history, 188 | }, { __call = function(_, ...) return readline(...) end }) 189 | 190 | return _M 191 | -------------------------------------------------------------------------------- /lib/resty/repl/readline_stub.lua: -------------------------------------------------------------------------------- 1 | local function teardown() 2 | end 3 | 4 | local function puts(text) 5 | if nil == text then 6 | text = '' 7 | else 8 | text = tostring(text) 9 | end 10 | 11 | return print(text) 12 | end 13 | 14 | local function set_attempted_completion_function() 15 | end 16 | 17 | local function add_to_history() 18 | end 19 | 20 | local function readline(prompt) 21 | io.write(prompt) 22 | io.stdout:flush() 23 | io.input(io.stdin) 24 | return io.read() 25 | end 26 | 27 | local set_startup_hook = function() end 28 | 29 | local _M = setmetatable({ 30 | set_startup_hook = set_startup_hook, 31 | teardown = teardown, 32 | puts = puts, 33 | set_attempted_completion_function = set_attempted_completion_function, 34 | add_to_history = add_to_history, 35 | }, { __call = function(_, ...) return readline(...) end }) 36 | 37 | return _M 38 | -------------------------------------------------------------------------------- /lib/resty/repl/readline_utils.lua: -------------------------------------------------------------------------------- 1 | local _M = {} 2 | 3 | local function find_home_dir() 4 | local home = os.getenv('HOME') or os.getenv('USERPROFILE') 5 | 6 | if nil == home and 'table' == type(_G.ngx) then 7 | home = _G.ngx.config.nginx_configure():match('--prefix=([^%s]+)') 8 | end 9 | 10 | if nil == home then home = '/tmp' end 11 | 12 | return home 13 | end 14 | 15 | _M.home_dir = find_home_dir() 16 | _M.history_file_name = '/.luahistory' 17 | 18 | _M.history_fn = function() 19 | return _M.home_dir .. _M.history_file_name 20 | end 21 | 22 | return _M 23 | -------------------------------------------------------------------------------- /lib/resty/repl/sources.lua: -------------------------------------------------------------------------------- 1 | local _M = {} 2 | 3 | local show_n_lines = 5 4 | 5 | local function get_source(info) 6 | local defined_in_file = 1 == info.source:find '@' 7 | if not defined_in_file then return '\n' end 8 | 9 | local from_no = info.currentline - show_n_lines 10 | local to_no = info.currentline + show_n_lines 11 | local number_len = #tostring(to_no) 12 | local result = { '\n' } 13 | 14 | local line_number = 1 15 | for line in io.lines(info.source:sub(2)) do 16 | if line_number >= from_no and line_number <= to_no then 17 | local prefix = string.rep(' ', 4) 18 | local line_number_s = string.format('%' .. number_len .. 'd', line_number) 19 | 20 | if line_number == info.currentline then prefix = ' => ' end 21 | 22 | table.insert(result, prefix .. line_number_s .. ': ' .. line) 23 | end 24 | line_number = line_number + 1 25 | end 26 | 27 | return table.concat(result, '\n') .. '\n' 28 | end 29 | 30 | function _M:whereami() 31 | local info = self.binding.info 32 | 33 | -- Don't show context of executable resty-repl started 34 | if info.short_src:find('bin/resty%-repl') then return end 35 | 36 | local heading = '\n' .. 37 | 'From: ' .. info.short_src .. ' @ line ' .. info.currentline 38 | 39 | return heading .. get_source(info) 40 | end 41 | 42 | function _M.new(binding) 43 | return setmetatable({ binding = binding }, { __index = _M }) 44 | end 45 | 46 | return _M 47 | -------------------------------------------------------------------------------- /lib/resty/repl/ui.lua: -------------------------------------------------------------------------------- 1 | local readline = require 'resty.repl.readline' 2 | local new_completer = require('resty.repl.completer').new 3 | local new_sources = require('resty.repl.sources').new 4 | 5 | local context = function() 6 | if _G.ngx and _G.ngx.get_phase then 7 | return 'ngx(' .. _G.ngx.get_phase() .. ')' 8 | else 9 | return 'lua(main)' 10 | end 11 | end 12 | 13 | local commands = {} 14 | 15 | commands[{nil, 'exit'}] = function(_, input) 16 | readline.teardown() 17 | readline.puts() 18 | input.stop = true 19 | end 20 | 21 | commands[{'exit!'}] = function(_, input) 22 | input.exit = true 23 | readline.teardown() 24 | end 25 | 26 | commands[{'whereami'}] = function(self, input) 27 | self:whereami() 28 | input.code = nil 29 | end 30 | 31 | local command_codes = {} 32 | for all_codes, _ in pairs(commands) do 33 | local codes_len = select('#', unpack(all_codes)) 34 | for i = 1, codes_len do 35 | local code = all_codes[i] 36 | if code then table.insert(command_codes, code) end 37 | end 38 | end 39 | 40 | local InstanceMethods = {} 41 | function InstanceMethods:readline() 42 | local input = { code = readline(self:prompt_line()) } 43 | 44 | for all_command_codes, command_handler in pairs(commands) do 45 | local codes_len = select('#', unpack(all_command_codes)) 46 | for i = 1, codes_len do 47 | if input.code == all_command_codes[i] then 48 | command_handler(self, input) 49 | return input 50 | end 51 | end 52 | end 53 | 54 | return input 55 | end 56 | 57 | function InstanceMethods:prompt_line() 58 | local res = '[' .. self.line_count .. '] ' .. context() .. '> ' 59 | self.line_count = self.line_count + 1 60 | return res 61 | end 62 | 63 | function InstanceMethods.add_to_history(_, text) 64 | readline.add_to_history(text) 65 | end 66 | 67 | function InstanceMethods:whereami() 68 | local ctx = self.sources:whereami() 69 | if ctx then readline.puts(ctx) end 70 | end 71 | 72 | local mt = { __index = InstanceMethods } 73 | 74 | local function new(binding) 75 | local ui = setmetatable({ 76 | completer = new_completer(binding, command_codes), 77 | sources = new_sources(binding), 78 | line_count = 1, 79 | }, mt) 80 | 81 | readline.set_attempted_completion_function(function(word) 82 | return ui.completer:find_matches(word) 83 | end) 84 | 85 | readline.set_startup_hook(function() 86 | if 2 == ui.line_count then ui:whereami() end 87 | end) 88 | 89 | return ui 90 | end 91 | 92 | return { new = new } 93 | -------------------------------------------------------------------------------- /lua-resty-repl-0.0.5-1.rockspec: -------------------------------------------------------------------------------- 1 | package = 'lua-resty-repl' 2 | version = '0.0.5-1' 3 | source = { 4 | url = 'git://github.com/saks/lua-resty-repl', 5 | tag = 'v0.0.5-1' 6 | } 7 | description = { 8 | summary = 'repl for openresty.', 9 | detailed = [[ 10 | Repl with locals, upvalue and global env access, can be started from 11 | anywhere with require('resty.repl').start(). It depends on 12 | https://github.com/kikito/inspect.lua library. 13 | ]], 14 | homepage = 'https://github.com/saks/lua-resty-repl', 15 | license = 'MIT ' 16 | } 17 | dependencies = { 18 | 'lua >= 5.1' 19 | } 20 | build = { 21 | type = 'builtin', 22 | modules = { 23 | ['resty.repl'] = 'lib/resty/repl.lua', 24 | ['resty.repl.binding'] = 'lib/resty/repl/binding.lua', 25 | ['resty.repl.readline'] = 'lib/resty/repl/readline.lua', 26 | ['resty.repl.readline_stub'] = 'lib/resty/repl/readline_stub.lua', 27 | ['resty.repl.readline_utils'] = 'lib/resty/repl/readline_utils.lua', 28 | ['resty.repl.compiler'] = 'lib/resty/repl/compiler.lua', 29 | ['resty.repl.formatter'] = 'lib/resty/repl/formatter.lua', 30 | ['resty.repl.ui'] = 'lib/resty/repl/ui.lua', 31 | ['resty.repl.completer'] = 'lib/resty/repl/completer.lua', 32 | ['inspect'] = 'vendor/inspect.lua' 33 | }, 34 | install = { 35 | bin = { 36 | ['resty-repl'] = 'bin/resty-repl' 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lua-resty-repl-0.0.6-0.rockspec: -------------------------------------------------------------------------------- 1 | package = 'lua-resty-repl' 2 | version = '0.0.6-0' 3 | source = { 4 | url = 'git://github.com/saks/lua-resty-repl', 5 | tag = 'v0.0.6-0' 6 | } 7 | description = { 8 | summary = 'repl for openresty.', 9 | detailed = [[ 10 | Repl with locals, upvalue and global env access, can be started from 11 | anywhere with require('resty.repl').start(). It depends on 12 | https://github.com/kikito/inspect.lua library. 13 | ]], 14 | homepage = 'https://github.com/saks/lua-resty-repl', 15 | license = 'MIT ' 16 | } 17 | dependencies = { 18 | 'lua >= 5.1' 19 | } 20 | build = { 21 | type = 'builtin', 22 | modules = { 23 | ['resty.repl'] = 'lib/resty/repl.lua', 24 | ['resty.repl.binding'] = 'lib/resty/repl/binding.lua', 25 | ['resty.repl.readline'] = 'lib/resty/repl/readline.lua', 26 | ['resty.repl.readline_stub'] = 'lib/resty/repl/readline_stub.lua', 27 | ['resty.repl.readline_utils'] = 'lib/resty/repl/readline_utils.lua', 28 | ['resty.repl.sources'] = 'lib/resty/repl/sources.lua', 29 | ['resty.repl.compiler'] = 'lib/resty/repl/compiler.lua', 30 | ['resty.repl.formatter'] = 'lib/resty/repl/formatter.lua', 31 | ['resty.repl.ui'] = 'lib/resty/repl/ui.lua', 32 | ['resty.repl.completer'] = 'lib/resty/repl/completer.lua', 33 | ['inspect'] = 'vendor/inspect.lua' 34 | }, 35 | install = { 36 | bin = { 37 | ['resty-repl'] = 'bin/resty-repl' 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lua-resty-repl-scm-0.rockspec: -------------------------------------------------------------------------------- 1 | package = 'lua-resty-repl' 2 | version = 'scm-0' 3 | source = { 4 | url = 'git://github.com/saks/lua-resty-repl', 5 | branch = 'master' 6 | } 7 | description = { 8 | summary = 'repl for openresty.', 9 | detailed = [[ 10 | Repl with locals, upvalue and global env access, can be started from 11 | anywhere with require('resty.repl').start(). It depends on 12 | https://github.com/kikito/inspect.lua library. 13 | ]], 14 | homepage = 'https://github.com/saks/lua-resty-repl', 15 | license = 'MIT ' 16 | } 17 | dependencies = { 18 | 'lua >= 5.1' 19 | } 20 | build = { 21 | type = 'builtin', 22 | modules = { 23 | ['resty.repl'] = 'lib/resty/repl.lua', 24 | ['resty.repl.binding'] = 'lib/resty/repl/binding.lua', 25 | ['resty.repl.readline'] = 'lib/resty/repl/readline.lua', 26 | ['resty.repl.readline_stub'] = 'lib/resty/repl/readline_stub.lua', 27 | ['resty.repl.readline_utils'] = 'lib/resty/repl/readline_utils.lua', 28 | ['resty.repl.sources'] = 'lib/resty/repl/sources.lua', 29 | ['resty.repl.compiler'] = 'lib/resty/repl/compiler.lua', 30 | ['resty.repl.formatter'] = 'lib/resty/repl/formatter.lua', 31 | ['resty.repl.ui'] = 'lib/resty/repl/ui.lua', 32 | ['resty.repl.completer'] = 'lib/resty/repl/completer.lua', 33 | ['inspect'] = 'vendor/inspect.lua' 34 | }, 35 | install = { 36 | bin = { 37 | ['resty-repl'] = 'bin/resty-repl' 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /spec/lib/resty/binding_spec.lua: -------------------------------------------------------------------------------- 1 | local repl_binding = require 'resty.repl.binding' 2 | 3 | describe('resty repl binding', function() 4 | local caller_info 5 | local repl_code 6 | local eval_result 7 | local binding 8 | 9 | local eval_with_binding = function() 10 | binding = repl_binding.new(caller_info) 11 | eval_result = binding:eval(repl_code) 12 | end 13 | 14 | local outer_function = function(outer_arg) 15 | local outer_local = 'outer_local' 16 | local invisible_outer_local = 'invisible_outer_local' 17 | local upvalue_false = false 18 | 19 | local caller_function = function(caller_arg) 20 | local caller_local = 'caller_local' 21 | local caller_local_nil = nil 22 | local caller_local_false = false 23 | 24 | assert(caller_local_nil == nil) 25 | assert(caller_local_false == false) 26 | assert(upvalue_false == false) 27 | 28 | caller_info = debug.getinfo(1) 29 | 30 | eval_with_binding() 31 | 32 | return { 33 | caller_local = caller_local, 34 | caller_arg = caller_arg, 35 | caller_local_nil = caller_local_nil, 36 | caller_local_nil_type = type(caller_local_nil), 37 | caller_local_false = caller_local_false, 38 | caller_local_false_type = type(caller_local_false), 39 | outer_local = outer_local, 40 | outer_arg = outer_arg, 41 | } 42 | end 43 | 44 | local result = caller_function('caller_arg_value') 45 | result.invisible_outer_local = invisible_outer_local 46 | 47 | return result 48 | end 49 | 50 | local run = function(code) 51 | repl_code = code 52 | local outer_func_ret = outer_function 'outer_arg' 53 | return eval_result, outer_func_ret 54 | end 55 | 56 | it('should calculate simple expression', function() 57 | local result = run '1 + 1' 58 | assert.are_same({ true, 2, n = 2 }, result) 59 | end) 60 | 61 | it('should return locals', function() 62 | local _, outer_func_ret = run 'caller_local' 63 | assert.are_equal('caller_local', outer_func_ret.caller_local) 64 | end) 65 | 66 | it('should return locals with nil value', function() 67 | local result = run 'caller_local_nil' 68 | assert.are_same({ true, nil, n = 2 }, result) 69 | end) 70 | 71 | it('should return locals with false value', function() 72 | local result = run 'caller_local_false' 73 | assert.are_same({ true, false, n = 2 }, result) 74 | end) 75 | 76 | it('should return upvalues with false value', function() 77 | local result = run 'upvalue_false' 78 | assert.are_same({ true, false, n = 2 }, result) 79 | end) 80 | 81 | it('should be able to set locals with false value', function() 82 | local result = run 'caller_local_false = 123; return caller_local_false' 83 | assert.are_same({ true, 123, n = 2 }, result) 84 | end) 85 | 86 | it('should be able to set upvalues with false value', function() 87 | local result = run 'upvalue_false = 123; return upvalue_false' 88 | assert.are_same({ true, 123, n = 2 }, result) 89 | end) 90 | 91 | it('should return locals with nil value even if global is not nil', function() 92 | local _, outer_func_ret = run '_G.caller_local_nil = 123' 93 | assert.are_equal('nil', outer_func_ret.caller_local_nil_type) 94 | end) 95 | 96 | it('should set locals with nil value', function() 97 | local _, outer_func_ret = run 'caller_local_nil = 123' 98 | 99 | assert.are_equal('number', outer_func_ret.caller_local_nil_type) 100 | assert.are_equal(123, outer_func_ret.caller_local_nil) 101 | end) 102 | 103 | it('should return local arg', function() 104 | local result, outer_func_ret = run 'caller_arg' 105 | 106 | assert.are_same({ true, 'caller_arg_value', n = 2 }, result) 107 | assert.are_equal('caller_arg_value', outer_func_ret.caller_arg) 108 | end) 109 | 110 | it('should return upvalue', function() 111 | local result, outer_func_ret = run 'outer_local' 112 | 113 | assert.are_same({ true, 'outer_local', n = 2 }, result) 114 | assert.are_equal('outer_local', outer_func_ret.outer_local) 115 | end) 116 | 117 | it('should return upvalue arg', function() 118 | local result, outer_func_ret = run 'outer_arg' 119 | 120 | assert.are_same({ true, 'outer_arg', n = 2 }, result) 121 | assert.are_equal('outer_arg', outer_func_ret.outer_arg) 122 | end) 123 | 124 | it('should update locals', function() 125 | local result, outer_func_ret = run 'caller_local = 123' 126 | 127 | assert.are_same({ true, n = 1 }, result) -- no ret value 128 | assert.are_equal(123, outer_func_ret.caller_local) 129 | end) 130 | 131 | it('should update local args', function() 132 | local result, outer_func_ret = run 'caller_arg = "123"; return caller_arg' 133 | 134 | assert.are_same({ true, '123', n = 2 }, result) 135 | assert.are_equal('123', outer_func_ret.caller_arg) 136 | end) 137 | 138 | it('should update upvalues', function() 139 | local result, outer_func_ret = run 'outer_local = 123' 140 | 141 | assert.are_same({ true, n = 1 }, result) -- no ret value 142 | assert.are_equal(123, outer_func_ret.outer_local) 143 | end) 144 | 145 | it('should update upvalues with return', function() 146 | local result, outer_func_ret = run 'outer_local = 123; return outer_local' 147 | 148 | assert.are_same({ true, 123, n = 2 }, result) 149 | assert.are_equal(123, outer_func_ret.outer_local) 150 | end) 151 | 152 | it('should update upvalues with ret value nil', function() 153 | local result, outer_func_ret = run 'outer_local = 123; return nil' 154 | 155 | assert.are_same({ true, nil, n = 2 }, result) 156 | assert.are_equal(123, outer_func_ret.outer_local) 157 | end) 158 | 159 | it('should update upargs', function() 160 | local result, outer_func_ret = run 'outer_arg = 123' 161 | 162 | assert.are_same({ true, n = 1 }, result) -- no ret value 163 | assert.are_equal(123, outer_func_ret.outer_arg) 164 | end) 165 | 166 | it('should read from fenv', function() 167 | local result = run 'foo' 168 | assert.are_same({ true, nil, n = 2 }, result) 169 | 170 | binding.env.foo = 'foo' 171 | 172 | result = run 'foo' 173 | assert.are_same({ true, 'foo', n = 2 }, result) 174 | end) 175 | 176 | it('should update last return value into `_`', function() 177 | local result = run '_' 178 | assert.are_same({ true, nil, n = 2 }, result) 179 | 180 | run '123' 181 | 182 | result = run '_' 183 | assert.are_same({ true, 123, n = 2 }, result) 184 | end) 185 | 186 | it('should remember vars between runs', function() 187 | local result = run 'a' 188 | assert.are_same({ true, nil, n = 2 }, result) 189 | 190 | run 'a = 123' 191 | 192 | result = run 'a' 193 | assert.are_same({ true, 123, n = 2 }, result) 194 | end) 195 | 196 | it('should compile more complicated expressions', function() 197 | local result = run 'f=function() return 123 end; return f()' 198 | assert.are_same({ true, 123, n = 2 }, result) 199 | end) 200 | 201 | context('eval result', function() 202 | context('success', function() 203 | it('should be success', function() 204 | local res = repl_binding.eval_result.new { true, n = 1 } 205 | assert.is_true(res:is_success()) 206 | end) 207 | 208 | it('should have value', function() 209 | local res = repl_binding.eval_result.new { true, 'foo', n = 2 } 210 | assert.are_equal('foo', res:value()) 211 | end) 212 | 213 | it('should have no value', function() 214 | local res = repl_binding.eval_result.new { true, n = 1 } 215 | assert.is_nil(res:value()) 216 | end) 217 | 218 | it('should have no return values', function() 219 | local res = repl_binding.eval_result.new { true, n = 1 } 220 | assert.is_false(res:has_return_value()) 221 | end) 222 | 223 | it('should have no error because success with no ret value', function() 224 | local res = repl_binding.eval_result.new { true, n = 1 } 225 | assert.is_nil(res:err()) 226 | end) 227 | 228 | it('should have no error because success with ret value', function() 229 | local res = repl_binding.eval_result.new { true, 'foo', n = 2 } 230 | assert.is_nil(res:err()) 231 | end) 232 | 233 | it('should have no error because success with ret value', function() 234 | local res = repl_binding.eval_result.new { true, 'foo', n = 2 } 235 | assert.is_nil(res:err()) 236 | end) 237 | 238 | it('should have table value if table returned', function() 239 | local res = repl_binding.eval_result.new { true, { 1, 2 }, n = 2 } 240 | assert.are_same({ 1, 2 }, res:value()) 241 | end) 242 | 243 | it('should have table value if more returned', function() 244 | local res = repl_binding.eval_result.new { true, 1, 2, n = 3 } 245 | assert.are_same({ 1, 2 }, res:value()) 246 | end) 247 | 248 | it('should have table value if many tables returned', function() 249 | local res = repl_binding.eval_result.new { 250 | true, 251 | { 1, 2 }, 252 | { 3, 4 }, 253 | n = 3 254 | } 255 | assert.are_same({ { 1, 2 }, { 3, 4 } }, res:value()) 256 | end) 257 | end) 258 | 259 | context('not success', function() 260 | it('should not be success', function() 261 | local res = repl_binding.eval_result.new { false, n = 1 } 262 | assert.is_false(res:is_success()) 263 | end) 264 | 265 | it('should have no value with error without ret value', function() 266 | local res = repl_binding.eval_result.new { false, n = 1 } 267 | assert.is_nil(res:value()) 268 | end) 269 | 270 | it('should have no value with error', function() 271 | local res = repl_binding.eval_result.new { false, 'foo', n = 2 } 272 | assert.is_nil(res:value()) 273 | end) 274 | 275 | it('should have no error without error specified', function() 276 | local res = repl_binding.eval_result.new { false, n = 1 } 277 | assert.is_nil(res:err()) 278 | end) 279 | 280 | it('should have error', function() 281 | local res = repl_binding.eval_result.new { false, 'foo', n = 2 } 282 | assert.are_equal('foo', res:err()) 283 | end) 284 | 285 | it('should have no return values', function() 286 | local res = repl_binding.eval_result.new { false, n = 1 } 287 | assert.is_false(res:has_return_value()) 288 | end) 289 | 290 | it('should have return values with error', function() 291 | local res = repl_binding.eval_result.new { false, 'foo', n = 2 } 292 | assert.is_true(res:has_return_value()) 293 | end) 294 | end) 295 | end) 296 | end) 297 | -------------------------------------------------------------------------------- /spec/lib/resty/compiler_spec.lua: -------------------------------------------------------------------------------- 1 | local compile = require('resty.repl.compiler').compile 2 | 3 | describe('compiler', function() 4 | it('should compile simple expression', function() 5 | assert.are_equal(compile('1 + 1')(), 2) 6 | end) 7 | 8 | it('should compile expression with return', function() 9 | assert.are_equal(compile('a = 1; return a')(), 1) 10 | end) 11 | 12 | it('should compile expression without return', function() 13 | assert.are_equal(compile('a = 1')(), nil) 14 | end) 15 | 16 | it('should compile expression with function', function() 17 | local res = compile('function() end')() 18 | assert.are_equal('function', type(res)) 19 | end) 20 | 21 | it('should compile expression with table', function() 22 | local res = compile('setmetatable({}, { __index = { a = 1 } })')() 23 | assert.are_equal(1, res.a) 24 | end) 25 | 26 | it('should handle empty string', function() 27 | assert.is_nil(compile('')()) 28 | end) 29 | 30 | it('should handle nil', function() 31 | assert.is_nil(compile(nil)()) 32 | end) 33 | 34 | it('should handle no args', function() 35 | assert.is_nil(compile()()) 36 | end) 37 | end) 38 | -------------------------------------------------------------------------------- /spec/lib/resty/completer_spec.lua: -------------------------------------------------------------------------------- 1 | local new_binding = require('resty.repl.binding').new 2 | local new_completer = require('resty.repl.completer').new 3 | local binding, completer, result, word 4 | local commands = {} 5 | 6 | local test_completer = function() 7 | local info = debug.getinfo(2) 8 | binding = new_binding(info) 9 | completer = new_completer(binding, commands) 10 | result = completer:find_matches(word) 11 | end 12 | 13 | local upvalue_with_mt = setmetatable({ foo = 'bar' }, { 14 | __index = { bar = 'foo' } 15 | }) 16 | 17 | local example_function = function(local_arg) 18 | assert(local_arg) 19 | assert(new_binding) 20 | assert(new_completer) 21 | assert(upvalue_with_mt) 22 | 23 | local local_false = false 24 | 25 | assert(local_false == false) 26 | 27 | local myngx = { 28 | req = { get_body_data = function() end, get_body_file = function() end }, 29 | ERR = 1, 30 | ERROR = 2, 31 | } 32 | 33 | function myngx:print() assert(self) end 34 | local _mt = {} 35 | function _mt:xprint() assert(self) end 36 | 37 | setmetatable(myngx, { __index = _mt }) 38 | 39 | test_completer() 40 | 41 | return myngx 42 | end 43 | 44 | local complete = function(t_word) 45 | word = t_word 46 | example_function(123) 47 | return result 48 | end 49 | 50 | describe('repl completer', function() 51 | it('should complete local vars', function() 52 | assert.are_same({ 'myngx' }, complete 'myn') 53 | local res = complete 'myngx.' 54 | table.sort(res) 55 | assert.are_same( 56 | { 57 | 'myngx.ERR', 58 | 'myngx.ERROR', 59 | 'myngx.print()', 60 | 'myngx.req.', 61 | 'myngx.xprint()' 62 | }, 63 | res 64 | ) 65 | assert.are_same({ 'myngx.req.' }, complete 'myngx.re') 66 | assert.are_same( 67 | { 'myngx.req.get_body_file()', 'myngx.req.get_body_data()' }, 68 | complete 'myngx.req.get_' 69 | ) 70 | assert.are_same({ 'myngx.req.get_body_data()' }, 71 | complete 'myngx.req.get_body_d') 72 | end) 73 | 74 | it('should complete local args', function() 75 | assert.are_same({ 'local_arg', 'local_false' }, complete 'loc') 76 | end) 77 | 78 | it('should complete upvalues', function() 79 | assert.are_same({ 'new_binding', 'new_completer' }, complete 'new_') 80 | assert.are_same({ 'new_binding()' }, complete 'new_b') 81 | end) 82 | 83 | it('should complete globals', function() 84 | assert.are_same({ 'debug.getlocal()' }, complete 'debug.getl') 85 | end) 86 | 87 | -- TODO: more specs 88 | 89 | context('object with metatable', function() 90 | it('should complete own keys', function() 91 | assert.are_same({ 'upvalue_with_mt.foo' }, complete 'upvalue_with_mt.f') 92 | end) 93 | 94 | it('should complete meta keys', function() 95 | assert.are_same({ 'upvalue_with_mt.bar' }, complete 'upvalue_with_mt.b') 96 | end) 97 | end) 98 | 99 | context('similar prefix fields', function() 100 | it('should complete all of them', function() 101 | local expected = { 'myngx.ERROR', 'myngx.ERR' } 102 | local actual = complete 'myngx.ERR' 103 | 104 | table.sort(expected) 105 | table.sort(actual) 106 | 107 | assert.are_same(expected, actual) 108 | end) 109 | end) 110 | 111 | context('values in _G', function() 112 | before_each(function() _G.foo = { bar = 'buz' } end) 113 | after_each(function() _G.foo = nil end) 114 | 115 | it('should complete', function() 116 | assert.are_same({ 'foo' }, complete 'fo') 117 | assert.are_same({ 'foo.bar' }, complete 'foo.') 118 | assert.are_same({ 'foo.bar' }, complete 'foo.bar.b') 119 | end) 120 | end) 121 | 122 | context('values in _G metatable', function() 123 | local _G_mt = getmetatable(_G) 124 | 125 | before_each(function() 126 | setmetatable(_G, { __index = { foo = { bar = 'buz' } }}) 127 | end) 128 | 129 | after_each(function() setmetatable(_G, _G_mt) end) 130 | 131 | it('should complete', function() 132 | assert.are_same({ 'foo' }, complete 'fo') 133 | assert.are_same({ 'foo.bar' }, complete 'foo.') 134 | assert.are_same({ 'foo.bar' }, complete 'foo.bar.b') 135 | end) 136 | end) 137 | 138 | context('methods', function() 139 | it('should complete list of methods', function() 140 | assert.are_same({ 'myngx:print()', 'myngx:xprint()' }, complete 'myngx:') 141 | end) 142 | 143 | it('should complete method name', function() 144 | assert.are_same({ 'myngx:print()' }, complete 'myngx:pri') 145 | assert.are_same({ 'myngx:xprint()' }, complete 'myngx:xp') 146 | end) 147 | end) 148 | 149 | context('edge cases', function() 150 | it('should not fail', function() 151 | assert.are_same({}, complete 'myngx:xprint(') 152 | end) 153 | end) 154 | end) 155 | -------------------------------------------------------------------------------- /spec/lib/resty/formatter_spec.lua: -------------------------------------------------------------------------------- 1 | local readline = require 'resty.repl.readline' 2 | local formatter = require 'resty.repl.formatter' 3 | local eval_result = require('resty.repl.binding').eval_result 4 | 5 | describe('formatter', function() 6 | local debug = false 7 | 8 | before_each(function() 9 | stub(readline, 'puts', function(text) 10 | if debug then print(text) end 11 | end) 12 | end) 13 | 14 | after_each(function() 15 | readline.puts:revert() 16 | debug = false 17 | end) 18 | 19 | it('should print string', function() 20 | formatter.print(eval_result.new { true, 'foo', n = 2 }, 10) 21 | 22 | assert.stub(readline.puts).was_called(1) 23 | assert.stub(readline.puts).was_called_with '=> foo' 24 | end) 25 | 26 | it('should print number', function() 27 | formatter.print(eval_result.new { true, 123, n = 2 }, 10) 28 | 29 | assert.stub(readline.puts).was_called(1) 30 | assert.stub(readline.puts).was_called_with '=> 123' 31 | end) 32 | 33 | it('should print ngx.null', function() 34 | local _G_ngx = _G.ngx 35 | local ngx 36 | 37 | if _G_ngx then 38 | ngx = _G_ngx 39 | else 40 | ngx = { null = {} } 41 | _G.ngx = ngx 42 | end 43 | 44 | formatter.print(eval_result.new { true, ngx.null, n = 2 }, 10) 45 | 46 | assert.stub(readline.puts).was_called(1) 47 | assert.stub(readline.puts).was_called_with '=> ' 48 | 49 | _G.ngx = _G_ngx 50 | end) 51 | 52 | it('should print table', function() 53 | formatter.print(eval_result.new { true, { 1, 2, a = 1 }, n = 2 }, 10) 54 | 55 | assert.stub(readline.puts).was_called(1) 56 | assert.stub(readline.puts).was_called_with '=> { 1, 2,\n a = 1\n}' 57 | end) 58 | 59 | it('should print nil', function() 60 | formatter.print(eval_result.new { true, nil, n = 2 }, 10) 61 | 62 | assert.stub(readline.puts).was_called(1) 63 | assert.stub(readline.puts).was_called_with('=> nil') 64 | end) 65 | 66 | it('should print nil in the table', function() 67 | formatter.print(eval_result.new { true, { nil, 'err' }, n = 2 }, 10) 68 | 69 | assert.stub(readline.puts).was_called(1) 70 | assert.stub(readline.puts).was_called_with('=> {\n [2] = "err"\n}') 71 | end) 72 | 73 | it('should print nothing if no args', function() 74 | formatter.print(eval_result.new { true, n = 1 }, 0) 75 | 76 | assert.stub(readline.puts).was_not_called() 77 | end) 78 | 79 | it('should print string error', function() 80 | formatter.print(eval_result.new { false, 'foo', n = 1 }, 10) 81 | 82 | assert.stub(readline.puts).was_called(1) 83 | assert.stub(readline.puts).was_called_with 'ERROR: foo' 84 | end) 85 | 86 | it('should print table error', function() 87 | formatter.print(eval_result.new { false, { foo = 'bar' }, n = 1 }, 10) 88 | 89 | assert.stub(readline.puts).was_called(1) 90 | assert.stub(readline.puts).was_called_with 'ERROR: {\n foo = "bar"\n}' 91 | end) 92 | end) 93 | -------------------------------------------------------------------------------- /spec/lib/resty/readline_spec.lua: -------------------------------------------------------------------------------- 1 | describe('resty repl readline', function() 2 | it('should puts numbers', function() 3 | -- TODO 4 | end) 5 | end) 6 | -------------------------------------------------------------------------------- /spec/lib/resty/repl_spec.lua: -------------------------------------------------------------------------------- 1 | local repl = require 'resty.repl' 2 | 3 | describe('resty repl', function() 4 | it('should work', function() 5 | assert.are_equal('function', type(repl.start)) 6 | end) 7 | end) 8 | -------------------------------------------------------------------------------- /spec/spec_helper.lua: -------------------------------------------------------------------------------- 1 | package.path = package.path ..';/lua-resty-repl/lib/?.lua;' 2 | package.path = package.path ..';/lua-resty-repl/vendor/?.lua;' 3 | -------------------------------------------------------------------------------- /vendor/inspect.lua: -------------------------------------------------------------------------------- 1 | local inspect ={ 2 | _VERSION = 'inspect.lua 3.1.0', 3 | _URL = 'http://github.com/kikito/inspect.lua', 4 | _DESCRIPTION = 'human-readable representations of tables', 5 | _LICENSE = [[ 6 | MIT LICENSE 7 | 8 | Copyright (c) 2013 Enrique García Cota 9 | 10 | Permission is hereby granted, free of charge, to any person obtaining a 11 | copy of this software and associated documentation files (the 12 | "Software"), to deal in the Software without restriction, including 13 | without limitation the rights to use, copy, modify, merge, publish, 14 | distribute, sublicense, and/or sell copies of the Software, and to 15 | permit persons to whom the Software is furnished to do so, subject to 16 | the following conditions: 17 | 18 | The above copyright notice and this permission notice shall be included 19 | in all copies or substantial portions of the Software. 20 | 21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 22 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 23 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 24 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 25 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 26 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 27 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 28 | ]] 29 | } 30 | 31 | local tostring = tostring 32 | 33 | inspect.KEY = setmetatable({}, {__tostring = function() return 'inspect.KEY' end}) 34 | inspect.METATABLE = setmetatable({}, {__tostring = function() return 'inspect.METATABLE' end}) 35 | 36 | -- Apostrophizes the string if it has quotes, but not aphostrophes 37 | -- Otherwise, it returns a regular quoted string 38 | local function smartQuote(str) 39 | if str:match('"') and not str:match("'") then 40 | return "'" .. str .. "'" 41 | end 42 | return '"' .. str:gsub('"', '\\"') .. '"' 43 | end 44 | 45 | -- \a => '\\a', \0 => '\\0', 31 => '\31' 46 | local shortControlCharEscapes = { 47 | ["\a"] = "\\a", ["\b"] = "\\b", ["\f"] = "\\f", ["\n"] = "\\n", 48 | ["\r"] = "\\r", ["\t"] = "\\t", ["\v"] = "\\v" 49 | } 50 | local longControlCharEscapes = {} -- \a => nil, \0 => \000, 31 => \031 51 | for i=0, 31 do 52 | local ch = string.char(i) 53 | if not shortControlCharEscapes[ch] then 54 | shortControlCharEscapes[ch] = "\\"..i 55 | longControlCharEscapes[ch] = string.format("\\%03d", i) 56 | end 57 | end 58 | 59 | local function escape(str) 60 | return (str:gsub("\\", "\\\\") 61 | :gsub("(%c)%f[0-9]", longControlCharEscapes) 62 | :gsub("%c", shortControlCharEscapes)) 63 | end 64 | 65 | local function isIdentifier(str) 66 | return type(str) == 'string' and str:match( "^[_%a][_%a%d]*$" ) 67 | end 68 | 69 | local function isSequenceKey(k, sequenceLength) 70 | return type(k) == 'number' 71 | and 1 <= k 72 | and k <= sequenceLength 73 | and math.floor(k) == k 74 | end 75 | 76 | local defaultTypeOrders = { 77 | ['number'] = 1, ['boolean'] = 2, ['string'] = 3, ['table'] = 4, 78 | ['function'] = 5, ['userdata'] = 6, ['thread'] = 7 79 | } 80 | 81 | local function sortKeys(a, b) 82 | local ta, tb = type(a), type(b) 83 | 84 | -- strings and numbers are sorted numerically/alphabetically 85 | if ta == tb and (ta == 'string' or ta == 'number') then return a < b end 86 | 87 | local dta, dtb = defaultTypeOrders[ta], defaultTypeOrders[tb] 88 | -- Two default types are compared according to the defaultTypeOrders table 89 | if dta and dtb then return defaultTypeOrders[ta] < defaultTypeOrders[tb] 90 | elseif dta then return true -- default types before custom ones 91 | elseif dtb then return false -- custom types after default ones 92 | end 93 | 94 | -- custom types are sorted out alphabetically 95 | return ta < tb 96 | end 97 | 98 | -- For implementation reasons, the behavior of rawlen & # is "undefined" when 99 | -- tables aren't pure sequences. So we implement our own # operator. 100 | local function getSequenceLength(t) 101 | local len = 1 102 | local v = rawget(t,len) 103 | while v ~= nil do 104 | len = len + 1 105 | v = rawget(t,len) 106 | end 107 | return len - 1 108 | end 109 | 110 | local function getNonSequentialKeys(t) 111 | local keys = {} 112 | local sequenceLength = getSequenceLength(t) 113 | for k,_ in pairs(t) do 114 | if not isSequenceKey(k, sequenceLength) then table.insert(keys, k) end 115 | end 116 | table.sort(keys, sortKeys) 117 | return keys, sequenceLength 118 | end 119 | 120 | local function getToStringResultSafely(t, mt) 121 | local __tostring = type(mt) == 'table' and rawget(mt, '__tostring') 122 | local str, ok 123 | if type(__tostring) == 'function' then 124 | ok, str = pcall(__tostring, t) 125 | str = ok and str or 'error: ' .. tostring(str) 126 | end 127 | if type(str) == 'string' and #str > 0 then return str end 128 | end 129 | 130 | local function countTableAppearances(t, tableAppearances) 131 | tableAppearances = tableAppearances or {} 132 | 133 | if type(t) == 'table' then 134 | if not tableAppearances[t] then 135 | tableAppearances[t] = 1 136 | for k,v in pairs(t) do 137 | countTableAppearances(k, tableAppearances) 138 | countTableAppearances(v, tableAppearances) 139 | end 140 | countTableAppearances(getmetatable(t), tableAppearances) 141 | else 142 | tableAppearances[t] = tableAppearances[t] + 1 143 | end 144 | end 145 | 146 | return tableAppearances 147 | end 148 | 149 | local copySequence = function(s) 150 | local copy, len = {}, #s 151 | for i=1, len do copy[i] = s[i] end 152 | return copy, len 153 | end 154 | 155 | local function makePath(path, ...) 156 | local keys = {...} 157 | local newPath, len = copySequence(path) 158 | for i=1, #keys do 159 | newPath[len + i] = keys[i] 160 | end 161 | return newPath 162 | end 163 | 164 | local function processRecursive(process, item, path, visited) 165 | 166 | if item == nil then return nil end 167 | if visited[item] then return visited[item] end 168 | 169 | local processed = process(item, path) 170 | if type(processed) == 'table' then 171 | local processedCopy = {} 172 | visited[item] = processedCopy 173 | local processedKey 174 | 175 | for k,v in pairs(processed) do 176 | processedKey = processRecursive(process, k, makePath(path, k, inspect.KEY), visited) 177 | if processedKey ~= nil then 178 | processedCopy[processedKey] = processRecursive(process, v, makePath(path, processedKey), visited) 179 | end 180 | end 181 | 182 | local mt = processRecursive(process, getmetatable(processed), makePath(path, inspect.METATABLE), visited) 183 | setmetatable(processedCopy, mt) 184 | processed = processedCopy 185 | end 186 | return processed 187 | end 188 | 189 | 190 | 191 | ------------------------------------------------------------------- 192 | 193 | local Inspector = {} 194 | local Inspector_mt = {__index = Inspector} 195 | 196 | function Inspector:puts(...) 197 | local args = {...} 198 | local buffer = self.buffer 199 | local len = #buffer 200 | for i=1, #args do 201 | len = len + 1 202 | buffer[len] = args[i] 203 | end 204 | end 205 | 206 | function Inspector:down(f) 207 | self.level = self.level + 1 208 | f() 209 | self.level = self.level - 1 210 | end 211 | 212 | function Inspector:tabify() 213 | self:puts(self.newline, string.rep(self.indent, self.level)) 214 | end 215 | 216 | function Inspector:alreadyVisited(v) 217 | return self.ids[v] ~= nil 218 | end 219 | 220 | function Inspector:getId(v) 221 | local id = self.ids[v] 222 | if not id then 223 | local tv = type(v) 224 | id = (self.maxIds[tv] or 0) + 1 225 | self.maxIds[tv] = id 226 | self.ids[v] = id 227 | end 228 | return tostring(id) 229 | end 230 | 231 | function Inspector:putKey(k) 232 | if isIdentifier(k) then return self:puts(k) end 233 | self:puts("[") 234 | self:putValue(k) 235 | self:puts("]") 236 | end 237 | 238 | function Inspector:putTable(t) 239 | if t == inspect.KEY or t == inspect.METATABLE then 240 | self:puts(tostring(t)) 241 | elseif self:alreadyVisited(t) then 242 | self:puts('') 243 | elseif self.level >= self.depth then 244 | self:puts('{...}') 245 | else 246 | if self.tableAppearances[t] > 1 then self:puts('<', self:getId(t), '>') end 247 | 248 | local nonSequentialKeys, sequenceLength = getNonSequentialKeys(t) 249 | local mt = getmetatable(t) 250 | local toStringResult = getToStringResultSafely(t, mt) 251 | 252 | self:puts('{') 253 | self:down(function() 254 | if toStringResult then 255 | self:puts(' -- ', escape(toStringResult)) 256 | if sequenceLength >= 1 then self:tabify() end 257 | end 258 | 259 | local count = 0 260 | for i=1, sequenceLength do 261 | if count > 0 then self:puts(',') end 262 | self:puts(' ') 263 | self:putValue(t[i]) 264 | count = count + 1 265 | end 266 | 267 | for _,k in ipairs(nonSequentialKeys) do 268 | if count > 0 then self:puts(',') end 269 | self:tabify() 270 | self:putKey(k) 271 | self:puts(' = ') 272 | self:putValue(t[k]) 273 | count = count + 1 274 | end 275 | 276 | if mt then 277 | if count > 0 then self:puts(',') end 278 | self:tabify() 279 | self:puts(' = ') 280 | self:putValue(mt) 281 | end 282 | end) 283 | 284 | if #nonSequentialKeys > 0 or mt then -- result is multi-lined. Justify closing } 285 | self:tabify() 286 | elseif sequenceLength > 0 then -- array tables have one extra space before closing } 287 | self:puts(' ') 288 | end 289 | 290 | self:puts('}') 291 | end 292 | end 293 | 294 | function Inspector:putValue(v) 295 | local tv = type(v) 296 | 297 | if tv == 'string' then 298 | self:puts(smartQuote(escape(v))) 299 | elseif tv == 'number' or tv == 'boolean' or tv == 'nil' then 300 | self:puts(tostring(v)) 301 | elseif tv == 'table' then 302 | self:putTable(v) 303 | else 304 | self:puts('<',tv,' ',self:getId(v),'>') 305 | end 306 | end 307 | 308 | ------------------------------------------------------------------- 309 | 310 | function inspect.inspect(root, options) 311 | options = options or {} 312 | 313 | local depth = options.depth or math.huge 314 | local newline = options.newline or '\n' 315 | local indent = options.indent or ' ' 316 | local process = options.process 317 | 318 | if process then 319 | root = processRecursive(process, root, {}, {}) 320 | end 321 | 322 | local inspector = setmetatable({ 323 | depth = depth, 324 | level = 0, 325 | buffer = {}, 326 | ids = {}, 327 | maxIds = {}, 328 | newline = newline, 329 | indent = indent, 330 | tableAppearances = countTableAppearances(root) 331 | }, Inspector_mt) 332 | 333 | inspector:putValue(root) 334 | 335 | return table.concat(inspector.buffer) 336 | end 337 | 338 | setmetatable(inspect, { __call = function(_, ...) return inspect.inspect(...) end }) 339 | 340 | return inspect 341 | 342 | --------------------------------------------------------------------------------