fs.lua 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683
  1. --- @brief <pre>help
  2. --- *vim.fs.exists()*
  3. --- Use |uv.fs_stat()| to check a file's type, and whether it exists.
  4. ---
  5. --- Example:
  6. ---
  7. --- >lua
  8. --- if vim.uv.fs_stat(file) then
  9. --- vim.print("file exists")
  10. --- end
  11. --- <
  12. local uv = vim.uv
  13. local M = {}
  14. -- Can't use `has('win32')` because the `nvim -ll` test runner doesn't support `vim.fn` yet.
  15. local sysname = uv.os_uname().sysname:lower()
  16. local iswin = not not (sysname:find('windows') or sysname:find('mingw'))
  17. local os_sep = iswin and '\\' or '/'
  18. --- Iterate over all the parents of the given path.
  19. ---
  20. --- Example:
  21. ---
  22. --- ```lua
  23. --- local root_dir
  24. --- for dir in vim.fs.parents(vim.api.nvim_buf_get_name(0)) do
  25. --- if vim.fn.isdirectory(dir .. "/.git") == 1 then
  26. --- root_dir = dir
  27. --- break
  28. --- end
  29. --- end
  30. ---
  31. --- if root_dir then
  32. --- print("Found git repository at", root_dir)
  33. --- end
  34. --- ```
  35. ---
  36. ---@since 10
  37. ---@param start (string) Initial path.
  38. ---@return fun(_, dir: string): string? # Iterator
  39. ---@return nil
  40. ---@return string|nil
  41. function M.parents(start)
  42. return function(_, dir)
  43. local parent = M.dirname(dir)
  44. if parent == dir then
  45. return nil
  46. end
  47. return parent
  48. end,
  49. nil,
  50. start
  51. end
  52. --- Return the parent directory of the given path
  53. ---
  54. ---@since 10
  55. ---@generic T : string|nil
  56. ---@param file T Path
  57. ---@return T Parent directory of {file}
  58. function M.dirname(file)
  59. if file == nil then
  60. return nil
  61. end
  62. vim.validate('file', file, 'string')
  63. if iswin then
  64. file = file:gsub(os_sep, '/') --[[@as string]]
  65. if file:match('^%w:/?$') then
  66. return file
  67. end
  68. end
  69. if not file:match('/') then
  70. return '.'
  71. elseif file == '/' or file:match('^/[^/]+$') then
  72. return '/'
  73. end
  74. ---@type string
  75. local dir = file:match('/$') and file:sub(1, #file - 1) or file:match('^(/?.+)/')
  76. if iswin and dir:match('^%w:$') then
  77. return dir .. '/'
  78. end
  79. return dir
  80. end
  81. --- Return the basename of the given path
  82. ---
  83. ---@since 10
  84. ---@generic T : string|nil
  85. ---@param file T Path
  86. ---@return T Basename of {file}
  87. function M.basename(file)
  88. if file == nil then
  89. return nil
  90. end
  91. vim.validate('file', file, 'string')
  92. if iswin then
  93. file = file:gsub(os_sep, '/') --[[@as string]]
  94. if file:match('^%w:/?$') then
  95. return ''
  96. end
  97. end
  98. return file:match('/$') and '' or (file:match('[^/]*$'))
  99. end
  100. --- Concatenate directories and/or file paths into a single path with normalization
  101. --- (e.g., `"foo/"` and `"bar"` get joined to `"foo/bar"`)
  102. ---
  103. ---@since 12
  104. ---@param ... string
  105. ---@return string
  106. function M.joinpath(...)
  107. return (table.concat({ ... }, '/'):gsub('//+', '/'))
  108. end
  109. ---@alias Iterator fun(): string?, string?
  110. --- Return an iterator over the items located in {path}
  111. ---
  112. ---@since 10
  113. ---@param path (string) An absolute or relative path to the directory to iterate
  114. --- over. The path is first normalized |vim.fs.normalize()|.
  115. --- @param opts table|nil Optional keyword arguments:
  116. --- - depth: integer|nil How deep the traverse (default 1)
  117. --- - skip: (fun(dir_name: string): boolean)|nil Predicate
  118. --- to control traversal. Return false to stop searching the current directory.
  119. --- Only useful when depth > 1
  120. ---
  121. ---@return Iterator over items in {path}. Each iteration yields two values: "name" and "type".
  122. --- "name" is the basename of the item relative to {path}.
  123. --- "type" is one of the following:
  124. --- "file", "directory", "link", "fifo", "socket", "char", "block", "unknown".
  125. function M.dir(path, opts)
  126. opts = opts or {}
  127. vim.validate('path', path, 'string')
  128. vim.validate('depth', opts.depth, 'number', true)
  129. vim.validate('skip', opts.skip, 'function', true)
  130. path = M.normalize(path)
  131. if not opts.depth or opts.depth == 1 then
  132. local fs = uv.fs_scandir(path)
  133. return function()
  134. if not fs then
  135. return
  136. end
  137. return uv.fs_scandir_next(fs)
  138. end
  139. end
  140. --- @async
  141. return coroutine.wrap(function()
  142. local dirs = { { path, 1 } }
  143. while #dirs > 0 do
  144. --- @type string, integer
  145. local dir0, level = unpack(table.remove(dirs, 1))
  146. local dir = level == 1 and dir0 or M.joinpath(path, dir0)
  147. local fs = uv.fs_scandir(dir)
  148. while fs do
  149. local name, t = uv.fs_scandir_next(fs)
  150. if not name then
  151. break
  152. end
  153. local f = level == 1 and name or M.joinpath(dir0, name)
  154. coroutine.yield(f, t)
  155. if
  156. opts.depth
  157. and level < opts.depth
  158. and t == 'directory'
  159. and (not opts.skip or opts.skip(f) ~= false)
  160. then
  161. dirs[#dirs + 1] = { f, level + 1 }
  162. end
  163. end
  164. end
  165. end)
  166. end
  167. --- @class vim.fs.find.Opts
  168. --- @inlinedoc
  169. ---
  170. --- Path to begin searching from. If
  171. --- omitted, the |current-directory| is used.
  172. --- @field path? string
  173. ---
  174. --- Search upward through parent directories.
  175. --- Otherwise, search through child directories (recursively).
  176. --- (default: `false`)
  177. --- @field upward? boolean
  178. ---
  179. --- Stop searching when this directory is reached.
  180. --- The directory itself is not searched.
  181. --- @field stop? string
  182. ---
  183. --- Find only items of the given type.
  184. --- If omitted, all items that match {names} are included.
  185. --- @field type? string
  186. ---
  187. --- Stop the search after finding this many matches.
  188. --- Use `math.huge` to place no limit on the number of matches.
  189. --- (default: `1`)
  190. --- @field limit? number
  191. --- Find files or directories (or other items as specified by `opts.type`) in the given path.
  192. ---
  193. --- Finds items given in {names} starting from {path}. If {upward} is "true"
  194. --- then the search traverses upward through parent directories; otherwise,
  195. --- the search traverses downward. Note that downward searches are recursive
  196. --- and may search through many directories! If {stop} is non-nil, then the
  197. --- search stops when the directory given in {stop} is reached. The search
  198. --- terminates when {limit} (default 1) matches are found. You can set {type}
  199. --- to "file", "directory", "link", "socket", "char", "block", or "fifo"
  200. --- to narrow the search to find only that type.
  201. ---
  202. --- Examples:
  203. ---
  204. --- ```lua
  205. --- -- list all test directories under the runtime directory
  206. --- local test_dirs = vim.fs.find(
  207. --- {'test', 'tst', 'testdir'},
  208. --- {limit = math.huge, type = 'directory', path = './runtime/'}
  209. --- )
  210. ---
  211. --- -- get all files ending with .cpp or .hpp inside lib/
  212. --- local cpp_hpp = vim.fs.find(function(name, path)
  213. --- return name:match('.*%.[ch]pp$') and path:match('[/\\\\]lib$')
  214. --- end, {limit = math.huge, type = 'file'})
  215. --- ```
  216. ---
  217. ---@since 10
  218. ---@param names (string|string[]|fun(name: string, path: string): boolean) Names of the items to find.
  219. --- Must be base names, paths and globs are not supported when {names} is a string or a table.
  220. --- If {names} is a function, it is called for each traversed item with args:
  221. --- - name: base name of the current item
  222. --- - path: full path of the current item
  223. --- The function should return `true` if the given item is considered a match.
  224. ---
  225. ---@param opts vim.fs.find.Opts Optional keyword arguments:
  226. ---@return (string[]) # Normalized paths |vim.fs.normalize()| of all matching items
  227. function M.find(names, opts)
  228. opts = opts or {}
  229. vim.validate('names', names, { 'string', 'table', 'function' })
  230. vim.validate('path', opts.path, 'string', true)
  231. vim.validate('upward', opts.upward, 'boolean', true)
  232. vim.validate('stop', opts.stop, 'string', true)
  233. vim.validate('type', opts.type, 'string', true)
  234. vim.validate('limit', opts.limit, 'number', true)
  235. if type(names) == 'string' then
  236. names = { names }
  237. end
  238. local path = opts.path or assert(uv.cwd())
  239. local stop = opts.stop
  240. local limit = opts.limit or 1
  241. local matches = {} --- @type string[]
  242. local function add(match)
  243. matches[#matches + 1] = M.normalize(match)
  244. if #matches == limit then
  245. return true
  246. end
  247. end
  248. if opts.upward then
  249. local test --- @type fun(p: string): string[]
  250. if type(names) == 'function' then
  251. test = function(p)
  252. local t = {}
  253. for name, type in M.dir(p) do
  254. if (not opts.type or opts.type == type) and names(name, p) then
  255. table.insert(t, M.joinpath(p, name))
  256. end
  257. end
  258. return t
  259. end
  260. else
  261. test = function(p)
  262. local t = {} --- @type string[]
  263. for _, name in ipairs(names) do
  264. local f = M.joinpath(p, name)
  265. local stat = uv.fs_stat(f)
  266. if stat and (not opts.type or opts.type == stat.type) then
  267. t[#t + 1] = f
  268. end
  269. end
  270. return t
  271. end
  272. end
  273. for _, match in ipairs(test(path)) do
  274. if add(match) then
  275. return matches
  276. end
  277. end
  278. for parent in M.parents(path) do
  279. if stop and parent == stop then
  280. break
  281. end
  282. for _, match in ipairs(test(parent)) do
  283. if add(match) then
  284. return matches
  285. end
  286. end
  287. end
  288. else
  289. local dirs = { path }
  290. while #dirs > 0 do
  291. local dir = table.remove(dirs, 1)
  292. if stop and dir == stop then
  293. break
  294. end
  295. for other, type_ in M.dir(dir) do
  296. local f = M.joinpath(dir, other)
  297. if type(names) == 'function' then
  298. if (not opts.type or opts.type == type_) and names(other, dir) then
  299. if add(f) then
  300. return matches
  301. end
  302. end
  303. else
  304. for _, name in ipairs(names) do
  305. if name == other and (not opts.type or opts.type == type_) then
  306. if add(f) then
  307. return matches
  308. end
  309. end
  310. end
  311. end
  312. if type_ == 'directory' then
  313. dirs[#dirs + 1] = f
  314. end
  315. end
  316. end
  317. end
  318. return matches
  319. end
  320. --- Find the first parent directory containing a specific "marker", relative to a file path or
  321. --- buffer.
  322. ---
  323. --- If the buffer is unnamed (has no backing file) or has a non-empty 'buftype' then the search
  324. --- begins from Nvim's |current-directory|.
  325. ---
  326. --- Example:
  327. ---
  328. --- ```lua
  329. --- -- Find the root of a Python project, starting from file 'main.py'
  330. --- vim.fs.root(vim.fs.joinpath(vim.env.PWD, 'main.py'), {'pyproject.toml', 'setup.py' })
  331. ---
  332. --- -- Find the root of a git repository
  333. --- vim.fs.root(0, '.git')
  334. ---
  335. --- -- Find the parent directory containing any file with a .csproj extension
  336. --- vim.fs.root(0, function(name, path)
  337. --- return name:match('%.csproj$') ~= nil
  338. --- end)
  339. --- ```
  340. ---
  341. --- @since 12
  342. --- @param source integer|string Buffer number (0 for current buffer) or file path (absolute or
  343. --- relative to the |current-directory|) to begin the search from.
  344. --- @param marker (string|string[]|fun(name: string, path: string): boolean) A marker, or list
  345. --- of markers, to search for. If a function, the function is called for each
  346. --- evaluated item and should return true if {name} and {path} are a match.
  347. --- @return string? # Directory path containing one of the given markers, or nil if no directory was
  348. --- found.
  349. function M.root(source, marker)
  350. assert(source, 'missing required argument: source')
  351. assert(marker, 'missing required argument: marker')
  352. local path ---@type string
  353. if type(source) == 'string' then
  354. path = source
  355. elseif type(source) == 'number' then
  356. if vim.bo[source].buftype ~= '' then
  357. path = assert(uv.cwd())
  358. else
  359. path = vim.api.nvim_buf_get_name(source)
  360. end
  361. else
  362. error('invalid type for argument "source": expected string or buffer number')
  363. end
  364. local paths = M.find(marker, {
  365. upward = true,
  366. path = vim.fn.fnamemodify(path, ':p:h'),
  367. })
  368. if #paths == 0 then
  369. return nil
  370. end
  371. return vim.fs.dirname(paths[1])
  372. end
  373. --- Split a Windows path into a prefix and a body, such that the body can be processed like a POSIX
  374. --- path. The path must use forward slashes as path separator.
  375. ---
  376. --- Does not check if the path is a valid Windows path. Invalid paths will give invalid results.
  377. ---
  378. --- Examples:
  379. --- - `//./C:/foo/bar` -> `//./C:`, `/foo/bar`
  380. --- - `//?/UNC/server/share/foo/bar` -> `//?/UNC/server/share`, `/foo/bar`
  381. --- - `//./system07/C$/foo/bar` -> `//./system07`, `/C$/foo/bar`
  382. --- - `C:/foo/bar` -> `C:`, `/foo/bar`
  383. --- - `C:foo/bar` -> `C:`, `foo/bar`
  384. ---
  385. --- @param path string Path to split.
  386. --- @return string, string, boolean : prefix, body, whether path is invalid.
  387. local function split_windows_path(path)
  388. local prefix = ''
  389. --- Match pattern. If there is a match, move the matched pattern from the path to the prefix.
  390. --- Returns the matched pattern.
  391. ---
  392. --- @param pattern string Pattern to match.
  393. --- @return string|nil Matched pattern
  394. local function match_to_prefix(pattern)
  395. local match = path:match(pattern)
  396. if match then
  397. prefix = prefix .. match --[[ @as string ]]
  398. path = path:sub(#match + 1)
  399. end
  400. return match
  401. end
  402. local function process_unc_path()
  403. return match_to_prefix('[^/]+/+[^/]+/+')
  404. end
  405. if match_to_prefix('^//[?.]/') then
  406. -- Device paths
  407. local device = match_to_prefix('[^/]+/+')
  408. -- Return early if device pattern doesn't match, or if device is UNC and it's not a valid path
  409. if not device or (device:match('^UNC/+$') and not process_unc_path()) then
  410. return prefix, path, false
  411. end
  412. elseif match_to_prefix('^//') then
  413. -- Process UNC path, return early if it's invalid
  414. if not process_unc_path() then
  415. return prefix, path, false
  416. end
  417. elseif path:match('^%w:') then
  418. -- Drive paths
  419. prefix, path = path:sub(1, 2), path:sub(3)
  420. end
  421. -- If there are slashes at the end of the prefix, move them to the start of the body. This is to
  422. -- ensure that the body is treated as an absolute path. For paths like C:foo/bar, there are no
  423. -- slashes at the end of the prefix, so it will be treated as a relative path, as it should be.
  424. local trailing_slash = prefix:match('/+$')
  425. if trailing_slash then
  426. prefix = prefix:sub(1, -1 - #trailing_slash)
  427. path = trailing_slash .. path --[[ @as string ]]
  428. end
  429. return prefix, path, true
  430. end
  431. --- Resolve `.` and `..` components in a POSIX-style path. This also removes extraneous slashes.
  432. --- `..` is not resolved if the path is relative and resolving it requires the path to be absolute.
  433. --- If a relative path resolves to the current directory, an empty string is returned.
  434. ---
  435. --- @see M.normalize()
  436. --- @param path string Path to resolve.
  437. --- @return string Resolved path.
  438. local function path_resolve_dot(path)
  439. local is_path_absolute = vim.startswith(path, '/')
  440. local new_path_components = {}
  441. for component in vim.gsplit(path, '/') do
  442. if component == '.' or component == '' then -- luacheck: ignore 542
  443. -- Skip `.` components and empty components
  444. elseif component == '..' then
  445. if #new_path_components > 0 and new_path_components[#new_path_components] ~= '..' then
  446. -- For `..`, remove the last component if we're still inside the current directory, except
  447. -- when the last component is `..` itself
  448. table.remove(new_path_components)
  449. elseif is_path_absolute then -- luacheck: ignore 542
  450. -- Reached the root directory in absolute path, do nothing
  451. else
  452. -- Reached current directory in relative path, add `..` to the path
  453. table.insert(new_path_components, component)
  454. end
  455. else
  456. table.insert(new_path_components, component)
  457. end
  458. end
  459. return (is_path_absolute and '/' or '') .. table.concat(new_path_components, '/')
  460. end
  461. --- @class vim.fs.normalize.Opts
  462. --- @inlinedoc
  463. ---
  464. --- Expand environment variables.
  465. --- (default: `true`)
  466. --- @field expand_env? boolean
  467. ---
  468. --- @field package _fast? boolean
  469. ---
  470. --- Path is a Windows path.
  471. --- (default: `true` in Windows, `false` otherwise)
  472. --- @field win? boolean
  473. --- Normalize a path to a standard format. A tilde (~) character at the beginning of the path is
  474. --- expanded to the user's home directory and environment variables are also expanded. "." and ".."
  475. --- components are also resolved, except when the path is relative and trying to resolve it would
  476. --- result in an absolute path.
  477. --- - "." as the only part in a relative path:
  478. --- - "." => "."
  479. --- - "././" => "."
  480. --- - ".." when it leads outside the current directory
  481. --- - "foo/../../bar" => "../bar"
  482. --- - "../../foo" => "../../foo"
  483. --- - ".." in the root directory returns the root directory.
  484. --- - "/../../" => "/"
  485. ---
  486. --- On Windows, backslash (\) characters are converted to forward slashes (/).
  487. ---
  488. --- Examples:
  489. --- ```lua
  490. --- [[C:\Users\jdoe]] => "C:/Users/jdoe"
  491. --- "~/src/neovim" => "/home/jdoe/src/neovim"
  492. --- "$XDG_CONFIG_HOME/nvim/init.vim" => "/Users/jdoe/.config/nvim/init.vim"
  493. --- "~/src/nvim/api/../tui/./tui.c" => "/home/jdoe/src/nvim/tui/tui.c"
  494. --- "./foo/bar" => "foo/bar"
  495. --- "foo/../../../bar" => "../../bar"
  496. --- "/home/jdoe/../../../bar" => "/bar"
  497. --- "C:foo/../../baz" => "C:../baz"
  498. --- "C:/foo/../../baz" => "C:/baz"
  499. --- [[\\?\UNC\server\share\foo\..\..\..\bar]] => "//?/UNC/server/share/bar"
  500. --- ```
  501. ---
  502. ---@since 10
  503. ---@param path (string) Path to normalize
  504. ---@param opts? vim.fs.normalize.Opts
  505. ---@return (string) : Normalized path
  506. function M.normalize(path, opts)
  507. opts = opts or {}
  508. if not opts._fast then
  509. vim.validate('path', path, 'string')
  510. vim.validate('expand_env', opts.expand_env, 'boolean', true)
  511. vim.validate('win', opts.win, 'boolean', true)
  512. end
  513. local win = opts.win == nil and iswin or not not opts.win
  514. local os_sep_local = win and '\\' or '/'
  515. -- Empty path is already normalized
  516. if path == '' then
  517. return ''
  518. end
  519. -- Expand ~ to users home directory
  520. if vim.startswith(path, '~') then
  521. local home = uv.os_homedir() or '~'
  522. if home:sub(-1) == os_sep_local then
  523. home = home:sub(1, -2)
  524. end
  525. path = home .. path:sub(2)
  526. end
  527. -- Expand environment variables if `opts.expand_env` isn't `false`
  528. if opts.expand_env == nil or opts.expand_env then
  529. path = path:gsub('%$([%w_]+)', uv.os_getenv)
  530. end
  531. if win then
  532. -- Convert path separator to `/`
  533. path = path:gsub(os_sep_local, '/')
  534. end
  535. -- Check for double slashes at the start of the path because they have special meaning
  536. local double_slash = false
  537. if not opts._fast then
  538. double_slash = vim.startswith(path, '//') and not vim.startswith(path, '///')
  539. end
  540. local prefix = ''
  541. if win then
  542. local is_valid --- @type boolean
  543. -- Split Windows paths into prefix and body to make processing easier
  544. prefix, path, is_valid = split_windows_path(path)
  545. -- If path is not valid, return it as-is
  546. if not is_valid then
  547. return prefix .. path
  548. end
  549. -- Remove extraneous slashes from the prefix
  550. prefix = prefix:gsub('/+', '/')
  551. end
  552. if not opts._fast then
  553. -- Resolve `.` and `..` components and remove extraneous slashes from path, then recombine prefix
  554. -- and path.
  555. path = path_resolve_dot(path)
  556. end
  557. -- Preserve leading double slashes as they indicate UNC paths and DOS device paths in
  558. -- Windows and have implementation-defined behavior in POSIX.
  559. path = (double_slash and '/' or '') .. prefix .. path
  560. -- Change empty path to `.`
  561. if path == '' then
  562. path = '.'
  563. end
  564. return path
  565. end
  566. --- @param path string Path to remove
  567. --- @param ty string type of path
  568. --- @param recursive? boolean
  569. --- @param force? boolean
  570. local function rm(path, ty, recursive, force)
  571. --- @diagnostic disable-next-line:no-unknown
  572. local rm_fn
  573. if ty == 'directory' then
  574. if recursive then
  575. for file, fty in vim.fs.dir(path) do
  576. rm(M.joinpath(path, file), fty, true, force)
  577. end
  578. elseif not force then
  579. error(string.format('%s is a directory', path))
  580. end
  581. rm_fn = uv.fs_rmdir
  582. else
  583. rm_fn = uv.fs_unlink
  584. end
  585. local ret, err, errnm = rm_fn(path)
  586. if ret == nil and (not force or errnm ~= 'ENOENT') then
  587. error(err)
  588. end
  589. end
  590. --- @class vim.fs.rm.Opts
  591. --- @inlinedoc
  592. ---
  593. --- Remove directories and their contents recursively
  594. --- @field recursive? boolean
  595. ---
  596. --- Ignore nonexistent files and arguments
  597. --- @field force? boolean
  598. --- Remove files or directories
  599. --- @since 13
  600. --- @param path string Path to remove
  601. --- @param opts? vim.fs.rm.Opts
  602. function M.rm(path, opts)
  603. opts = opts or {}
  604. local stat, err, errnm = uv.fs_stat(path)
  605. if stat then
  606. rm(path, stat.type, opts.recursive, opts.force)
  607. elseif not opts.force or errnm ~= 'ENOENT' then
  608. error(err)
  609. end
  610. end
  611. return M