29

I would like users to be able to select a directory interactively in R. The solution needs to work on different platforms (at least on Linux, Windows and Mac machines that have a graphical desktop environment). And it needs to be robust enough to work on a variety of computers. I've run into problems with the variants I know of:

file.choose() unfortunately only works for files - It won't allow to select a directory. Other than this limitation, file.choose is a good example of the type of solution I'm looking for - it works across platforms and does not have external dependencies that may not be available on a particular computer.

choose.dir() Only works on Windows.

tk_choose.dir() from library(tcltk) was my preferred solution until recently. But I've had users report that this throws an error

log4cplus:ERROR No appenders could be found for logger (AdSyncNamespace). log4cplus:ERROR Please initialize the log4cplus system properly.

which we tracked back to Autodesk360 software being installed, which for some reason interferes with tcltk. So this is not a suitable solution unless there is a fix for this. (the only solution I've found by googling is to uninstall Autodesk360, which won't be a solution for users who installed it because they actually use it).

This answer suggests the following as a possible alternative:

library(rJava)
library(rChoiceDialogs)
jchoose.dir()  

But, as an example of the sort of thing that can go wrong with this, when I tried to install.packages("rJava") I got:

checking whether JNI programs can be compiled... configure: error: Cannot compile a simple JNI program. See config.log for details.

Make sure you have Java Development Kit installed and correctly registered in R. If in doubt, re-run "R CMD javareconf" as root.

ERROR: configuration failed for package ‘rJava’ * removing ‘/home/dominic/R/x86_64-pc-linux-gnu-library/3.3/rJava’ Warning in install.packages : installation of package ‘rJava’ had non-zero exit status

I managed to fix this on my own machine (linux running openJDK) by installing the openjdk compiler using the linux package manager then running sudo R CMD javareconf. But I can't expect random users with varying levels of computer expertise to have to jump through hoops just so that they can select a directory. Even if they do manage to fix it, it will look bad when every other piece of software they use manages to open a directory-selection dialogue without any problems.

So my question: Is there a robust method that can reliably be expected to "just work" (like file.choose does for files), on a variety of platforms and makes no expectation of the end user being computer literate enough to solve these kinds of issues (such as incompatabilities with Autodesk360 or unresolved Java dependencies)?

dww
  • 30,425
  • 5
  • 68
  • 111
  • 3
    If you can assume the user will be using RStudio, you can use the `rstudioapi` packge's `selectFile` and `selectDirectory` functions – alistaire Jan 12 '18 at 02:41
  • Test out `rchoose.dir()` from the same package. It successively tries Java, tcltk and a cmd line version until it finds one that works. – G. Grothendieck Jan 12 '18 at 02:55
  • No I was referring to the question. rChoiceDialogs. Also `cmdchoose.files` from that package has minimal dependencies and can be used to choose only directories if you set the args appropriately. – G. Grothendieck Jan 12 '18 at 03:01
  • 1
    @G.Grothendieck ok - thanks. The problem with rChoiceDialogs was that the package won't install without rJava - which could be problematic if Java is not set up correctly. – dww Jan 12 '18 at 03:10
  • The gWidgets vignette defines a file/directory chooser that is only about half a dozen lines. You could try that with gWidgetstcltk and see if the you get the same conflict as reported in the question. – G. Grothendieck Jan 12 '18 at 03:46
  • Also gWidgets2 has `gfile(type = "selectdir")`. You could try that with gWidgets2tcltk or one of the other gWidgets drivers. – G. Grothendieck Jan 12 '18 at 04:20
  • Also you might try googling `log4cplus:ERROR` or `log4cplus:ERROR solved` – G. Grothendieck Jan 12 '18 at 05:56
  • How do your users use the R code? Is it a shiny app? – Borislav Aymaliev Jan 16 '18 at 15:58
  • @BorislavAymaliev - this is not currently in a shiny app. But I have no control over how end users run it. Could be in RStudio, another IDE, an R console, or from a command line. – dww Jan 16 '18 at 16:23
  • @dww, I was considering something like hyperlinked breadcrumb of the current folder for the user to navigate - much like in the windows explorer. But that would be plausable only in apps with hyperlink capabilities, such as shiny. So my "alternative solution" to your problem would be to print all reachable folders from the current folder (all parents and same-folder children) and ask the user to navigate with readline(). You may print them with an index, so that the user does not have to type the full folder name. This is the only platform_and_UI-independent solution I can think of. – Borislav Aymaliev Jan 16 '18 at 16:45
  • @BorislavAymaliev Nice thought. I think we can pretty much assume that there will be an html viewer available on any computer. So an HTML or HTML-widgets based solution might be possible. – dww Jan 16 '18 at 17:21
  • 5
    This one is worth bringing up on the official R-help mailing list. Being able to select a directory in a cross platform way is useful. It is not unlikely it will catch the attention by R core devs and maybe it's a quick fix for them to add this to the `file.choose()` function. – HenrikB Jan 16 '18 at 17:41

5 Answers5

22

