Skip to content

Conversation

@nwf
Copy link
Member

@nwf nwf commented Aug 9, 2018

See #2440.

  • This PR is for the dev branch rather than for master.
  • This PR is compliant with the other contributing guidelines as well (if not, please describe why).
  • I have thoroughly tested my contribution.
  • The code changes are reflected in the documentation at docs/en/*.

This adds an optional pattern to file.list() to filter results before handing them to Lua. This optimizes the relatively common pattern of

for f,s in pairs(file.list()) do
  if s:match(pattern) then
    -- process matching file name
  end
end

into the more concise, faster, and less-memory-consuming

for f,s in pairs (file.list(pattern)) do
  -- process each matching file name
end

(neglecting the possibility that pattern might raise an error. If that happens, file.list(pattern) will now return nil, errorstr, as typical).

This has been tested by running comprehensive test cases:

for k,v in pairs(file.list()) do print(k,v) end -- all files
for k,v in pairs(file.list("asdf")) do print(k,v) end -- pattern with no matches
for k,v in pairs(file.list("ini")) do print(k,v) end -- pattern some matches
for k,v in pairs(file.list({})) do print(k,v) end -- wrong type passed as argument
_, y = file.list("%fx"); print (y) -- pattern error passed up to caller

@nwf
Copy link
Member Author

nwf commented Aug 9, 2018

P.S. please be gentle, but please do review this for sanity and proper stack handling; it's only my second or third time programming against the Lua FFI.

a Lua table which contains the {file name: file size} pairs
a Lua table which contains all {file name: file size} pairs, if no pattern
given. If a pattern is given, only those file names matching the pattern
(interpreted as a traditional Lua pattern, not, say, a UNIX shell glob) will be
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure. 👍

@nwf nwf force-pushed the for-upstream-file-list branch from c417644 to c20d02a Compare August 9, 2018 11:56
@TerryE TerryE mentioned this pull request Aug 9, 2018
lua_pushstring( L, stat.name );
lua_pushvalue( L, -4 );
/* Call transforms stack from T P F F N P to T P F Result */
pcres = lua_pcall( L, 2, 1, 0 );
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't do a lua_pcall(), in this case just do a plain lua_call() is more appropriate. If the app programmer can't use a valid pattern then the correct thing to do is to let lstrlib.c throw a Lua error.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, that's not sufficient. Lua will longjmp over our activation frame and not give us the opportunity to call vfs_closedir, which might imply a leak of a directory iterator. If not in SPIFFS or FATFS, some day, all the same.

/* If we're doing a match, push the Pattern and Function */
if (pattern != emptystr) {
lua_pushstring( L, pattern );
/*
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why push this here? It was the call arg, so is already on the stack.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair enough. I suppose luaL_checkoptstring just returns what's already on the stack.

int res = 1;

pattern = luaL_optlstring(L, 1, emptystr, NULL);

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can just use NULL as the default and this works fine. It also makes the following code easier. Also it is often just easier to fix the stack length so a lua_settop(L,1); will add the add the nil is missing and trim excess args so that you can now use base relative rather than top relative addressing. A lot easier to follow, IMO.

lua_pop( L, 1 );
} else {
/* Stack just holds result table at the top; add result */
lua_pushinteger( L, stat.size );
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these to push/setfield calls can be folded easily

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I debated doing that. I can, if you like.

@TerryE
Copy link
Collaborator

TerryE commented Aug 9, 2018

Nathaniel, just out of interest, here was my attempt. Does this make sense to you? I know the manual talks as if it is a stack, but sometimes its just a lot simpler to base relative addressing.

static int file_list( lua_State* L ) {
  vfs_dir  *dir;
  const char *pattern;
  struct vfs_stat stat;

  /* make sure the stack contains exactly one string or nil */ 
  lua_settop(L, 1);
  pattern = luaL_optstring(L, 1, NULL);                  /* pattern at 1 */
  
  if ( (dir = vfs_pendir("")) == NULL) {
    return 0;
  }

  /* Allocate result table T */
  lua_newtable( L );                                     /* table at 2 */

  /* If we're doing a match, push the string.match function */
  if (pattern) {
    luaL_getmetafield( L, 1, "match" );                  /* function at 3 */
  }

  while (vfs_readdir(dir, &stat) == VFS_RES_OK) {
    if (pattern) {
      lua_settop( L,  3 );
      /* string.match(stat_name, pattern */
      lua_pushvalue( L, 3 );                             /* function at 4 */
      lua_pushstring( L, stat.name );                    /* filename at 5 */
      lua_pushvalue( L, 1 );                             /* pattern at 6 */
      lua_call( L, 2, 1 );                               /* result at 4 */
      if (lua_isnil( L, -1 )) {
        continue;
      }
    }
    lua_pushinteger( L, stat.size );
    lua_setfield( L, 2, stat.name );
  }

  lua_remove( L, 1 );           /* remove pattern at 1, so table now at 1 */              
  vfs_closedir(dir);
  return 1;
}

@nwf
Copy link
Member Author

nwf commented Aug 9, 2018

@TerryE Thanks for that. I reworked mine in light of yours and re-ran the tests I enumerated above.

@nwf nwf force-pushed the for-upstream-file-list branch from c20d02a to b83ecf7 Compare August 9, 2018 23:42
@TerryE
Copy link
Collaborator

TerryE commented Aug 9, 2018

@nwf, it takes a bit to get your head around using the API, but once you do, it is really quite potent. The more of us the merrier.

Are you using the gdb stub? It's really quite good. This is one time that I use esplorer, because I am cycling reboot/tweak and start script in explorer and then toogle into the xtensa gdb. Also note the .gdbinit and .gdbinitlua in the dev branch which contain useful macros.

lua_pushvalue( L, 3 );
lua_pushstring( L, stat.name );
lua_pushvalue( L, 1 );
pcres = lua_pcall( L, 2, 1, 0 );
Copy link
Collaborator

@TerryE TerryE Aug 10, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is really the wrong thing to do. You should just use a lua_call() and let the lua error system do its stuff. So for example if you do something like:

   for name, len in pairs(files.list('[a-f]%')) do`

you want it to throw a malformed pattern (ends with '%') rather than a bad argument #1 to 'pairs' (table expected, got number). 9 times out of 10 the pattern is programmed, and on the 10th time, the programmer can always wrap the file.list(filter) in a pcall()

Copy link
Member Author

@nwf nwf Aug 10, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I said above, I do not believe that longjump-ing out of file.list() is correct, as that will not call vfs_closedir() on its way out. I've reworked the patch to use lua_error to re-throw the exception.

Thanks to @TerryE for many useful suggestions
@nwf nwf force-pushed the for-upstream-file-list branch from b83ecf7 to 0343283 Compare August 10, 2018 00:31
@TerryE
Copy link
Collaborator

TerryE commented Aug 10, 2018

Sorry, missed that. In which case you don't need the goto. Just do the closedir and throw the error inline in the pcall status test code.

@nwf
Copy link
Member Author

nwf commented Aug 10, 2018

@TerryE The goto's gone; mind taking another look?

@marcelstoer marcelstoer added this to the next-release milestone Aug 10, 2018
@TerryE TerryE merged commit fd12be9 into nodemcu:dev Aug 10, 2018
@nwf nwf deleted the for-upstream-file-list branch August 10, 2018 15:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants