How to modularize/abstract away os.walkDir and os.walkDirRec - Nim

Viewed 259

I wonder if it is possible with function pointers or macros or the like, to make the code below more modular, as in, not repeat the whole for loop, with the only difference being the walkDirRec vs walkDir.

I tried with function pointers and a macro, but did not manage to get either one working. (-> nim-noob)

import os

proc containsFilesWithSuffix(dir: string, suffix: string, recursive: bool = true) : bool =
  if recursive:
    for file in os.walkDirRec(dir):
      if file.toLower().endsWith(suffix):
        return true
  else:
    for file in os.walkDir(dir):
      if file.toLower().endsWith(suffix):
        return true
  return false
4 Answers
import std/[os, strutils]

template anyOfIt*(sequence, predicate: untyped): bool =
  var result = false
  for it {.inject.} in sequence:
    if predicate:
      result = true
      break

  result


proc containsFilesWithSuffix(dir: string, suffix: string, recursive: bool = true) : bool =
  if recursive: anyOfIt(walkDirRec(dir), it.toLower.endsWith(suffix))
  else:         anyOfIt(walkDir(dir),    it.path.toLower.endsWith(suffix))

Something like that would work fine, though in this case I would just write a for loop anyway since it is easier to add more conditions/logic when I need it.

Here is a version using templates:

import os, strutils

template innerLoop(it, op: untyped): untyped =
  for file {.inject.} in it:
    if toLower(op).endsWith(suffix):
      return true

proc containsFilesWithSuffix(dir: string, suffix: string, recursive: bool = true) : bool =
  if recursive:
    innerLoop(walkDirRec(dir), file)
  else:
    innerLoop(walkDir(dir), file.path)
  return false

I find the main problem here is that walkDirRec and walkDir are {.inline.} iterators and have different signatures. Only {.closure.} iterators can be passed around freely (according to the manual). One way or another you have to unify their signatures, doing file vs file.path or interfacing.

I'm leaving this here to show how you can assign iterators to a variable if their signatures match:

import os, strutils

iterator walkRec(dir: string): string {.closure.} =
  for file in walkDirRec(dir):
    yield file

iterator walkDir(dir: string): string {.closure.} =
  for _, file in walkDir(dir):
    yield file

proc hasFile(dir, suffix: string, recursive: bool = true): bool =
  let iterFiles: iterator(dir: string): string =
    if recursive: walkRec else: walkDir

  for file in iterFiles(dir):
    if file.toLower().endsWith(suffix):
      return true

Now you can call the hasFile proc as follows:

echo hasFile("path", ".exe")  # Find .exe files recursively at path
echo hasFile("path", ".exe", recursive=false)  # Same, but no recursion

All of these answers are pretty much the same, this is just another variant that balances readability against reusability a bit differently.

One complexity comes from the fact that your code as written won't compile: file.toLower in the walkDir branch doesn't compile, due to the fact that walkDir doesn't yield a string, but a tuple, so your branches aren't as similar as you present.

In this version i elide that fact with a hacky template that makes file.path work for either branch, and reduce the noise of passing more variables by declaring the templates in the scope of the proc.

import std/[os,strutils]
proc containsFilesWithsuffix(dir: string, suffix: string, recursive: bool = true): bool =
  
  template path(s:string): string = s # walkDir/walkDirRec compatibility
  
  template loopWith(walk):untyped =
    for file in walk(dir):
      if file.path.toLower.endsWith(suffix):
        return true
  
  if recursive: 
    loopWith(os.walkDirRec)
  else:
    loopWith(os.walkDir)
Related