In the time since posting this question and an earlier version of this answer, I've managed to test the various options that have been suggested on a range of computers. This process has converged on a fairly simple solution. The only cases I have found where tcltk::tk_choose.dir() fails due to conflicts are on Windows computers running Autodesk software. But on Windows, we have utils::choose.dir available instead. So the answer I am currently running with is:

choose_directory = function(caption = 'Select data directory') {
  if (exists('utils::choose.dir')) {
    choose.dir(caption = caption) 
  } else {
    tk_choose.dir(caption = caption)
  }
}

For completeness, I think it is useful to summarise some of the issues with other approaches and why they do not meet the criteria of being generally robust on a variety of platforms (including robustness against potentially unresolved external dependencies that can't be fixed from within R and that that may require administrator privileges and/or expertise to fix):

  1. easycsv::choose_dir in Linux depends on zenity, which may not be available.
  2. rstudioapi::selectDirectory requires that we are in RStudio Version greater than 1.1.287.
  3. rChoiceDialogs::rchoose.dir requires not only that java runtime environment is installed, but also java compiler must be installed and configured correctly to work with rJava.
  4. utils::menu does not work if the R function is run from the command line, rather than in an interactive session. Also on Linux X11 it frequently leaves an orphan window open after execution, which can't be readily closed.
  5. gWidgets2::gfile has external dependency on either gtk2 or tcltk or Qt. Resolving these dependencies was found to be non-trivial in some cases.

Archived earlier version of this answer

Finally, an earlier version of this answer contained some longer code that tries out several possible solutions to find one that works. Although I have settled on the simple version above, I leave this version archived here in case it proves useful to someone else.

What it tries:

  1. Check whether the function utils::choose.dir exists (will only be available on Windows). If so, use that
  2. Check whether the user is working from within RStudio version 1.1.287 or greater. If so use the RStudio API.
  3. Check if we can load the tcltk package and then open and close a tcltk window without throwing an error. If so, use tcltk.
  4. Check whether we can load gWidgets2 and the RGtk2 widgets. If so, use gWidgets2. I don't try to load the tcltk widgets here, because if they worked, presumably we would already be using the tcltk package. I also do not try to load the Qt widgets, as they seem somewhat unmaintained and are not currently available on CRAN.
  5. Check if we can load rJava and rChoiceDialogs. If so, use rChoiceDialogs.
  6. If none of the above are successful, use a fallback position of requesting the directory name at the console.

Here's the longer version of the code:

# First a helper function to load packages, installing them first if necessary
# Returns logical value for whether successful
ensure_library = function (lib.name){
    x = require(lib.name, quietly = TRUE, character.only = TRUE)
    if (!x) {
      install.packages(lib.name, dependencies = TRUE, quiet = TRUE)
      x = require(lib.name, quietly = TRUE, character.only = TRUE)
      }
  x
}

select_directory_method = function() {
  # Tries out a sequence of potential methods for selecting a directory to find one that works 
  # The fallback default method if nothing else works is to get user input from the console
  if (!exists('.dir.method')){  # if we already established the best method, just use that
    # otherwise lets try out some options to find the best one that works here
    if (exists('utils::choose.dir')) {
      .dir.method = 'choose.dir'
    } else if (rstudioapi::isAvailable() & rstudioapi::getVersion() > '1.1.287') {
      .dir.method = 'RStudioAPI'
      ensure_library('rstudioapi')
    } else if(ensure_library('tcltk') & 
              class(try({tt  <- tktoplevel(); tkdestroy(tt)}, silent = TRUE)) != "try-error") {
      .dir.method = 'tcltk'
    } else if (ensure_library('gWidgets2') & ensure_library('RGtk2')) {
      .dir.method = 'gWidgets2RGtk2'
    } else if (ensure_library('rJava') & ensure_library('rChoiceDialogs')) {
      .dir.method = 'rChoiceDialogs'
    } else {
      .dir.method = 'console'
    }
    assign('.dir.method', .dir.method, envir = .GlobalEnv) # remember the chosen method for later
  }
  return(.dir.method)
}

choose_directory = function(method = select_directory_method(), title = 'Select data directory') {
  switch (method,
          'choose.dir' = choose.dir(caption = title),
          'RStudioAPI' = selectDirectory(caption = title),
          'tcltk' = tk_choose.dir(caption = title),
          'rChoiceDialogs' = rchoose.dir(caption = title),
          'gWidgets2RGtk2' = gfile(type = 'selectdir', text = title),
          readline('Please enter directory path: ')
  )
}
dww
  • 30,425
  • 5
  • 68
  • 111
  • Thanks a lot for sharing this useful code. I suggest to adapt the `choose_directory` that an initial folder can be set. See answer below. – RFelber Sep 06 '18 at 07:20
  • This is great, but should you force installation with `ensure_library` as default? Also, do you think it's worth it to wrap these 3 functions into a package? I think the community will approve and widely use! – Matias Andina Sep 18 '19 at 18:32
  • A limitation of easycsv::choose_dir() is that folder navigation does not (always) start in the current R session's working directory. On macOS Catalina 10.15.7 and running RStudio v1.4.17.17, it opens the most recently opened folder instead. – user2363777 Aug 02 '21 at 19:47
  • I found this useful as my supervisor wants to run my code and he is naive in R. I note that in macOS 12 - Monterey - there's no caption or title on the picker. It opens in the working directory and otherwise works well. – Nate Lockwood Feb 24 '22 at 23:46
  • +1 great answer. FWIW, `tk_choose.dir` keeps crashing my session. Using R version 4.3.0, package version 4.3.0, RStudio version 2023.06.1+524 on Windows OS. – LMc Aug 10 '23 at 19:31
4

you can use the choose_dir function from easycsv.
it works on Windows, Linux and OSX

easycsv::choose_dir() # can be run without parameters to prompt a folder selection window
Dror Bogin
  • 453
  • 4
  • 13
  • Thanks - looking at the source, this function checks the OS and then dispatches to either `choose.dir` (Windows), `zenity` (Linux), or `osascript` (OSX). Zenity is part of Gnome, and may or may not be installed on a particular linux machine. That itself is not the end of the world - we could require zenity installation if necessary. However on my KDE linux machine I get the warning `Gtk-Message: GtkDialog mapped without a transient parent. This is discouraged.` and although the directory name gets printed to the console, the function only returns NULL. – dww Jan 17 '18 at 15:44
  • What do you mean by the path gets printed but you get NULL? The output needs to be assigned if you want to use it. – Dror Bogin Jan 17 '18 at 15:52
  • Well of course I did that :-). Actually it gives a zero - not NULL as I said above. `x = choose_dir(); x` gives `[1] 0` – dww Jan 17 '18 at 16:02
  • OK I know why. There's a bug in the function. Where it says `system("zenity --file-selection --directory")`, it should be `system("zenity --file-selection --directory", intern = TRUE)` – dww Jan 17 '18 at 16:28
  • ok, thanks, will be fixed in the next version, you can reinstall from github in half an hour. [easycsv](https://github.com/bogind/easycsv) – Dror Bogin Jan 17 '18 at 16:53
  • TY. Any way to get suppress the gtk warning? `system("zenity --file-selection --directory &> /dev/null", intern = T)` doesn't work – dww Jan 17 '18 at 17:03
  • seems there's a fix [here](https://askubuntu.com/questions/896935/how-to-make-zenity-transient-parent-warning-disappear-permanently). but it has to be applied locally. – Dror Bogin Jan 17 '18 at 17:07
  • A limitation of easycsv::choose_dir() is that folder navigation does not (always) start in the current R session's working directory. On macOS Catalina 10.15.7 and running RStudio v1.4.17.17, it opens the most recently opened folder instead. – user2363777 Aug 02 '21 at 19:49
4

for some use cases a little trick might be to use dirname() around file.choose()

dir <- dirname(file.choose())

this will return the directory. It does however require at least one file to be present in the directory

Lod
  • 609
  • 7
  • 19
3

Here is a simple directory navigation menu (using menu{utils}):

d=1
while(d != 0) {
  a = getwd()
  a = strsplit(a, "/")
  a = unlist(a)
  b = list.dirs(recursive = F, full.names = F)
  c = paste("..", a[length(a) - 1], a[length(a)], sep = "/")
  d = menu(c("..", b), title = c, graphics = T)
  if(d==1){
    e=paste(paste(a[1:(length(a)-1)],collapse = '/',sep = ''),'/',sep = '')
    #print(e)
    setwd(e)
  }else{
    e=paste(paste(a,collapse = '/',sep = ''),'/',b[d-1],sep='')
    #print(e)
    setwd(e)
  }
}

Note: I did not (yet) test it under different systems. Here is what the documentation says:

If graphics = TRUE and a windowing system is available (Windows, macOS or X11 via Tcl/Tk) a listbox widget is used, otherwise a text menu. It is an error to use menu in a non-interactive session.

One limitation: The title = can only be a single line.

Borislav Aymaliev
  • 803
  • 2
  • 9
  • 20
  • Thanks. Interesting and not a bad solution. A little clunky that user has to first enter a directory then select the same directory again from within it. Another limitation is that it can't be run from a command line. E.g. `Rscript -e "menu(c('1','2','3')"` gives `Error... menu() cannot be used non-interactively. Execution halted`. But biggest issue for me is that, on my linux system, `menu` does not seem to always reliably close the Xwindow afterwards, leaving an orphan window that can't be easily closed. Not sure whether this is an R or an Xwindows issue. – dww Jan 17 '18 at 17:58
2

Suggestion for adaption of choose_directory() as mentioned in my comment (06.09.2018 RFelber):

choose_directory <- function(ini_dir = getwd(),
                             method = select_directory_method(), 
                             title = 'Select data directory') {
  switch(method,
          'choose.dir' = choose.dir(default = ini_dir, caption = title),
          'RStudioAPI' = selectDirectory(path = ini_dir, caption = title),
          'tcltk' = tk_choose.dir(default = ini_dir, caption = title),
          'rChoiceDialogs' = rchoose.dir(default = ini_dir, caption = title),
          'gWidgets2RGtk2' = gfile(type = 'selectdir', text = title, initial.dir = ini_dir),
          readline('Please enter directory path: ')
  )
}
RFelber
  • 164
  • 1
  • 12