Fork me on GitHub
#babashka
<
2023-05-12
>
anovick15:05:54

I noticed an issue with the script I wrote where once I provide a command line argument of a path that has whitespace in it (like "/path/to/Goodnotes 5/my-file.pdf") , it looks first at "/path to/Goodnotes" and then it looks at "5/my-file.pdf", and finds neither files and throws an exception 😅 As a newbie in debugging Clojure code, and babashka code in particular, my first reaction was to try and tweak the script to receive that same path "hard-coded" instead of receiving it as a command line argument, and try to fix the bug through inspection etc. However, there's a part of me that's curious about if there are other ways to go about doing that. Particularly, I've seen that babashka book showcases test files and test running. Is this something that I should look into? Alternatively, should I be using some Cursive features that I'm currently not? (I'm just running a REPL and gradually sending stuff to it to check if it works, or inspect stuff)

❤️ 2
anovick15:05:34

If I were to be able to use a debugger, in the way that I debug code for other languages, I would be placing a breakpoint inside the function that receives the path, but I'm unfamiliar with Cursive debugger, and also not even sure if I should be using that.

borkdude16:05:28

> As a newbie in debugging Clojure code, and babashka code in particular, my first reaction was to try and tweak the script to receive that same path "hard-coded" instead of receiving it as a command line argument, and try to fix the bug through inspection etc. Yes, I think this approach is good. Just call your function with the input which triggers the error in the REPL and then narrow down the issue.

anovick16:05:39

Is there a way to save the previous result before continuing to perform the rest of the function?

anovick16:05:01

I'm referring to the already generated values inside the let form. I'd want to be able to do part of the content in it, and use the vars again

anovick16:05:28

all this without having to re-send the entire let form every time

anovick16:05:17

Guess I could just def those vars outside the let clause

anovick16:05:37

work outside the let form in the namespace scope

borkdude16:05:52

indeed. what people use for this is a comment form in the namespace

borkdude16:05:08

and then you can evaluated (def whatever (my-function ...)) in the comment form

borkdude16:05:18

if you haven't already I recommend reading this: https://clojure.org/guides/repl/introduction

anovick16:05:52

I'll be sure to check it out sometime

anovick16:05:09

Regarding the comment, what is the purpose of that in short?

borkdude16:05:41

the purpose is that you can write your REPL expressions in there and evaluate them from your editor, without executing them when the program is called

borkdude16:05:16

;; scratch 
(comment
  (def x (+ 1 2 3)) ;;=> 3
)  

anovick16:05:37

well but I'm not re-running the script

borkdude16:05:53

I know, but at some point you will

borkdude16:05:09

and then it's not so nice when you have top level expressions that do stuff

anovick16:05:48

Oh so you mean people use those comment forms just so they don't have to clean it up afterwards?

anovick16:05:16

because I was gonna do the work outside the let clause and just put it back in there and remove the defs after that

borkdude16:05:51

yeah, that's the idea, also to give some people an idea how you can call things

anovick16:05:33

@U04V15CAJ I don't quite understand how comment helps me for debugging

anovick16:05:15

I could just comment things out with ; just the same, couldn't I?

anovick16:05:28

for reference, I'm now trying to debug this function here that gets called from the let clause

anovick16:05:34

(defn merge-pdfs [output-combined-pdf-path pdf-files]
  (shell (str/join " " ["pdftk",
                        (str/join " "
                                  (map
                                    (fn [pdf-file-path]
                                      (str pdf-file-path)
                                      )
                                    pdf-files
                                    )),
                        "cat",
                        "output",
                        output-combined-pdf-path
                        ]))
  )

anovick16:05:30

Now I wish to evaluate the (str/join) form inside the function, once it gets invoked (by being sent to the REPL)

anovick16:05:11

Is there a way to debug this without messing with my code too much?

anovick16:05:48

In other languages, I would just put a debugger on a line inside the function and evaluate the values at that point in time

anovick17:05:39

So in the end I just changed the inside of the function so that instead of calling the shell immediately, I just return the value of (str/join) and was able to inspect it afterwards. Turns out the bug fix was that I had to add single-quotes to every file name to avoid whitespace being interpreted by the shell as separate arguments. The change was a one-liner, so I just copied it, reset my git back to previous state and pasted it in :rolling_on_the_floor_laughing:

borkdude17:05:20

> So in the end I just changed the inside of the function so that instead of calling the shell immediately, I just return the value of (str/join) and was able to inspect it afterwards. 👍 > Turns out the bug fix was that I had to add single-quotes to every file name to avoid whitespace being interpreted by the shell as separate arguments. 👎

borkdude17:05:01

I already told you many times that you can supply separate string arguments to shell. There is no need to escape those with single quotes, just do not concatenate them :)

anovick17:05:36

:thinking_face:

anovick17:05:53

You did in fact say that

borkdude17:05:13

(defn merge-pdfs [output-combined-pdf-path pdf-files]
  (apply shell "pdftk" ["file1" "file2" "file3"]))

