Fork me on GitHub
#babashka
<
2020-07-31
>
borkdude07:07:37

@cfleming babashka tries to be as compatible as possible with normal clojure, so you can run scripts with both the JVM or babashka. So by design there's no easy way to distinguish just by looking at the code, like @isak has also said correctly

cfleming07:07:25

Ok, thanks. I’ll need some way to mark files as babashka scripts in the IDE, then.

borkdude07:07:20

Why is that necessary for Cursive and not say for CIDER, Chlorine, Calva and all the other IDEs that support babashka?

borkdude08:07:38

If I can do something to make it easier for you, let me know.

cfleming08:07:52

Because Cursive uses static analysis, so it has to understand the dependencies that are available. These are normally provided by the containing project, but in babashka’s case they’re implicitly available, right?

borkdude08:07:08

Ah yes. That's true!

cfleming08:07:44

So I’ll need some way of knowing which deps babashka has available, and somehow providing them for these scripts.

cfleming08:07:10

I assume the ones that are available are from babashka’s deps.edn, correct? But that’s not actually published anywhere like Clojars?

borkdude08:07:50

The project.clj is probably the best source of truth. The available namespaces are listed here: https://github.com/borkdude/babashka#usage You can also see them with:

bb -e '(map ns-name (all-ns))'

borkdude08:07:22

Many babashka.* namespaces are also available on clojars, like babashka.curl and babashka.pods

borkdude08:07:41

I see the documentation is slightly out of date and e.g. babashka.curl is not mentioned in the list. I'll do something about that.

cfleming08:07:58

Yeah, I’d need the actual dependencies rather than the namespaces. Although I might be able to do something similar to Cursive’s existing stubs functionality, which introspects namespaces and creates function stubs which Cursive uses.

cfleming08:07:35

That’s probably the best solution, now that I think about it.

borkdude08:07:49

Yeah, maybe it is

borkdude08:07:16

bb supports all-ns, ns-interns etc, so it's pretty easy to write out some edn file with stubs

cfleming08:07:36

Ok, great. That should be pretty easy then.

cfleming08:07:48

I’ll investigate how to make that available.

tvaughan13:07:35

Is it possible to use java.nio.charset? I have:

(ns random-string
  (:require
   [clojure.edn :as edn]
   [clojure.string :as str])
  (:import
   (java.nio.charset
    Charset)))
Which produces java.lang.Exception: Unable to resolve classname: java.nio.charset.Charset. I don't see this included in https://github.com/borkdude/babashka/blob/master/src/babashka/impl/classes.clj#L143. I'm using the latest bb as provided by brew

borkdude13:07:05

I guess we could add it. What are you using it for?

tvaughan13:07:07

To create strings of random characters. What I need is a list of all alphanumeric characters. What I have is:

