├── .ci
├── appveyor.yml
├── install.bat
├── platform.sh
├── set_compiler_env.bat
├── setenv_lua.sh
├── setup_lua.sh
└── winmake.bat
├── .gitignore
├── .lift
├── init.lua
└── lift.rockspec
├── .luacheckrc
├── .luacov
├── .travis.yml
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── bin
├── lift
└── lift.bat
├── doc
├── assets
│ ├── sass
│ │ ├── _code.scss
│ │ ├── _octicons.scss
│ │ └── main.scss
│ └── templates
│ │ ├── footer.html
│ │ ├── header.html
│ │ ├── nav.html
│ │ └── page.html
├── content
│ ├── api
│ │ ├── index.md
│ │ └── string.md
│ ├── examples.md
│ ├── index.md
│ └── quickstart.md
├── doc_vars.lua
└── static
│ ├── CNAME
│ ├── js
│ ├── jquery.js
│ ├── main.js
│ └── prism.js
│ ├── media
│ ├── favicon.ico
│ ├── lift-logo.svg
│ ├── lift-mark.svg
│ ├── luarocks-mark.svg
│ └── octicons.woff
│ └── robots.txt
├── examples
├── build-lua
│ └── Liftfile.lua
├── count-cmd
│ └── .lift
│ │ └── cli.lua
├── downloads
│ └── Liftfile.lua
├── lua-logo
│ └── Liftfile.lua
└── tasks
│ └── Liftfile.lua
├── lift-scm-0.rockspec
├── lift
├── async.lua
├── cli.lua
├── color.lua
├── config.lua
├── diagnostics.lua
├── files
│ ├── cli.lua
│ ├── cli_config.lua
│ ├── init.lua
│ └── lift
│ │ └── cli_task.lua
├── fs.lua
├── loader.lua
├── os.lua
├── path.lua
├── request.lua
├── stream.lua
├── string.lua
├── task.lua
├── template.lua
└── util.lua
└── spec
├── Liftfile.lua
├── async_spec.lua
├── cli_spec.lua
├── color_spec.lua
├── config_spec.lua
├── diagnostics_spec.lua
├── files
├── init.lua
├── invalid
│ ├── Liftfile.lua
│ ├── foo.lua
│ ├── foo
│ │ ├── bar.lua
│ │ ├── bar_abc.lua
│ │ ├── barabc.lua
│ │ └── z
│ ├── foo_bar.lua
│ ├── init.lua
│ ├── init_abc.lua
│ ├── init_abc_def.lua
│ ├── init_abcdef.lua
│ └── initabc.lua
├── project1
│ └── .lift
│ │ └── init.lua
├── project2
│ └── Liftfile.lua
├── system
│ └── init.lua
├── templates
│ ├── file.lua
│ ├── row.lua
│ ├── sub
│ │ └── invalid.lua
│ └── table.lua
└── user
│ └── init.lua
├── fs_spec.lua
├── loader_spec.lua
├── os_spec.lua
├── path_spec.lua
├── request_spec.lua
├── stream_spec.lua
├── string_spec.lua
├── task_spec.lua
├── template_spec.lua
├── util.lua
└── util_spec.lua
/.ci/appveyor.yml:
--------------------------------------------------------------------------------
1 | environment:
2 | LUAROCKS_VER: 2.3.0
3 | matrix:
4 | - LUA_VER: 5.2.4
5 | NOCOMPAT: true
6 | - LUA_VER: 5.3.2
7 | NOCOMPAT: true
8 | - LJ_VER: 2.0.4
9 | - LJ_VER: 2.1
10 |
11 | platform:
12 | - x86
13 | - x64
14 |
15 | configuration:
16 | - 2015
17 |
18 | matrix:
19 | fast_finish: true
20 |
21 | cache:
22 | - c:\lua -> .ci\appveyor.yml
23 | - c:\external -> .ci\appveyor.yml
24 |
25 | # init:
26 | # - ps: iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1'))
27 |
28 | install:
29 | - call .ci\set_compiler_env.bat
30 | - call .ci\install.bat
31 |
32 | build_script:
33 | - luarocks install lpeg
34 | - luarocks install luv
35 | - luarocks install busted
36 | - busted -o tap -v
37 | - luarocks make lift-scm-0.rockspec
38 | - lift --help
39 |
40 | after_build:
41 | - luarocks remove lift
42 |
43 | test: off
44 |
45 | # on_finish:
46 | # - ps: $blockRdp = $true; iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1'))
47 |
--------------------------------------------------------------------------------
/.ci/install.bat:
--------------------------------------------------------------------------------
1 | @echo off
2 |
3 | cd %APPVEYOR_BUILD_FOLDER%
4 |
5 | :: =========================================================
6 | :: Set some defaults. Infer some variables.
7 | ::
8 | :: These are set globally
9 | if "%LUA_VER%" NEQ "" (
10 | set LUA=lua
11 | set LUA_SHORTV=%LUA_VER:~0,3%
12 | ) else (
13 | set LUA=luajit
14 | set LJ_SHORTV=%LJ_VER:~0,3%
15 | set LUA_SHORTV=5.1
16 | )
17 |
18 | :: defines LUA_DIR so Cmake can find this Lua install
19 | if "%LUA%"=="luajit" (
20 | set LUA_DIR=c:\lua\%platform%\lj%LJ_SHORTV%
21 | ) else (
22 | set LUA_DIR=c:\lua\%platform%\%LUA_VER%
23 | )
24 |
25 | :: Now we declare a scope
26 | Setlocal EnableDelayedExpansion EnableExtensions
27 |
28 | if not defined LUAROCKS_URL set LUAROCKS_URL=http://keplerproject.github.io/luarocks/releases
29 | if not defined LUAROCKS_REPO set LUAROCKS_REPO=https://luarocks.org
30 | if not defined LUA_URL set LUA_URL=http://www.lua.org/ftp
31 | if defined NOCOMPAT (
32 | set COMPATFLAG=--nocompat
33 | ) else (
34 | set COMPATFLAG=
35 | )
36 | if not defined LUAJIT_GIT_REPO set LUAJIT_GIT_REPO=https://github.com/LuaJIT/LuaJIT.git
37 | if not defined LUAJIT_URL set LUAJIT_URL=https://github.com/LuaJIT/LuaJIT/archive
38 |
39 | if not defined LR_EXTERNAL set LR_EXTERNAL=c:\external
40 | if not defined LUAROCKS_INSTALL set LUAROCKS_INSTALL=%LUA_DIR%\LuaRocks
41 |
42 |
43 | :: LuaRocks <= 2.2.2 used a versioned directory
44 | :: HEAD and newer versions do not, so act accordingly.
45 | if defined LR_ROOT goto :skiplrver
46 |
47 | if "%LUAROCKS_VER%" EQU "HEAD" (
48 | set LR_ROOT=%LUAROCKS_INSTALL%
49 | goto :skiplrver
50 | )
51 | set LR_ROOT=%LUAROCKS_INSTALL%
52 | if %LUAROCKS_VER:~0,1% LEQ 2 (
53 | if %LUAROCKS_VER:~2,1% LEQ 2 (
54 | if %LUAROCKS_VER:~4,1% LEQ 3 (
55 | set LR_ROOT=%LUAROCKS_INSTALL%\!LUAROCKS_VER:~0,3!
56 | )
57 | )
58 | )
59 | :skiplrver
60 |
61 | if not defined LR_SYSTREE set LR_SYSTREE=%LUAROCKS_INSTALL%\systree
62 |
63 | if not defined SEVENZIP set SEVENZIP=7z
64 | ::
65 | :: =========================================================
66 |
67 | :: first create some necessary directories:
68 | mkdir downloads 2>NUL
69 |
70 | :: Download and compile Lua (or LuaJIT)
71 | if "%LUA%"=="luajit" (
72 | if not exist %LUA_DIR% (
73 | if "%LJ_SHORTV%"=="2.1" (
74 | :: Clone repository and checkout 2.1 branch
75 | set lj_source_folder=%APPVEYOR_BUILD_FOLDER%\downloads\luajit-%LJ_VER%
76 | if not exist !lj_source_folder! (
77 | echo Cloning git repo %LUAJIT_GIT_REPO% !lj_source_folder!
78 | git clone %LUAJIT_GIT_REPO% !lj_source_folder! || call :die "Failed to clone repository"
79 | ) else (
80 | cd !lj_source_folder!
81 | git pull || call :die "Failed to update repository"
82 | )
83 | cd !lj_source_folder!\src
84 | git checkout v2.1 || call :die
85 | ) else (
86 | set lj_source_folder=%APPVEYOR_BUILD_FOLDER%\downloads\luajit-%LJ_VER%
87 | if not exist !lj_source_folder! (
88 | echo Downloading... %LUAJIT_URL%/v%LJ_VER%.tar.gz
89 | curl --location --silent --fail --max-time 120 --connect-timeout 30 %LUAJIT_URL%/v%LJ_VER%.tar.gz | %SEVENZIP% x -si -so -tgzip | %SEVENZIP% x -si -ttar -aoa -odownloads
90 | )
91 | cd !lj_source_folder!\src
92 | )
93 | :: Compiles LuaJIT
94 | if "%Configuration%"=="MinGW" (
95 | call mingw32-make
96 | ) else (
97 | call msvcbuild.bat
98 | )
99 |
100 | mkdir %LUA_DIR% 2> NUL
101 | for %%a in (bin bin\lua bin\lua\jit include lib) do ( mkdir "%LUA_DIR%\%%a" )
102 |
103 | for %%a in (luajit.exe lua51.dll) do ( move "!lj_source_folder!\src\%%a" "%LUA_DIR%\bin" )
104 | copy "%LUA_DIR%\bin\luajit.exe" "%LUA_DIR%\bin\lua.exe"
105 |
106 | move "!lj_source_folder!\src\lua51.lib" "%LUA_DIR%\lib"
107 | for %%a in (lauxlib.h lua.h lua.hpp luaconf.h lualib.h luajit.h) do (
108 | copy "!lj_source_folder!\src\%%a" "%LUA_DIR%\include"
109 | )
110 |
111 | copy "!lj_source_folder!\src\jit\*.lua" "%LUA_DIR%\bin\lua\jit"
112 |
113 | ) else (
114 | echo LuaJIT %LJ_VER% already installed at %LUA_DIR%
115 | )
116 | ) else (
117 | if not exist %LUA_DIR% (
118 | :: Download and compile Lua
119 | if not exist downloads\lua-%LUA_VER% (
120 | curl --silent --fail --max-time 120 --connect-timeout 30 %LUA_URL%/lua-%LUA_VER%.tar.gz | %SEVENZIP% x -si -so -tgzip | %SEVENZIP% x -si -ttar -aoa -odownloads
121 | )
122 |
123 | mkdir downloads\lua-%LUA_VER%\etc 2> NUL
124 | copy %~dp0\winmake.bat downloads\lua-%LUA_VER%\etc\winmake.bat
125 |
126 | cd downloads\lua-%LUA_VER%
127 | call etc\winmake %COMPATFLAG%
128 | call etc\winmake install %LUA_DIR%
129 | ) else (
130 | echo Lua %LUA_VER% already installed at %LUA_DIR%
131 | )
132 | )
133 |
134 | if not exist %LUA_DIR%\bin\%LUA%.exe call :die "Missing Lua interpreter at %LUA_DIR%\bin\%LUA%.exe"
135 |
136 | set PATH=%LUA_DIR%\bin;%PATH%
137 | call !LUA! -v
138 |
139 |
140 |
141 | :: ==========================================================
142 | :: LuaRocks
143 | :: ==========================================================
144 |
145 | if not exist "%LR_ROOT%" (
146 | :: Downloads and installs LuaRocks
147 | cd %APPVEYOR_BUILD_FOLDER%
148 |
149 | if %LUAROCKS_VER%==HEAD (
150 | set lr_source_folder=%APPVEYOR_BUILD_FOLDER%\downloads\luarocks-%LUAROCKS_VER%-win32
151 | if not exist !lr_source_folder! (
152 | git clone https://github.com/keplerproject/luarocks.git --single-branch --depth 1 !lr_source_folder! || call :die "Failed to clone LuaRocks repository"
153 | ) else (
154 | cd !lr_source_folder!
155 | git pull || call :die "Failed to update LuaRocks repository"
156 | )
157 | ) else (
158 | if not exist downloads\luarocks-%LUAROCKS_VER%-win32.zip (
159 | echo Downloading LuaRocks...
160 | curl --silent --fail --max-time 120 --connect-timeout 30 --output downloads\luarocks-%LUAROCKS_VER%-win32.zip %LUAROCKS_URL%/luarocks-%LUAROCKS_VER%-win32.zip
161 | %SEVENZIP% x -aoa -odownloads downloads\luarocks-%LUAROCKS_VER%-win32.zip
162 | )
163 | )
164 |
165 | cd downloads\luarocks-%LUAROCKS_VER%-win32
166 | if "%Configuration%"=="MinGW" (
167 | call install.bat /LUA %LUA_DIR% /Q /LV %LUA_SHORTV% /P "%LUAROCKS_INSTALL%" /TREE "%LR_SYSTREE%" /MW
168 | ) else (
169 | call install.bat /LUA %LUA_DIR% /Q /LV %LUA_SHORTV% /P "%LUAROCKS_INSTALL%" /TREE "%LR_SYSTREE%"
170 | )
171 |
172 | :: Configures LuaRocks to instruct CMake the correct generator to use. Else, CMake will pick the highest
173 | :: Visual Studio version installed
174 | if "%Configuration%"=="MinGW" (
175 | echo cmake_generator = "MinGW Makefiles" >> %LUAROCKS_INSTALL%\config-%LUA_SHORTV%.lua
176 | ) else (
177 | set MSVS_GENERATORS[2008]=Visual Studio 9 2008
178 | set MSVS_GENERATORS[2010]=Visual Studio 10 2010
179 | set MSVS_GENERATORS[2012]=Visual Studio 11 2012
180 | set MSVS_GENERATORS[2013]=Visual Studio 12 2013
181 | set MSVS_GENERATORS[2015]=Visual Studio 14 2015
182 |
183 | set CMAKE_GENERATOR=!MSVS_GENERATORS[%Configuration%]!
184 | if "%platform%" EQU "x64" (set CMAKE_GENERATOR=!CMAKE_GENERATOR! Win64)
185 |
186 | echo cmake_generator = "!CMAKE_GENERATOR!" >> %LUAROCKS_INSTALL%\config-%LUA_SHORTV%.lua
187 | )
188 | )
189 |
190 | if not exist "%LR_ROOT%" call :die "LuaRocks not found at %LR_ROOT%"
191 |
192 | set PATH=%LR_ROOT%;%LR_SYSTREE%\bin;%PATH%
193 |
194 | :: Lua will use just the system rocks
195 | set LUA_PATH=%LR_ROOT%\lua\?.lua;%LR_ROOT%\lua\?\init.lua
196 | set LUA_PATH=%LUA_PATH%;%LR_SYSTREE%\share\lua\%LUA_SHORTV%\?.lua
197 | set LUA_PATH=%LUA_PATH%;%LR_SYSTREE%\share\lua\%LUA_SHORTV%\?\init.lua
198 | set LUA_PATH=%LUA_PATH%;.\?.lua;.\?\init.lua
199 | set LUA_CPATH=%LR_SYSTREE%\lib\lua\%LUA_SHORTV%\?.dll;.\?.dll
200 |
201 | call luarocks --version || call :die "Error with LuaRocks installation"
202 | call luarocks list
203 |
204 |
205 | if not exist "%LR_EXTERNAL%" (
206 | mkdir "%LR_EXTERNAL%"
207 | mkdir "%LR_EXTERNAL%\lib"
208 | mkdir "%LR_EXTERNAL%\include"
209 | )
210 |
211 | set PATH=%LR_EXTERNAL%;%PATH%
212 |
213 | :: Exports the following variables:
214 | :: (beware of whitespace between & and ^ below)
215 | endlocal & set PATH=%PATH%&^
216 | set LR_SYSTREE=%LR_SYSTREE%&^
217 | set LUA_PATH=%LUA_PATH%&^
218 | set LUA_CPATH=%LUA_CPATH%&^
219 | set LR_EXTERNAL=%LR_EXTERNAL%
220 |
221 | echo.
222 | echo ======================================================
223 | if "%LUA%"=="luajit" (
224 | echo Installation of LuaJIT %LJ_VER% and LuaRocks %LUAROCKS_VER% done.
225 | ) else (
226 | echo Installation of Lua %LUA_VER% and LuaRocks %LUAROCKS_VER% done.
227 | if defined NOCOMPAT echo Lua was built with compatibility flags disabled.
228 | )
229 | echo Platform - %platform%
230 | echo LUA - %LUA%
231 | echo LUA_SHORTV - %LUA_SHORTV%
232 | echo LJ_SHORTV - %LJ_SHORTV%
233 | echo LUA_PATH - %LUA_PATH%
234 | echo LUA_CPATH - %LUA_CPATH%
235 | echo.
236 | echo LR_EXTERNAL - %LR_EXTERNAL%
237 | echo ======================================================
238 | echo.
239 |
240 | goto :eof
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
257 |
258 |
259 | :: This blank space is intentional. If you see errors like "The system cannot find the batch label specified 'foo'"
260 | :: then try adding or removing blank lines lines above.
261 | :: Yes, really.
262 | :: http://stackoverflow.com/questions/232651/why-the-system-cannot-find-the-batch-label-specified-is-thrown-even-if-label-e
263 |
264 | :: helper functions:
265 |
266 | :: for bailing out when an error occurred
267 | :die %1
268 | echo %1
269 | exit /B 1
270 | goto :eof
271 |
--------------------------------------------------------------------------------
/.ci/platform.sh:
--------------------------------------------------------------------------------
1 | if [ -z "${PLATFORM:-}" ]; then
2 | PLATFORM=$TRAVIS_OS_NAME;
3 | fi
4 |
5 | if [ "$PLATFORM" == "osx" ]; then
6 | PLATFORM="macosx";
7 | fi
8 |
9 | if [ -z "$PLATFORM" ]; then
10 | if [ "$(uname)" == "Linux" ]; then
11 | PLATFORM="linux";
12 | else
13 | PLATFORM="macosx";
14 | fi;
15 | fi
16 |
--------------------------------------------------------------------------------
/.ci/set_compiler_env.bat:
--------------------------------------------------------------------------------
1 | @echo off
2 |
3 | :: Now we declare a scope
4 | Setlocal EnableDelayedExpansion EnableExtensions
5 |
6 | if not defined Configuration set Configuration=2015
7 |
8 | if "%Configuration%"=="MinGW" ( goto :mingw )
9 |
10 | set arch=x86
11 |
12 | if "%platform%" EQU "x64" ( set arch=x86_amd64 )
13 |
14 | if "%Configuration%"=="2015" (
15 | set SET_VS_ENV="C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat"
16 | )
17 |
18 | if "%Configuration%"=="2013" (
19 | set SET_VS_ENV="C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\vcvarsall.bat"
20 | )
21 |
22 | if "%Configuration%"=="2012" (
23 | set SET_VS_ENV="C:\Program Files (x86)\Microsoft Visual Studio 11.0\VC\vcvarsall.bat"
24 | )
25 |
26 | if "%Configuration%"=="2010" (
27 | set SET_VS_ENV="C:\Program Files (x86)\Microsoft Visual Studio 10.0\VC\vcvarsall.bat"
28 | )
29 |
30 | if "%Configuration%"=="2008" (
31 | set SET_VS_ENV="C:\Program Files (x86)\Microsoft Visual Studio 9.0\VC\vcvarsall.bat"
32 | )
33 |
34 | :: Visual Studio detected
35 | endlocal & call %SET_VS_ENV% %arch%
36 | goto :eof
37 |
38 | :: MinGW detected
39 | :mingw
40 | endlocal & set PATH=c:\mingw\bin;%PATH%
41 |
--------------------------------------------------------------------------------
/.ci/setenv_lua.sh:
--------------------------------------------------------------------------------
1 | export PATH=${PATH}:$HOME/.lua:$HOME/.local/bin:${TRAVIS_BUILD_DIR}/install/luarocks/bin
2 | bash .ci/setup_lua.sh
3 | eval `$HOME/.lua/luarocks path`
4 |
--------------------------------------------------------------------------------
/.ci/setup_lua.sh:
--------------------------------------------------------------------------------
1 | #! /bin/bash
2 |
3 | # A script for setting up environment for travis-ci testing.
4 | # Sets up Lua and Luarocks.
5 | # LUA must be "lua5.1", "lua5.2" or "luajit".
6 | # luajit2.0 - master v2.0
7 | # luajit2.1 - master v2.1
8 |
9 | set -eufo pipefail
10 |
11 | LUAJIT_VERSION="2.0.4"
12 | LUAJIT_BASE="LuaJIT-$LUAJIT_VERSION"
13 |
14 | source .ci/platform.sh
15 |
16 | LUA_HOME_DIR=$TRAVIS_BUILD_DIR/install/lua
17 |
18 | LR_HOME_DIR=$TRAVIS_BUILD_DIR/install/luarocks
19 |
20 | mkdir $HOME/.lua
21 |
22 | LUAJIT="no"
23 |
24 | if [ "$PLATFORM" == "macosx" ]; then
25 | if [ "$LUA" == "luajit" ]; then
26 | LUAJIT="yes";
27 | fi
28 | if [ "$LUA" == "luajit2.0" ]; then
29 | LUAJIT="yes";
30 | fi
31 | if [ "$LUA" == "luajit2.1" ]; then
32 | LUAJIT="yes";
33 | fi;
34 | elif [ "$(expr substr $LUA 1 6)" == "luajit" ]; then
35 | LUAJIT="yes";
36 | fi
37 |
38 | mkdir -p "$LUA_HOME_DIR"
39 |
40 | if [ "$LUAJIT" == "yes" ]; then
41 |
42 | if [ "$LUA" == "luajit" ]; then
43 | curl --location https://github.com/LuaJIT/LuaJIT/archive/v$LUAJIT_VERSION.tar.gz | tar xz;
44 | else
45 | git clone https://github.com/LuaJIT/LuaJIT.git $LUAJIT_BASE;
46 | fi
47 |
48 | cd $LUAJIT_BASE
49 |
50 | if [ "$LUA" == "luajit2.1" ]; then
51 | git checkout v2.1;
52 | # force the INSTALL_TNAME to be luajit
53 | perl -i -pe 's/INSTALL_TNAME=.+/INSTALL_TNAME= luajit/' Makefile
54 | fi
55 |
56 | make && make install PREFIX="$LUA_HOME_DIR"
57 |
58 | ln -s $LUA_HOME_DIR/bin/luajit $HOME/.lua/luajit
59 | ln -s $LUA_HOME_DIR/bin/luajit $HOME/.lua/lua;
60 |
61 | else
62 |
63 | if [ "$LUA" == "lua5.1" ]; then
64 | curl http://www.lua.org/ftp/lua-5.1.5.tar.gz | tar xz
65 | cd lua-5.1.5;
66 | elif [ "$LUA" == "lua5.2" ]; then
67 | curl http://www.lua.org/ftp/lua-5.2.4.tar.gz | tar xz
68 | cd lua-5.2.4;
69 | elif [ "$LUA" == "lua5.3" ]; then
70 | curl http://www.lua.org/ftp/lua-5.3.2.tar.gz | tar xz
71 | cd lua-5.3.2;
72 | fi
73 |
74 | # Build Lua without backwards compatibility for testing
75 | perl -i -pe 's/-DLUA_COMPAT_(ALL|5_2)//' src/Makefile
76 | make $PLATFORM
77 | make INSTALL_TOP="$LUA_HOME_DIR" install;
78 |
79 | ln -s $LUA_HOME_DIR/bin/lua $HOME/.lua/lua
80 | ln -s $LUA_HOME_DIR/bin/luac $HOME/.lua/luac;
81 |
82 | fi
83 |
84 | cd $TRAVIS_BUILD_DIR
85 |
86 | lua -v
87 |
88 | LUAROCKS_BASE=luarocks-$LUAROCKS
89 |
90 | curl --location http://luarocks.org/releases/$LUAROCKS_BASE.tar.gz | tar xz
91 |
92 | cd $LUAROCKS_BASE
93 |
94 | if [ "$LUA" == "luajit" ]; then
95 | ./configure --lua-suffix=jit --with-lua-include="$LUA_HOME_DIR/include/luajit-2.0" --prefix="$LR_HOME_DIR";
96 | elif [ "$LUA" == "luajit2.0" ]; then
97 | ./configure --lua-suffix=jit --with-lua-include="$LUA_HOME_DIR/include/luajit-2.0" --prefix="$LR_HOME_DIR";
98 | elif [ "$LUA" == "luajit2.1" ]; then
99 | ./configure --lua-suffix=jit --with-lua-include="$LUA_HOME_DIR/include/luajit-2.1" --prefix="$LR_HOME_DIR";
100 | else
101 | ./configure --with-lua="$LUA_HOME_DIR" --prefix="$LR_HOME_DIR"
102 | fi
103 |
104 | make build && make install
105 |
106 | ln -s $LR_HOME_DIR/bin/luarocks $HOME/.lua/luarocks
107 |
108 | cd $TRAVIS_BUILD_DIR
109 |
110 | luarocks --version
111 |
112 | rm -rf $LUAROCKS_BASE
113 |
114 | if [ "$LUAJIT" == "yes" ]; then
115 | rm -rf $LUAJIT_BASE;
116 | elif [ "$LUA" == "lua5.1" ]; then
117 | rm -rf lua-5.1.5;
118 | elif [ "$LUA" == "lua5.2" ]; then
119 | rm -rf lua-5.2.4;
120 | elif [ "$LUA" == "lua5.3" ]; then
121 | rm -rf lua-5.3.1;
122 | fi
123 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.rockspec
2 | doc/output
3 |
--------------------------------------------------------------------------------
/.lift/init.lua:
--------------------------------------------------------------------------------
1 | local fs = require 'lift.fs'
2 | local los = require 'lift.os'
3 | local path = require 'lift.path'
4 | local task = require 'lift.task'
5 | local config = require 'lift.config'
6 | local loader = require 'lift.loader'
7 | local template = require 'lift.template'
8 | local diagnostics = require 'lift.diagnostics'
9 |
10 | ------------------------------------------------------------------------------
11 | -- Helpers
12 | ------------------------------------------------------------------------------
13 |
14 | -- Returns whether the rock named `name` is installed.
15 | -- If `min_version` is given, older rocks are ignored.
16 | local function is_rock_installed(name, min_version)
17 | local v = los.try_sh('luarocks show --mversion '..name)
18 | if not v then return false end
19 | if min_version then
20 | local cur_major, cur_minor = v:match('(%d+)%.(%d+)')
21 | local min_major, min_minor = min_version:match('(%d+)%.(%d+)')
22 | if cur_major < min_major or
23 | (cur_major == min_major and cur_minor < min_minor) then
24 | return false, v
25 | end
26 | end
27 | return true, v
28 | end
29 |
30 | -- Installs a rock if necessary. The min_version string is optional, and
31 | -- if given it should follow the format "major.minor" (two numbers only).
32 | -- Raises an error if the rock cannot be installed.
33 | local function require_rock(name, min_version)
34 | local ok, v = is_rock_installed(name, min_version)
35 | if ok then return v end
36 | -- install rock (raises an exception if the command fails)
37 | los.sh('luarocks install '..name)
38 | -- make sure the install succeeded and we met the min_version
39 | ok, v = is_rock_installed(name, min_version)
40 | if not ok then
41 | diagnostics.raise({"error: rock '${name}' version ${v} was installed "
42 | .. "but does not meet the required min_version (${min_version})",
43 | name = name, v = v, min_version = min_version}, 2)
44 | end
45 | return v
46 | end
47 |
48 | ------------------------------------------------------------------------------
49 | -- Generate Documentation
50 | ------------------------------------------------------------------------------
51 |
52 | -- Returns a table with settings and vars to be passed to templates.
53 | function task.get_doc_vars()
54 | local src_dir = path.abs('doc')
55 | local vars = {
56 | site = {},
57 | config = config,
58 | assets_dir = src_dir..'/assets',
59 | content_dir = src_dir..'/content',
60 | output_dir = src_dir..'/output',
61 | static_dir = src_dir..'/static',
62 | }
63 | local init_script = src_dir..'/doc_vars.lua'
64 | if fs.access(init_script) then
65 | loader.load_file(init_script, vars)
66 | end
67 | return vars
68 | end
69 |
70 | local function parse_page(markdown_file)
71 | local markdown = fs.read_file(markdown_file)
72 | if markdown:sub(1, 1) ~= '{' then
73 | return {markdown = markdown} -- no Lua front matter
74 | end
75 | -- extract Lua front matter
76 | local lua_src = assert(markdown:match('^({.-\n})'))
77 | local chunk = assert(load('return '..lua_src, '@'..markdown_file, 't'))
78 | local page = chunk()
79 | assert(type(page) == 'table')
80 | page.markdown = markdown:sub(#lua_src + 1)
81 | return page
82 | end
83 |
84 | local function highlight_console(code)
85 | local sb = {'\n
'}
86 | for line, ws in code:gmatch('([^\n]*)(\n*)') do
87 | local cwd, cmd = line:match('^([^$]*)$(.*)$')
88 | if cwd then
89 | sb[#sb+1] = ''..cwd
90 | ..' ❯ '
91 | ..cmd..' '
92 | else
93 | sb[#sb+1] = line
94 | end
95 | sb[#sb+1] = ws
96 | end
97 | sb[#sb+1] = '
\n'
98 | return table.concat(sb)
99 | end
100 |
101 | local function expand_code_block(lang, code)
102 | if lang == 'console' then return highlight_console(code) end
103 | return '\n'..code..'
\n'
104 | end
105 |
106 | local function process_markdown(src)
107 | -- process fenced code blocks properly, since discount doesn't...
108 | return src:gsub("\n~~~ *([^\n]*) *\n(.-)\n~~~\n", expand_code_block)
109 | end
110 |
111 | -- Regenerates the documentation in OUTPUT_DIR
112 | function task.doc()
113 | local v = task.get_doc_vars()
114 | -- create or clean the output dir
115 | if fs.access(v.output_dir) then
116 | los.sh('rm -Rf '..v.output_dir..'/.git '..v.output_dir..'/*')
117 | else
118 | fs.mkdir(v.output_dir)
119 | end
120 | -- copy static content
121 | los.sh('cp -R '..v.static_dir..'/ '..v.output_dir)
122 | -- generate styles.css
123 | local css = los.sh('sassc -t compressed '..v.assets_dir..'/sass/main.scss')
124 | fs.write_file(v.output_dir..'/styles.css', css)
125 | -- process sections and create dirs
126 | for i, section_id in ipairs(v.sections) do
127 | local section = v.sections[section_id]
128 | section.id = section_id
129 | fs.mkdir(v.output_dir..section_id)
130 | end
131 | -- parse markdown files
132 | local pages = {}
133 | for src_file in fs.glob(v.content_dir..'/**/*.md') do
134 | local page = parse_page(src_file)
135 | page.url = src_file:sub(#v.content_dir + 1, -4)
136 | local section_id = path.dir(page.url)
137 | local section = v.sections[section_id]
138 | if not section then error('unknown section: '..section_id) end
139 | page.section = section
140 | section[#section+1] = page
141 | pages[#pages+1] = page
142 | end
143 | v.pages = pages
144 | -- sort pages within sections
145 | local function compare(a, b)
146 | local wa, wb = a.weight or 1000, b.weight or 1000
147 | return wa == wb and a.title_short < b.title_short or wa < wb
148 | end
149 | for i, section_id in ipairs(v.sections) do
150 | local section = v.sections[section_id]
151 | table.sort(section, compare)
152 | end
153 | -- generate HTML
154 | require_rock('discount')
155 | local discount = require 'discount'
156 | local page_tpl = template.load('page.html', v.assets_dir..'/templates/')
157 | for i, page in ipairs(pages) do
158 | local f = assert(io.open(v.output_dir..page.url..'.html', 'w'))
159 | local markdown = process_markdown(page.markdown)
160 | local doc = assert(discount.compile(markdown, 'toc', 'strict'))
161 | page.body = doc.body
162 | page.index = doc.index
163 | v.page = page
164 | page_tpl(function(s) f:write(s) end, v)
165 | f:close()
166 | end
167 | end
168 |
169 | function task.doc_publish()
170 | task.doc()
171 | local v = task.get_doc_vars()
172 | los.sh('cd '..v.output_dir..' && git init && git add . && '..
173 | 'git commit -m "Deploy to GitHub Pages" && '..
174 | 'git push --force git@github.com:tbastos/lift.run.git master:gh-pages')
175 | end
176 |
177 | ------------------------------------------------------------------------------
178 | -- Release Management (use `lift new_version=x.y.z` to prepare a new release)
179 | ------------------------------------------------------------------------------
180 |
181 | -- Checks and returns the current version string
182 | function task.get_version()
183 | local version = config.LIFT_VERSION
184 | -- make sure it matches the version in config.lua
185 | local config_src = fs.read_file('lift/config.lua')
186 | local config_ver = config_src:match("set_const%('APP_VERSION', '([^']*)'%)")
187 | if version ~= config_ver then
188 | diagnostics.report('fatal: current lift version (${1}) does not match '
189 | .. 'the version in config.lua (${2})', version, config_ver)
190 | end
191 | return version
192 | end
193 |
194 | -- Generates lift-VERSION-0.rockspec
195 | local function generate_rockspec(version)
196 | local tpl = assert(template.load('lift.rockspec'))
197 | local filename = 'lift-'..version..'-0.rockspec'
198 | local f = assert(io.open(filename, 'w'))
199 | tpl(function(s) f:write(s) end, {
200 | version = version,
201 | base_dir = config.project_dir,
202 | modules = fs.glob('lift/**/*.lua')
203 | })
204 | f:close()
205 | end
206 |
207 | -- Updates the current version string
208 | function task.set_version(new_version)
209 | if not new_version:match('%d+%.%d+%.%d+') then
210 | diagnostics.report("fatal: bad version '${1}' (expected d.d.d)", new_version)
211 | end
212 | local v = task.get_version()
213 | generate_rockspec(new_version)
214 | local cur_config = fs.read_file('lift/config.lua')
215 | local new_config, count = cur_config:gsub("set_const%('APP_VERSION', '"..v.."'",
216 | "set_const('APP_VERSION', '"..new_version.."'")
217 | assert(count == 1, "failed to update version string in config.lua")
218 | fs.write_file('lift/config.lua', new_config)
219 | end
220 |
221 | ------------------------------------------------------------------------------
222 | -- Default: update the necessary files
223 | ------------------------------------------------------------------------------
224 |
225 | function task.default()
226 | generate_rockspec('scm') -- update lift-scm-0.rockspec
227 | if config.new_version then
228 | task.set_version(config.new_version)
229 | end
230 | end
231 |
232 |
--------------------------------------------------------------------------------
/.lift/lift.rockspec:
--------------------------------------------------------------------------------
1 | package = "Lift"
2 | version = "{{version}}-0"
3 |
4 | source = {
5 | {% if version == 'scm' then %}
6 | url = "git://github.com/tbastos/lift",
7 | branch = "master"
8 | {% else %}
9 | url = "https://github.com/tbastos/lift/archive/v{{version}}.tar.gz",
10 | dir = "lift-{{version}}"
11 | {% end %}
12 | }
13 |
14 | description = {
15 | summary = "Lua automation tool and scripting framework.",
16 | homepage = "http://lift.run",
17 | license = "MIT",
18 | }
19 |
20 | dependencies = {
21 | 'lua >= 5.1', -- actually >= 5.2 or LuaJIT, but LuaJIT self-identifies as 5.1
22 | 'lpeg >= 1.0.0',
23 | 'luv >= 1.8.0-2',
24 | }
25 |
26 | build = {
27 | type = "builtin",
28 |
29 | modules = {
30 | {%
31 | for path in modules do
32 | path = path:sub(#base_dir + 2, -5)
33 | %}
34 | ["{{path:gsub('[/]', '.')}}"] = "{{path}}.lua",
35 | {% end %}
36 | },
37 |
38 | install = {
39 | bin = {
40 | ['lift'] = 'bin/lift'
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/.luacheckrc:
--------------------------------------------------------------------------------
1 | std = "lua51+lua52+lua53"
2 | files["spec"].std = "+busted"
3 | files["spec/data"].global = false
4 |
--------------------------------------------------------------------------------
/.luacov:
--------------------------------------------------------------------------------
1 | return {
2 | -- Patterns for files to include when reporting
3 | -- all will be included if nothing is listed
4 | -- (exclude overrules include, do not include
5 | -- the .lua extension)
6 | include = {
7 | "^%./lift",
8 | },
9 |
10 | -- Patterns for files to exclude when reporting
11 | -- all will be included if nothing is listed
12 | -- (exclude overrules include, do not include
13 | -- the .lua extension)
14 | exclude = {
15 | },
16 | }
17 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: c
2 | sudo: false
3 |
4 | addons:
5 | apt:
6 | sources:
7 | - kalakris-cmake
8 | packages:
9 | - cmake
10 |
11 | env:
12 | global:
13 | - LUAROCKS=2.3.0
14 | matrix:
15 | - LUA=lua5.2
16 | - LUA=lua5.3
17 | - LUA=luajit2.0 # current head of 2.0 branch
18 | - LUA=luajit2.1 # current head of 2.1 branch
19 |
20 | branches:
21 | only:
22 | - master
23 |
24 | before_install:
25 | - source .ci/setenv_lua.sh
26 |
27 | before_script:
28 | - luarocks install lpeg
29 | - luarocks install luv
30 | - luarocks install busted
31 | - luarocks install luacov
32 | - luarocks install luacov-coveralls
33 |
34 | script:
35 | - LUA_INIT="require'luacov'" busted -v
36 | - luarocks make lift-scm-0.rockspec
37 | - cd $TRAVIS_BUILD_DIR/examples/tasks && lift
38 | - cd $TRAVIS_BUILD_DIR/examples/downloads && lift
39 | - cd $TRAVIS_BUILD_DIR/examples/lua-logo && lift
40 | - cd $TRAVIS_BUILD_DIR/examples/build-lua && lift
41 | - cd $TRAVIS_BUILD_DIR/examples/count-cmd && lift count lua $CI_HOME
42 | - cd $TRAVIS_BUILD_DIR
43 |
44 | after_success:
45 | - luacov-coveralls -v
46 |
47 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to Lift
2 |
3 | :+1::tada: First off, thanks for taking the time to contribute! :tada::+1:
4 |
5 | The following is a set of guidelines for contributing to Lift on GitHub.
6 | These are just guidelines, not rules, use your best judgment and feel free to
7 | propose changes to this document in a pull request.
8 |
9 | ## Submitting Issues
10 |
11 | * Include the version of Lift you are using, the OS, and as many details as
12 | possible with your report.
13 | * Check the [debugging guide](#debugging) below for tips on debugging.
14 | You might be able to find the cause of the problem and fix things yourself.
15 | * Include the behavior you expected and other places you've seen that behavior
16 | such as Rake, NPM, etc.
17 | * Perform a cursory search to see if a similar issue has already been submitted.
18 |
19 | ## Pull Requests
20 |
21 | * Please follow the existing code style.
22 | * Include thoughtfully-worded, well-structured [busted] specs in the `./spec` folder.
23 | * Run `busted` and make sure all tests are passing.
24 | * In your pull request, explain the reason for the proposed change and how it is valuable.
25 | * After your pull request is accepted, please help update any obsoleted documentation.
26 |
27 | ## Git Commit Messages
28 |
29 | * Use the present tense ("Add feature" not "Added feature").
30 | * Use the imperative mood ("Move cursor to..." not "Moves cursor to...").
31 | * Limit the first line to 72 characters or less.
32 | * Reference issues and pull requests liberally.
33 |
34 | ## Debugging
35 |
36 | _Under construction._
37 |
38 | ## Philosophy
39 |
40 | Lift's _raison d'être_ is to promote the development of top-notch tools in Lua,
41 | and thus help the Lua ecosystem to evolve.
42 |
43 | Lift stands for _Lua Infrastructure For Tools_.
44 | And it helps your project to fly!
45 |
46 | Design priorities are simplicity first, then conciseness and then efficiency.
47 | In line with Lua's philosophy we should offer maximal value, minimal code,
48 | and concise and precise documentation.
49 |
50 | [busted]: http://olivinelabs.com/busted
51 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2014 Thiago Bastos.
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.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Lift – automate tasks and create tools in Lua
2 |
3 | [](https://github.com/tbastos/lift/releases) [](https://travis-ci.org/tbastos/lift) [](https://ci.appveyor.com/project/tbastos/lift) [](https://coveralls.io/github/tbastos/lift?branch=master) [](LICENSE)
4 |
5 | Lift is both a general-purpose **task automation tool** and a **framework for command-line tools** in Lua. It’s well suited for creating build scripts, checkers, code generators, package managers and other kinds of command-line productivity tools.
6 |
7 | ## Contributing
8 |
9 | Anyone can help make this project better – follow our [contribution guidelines](CONTRIBUTING.md) and check out the [project's philosophy](CONTRIBUTING.md#philosophy).
10 |
11 | ## Running Tests
12 |
13 | Install [busted] and run `busted -v` at the root dir.
14 |
15 | ## References
16 |
17 | The following projects have in some way influenced Lift's design:
18 |
19 | - Command-line interface: [Go], [argparse], [npm]
20 | - Configuration: [npm], [CMake], [Vim]
21 | - Diagnostics: [Clang]
22 | - Task/build system: [Rake]/[Jake], [Gulp]
23 |
24 | [argparse]: https://github.com/mpeterv/argparse
25 | [busted]: http://olivinelabs.com/busted
26 | [Clang]: http://clang.llvm.org/docs/InternalsManual.html
27 | [CMake]: http://www.cmake.org/
28 | [Go]: https://golang.org/cmd/go/
29 | [Gulp]: http://gulpjs.com/
30 | [Jake]: http://jakejs.com/
31 | [libuv]: http://libuv.org/
32 | [luv]: https://github.com/luvit/luv
33 | [LPeg]: http://www.inf.puc-rio.br/~roberto/lpeg/
34 | [Lua]: http://www.lua.org/
35 | [LuaRocks]: http://www.luarocks.org/
36 | [npm]: https://www.npmjs.org/doc/
37 | [Rake]: http://en.wikipedia.org/wiki/Rake_(software)
38 | [Vim]: http://en.wikipedia.org/wiki/Vim_(text_editor)
39 |
--------------------------------------------------------------------------------
/bin/lift:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env lua
2 |
3 | local cli = require 'lift.cli'
4 | local task = require 'lift.task'
5 | local async = require 'lift.async'
6 | local config = require 'lift.config'
7 | local loader = require 'lift.loader'
8 | local diagnostics = require 'lift.diagnostics'
9 |
10 | -- enable colors by default on supported terminals
11 | require'lift.color'.set_enabled(os.getenv('TERM') or os.getenv('ANSICON'))
12 |
13 | -- add files specific to this executable to the ${load_path}
14 | config.set_const('APP_LOAD_PATH', config.LIFT_SRC_DIR..'/files/lift')
15 |
16 | os.exit(diagnostics.wrap(function()
17 | -- run init scripts
18 | local top_scope = loader.init()
19 |
20 | -- run command-line interface scripts
21 | local app = cli.new()
22 | config:new_parent('cli')
23 | loader.load_all('cli', nil, app)
24 |
25 | -- root command is an alias to the 'task run' command
26 | app:delegate_to(app:get_command('task run'))
27 |
28 | -- command 'list' is an alias to 'task list'
29 | app:command 'list' :delegate_to(app:get_command 'task list')
30 | :desc('list [pattern]', 'Show list of tasks (that match a pattern)')
31 |
32 | -- run the CLI in a new thread
33 | local future = async(function()
34 | return app:process(arg)
35 | end)
36 |
37 | async.run() -- main loop
38 | future:check_error() -- propagate CLI errors
39 | async.check_errors() -- propagate unchecked errors from other threads
40 |
41 | end))
42 |
--------------------------------------------------------------------------------
/bin/lift.bat:
--------------------------------------------------------------------------------
1 | @echo off
2 | lua .\bin\lift %*
3 | exit /b %ERRORLEVEL%
4 |
--------------------------------------------------------------------------------
/doc/assets/sass/_code.scss:
--------------------------------------------------------------------------------
1 | // prism.js theme
2 |
3 | $blue: #2239a8;
4 | $gray: #969896;
5 | $green: #00A000;
6 | $magenta: #900090;
7 | $maroon: #a71d5d;
8 | $purple: #795da3;
9 | $sky: #07a;
10 | $teal: #009090;
11 |
12 | // base
13 | code,
14 | code[class*='language-'],
15 | pre[class*='language-'] {
16 | color: $black;
17 | cursor: text;
18 | direction: ltr;
19 | font-family: $code-font;
20 | hyphens: none;
21 | line-height: 1.4;
22 | tab-size: 4;
23 | text-align: left;
24 | white-space: pre;
25 | word-spacing: normal;
26 | }
27 |
28 | // code blocks
29 | pre[class*='language-'], pre.console {
30 | // border-left: 3px solid #eee;
31 | font-size: $code-font-size;
32 | margin: 0 0 16px 0;
33 | overflow: auto;
34 | padding: 16px;
35 | }
36 |
37 | p code,
38 | li code,
39 | table code {
40 | margin: 0;
41 | padding: 0.2em 0;
42 | border-radius: 3px;
43 | font-size: $code-font-size;
44 | background-color: rgba(0, 0, 0, 0.04);
45 | &:before,
46 | &:after {
47 | letter-spacing: -0.2em;
48 | content: '\00a0';
49 | }
50 | }
51 |
52 | code,
53 | :not(pre) > code[class*='language-'],
54 | pre[class*='language-'] {
55 | background-color: #f7f7f7;
56 | border-radius: $border-radius;
57 | }
58 |
59 | // inline code
60 | :not(pre) > code[class*='language-'] {
61 | padding: 0.1em;
62 | border-radius: 0.3em;
63 | }
64 |
65 |
66 | // token colors
67 | .token {
68 |
69 | &.comment,
70 | &.prolog,
71 | &.doctype,
72 | &.cdata {
73 | color: $gray;
74 | }
75 |
76 | &.string,
77 | &.atrule,
78 | &.attr-value {
79 | color: $teal;
80 | }
81 |
82 | &.punctuation,
83 | &.property,
84 | &.tag {
85 | color: $black;
86 | }
87 |
88 | &.function {
89 | color: $blue;
90 | }
91 |
92 | &.boolean,
93 | &.number {
94 | color: $sky;
95 | }
96 |
97 | &.selector,
98 | &.attr-name,
99 | &.attr-value .punctuation:first-child,
100 | &.operator,
101 | &.keyword,
102 | &.regex,
103 | &.important {
104 | color: $maroon;
105 | }
106 |
107 |
108 | &.entity,
109 | &.url,
110 | .language-css &.string {
111 | color: $blue;
112 | }
113 |
114 | &.entity {
115 | cursor: help;
116 | }
117 |
118 | }
119 |
120 | .namespace {
121 | opacity: 0.7;
122 | }
123 | // Command-Line Prompt
124 |
125 | .command-line-prompt {
126 | border-right: 1px solid #999;
127 | display: block;
128 | float: left;
129 | font-size: 100%;
130 | letter-spacing: -1px;
131 | margin-right: 1em;
132 | pointer-events: none;
133 |
134 | -webkit-user-select: none;
135 | -moz-user-select: none;
136 | -ms-user-select: none;
137 | user-select: none;
138 | }
139 |
140 | .command-line-prompt > span:before {
141 | color: #999;
142 | content: ' ';
143 | display: block;
144 | padding-right: 0.8em;
145 | }
146 |
147 | .command-line-prompt > span[data-user]:before {
148 | content: "[" attr(data-user) "@" attr(data-host) "] $";
149 | }
150 |
151 | .command-line-prompt > span[data-user="root"]:before {
152 | content: "[" attr(data-user) "@" attr(data-host) "] #";
153 | }
154 |
155 | .command-line-prompt > span[data-prompt]:before {
156 | content: attr(data-prompt);
157 | }
158 |
--------------------------------------------------------------------------------
/doc/assets/templates/footer.html:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/doc/assets/templates/header.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
{:site.subtitle:}
6 |
7 |
8 |
9 |
10 |
11 |
23 |
24 |
--------------------------------------------------------------------------------
/doc/assets/templates/nav.html:
--------------------------------------------------------------------------------
1 |
20 |
--------------------------------------------------------------------------------
/doc/assets/templates/page.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {% if page.module then %}
6 | {:page.module:} - {:site.title:} {:page.section.title_short:}
7 | {% else %}
8 | {:site.title:} - {: page.url == '/index' and site.subtitle or page.title :}
9 | {% end %}
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | {! "header.html" !}
18 |
19 | {! "nav.html" !}
20 |
21 |
22 |
23 | {% if page.title then %}
24 | {:page.title:}
25 | {% end %}
26 | {% if page.show_toc then %}
27 | Table of Contents
28 | {:page.index:}
29 | {% end %}
30 | {:page.body:}
31 |
32 | {? "tasks.html" ?}
33 |
34 | {! "footer.html" !}
35 |
36 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/doc/content/api/index.md:
--------------------------------------------------------------------------------
1 | {
2 | module = 'lift',
3 | title = 'API Reference Overview',
4 | title_short = 'Introduction',
5 | }
6 |
7 | Under construction.
8 |
--------------------------------------------------------------------------------
/doc/content/api/string.md:
--------------------------------------------------------------------------------
1 | {
2 | module = 'lift.string',
3 | title = 'String Module',
4 | title_short = 'String',
5 | }
6 |
7 | Under construction.
8 |
--------------------------------------------------------------------------------
/doc/content/examples.md:
--------------------------------------------------------------------------------
1 | {
2 | title = 'Examples',
3 | title_short = 'Examples',
4 | weight = 10,
5 | }
6 |
7 | For examples, please check out Lift's [examples directory on GitHub](https://github.com/tbastos/lift/tree/master/examples).
8 |
--------------------------------------------------------------------------------
/doc/content/index.md:
--------------------------------------------------------------------------------
1 | {
2 | title_short = 'Overview',
3 | weight = 0,
4 | }
5 |
6 |
7 |
8 | ## What is Lift?
9 |
10 | Lift is both a general-purpose **task automation tool** and a **framework for command-line tools** in Lua. It's well suited for creating build scripts, checkers, code generators, package managers and other kinds of command-line productivity tools.
11 |
12 | ## What does Lift do?
13 |
14 | You can use Lift as a **library** to develop standalone applications _(best for control)_, or as a **scripting platform** based on the `lift` command-line tool _(best for productivity)_.
15 |
16 | The `lift` tool gives you access to a collection of commands and tasks defined on a per-project, per-user and per-system basis. It is very flexible and allows you to leverage plugins to do almost anything.
17 |
18 | ~~~lua
19 | local task = require 'lift.task'
20 | local async = require 'lift.async'
21 |
22 | function task.brush_teeth()
23 | print 'Brushing teeth...'
24 | async.sleep(2000) -- 2 seconds
25 | print 'Finished brushing teeth.'
26 | end
27 |
28 | function task.take_shower()
29 | print 'Taking a shower...'
30 | async.sleep(3000) -- 3 seconds
31 | print 'Finished taking a shower.'
32 | end
33 |
34 | function task.get_ready() -- takes 5 seconds total
35 | -- take a shower then brush teeth (serial execution)
36 | task.take_shower()
37 | task.brush_teeth()
38 | print 'Done!'
39 | end
40 |
41 | function task.get_ready_fast() -- takes just 3 seconds
42 | -- brush teeth in the shower (parallel execution)
43 | task{task.take_shower, task.brush_teeth}()
44 | print 'Done fast!'
45 | end
46 |
47 | task.default = task.get_ready
48 | ~~~
49 |
50 |
51 |
52 | ## Features
53 |
54 | - **Tasks** and dependencies concisely written as Lua functions that can run in parallel.
55 | - **Multitasking** with async/await, futures and cooperative scheduling on top of Lua coroutines.
56 | - **Asynchronous I/O** (files, networking, IPC) and process spawning powered by [libuv].
57 | - **Pipelines** consisting of object streams (readable, writable, duplex), pipes (flow control) and filters (transform streams).
58 | - **Diagnostics** engine for high-quality error reporting, testing and tracing.
59 | - Portable **filesystem operations** and `glob()` for shell-style filename matching.
60 | - Composable **command-line interfaces** based on command hierarchies.
61 | - Scoped **configuration system** that gets values from the CLI, environment and Lua files.
62 | - General-purpose [LPeg]-based parsing and AST manipulation framework.
63 | - Text templating engine with support for embedded Lua, partials and indentation.
64 | - Modular, extensible architecture with plugins.
65 |
66 | [libuv]: http://libuv.org/
67 | [LPeg]: http://www.inf.puc-rio.br/~roberto/lpeg/
68 |
69 | ## Why did you write Lift?
70 |
71 | First, because Lua always lacked a general-purpose build/automation tool similar to Ruby's Rake/Thor, or JavaScript's Grunt/Gulp. If I needed to automate something in my C++/Lua projects I would have to resort to non-portable shell scripts, or depend on yet another language toolchain.
72 |
73 | Second, because Lua is an excellent language for automation scripts. It's easy to pick up, supports coroutines, closures and metamethods, and its small size means it's much easier to deploy than other scripting languages. Lua-based tools can achieve exceptional levels of efficiency and portability. However, since Lua comes "without batteries", it was never an easy language to write tools in.
74 |
75 | As a standalone tool and framework, Lift intends to solve both these problems.
76 | I'm personally using it (as a framework) to write a development tool for C/C++, and also (as a tool) to generate this website.
77 |
78 | In the future Lift will be available as an easy-to-deploy standalone executable independent of LuaRocks, so that you can use it to bootstrap your development environment from scratch.
79 |
80 |
--------------------------------------------------------------------------------
/doc/content/quickstart.md:
--------------------------------------------------------------------------------
1 | {
2 | title = 'Quickstart Guide',
3 | title_short = 'Quickstart',
4 | weight = 2,
5 | }
6 |
7 | ## Installing Lift
8 |
9 | Please use [LuaRocks] to install:
10 |
11 | ~~~console
12 | $ luarocks install lift
13 | ~~~
14 |
15 | ### Prerequisites
16 | - **OS:** Linux, OSX, Windows or another OS supported by [libuv].
17 | - **Lua:** Lua 5.2, Lua 5.3, LuaJIT 2.0 or LuaJIT 2.1
18 | - **Libraries:** [LPeg] and [luv] (automatically compiled by LuaRocks)
19 |
20 | **Note:** LuaRocks uses [CMake] to build [luv]. If you have CMake installed and still get build errors, please [create an issue on GitHub](https://github.com/tbastos/lift/issues) with as much information as possible about the error.
21 |
22 | ## Sample Liftfile.lua (project-specific build script)
23 |
24 | Create a file named `Liftfile.lua` in your project's root directory:
25 |
26 | ~~~lua
27 | local fs = require 'lift.fs'
28 | local task = require 'lift.task'
29 | local config = require 'lift.config'
30 | local request = require 'lift.request'
31 |
32 | local function download(file_url)
33 | print('Downloading '..file_url)
34 | local filename = file_url:match('/([^/]+)$')
35 | request(file_url):pipe(fs.write_to(filename)):wait_finish()
36 | return filename
37 | end
38 |
39 | function task.greet() -- executed once, despite being called multiple times
40 | print('Hello '..(config.USER or 'unknown')..'!')
41 | end
42 |
43 | function task.download_lua()
44 | task.greet()
45 | print('Saved '..download('http://www.lua.org/ftp/lua-5.3.2.tar.gz'))
46 | end
47 |
48 | function task.download_luarocks()
49 | task.greet()
50 | print('Saved '..download('https://github.com/keplerproject/luarocks/archive/v2.3.0.tar.gz'))
51 | end
52 |
53 | function task.default()
54 | task.greet()
55 | task{task.download_lua, task.download_luarocks}() -- these tasks run in parallel
56 | print('Done!')
57 | end
58 |
59 | function task.clean()
60 | for path in fs.glob('*.tar.gz') do
61 | print('Deleting '..path)
62 | fs.unlink(path)
63 | end
64 | end
65 | ~~~
66 |
67 | Calling `lift` from any project dir will produce:
68 |
69 | ~~~console
70 | $ lift
71 | Hello tbastos!
72 | Downloading http://www.lua.org/ftp/lua-5.3.2.tar.gz
73 | Downloading https://github.com/keplerproject/luarocks/archive/v2.3.0.tar.gz
74 | Saved lua-5.3.2.tar.gz
75 | Saved v2.3.0.tar.gz
76 | Done!
77 |
78 | $ lift clean
79 | Deleting /Users/tbastos/Work/lift/examples/downloads/lua-5.3.2.tar.gz
80 | Deleting /Users/tbastos/Work/lift/examples/downloads/v2.3.0.tar.gz
81 |
82 | $ lift download_lua
83 | Hello tbastos!
84 | Downloading http://www.lua.org/ftp/lua-5.3.2.tar.gz
85 | Saved lua-5.3.2.tar.gz
86 |
87 | $ lift clean greet
88 | Hello tbastos!
89 | Deleting /Users/tbastos/Work/lift/examples/downloads/lua-5.3.2.tar.gz
90 | ~~~
91 |
92 | To plot a graph of task calls use (requires graphviz):
93 | ~~~console
94 | $ lift task run --plot graph.svg
95 | ~~~
96 |
97 | 
98 |
99 | For debugging purposes you may want to run `lift` with tracing enabled:
100 |
101 | ~~~console
102 | $ lift --trace
103 | [cli] running root command
104 | [task] running task list {default} (nil)
105 | [thread] async(function) started
106 | [task] running greet (nil)
107 | [thread] async(function) started
108 | Hello tbastos!
109 | [thread] async(function) ended with true {}
110 | [task] finished greet (nil) [0.00s]
111 | [task] running task list {download_lua, download_luarocks} (nil)
112 | ... redacted for length ...
113 | [task] finished task list {download_lua, download_luarocks} (nil) [4.87s]
114 | Done!
115 | [thread] async(function) ended with true {}
116 | [task] finished task list {default} (nil) [4.87s]
117 | [cli] finished root command [4.87s]
118 | [thread] async(function) ended with true {}
119 | Total time 4.87s, memory 603K
120 | ~~~
121 |
122 | [CMake]: http://www.cmake.org/
123 | [libuv]: http://libuv.org/
124 | [luv]: https://github.com/luvit/luv
125 | [LPeg]: http://www.inf.puc-rio.br/~roberto/lpeg/
126 | [Lua]: http://www.lua.org/
127 | [LuaRocks]: http://www.luarocks.org/
128 |
--------------------------------------------------------------------------------
/doc/doc_vars.lua:
--------------------------------------------------------------------------------
1 | local path = require 'lift.path'
2 |
3 | local v = ... -- table of vars that is passed to templates
4 |
5 | ------------------------------------------------------------------------------
6 | -- Global Variables
7 | ------------------------------------------------------------------------------
8 |
9 | v.site.title = 'Lift'
10 | v.site.subtitle = 'Lua automation tool and scripting framework'
11 |
12 | v.github_url = 'https://github.com/tbastos/lift'
13 | v.luarocks_url = 'https://luarocks.org/modules/tbastos/lift'
14 | v.base_edit_url = 'https://github.com/tbastos/lift/edit/master/doc/content'
15 |
16 | -- organize content into sections
17 | v.sections = {
18 | -- list of section ids (sets the order)
19 | '/', '/api',
20 | -- mapping of ids to section data
21 | ['/'] = {title='Documentation', title_short='Documentation'},
22 | ['/api'] = {title='API Reference', title_short='API Reference'},
23 | }
24 |
25 | ------------------------------------------------------------------------------
26 | -- Helper Methods
27 | ------------------------------------------------------------------------------
28 |
29 | function v.to_file(abs_url)
30 | return path.rel(path.dir(v.page.url), abs_url)
31 | end
32 |
33 | function v.to_page(page_id)
34 | return v.to_file(page_id..'.html')
35 | end
36 |
37 |
--------------------------------------------------------------------------------
/doc/static/CNAME:
--------------------------------------------------------------------------------
1 | lift.run
2 | www.lift.run
3 |
--------------------------------------------------------------------------------
/doc/static/js/main.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Replace all SVG images with inline SVG
3 | */
4 | jQuery('img.svg').each(function(){
5 | var $img = jQuery(this);
6 | var imgID = $img.attr('id');
7 | var imgClass = $img.attr('class');
8 | var imgURL = $img.attr('src');
9 | jQuery.get(imgURL, function(data) {
10 | // Get the SVG tag, ignore the rest
11 | var $svg = jQuery(data).find('svg');
12 | // Add replaced image's ID to the new SVG
13 | if(typeof imgID !== 'undefined') {
14 | $svg = $svg.attr('id', imgID);
15 | }
16 | // Add replaced image's classes to the new SVG
17 | if(typeof imgClass !== 'undefined') {
18 | $svg = $svg.attr('class', imgClass+' replaced-svg');
19 | }
20 | // Remove any invalid XML tags as per http://validator.w3.org
21 | $svg = $svg.removeAttr('xmlns:a');
22 | // Replace image with new SVG
23 | $img.replaceWith($svg);
24 | }, 'xml');
25 | });
26 |
--------------------------------------------------------------------------------
/doc/static/js/prism.js:
--------------------------------------------------------------------------------
1 | /* http://prismjs.com/download.html?themes=prism-solarizedlight&languages=lua&plugins=show-language+command-line */
2 | var _self="undefined"!=typeof window?window:"undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?self:{},Prism=function(){var e=/\blang(?:uage)?-(\w+)\b/i,t=0,n=_self.Prism={util:{encode:function(e){return e instanceof a?new a(e.type,n.util.encode(e.content),e.alias):"Array"===n.util.type(e)?e.map(n.util.encode):e.replace(/&/g,"&").replace(/e.length)break e;if(!(d instanceof a)){u.lastIndex=0;var m=u.exec(d);if(m){g&&(f=m[1].length);var y=m.index-1+f,m=m[0].slice(f),v=m.length,b=y+v,k=d.slice(0,y+1),w=d.slice(b+1),_=[p,1];k&&_.push(k);var P=new a(i,c?n.tokenize(m,c):m,h);_.push(P),w&&_.push(w),Array.prototype.splice.apply(r,_)}}}}}return r},hooks:{all:{},add:function(e,t){var a=n.hooks.all;a[e]=a[e]||[],a[e].push(t)},run:function(e,t){var a=n.hooks.all[e];if(a&&a.length)for(var r,l=0;r=a[l++];)r(t)}}},a=n.Token=function(e,t,n){this.type=e,this.content=t,this.alias=n};if(a.stringify=function(e,t,r){if("string"==typeof e)return e;if("Array"===n.util.type(e))return e.map(function(n){return a.stringify(n,t,e)}).join("");var l={type:e.type,content:a.stringify(e.content,t,r),tag:"span",classes:["token",e.type],attributes:{},language:t,parent:r};if("comment"==l.type&&(l.attributes.spellcheck="true"),e.alias){var i="Array"===n.util.type(e.alias)?e.alias:[e.alias];Array.prototype.push.apply(l.classes,i)}n.hooks.run("wrap",l);var o="";for(var s in l.attributes)o+=(o?" ":"")+s+'="'+(l.attributes[s]||"")+'"';return"<"+l.tag+' class="'+l.classes.join(" ")+'" '+o+">"+l.content+""+l.tag+">"},!_self.document)return _self.addEventListener?(_self.addEventListener("message",function(e){var t=JSON.parse(e.data),a=t.language,r=t.code,l=t.immediateClose;_self.postMessage(n.highlight(r,n.languages[a],a)),l&&_self.close()},!1),_self.Prism):_self.Prism;var r=document.currentScript||[].slice.call(document.getElementsByTagName("script")).pop();return r&&(n.filename=r.src,document.addEventListener&&!r.hasAttribute("data-manual")&&document.addEventListener("DOMContentLoaded",n.highlightAll)),_self.Prism}();"undefined"!=typeof module&&module.exports&&(module.exports=Prism),"undefined"!=typeof global&&(global.Prism=Prism);
3 | Prism.languages.lua={comment:/^#!.+|--(?:\[(=*)\[[\s\S]*?\]\1\]|.*)/m,string:/(["'])(?:(?!\1)[^\\\r\n]|\\z(?:\r\n|\s)|\\(?:\r\n|[\s\S]))*\1|\[(=*)\[[\s\S]*?\]\2\]/,number:/\b0x[a-f\d]+\.?[a-f\d]*(?:p[+-]?\d+)?\b|\b\d+(?:\.\B|\.?\d*(?:e[+-]?\d+)?\b)|\B\.\d+(?:e[+-]?\d+)?\b/i,keyword:/\b(?:and|break|do|else|elseif|end|false|for|function|goto|if|in|local|nil|not|or|repeat|return|then|true|until|while)\b/,"function":/(?!\d)\w+(?=\s*(?:[({]))/,operator:[/[-+*%^&|#]|\/\/?|<[<=]?|>[>=]?|[=~]=?/,{pattern:/(^|[^.])\.\.(?!\.)/,lookbehind:!0}],punctuation:/[\[\](){},;]|\.+|:+/};
4 | !function(){"undefined"!=typeof self&&self.Prism&&self.document&&Prism.hooks.add("complete",function(e){if(e.code){var t=e.element.parentNode,a=/\s*\bcommand-line\b\s*/;if(t&&/pre/i.test(t.nodeName)&&(a.test(t.className)||a.test(e.element.className))&&!e.element.querySelector(".command-line-prompt")){a.test(e.element.className)&&(e.element.className=e.element.className.replace(a,"")),a.test(t.className)||(t.className+=" command-line");var n=new Array(1+e.code.split("\n").length),s=t.getAttribute("data-prompt")||"";if(""!==s)n=n.join(' ');else{var r=t.getAttribute("data-user")||"user",l=t.getAttribute("data-host")||"localhost";n=n.join(' ')}var m=document.createElement("span");m.className="command-line-prompt",m.innerHTML=n;var o=t.getAttribute("data-output")||"";o=o.split(",");for(var i=0;i=u&&u<=m.children.length;u++){var N=m.children[u-1];N.removeAttribute("data-user"),N.removeAttribute("data-host"),N.removeAttribute("data-prompt")}}e.element.innerHTML=m.outerHTML+e.element.innerHTML}}})}();
5 |
--------------------------------------------------------------------------------
/doc/static/media/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tbastos/lift/454a6a06b696022dbdbd1f6d8e8a1789fc4d6a4f/doc/static/media/favicon.ico
--------------------------------------------------------------------------------
/doc/static/media/lift-logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/doc/static/media/lift-mark.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/doc/static/media/luarocks-mark.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/doc/static/media/octicons.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tbastos/lift/454a6a06b696022dbdbd1f6d8e8a1789fc4d6a4f/doc/static/media/octicons.woff
--------------------------------------------------------------------------------
/doc/static/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Disallow: /404
3 | Disallow: /500
--------------------------------------------------------------------------------
/examples/build-lua/Liftfile.lua:
--------------------------------------------------------------------------------
1 | -- These tasks will fetch the list of all official Lua releases, select
2 | -- the latest release in branches 5.3, 5.2 and 5.1, and download and
3 | -- build these releases in parallel.
4 |
5 | local fs = require 'lift.fs'
6 | local task = require 'lift.task'
7 | local async = require 'lift.async'
8 | local stream = require 'lift.stream'
9 | local request = require 'lift.request'
10 | local diagnostics = require 'lift.diagnostics'
11 | local sh = require'lift.os'.sh
12 |
13 | -- Returns the contents of a web page
14 | function task.fetch_page(url)
15 | local buf = {}
16 | request(url):pipe(stream.to_array(buf)):wait_finish()
17 | return table.concat(buf)
18 | end
19 |
20 | -- Returns a sorted list of Lua releases (also a map: version => release)
21 | function task.get_lua_releases()
22 | local t = {}
23 | local url = 'http://www.lua.org/ftp/'
24 | local html = task.fetch_page(url)
25 | for f, v in html:gmatch[[HREF="(lua%-([%d.]+)%.tar%.gz)"]] do
26 | if not t[v] then
27 | local release = {version = v, filename = f, url = url..f}
28 | t[v] = release
29 | t[#t+1] = release
30 | end
31 | end
32 | return t
33 | end
34 |
35 | -- Returns the abs path to a subdir created with the given name
36 | function task.get_dir(name)
37 | local dir = fs.cwd()..'/'..name
38 | fs.mkdir(dir)
39 | return dir
40 | end
41 |
42 | -- Downloads a Lua release archive (tar.gz)
43 | function task.download(release)
44 | print('Downloading '..release.url)
45 | local dest = task.get_dir('archives')..'/'..release.filename
46 | request(release.url):pipe(fs.write_to(dest)):wait_finish()
47 | return dest
48 | end
49 |
50 | -- helper function to print the elapsed time
51 | local function get_elapsed(t0)
52 | return string.format('%.2fs', (async.now() - t0) / 1000)
53 | end
54 |
55 | -- Downloads and builds a given Lua release
56 | function task.build_release(release)
57 | local t0 = async.now()
58 | local filename = task.download(release)
59 | sh('tar -xzf '..filename)
60 | sh('cd lua-'..release.version..' && make generic > build.log')
61 | print('Lua '..release.version..' built in '..get_elapsed(t0))
62 | end
63 |
64 | -- Given a list of version strings, builds a set of Lua releases in parallel
65 | function task.build_versions(versions)
66 | local t0 = async.now()
67 | local releases = task.get_lua_releases()
68 | local futures = {}
69 | for i, version in ipairs(versions) do
70 | if version:sub(1, 1) ~= '5' then
71 | diagnostics.report('fatal: version must be >= 5.x (${1} is too old)',
72 | version)
73 | end
74 | local release = releases[version]
75 | if not release then
76 | diagnostics.report("fatal: no such release '${1}'", version)
77 | end
78 | futures[#futures+1] = task.build_release:async(release)
79 | end
80 | async.wait_all(futures)
81 | print('Total time '..get_elapsed(t0))
82 | end
83 |
84 | -- Determines the latest Lua release versions in multiple 5.x branches
85 | -- and calls build_versions to build them in parallel
86 | function task.default()
87 | local releases = task.get_lua_releases()
88 | local branches = {
89 | ['5.3'] = true,
90 | ['5.2'] = true,
91 | ['5.1'] = true,
92 | }
93 | local versions = {}
94 | for i, release in ipairs(releases) do
95 | local branch = release.version:sub(1, 3)
96 | if branches[branch] then
97 | print('Latest '..branch..' release is '..release.version)
98 | branches[branch] = nil
99 | versions[#versions+1] = release.version
100 | end
101 | end
102 | task.build_versions(versions)
103 | end
104 |
--------------------------------------------------------------------------------
/examples/count-cmd/.lift/cli.lua:
--------------------------------------------------------------------------------
1 | local fs = require 'lift.fs'
2 | local async = require 'lift.async'
3 |
4 | -- extend the CLI with a new 'count' command
5 | local app = ... -- app is the root CLI command, which is passed to this script
6 | local count_cmd = app:command 'count'
7 | :desc('count [dir]',
8 | 'Count file names ending in ".ext" within [dir] (defaults to current dir)')
9 |
10 | count_cmd:flag 'lines' -- count accepts the '--lines' flag
11 | :desc('--lines', 'Count the number of lines in files')
12 |
13 | -- function to count lines by streaming from a file in a thread
14 | local function count_lines_in(filename)
15 | local count = 0
16 | local readable = fs.read_from(filename)
17 | while 1 do
18 | local data = readable:read() -- sync read
19 | if not data then break end -- EOF
20 | local pos = 0
21 | while 1 do
22 | pos = data:find('\n', pos + 1, true) -- find next \n in data
23 | if not pos then break end
24 | count = count + 1
25 | end
26 | end
27 | return count
28 | end
29 |
30 | -- define action for the 'count' command
31 | count_cmd:action(function(cmd)
32 | local ext = cmd:consume('extension') -- read required ext argument
33 | if not ext:match('^%w+$') then -- validate the extension name
34 | return "invalid extension '"..ext.."' (expected a plain word)"
35 | end
36 | -- read the optional dir argument, which defaults to CWD
37 | local dir = #cmd.args > 1 and cmd:consume('dir') or fs.cwd()
38 | if not fs.is_dir(dir) then return "no such dir "..dir end
39 | local count_lines = cmd.options.lines.value -- state of --lines flag
40 | local futures = {} -- only used if we spawn threads to count lines
41 | -- use glob to find files (note: /**/ ignores dot dirs by default)
42 | local num_files, vars = 0, {dir = dir, ext = ext}
43 | for filename in fs.glob('${dir}/**/*.${ext}', vars) do
44 | num_files = num_files + 1
45 | if count_lines then -- spawn thread to count lines
46 | futures[#futures + 1] = async(count_lines_in, filename)
47 | end
48 | end
49 | print('There are '..num_files..' files with extension .'..ext..' in '..dir)
50 | if count_lines then
51 | print('Counting number of lines in files...')
52 | async.wait_all(futures) -- wait for threads to finish
53 | local num_lines = 0 -- sum line counts returned by each thread
54 | for i, future in ipairs(futures) do
55 | num_lines = num_lines + future.results[1]
56 | end
57 | print('Total number of lines: '..num_lines)
58 | end
59 | end)
60 |
61 |
--------------------------------------------------------------------------------
/examples/downloads/Liftfile.lua:
--------------------------------------------------------------------------------
1 | -- Task 'default' downloads two files concurrently
2 | -- Task 'clean' deletes the downloaded files
3 |
4 | local fs = require 'lift.fs'
5 | local task = require 'lift.task'
6 | local config = require 'lift.config'
7 | local request = require 'lift.request'
8 |
9 | local function download(file_url)
10 | print('Downloading '..file_url)
11 | local filename = file_url:match('/([^/]+)$')
12 | request(file_url):pipe(fs.write_to(filename)):wait_finish()
13 | return filename
14 | end
15 |
16 | function task.greet() -- executed once, despite multiple calls
17 | print('Hello '..(config.USER or 'unknown')..'!')
18 | end
19 |
20 | function task.download_lua()
21 | task.greet()
22 | print('Saved '..download('http://www.lua.org/ftp/lua-5.3.2.tar.gz'))
23 | end
24 |
25 | function task.download_luarocks()
26 | task.greet()
27 | print('Saved '..download('https://github.com/keplerproject/luarocks/archive/v2.3.0.tar.gz'))
28 | end
29 |
30 | function task.default()
31 | task.greet()
32 | task{task.download_lua, task.download_luarocks}() -- these tasks run in parallel
33 | print('Done!')
34 | end
35 |
36 | function task.clean()
37 | for path in fs.glob('*.tar.gz') do
38 | print('Deleting '..path)
39 | fs.unlink(path)
40 | end
41 | end
42 |
43 |
--------------------------------------------------------------------------------
/examples/lua-logo/Liftfile.lua:
--------------------------------------------------------------------------------
1 | local los = require 'lift.os'
2 | local task = require 'lift.task'
3 | local config = require 'lift.config'
4 | local stream = require 'lift.stream'
5 | local request = require 'lift.request'
6 |
7 | -- Returns the contents of an URL
8 | local function fetch(url)
9 | local buf = {}
10 | request(url):pipe(stream.to_array(buf)):wait_finish()
11 | return table.concat(buf)
12 | end
13 |
14 | -- Returns contents of 'lua-logo-label.ps' patched with a new label
15 | local function get_logo_postscript(label)
16 | local ps = fetch('http://www.lua.org/images/lua-logo-label.ps')
17 | ps = ps:gsub('(powered by)', label)
18 | return ps
19 | end
20 |
21 | -- Creates SVG file by converting 'lua-logo-label.ps' from PostScript
22 | function task.generate_logo()
23 | local label = config.label or 'Lift'
24 | local postscript = get_logo_postscript(label)
25 | -- use the 'convert' tool to convert PostScript to SVG
26 | -- requires ImageMagick http://www.imagemagick.org/
27 | local convert_program = assert(los.find_program('convert'))
28 | local proc = los.spawn{file = convert_program, 'ps:-', 'logo.svg',
29 | stdout = 'inherit', stderr = 'inherit'}
30 | proc:write(postscript) -- writes to proc's stdin
31 | proc:write() -- sends EOF to proc's stdin
32 | proc:wait() -- wait for 'convert' to finish (optional)
33 | print("Generated logo.svg with label "..label)
34 | end
35 |
36 | task.default = task.generate_logo
37 |
--------------------------------------------------------------------------------
/examples/tasks/Liftfile.lua:
--------------------------------------------------------------------------------
1 | local task = require 'lift.task'
2 | local async = require 'lift.async'
3 |
4 | function task.brush_teeth()
5 | print 'Brushing teeth...'
6 | async.sleep(2000) -- 2 seconds
7 | print 'Finished brushing teeth.'
8 | end
9 |
10 | function task.take_shower()
11 | print 'Taking a shower...'
12 | async.sleep(3000) -- 3 seconds
13 | print 'Finished taking a shower.'
14 | end
15 |
16 | function task.get_ready() -- takes 5 seconds total
17 | task.take_shower()
18 | task.brush_teeth()
19 | print 'Done!'
20 | end
21 |
22 | function task.get_ready_fast() -- takes just 3 seconds
23 | task{task.take_shower, task.brush_teeth}()
24 | print 'Done fast!'
25 | end
26 |
27 | -- annotate the main tasks
28 | task.get_ready:desc('Take a shower then brush teeth (serial)')
29 | task.get_ready_fast:desc('Brush teeth while taking a shower (parallel)')
30 |
31 | task.default = task.get_ready
32 |
--------------------------------------------------------------------------------
/lift-scm-0.rockspec:
--------------------------------------------------------------------------------
1 | package = "Lift"
2 | version = "scm-0"
3 |
4 | source = {
5 | url = "git://github.com/tbastos/lift",
6 | branch = "master"
7 | }
8 |
9 | description = {
10 | summary = "Lua automation tool and scripting framework.",
11 | homepage = "http://lift.run",
12 | license = "MIT",
13 | }
14 |
15 | dependencies = {
16 | 'lua >= 5.1', -- actually >= 5.2 or LuaJIT, but LuaJIT self-identifies as 5.1
17 | 'lpeg >= 1.0.0',
18 | 'luv >= 1.8.0-2',
19 | }
20 |
21 | build = {
22 | type = "builtin",
23 |
24 | modules = {
25 | ["lift.async"] = "lift/async.lua",
26 | ["lift.cli"] = "lift/cli.lua",
27 | ["lift.color"] = "lift/color.lua",
28 | ["lift.config"] = "lift/config.lua",
29 | ["lift.diagnostics"] = "lift/diagnostics.lua",
30 | ["lift.fs"] = "lift/fs.lua",
31 | ["lift.loader"] = "lift/loader.lua",
32 | ["lift.os"] = "lift/os.lua",
33 | ["lift.path"] = "lift/path.lua",
34 | ["lift.request"] = "lift/request.lua",
35 | ["lift.stream"] = "lift/stream.lua",
36 | ["lift.string"] = "lift/string.lua",
37 | ["lift.task"] = "lift/task.lua",
38 | ["lift.template"] = "lift/template.lua",
39 | ["lift.util"] = "lift/util.lua",
40 | ["lift.files.cli"] = "lift/files/cli.lua",
41 | ["lift.files.cli_config"] = "lift/files/cli_config.lua",
42 | ["lift.files.init"] = "lift/files/init.lua",
43 | ["lift.files.lift.cli_task"] = "lift/files/lift/cli_task.lua",
44 | },
45 |
46 | install = {
47 | bin = {
48 | ['lift'] = 'bin/lift'
49 | }
50 | }
51 | }
--------------------------------------------------------------------------------
/lift/color.lua:
--------------------------------------------------------------------------------
1 | ------------------------------------------------------------------------------
2 | -- Encoding of ANSI color escape sequences
3 | ------------------------------------------------------------------------------
4 |
5 | local codes = {
6 | -- attributes
7 | reset = 0,
8 | clear = 0,
9 | bold = 1,
10 | bright = 1,
11 | dim = 2,
12 | underline = 4,
13 | blink = 5,
14 | reverse = 7,
15 | hidden = 8,
16 | -- foreground
17 | black = 30,
18 | red = 31,
19 | green = 32,
20 | yellow = 33,
21 | blue = 34,
22 | magenta = 35,
23 | cyan = 36,
24 | white = 37,
25 | -- background
26 | onblack = 40,
27 | onred = 41,
28 | ongreen = 42,
29 | onyellow = 43,
30 | onblue = 44,
31 | onmagenta = 45,
32 | oncyan = 46,
33 | onwhite = 47,
34 | }
35 |
36 | -- given a string such as 'reset;red;onblack' returns a escape sequence
37 | local str_gsub = string.gsub
38 | local function encode(seq)
39 | return '\27[' .. str_gsub(seq, '([^;]+)', codes) .. 'm'
40 | end
41 |
42 | -- colors are disabled by default; use set_enabled()
43 | local enabled = false
44 | local function set_enabled(v) enabled = v end
45 |
46 | -- =encode(seq) if colors are enabled; otherwise returns empty string
47 | local function ESC(seq) if enabled then return encode(seq) end return '' end
48 |
49 | -- =encode(seq) if colors are enabled; otherwise returns nil
50 | local function esc(seq) if enabled then return encode(seq) end return nil end
51 |
52 | -- encodes an escape sequence based on a style table like {fg='red',
53 | -- bg='black', bold=true} etc. Returns an empty string if not enabled.
54 | local function from_style(t)
55 | if not enabled then return '' end
56 | local seq = ''
57 | for k, v in pairs(t) do
58 | local c = codes[k] ; if c and v then seq = seq .. ';' .. c end
59 | end
60 | local c = codes[t.fg] if c then seq = seq .. ';' .. c end
61 | c = codes[t.bg] if c then seq = seq .. ';' .. c + 10 end
62 | return '\27[0'..seq..'m'
63 | end
64 |
65 | ------------------------------------------------------------------------------
66 | -- Module Table
67 | ------------------------------------------------------------------------------
68 |
69 | local M = {
70 | encode = encode,
71 | ESC = ESC,
72 | esc = esc,
73 | from_style = from_style,
74 | set_enabled = set_enabled,
75 | }
76 |
77 | return M
78 |
--------------------------------------------------------------------------------
/lift/config.lua:
--------------------------------------------------------------------------------
1 | ------------------------------------------------------------------------------
2 | -- Configuration System (global, transient, hierarchical key=value store)
3 | ------------------------------------------------------------------------------
4 | -- Gathers vars from the command-line, config files and environment.
5 | -- Initial scope hierarchy: (env) <-- {root} <-- {config}
6 |
7 | local assert, type = assert, type
8 | local rawget, rawset, getmetatable = rawget, rawset, getmetatable
9 | local getenv, tinsert = os.getenv, table.insert
10 |
11 | local util = require 'lift.util'
12 | local ls = require 'lift.string'
13 | local str_to_bool, str_to_list = ls.to_bool, ls.to_list
14 |
15 | ------------------------------------------------------------------------------
16 | -- The immutable root scope (methods, constants, access to env vars)
17 | ------------------------------------------------------------------------------
18 |
19 | local env_vars = {} -- helper table to get env vars
20 | local root = setmetatable({}, {id = 'built-in constants', __index = env_vars})
21 | local config = {} -- the lift.config scope (a proxy)
22 |
23 | -- enable access to env vars through root
24 | setmetatable(env_vars, {id = 'environment variables', __index = function(t, k)
25 | if type(k) ~= 'string' then return end
26 | -- try MYAPP_VAR_NAME first, then just var_name
27 | local v = getenv(config.APP_ID:upper().."_"..k:upper()) or getenv(k)
28 | if v then t[k] = v end
29 | return v
30 | end})
31 |
32 | -- root vars are immutable; forbid child scopes from shadowing them
33 | local function __newindex(t, k, v)
34 | if rawget(root, k) then
35 | error("'"..k.."' is reserved for internal use and cannot be changed")
36 | end
37 | rawset(t, k, v)
38 | end
39 |
40 | -- Returns the parent of a scope.
41 | function root:get_parent()
42 | return assert(getmetatable(self)).__index
43 | end
44 |
45 | -- Sets the parent of a scope.
46 | function root:set_parent(new_parent)
47 | assert(self ~= root, "the root scope's parent cannot be changed")
48 | assert(getmetatable(self)).__index = new_parent
49 | return new_parent
50 | end
51 |
52 | -- Creates a child scope that inherits from parent (default = root).
53 | function root.new_child(parent, id)
54 | return setmetatable({}, {id = id or '?',
55 | __index = parent or root, __newindex = __newindex})
56 | end
57 |
58 | -- Sets a new parent scope for child that inherits from the previous parent.
59 | function root.new_parent(child, id)
60 | return child:set_parent(child:get_parent():new_child(id))
61 | end
62 |
63 | -- Returns the string that identifies a scope.
64 | function root:get_id()
65 | return assert(getmetatable(self)).id
66 | end
67 |
68 | ------------------------------------------------------------------------------
69 | -- The lift.config scope (works as a proxy to its parent scope)
70 | ------------------------------------------------------------------------------
71 |
72 | local configMT = {id = 'config proxy', __index = root}
73 | configMT.__newindex = function(t, k, v)
74 | configMT.__index[k] = v -- write to config's parent
75 | end
76 | setmetatable(config, configMT)
77 |
78 | ------------------------------------------------------------------------------
79 | -- Scope Data Methods
80 | ------------------------------------------------------------------------------
81 |
82 | -- Gets a var as a boolean.
83 | function root:get_bool(var_name)
84 | local v = self[var_name] ; if not v or v == true then return v end
85 | if type(v) == 'string' then v = str_to_bool(v) else v = true end
86 | self[var_name] = v
87 | return v
88 | end
89 |
90 | -- Gets a var as a list. If the variable is a scalar it will be first converted
91 | -- to a list. Strings are split using lift.string.to_list(), other values are
92 | -- simply wrapped in a table.
93 | function root:get_list(var_name, read_only)
94 | local v = self[var_name] ; local tp = type(v)
95 | if tp == 'table' then return v end
96 | local t
97 | if tp == 'string' then -- split strings
98 | t = str_to_list(v)
99 | else -- return {v}
100 | t = {v}
101 | end
102 | if not read_only then
103 | self[var_name] = t -- update value
104 | end
105 | return t
106 | end
107 |
108 | -- Like get_list() but excludes duplicate values in the list.
109 | function root:get_unique_list(var_name, read_only)
110 | local t = self:get_list(var_name, read_only)
111 | local mt = getmetatable(t)
112 | if not mt then
113 | mt = {[0] = 'unique'}
114 | setmetatable(t, mt)
115 | local n = 1
116 | for i = 1, #t do
117 | local v = t[i]
118 | if not mt[v] then
119 | mt[v] = true
120 | if n < i then t[n] = v ; t[i] = nil end
121 | n = n + 1
122 | else
123 | t[i] = nil
124 | end
125 | end
126 | else
127 | assert(mt[0] == 'unique', 'incompatible table')
128 | end
129 | return t, mt
130 | end
131 |
132 | -- Gets a var as a list and inserts a value at position pos.
133 | -- Argument `pos` is optional and defaults to #list+1 (append).
134 | function root:insert(list_name, value, pos)
135 | local t = self:get_list(list_name)
136 | tinsert(t, pos or (#t+1), value)
137 | return self
138 | end
139 |
140 | -- Like insert() but, if the list already contains value, it's moved to the
141 | -- new position, instead of inserted (unless the value is being appended, in
142 | -- which case nothing is done). In order to guarantee uniqueness, all
143 | -- elements in the list must be inserted using this method.
144 | function root:insert_unique(list_name, value, pos)
145 | local t, mt = self:get_unique_list(list_name)
146 | if mt[value] then
147 | if not pos then return self end
148 | for i = 1, #t do if t[i] == value then table.remove(t, i) end end
149 | else
150 | mt[value] = true
151 | end
152 | tinsert(t, pos or (#t+1), value)
153 | return self
154 | end
155 |
156 | -- For each var call callback(key, value, scope_id, overridden).
157 | -- This includes inherited vars but excludes constants.
158 | -- Overridden vars are only included if `include_overridden` is true.
159 | function root:list_vars(callback, include_overridden)
160 | local vars = {} -- visited vars
161 | local s = self -- current scope
162 | while true do
163 | local mt = getmetatable(s)
164 | if s ~= root then -- skip constants
165 | for i, k in ipairs(util.keys_sorted_by_type(s)) do
166 | local visited = vars[k]
167 | if not visited then
168 | vars[k] = true
169 | end
170 | if not visited or include_overridden then
171 | callback(k, s[k], mt.id, visited)
172 | end
173 | end
174 | end
175 | if s == env_vars then break end
176 | s = mt.__index -- move to parent scope
177 | end
178 | end
179 |
180 | ------------------------------------------------------------------------------
181 | -- Module Methods
182 | ------------------------------------------------------------------------------
183 |
184 | -- Reverts lift.config to its initial state.
185 | -- This doesn't affect constants set through set_const().
186 | function root.reset()
187 | config:set_parent(root)
188 | end
189 |
190 | -- Allows apps to configure their own constants at startup.
191 | function root.set_const(key, value)
192 | root[key] = value
193 | end
194 |
195 | ------------------------------------------------------------------------------
196 | -- Initialization
197 | ------------------------------------------------------------------------------
198 |
199 | local path = require 'lift.path'
200 |
201 | -- built-in immutable vars
202 | config.set_const('APP_ID', 'lift')
203 | config.set_const('APP_VERSION', '0.1.0')
204 | config.set_const('LIFT_VERSION', config.APP_VERSION)
205 | config.set_const('LIFT_SRC_DIR',
206 | path.abs(path.dir(path.to_slash(debug.getinfo(1, "S").source:sub(2)))))
207 | config.set_const('LUA_EXE_PATH', require'luv'.exepath())
208 |
209 | return config
210 |
--------------------------------------------------------------------------------
/lift/files/cli.lua:
--------------------------------------------------------------------------------
1 | ------------------------------------------------------------------------------
2 | -- Default Command-Line Interface
3 | ------------------------------------------------------------------------------
4 |
5 | local color = require 'lift.color'
6 | local config = require 'lift.config'
7 | local diagnostics = require 'lift.diagnostics'
8 |
9 | local app = ...
10 |
11 | -- hide options --help and --version
12 | app.options.help.hidden = true
13 | app.options.version.hidden = true
14 |
15 | app:flag 'color'
16 | :desc('--color=off', 'Disable colorized output')
17 | :action(function(option, value) color.set_enabled(value) end)
18 |
19 | local quiet = app:flag 'quiet'
20 | :desc('--quiet', 'Suppress messages (prints warnings and errors)')
21 | :action(function(option, value)
22 | diagnostics.levels.remark = value and 'ignored' or 'remark'
23 | end)
24 |
25 | app:flag 'silent'
26 | :desc('--silent', 'Suppress messages (prints errors)')
27 | :action(function(option, value)
28 | quiet(value) -- implies -quiet
29 | diagnostics.levels.remark = value and 'ignored' or 'remark'
30 | end)
31 |
32 | app:flag 'trace'
33 | :desc('--trace', 'Enable debug tracing')
34 | :action(function(option, value) diagnostics.set_tracing(value) end)
35 |
36 | ------------------------------------------------------------------------------
37 | -- If config 'gc' is set, toggle garbage collection
38 | ------------------------------------------------------------------------------
39 |
40 | local gc = config:get_bool'gc'
41 | if gc ~= nil then
42 | collectgarbage(gc and 'restart' or 'stop')
43 | diagnostics.report('remark: Garbage collection is ${1}',
44 | gc and 'enabled' or 'disabled')
45 | end
46 |
47 |
--------------------------------------------------------------------------------
/lift/files/cli_config.lua:
--------------------------------------------------------------------------------
1 | ------------------------------------------------------------------------------
2 | -- CLI config management commands
3 | ------------------------------------------------------------------------------
4 |
5 | local path = require 'lift.path'
6 | local config = require 'lift.config'
7 | local diagnostics = require 'lift.diagnostics'
8 | local inspect = require'lift.util'.inspect
9 | local ESC = require'lift.color'.ESC
10 | local str_find = string.find
11 |
12 | local function config_get(command)
13 | local key = command:consume('key')
14 | local value = config[key]
15 | if type(value) ~= 'string' then
16 | value = inspect(value)
17 | end
18 | io.write(value)
19 | end
20 |
21 | local function config_list(command)
22 | local patt = '.'
23 | if #command.args > 0 then
24 | patt = command:consume('pattern')
25 | end
26 | local write, prev_scope = io.write, nil
27 | config:list_vars(function(key, value, scope, overridden)
28 | if not str_find(key, patt) then return end -- filter by pattern
29 | if scope ~= prev_scope then
30 | write(ESC'dim', '\n-- from ', scope, ESC'clear', '\n')
31 | prev_scope = scope
32 | end
33 | if overridden then
34 | write(ESC'dim;red', tostring(key), ESC'clear',
35 | ESC'dim', ' (overridden)', ESC'clear', '\n')
36 | else
37 | write(ESC'red', tostring(key), ESC'clear',
38 | ESC'green', ' = ', ESC'clear',
39 | ESC'cyan', inspect(value), ESC'clear', '\n')
40 | end
41 | end, true)
42 | end
43 |
44 | local function config_edit(command)
45 | local sys = command.options.system.value
46 | local dir = sys and config.system_config_dir or config.user_config_dir
47 | local filename = path.from_slash(dir..'/init.lua')
48 | local cmd = ('%s %q'):format(config.editor, filename)
49 | diagnostics.report('remark: running ${1}', cmd)
50 | os.execute(cmd)
51 | end
52 |
53 | local app = ...
54 |
55 | local config_cmd = app:command 'config'
56 | :desc('config', 'Configuration management subcommands')
57 |
58 | config_cmd:command 'edit' :action(config_edit)
59 | :desc('config edit [-s]', 'Opens the config file in an editor')
60 | :flag 'system' :alias 's'
61 | :desc('-s, --system', "Edit the system's config file instead of the user's")
62 |
63 | config_cmd:command 'get' :action(config_get)
64 | :desc('config get ', 'Print a config value to stdout')
65 |
66 | config_cmd:command 'list' :action(config_list)
67 | :desc('config list [pattern]', 'List config variables along with their values')
68 |
69 |
--------------------------------------------------------------------------------
/lift/files/init.lua:
--------------------------------------------------------------------------------
1 | ------------------------------------------------------------------------------
2 | -- Initial Configurations
3 | ------------------------------------------------------------------------------
4 |
5 | local config = ...
6 | local fs = require 'lift.fs'
7 | local ls = require 'lift.string'
8 | local path = require 'lift.path'
9 | local WINDOWS = require'lift.util'._WINDOWS
10 |
11 | -- Default editor
12 | if not config.editor then
13 | local v = os.getenv'EDITOR'
14 | if not v then
15 | v = WINDOWS and 'notepad' or 'vi'
16 | end
17 | config.editor = v
18 | end
19 |
20 | ------------------------------------------------------------------------------
21 | -- Detect project_file and project_dir
22 | ------------------------------------------------------------------------------
23 |
24 | -- Default project_file_names
25 | if not config.project_file_names then
26 | config.project_file_names = {
27 | ls.capitalize(config.APP_ID)..'file.lua',
28 | config.APP_ID..'file.lua',
29 | }
30 | end
31 |
32 | -- Default project_dir_name
33 | if not config.project_dir_name then
34 | config.project_dir_name = '.'..config.APP_ID
35 | end
36 |
37 | (function()
38 | local dir = fs.cwd()
39 | repeat
40 | for i, name in ipairs(config.project_file_names) do
41 | local file = dir..'/'..name
42 | if fs.is_file(file) then
43 | config.project_file = file
44 | config.project_dir = dir
45 | return
46 | end
47 | end
48 | if fs.is_dir(dir..'/'..config.project_dir_name) then
49 | config.project_dir = dir
50 | return
51 | end
52 | dir = path.dir(dir)
53 | until #dir <= 1 or path.is_root(dir)
54 | end)()
55 |
56 | -- change cwd to project_dir
57 | if config.project_dir then
58 | fs.chdir(config.project_dir)
59 | end
60 |
61 | ------------------------------------------------------------------------------
62 | -- Portable Directory Paths: {system,user}_{config,data}_dir and cache_dir
63 | -- We follow the XDG specification on UNIX and something sensible on Windows.
64 | ------------------------------------------------------------------------------
65 |
66 | local function env(var_name, default_value)
67 | local v = var_name and os.getenv(var_name)
68 | return (v and path.to_slash(v)) or default_value
69 | end
70 |
71 | local function set_dir(name, unix_var, unix_default, win_var, win_default)
72 | if not config[name] then
73 | if WINDOWS then
74 | config[name] = env(win_var, win_default) ..'/'.. config.APP_ID
75 | else
76 | config[name] = env(unix_var, unix_default) ..'/'.. config.APP_ID
77 | end
78 | end
79 | end
80 |
81 | local function user_home(p)
82 | local home = config.HOME
83 | return home and (home..p) or p
84 | end
85 |
86 | set_dir('system_config_dir',
87 | nil, '/etc/xdg',
88 | 'ProgramFiles', 'c:/Program Files')
89 |
90 | set_dir('system_data_dir',
91 | nil, '/usr/local/share',
92 | 'ProgramData', 'c:/ProgramData')
93 |
94 | set_dir('user_config_dir',
95 | 'XDG_CONFIG_HOME', user_home('/.config'),
96 | 'APPDATA', 'c:')
97 |
98 | set_dir('user_data_dir',
99 | 'XDG_DATA_HOME', user_home('/.local/share'),
100 | 'LOCALAPPDATA', 'c:')
101 |
102 | set_dir('cache_dir',
103 | 'XDG_CACHE_HOME', user_home('/.cache'),
104 | 'TEMP', 'c:/Temp')
105 |
106 | ------------------------------------------------------------------------------
107 | -- Default load_path
108 | ------------------------------------------------------------------------------
109 |
110 | local function add_path(p)
111 | if fs.is_dir(p) then
112 | config:insert_unique('load_path', p)
113 | return true
114 | end
115 | end
116 |
117 | -- env vars have precedence over everything except the CLI
118 | if config.LOAD_PATH then
119 | for i, dir in ipairs(config:get_list('LOAD_PATH', true)) do
120 | add_path(dir)
121 | end
122 | end
123 |
124 | -- add project-specific dir
125 | if config.project_dir then
126 | add_path(config.project_dir..'/'..config.project_dir_name)
127 | end
128 |
129 | -- add user and system-specific dirs
130 | add_path(config.user_config_dir)
131 | add_path(config.system_config_dir)
132 |
133 | -- add app-specific dirs specified via APP_LOAD_PATH
134 | if config.APP_LOAD_PATH then
135 | for i, dir in ipairs(config:get_list('APP_LOAD_PATH', true)) do
136 | add_path(dir)
137 | end
138 | end
139 |
--------------------------------------------------------------------------------
/lift/files/lift/cli_task.lua:
--------------------------------------------------------------------------------
1 | local task = require 'lift.task'
2 | local path = require 'lift.path'
3 | local util = require 'lift.util'
4 | local write, inspect = io.write, util.inspect
5 | local ESC = require'lift.color'.ESC
6 | local app = ...
7 |
8 | local task_cmd = app:command 'task'
9 | :desc('task', 'Subcommands to interface with tasks')
10 |
11 | ------------------------------------------------------------------------------
12 | -- Option: --plot file.svg
13 | ------------------------------------------------------------------------------
14 |
15 | local plot_option = task_cmd:option 'plot'
16 | :desc('--plot file.svg', 'Use graphviz to plot task dependencies')
17 |
18 | local plot_file
19 | function plot_option:matched(filename)
20 | plot_file = filename
21 | -- monkey patch Task:async() to track elapsed times
22 | local now = os.time
23 | local task_async = task._Task.async
24 | local function set_dt(future)
25 | future.dt = now() - future.t0
26 | end
27 | function task._Task.async(tsk, ...)
28 | local future = task_async(tsk, ...)
29 | if not future.t0 then
30 | future.t0 = now()
31 | future:on_ready(set_dt)
32 | end
33 | return future
34 | end
35 | end
36 |
37 | local function format_task(future)
38 | local arg = future.arg
39 | if arg then
40 | arg = ' ('..util.inspect(arg)..')'
41 | else
42 | arg = ''
43 | end
44 | return '<'..tostring(future.task)..arg..'>'
45 | end
46 |
47 | local function visit(from, sb)
48 | if from.visited then return end
49 | from.visited = true
50 | local calls = from.calls
51 | for i = 1, #calls do
52 | local to = calls[i]
53 | sb[#sb+1] = format_task(from)
54 | sb[#sb+1] = ' -> '
55 | sb[#sb+1] = format_task(to)
56 | sb[#sb+1] = '[label=" '..to.dt..'s "];\n'
57 | visit(to, sb)
58 | end
59 | end
60 |
61 | local function plot_graph()
62 | if not plot_file then return end
63 | local sb = {'digraph graphname {\n'}
64 | local roots = task._get_roots()
65 | for i = 1, #roots do
66 | visit(roots[i], sb)
67 | end
68 | sb[#sb+1] = '}\n'
69 | local format = path.ext(plot_file)
70 | local dot = require'lift.os'.spawn{file = 'dot', '-T'..format,
71 | '-o', plot_file, stdout = 'inherit', stderr = 'inherit'}
72 | dot:write(table.concat(sb))
73 | dot:write()
74 | end
75 |
76 | ------------------------------------------------------------------------------
77 | -- Command: task run
78 | ------------------------------------------------------------------------------
79 |
80 | local run_cmd = task_cmd:command 'run'
81 | :desc('run [tasks]', 'Run a set of tasks concurrently')
82 | :epilog("If no task name is given, the 'default' task is run.")
83 |
84 | function run_cmd:run()
85 | local tasks = task{}
86 | for i, name in ipairs(self.args) do
87 | tasks[i] = task:get_task(name)
88 | end
89 | if #tasks == 0 then
90 | tasks[1] = task:get_task'default'
91 | end
92 | tasks()
93 | plot_graph()
94 | end
95 |
96 | ------------------------------------------------------------------------------
97 | -- Command: task call
98 | ------------------------------------------------------------------------------
99 |
100 | local call_cmd = task_cmd:command 'call'
101 | :desc('call [args]', 'Invoke a task passing a list of arguments')
102 |
103 | function call_cmd:run()
104 | local callee = task:get_task(self:consume 'task')
105 | local args = self.args
106 | args[0] = nil
107 | args.used = nil -- disable warning about unused args
108 | table.remove(args, 1)
109 | write(ESC'green', 'Calling ', tostring(callee), inspect(args),
110 | ESC'clear', '\n')
111 | callee(args)
112 | local res = callee:get_results(args)
113 | write(ESC'green', 'Task results = ', inspect(res), ESC'clear')
114 | plot_graph()
115 | end
116 |
117 | ------------------------------------------------------------------------------
118 | -- Command: task list
119 | ------------------------------------------------------------------------------
120 |
121 | local list_cmd = task_cmd:command 'list'
122 | :desc('list [pattern]', 'Print tasks that match an optional pattern')
123 |
124 | local function list_namespace(ns, pattern)
125 | local task_descs, width = {}, 0
126 | for name, task_object in pairs(ns.tasks) do
127 | local full_name = tostring(task_object)
128 | if full_name:find(pattern) then -- filter by pattern
129 | if name == 'default' and full_name ~= 'default' then
130 | task_descs[#task_descs+1] = name
131 | task_descs[name] = '= '..full_name
132 | else
133 | task_descs[#task_descs+1] = full_name
134 | task_descs[full_name] = task_object.description or ''
135 | end
136 | if width < #full_name then width = #full_name end
137 | end
138 | end
139 | table.sort(task_descs)
140 | for i, name in ipairs(task_descs) do
141 | local indent = (' '):rep(width - #name)
142 | write(ESC'green', name, ESC'clear', indent, ' ', task_descs[name], '\n')
143 | end
144 | local count, nested = #task_descs, ns.nested
145 | for i, name in ipairs(util.keys_sorted(nested)) do
146 | count = count + list_namespace(nested[name], pattern)
147 | end
148 | return count
149 | end
150 |
151 | function list_cmd:run()
152 | local pattern = '.'
153 | if #self.args > 0 then
154 | pattern = self:consume 'pattern'
155 | end
156 | local count = list_namespace(task, pattern)
157 | local suffix = (pattern == '.' and '' or (" with pattern '"..pattern.."'"))
158 | write('# Found ', count, ' tasks', suffix, '\n')
159 | end
160 |
--------------------------------------------------------------------------------
/lift/loader.lua:
--------------------------------------------------------------------------------
1 | ------------------------------------------------------------------------------
2 | -- Find and Load Lua Files in the ${load_path}
3 | ------------------------------------------------------------------------------
4 |
5 | local loadfile = loadfile
6 | local str_match, str_sub = string.match, string.sub
7 |
8 | local glob = require'lift.fs'.glob
9 | local path = require 'lift.path'
10 | local config = require 'lift.config'
11 | local diagnostics = require 'lift.diagnostics'
12 |
13 | -- Returns an iterator over the Lua files in the ${load_path} that follow
14 | -- a certain naming convention and match the 'type' and 'subtype' strings.
15 | -- When subtype is omitted all subtypes are matched.
16 | -- ./${type}.lua
17 | -- ./${type}[_/]${subtype}.lua
18 | -- ./${type}[_/]${subtype}[_/]*.lua
19 | local separators = {'_', '/'}
20 | local endings = {'.lua', '_*.lua', '/*.lua'}
21 | local function find_scripts(type, subtype)
22 | local vars = {
23 | path = config:get_list'load_path',
24 | type = type,
25 | sep = separators,
26 | subtype = subtype,
27 | ending = endings,
28 | }
29 | local pattern = '${path}/${type}${sep}${subtype}${ending}'
30 | if not subtype then pattern = '${path}/${type}${ending}' end
31 | return glob(pattern, vars)
32 | end
33 |
34 | -- custom diagnostic for Lua syntax errors
35 | diagnostics.levels.lua_syntax_error = 'fatal'
36 | diagnostics.styles.lua_syntax_error = {prefix = 'syntax error:', fg = 'red'}
37 |
38 | local load_file = diagnostics.trace('[loader] loading ${filename}',
39 | function(filename, ...)
40 | local chunk, err = loadfile(path.from_slash(filename), 't')
41 | if chunk then
42 | chunk(...)
43 | else
44 | local line, e = str_match(err, '^..[^:]+:([^:]+): ()')
45 | local msg = str_sub(err, e)
46 | diagnostics.new{'lua_syntax_error: ', message = msg,
47 | location = {file = filename, line = tonumber(line)}}:report()
48 | end
49 | end)
50 |
51 | local load_all = diagnostics.trace(
52 | '[loader] running all ${type} ${subtype} scripts',
53 | '[loader] finished all ${type} ${subtype} scripts',
54 | function(type, subtype, ...)
55 | for filename in find_scripts(type, subtype) do
56 | load_file(filename, ...)
57 | end
58 | end)
59 |
60 | local function run_init(filename)
61 | local scope = config:new_parent(filename)
62 | load_file(filename, scope)
63 | return scope
64 | end
65 |
66 | -- Executes all init scripts in the ${load_path}.
67 | -- The first loaded script is "${LIFT_SRC_DIR}/files/init.lua" (hardcoded).
68 | -- Other scripts are then loaded in the reverse order of entry in ${load_path}.
69 | -- This usually means that system configs are loaded next, then user configs,
70 | -- then local filesystem (project) configs.
71 | local init = diagnostics.trace(
72 | '[loader] running init scripts',
73 | '[loader] finished init scripts',
74 | function()
75 | local top_scope -- keep track of the top config scope
76 | -- run the built-in init script
77 | local builtin_files = config.LIFT_SRC_DIR..'/files'
78 | top_scope = run_init(builtin_files..'/init.lua')
79 | -- run all init scripts in ${load_path}
80 | local list = {}
81 | for script in find_scripts('init') do
82 | list[#list+1] = script
83 | end
84 | for i = #list, 1, -1 do
85 | run_init(list[i])
86 | end
87 | -- run ${project_file} if available
88 | if config.project_file then
89 | run_init(config.project_file)
90 | end
91 | -- add built-in files to the ${load_path}
92 | config:insert_unique('load_path', builtin_files)
93 | return top_scope
94 | end)
95 |
96 | return {
97 | find_scripts = find_scripts,
98 | init = init,
99 | load_all = load_all,
100 | load_file = load_file,
101 | }
102 |
--------------------------------------------------------------------------------
/lift/os.lua:
--------------------------------------------------------------------------------
1 | ------------------------------------------------------------------------------
2 | -- Child process management and operating system utility functions
3 | ------------------------------------------------------------------------------
4 |
5 | local assert, getmetatable, setmetatable = assert, getmetatable, setmetatable
6 | local co_yield = coroutine.yield
7 | local diagnostics = require 'lift.diagnostics'
8 | local native_to_lf = require'lift.string'.native_to_lf
9 | local async = require 'lift.async'
10 | local async_get, async_resume = async._get, async._resume
11 | local stream = require 'lift.stream'
12 | local from_uv, to_uv = stream.from_uv, stream.to_uv
13 | local fs = require 'lift.fs'
14 | local util = require 'lift.util'
15 | local UNIX, WINDOWS = util._UNIX, util._WINDOWS
16 | local uv = require 'luv'
17 | local uv_new_pipe, uv_read_start = uv.new_pipe, uv.read_start
18 | local uv_new_timer, uv_timer_start = uv.new_timer, uv.timer_start
19 | local uv_close, uv_process_kill, uv_spawn = uv.close, uv.process_kill, uv.spawn
20 |
21 | ------------------------------------------------------------------------------
22 | -- Find an installed program file in a portable way
23 | ------------------------------------------------------------------------------
24 |
25 | local config = require 'lift.config'
26 | local extensions = config:get_list('PATHEXT', true)
27 | if #extensions == 0 then extensions[1] = '' end
28 | config.program_file_extensions = extensions
29 |
30 | local function find_program(name)
31 | local path = fs.glob('${PATH}/'..name..'${program_file_extensions}')()
32 | if path then return path end
33 | return nil, "could not find program '"..name.."' in the system"
34 | end
35 |
36 | ------------------------------------------------------------------------------
37 | -- ChildProcess objects and spawn()
38 | ------------------------------------------------------------------------------
39 |
40 | -- custom diagnostic for child process related errors
41 | diagnostics.levels.child_process_error = 'fatal'
42 | diagnostics.styles.child_process_error =
43 | {prefix = 'error in child process:', fg = 'red'}
44 |
45 | local ChildProcess = {}
46 | ChildProcess.__index = ChildProcess
47 |
48 | -- Sends a signal to the process. Defaults to SIGTERM (terminate process).
49 | function ChildProcess:kill(signal)
50 | local h = self.handle
51 | if h then
52 | uv_process_kill(self.handle, signal or 'sigterm')
53 | else
54 | error('process:kill() called after process termination', 2)
55 | end
56 | end
57 |
58 | -- Registers a function to be called when the process terminates.
59 | function ChildProcess:on_exit(cb)
60 | local t = self.on_exit_cb
61 | t[#t+1] = cb
62 | end
63 |
64 | function ChildProcess:wait(timeout)
65 | if not self.handle then return true end -- already exited
66 | local this_future = async_get()
67 | local status, signal, cb
68 | if timeout then
69 | local timer = uv_new_timer()
70 | cb = function(p, _status, _signal)
71 | if timer == nil then return false end -- ignore second call
72 | status, signal = _status or false, _signal or 'timed out'
73 | async_resume(this_future)
74 | uv_close(timer)
75 | timer = nil
76 | end
77 | uv_timer_start(timer, timeout, 0, cb)
78 | else
79 | cb = function(p, _status, _signal)
80 | status, signal = _status, _signal
81 | async_resume(this_future)
82 | end
83 | end
84 | self:on_exit(cb)
85 | co_yield()
86 | return status, signal
87 | end
88 |
89 | function ChildProcess:pipe(writable_process, keep_open)
90 | local writable = writable_process
91 | if getmetatable(writable) == ChildProcess then
92 | writable = assert(writable.stdin, 'process has no stdin')
93 | end
94 | self.stdout:pipe(writable, keep_open)
95 | return writable_process
96 | end
97 |
98 | function ChildProcess:write(...)
99 | return self.stdin:write(...)
100 | end
101 |
102 | function ChildProcess:read()
103 | return self.stdout:read()
104 | end
105 |
106 | function ChildProcess:try_read()
107 | return self.stdout:try_read()
108 | end
109 |
110 | local function prepare_stdio(p, name, fd, readable)
111 | local v, s = p[name] or 'pipe', nil
112 | if v == 'pipe' then
113 | v = uv_new_pipe(false)
114 | s = (readable and from_uv or to_uv)(v)
115 | elseif v == 'ignore' then
116 | v = nil
117 | elseif v == 'inherit' then
118 | v = fd
119 | s = (readable and from_uv or to_uv)(uv.new_tty(fd, not readable))
120 | else
121 | error("invalid stdio option '"..tostring(v).."'", 3)
122 | end
123 | p[name] = s
124 | return v
125 | end
126 |
127 | local spawn = diagnostics.trace(
128 | '[os] spawning process ${p}',
129 | function(p)
130 | local file = p.file
131 | if not file then error('you must specify a file to spawn()', 2) end
132 | p.args = p
133 | -- handle stdio options
134 | local si = prepare_stdio(p, 'stdin', 0, false)
135 | local so = prepare_stdio(p, 'stdout', 1, true)
136 | local se = prepare_stdio(p, 'stderr', 2, true)
137 | p.stdio = {si, so, se} -- TODO we could avoid using a table for stdio
138 | -- hide console windows by default on Windows
139 | if WINDOWS and p.hide == nil then p.hide = true end
140 | -- spawn and check for error
141 | local proc, pid = uv_spawn(file, p, function(status, signal)
142 | p.status = status
143 | p.signal = signal
144 | uv_close(assert(p.handle))
145 | p.handle = nil
146 | local cb_list = p.on_exit_cb
147 | for i = 1, #cb_list do
148 | cb_list[i](p, status, signal)
149 | end
150 | end)
151 | if not proc then
152 | return nil, diagnostics.new("child_process_error: spawn failed: ${1}", pid)
153 | end
154 | p.args, p.stdio = nil, nil
155 | p.pid = pid
156 | p.handle = proc
157 | p.on_exit_cb = {}
158 | return setmetatable(p, ChildProcess)
159 | end)
160 |
161 | ------------------------------------------------------------------------------
162 | -- sh() facilitates the execution of shell commmands
163 | ------------------------------------------------------------------------------
164 |
165 | local shell_program = (UNIX and '/bin/sh' or os.getenv'COMSPEC')
166 |
167 | -- Returns the stdout and stderr of running `command` in the OS shell, or nil
168 | -- plus a diagnostic object (with exit state and output) when `command` fails.
169 | -- Line endings in stdout and stderr are normalized to LF.
170 | -- Security Notice: never execute a command interpolated with external input
171 | -- (such as a config string) as that leaves you vulnerable to shell injection.
172 | -- See sh().
173 | local try_sh = diagnostics.trace(
174 | '[os] executing shell command: ${command}',
175 | function(command, level)
176 | local this_future = async_get()
177 | local status, signal
178 | local function on_exit(_status, _signal)
179 | status = _status
180 | signal = _signal
181 | async_resume(this_future)
182 | end
183 | local stdout, stderr = uv_new_pipe(false), uv_new_pipe(false)
184 | local options = {UNIX and '-c' or '/c', command, -- args
185 | stdio = {nil, stdout, stderr}, hide = true, verbatim = true}
186 | options.args = options
187 | local proc, pid = uv_spawn(shell_program, options, on_exit)
188 | if not proc then
189 | return nil, diagnostics.new("child_process_error: spawn failed: ${1}", pid)
190 | end
191 | local so, se = '', ''
192 | uv_read_start(stdout, function(err, data)
193 | if data then so = so..data end
194 | end)
195 | uv_read_start(stderr, function(err, data)
196 | if data then se = se..data end
197 | end)
198 | co_yield()
199 | uv_close(proc)
200 | so, se = native_to_lf(so), native_to_lf(se)
201 | if status ~= 0 or signal ~= 0 then
202 | local what
203 | if signal == 0 then
204 | what = 'failed with status '..status
205 | else
206 | what = 'interrupted with signal '..signal
207 | end
208 | return nil, diagnostics.new{
209 | 'child_process_error: shell command ${what}: ${stderr}',
210 | what = what, status = status, signal = signal, stdout = so,
211 | stderr = se}:set_location(level or 2)
212 | end
213 | return so, se
214 | end)
215 |
216 | -- Like try_sh() but raises an error if the command fails.
217 | local function sh(command)
218 | local out, err = try_sh(command, 3)
219 | if out == nil then error(err, 2) end
220 | return out, err
221 | end
222 |
223 | ------------------------------------------------------------------------------
224 | -- Module Initialization
225 | ------------------------------------------------------------------------------
226 |
227 | return {
228 | UNIX = UNIX,
229 | WINDOWS = WINDOWS,
230 | find_program = find_program,
231 | sh = sh,
232 | spawn = spawn,
233 | try_sh = try_sh,
234 | }
235 |
--------------------------------------------------------------------------------
/lift/path.lua:
--------------------------------------------------------------------------------
1 | ------------------------------------------------------------------------------
2 | -- File path manipulation routines (by string manipulation)
3 | ------------------------------------------------------------------------------
4 |
5 | local tbl_concat = table.concat
6 | local str_gmatch, str_match = string.gmatch, string.match
7 | local str_gsub, str_lower, str_sub = string.gsub, string.lower, string.sub
8 |
9 | local WINDOWS = require'lift.util'._WINDOWS
10 |
11 | ------------------------------------------------------------------------------
12 | -- OS abstraction
13 | ------------------------------------------------------------------------------
14 |
15 | local to_slash -- Converts each system-specific path separator to '/'.
16 | local from_slash -- Converts each '/' to the system-specific path separator.
17 | local SEP -- Platform-specific path separator ('/' or '\')
18 | local DELIMITER -- Platform-specific path delimiter (':' or ';')
19 |
20 | if WINDOWS then
21 | SEP, DELIMITER = '\\', ';'
22 | to_slash = function(path) return str_lower((str_gsub(path, SEP, '/'))) end
23 | from_slash = function(path) return (str_gsub(path, '/', SEP)) end
24 | else
25 | SEP, DELIMITER = '/', ':'
26 | to_slash = function(path) return path end
27 | from_slash = to_slash
28 | end
29 |
30 | ------------------------------------------------------------------------------
31 | -- All routines below assume paths separated with '/' and case sensitive.
32 | -- Use to_slash() when you obtain a path from outside Lift.
33 | ------------------------------------------------------------------------------
34 |
35 | -- Returns true if the path is a FS root ('/' on UNIX, 'x:/' on Windows).
36 | local function is_root(path)
37 | return path == '/' or (#path == 3 and str_match(path, '^%a:/$'))
38 | end
39 |
40 | -- Returns the leading volume name (for Windows paths only).
41 | -- Given 'C:/foo' it returns 'C:'. Given '/foo' it returns ''.
42 | local function volume(path)
43 | return str_match(path, '^(%a:)') or ''
44 | end
45 |
46 | -- Returns the last element of a path. Ignores any trailing slash.
47 | -- Returns '.' if the path is empty, or '/' if the path is '/'.
48 | local function base(path)
49 | if path == '' then return '.' end
50 | return str_match(path, '([^/]+)/?$') or '/'
51 | end
52 |
53 | -- Returns the directory part of a path (all but the last element).
54 | -- The result has no trailing '/' unless it is the root directory.
55 | local function dir(path)
56 | path = str_match(path, '^(.*)/')
57 | if not path then return '.' end
58 | if path == '' then return '/' end
59 | if str_sub(path, -1) == ':' then path = path .. '/' end
60 | return path
61 | end
62 |
63 | -- Returns the extension of the path, from the last '.' to the end of string
64 | -- in the last portion of the path. Returns the empty string if there is no '.'
65 | local function ext(path)
66 | return str_match(path, '%.([^./]*)$') or ''
67 | end
68 |
69 | -- Returns the shortest equivalent of a path by lexical processing.
70 | -- All '//', '/./' and '/dir/../' become just '/'. The result has
71 | -- no trailing slash unless it is the root, or if preserve_slash is true.
72 | -- If the path is empty, returns '.' (the current working directory).
73 | local function clean(path, preserve_trailing_slash)
74 | if path == '' then return '.' end
75 | if preserve_trailing_slash then
76 | if str_sub(path, -1) ~= '/' then
77 | path = path .. '/'
78 | preserve_trailing_slash = false
79 | end
80 | else path = path .. '/' end
81 | path = str_gsub(path, '/%./', '/') -- '/./' to '/'
82 | path = str_gsub(path, '/+', '/') -- order matters here
83 | path = str_gsub(path, '/[^/]+/%.%./', '/') -- '/dir/../' to '/'
84 | path = str_gsub(path, '^/%.%./', '/') -- ignore /../ at root
85 | if preserve_trailing_slash or is_root(path) then return path end
86 | return str_sub(path, 1, -2)
87 | end
88 |
89 | -- Returns whether path is an absolute path.
90 | -- True if it starts with '/' on UNIX, or 'X:/' on Windows.
91 | local function is_abs(path)
92 | return str_sub(path, 1, 1) == '/' or str_sub(path, 2, 2) == ':'
93 | end
94 |
95 | -- Resolves `path` to an absolute path. If `path` isn't already absolute, it
96 | -- is prepended with `from` (which when not given, defaults to the cwd).
97 | -- The resulting path is cleaned and trailing slashes are removed unless
98 | -- `preserve_trailing_slash` is true.
99 | local function abs(path, from, preserve_trailing_slash)
100 | if is_abs(path) then return path end
101 | if not from then from = to_slash(require'luv'.cwd()) end
102 | return clean(from..'/'..path, preserve_trailing_slash)
103 | end
104 |
105 | -- Solves the relative path from `from` to `to`.
106 | -- Paths must be both absolute or both relative, or an error is raised.
107 | -- This is the reverse transform of abs(): abs(rel(from, to), from) == rel(to).
108 | -- The resulting path is always relative on UNIX. On Windows, when paths
109 | -- are on different volumes it's impossible to create a relative path,
110 | -- so `to` is returned (in this case, `to` is absolute).
111 | local function rel(from, to)
112 | local is_abs_from, is_abs_to = is_abs(from), is_abs(to)
113 | if is_abs_from ~= is_abs_to then
114 | error("expected two relative paths or two absolute paths", 2)
115 | end
116 | if volume(from) ~= volume(to) then
117 | return to -- should we raise an error instead?
118 | end
119 | from, to = clean(from), clean(to)
120 | if from == to then return '.' end
121 | -- position both iterators at the first differing elements
122 | local match_from = str_gmatch(from, '[^/]+')
123 | local match_to = str_gmatch(to, '[^/]+')
124 | local res, from_elem, to_elem = {}
125 | repeat from_elem, to_elem = match_from(), match_to()
126 | until from_elem ~= to_elem
127 | -- we go up the hierarchy while there are elements left in `from`
128 | while from_elem do res[#res + 1] = '..' ; from_elem = match_from() end
129 | -- then we go down the path to `to`
130 | while to_elem do
131 | res[#res + 1] = to_elem ; to_elem = match_to()
132 | end
133 | return tbl_concat(res, '/')
134 | end
135 |
136 | -- Joins any number of path elements and cleans the resulting path.
137 | local function join(...)
138 | return clean(tbl_concat({...}, '/'))
139 | end
140 |
141 | -- Splits a path at its last separator. Returns dir, file (path = dir..file).
142 | -- If path has no separator, returns '', path.
143 | local function split(path)
144 | local d, f = str_match(path, '^(.*/)([^/]*)$')
145 | return d or '', f or path
146 | end
147 |
148 | ------------------------------------------------------------------------------
149 | -- Module Table
150 | ------------------------------------------------------------------------------
151 |
152 | return {
153 | abs = abs,
154 | base = base,
155 | clean = clean,
156 | delimiter = DELIMITER,
157 | dir = dir,
158 | ext = ext,
159 | from_slash = from_slash,
160 | is_abs = is_abs,
161 | is_root = is_root,
162 | join = join,
163 | rel = rel,
164 | sep = SEP,
165 | split = split,
166 | to_slash = to_slash,
167 | volume = volume,
168 | }
169 |
--------------------------------------------------------------------------------
/lift/request.lua:
--------------------------------------------------------------------------------
1 | ------------------------------------------------------------------------------
2 | -- Simplified HTTP request client
3 | ------------------------------------------------------------------------------
4 | -- Current implementation assumes the `curl` executable is in the ${PATH}.
5 | -- Inspired by https://github.com/request/request
6 |
7 | local assert = assert
8 | local stream = require 'lift.stream'
9 |
10 | local os = require 'lift.os'
11 | local spawn = os.spawn
12 |
13 | -- HTTP(S) GET request.
14 | local curl_path = assert(os.find_program('curl'))
15 | local function noop() end
16 | local function get(url)
17 | if url:sub(1, 1) == '-' then error('malformed URL', 2) end
18 | local cp = assert(spawn{file = curl_path, '-sS', '-L', url, stdin = 'ignore'})
19 | local s = stream.new_readable(noop, 'request.get('..url..')')
20 | local e -- gathers any error received via either stderr or stdout
21 | local waiting = 2
22 | local so, se = cp.stdout, cp.stderr
23 | so:on_data(function(_, data, err)
24 | if data == nil then
25 | waiting = waiting - 1
26 | if waiting == 0 then
27 | s:push(nil, err or e)
28 | else
29 | e = err
30 | end
31 | else
32 | s:push(data)
33 | end
34 | end)
35 | local err_msg = ''
36 | se:on_data(function(_, data, err)
37 | if data == nil then
38 | if err_msg ~= '' then e = err_msg end
39 | waiting = waiting - 1
40 | if waiting == 0 then
41 | s:push(nil, err or e)
42 | end
43 | else
44 | err_msg = err_msg .. data
45 | end
46 | end)
47 | so:start()
48 | se:start()
49 | return s
50 | end
51 |
52 | ------------------------------------------------------------------------------
53 | -- Module Table/Functor
54 | ------------------------------------------------------------------------------
55 |
56 | return setmetatable({
57 | get = get,
58 | }, {__call = function(M, ...) -- calling the module == calling get()
59 | return get(...)
60 | end})
61 |
--------------------------------------------------------------------------------
/lift/string.lua:
--------------------------------------------------------------------------------
1 | ------------------------------------------------------------------------------
2 | -- String manipulation routines
3 | ------------------------------------------------------------------------------
4 |
5 | local tostring, tonumber = tostring, tonumber
6 | local str_find, str_gmatch, str_gsub = string.find, string.gmatch, string.gsub
7 | local str_sub, str_upper = string.sub, string.upper
8 |
9 | local WINDOWS = require'lift.util'._WINDOWS
10 |
11 | local lpeg = require 'lpeg'
12 | local P, R, V, Ca, Cs = lpeg.P, lpeg.R, lpeg.V, lpeg.Carg, lpeg.Cs
13 |
14 | ------------------------------------------------------------------------------
15 | -- Basic transformations
16 | ------------------------------------------------------------------------------
17 |
18 | -- Returns the Capitalized form of a string
19 | local function capitalize(str)
20 | return (str_gsub(str, '^%l', str_upper))
21 | end
22 |
23 | -- Returns the camelCase form of a string keeping the 1st word unchanged
24 | local function camelize(str)
25 | return (str_gsub(str, '%W+(%w+)', capitalize))
26 | end
27 |
28 | -- Returns the UpperCamelCase form of a string
29 | local function classify(str)
30 | return (str_gsub(str, '%W*(%w+)', capitalize))
31 | end
32 |
33 | -- Separates a camelized string by underscores, keeping capitalization
34 | local function decamelize(str)
35 | return (str_gsub(str, '(%l)(%u)', '%1_%2'))
36 | end
37 |
38 | -- Replaces each word separator with a single dash
39 | local function dasherize(str)
40 | return (str_gsub(str, '%W+', '-'))
41 | end
42 |
43 | ------------------------------------------------------------------------------
44 | -- Iterate substrings by splitting at any character in a set of delimiters
45 | ------------------------------------------------------------------------------
46 |
47 | local DELIMITERS = (WINDOWS and ';,' or ':;,')
48 |
49 | local function split(str, delimiters)
50 | delimiters = delimiters or DELIMITERS
51 | return str_gmatch(str, '([^'..delimiters..']+)['..delimiters..']*')
52 | end
53 |
54 | ------------------------------------------------------------------------------
55 | -- String-to-type conversions
56 | ------------------------------------------------------------------------------
57 |
58 | local BOOLEANS = {
59 | ['1'] = true, ON = true, TRUE = true, Y = true, YES = true,
60 | ['0'] = false, FALSE = false, N = false, NO = false, OFF = false,
61 | }
62 |
63 | -- Returns true/false for well-defined bool constants, or nil otherwise
64 | local function to_bool(str)
65 | return BOOLEANS[str_upper(str)]
66 | end
67 |
68 | -- Splits a string on ';' or ',' (or ':' on UNIX).
69 | -- Can be used to split ${PATH}. Returns an iterator, NOT a table.
70 | local LIST_ELEM_PATT = '([^'..DELIMITERS..']+)['..DELIMITERS..']*'
71 | local function to_list(str)
72 | local t = {}
73 | for substr in str_gmatch(str, LIST_ELEM_PATT) do
74 | t[#t+1] = substr
75 | end
76 | return t
77 | end
78 |
79 | ------------------------------------------------------------------------------
80 | -- Line-ending conversions
81 | ------------------------------------------------------------------------------
82 |
83 | local function lf_to_crlf(text)
84 | return str_gsub(text, '\n', '\r\n')
85 | end
86 |
87 | local function crlf_to_lf(text)
88 | return str_gsub(text, '\r\n', '\n')
89 | end
90 |
91 | local function native_to_lf(text)
92 | return WINDOWS and crlf_to_lf(text) or text
93 | end
94 |
95 | local function lf_to_native(text)
96 | return WINDOWS and lf_to_crlf(text) or text
97 | end
98 |
99 | ------------------------------------------------------------------------------
100 | -- Pattern-matching utilities
101 | ------------------------------------------------------------------------------
102 |
103 | -- Escapes any "magic" character in str for use in a Lua pattern.
104 | local function escape_magic(str)
105 | return (str_gsub(str, '[$^%().[%]*+-?]', '%%%1'))
106 | end
107 |
108 | -- Converts a basic glob pattern to a Lua pattern. Supports '*', '?'
109 | -- and Lua-style [character classes]. Use a char class to escape: '[*]'.
110 | local glob_to_lua = { ['^'] = '%^', ['$'] = '%$', ['%'] = '%%',
111 | ['('] = '%(', [')'] = '%)', ['.'] = '%.', ['['] = '%[', [']'] = '%]',
112 | ['+'] = '%+', ['-'] = '%-', ['?'] = '[^/]', ['*'] = '[^/]*' }
113 | local function from_glob(glob)
114 | -- copy [char-classes] verbatim; translate magic chars everywhere else
115 | local i, res = 1, ''
116 | repeat
117 | local s, e, cclass = str_find(glob, '(%[.-%])', i)
118 | local before = str_sub(glob, i, s and s - 1)
119 | res = res..str_gsub(before, '[$^%().[%]*+-?]', glob_to_lua)..(cclass or '')
120 | i = e and e + 1
121 | until not i
122 | return res
123 | end
124 |
125 | ------------------------------------------------------------------------------
126 | -- String interpolation (recursive variable expansions using LPeg)
127 | ------------------------------------------------------------------------------
128 |
129 | local VB, VE = P'${', P'}'
130 | local INTEGER = R'09'^1 / tonumber
131 | local function map_var(f, m, k)
132 | local v = f(m, k)
133 | if not v then v = '${MISSING:'..k..'}' end
134 | return tostring(v)
135 | end
136 | local Xpand = P{
137 | Cs( (1-VB)^0 * V'Str' * P(1)^0 ),
138 | Str = ( (1-VB-VE)^1 + V'Var' )^1,
139 | Var = Ca(1) * Ca(2) * VB * (INTEGER*VE + Cs(V'Str')*VE) / map_var
140 | }
141 |
142 | local function index_table(t, k) return t[k] end -- default get_var
143 |
144 | -- Replaces '${foo}' with the result of get_var(vars, 'foo'). The key can be
145 | -- a string or an integer. When `vars` is a table, `get_var` can be omitted.
146 | local function expand(str, vars, get_var)
147 | return lpeg.match(Xpand, str, nil, get_var or index_table, vars) or str
148 | end
149 |
150 | ------------------------------------------------------------------------------
151 | -- Module Table
152 | ------------------------------------------------------------------------------
153 |
154 | return {
155 | DELIMITERS = DELIMITERS,
156 | camelize = camelize,
157 | capitalize = capitalize,
158 | classify = classify,
159 | crlf_to_lf = crlf_to_lf,
160 | dasherize = dasherize,
161 | decamelize = decamelize,
162 | escape_magic = escape_magic,
163 | expand = expand,
164 | from_glob = from_glob,
165 | lf_to_crlf = lf_to_crlf,
166 | lf_to_native = lf_to_native,
167 | native_to_lf = native_to_lf,
168 | split = split,
169 | to_bool = to_bool,
170 | to_list = to_list,
171 | }
172 |
--------------------------------------------------------------------------------
/lift/task.lua:
--------------------------------------------------------------------------------
1 | ------------------------------------------------------------------------------
2 | -- Task Engine
3 | ------------------------------------------------------------------------------
4 |
5 | local tostring, type = tostring, type
6 | local getmetatable, setmetatable = getmetatable, setmetatable
7 | local unpack = table.unpack or unpack -- LuaJIT compatibility
8 | local str_find, str_gmatch, str_match = string.find, string.gmatch, string.match
9 | local tbl_concat, tbl_sort = table.concat, table.sort
10 | local dbg_getlocal = debug.getlocal
11 |
12 | local inspect = require'lift.util'.inspect
13 | local diagnostics = require 'lift.diagnostics'
14 |
15 | local async = require 'lift.async'
16 | local async_get = async._get
17 | local try_wait, try_wait_all = async.try_wait, async.try_wait_all
18 |
19 | ------------------------------------------------------------------------------
20 | -- Graph construction and cycle detection
21 | ------------------------------------------------------------------------------
22 |
23 | local roots = {} -- list of root task futures (called from non-task threads)
24 |
25 | -- Adds an edge to the graph of task (future) calls.
26 | local function on_call(from_future, from_task, to_future, to_task)
27 | if not from_task then
28 | roots[#roots + 1] = to_future
29 | else
30 | local t = from_future.calls
31 | t[#t+1] = to_future
32 | end
33 | end
34 |
35 | -- Finds the first cycle in the graph of task futures.
36 | -- Returns nil if no cycle is found, or a circular path {a, ..., a} otherwise.
37 | local function visit(future, visited, dist)
38 | if visited[future] then -- found cycle
39 | visited[dist + 1] = future
40 | return visited
41 | end
42 | visited[future] = dist
43 | dist = dist + 1
44 | local calls = future.calls
45 | for i = 1, #calls do
46 | local cycle = visit(calls[i], visited, dist)
47 | if cycle then cycle[dist] = future return cycle end
48 | end
49 | visited[future] = nil
50 | end
51 | local function find_cycle()
52 | local visited = {}
53 | for i = 1, #roots do
54 | local cycle = visit(roots[i], visited, 0)
55 | if cycle then return cycle end
56 | end
57 | end
58 |
59 | ------------------------------------------------------------------------------
60 | -- Task (memoized async function with a single argument and multiple results)
61 | ------------------------------------------------------------------------------
62 |
63 | local Task = {
64 | name = '?', -- unique, fully qualified name
65 | }
66 |
67 | Task.__index = Task
68 |
69 | Task.__call = diagnostics.trace(
70 | '[task] running ${self} (${arg})',
71 | '[task] finished ${self} (${arg})',
72 | function(self, arg, extra)
73 | local future = self:async(arg, extra)
74 | local ok, res = try_wait(future)
75 | if not ok then
76 | res:report()
77 | end
78 | return unpack(res)
79 | end)
80 |
81 | function Task.__tostring(task)
82 | local prefix = tostring(task.ns)
83 | return prefix..(prefix == '' and '' or '.')..task.name
84 | end
85 |
86 | function Task:desc(desc)
87 | assert(type(desc) == 'string')
88 | self.description = desc
89 | end
90 |
91 | local function get_or_start(self, arg, extra)
92 | local futures = self.futures
93 | local future = futures[arg or 1]
94 | if future then return future end -- already started
95 | -- check if the task was called correctly
96 | local ns = self.ns
97 | if arg == ns then
98 | error('task must be called as .function() not :method()', 4)
99 | end
100 | if extra ~= nil then error('tasks can only take one argument', 4) end
101 | -- start the task
102 | future = async(self.f, arg)
103 | future.task = self
104 | future.calls = {} -- list of calls to other task futures
105 | futures[arg or 1] = future
106 | return future
107 | end
108 |
109 | local function format_cycle(d)
110 | local path = d.cycle
111 | local msg = tostring(path[1].task)
112 | for i = 2, #path do
113 | msg = msg .. ' -> ' .. tostring(path[i].task)
114 | end
115 | return msg
116 | end
117 |
118 | function Task:async(arg, extra)
119 | local future = get_or_start(self, arg, extra)
120 | local running_future = async_get()
121 | local running_task = running_future.task
122 | on_call(running_future, running_task, future, self)
123 | if future:is_running() then -- this is a cycle
124 | diagnostics.new{'fatal: cycle detected in tasks: ${format_cycle}',
125 | cycle = find_cycle(), format_cycle = format_cycle}:set_location(3):report()
126 | end
127 | return future
128 | end
129 |
130 | function Task:get_results(arg)
131 | local future = self.futures[arg or 1]
132 | return future and future.results
133 | end
134 |
135 | local function validate_name(name)
136 | if type(name) ~= 'string' or str_find(name, '^%a[_%w]*$') == nil then
137 | error('expected a task name, got '..inspect(name), 4)
138 | end
139 | end
140 |
141 | local function new_task(ns, name, f)
142 | validate_name(name)
143 | if type(f) ~= 'function' then
144 | error('expected a function, got '..inspect(f), 3)
145 | end
146 | local param = dbg_getlocal(f, 1)
147 | if param == 'self' then
148 | error('tasks must be declared as .functions() not :methods()', 3)
149 | end
150 | return setmetatable({ns = ns, name = name, f = f, futures = {false}}, Task)
151 | end
152 |
153 | ------------------------------------------------------------------------------
154 | -- TaskList object (callable list of tasks)
155 | ------------------------------------------------------------------------------
156 |
157 | local TaskList = {}
158 |
159 | TaskList.__call = diagnostics.trace(
160 | '[task] running ${self} (${arg})',
161 | '[task] finished ${self} (${arg})',
162 | function(self, arg, extra)
163 | local t = {}
164 | for i = 1, #self do
165 | t[i] = self[i]:async(arg, extra)
166 | end
167 | local ok, err = try_wait_all(t)
168 | if not ok then
169 | if #err.nested == 1 then err = err.nested[1] end
170 | err:report()
171 | end
172 | end)
173 |
174 | function TaskList:__tostring()
175 | local t = {}
176 | for i = 1, #self do
177 | t[i] = tostring(self[i])
178 | end
179 | tbl_sort(t)
180 | return 'task list {'..tbl_concat(t, ', ')..'}'
181 | end
182 |
183 | ------------------------------------------------------------------------------
184 | -- Namespace (has an unique name; contains tasks and methods)
185 | ------------------------------------------------------------------------------
186 |
187 | local Namespace = {
188 | name = '?', -- unique name within its parent
189 | tasks = nil, -- tasks map {name = task}
190 | parent = nil, -- namespace hierarchy
191 | nested = nil, -- nested namespaces map {name = child_namespace}
192 | }
193 |
194 | local function new_namespace(name, parent)
195 | return setmetatable({name = name, parent = parent, tasks = {}, nested = {}}, Namespace)
196 | end
197 |
198 | function Namespace.__index(t, k)
199 | return t.tasks[k] or Namespace[k] or t.nested[k]
200 | end
201 |
202 | function Namespace.__newindex(t, k, v)
203 | if getmetatable(v) ~= Task then
204 | v = new_task(t, k, v)
205 | end
206 | t.tasks[k] = v
207 | end
208 |
209 | function Namespace.__call(namespace, t)
210 | local tp = type(t)
211 | if tp ~= 'table' then error('expected a table, got '..tp, 2) end
212 | for i = 1, #t do
213 | if getmetatable(t[i]) ~= Task then
214 | error('element #'..i..' is not a Task, but a '..type(t[i]))
215 | end
216 | end
217 | return setmetatable(t, TaskList)
218 | end
219 |
220 | function Namespace.__tostring(ns)
221 | local parent = ns.parent
222 | if not parent then return ns.name end
223 | if not parent.parent then return ns.name end
224 | return tostring(parent)..'.'..ns.name
225 | end
226 |
227 | function Namespace:namespace(name)
228 | validate_name(name)
229 | local child = new_namespace(name, self)
230 | self.nested[name] = child
231 | return child
232 | end
233 |
234 | function Namespace:get_namespace(name)
235 | local g = self
236 | for s in str_gmatch(name, '([^.]+)%.?') do
237 | local child = g.nested[s]
238 | if not child then
239 | diagnostics.report("fatal: no such namespace '${1}.${2}'", g, s)
240 | end
241 | g = child
242 | end
243 | return g
244 | end
245 |
246 | function Namespace:get_task(name)
247 | local gn, tn = str_match(name, '^(.-)%.?([^.]+)$')
248 | local task = self:get_namespace(gn).tasks[tn]
249 | if not task then
250 | diagnostics.report("fatal: no such task '${1}'", name)
251 | end
252 | return task
253 | end
254 |
255 | function Namespace:reset(args)
256 | self.tasks = {}
257 | self.nested = {}
258 | end
259 |
260 | -- internal symbols:
261 | Namespace._Task = Task
262 | function Namespace._get_roots() return roots end
263 |
264 | -- return root task namespace
265 | return new_namespace('')
266 |
--------------------------------------------------------------------------------
/lift/template.lua:
--------------------------------------------------------------------------------
1 | ------------------------------------------------------------------------------
2 | -- Templating Engine
3 | ------------------------------------------------------------------------------
4 |
5 | local assert, load, tostring, type = assert, load, tostring, type
6 | local setmetatable = setmetatable
7 | local ssub, sfind, sformat = string.sub, string.find, string.format
8 | local tconcat = table.concat
9 |
10 | local fs = require 'lift.fs'
11 | local path = require 'lift.path'
12 |
13 | ------------------------------------------------------------------------------
14 | -- Rewrite a template as a Lua chunk with params: writer, context, indent
15 | ------------------------------------------------------------------------------
16 |
17 | local tags = {
18 | ['{:'] = ':}', -- expressions
19 | ['{%'] = '%}', -- statements (at line start supresses the preceding \n)
20 | ['{!'] = '!}', -- includes
21 | ['{?'] = '?}', -- comments (at line start supresses the preceding \n)
22 | }
23 |
24 | local rewriteTag = {
25 | ['{:'] = function(s) return ' _p(_tostr('..s..'))' end,
26 | ['{%'] = function(s) return s end,
27 | ['{!'] = function(s, ns)
28 | local name, ctx, sep = s, '_ctx', sfind(s, '!!', nil, true)
29 | if sep then name = ssub(s, 1, sep - 1) ; ctx = ssub(s, sep + 2) end
30 | return '_load('..name..', _name)(_p,'..ctx..', _ns+'..ns..')'..
31 | (ctx == '_ctx' and '' or '_ctx=context;') -- restore _ctx
32 | end,
33 | ['{?'] = function(s) return nil end,
34 | }
35 |
36 | local function rewrite_lines(c, ns, str, s, e)
37 | local before, after = ssub(str, s - 2, s), ssub(str, e, e + 2)
38 | if before == '%}\\' or before == '?}\\' then s = s + 2 end
39 | if after == '\n{%' or after == '\n{?' then e = e - 1 ; c[#c+1] = '\n' end
40 | while true do
41 | local i, j = sfind(str, '\n *', s)
42 | if not i or i >= e then -- last string
43 | if s <= e then
44 | c[#c + 1] = ' _p'
45 | c[#c + 1] = sformat('%q', ssub(str, s, e))
46 | end
47 | return ns
48 | else -- string + newline + spaces
49 | ns = j - i
50 | c[#c + 1] = ' _p'
51 | c[#c + 1] = sformat('%q', ssub(str, s, j))
52 | c[#c + 1] = ' _p(_id)'
53 | end
54 | s = j + 1
55 | end
56 | end
57 |
58 | local function rewrite(str, name)
59 | assert(str, 'missing template string')
60 | local c = {'local _name="', name or 'unnamed',
61 | '";local _p,context,_ns=...;_ctx=context or _env;',
62 | 'local _tostr,_ns=_tostr,_ns or 0;local _id=(" "):rep(_ns);'}
63 | local i, j, ns = 1, 1, 0 -- ns: num spaces after last \n, -1 after indenting
64 | while true do
65 | local s, e = sfind(str, '{', i, true)
66 | if not s then break end
67 | local ts = ssub(str, s, e + 1) -- tag start
68 | local te = tags[ts] -- tag end; nil if ts is invalid
69 | if te then
70 | local x, y = sfind(str, te, e + 2, true)
71 | if x then
72 | if j < s then ns = rewrite_lines(c, ns, str, j, s - 1) end
73 | c[#c + 1] = rewriteTag[ts](ssub(str, e + 2, x - 1), ns)
74 | j = y + 1; i = j
75 | else
76 | name = name or str
77 | error("missing "..te.." in template '"..name.."'", 2)
78 | end
79 | else
80 | i = s + 1
81 | end
82 | end
83 | rewrite_lines(c, ns, str, j, #str)
84 | return tconcat(c)
85 | end
86 |
87 | ------------------------------------------------------------------------------
88 | -- Compile a template string into a Lua function
89 | ------------------------------------------------------------------------------
90 |
91 | local function to_str(x)
92 | if x == nil then return '' end
93 | if type(x) == 'function' then return to_str(x()) end
94 | return tostring(x)
95 | end
96 |
97 | local env = setmetatable({
98 | _ctx = '', -- changed at the start of every template function call
99 | _env = { -- set via set_env()
100 | assert = assert,
101 | ipairs = ipairs,
102 | os = os,
103 | pairs = pairs,
104 | string = string,
105 | table = table,
106 | type = type,
107 | },
108 | _load = '', -- constant, set to function 'load' below
109 | _tostr = to_str, -- constant
110 | }, {
111 | __index = function(t, k) return t._ctx[k] or t._env[k] end,
112 | __newindex = function(t, k, v)
113 | error("cannot modify context."..tostring(k)..", please declare local", 2)
114 | end,
115 | })
116 |
117 | -- given a string, return a template function f(writer, context, indent)
118 | local setfenv = setfenv -- LuaJIT compatibility
119 | local function compile(str, name)
120 | local source = rewrite(str, name)
121 | if name then name = '@'..name end
122 | local f, err = load(source, name, 't', env)
123 | if err then error(err) end
124 | if setfenv then setfenv(f, env) end
125 | return f
126 | end
127 |
128 | local function set_env(new_env)
129 | env._env = new_env
130 | end
131 |
132 | ------------------------------------------------------------------------------
133 | -- Loading and caching of template files
134 | ------------------------------------------------------------------------------
135 |
136 | local cache = {} -- {abs_filename = function}
137 |
138 | -- resolve filename relative to abs_name or search ${load_path}
139 | local function resolve(rel_name, abs_name)
140 | if path.is_abs(rel_name) then return rel_name end
141 | if abs_name then -- if abs_name is given we never search ${load_path}
142 | return path.clean(path.dir(abs_name) .. '/' .. rel_name)
143 | end
144 | local filename = fs.glob('${load_path}/'..rel_name)()
145 | if not filename then
146 | error("cannot find template '"..rel_name.."'", 3)
147 | end
148 | return filename
149 | end
150 |
151 | -- Loads a function from a template file. If 'from' is given it should be
152 | -- an absolute filename relative to which 'name' is resolved.
153 | -- Otherwise search for 'name' in ${load_path}.
154 | local function load_template(name, from)
155 | name = resolve(name, from)
156 | local cached = cache[name]
157 | if cached then return cached end
158 | local file = assert(io.open(name))
159 | local str = file:read'*a'
160 | if str:sub(-1, -1) == '\n' then
161 | -- remove the last \n from files
162 | str = str:sub(1, -2)
163 | end
164 | file:close()
165 | local func = compile(str, name)
166 | cache[name] = func
167 | return func
168 | end
169 |
170 | env._load = load_template
171 |
172 | ------------------------------------------------------------------------------
173 | -- Module Table
174 | ------------------------------------------------------------------------------
175 |
176 | local M = {
177 | load = load_template,
178 | cache = cache,
179 | compile = compile,
180 | set_env = set_env,
181 | }
182 |
183 | return M
184 |
--------------------------------------------------------------------------------
/lift/util.lua:
--------------------------------------------------------------------------------
1 | ------------------------------------------------------------------------------
2 | -- Utility functions (mostly to support other Lift modules)
3 | ------------------------------------------------------------------------------
4 |
5 | local getmetatable, pairs, tostring, type = getmetatable, pairs, tostring, type
6 | local tbl_concat, tbl_sort = table.concat, table.sort
7 | local str_find, str_format = string.find, string.format
8 |
9 | ------------------------------------------------------------------------------
10 | -- OS-specific constants
11 | ------------------------------------------------------------------------------
12 |
13 | local DIR_SEP = package.config:sub(1, 1)
14 | assert(DIR_SEP == '/' or DIR_SEP == '\\')
15 | local UNIX = (DIR_SEP == '/') -- true on UNIX, false on Windows
16 | local WINDOWS = (DIR_SEP ~= '/') -- true on Windows, false on UNIX
17 |
18 | ------------------------------------------------------------------------------
19 | -- Table Key Sorting
20 | ------------------------------------------------------------------------------
21 |
22 | local function compare_as_string(a, b)
23 | return tostring(a) < tostring(b)
24 | end
25 |
26 | local type_order = {
27 | number = 1,
28 | string = 2,
29 | boolean = 3,
30 | ['function'] = 4,
31 | userdata = 5,
32 | thread = 6,
33 | table = 7
34 | }
35 |
36 | local function compare_by_type(a, b)
37 | local ta, tb = type(a), type(b)
38 | return ta == tb and a < b or type_order[ta] < type_order[tb]
39 | end
40 |
41 | -- Returns a list of the keys from table t sorted according to compare.
42 | local function keys_sorted(t, compare)
43 | local keys = {}
44 | for k in pairs(t) do
45 | keys[#keys+1] = k
46 | end
47 | tbl_sort(keys, compare)
48 | return keys
49 | end
50 |
51 | -- Returns a list of the keys from table t sorted by their string value.
52 | local function keys_sorted_as_string(t)
53 | return keys_sorted(t, compare_as_string)
54 | end
55 |
56 | -- Returns a list of the keys from table t sorted by type, then value.
57 | local function keys_sorted_by_type(t)
58 | return keys_sorted(t, compare_by_type)
59 | end
60 |
61 | ------------------------------------------------------------------------------
62 | -- Inspect (string representation of objects)
63 | ------------------------------------------------------------------------------
64 |
65 | -- Formats an elementary value.
66 | local function inspect_value(v, tp)
67 | if (tp or type(v)) == 'string' then
68 | return str_format('%q', v)
69 | else
70 | return tostring(v)
71 | end
72 | end
73 |
74 | -- Formats a value for indexing a table.
75 | local function inspect_key(v)
76 | local tp = type(v)
77 | if tp == 'string' and str_find(v, '^[%a_][%w_]*$') then
78 | return v, true
79 | end
80 | return '['..inspect_value(v, tp)..']'
81 | end
82 |
83 | -- Formats a flat list of values. Returns nil if the list contains a table,
84 | -- or if the resulting string would be longer than max_len (optional).
85 | local function inspect_flat_list(t, max_len)
86 | local str, sep = '', ''
87 | for i = 1, #t do
88 | local v = t[i]
89 | local tp = type(v)
90 | if tp == 'table' then return end -- not flat!
91 | str = str..sep..inspect_value(v, tp)
92 | if max_len and #str > max_len then return end -- too long
93 | sep = ', '
94 | end
95 | return str
96 | end
97 |
98 | -- Formats a flat table. Returns nil if t contains a table, or if the
99 | -- resulting string would be longer than max_len (optional).
100 | local function inspect_flat_table(t, max_len, keys)
101 | keys = keys or keys_sorted_by_type(t)
102 | local str, sep = '', ''
103 | for i = 1, #keys do
104 | local k = keys[i]
105 | local v = t[k]
106 | local tp = type(v)
107 | if tp == 'table' then return end -- oops, not flat!
108 | local vs = inspect_value(v, tp)
109 | if k == i then
110 | str = str..sep..vs
111 | else
112 | str = str..sep..inspect_key(k)..' = '..vs
113 | end
114 | if max_len and #str > max_len then return end -- too long
115 | sep = ', '
116 | end
117 | return str
118 | end
119 |
120 | -- Formats anything into a string buffer. Handles tables and cycles.
121 | local function sb_format(sb, name, t, indent, max_len)
122 | -- handle plain values
123 | local tp = type(t)
124 | if tp ~= 'table' then
125 | sb[#sb+1] = inspect_value(t, tp)
126 | return
127 | end
128 | -- solve cycles
129 | if sb[t] then
130 | sb[#sb+1] = sb[t]
131 | return
132 | end
133 | -- handle nested tables
134 | sb[t] = name
135 | sb[#sb+1] = '{'
136 | local keys = keys_sorted_by_type(t)
137 | if #keys > 0 then
138 | local ml = max_len - #indent
139 | local flat = (#keys == #t and
140 | inspect_flat_list(t, ml) or inspect_flat_table(t, ml, keys))
141 | if flat then
142 | sb[#sb+1] = flat
143 | else
144 | sb[#sb+1] = '\n'
145 | local new_indent = indent..' '
146 | for i = 1, #keys do
147 | local k = keys[i]
148 | local v = t[k]
149 | local fk, as_id = inspect_key(k)
150 | sb[#sb+1] = new_indent
151 | sb[#sb+1] = fk
152 | sb[#sb+1] = ' = '
153 | sb_format(sb, name..(as_id and '.'..fk or fk), v, new_indent, max_len)
154 | sb[#sb+1] = ',\n'
155 | end
156 | sb[#sb+1] = indent
157 | end
158 | end
159 | sb[#sb+1] = '}'
160 | end
161 |
162 | -- Formats anything into a string. Handles tables and cycles.
163 | -- Ignores `__tostring` metamethods and treats objects as regular tables.
164 | local function inspect_table(value, max_len)
165 | local sb = {}
166 | sb_format(sb, '@', value, '', max_len or 78)
167 | return tbl_concat(sb)
168 | end
169 |
170 | -- Formats anything into a string. Handles objects, tables and cycles.
171 | -- Uses metamethod `__tostring` to format objects that implement it.
172 | local function inspect(value, max_len)
173 | local mt = getmetatable(value)
174 | if mt and mt.__tostring then return tostring(value) end
175 | return inspect_table(value, max_len)
176 | end
177 |
178 | ------------------------------------------------------------------------------
179 | -- print(v) == print(inspect(v)) (use print{x, y, z} to print many values)
180 | ------------------------------------------------------------------------------
181 |
182 | local function _print(v)
183 | io.write(inspect(v), '\n')
184 | end
185 |
186 | ------------------------------------------------------------------------------
187 | -- Module Table
188 | ------------------------------------------------------------------------------
189 |
190 | return {
191 | _UNIX = UNIX, -- for internal use; prefer os.UNIX/WINDOWS
192 | _WINDOWS = WINDOWS, -- (solves circular dependency)
193 | inspect = inspect,
194 | inspect_flat_list = inspect_flat_list,
195 | inspect_flat_table = inspect_flat_table,
196 | inspect_key = inspect_key,
197 | inspect_table = inspect_table,
198 | inspect_value = inspect_value,
199 | keys_sorted = keys_sorted,
200 | keys_sorted_as_string = keys_sorted_as_string,
201 | keys_sorted_by_type = keys_sorted_by_type,
202 | print = _print,
203 | }
204 |
--------------------------------------------------------------------------------
/spec/Liftfile.lua:
--------------------------------------------------------------------------------
1 | -- The purpose of this file is to make lift detect this directory
2 | -- as the ${project_dir} in most tests.
3 |
--------------------------------------------------------------------------------
/spec/color_spec.lua:
--------------------------------------------------------------------------------
1 | describe('lift.color', function()
2 |
3 | local color = require 'lift.color'
4 | after_each(function() color.set_enabled(false) end)
5 |
6 | it('only encodes colors when enabled', function()
7 | local code = color.encode'reset;bright;red;onblack'
8 | assert.equal('\27[0;1;31;40m', code)
9 | assert.equal(nil, color.esc'red')
10 | assert.equal('', color.ESC'red')
11 | color.set_enabled(true)
12 | assert.equal(code, color.esc'reset;bright;red;onblack')
13 | assert.equal(code, color.ESC'reset;bright;red;onblack')
14 | end)
15 |
16 | it('returns nil in ESC() when disabled', function()
17 | local t = {color.esc'red;onblack'}
18 | t[#t + 1] = 'Red on black!'
19 | t[#t + 1] = color.esc'reset'
20 | assert.equal('Red on black!', table.concat(t))
21 | color.set_enabled(true)
22 | t = {color.esc'red;onblack'}
23 | t[#t + 1] = 'Red on black!'
24 | t[#t + 1] = color.esc'reset'
25 | assert.equal('\27[31;40mRed on black!\27[0m', table.concat(t))
26 | end)
27 |
28 | it('supports style tables', function()
29 | local t = {fg = 'red', bg = 'black', bold = true, dim = false, y = 'x'}
30 | assert.equal('', color.from_style(t))
31 | color.set_enabled(true)
32 | assert.equal(color.esc'reset;bold;red;onblack', color.from_style(t))
33 | end)
34 |
35 | end)
36 |
--------------------------------------------------------------------------------
/spec/config_spec.lua:
--------------------------------------------------------------------------------
1 | describe('lift.config', function()
2 |
3 | local config = require 'lift.config'
4 | local diagnostics = require 'lift.diagnostics'
5 |
6 | before_each(function()
7 | config.reset()
8 | config:new_parent('cli')
9 | diagnostics.Verifier.set_new()
10 | end)
11 |
12 | it('is itself the global scope', function()
13 | assert.is_string(config.LIFT_VERSION)
14 | end)
15 |
16 | describe('internal root scope', function()
17 | it('has immutable vars', function()
18 | config.set_const('MY_CONST', 42)
19 | assert.equal(42, config.MY_CONST)
20 | assert.error_match(function() config.MY_CONST = 1 end, 'cannot be changed')
21 | end)
22 |
23 | it('reads env vars as a fallback', function()
24 | assert.not_nil(config.PATH)
25 | assert.Nil(config.NOT_AN_ENV_VAR)
26 | end)
27 | end)
28 |
29 | describe('scopes', function()
30 |
31 | it('support nested scopes', function()
32 | assert.Nil(config.version)
33 | config.version = 'c1'
34 | assert.equal('c1', config.version)
35 |
36 | local s1 = config:new_child()
37 | assert.equal('c1', s1.version)
38 | s1.version = 's1'
39 | assert.equal('s1', s1.version)
40 | assert.equal('c1', config.version)
41 |
42 | local s2 = s1:new_child()
43 | assert.equal('s1', s2.version)
44 | s2.version = 's2'
45 | assert.equal('s2', s2.version)
46 | assert.equal('s1', s1.version)
47 | end)
48 |
49 | it("auto-convert vars to boolean with get_bool()", function()
50 | config.true_str = 'on'
51 | config.false_str = 'off'
52 | config.one_str = '1'
53 | config.zero_str = '0'
54 | config.zero_num = 0
55 | assert.Nil(config:get_bool'undefined')
56 | assert.True(config:get_bool'true_str')
57 | assert.False(config:get_bool'false_str')
58 | assert.True(config:get_bool'one_str')
59 | assert.False(config:get_bool'zero_str')
60 | assert.True(config:get_bool'zero_num')
61 | end)
62 |
63 | it('auto-convert vars to list with get_list()', function()
64 | config.foo = 3
65 | assert.equal(config:get_list('foo'), config.foo, {3})
66 | config.bar = 'a;b;c;'
67 | assert.equal(config:get_list('bar'), config.bar, {'a', 'b', 'c'})
68 | config.nop = {x = 3}
69 | assert.equal(config:get_list('nop'), config.nop, {x = 3})
70 | end)
71 |
72 | it('have insert() to insert into list vars', function()
73 | config.lst = 1
74 | assert.equal(1, config.lst)
75 | config:insert('lst', 2)
76 | assert.same({1, 2}, config.lst)
77 | config:insert('lst', 0, 1)
78 | assert.same({0, 1, 2}, config.lst)
79 | end)
80 |
81 | it('auto-convert vars to unique list with get_unique_list()', function()
82 | config.unique = 'a;c;b;c;b;d;'
83 | assert.same(config:get_unique_list('unique'), {'a', 'c', 'b', 'd'})
84 | end)
85 |
86 | it('have insert_unique() to insert into unique lists', function()
87 | config:insert_unique('unq', 2)
88 | config:insert_unique('unq', 5)
89 | config:insert_unique('unq', 2)
90 | assert.same({2, 5}, config.unq)
91 | config:insert_unique('unq', 5, 1) -- moves 5 to pos 1
92 | assert.same({5, 2}, config.unq)
93 | config:insert_unique('unq', 1, 2) -- inserts 1 at pos 2
94 | assert.same({5, 1, 2}, config.unq)
95 | config:insert_unique('unq', 5) -- does nothing
96 | assert.same({5, 1, 2}, config.unq)
97 | config:insert_unique('unq', 5, 3) -- moves 5 to pos 3
98 | assert.same({1, 2, 5}, config.unq)
99 | end)
100 |
101 | end)
102 |
103 | end)
104 |
105 |
--------------------------------------------------------------------------------
/spec/diagnostics_spec.lua:
--------------------------------------------------------------------------------
1 | describe('lift.diagnostics', function()
2 |
3 | local function dummy() end -- keep this at line 3...
4 |
5 | local diagnostics = require 'lift.diagnostics'
6 |
7 | describe('when creating a diagnostic object', function()
8 |
9 | it('formats messages by interpolating variables', function()
10 | local d = diagnostics.new('remark: lift is awesome!', 5, 7)
11 | assert.True(diagnostics.is_a(d))
12 | assert.False(diagnostics.is_a('string'))
13 | assert.equal('remark', d.kind, d.level)
14 | assert.equal('lift is awesome!', d.message)
15 | assert.equal(5, d[1]) assert.equal(7, d[2])
16 |
17 | assert.error(function() diagnostics.new() end,
18 | "first arg must be a message")
19 | assert.error(function() diagnostics.new('no kind') end,
20 | "malformed diagnostic message 'no kind'")
21 | assert.error(function() diagnostics.new('crazy: kind') end,
22 | "unknown diagnostic kind 'crazy'")
23 | end)
24 |
25 | it('implements lazy formatting of messages', function()
26 | local three = diagnostics.new('remark: 3')
27 | local d = diagnostics.new('warning: ${1} + ${3} is not ${2}',
28 | 1, '2', three.message)
29 | assert.equal('${1} + ${3} is not ${2}', d[0])
30 | assert.equal('1 + 3 is not 2', d.message)
31 | local remark = diagnostics.new('remark: Hey, ${1}!', d)
32 | assert.equal('Hey, warning: 1 + 3 is not 2!', remark.message)
33 | assert.equal('warning', d.level)
34 | assert.equal('remark', three.level, remark.level)
35 | end)
36 |
37 | it('supports table-based construction', function()
38 | local d = diagnostics.new({'remark: ${1}${2}${3} ${4}${5}',
39 | 1, '2', 3}, 4, 5)
40 | assert.equal('123 ${MISSING:4}${MISSING:5}', d.message)
41 | end)
42 |
43 | it("can aggregate multiple diagnostics into one", function()
44 | local d1 = diagnostics.new('remark: a ${kind}')
45 | local d2 = diagnostics.new('warning: a ${kind}')
46 | local d3 = diagnostics.new('error: an ${kind}'):set_location(dummy)
47 | local a1 = diagnostics.aggregate('warning: ${n} diagnostic${s}', {d2})
48 | assert.equal([[
49 | warning: 1 diagnostic
50 | (1) warning: a warning]], tostring(a1))
51 | local a2 = diagnostics.aggregate('error: ${n} diagnostic${s}',
52 | {d1, d2, d3})
53 | assert.equal([[
54 | error: 3 diagnostics
55 | (1) remark: a remark
56 | (2) warning: a warning
57 | (3) spec/diagnostics_spec.lua:3: error: an error]], tostring(a2))
58 | end)
59 | end)
60 |
61 | it('supports diagnostic consumers', function()
62 | diagnostics.set_consumer(nil)
63 | assert.error(function() diagnostics.new('error: oops'):report() end,
64 | 'undefined diagnostics consumer')
65 | assert.error(function() diagnostics.report('error: oops') end,
66 | 'undefined diagnostics consumer')
67 |
68 | local last
69 | diagnostics.set_consumer(function(d) last = d end)
70 | local d = diagnostics.new('warning: this works')
71 | assert.equal(nil, last) d:report() assert.equal(d, last)
72 | end)
73 |
74 | it('provides Verifier for error handling and testing', function()
75 | -- setting a new consumer resets the error count
76 | local verifier = diagnostics.Verifier.set_new()
77 | assert.no_error(function() diagnostics.check_error() end)
78 |
79 | -- at any time we can raise a fatal diagnostic
80 | local fatal = diagnostics.new('fatal: killer')
81 | assert.error(function() fatal:report() end, fatal)
82 | assert.error(function() diagnostics.report('fatal: brace yourselves') end,
83 | {kind = 'fatal', level = 'fatal', [0] = 'brace yourselves'})
84 |
85 | local ok, err = pcall(diagnostics.report,
86 | 'fatal: ${1} is coming', 'winter')
87 | assert.False(ok) assert.equal('fatal: winter is coming', tostring(err))
88 |
89 | -- check_error() raises the latest error diagnostic, if any
90 | assert.no_error(function() diagnostics.check_error() end)
91 | diagnostics.report('error: first')
92 | diagnostics.report('error: second')
93 | assert.error(function() diagnostics.check_error() end,
94 | {kind = 'error', level = 'error', [0] = 'second'})
95 |
96 | -- our verifier should have accumulated only the two errors
97 | assert.equal(2, #verifier)
98 | assert.equal('first', verifier[1].message)
99 | assert.equal('second', verifier[2].message)
100 |
101 | -- shorthand version of the above checks:
102 | assert.no_error(function() verifier:verify{'first', 'second'} end)
103 | assert.error_match(function() verifier:verify{'first'} end,
104 | 'expected 1 but got 2 diagnostics')
105 | assert.error_match(function() verifier:verify{'first', 'nop'} end,
106 | 'mismatch at diagnostic #2\nActual: error: second\nExpected: nop')
107 |
108 | -- verifier should receive all diagnostics except the 'ignored' ones
109 | diagnostics.report('ignored: not reported')
110 | assert.equal(2, #verifier)
111 | diagnostics.report('remark: reported')
112 | assert.equal(3, #verifier)
113 | end)
114 |
115 | it("provides wrap(f) to automatically report diagnostics", function()
116 | local f = diagnostics.trace('Pre!', 'Post!', function()
117 | diagnostics.new('error: zomg!')
118 | :source_location('dummy.lua', 'omg error', 5):report()
119 | end)
120 | local error_log = io.tmpfile()
121 | local original = diagnostics.set_tracing(true)
122 | diagnostics.set_stderr(error_log)
123 | diagnostics.wrap(function() f() end)
124 | diagnostics.set_stderr(io.stderr)
125 | diagnostics.set_tracing(original)
126 | error_log:seek('set')
127 | local out = error_log:read('*a')
128 | assert.match([[
129 | Pre!
130 | dummy.lua:1:5: error: zomg!
131 | Post! %[.*s%]
132 | Total time .*, memory .*]], out)
133 | end)
134 |
135 | end)
136 |
--------------------------------------------------------------------------------
/spec/files/init.lua:
--------------------------------------------------------------------------------
1 | local scope = ...
2 | scope.pi = 3.14
3 | scope.path = scope:get_list'PATH'
4 | scope:insert('list', 'd')
5 | scope:insert('list', 'A', 1)
6 |
--------------------------------------------------------------------------------
/spec/files/invalid/Liftfile.lua:
--------------------------------------------------------------------------------
1 | if end then error
2 |
--------------------------------------------------------------------------------
/spec/files/invalid/foo.lua:
--------------------------------------------------------------------------------
1 | if end then error
2 |
--------------------------------------------------------------------------------
/spec/files/invalid/foo/bar.lua:
--------------------------------------------------------------------------------
1 | if end then error
2 |
--------------------------------------------------------------------------------
/spec/files/invalid/foo/bar_abc.lua:
--------------------------------------------------------------------------------
1 | if end then error
2 |
--------------------------------------------------------------------------------
/spec/files/invalid/foo/barabc.lua:
--------------------------------------------------------------------------------
1 | if end then error
2 |
--------------------------------------------------------------------------------
/spec/files/invalid/foo/z:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tbastos/lift/454a6a06b696022dbdbd1f6d8e8a1789fc4d6a4f/spec/files/invalid/foo/z
--------------------------------------------------------------------------------
/spec/files/invalid/foo_bar.lua:
--------------------------------------------------------------------------------
1 | if end then error
2 |
--------------------------------------------------------------------------------
/spec/files/invalid/init.lua:
--------------------------------------------------------------------------------
1 | if end then error
2 |
--------------------------------------------------------------------------------
/spec/files/invalid/init_abc.lua:
--------------------------------------------------------------------------------
1 | if end then error
2 |
--------------------------------------------------------------------------------
/spec/files/invalid/init_abc_def.lua:
--------------------------------------------------------------------------------
1 | if end then error
2 |
--------------------------------------------------------------------------------
/spec/files/invalid/init_abcdef.lua:
--------------------------------------------------------------------------------
1 | if end then error
2 |
--------------------------------------------------------------------------------
/spec/files/invalid/initabc.lua:
--------------------------------------------------------------------------------
1 | if end then error
2 |
--------------------------------------------------------------------------------
/spec/files/project1/.lift/init.lua:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tbastos/lift/454a6a06b696022dbdbd1f6d8e8a1789fc4d6a4f/spec/files/project1/.lift/init.lua
--------------------------------------------------------------------------------
/spec/files/project2/Liftfile.lua:
--------------------------------------------------------------------------------
1 | local task = require 'lift.task'
2 |
3 | function task.passthrough(...)
4 | return ...
5 | end
6 |
7 | function task.default()
8 | return task.passthrough(42)
9 | end
10 |
--------------------------------------------------------------------------------
/spec/files/system/init.lua:
--------------------------------------------------------------------------------
1 | local scope = ...
2 | scope.list = 'a;b'
3 | scope.opt1 = 'system'
4 |
--------------------------------------------------------------------------------
/spec/files/templates/file.lua:
--------------------------------------------------------------------------------
1 | pi = {: tostring(math.pi):sub(1,6) :}
2 | {? Use templates to pretty print an acyclic table ?}
3 | {! 'table.lua' !! {t = {a = 1, b = true, c = {d = 'e'}}} !}
4 |
--------------------------------------------------------------------------------
/spec/files/templates/row.lua:
--------------------------------------------------------------------------------
1 | {:k:} = {% local tp = type(v) %}
2 | {% if tp == 'table' then %}{! 'table.lua' !! {t = v} !}
3 | {% elseif tp == 'string' then %}'{:v:}'
4 | {% elseif tp == 'number' then %}{:v:}
5 | {% elseif tp == 'boolean' then %}{: v and 'true' or 'false' :}
6 | {% else error('unsupported type') end %}
7 |
--------------------------------------------------------------------------------
/spec/files/templates/sub/invalid.lua:
--------------------------------------------------------------------------------
1 | This template file contains invalid Lua code
2 |
16 |
17 |
--------------------------------------------------------------------------------
/spec/files/templates/table.lua:
--------------------------------------------------------------------------------
1 | {
2 | {%
3 | -- traverse table in sorted order
4 | local keys = {}
5 | for k in pairs(t) do keys[#keys+1] = k end
6 | table.sort(keys)
7 | for i, k in ipairs(keys) do
8 | %}
9 | {! 'row.lua' !! {k = k, v = t[k]} !},
10 | {% end %}
11 | }
12 |
--------------------------------------------------------------------------------
/spec/files/user/init.lua:
--------------------------------------------------------------------------------
1 | local scope = ...
2 | scope:insert('list', 'c')
3 | scope.opt1 = 'user'
4 |
5 |
--------------------------------------------------------------------------------
/spec/fs_spec.lua:
--------------------------------------------------------------------------------
1 | describe('lift.fs', function()
2 |
3 | local fs = require 'lift.fs'
4 | local stream = require 'lift.stream'
5 | local su = require 'spec.util'
6 |
7 | it('offers is_dir() to test if a directory exists', function()
8 | assert.False(fs.is_dir'nothing', fs.is_dir'README.md')
9 | assert.True(fs.is_dir'spec')
10 | end)
11 |
12 | it('offers is_file() to test if a file exists', function()
13 | assert.False(fs.is_file'nothing', fs.is_file'spec')
14 | assert.True(fs.is_file'README.md')
15 | end)
16 |
17 | it('offers mkdir_all() to create dirs with missing parents', function()
18 | assert.no_error(function()
19 | assert(fs.mkdir_all'sub1/sub2')
20 | assert(fs.rmdir('sub1/sub2'))
21 | assert(fs.rmdir('sub1'))
22 | end)
23 | end)
24 |
25 | it('offers scandir() to iterate dir entries', function()
26 | local t = {}
27 | for name, et in fs.scandir('spec/files/templates') do
28 | if not name:find('^%.') then
29 | t[#t+1] = name
30 | end
31 | end
32 | assert.same({'file.lua', 'row.lua', 'sub', 'table.lua'}, t)
33 | end)
34 |
35 | describe('file globbing', function()
36 | local vars = {
37 | name = 'fname',
38 | path = {'/var', '/usr/local/var'},
39 | exts = {'png', 'jpg'},
40 | readme = 'README',
41 | list = {'valid/foo', 'README', 'spec'},
42 | }
43 |
44 | it("accepts **, *, ?, [cclass] and n-fold variable expansions", function()
45 | -- parsing of glob patterns
46 | assert.same({'*.lua'}, fs.glob_parse('*.lua', vars))
47 | assert.same({'*/fname.lua'}, fs.glob_parse('*/${name}.lua', vars))
48 | assert.same({'*/fname.', vars.exts},
49 | fs.glob_parse('*/${name}.${exts}', vars))
50 | -- set product of vars in glob patterns
51 | local list = {}
52 | local function collect(patt) list[#list+1] = patt end
53 | fs.glob_product(fs.glob_parse('*.lua', vars), collect)
54 | assert.same({'*.lua'}, list)
55 | list = {}
56 | fs.glob_product(fs.glob_parse('${name}.lua', vars), collect)
57 | assert.same({'fname.lua'}, list)
58 | list = {}
59 | fs.glob_product(fs.glob_parse('${exts}', vars), collect)
60 | assert.same(vars.exts, list)
61 | list = {}
62 | fs.glob_product(fs.glob_parse('${name}.${exts}', vars), collect)
63 | assert.same({'fname.png', 'fname.jpg'}, list)
64 | list = {}
65 | fs.glob_product(fs.glob_parse('${path}/${name}.${exts}', vars), collect)
66 | assert.same({'/var/fname.png', '/var/fname.jpg',
67 | '/usr/local/var/fname.png', '/usr/local/var/fname.jpg'}, list)
68 | end)
69 |
70 | it("can match a string against a glob pattern", function()
71 | assert.True(fs.match('/dir/file.ext', '/*/file.*'))
72 | assert.True(fs.match('/dir/file.ext', '/*/file?ext'))
73 | assert.False(fs.match('/dir/file.ext', '*/file.*'))
74 | assert.False(fs.match('/dir/file.ext', '*file*'))
75 | assert.True(fs.match('/x/y/z/file.jpg', '**/z/*.${exts}', vars))
76 | assert.True(fs.match('/z/file.jpg', '**/z/*.${exts}', vars))
77 | assert.False(fs.match('file.jpeg', '*.${exts}', vars))
78 | end)
79 |
80 | it("can find files using wildcards", function()
81 | local it = fs.glob('*.md') ; assert.is_function(it)
82 | local filename = it() ; assert.is_string(filename)
83 | assert.match('/CONTRIBUTING%.md$', filename)
84 | assert.match('/README%.md$', it())
85 | assert.is_nil(it())
86 | assert.error(function() it() end, "cannot resume dead coroutine")
87 | assert.is_nil(fs.glob('/invalid/*.md')())
88 | assert.match('/README.md$', fs.glob('./REA*.??')())
89 | assert.match('/spec/files/user/init.lua$', fs.glob('spec/*/user/ini*')())
90 | assert.match('/spec/files/init.lua$', fs.glob('spec/*/init.lua')())
91 | assert.match('/spec/files/invalid/foo/z$', fs.glob('**/z')())
92 | assert.error(function() fs.glob('**')() end,
93 | "expected a name or pattern after wildcard '**'")
94 | end)
95 |
96 | it("wildcards **/ and /*/ ignore dot files by default", function()
97 | if fs.is_dir('.git') then
98 | assert.is_nil(fs.glob('**/HEAD')())
99 | assert.is_nil(fs.glob('*/HEAD')())
100 | assert.is_nil(fs.glob('./*/HEAD')())
101 | assert.is_string(fs.glob('.*/HEAD')())
102 | assert.is_string(fs.glob('./.*git/HEAD')())
103 | else
104 | pending("skipped some tests because lift/.git doesn't exist")
105 | end
106 | end)
107 |
108 | it('supports configurable ${var} and ${list} expansions', function()
109 | local it = fs.glob('./${readme}.md', vars)
110 | assert.match('/README%.md$', it())
111 | assert.is_nil(it())
112 |
113 | assert.error(function() fs.glob('${invalid}', vars) end,
114 | 'no such variable ${invalid}')
115 |
116 | it = fs.glob('${list}.md', vars)
117 | assert.match('/README%.md$', it())
118 | assert.is_nil(it())
119 |
120 | it = fs.glob('spec/*/in${list}.lua', vars)
121 | assert.match('/spec/files/invalid/foo%.lua$', it())
122 | assert.is_nil(it())
123 | end)
124 |
125 | it('expands config variables by default', function()
126 | assert.is_string(fs.glob('${PATH}/lua*')())
127 | end)
128 |
129 | end)
130 |
131 | describe("convenience functions", function()
132 | it("offers read_file()/write_file() to read/write a whole file", function()
133 | local str = "One\nTwo\nThree\n"
134 | fs.write_file('temp_fs_rw_file', str)
135 | assert.equal(str, fs.read_file('temp_fs_rw_file'))
136 | fs.unlink('temp_fs_rw_file')
137 | end)
138 | end)
139 |
140 | describe("readable file stream", function()
141 |
142 | local LICENSE = fs.read_file('LICENSE')
143 | assert.is_string(LICENSE)
144 | assert.True(#LICENSE > 100 and #LICENSE < 8000)
145 |
146 | it("can read from a file (in one chunk)", su.async(function()
147 | local out = {}
148 | local to_out = stream.to_array(out)
149 | fs.read_from('LICENSE'):pipe(to_out):wait_finish()
150 | assert.same({LICENSE}, out)
151 | end))
152 |
153 | it("can read from a file (in many chunks)", su.async(function()
154 | local out = {}
155 | local to_out = stream.to_array(out, 20) -- with 50ms delay
156 | to_out.high_water = 3 -- forces readable to buffer
157 | local readable = fs.read_from('LICENSE', 100)
158 | readable.high_water = 3 -- forces reader to pause
159 | readable:pipe(to_out):wait_finish()
160 | assert.True(#out > 10) -- out should contain 11 chunks
161 | assert.equal(LICENSE, table.concat(out))
162 | end))
163 |
164 | it("push error if trying to read from inaccessible file", su.async(function()
165 | local out = {}
166 | local to_out = stream.to_array(out)
167 | local readable = fs.read_from('non_existing')
168 | assert.falsy(readable.read_error)
169 | assert.falsy(to_out.write_error)
170 | readable:pipe(to_out):wait_finish()
171 | assert.truthy(readable.read_error)
172 | assert.truthy(to_out.write_error)
173 | assert.equal('ENOENT: no such file or directory: non_existing',
174 | to_out.write_error.uv_err)
175 | end))
176 | end)
177 |
178 | describe("writable file stream", function()
179 | it("can write a string to a file", su.async(function()
180 | local sb = {'Hello world!\n'}
181 | local from_sb = stream.from_array(sb)
182 | from_sb:pipe(fs.write_to('tmp_hello')):wait_finish()
183 | assert.equal('Hello world!\n', fs.read_file('tmp_hello'))
184 | fs.unlink('tmp_hello')
185 | end))
186 |
187 | it("can write a string buffer to a file", su.async(function()
188 | local sb = {'Hello ', 'world!', '\nFrom string buffer\n'}
189 | local from_sb = stream.from_array(sb)
190 | from_sb:pipe(fs.write_to('tmp_hello')):wait_finish()
191 | assert.equal('Hello world!\nFrom string buffer\n', fs.read_file('tmp_hello'))
192 | fs.unlink('tmp_hello')
193 | end))
194 |
195 | it("can write a copy of a readable file", su.async(function()
196 | local path = 'LICENSE'
197 | fs.read_from(path):pipe(fs.write_to('tmp_copy')):wait_finish()
198 | assert.equal(fs.read_file(path), fs.read_file('tmp_copy'))
199 | fs.unlink('tmp_copy')
200 | end))
201 | end)
202 |
203 | end)
204 |
--------------------------------------------------------------------------------
/spec/loader_spec.lua:
--------------------------------------------------------------------------------
1 | describe("lift.loader", function()
2 |
3 | local fs = require 'lift.fs'
4 | local path = require 'lift.path'
5 | local loader = require 'lift.loader'
6 | local config = require 'lift.config'
7 | local diagnostics = require 'lift.diagnostics'
8 |
9 | after_each(function()
10 | diagnostics.Verifier.set_new()
11 | config.reset()
12 | config:new_parent('cli')
13 | config.load_path = config.LIFT_SRC_DIR..'/files'
14 | end)
15 |
16 | local function count(iterator)
17 | local n = 0
18 | while iterator() do n = n + 1 end
19 | return n
20 | end
21 |
22 | it("offers find_scripts() to find Lua files in the ${load_path}", function()
23 | local prev_lp = config.load_path
24 | config.load_path = 'spec/files/invalid'
25 | finally(function() config.load_path = prev_lp end)
26 | assert.equal(4, count(loader.find_scripts('init')))
27 | assert.equal(1, count(loader.find_scripts('initabc')))
28 | assert.equal(2, count(loader.find_scripts('init', 'abc')))
29 | assert.equal(0, count(loader.find_scripts('init', 'none')))
30 | assert.equal(5, count(loader.find_scripts('foo')))
31 | assert.equal(3, count(loader.find_scripts('foo', 'bar')))
32 | assert.equal(1, count(loader.find_scripts('foo', 'barabc')))
33 | end)
34 |
35 | describe("init()", function()
36 | local spec_dir = path.clean(config.LIFT_SRC_DIR..'/../spec')
37 |
38 | -- change the CWD during a call to f
39 | local function init_in_dir(dir)
40 | local cwd = fs.cwd()
41 | assert(fs.chdir(dir))
42 | local ok, err = pcall(loader.init, loader)
43 | assert(fs.chdir(cwd))
44 | if not ok then error(err, 0) end
45 | end
46 |
47 | it('runs init scripts ir the order listed in the ${load_path}', function()
48 | assert.Nil(config.pi)
49 | config.load_path = 'files'
50 | config.user_config_dir = 'files/user'
51 | config.system_config_dir = 'files/system'
52 | init_in_dir(spec_dir..'/files/templates')
53 | assert.equal(spec_dir, config.project_dir)
54 | assert.equal(spec_dir..'/Liftfile.lua', config.project_file)
55 | assert.equal(config.APP_VERSION, config.LIFT_VERSION)
56 | assert.equal(3.14, config.pi)
57 | assert.equal('user', config.opt1)
58 | assert.same({'A','a','b','c','d'}, config.list)
59 |
60 | config.load_path = 'files;files/invalid'
61 | assert.error_match(function()
62 | init_in_dir(spec_dir..'/files/templates')
63 | end, 'unexpected symbol')
64 | end)
65 |
66 | it("detects project_dir based on presence of Liftfile.lua", function()
67 | assert.Nil(config.project_dir)
68 | assert.error_match(function() init_in_dir(spec_dir..'/files/invalid/foo') end,
69 | "Liftfile.lua:1: lua_syntax_error: unexpected symbol")
70 | assert.matches('spec/files/invalid', config.project_dir)
71 | assert.matches('spec/files/invalid/Liftfile.lua', config.project_file)
72 | end)
73 |
74 | it("detects project_dir based on presence of .lift dir", function()
75 | assert.Nil(config.project_dir)
76 | init_in_dir(spec_dir..'/files/project1')
77 | assert.matches('files/project1', config.project_dir)
78 | assert.Nil(config.project_file)
79 | end)
80 |
81 | end)
82 |
83 | end)
84 |
--------------------------------------------------------------------------------
/spec/os_spec.lua:
--------------------------------------------------------------------------------
1 | describe('lift.os', function()
2 |
3 | local WAIT = 100 -- how much to wait for a process to finish
4 | if os.getenv('CI') then
5 | WAIT = 300
6 | end
7 |
8 | local os = require 'lift.os'
9 | local ls = require 'lift.string'
10 | local async = require 'lift.async'
11 | local config = require 'lift.config'
12 | local su = require 'spec.util'
13 |
14 | it('offers try_sh() to try to execute a shell command', su.async(function()
15 | local out, err = assert(os.sh'echo Hello world!|cat')
16 | assert.equal('Hello world!\n', out)
17 | assert.equal('', err)
18 |
19 | out, err = assert(os.sh[[lua -e "io.write'Hello from stdout'"]])
20 | assert.equal('Hello from stdout', out)
21 | assert.equal('', err)
22 |
23 | out, err = assert(os.sh[[lua -e "io.stderr:write'Hello from stderr'"]])
24 | assert.equal('', out)
25 | assert.equal('Hello from stderr', err)
26 |
27 | out, err = os.try_sh'invalid_cmd error'
28 | assert.Nil(out)
29 | assert.match('shell command failed', err)
30 | end))
31 |
32 | it("offers sh() to execute a shell command or raise an error", su.async(function()
33 | os.sh'echo Hello'
34 | assert.error_match(function() os.sh'invalid_cmd error' end,
35 | 'shell command failed with status')
36 | end))
37 |
38 | describe("child processes", function()
39 |
40 | it("can be started with spawn()", su.async(function()
41 | local c = assert(os.spawn{file = config.LUA_EXE_PATH, '-e', 'os.exit(7)',
42 | stdin = 'ignore', stdout = 'ignore', stderr = 'ignore'})
43 | assert.is_number(c.pid)
44 | assert.is_nil(c.status)
45 | assert.is_nil(c.signal)
46 | async.sleep(WAIT)
47 | assert.equal(7, c.status)
48 | assert.equal(0, c.signal)
49 | end))
50 |
51 | it("can be terminated with :kill()", su.async(function()
52 | local c = assert(os.spawn{file = 'sleep', '3',
53 | stdin = 'ignore', stdout = 'ignore', stderr = 'ignore'})
54 | assert.is_number(c.pid)
55 | assert.is_nil(c.status)
56 | assert.is_nil(c.signal)
57 | c:kill()
58 | async.sleep(WAIT)
59 | assert.equal(15, c.signal) -- sigterm
60 | assert.error(function() c:kill() end,
61 | 'process:kill() called after process termination')
62 | end))
63 |
64 | it("can inherit fds from parent and be waited for", su.async(function()
65 | local c = assert(os.spawn{file = config.LUA_EXE_PATH,
66 | '-e', 'print[[Hello from child process]]',
67 | stdin = 'ignore', stdout = 'inherit', stderr = 'inherit'})
68 | assert.Nil(c.status)
69 | c:wait()
70 | assert.equal(0, c.status, c.signal)
71 | end))
72 |
73 | it("can be waited for with a timeout", su.async(function()
74 | -- with enough time
75 | local c = assert(os.spawn{file = config.LUA_EXE_PATH,
76 | '-e', 'print[[this is fast]]',
77 | stdin = 'ignore', stdout = 'ignore', stderr = 'ignore'})
78 | assert.Nil(c.status)
79 | local status, signal = c:wait(300)
80 | assert.equal(0, status, signal)
81 | -- without enough time
82 | c = assert(os.spawn{file = 'sleep', '5',
83 | stdin = 'ignore', stdout = 'ignore', stderr = 'ignore'})
84 | assert.Nil(c.status)
85 | status, signal = c:wait(300)
86 | c:kill()
87 | assert.False(status)
88 | assert.equal(signal, 'timed out')
89 | end))
90 |
91 | it("can be read from (stdout, stderr)", su.async(function()
92 | local c = assert(os.spawn{file = config.LUA_EXE_PATH,
93 | '-e', 'print[[Hello world]]', stdin = 'ignore'})
94 | assert.Nil(c:try_read())
95 | assert.Nil(c.stderr:try_read())
96 | c:wait() -- await exit before reading
97 | assert.equal('Hello world\n', ls.native_to_lf(c:try_read()))
98 | assert.Nil(c.stderr:try_read())
99 | end))
100 |
101 | it("can be written to (stdin) and read from (stdout)", su.async(function()
102 | local c = assert(os.spawn{file = 'cat'})
103 | c:write('One')
104 | c.stdin:write('Two')
105 | c:write() -- shuts down stdin, causing 'cat' to exit
106 | assert.Nil(c.stdout:try_read())
107 | assert.Nil(c.stderr:try_read())
108 | c:wait() -- await exit before reading
109 | local sb = {c:try_read()}
110 | sb[#sb+1] = c:try_read()
111 | assert.equal('OneTwo', table.concat(sb))
112 | assert.Nil(c.stderr:try_read())
113 | end))
114 |
115 | it("can be written to (stdin) and read from (stderr)", su.async(function()
116 | local c = assert(os.spawn{file = 'lua',
117 | '-e', 'io.stderr:write(io.read())', stdout = 'ignore'})
118 | c:write('Hello from stderr')
119 | c:write() -- shuts down stdin, causing the process to exit
120 | assert.Nil(c.stdout)
121 | assert.Nil(c.stderr:try_read())
122 | c:wait() -- await exit before reading
123 | assert.equal('Hello from stderr', c.stderr:try_read())
124 | end))
125 |
126 | it("can be written to (stdin) and read from (stdout) synchronously", su.async(function()
127 | local c = assert(os.spawn{file = 'cat'})
128 | c:write('One')
129 | c.stdin:write('Two')
130 | assert.equal('OneTwo', c:read())
131 | c:write('Three')
132 | assert.equal('Three', c:read())
133 | c:write()
134 | assert.Nil(c:read())
135 | assert.Nil(c.stderr:read())
136 | end))
137 |
138 | it("can be piped to another process", su.async(function()
139 | local echo1 = assert(os.spawn{file = config.LUA_EXE_PATH,
140 | '-e', 'io.write[[OneTwoThree]]', stdin = 'ignore'})
141 | local echo2 = assert(os.spawn{file = config.LUA_EXE_PATH,
142 | '-e', 'io.write[[FourFive]]', stdin = 'ignore'})
143 | local cat1 = assert(os.spawn{file = 'cat'})
144 | local cat2 = assert(os.spawn{file = 'cat'})
145 | cat1:pipe(cat2)
146 | echo1:pipe(cat1, true) -- pipe to cat1 and keep cat1 open
147 | echo1.stdout:wait_end()
148 | echo2:pipe(cat1) -- pipe to cat1 and shut down cat1
149 | echo2.stdout:wait_end()
150 | local sb = {cat2:read()}
151 | sb[#sb+1] = cat2:read()
152 | assert.equal('OneTwoThreeFourFive', table.concat(sb))
153 | end))
154 |
155 | end)
156 |
157 | end)
158 |
--------------------------------------------------------------------------------
/spec/path_spec.lua:
--------------------------------------------------------------------------------
1 | describe('lift.path', function()
2 |
3 | local path = require 'lift.path'
4 | local fs = require 'lift.fs'
5 |
6 | it('offers is_root() to test if path is lexically a root dir', function()
7 | assert.False(path.is_root'/foo', path.is_root'C:/foo', path.is_root'C:')
8 | assert.True(path.is_root'/', path.is_root'C:/')
9 | end)
10 |
11 | it('offers volume() to get the volume name from Windows paths', function()
12 | assert.equal('', path.volume'/foo/file.ext')
13 | assert.equal('C:', path.volume'C:/foo')
14 | assert.equal('X:', path.volume'X:/foo/file.ext')
15 | end)
16 |
17 | it('offers base() to get the last element of a path', function()
18 | assert.equal('file.ext', path.base'/foo/dir/file.ext')
19 | assert.equal('file.ext', path.base'C:/foo/dir/file.ext')
20 | assert.equal('dir', path.base'/foo/dir/')
21 | assert.equal('/', path.base'/')
22 | assert.equal('.', path.base'')
23 | end)
24 |
25 | it('offers dir() to get the directory of a path', function()
26 | assert.equal('/foo/dir', path.dir'/foo/dir/file.ext')
27 | assert.equal('C:/foo/dir', path.dir'C:/foo/dir/file.ext')
28 | assert.equal('/foo/dir', path.dir'/foo/dir/')
29 | assert.equal('/', path.dir'/')
30 | assert.equal('C:/', path.dir'C:/', path.dir'C:/foo')
31 | assert.equal('.', path.dir'', path.dir'C:', path.dir'file.ext')
32 | end)
33 |
34 | it('offers ext() to get the filename extension of a path', function()
35 | assert.equal('ext', path.ext'/dir/file.ext', path.ext'C:/dir/file.x.ext')
36 | assert.equal('', path.ext'/dir/file', path.ext'/dir/file.')
37 | end)
38 |
39 | it('offers clean() to normalize a path', function()
40 | assert.equal('..', path.clean'..')
41 | assert.equal('/', path.clean'/', path.clean'/..', path.clean'/../')
42 | assert.equal('C:/', path.clean'C:/', path.clean'C:/..')
43 | assert.equal('.', path.clean'', path.clean'.', path.clean'./')
44 | assert.equal('/foo/dir', path.clean'/foo/dir/')
45 | assert.equal('/foo/dir', path.clean'/foo/dir')
46 | assert.equal('/foo/dir', path.clean('/foo/dir', true))
47 | assert.equal('/foo/dir/', path.clean('/foo/dir/', true))
48 | assert.equal('/foo/dir/file.ext', path.clean'/foo/dir/file.ext')
49 | end)
50 |
51 | it('offers is_abs() to test if a path is absolute', function()
52 | assert.True(path.is_abs'/foo/file.ext')
53 | assert.True(path.is_abs'C:/foo/file.ext')
54 | assert.False(path.is_abs'./foo/dir/')
55 | assert.False(path.is_abs'file.ext')
56 | end)
57 |
58 | it('offers abs() to make a path absolute', function()
59 | assert.equal('/foo/file.ext', path.abs'/foo/file.ext')
60 | assert.equal('/foo/dir/', path.abs'/foo/dir/')
61 | assert.equal('/', path.abs'/')
62 | assert.equal(fs.cwd(), path.abs'')
63 | assert.equal(fs.cwd()..'/file', path.abs'file')
64 | assert.equal('/usr/local/*/file', path.abs('../*/file', '/usr/local/bin'))
65 | assert.equal('C:/usr/local/file', path.abs('../file', 'C:/usr/local/bin'))
66 | assert.equal('/usr/dir', path.abs('../dir', '/usr/local', true))
67 | assert.equal('/usr/dir/', path.abs('../dir/', '/usr/local', true))
68 | end)
69 |
70 | it('offers rel() to make a path relative to some other path', function()
71 | assert.equal('b/c', path.rel('/a', '/a/b/c'))
72 | assert.equal('b/c', path.rel('C:/a', 'C:/a/b/c'))
73 | assert.equal('../b/c', path.rel('/a', '/b/c'))
74 | assert.equal('../b/c', path.rel('C:/a', 'C:/b/c'))
75 | assert.equal('c', path.rel('a/b', 'a/b/c'))
76 | assert.equal('c', path.rel('./a/b', './a/b/c'))
77 | assert.equal('..', path.rel('./a/b/c', './a/b/'))
78 | assert.error(function() path.rel('/a', './b/c') end,
79 | "expected two relative paths or two absolute paths")
80 | assert.equal('X:/b/c', path.rel('C:/a', 'X:/b/c'))
81 | end)
82 |
83 | it('offers join() to join path elements', function()
84 | assert.equal('/usr/local', path.join('/usr', '', '', 'local'))
85 | assert.equal('/usr/local/bin', path.join('/./usr/', 'local', 'bin/'))
86 | end)
87 |
88 | it('offers split() to get the dir and file components of a path', function()
89 | assert.same({'/usr/local/',''}, {path.split('/usr/local/')})
90 | assert.same({'C:/usr/local/',''}, {path.split('C:/usr/local/')})
91 | assert.same({'/usr/local/','bin'}, {path.split('/usr/local/bin')})
92 | assert.same({'C:/usr/local/','bin'}, {path.split('C:/usr/local/bin')})
93 | assert.same({'C:/','usr'}, {path.split('C:/usr')})
94 | assert.same({'C:/',''}, {path.split('C:/')})
95 | assert.same({'','file.ext'}, {path.split('file.ext')})
96 | end)
97 |
98 | end)
99 |
--------------------------------------------------------------------------------
/spec/request_spec.lua:
--------------------------------------------------------------------------------
1 | describe('lift.request', function()
2 |
3 | local req = require 'lift.request'
4 | local stream = require 'lift.stream'
5 | local su = require 'spec.util'
6 |
7 | it("can fetch an HTML page", su.async(function()
8 | local sb = {} -- string buffer containing the page
9 | req('www.google.com/invalid_url'):pipe(stream.to_array(sb)):wait_finish()
10 | assert.match('404 Not Found', table.concat(sb))
11 | end))
12 |
13 | local function try_get(url)
14 | return function()
15 | local rs = req(url)
16 | repeat local data = rs:read() until data == nil
17 | assert(not rs.read_error)
18 | end
19 | end
20 |
21 | it("pushes errors onto the stream", su.async(function()
22 | assert.no_error(try_get('www.google.com/invalid_url'))
23 | assert.error_matches(try_get('-s www.google.com'), 'malformed URL')
24 | assert.error_matches(try_get('invalid.url'), 't resolve host')
25 | assert.error_matches(try_get('weird://protocol.com'),
26 | 'Protocol .- not supported')
27 | end))
28 |
29 | it("can fetch a PNG image", su.async(function()
30 | local sb = {} -- string buffer containing the page
31 | req('www.google.com'):pipe(stream.to_array(sb)):wait_finish()
32 | local html = table.concat(sb)
33 | assert.equal('