borkdude17:05:56

I think something like this should work:

(defn merge-pdfs [output-combined-pdf-path pdf-files]
  (apply shell "pdftk" (concat (map str pdf-files) ["cat" "output" output-combined-pdf-path])))

borkdude17:05:14

I think map str isn't even necessary since shell will already call str on the arguments

anovick17:05:29

@U04V15CAJ your code works 🙂

anovick17:05:56

I tried to embrace what you taught and made another version kinda like yours:

(defn merge-pdfs [output-combined-pdf-path pdf-files]
  (shell "pdftk",
         (vector pdf-files),
         "cat",
         "output",
         output-combined-pdf-path
         )
  )

anovick17:05:00

But mine doesn't work

borkdude17:05:38

this is because shell expects strings as arguments, not vectors of strings :)

borkdude17:05:17

you are passing this to shell:

(shell "pdftk" [....] "cat" "output" output-combined-pdf-path)

borkdude17:05:32

but it expects:

(shell "pdftk" ... "cat" "output" output-combined-pdf-path)

anovick17:05:43

oh so like str* ?

anovick17:05:54

where it can either be single string or multiple...

borkdude17:05:06

what is str*?

anovick17:05:17

idk how it's called in

anovick17:05:24

in JS there is ...str

anovick17:05:37

rest parameter

borkdude17:05:44

ah yes, the str function accepts one or more strings, this is called "varargs"

borkdude17:05:07

and shell is a varargs function: it accepts one or more arguments

anovick17:05:24

I see 🙂 yes that's what I meant exactly

anovick17:05:36

thanks for teaching me how to write better code

anovick17:05:40

and how to call this

anovick17:05:57

:hugging_face:

❤️ 2
anovick17:05:45

Hovering on shell in my editor gives me the docs: > [...] The first command line argument is automatically tokenized Does tokenized means adding the single-quotes on path strings?

anovick17:05:40

and if so, is it really only the first argument? because there is "pdftk", "file1", "file2" "cat" "output", then first argument "file1" is tokenized? but about what bout "file2" is that also tokenized?

borkdude17:05:19

you can call (process/tokenize "foo bar 'foo bar'") to see what it does

borkdude17:05:44

only the first argument to shell is tokenized, e.g.: (shell "ls -la") => (shell "ls" "-la")

borkdude17:05:02

but arguments after the first argument are not tokenized

borkdude17:05:24

this is just so you can easily copy things between bash and shell . you don't have to use that feature

anovick17:05:54

Oh this is not related to the issue I had with single quotes

borkdude17:05:25

yes it is because what you did is concatenate everything to one big string and then feed that as the only string to shell

borkdude17:05:36

so then shell tokenizes that again and pulls it apart in separate strings again

anovick18:05:39

Sorry that it's hard to explain this to me, I'm not aware of this terminology...

anovick18:05:06

I do understand sometimes 🙂 other times it falls on deaf ears

borkdude18:05:30

ok, if you write (shell "foo bar 'foo bar'") this executes the same as (shell "foo" "bar" "foo bar") because shell pulls apart the first string

borkdude18:05:20

and you constructed one big string like (shell (str/join " " files)) so shell first tokenizes (pulls apart) that big string into separate strings

borkdude18:05:52

and if one of the file names contain spaces, you get two words instead of just one

borkdude18:05:02

but it's better to just use separate strings to begin with

borkdude18:05:13

if you already have those as separate values

borkdude18:05:56

hope that makes sense

anovick18:05:25

So the shell function received my long string and then pulled it apart on the whitespaces?

borkdude18:05:13

yes. if you write:

(shell "rm my file")
this will run the same as (shell "rm" "my" "file")

borkdude18:05:52

but if you already have "rm" and "my file" there is no need to make that into a single string, just pass them both:

(shell "rm" "my file")

anovick18:05:58

oh I thought it was gonna send that verbatim to the shell for execution directly

anovick18:05:10

but it's your library and you designed it certain way

anovick18:05:15

and I'm only the consumer

anovick18:05:21

so I need to read your docs to understand how to use it

borkdude18:05:30

plz read the docs? :)

anovick18:05:05

Thank you for such great explanation!! very much appreciate you ❤️

chromalchemy05:05:09

Thx for shelling out tutorial!

jmv17:05:38

is this a pattern that i should expect to work? this currently fails

user> (spit (fs/create-temp-file) "foo")
this works, but it reads awkwardly to convert a file to a file
user> (spit (.toFile (fs/create-temp-file)) "foo")

borkdude17:05:42

@UDXEK491P

(spit (fs/file (fs/create-temp-file)) "foo")
Yes, unfortunately clojure 1.11 does not support java.nio.file.Path in spit, but this might change in 1.12 (cc @U064X3EF3 😉 )

🙏 4
💡 6
💯 4
jmv17:05:11

ah i missed that function, ty!

jmv17:05:26

and that is great to hear