(defn alphanum
  [charset-name]
  (let [ce (-> charset-name Charset/forName .newEncoder)]
    (->> (range 0 (int Character/MAX_VALUE)) (map char)
         (filter #(and (.canEncode ce %) (Character/isAlphanumeric %))))))
I'm definitely open to other solutions

borkdude13:07:24

I know that this library works with bb: https://github.com/clojure/data.generators You could maybe take a peek at how they do it first

borkdude13:07:51

btw,

user=> #(Character/isAlphanumeric %)
Syntax error (IllegalArgumentException) compiling . at (REPL:1:2).
No matching method isAlphanumeric found taking 1 args for class java.lang.Character
that might be a typo?

tvaughan13:07:51

Could be 😞

tvaughan13:07:11

(def ^:dynamic ^java.util.Random
     *rnd*
     "Random instance for use in generators. By consistently using this
instance you can get a repeatable basis for tests."
     (java.util.Random. 42))

(defn uniform
  "Uniform distribution from lo (inclusive) to hi (exclusive).
   Defaults to range of Java long."
  (^long [] (.nextLong *rnd*))
  (^long[lo hi] {:pre [(< lo hi)]}
         (long (Math/floor (+ lo (* (.nextDouble *rnd*) (- hi lo)))))))

(defn printable-ascii-char
  "Returns a char based on *rnd* in the printable ascii range."
  []
  (char (uniform 32 127)))

(print (printable-ascii-char))
(print (printable-ascii-char))
(print (printable-ascii-char))
This does what I need. Thanks!

borkdude13:07:57

Someone just published a Babashka Github action: https://github.com/marketplace/actions/babashka-clojure

👀 1
👏 2
onetom15:07:52

armed with the knowledge about ProcessBuilder - which I just gained from the babashka readme! thx, @borkdude! 🙂 - i thought i can just play with a datomic peer-server, directly from a REPL, like this:

(def peer-server-opts
    ["-h" "localhost"
     "-p" "8998"
     "-a" "myaccesskey,mysecret"
     "-d" "hello,datomic:"])

  (def peer-server-process
    ^java.lang.Process
    (-> (into ["CURRENT/bin/run" "-m" "datomic.peer-server"] peer-server-opts)
        (doto println)
        (ProcessBuilder.)
        (.inheritIO)
        (.start)))
to my surprise, (.destroy peer-server-process) only kills the run shell script, but not the java process started by it! so i went on a quest to see how can i send the equivalent of Ctrl-C to the peer-server-process. typically very helpful sites couldn't answer this: https://www.baeldung.com/java-lang-processbuilder-api https://www.baeldung.com/java-9-process-api searched more and i found... nothing, but endless questions for decades, with OS specific answers, to the effect of "just shell out to run kill -SIGINT <pid>" like this and a bunch more similar conversations: https://coderanch.com/t/676288/java/Wait-kill-interrupt-process-complete there was one interesting variant, which was about trying to send byte 0x03 to the stdin of the process, to simulate Ctrl-C, which is the 3rd letter in the ABC, right? https://garethrees.org/2015/08/07/ping/ but that assumes that the process is somehow listening to stdin and indeed interprets the byte 3 as ctrl-c and in turn sends a SIGINT to the process, which then also must have a handler assigned to it and handle it as process exit... (i haven't tried this yet, but because it feels a little hacky and i hope there is some more generic/official/idiomatic solution) so am i missing something? am i asking the wrong question? how can i stop that peer-server scripts the same way as pressing ctrl-c would in a terminal?

onetom15:07:46

this btw works, but relies on this very specific situation:

(.destroy (first (iterator-seq (.iterator (.children ^java.lang.Process peer-server-process)))))

borkdude15:07:18

I start java processes all the time. E.g. this is in our dev script for work:

(defn less []
  (let [cmd ["clojure" "-A:frontend:less/dev"]
        pb (doto (ProcessBuilder. cmd)
             (.redirectOutput ProcessBuilder$Redirect/INHERIT)
             (.redirectError ProcessBuilder$Redirect/INHERIT))
        proc (.start pb)]
    (-> (Runtime/getRuntime)
        (.addShutdownHook (Thread. #(.destroy proc))))
    proc))
and it kills the java process reliably.

borkdude15:07:54

Not sure if it will make a different for your script, but worth a try.

onetom15:07:36

i want to initiate the shutdown manually from the repl session, not on the exit hook of the repl session

borkdude15:07:02

yeah, ok. not sure what's going on then

onetom15:07:11

because i want to start the peer server with different -d parameters, exposing different peers

borkdude15:07:31

Maybe you can do it via some PID and kill

borkdude15:07:38

pgrep, pkill, etc

onetom15:07:07

yeah, but that's platform specific. i was hoping to be fully platform agnostic

borkdude15:07:12

What does the peer script look like btw? Does it fork the process into the background or something?

onetom15:07:28

it's just the stock datomic run script:

#!/bin/bash
cd `dirname $0`/..

while [ $# -gt 0 ]
do
    case "$1" in
        -X*)
            java_opts="${java_opts} $1"
            ;;
        -D*)
            java_opts="${java_opts} $1"
            ;;
        *) 
            clojure_args="${clojure_args} $1"
            ;;
    esac
    shift
done

/usr/bin/env java -server -Xmx1g -Xms1g $DATOMIC_JAVA_OPTS ${java_opts} -cp `bin/classpath` clojure.main -i "bin/bridge.clj" ${clojure_args}
pretty generic wrapper script

borkdude15:07:52

Right. I don't have an explanation for this. Seems pretty standard to me.

borkdude15:07:07

Maybe -server does something weird?

onetom15:07:09

if that last line would just be an exec /usr/bin/env, that might make it work, but regardless i want to know how to "simulate a ctrl-c" because that's a pretty common use-case

borkdude15:07:26

So how to send a control C to the process?

onetom15:07:58

if im lucky, sending a 0x3 to its stdin might work, but i can't believe that java hasn't exposed the signal sending api of libc 😕

onetom15:07:36

and see what does the comment to that answer say: > That seems to send a SIGTERM signal, not SIGINT. See this question here: http://stackoverflow.com/questions/2950338/… – Lukas Eder Oct 20 '11 at 11:58

borkdude15:07:57

you could try emulating that from your shell, kill -SIGTERM the script

borkdude15:07:03

and then see what happens to the java process

onetom15:07:21

true, just to confirm that indeed that's what's happening, right?

borkdude15:07:45

I don't know the implementation details of java.lang.Process, but they say so 😉

borkdude15:07:28

public abstract void destroy()
Kills the subprocess. Whether the subprocess represented by this Process object is forcibly terminated or not is implementation dependent.

onetom15:07:15

does that mean jvm implementation dependent or what implementation?

borkdude15:07:25

I think they might mean OS dependent, not sure

borkdude15:07:07

What are you running this on?

onetom15:07:00

macOS at the moment but it's expected to work on linux too

onetom15:07:54

im using nix to make sure that my base utilities are gnu not bsd, so i can rely on that to have the same kill command, but only if i make sure im running kill via a nix-shell...

borkdude15:07:35

yeah. you said destroying all .children also worked?

onetom15:07:15

i just destroyed the 1st one, because there was only one 🙂 but yes that worked

onetom15:07:53

im promoting babashka in the company to adopt it for replacing makefiles and bash scripts

borkdude15:07:32

Ah I see. They added children in java 11! https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/Process.html This may work indeed. I'd never seen that before.

onetom15:07:32

so i want to be knowledgeable about it and armed to deal with these common cases

borkdude15:07:57

It also has destroyForcibly()

borkdude15:07:00

worth a shot

onetom15:07:14

in case of a shell script, i have no choice, i must rely on kill, which is btw often a shell builtin, eg:

➜ which kill
kill: shell built-in command

onetom15:07:32

the promise of babashka is to free people from these complexities, isn't it?

onetom15:07:06

i tried destroyForcibly too, that's why i have the type annotation in my examples; to allow autocompletion in cursive 🙂 but let me try it again

borkdude15:07:00

babashka doesn't promise anything about freeing people from complexity, it just brings Clojure with fast startup time, comparable to bash, with a couple of built-in libraries.

onetom15:07:39

➜ pstree | grep datomic.peer

 | | \-+= 17326 onetom /bin/bash CURRENT/bin/run -m datomic.peer-server -h localhost -p 8998 -a myaccesskey,mysecret -d hello,datomic:
 | |   \--- 17330 onetom java -server -Xmx1g -Xms1g -cp resources:datomic-transactor-pro-1.0.6165.jar:lib/*:samples/clj:bin: clojure.main -i bin/bridge.clj -m datomic.peer-server -h localhost -p 8998 -a myaccesskey,mysecret -d hello,datomic:

➜ kill -TERM 17326

➜ pstree | grep datomic.peer

 \--- 17330 onetom java -server -Xmx1g -Xms1g -cp resources:datomic-transactor-pro-1.0.6165.jar:lib/*:samples/clj:bin: clojure.main -i bin/bridge.clj -m datomic.peer-server -h localhost -p 8998 -a myaccesskey,mysecret -d hello,datomic:
just to confirm that SIGTERM leads to this detached java server process

borkdude15:07:59

good, so at least it's reproducable

borkdude15:07:47

can you try without -server to see if that makes a difference?

onetom15:07:43

same result

borkdude15:07:24

maybe the java process is winding down but it takes a while?

onetom15:07:40

no, it stays like that forever.

onetom15:07:51

if i add an exec in front of the /usr/bin/env then the shell process is replaced by the java process and i can just use (.destroy peer-server-process) BUT that assumes i have some code everywhere where im deploying the datomic distribution, which patches this script, so we just shifted the complexity then 🙂

borkdude15:07:47

Btw, I noticed that bb doesn't have iterator-seq yet.

borkdude15:07:55

Did you also notice this, or am I missing something?

borkdude15:07:45

Can be easily added in the next version, just checking

onetom15:07:51

im trying this from clojure first because i had some datomic.client.api code prepared there which i can use to test the started peer-server

borkdude15:07:18

ok. I'll add it now

onetom15:07:45

i have no opinion about iterator-seq or enumeration-seq or the likes, because this is the first time i had to use it actually... i didn't have to do much java interop in the past 5 years im using clojure

borkdude15:07:22

I'll add it anyway

onetom15:07:52

what's your decision process for deciding what goes into babashka from clojure.core? or is that documented in the development section of the docs?

borkdude15:07:36

clojure.core public vars should be there mostly for completeness.

onetom15:07:22

i see. there is a resultset-seq right next to these other 2 functions

onetom15:07:51

that's something i needed (in clojure) before but was not aware it existed

onetom15:07:36

for the record, i've just checked again, and .destroyForcibly also just kills the bash process, not the java one started from it.

borkdude16:07:44

Made an issue to make this work in babashka: https://github.com/borkdude/babashka/issues/519

👍 1
borkdude16:07:37

Do you know a good way to create a process with children?

./bb '(run! #(prn (.pid %)) (iterator-seq (.iterator (.children (.start (let [pb (ProcessBuilder. ["bash" "-c" "/usr/bin/env clojure"])] (Thread/sleep 3000) pb))))))'
This returns an empty stream.

onetom16:07:09

im experimenting with this now:

(def process-to-stop
    (-> (ProcessBuilder.
          ["bash" "-c"
           "echo sleep 20; (sleep 20 && echo done 20) &
            echo sleep 10; sleep 10; echo done 10"])
        (.inheritIO)
        (.start)))

onetom16:07:24

a more complete example:

(defn pstree [grep-for]
    (->> (:out (clojure.java.shell/sh "bash" "-c" "pstree"))
        (str/split-lines)
        (filter #(str/includes? % grep-for))
        (map #(subs % 0 (min 60 (count %))))
        (run! println)))

  (defn spawn []
    (-> (ProcessBuilder.
          ["bash" "-c"
           "echo sleep 20; (sleep 20 && echo done 20) &
            echo sleep 10; sleep 10; echo done 10"])
        (.inheritIO)
        (.start)))
  
  (defn stop-children [proc]
    (run! #(.destroy %) (iterator-seq (.iterator (.children proc)))))

  (pstree "sleep")
  (def proc (spawn))
  (pstree "sleep")
  (stop-children proc)
  (pstree "sleep")

borkdude16:07:51

With this script:

(let [depth (System/getenv "DEPTH")]
  (prn depth)
  (when-not (= "4" depth)
    (let [cmd ["bb" "child.clj"]
          pb (doto (ProcessBuilder. cmd)
               (.inheritIO))
          _ (.put (.environment pb) "DEPTH"
                  (str (inc (Integer. (or depth
                                          0)))))
          proc (.start pb)]
      (if-not depth ;; the top process
        (do
          (Thread/sleep 500)
          (prn (mapv #(.pid %) (iterator-seq (.iterator (.descendants proc))))))
        (Thread/sleep 10000)))))
I get to see:
nil
"1"
"2"
"3"
"4"
[58990 58991]
with some modification in a branch process-children

borkdude16:07:58

I wonder why I only see two descendants

onetom16:07:32

so this shell script is expected to keep the backgrounded sleep 20 process running after Ctrl-C:

#!/usr/bin/env bash

echo sleep 20; (sleep 20 && echo done 20) &
echo sleep 10; sleep 10; echo done 10
so it's not a good example:
➜ chmod +x sigint-test.sh 

➜ ./sigint-test.sh 
sleep 20
sleep 10
^C

➜ pstree | rg sleep
 | | \--- 22861 onetom rg sleep
   \--- 22768 onetom sleep 20

➜ done 20

onetom16:07:28

try to print the pstree along the way.

borkdude16:07:09

ok, brew installing it

borkdude16:07:18

|-+- 59113 borkdude bb child.clj
 | \-+- 59114 borkdude bb child.clj
 |   \--- 59115 borkdude bb child.clj
 \-+- 59152 borkdude bb child.clj
   \-+- 59153 borkdude bb child.clj
     \--- 59154 borkdude bb child.clj
but prints:
[59153 59154]

borkdude16:07:53

anyway, the interop works

borkdude16:07:58

and the output is the same as in clojure

onetom16:07:52

the elements returned by the iterator-seq are java.lang.ProcessHandleImpl objects, so you would need to call .descendats on those too.

onetom16:07:15

but whatever. enough java for today :)

onetom16:07:54

thanks for the co-thinking session!

onetom17:07:30

> ok, brew installing it you should just nix-env -i pstree it ;) or even call it as nix-shell -p pstree --run pstree and it would download it on first use on any machine (which has nix installed)

onetom17:07:11

i don't have clojure on my path by default, so if im in a directory outside of my clojure projects, i always run clojure as `nix-shell -p clojure --run 'clj ...' this makes me very conscious about which version of clojure im using! the answer to that is "who knows..." which is exactly the same in case of a brew install clojure. which version is it? "whatever is on the internet at that moment"

onetom17:07:03

the projects im working on professionally, always have a pinned package tree - like i showed the other day - so it's guaranteed im getting the same clojure version on every machine

borkdude17:07:26

yes, I knew how to trigger you with brew, didn't I 😉

🙂 1
onetom17:07:55

i was moving the current company over to nix (and clojure and now babashka 😉 in the past few month, so it became a reflex to take note of this

borkdude17:07:53

I might seriously take a stab at this at one point 😉

onetom17:07:46

if it weren't for nix, i wouldn't be able to keep up with the latest clojure cli versions so easily and immediately make it available to all my colleagues. all they have to do is git pull then press an extra enter and the new clojure version is gets downloaded. no extra communication needed!

onetom17:07:00

literally this is the git commit for an upgrade:

index a2c3ce138..2b5ea1f12 100644
--- a/deps/clojure/default.nix
+++ b/deps/clojure/default.nix
@@ -2,11 +2,11 @@
 
 stdenv.mkDerivation rec {
   pname = "clojure";
-  version = "1.10.1.561";
+  version = "1.10.1.590";
 
   src = fetchurl {
     url = "";
-    sha256 = "0baf23npillrf7sy5piwb4brgv3cb1ij832kbq79rklgkz897ha6";
+    sha256 = "18x8xkxsqwnv3k1mf42ylfv7zzjllm7yiagq16b2gkq62j5sm1k7";
   };
 
   nativeBuildInputs = [
and that sha256 is the part which answers the "which version is it" part with extreme precision.

borkdude17:07:17

This seems to work fine now, it prints 4 pids:

(defn pids [^java.lang.ProcessHandle x]
  (distinct (cons (.pid x) (mapcat pids (iterator-seq (.iterator (.descendants x)))))))

borkdude17:07:27

The entire script:

(defn pids [^java.lang.ProcessHandle x]
  (distinct (cons (.pid x) (mapcat pids (iterator-seq (.iterator (.descendants x)))))))

(let [depth (System/getenv "DEPTH")]
  (when-not (= "4" depth)
    (let [cmd ["bb" "child.clj"]
          pb (doto (ProcessBuilder. cmd)
               (.inheritIO))
          _ (.put (.environment pb) "DEPTH"
                  (str (inc (Integer. (or depth
                                          0)))))
          proc (.start pb)]
      (if-not depth ;; the top process
        (do
          (Thread/sleep 100)
          (prn (pids (.toHandle proc))))
        (Thread/sleep 100000000)))))

👍 1
borkdude14:08:51

Added a unit test now in the branch: https://github.com/borkdude/babashka/commit/a7b3224a28e99d985c4944db8f18457a01ad57aa This will be available in #babashka_circleci_builds in a few minutes. java.util.List will also be in there.

borkdude14:08:53

@U086D6TBN There's no urgency to make a release from my side, but I can do a release whenever you need it.

onetom14:08:16

no need, thx

onetom14:08:59

now that i know that u have these ci builds available; that already helps. but if (or rather when) we adopt bb at work, we will make sure that we can build it ourselves too, preferably with nix and then i wont bother you this much 🙂

borkdude14:08:59

You can bother me for improvements to bb anytime 😉

borkdude14:08:49

Merged to master now

onetom14:08:01

i was just looking those new tests. you mixed a lot of concerns into one test. i also can't see clearly the intent behind the names like , handles and run.

onetom14:08:03

i might comment on your commits to share my concerns, but i want you to know that im not expecting answers necessarily, neither do i expect to reach any agreement. i just just hope if u have time, u might consider them, but even if not, other bb developers later have a chance to understand the source better (if github is still around by then :)

borkdude14:08:18

http://child.bb -> killing child processes handles -> process handles (ProcessHandle is a class) run -> I needed to make this a function to trigger the reflection problem you had

borkdude14:08:57

> if github is still around by then in what type of dystopian future do you believe? 😉

borkdude14:08:52

I'll add a testing around the test with some explanation

borkdude14:08:35

a lot of tests are just a bunch of code proving by example that the interop works as expected in the binary image

onetom15:08:27

well, i live in hong kong, so i don't have to go far to even experience elements of dystopia... 😉

onetom15:08:54

and im not saying i don't understand what do those names refer to, but i only understand it because i was involved directly what that code was made. some one else later, will not have this context. for their sake, you should have better names, for example the way u explained those names can be the names themselves. like killing_child_ or more like `spawning_child_http://processes.bb handles can be process-handles-of-all-decendants-recursively it's stupidly long, but since you are only calling it once, why not? it's very explanatory. alternatively u can put it into a function docstring...

onetom15:08:42

and run can also be something less generic, like start-spawning

borkdude15:08:02

I agree. If you want, you may submit a PR with those improvements

borkdude15:08:08

as a heads up, you need to clone babashka with --recursive, here are the instructions: https://github.com/borkdude/babashka/blob/master/doc/dev.md#clone-repository

👍 1
borkdude15:07:14

I think this problem was mentioned before here. Might be worth searching the Zulip archives for this channel: https://clojurians.zulipchat.com/#narrow/stream/180378-slack-archive/topic/babashka/search/kill Not sure if there was a proper solution

👍 1
borkdude17:07:43

tl;dr for folks wanting to kill all subprocesses:

(defn handles [^java.lang.ProcessHandle x]
  (distinct (cons x (mapcat handles (iterator-seq (.iterator (.descendants x)))))))

(run! #(.destroy %) (handles (.toHandle proc)))

👍 1
borkdude17:07:57

The necessary classes will be part of the next bb

borkdude18:07:41

It already works with the new binaries in the #babashka_circleci_builds channel, so testing is already possible

onetom19:07:48

on a 2nd thought, i googled for java bash and signals and this came up: http://veithen.io/2014/11/16/sigterm-propagation.html which kinda suggests that leaving out bash from the equation is the best course of action probably :)

borkdude20:07:24

Interesting read. So what's your next course of action now, run the java process directly with bb and skip the bash script altogether?

onetom20:07:05

something like that. currently im experimenting with various ways to connect to datomic on-prem, so i need both the peer and the client api in the same process, which means i have to use clojure to get access to those. but i also need things like wait/wait-for-port, which i just quickly copied over from babashka's source.

onetom20:07:17

this is what i have so far:

(defn datomic-classpath []
  (str/trim (:out (shell/sh "CURRENT/bin/classpath" :dir (io/file "CURRENT")))))

(defn ^List peer-server-cmd [opts]
  (into ["/usr/bin/env" "java" "-server" "-Xmx1g" "-Xms1g"
         "-cp" (datomic-classpath)
         "clojure.main" "-i" "bin/bridge.clj"
         "-m" "datomic.peer-server"]
        opts))

(defn ^Process make-process [cmd]
  (println cmd)
  (doto (ProcessBuilder. ^List cmd)
      (.directory (io/file "CURRENT"))
      ;(.redirectOutput ProcessBuilder$Redirect/INHERIT)
      ;(.redirectError ProcessBuilder$Redirect/INHERIT)
      (.inheritIO)))

onetom20:07:20

we (or rather i?) decided literally only 2 days ago that the time has come to try using babashka. to help my colleagues to get going, i want to make some examples. my current work need clojure but i deliberately target parts of it for babashka. that's why i was asking about aws integration the other day. for example, we are using ssm to get rds credentials and for that i turned an aws cli invocation into clojure a few months ago and used that as a reference implementation for the clojure implementation, which was using the cognitect.aws-api:

(defn cli-ssm-get-param-via-profile [profile param]
  (-> (sh "direnv" "exec" "."
          "aws" "ssm" "get-parameter"
          "--name" param
          "--query" "Parameter.Value"
          "--with-decryption"
          "--output" "text"
          "--region" "ap-southeast-1"
          "--profile" profile)
      :out
      str/trim-newline))