47

I don't understand R's message vs cat vs print vs etc. too deeply, but I'm wondering if it's possible to capture messages and show them in a shiny app?

Example: the following app can capture cat statements (and print statements as well) but not message statements

runApp(shinyApp(
  ui = fluidPage(
    textOutput("test")
  ),
  server = function(input,output, session) {
    output$test <- renderPrint({
      cat("test cat")
      message("test message")
    })
  }
))

Cross post from the shiny-discuss Google group since I got 0 answers.

zx8754
  • 52,746
  • 12
  • 114
  • 209
DeanAttali
  • 25,268
  • 10
  • 92
  • 118

3 Answers3

54

Yihui suggested I use withCallingHandlers, and that indeed let me to a solution. I wasn't quite sure how to use that function in a way that would do exactly what I needed because my problem was that I had a function that printed out several messages one at a time and using a naive approach only printed the last message. Here is the my first attempt (which works if you only have one message to show):

foo <- function() {
  message("one")
  message("two")
}

runApp(shinyApp(
  ui = fluidPage(
    actionButton("btn","Click me"),
    textOutput("text")
  ),
  server = function(input,output, session) {
    observeEvent(input$btn, {
      withCallingHandlers(
        foo(),
        message = function(m) output$text <- renderPrint(m$message)
      )
    })
  }
))

Notice how only two\n gets outputted. So my final solution was to use the html function from shinyjs package (disclaimer: I wrote that package), which lets me change or append to the HTML inside an element. It worked perfectly - now both messages got printed out in real-time.

foo <- function() {
  message("one")
  Sys.sleep(0.5)
  message("two")
}

runApp(shinyApp(
  ui = fluidPage(
    shinyjs::useShinyjs(),
    actionButton("btn","Click me"),
    textOutput("text")
  ),
  server = function(input,output, session) {
    observeEvent(input$btn, {
      withCallingHandlers({
        shinyjs::html("text", "")
        foo()
      },
        message = function(m) {
          shinyjs::html(id = "text", html = m$message, add = TRUE)
      })
    })
  }
))
DeanAttali
  • 25,268
  • 10
  • 92
  • 118
  • 2
    I tip my hat to you, sir. – geotheory May 19 '17 at 23:28
  • Thank you very much. I've had a function that printed message() text. With your solution I can edit the `shinyjs::html(id = "text", html...` line using HTML tags. – Fábio Feb 24 '18 at 04:39
  • This currently outputs the messages without line returns in between. Is this feasible to do? How would one do it? – kennyB Mar 27 '18 at 02:21
  • @kennyB Did you manage to find a way to inset a line return so messages are displayed in different lines? Thanks! – alvaropr Aug 29 '18 at 13:38
  • This doesn't seem to work. Has anyone used this recently? – knapply Mar 17 '19 at 21:48
  • @DeanAttali Something’s wacky on my end then. Thank you for checking! – knapply Mar 18 '19 at 03:40
  • I'm trying to use this in a module framework... doesn't look like the namespace `ns()` is working properly with this solution...? – quickreaction Apr 01 '20 at 22:20
  • That's possible, I haven't tried. Though to be honest, I've yet to see many things that break inside modules, usually it's a user error. – DeanAttali Apr 14 '20 at 18:02
  • 1
    To add a line break after each message you can adjust to `shinyjs::html(id = "text", html = paste0(m$message, '
    '), add = TRUE)`
    – jbaums Jul 25 '20 at 09:17
  • @DeanAttali: This is very impressive, thank you very much. Is there a way, thou, to show the messages only in the Shiny UI, but not in the R console? That is, fully redirecting them to the UI. – panman Aug 02 '20 at 22:43
  • Is there a way? Probably :) Is it going to be annoying to play around with code until you find it? Also probably :) I think that would be a different solution than what I came up with here. Look into the `sink()` function and perhaps have a sort of timer that checks on any new messages in a regular interval, that would be the first thing I'd look into. – DeanAttali Aug 02 '20 at 23:55
  • 4
    @DeanAttali, Is it possible to capture `cat` and `print` to UI in real-time? If someone's function use cat or print, `withCallingHandlers` or `tryCatch` is not working. – lz100 Oct 28 '20 at 05:13
  • 2
    It seems this only works with `observeEvent`. Am I missing something for this to work on `eventReactive`? – Jeff Parker Mar 04 '21 at 18:30
  • 1
    I am using sink() with reactiveFileReader() for the generated log file of console output and then using renderPrint() to display output by verbatimTextOutput() but it is not live. Couldn't find an good examples that can do the live line by line display of console to Shiny. @DeanAttali if you have any example of this that you could share, I would really appreciate that. – Tarun Parmar Mar 18 '21 at 03:54
3

I know this isn't nearly as elegant, but I worked around a bit similar problem using capture.output; sadly sink doesn't allow simultaneous capture of messages and output though. You don't get them in the original order, but you can extract both streams at least (here turned to HTML):

runApp(shinyApp(
  ui = fluidPage(
    uiOutput("test")
  ),
  server = function(input,output, session) {
    output$test <- renderUI({
      HTML(
      paste(capture.output(type = "message", expr = { 
        message(capture.output(type = "output", expr = {
          cat("test cat<br>")
          message("test message")
          cat("test cat2<br>")
          message("test message2")
        }))
      }), collapse="<br>")
  )})
 })
)

Output:

test message
test message2
test cat
test cat2

Perhaps in the case if user wants to capture both but also separate them, this will provide a handy work-around. (Your shinyjs package seems neat, need to take a look at it!)

Teemu Daniel Laajala
  • 2,316
  • 1
  • 26
  • 37
  • 5
    The problem with this approach is that all the output gets printed at the end, it doesn't come in live. So if you run a slow function that prints output as it runs, you won't see it until it's done – DeanAttali Nov 21 '16 at 03:10
1

This can now be done with the high-level function shinyCatch from the spsComps package.

Basic usage

library(shiny)
library(spsComps)
ui <- fluidPage(
  actionButton("msg", "msg"),
  actionButton("warn", "warn"),
  actionButton("err", "err"),
)

server <- function(input, output, session) {
    observeEvent(input$msg, {
      shinyCatch({message("a message")}, prefix = '')
    })
    observeEvent(input$warn, {
        shinyCatch({warning("a warning")}, prefix = '')
    })
    observeEvent(input$err, {
        shinyCatch({stop("an error")}, prefix = '')
    })
    
}

shinyApp(ui, server)

enter image description here

Choose blocking level

If exceptions happened, we can choose to continue the code or block downstream code in the reactive context. For example, we want to stop downstream code if an error/warning/message happens:

library(shiny)
library(spsComps)
ui <- fluidPage(
  actionButton("err", "code not blocked after error"),
  actionButton("err_block", "code blocked after error"),
)

server <- function(input, output, session) {
    observeEvent(input$err, {
        shinyCatch({stop("an error")}, prefix = '')
        print("error does not block ")
    })
    
    observeEvent(input$err_block, {
        shinyCatch({stop("an error")}, prefix = '', blocking_level = "error")
        print("you can't see me if error happens")
    })
}

shinyApp(ui, server)

enter image description here

More advanced use

check website and demo

cat and print

There is still no good method to catch real-time info from cat and print. I will come back to update this answer if I found a fix for this.

lz100
  • 6,990
  • 6
  • 29