ruricolist / serapeum

Utilities beyond Alexandria
MIT License
415 stars 41 forks source link

with-output-to-destination #155

Open mmontone opened 1 year ago

mmontone commented 1 year ago

Hi.

I use this macro in some of my systems, to write to an output stream created from a "destination" spec:

Similar to CL:FORMAT destination argument, but also writes to files.

I've opened this issue in case you are interested and would like to include in Serapeum.

(defun call-with-output-to-destination (destination function &rest args)
  "Evaluate FUNCTION with a stream created from DESTINATION as argument.
If DESTINATION is a pathname, then it is opened for writing.
If it is a string, then it is interpreted as a PATHNAME.
If it is a stream, then it is used as it is.
If it is NIL, then WITH-OUTPUT-TO-STRING is used to create the stream.
If it is T, then *STANDARD-OUTPUT* is used for the stream.
ARGS are used for OPEN-FILE calls."
  (etypecase destination
    ((or pathname string)
     (let ((stream (apply #'open destination :direction :output args)))
       (unwind-protect
            (funcall function stream)
         (close stream))))
    (stream
     (funcall function destination))
    (null
     (with-output-to-string (stream)
       (funcall function stream)))
    (t
     (funcall function *standard-output*))))       

(defmacro with-output-to-destination ((var destination &rest args) &body body)
  "Evaluate BODY with VAR bound to a stream created from DESTINATION.
If DESTINATION is a pathname, then it is opened for writing.
If it is a string, then it is interpreted as a PATHNAME.
If it is a stream, then it is used as it is.
If it is NIL, then WITH-OUTPUT-TO-STRING is used to create the stream.
If it is T, then *STANDARD-OUTPUT* is used for the stream.
ARGS are used for OPEN-FILE calls."
  `(call-with-output-to-destination ,destination (lambda (,var) ,@body) ,@args))
mmontone commented 1 year ago

I've just realized this is equivalent to UIOP/STREAM:WITH-OUTPUT

mmontone commented 1 year ago

My version supports passing arguments to OPEN when opening files, and that's important for my use cases, as I may want to overwrite, etc. So this may still be useful.

ruricolist commented 1 year ago

The one concern I have it here is interpreting strings as files -- cl:with-output-to-string, serapeum:with-string, and uiop:with-output all allow writing to a string with a fill pointer as if it were a stream. The function should at least check for a fill pointer and treat it as a stream in that case.

mmontone commented 1 year ago

Ok. I'll improve it

El dom., 23 jul. 2023 11:28, Paul M. Rodriguez @.***> escribió:

The one concern I have it here is interpreting strings as files -- cl:with-output-to-string, serapeum:with-string, and uiop:with-output all allow writing to a string with a fill pointer as if it were a stream. The function should at least check for a fill pointer and treat it as a stream in that case.

— Reply to this email directly, view it on GitHub https://github.com/ruricolist/serapeum/issues/155#issuecomment-1646853656, or unsubscribe https://github.com/notifications/unsubscribe-auth/AADKPDWQN4VDKRTMIZ3QLBLXRUYHHANCNFSM6AAAAAA2UPODMU . You are receiving this because you modified the open/close state.Message ID: @.***>

mmontone commented 1 year ago

How about this:

(defun call-with-output-to-destination (destination function &rest args)
  "Evaluate FUNCTION with a stream created from DESTINATION as argument.
If DESTINATION is a pathname, then open the file for writing. ARGS are used in the OPEN call.
If it is a string with a fill-pointer, WITH-OUTPUT-TO-STRING is used to create a stream for it.
If it is a stream, then it is used as it is.
If it is NIL, then WITH-OUTPUT-TO-STRING is used to create the stream.
If it is T, then *STANDARD-OUTPUT* is used for the stream."
  (etypecase destination
    (pathname
     (let ((stream (apply #'open destination :direction :output args)))
       (unwind-protect
            (funcall function stream)
         (close stream))))
    (string
     (assert (array-has-fill-pointer-p destination)
             nil "Destination string doesn't have a fill-pointer")
     (let ((result nil))
       (with-output-to-string (stream destination)
         (setf result (funcall function stream)))
       result))
    (stream
     (funcall function destination))
    (null
     (with-output-to-string (stream)
       (funcall function stream)))
    ((eql t)
     (funcall function *standard-output*))))

(defmacro with-output-to-destination ((var destination &rest args) &body body)
  "Evaluate BODY with VAR bound to a stream created from DESTINATION.
If DESTINATION is a pathname, then open the file for writing. ARGS are used in the OPEN call.
If it is a string with a fill-pointer, use WITH-OUTPUT-TO-STRING to create a stream for it.
If it is a stream, then it is used as it is.
If it is NIL, then WITH-OUTPUT-TO-STRING is used to create the stream.
If it is T, then *STANDARD-OUTPUT* is used for the stream."
  `(call-with-output-to-destination ,destination (lambda (,var) ,@body) ,@args))

Strings are not interpreted as pathnames anymore, to avoid any confusion. And are required to have a fill-pointer.

ruricolist commented 1 year ago

That sounds good to me, if you'd like to make an MR.