findup

function findup( path, name [, mode [, depth]] ) --> entrypath

Description

Find a file or directory by walking up parent directories.

Parameters

path

Start path to a directory that should be checked.

name

Name of the entry this function is looking for.

mode

Optional string representing a associated protection mode (the values can be file, directory, link, socket, named pipe, char device, block device or other)

depth

Optional number of parents the function should check at most.

Return Values

entrypath

The path to the entry, otherwise nil.

Code

local lfs = require( "lfs" ) --ZREQ-lfs

--ZFUNC-isdir-v1
local function isdir( path ) --> res
   local mode = lfs.attributes( path, "mode" )
   if mode == "directory" then
      return true
   else
      return false
   end
end
--ZFUNC-dirhas-v1
local function dirhas( path, name, mode ) --> entrypath
   local modeval = lfs.attributes( path, "mode" )
   if modeval ~= "directory" then return nil end

   for entry in lfs.dir( path ) do
      if entry == name then
         local entrypath = path.."/"..entry

         if mode then
            local modeval = lfs.attributes( entrypath, "mode" )
            if modeval ~= mode then
               return nil
            end
         end

         return entrypath
      end
   end

   return nil
end
--ZFUNC-dirname-v1
local function dirname( path ) --> parent
   local i = #path
   local c = string.sub( path, i, i )
   if c == "/" and #path >= 1 then
      i = i - 1
      c = string.sub( path, i, i )
   end

   while i > 0 and c ~= "/" do
      i = i - 1
      c = string.sub( path, i, i )
   end

   if i == 0 then
      return path
   elseif i == 1 then -- root case
      return string.sub( path, 1, 1 )
   else
      return string.sub( path, 1, i-1 )
   end
end
--ZFUNC-unixpath-v1
local function unixpath( path )
   return path:gsub( "\\", "/" )
end
--ZFUNC-currentdir-v1
local function currentdir()
   local path = lfs.currentdir()
   return unixpath( path )
end
--ZFUNC-rootprefix-v1
local function rootprefix( path )
   local remote = path:match[[^//%w+/]] or path:match[[^\\%w+\]]
   if remote then return remote end

   local unix = path:sub( 1, 1 )
   if unix == "/" then return unix end

   local win = path:match[=[^[a-zA-Z]:[\/]]=]
   if win then return win end

   return ""
end
--ZFUNC-isabsolute-v1
local function isabsolute( path ) --> res
   return rootprefix( path ) ~= ""
end
--ZFUNC-firstchar-v1
local function firstchar( str )
   return string.sub( str, 1, 1 )
end
--ZFUNC-lastchar-v1
local function lastchar( str )
   return string.sub( str, #str )
end
--ZFUNC-joinpath-v1
local function joinpath( tab )
   local rooted = false
   local tmptab = {}
   for k, s in ipairs( tab ) do
      if k == 1 and firstchar( s ) == "/" then
         rooted = true
      end

      if firstchar( s ) == "/" then
         s = s:sub( 2 )
      end
      if lastchar( s ) == "/" then
         s = s:sub( 1, #s - 1 )
      end

      if #s > 0 then
         table.insert( tmptab, s )
      end
   end

   if rooted then
      return "/"..table.concat( tmptab, "/" )
   end

   return table.concat( tmptab, "/" )
end
--ZFUNC-splitpath-v1
local function splitpath( path )
   local tab = {}
   for token in string.gmatch( path, "[^/]+" ) do
      if #token > 0 then
         table.insert( tab, token )
      end
   end
   return tab
end
--ZFUNC-normpath-v1
local function normpath( dirty ) --> clean
   if dirty == "" then return "." end

   local rooted = dirty:sub( 1, 1 ) == "/"

   local dirtytab = splitpath( dirty )
   local cleantab = {}
   for k, v in ipairs( dirtytab ) do
      if v == "." then
      elseif v == ".." then
         table.remove( cleantab )
      elseif v == "" then
      else
         table.insert( cleantab, v )
      end
   end

   if rooted then
      return "/"..table.concat( cleantab, "/" )
   end

   return table.concat( cleantab, "/" )
end
--ZFUNC-abspath-v3
local function abspath( path ) --> abs
   if isabsolute( path ) then
      return normpath( path )
   end

   return normpath( joinpath{ currentdir(), path } )
end
--ZFUNC-findup-v1
local function findup( path, name, mode, depth ) --> entrypath
   if not isdir( path ) then return nil end

   local currdir = abspath( path )

   while not dirhas( currdir, name, mode ) do
      local parentdir = dirname( currdir )
      if parentdir == currdir then return nil end
      if depth then
         depth = depth-1
         if depth < 0 then return nil end
      end

      currdir = parentdir
   end

   return joinpath{ currdir, name }
end

return findup

Examples

local t = require( "taptest" )
local currentdir = require( "currentdir" )
local findup = require( "findup" )
local mkdirtree = require( "mkdirtree" )
local rmdirtree = require( "rmdirtree" )
local relativepath = require( "relativepath" )

local function p( path )
   return relativepath( currentdir(), path )
end

-- test
mkdirtree{
   [ "tmp_findup" ] = {
      [ "a" ] = {
         [ "b" ] = {
            [ "c" ] = {
               [ "d" ] = {
                  [ "e" ] = {
                     [ "f" ] = {
                        [ "g" ] = {
                           [ "h" ] = {}
                        }
                     }
                  },
                  [ "foo.txt" ] = "", -- in "d"
               }
            }
         },
         [ "foo.txt" ] = "" -- in "a", will not be found
      },
      [ "b" ] = "" -- file in "tmp_findup"
   }
}

-- find file or directory
res = findup( "tmp_findup/a/b/c/d/e/f", "foo.txt" )
t( p( res ), "tmp_findup/a/b/c/d/foo.txt" )

res = findup( "tmp_findup/a/b/c/d/e/f", "b"  )
t( p( res ), "tmp_findup/a/b" )

-- find only file
res = findup( "tmp_findup/a/b/c/d/e/f/g/h", "b", "file" )
t( p( res ), "tmp_findup/b" )

-- find only directory
res = findup( "tmp_findup/a/b/c/d/e/f/g/h", "b", "directory" )
t( p( res ), "tmp_findup/a/b" )

-- restrict search depth
res = findup( "tmp_findup/a/b/c/d/e", "foo.txt", "file", 1 ) -- check parent
t( p( res ), "tmp_findup/a/b/c/d/foo.txt" )

res = findup( "tmp_findup/a/b/c/d/e", "foo.txt", "file", 0 ) -- like dirhas
t( nil, nil )

res = findup( "tmp_findup/a/b/c/d/e/f/g/h", "foo.txt", "file", 4 )
t( p( res ), "tmp_findup/a/b/c/d/foo.txt" )

res = findup( "tmp_findup/a/b/c/d/e/f/g/h", "foo.txt", "file", 3 )
t( nil, nil )

rmdirtree( "tmp_findup" )

t()

Inspired by

See also