All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.github.jlangch.venice.docker.venice Maven / Gradle / Ivy

There is a newer version: 1.12.34
Show newest version
;;;;   __    __         _
;;;;   \ \  / /__ _ __ (_) ___ ___
;;;;    \ \/ / _ \ '_ \| |/ __/ _ \
;;;;     \  /  __/ | | | | (_|  __/
;;;;      \/ \___|_| |_|_|\___\___|
;;;;
;;;;
;;;; Copyright 2017-2024 Venice
;;;;
;;;; Licensed under the Apache License, Version 2.0 (the "License");
;;;; you may not use this file except in compliance with the License.
;;;; You may obtain a copy of the License at
;;;;
;;;;     http://www.apache.org/licenses/LICENSE-2.0
;;;;
;;;; Unless required by applicable law or agreed to in writing, software
;;;; distributed under the License is distributed on an "AS IS" BASIS,
;;;; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
;;;; See the License for the specific language governing permissions and
;;;; limitations under the License.

;;;; Docker utilities

;;;; See:  https://docs.docker.com/engine/reference/commandline/

;;;; Most used commands
;;;; ---------------------------------------------------------------------------
;;;; Images     (println (docker/images :format :table))
;;;;            (println (docker/image-pull "arangodb/arangodb:3.10.10"))
;;;;            (println (docker/image-rm "184e47dd1c58"))
;;;;            (println (docker/image-prune :all true))
;;;;
;;;; Containers

;;;; Volumes:  https://forums.docker.com/t/how-to-access-docker-volume-data-from-host-machine/88063

(ns docker)

(load-module :jsonl)

(import :com.github.jlangch.venice.ShellException)


(def-dynamic *debug* :off)

(defonce binary (system-prop :docker.binary "docker"))


(defn
  ^{ :arglists '("(docker/debug mode)")
     :doc """
          Sets the debugging mode.

          Without argument returns the current debug mode.

          Mode:

          | [![width: 20%]] | [![width: 80%]] |
          | :off        | No debug output |
          | :on         | Prints the raw docker command line to the current \
                          stdout channel ahead of running the command |
          | :on-no-exec | Prints the raw docker command line to the current \
                          stdout channel without running the command |
          """
     :examples '(
          "(docker/debug :on)"
          "(docker/debug :on-no-exec)"
          "(docker/debug :off)") }

  debug

  ([] docker/*debug*)

  ([mode]
    (assert (contains? #{:off :on :on-no-exec} mode)
            "A debug mode must be one of {:off, :on, :on-no-exec}")
    (set! docker/*debug* mode)))


(defn
  ^{ :arglists '("(docker/version & options)")
     :doc """
          Returns the Docker version.

          Options:

          | [![width: 20%]] | [![width: 70%]] |
          | :format f  | Returns the output either as a stringor as JSON data. \
                         The format is one of {:string, :json} |
          | :version v | Returns full (default), server, or client version. \
                         The version is one of {:full, :server, :client} |
          """
     :examples '(
          "(docker/version)"
          "(docker/version :version :client)"
          "(docker/version :version :server)"
          "(docker/version :format :json)"
          "(println (docker/version :format :string))" )
     :see-also '(
          "docker/images"
          "docker/run" )
     :spec { :options { :format  [:optional #{:string, :json}]
                        :version [:optional #{:full, :server, :client}] } } }

  version [& options]

  (let [opts     (apply hash-map options)
        _        (validate-map "option" opts (-> docker/version meta :spec :options))
        format   (:format opts nil)
        version  (:version opts :full)]
    (case version
      :full   (as-> (if (= format :json) "version -f json" "version") $
                    (docker/cmd $)
                    (:out $)
                    (parse-output format $))
      :server (as-> (docker/version :version :full :format :json) $
                    (get-in $ ["Server" "Version"]))
      :client (as-> (docker/version :version :full :format :json) $
                    (get-in $ ["Client" "Version"])))))

(defn version* [] (println (version :version :client)))



;; -----------------------------------------------------------------------------
;; Image functions
;;   - docker/images
;;   - docker/rmi
;;   - docker/image-rm
;;   - docker/image-pull
;;   - docker/image-prune
;; -----------------------------------------------------------------------------

(defn
  ^{ :arglists '("(docker/images & options)")
     :doc """
          List images.

          Options:

          | :all {true, false}      | Show all images (default hides \
                                      intermediate images) |
          | :digests {true, false}  | Show digests |
          | :quiet {true, false}    | If true only display image IDs |
          | :no-trunc {true, false} | Don't truncate output |
          | :format f               | Returns the output either as a table \
                                      string or as JSON data. \
                                      The format is one of{:table, :json} |
          """
     :examples '(
          "(println (docker/images :format :table))"
          "(docker/images :quiet true :no-trunc true :format :json)"
          "(println (docker/images :format :json))" )
     :see-also '(
          "docker/image-pull"
          "docker/rmi"
          "docker/image-rm"
          "docker/image-prune"
          "docker/run"
          "docker/images-query-by-repo"
          "docker/image-ready?")
     :spec { :options { :all      [:optional #{true, false}]
                        :digests  [:optional #{true, false}]
                        :quiet    [:optional #{true, false}]
                        :no-trunc [:optional #{true, false}]
                        :format   [:optional #{:table, :json}] } } }

  images [& options]

  (let [opts     (apply hash-map options)
        _        (validate-map "option" opts (-> docker/images meta :spec :options))
        all      (:all opts false)      ;; (if all "--all" nil)  ;; hangs on MacOSX
        digests  (:digests opts false)
        quiet    (:quiet opts false)
        no-trunc (:no-trunc opts false)
        format   (:format opts :json)
        cmdargs* ["images"]
        cmdargs* (if digests (conj cmdargs* "--digests") cmdargs*)
        cmdargs* (if quiet (conj cmdargs* "--quiet") cmdargs*)
        cmdargs* (if no-trunc (conj cmdargs* "--no-trunc") cmdargs*)
        cmdargs* (into cmdargs* ["--format" (name format)])]
    (->> (apply docker/cmd cmdargs*)
         (:out)
         (parse-output format))))

(defn images* [] (println (docker/images :format :table)))


(defn
  ^{ :arglists '("(docker/rmi image & options)")
     :doc """
          Remove an image.

          Images can be removed by name, name and tag, or image id

          Options:

          | :force {true, false}    | Force removal of the image |
          | :no-prune {true, false} | Do not delete untagged parents |
          """
     :examples '(
          """
          (println (docker/rmi "arangodb/arangodb:3.10.10" :force true))
          """ )
     :see-also '(
          "docker/images"
          "docker/image-pull"
          "docker/image-rm"
          "docker/image-prune" )
     :spec { :options { :force    [:optional #{true, false}]
                        :no-prune [:optional #{true, false}] } } }

  rmi [image & options]

  { :pre [(string? image)] }

  (let [opts     (apply hash-map options)
        _        (validate-map "option" opts (-> docker/rmi meta :spec :options))
        force    (:force opts false)
        no-prune (:no-prune opts false)
        cmdargs* ["rmi"]
        cmdargs* (if force (conj cmdargs* "--force") cmdargs*)
        cmdargs* (if no-prune (conj cmdargs* "--no-prune") cmdargs*)
        cmdargs* (conj cmdargs* image)]
    (:out (apply docker/cmd cmdargs*))))

(defn rmi* [image] (println (docker/rmi :force true)))


(defn
  ^{ :arglists '("(docker/image-pull name & options)")
     :doc """
          Download an image from a registry.

          Images can be pulled by name, name and tag, or digest

          Returns the stdout text from the command.

          Options:

          | :quiet {true, false} | Suppress verbose output |
          """
     :examples '(
          """
          (println (docker/image-pull "arangodb/arangodb:3.10.10"))
          """,
          """
          (println (docker/image-pull "arangodb/arangodb"))
          """ )
     :see-also '(
          "docker/images"
          "docker/rmi"
          "docker/image-rm"
          "docker/image-prune" )
     :spec { :options { :quiet [:optional #{true, false}] } } }

  image-pull [name & options]

  { :pre [(string? name)] }

  (let [opts     (apply hash-map options)
        _        (validate-map "option" opts (-> docker/image-pull meta :spec :options))
        quiet    (:quiet opts false)
        cmdargs* ["image" "pull"]
        cmdargs* (if quiet (conj cmdargs* "--quiet") cmdargs*)
        cmdargs* (conj cmdargs* name)]
    (:out (apply docker/cmd cmdargs*))))


(defn
  ^{ :arglists '("(docker/image-rm image)")
     :doc """
          Remove an image.
          """
     :examples '(
          """
          (println (docker/image-rm "184e47dd1c58"))
          """ )
     :see-also '(
          "docker/images"
          "docker/image-pull"
          "docker/rmi"
          "docker/image-prune" ) }

  image-rm [image]

  { :pre [(string? image)] }

  (let [cmdargs* ["image" "rm" image]]
    (:out (apply docker/cmd cmdargs*))))


(defn
  ^{ :arglists '("(docker/image-prune & options)")
     :doc """
          Remove unused images.

          If `:all true` is specified, will also remove all images not
          referenced by any container. This is what you usually expect

          Returns the stdout text from the command.

          Options:

          | :all {true, false} | Remove all unused images, not just dangling ones |
          """
     :examples '(
          "(println (docker/image-prune))"
          "(println (docker/image-prune :all true))" )
     :see-also '(
         "docker/images"
         "docker/image-pull"
         "docker/rmi"
         "docker/image-rm" )
    :spec { :options { :all [:optional #{true, false}] } } }

  image-prune [& options]

  (let [opts     (apply hash-map options)
        _        (validate-map "option" opts (-> docker/image-prune meta :spec :options))
        all      (:all opts false)
        cmdargs* ["image" "prune" "--force"]  ;; Do not prompt for confirmation
        cmdargs* (if all (conj cmdargs* "--all") cmdargs*)]
    (:out (apply docker/cmd cmdargs*))))



;; -----------------------------------------------------------------------------
;; Container functions
;;   - docker/run
;;   - docker/ps
;;   - docker/start
;;   - docker/stop
;;   - docker/rm
;;   - docker/prune
;;   - docker/exec
;;   - docker/cp
;;   - docker/diff
;;   - docker/pause
;;   - docker/unpause
;;   - docker/cp
;;   - docker/logs
;; -----------------------------------------------------------------------------

(defn
  ^{ :arglists '("(docker/run image & options)")
     :doc """
          Create and run a new container from an image.

          Images can be run by name, name and tag, or image id

          Options:

          | [![width: 30%]]         | [![width: 70%]] |
          | :detach {true, false}   | Run container in background and return \
                                      container ID |
          | :attach s               | Attach to STDIN, STDOUT or STDERR. \
                                      Use one of {:stdin, :stdout, :stderr} |
          | :publish port           | Publish a container's port to the host. \
                                      To expose port 8080 inside the container \
                                      to port 3000 outside the container, \
                                      pass "3000:8080" |
          | :envs vars              | Set environment variable (a sequence of \
                                      env var defs) |
          | :memory limit           | Memory limit |
          | :name name              | Assign a name to the container |
          | :quiet {true, false}    | Suppress the pull output |
          | :volumes vol            | Bind mount a volume (a sequence of \
                                      volume defs)|
          | :workdir dir            | Working directory inside the container |
          | :args args              | Arguments passed to container process \
                                      (a sequence of args or a string) |

          See also `cargo/start` / `cargo/stop` for a smarter way to start/stop
          a container.
          """
     :examples '(
          """
          ;; Run an ArangoDB container (use bind mounts, very slow on macOSX)
          (docker/run "arangodb/arangodb:3.10.10"
                      :name "myapp"
                      :publish [ "8529:8529" ]
                      :detach true
                      :envs ["ARANGO_ROOT_PASSWORD=xxxxxx"
                             "ARANGODB_OVERRIDE_DETECTED_TOTAL_MEMORY=8G"
                             "ARANGODB_OVERRIDE_DETECTED_NUMBER_OF_CORES=1"]
                      :volumes ["/Users/foo/arangodb/db:/var/lib/arangodb3"
                                "/Users/foo/arangodb/apps:/var/lib/arangodb3-apps"])
          """,
          """
          ;; Run an ArangoDB container (use docker volume, faster than bind mount)
          (do
            (docker/volume-create "arangodb-db")
            (docker/volume-create "arangodb-apps")
            (docker/run "arangodb/arangodb:3.10.10"
                        :name "myapp"
                        :publish [ "8529:8529" ]
                        :detach true
                        :envs ["ARANGO_ROOT_PASSWORD=xxxxxx"
                               "ARANGODB_OVERRIDE_DETECTED_TOTAL_MEMORY=8G"
                               "ARANGODB_OVERRIDE_DETECTED_NUMBER_OF_CORES=1"]
                        :volumes ["arangodb-db:/var/lib/arangodb3"
                                  "arangodb-apps:/var/lib/arangodb3-apps"]
                        :args ["--database.auto-upgrade"]))
          """ )
     :see-also '(
          "cargo/start"
          "docker/images" "docker/ps"
          "docker/start"  "docker/stop"
          "docker/rm"     "docker/prune"
          "docker/exec"   "docker/cp"   "docker/diff"
          "docker/pause"  "docker/unpause"
          "docker/cp"     "docker/logs"
          "docker/container-find-by-name"
          "docker/container-exists-with-name?"
          "docker/container-running-with-name?"
          "docker/container-start-by-name"
          "docker/container-stop-by-name"
          "docker/container-remove-by-name"
          "docker/container-status-by-name"
          "docker/container-exec-by-name"
          "docker/container-logs"
          "docker/container-purge-by-name"
          "docker/container-image-info-by-name" )
     :spec { :options { :detach  [:optional #{true, false}]
                        :attach  [:optional #{:stdin, :stdout, :stderr}]
                        :publish [:optional #(and (vector? %) (every? string? %))]
                        :envs    [:optional #(and (vector? %) (every? string? %))]
                        :memory  [:optional #(string? %)]
                        :name    [:optional #(string? %)]
                        :quiet   [:optional #{true, false}]
                        :volumes [:optional #(and (vector? %) (every? string? %))]
                        :workdir [:optional #(string? %)]
                        :args    [:optional #(and (vector? %) (every? string? %))] } } }

  run [image & options]

  { :pre [(string? image)] }

  (let [opts     (apply hash-map options)
        _        (validate-map "option" opts (-> docker/run meta :spec :options))
        detach   (:detach opts false)
        attach   (:attach opts nil)
        publish  (:publish opts [])
        envs     (:envs opts [])
        memory   (:memory opts 0)
        name     (:name opts nil)
        quiet    (:quiet opts false)
        volumes  (:volumes opts [])
        workdir  (:workdir opts nil)
        args     (:args opts [])
        cmdargs* ["run"]
        cmdargs* (if detach (conj cmdargs* "--detach") cmdargs*)
        cmdargs* (if attach (conj cmdargs* "--attach" (name attach)) cmdargs*)
        cmdargs* (if quiet (conj cmdargs* "--quiet") cmdargs*)
        cmdargs* (if (and (vector? publish) (not-empty? publish))
                   (into cmdargs* (interleave (lazy-seq (constantly "--publish")) publish))
                   cmdargs*)
        cmdargs* (if (pos? memory) (conj cmdargs* "--memory" memory) cmdargs*)
        cmdargs* (if (some? name) (conj cmdargs* "--name" name) cmdargs*)
        cmdargs* (if (some? workdir) (conj cmdargs* "--workdir" workdir) cmdargs*)
        cmdargs* (if (and (vector? volumes) (not-empty? volumes))
                   (into cmdargs* (interleave (lazy-seq (constantly "--volume")) volumes))
                   cmdargs*)
        cmdargs* (if (and (vector? envs) (not-empty? envs))
                   (into cmdargs* (interleave (lazy-seq (constantly "--env")) envs))
                   cmdargs*)
        cmdargs* (conj cmdargs* image)
        cmdargs* (if (and (vector? args) (not-empty? args)) 
                   (into cmdargs* args) cmdargs*)]
    ;; check if there is a already container with the name
    (when (some? name)
      (assert (not (container-running-with-name? name))
              "There is already a container running with the name \"~{name}\"!")
      (assert (not (container-exists-with-name? name))
              "There is already a container with the name \"~{name}\"! You can use `docker/start` instead."))
    (:out (apply docker/cmd cmdargs*))))


(defn
  ^{ :arglists '("(docker/ps & options)")
     :doc """
          List containers.

          Options:

          | :all {true, false}      | Show all containers (default shows just running) |
          | :last n                 | Show n last created containers |
          | :quiet {true, false}    | If true only display container IDs |
          | :no-trunc {true, false} | Don't truncate output |
          | :format {:table, :json} | Returns the output either as a table string or as JSON data |
          """
     :examples '(
          "(println (docker/ps :all true :format :table))"
          "(docker/ps :all true :format :json)"
          "(docker/ps :all true :no-trunc true :format :json)"
          "(docker/ps :all true :no-trunc true :last 3 :format :json)"
          "(println (docker/ps :all true :format :json))" )
     :see-also '(
          "docker/start"
          "docker/stop"
          "docker/rm"
          "docker/run" )
     :spec { :options { :all      [:optional #{true, false}]
                        :last     [:optional #(and (long? %) (pos? %))]
                        :quiet    [:optional #{true, false}]
                        :no-trunc [:optional #{true, false}]
                        :format   [:optional #{:table, :json}] } } }

  ps [& options]

  (let [opts     (apply hash-map options)
        _        (validate-map "option" opts (-> docker/ps meta :spec :options))
        all      (:all opts false)
        lastn    (:last opts -1)
        quiet    (:quiet opts false)
        no-trunc (:no-trunc opts false)
        format   (:format opts :json)
        cmdargs* ["ps"]
        cmdargs* (if all (conj cmdargs* "--all") cmdargs*)
        cmdargs* (if quiet (conj cmdargs* "--quiet") cmdargs*)
        cmdargs* (if no-trunc (conj cmdargs* "--no-trunc") cmdargs*)
        cmdargs* (into cmdargs* ["--last" lastn])
        cmdargs* (into cmdargs* ["--format" (name format)])]
    (->> (apply docker/cmd cmdargs*)
         (:out)
         (parse-output format))))

(defn ps* [image] (println (docker/ps :all true :format :table)))


(defn
  ^{ :arglists '("(docker/start container & options)")
     :doc """
          Start a stopped container.

          Options:

          | :attach {true, false} | Attach STDOUT/STDERR and forward signals |

          See also `cargo/start` / `cargo/stop` for a smarter way to start/stop
          a container.
          """
     :examples '(
          """
          (docker/start "74789744g489")
          """ )
     :see-also '(
          "cargo/start"
          "docker/container-start-by-name"
          "docker/stop"
          "docker/ps"
          "docker/run" )
     :spec { :options { :attach [:optional #{true, false}] } } }

  start [container & options]

  { :pre [(string? container)] }

  (let [opts     (apply hash-map options)
        _        (validate-map "option" opts (-> docker/start meta :spec :options))
        attach   (:attach opts false)
        cmdargs* ["start"]
        cmdargs* (if attach (conj cmdargs* "--attach") cmdargs*)
        cmdargs* (conj cmdargs* container)]
    (:out (apply docker/cmd cmdargs*))))

(defn start* [container] (println (docker/start container)))


(defn
  ^{ :arglists '("(docker/stop container & options)")
     :doc """
          Stop a container.

          Options:

          | :signal name | Signal to send to the container |
          | :time n      | Seconds to wait before killing the container |

          See also `cargo/start` / `cargo/stop` for a smarter way to start/stop
          a container.
          """
     :examples '(
          """
          (docker/stop "74789744g489" :time 30)
          """ )
     :see-also '(
          "cargo/stop"
          "docker/container-stop-by-name"
          "docker/start"
          "docker/ps"
          "docker/run" )
     :spec { :options { :signal [:optional #(string? %)]
                        :time   [:optional #(and (long? %) (pos? %))] } } }

  stop [container & options]

  { :pre [(string? container)] }

  (let [opts     (apply hash-map options)
        _        (validate-map "option" opts (-> docker/stop meta :spec :options))
        signal   (:signal opts nil)
        time     (:time opts 0)
        cmdargs* ["stop"]
        cmdargs* (if (and (some? signal) (string? signal))
                   (into cmdargs* ["--signal" signal])
                   cmdargs*)
        cmdargs* (if (and (number? time) (pos? time))
                   (into cmdargs* ["--time" time])
                   cmdargs*)
        cmdargs* (conj cmdargs* container)]
    (:out (apply docker/cmd cmdargs*))))

(defn stop* [container] (println (docker/stop container)))


(defn
  ^{ :arglists '("(docker/exec& container command args)")
     :doc """
          Execute a command in a running container (always in detached mode).

          Returns always an empty string because the command is run in detached
          mode. To get the commands captured stdout text use `docker/exec` 
          instead.

          Throws `ShellException` if the command fails. The ShellException
          carries the exit code, stdout, and stderr text.
          """
     :examples '(
          """
          (docker/exec& "74789744g489" "touch" "/tmp/execWorks")
          """
          """
          (docker/exec& "74789744g489" "ls" "/var")
          """ )
     :see-also '(
          "docker/exec"
          "docker/ps"
          "docker/run" ) }

  exec& [container command & args]

  { :pre [(string? container) (string? command)] }

  (let [cmdargs* ["exec" "--detach" container command]
        cmdargs* (into cmdargs* args)]
    (:out (apply docker/cmd cmdargs*))))


(defn
  ^{ :arglists '("(docker/exec container command args)")
     :doc """
          Execute a command in a running container (always in non detached mode).
 
          Returns the captured stdout text if the command succeeds.

          Throws `ShellException` if the command fails. The exception carries
          the exit code and the captured stderr text.
         """
     :examples '(
          """
          (docker/exec "74789744g489" "touch" "/tmp/execWorks")
          """
          """
          (println (docker/exec "74789744g489" "ls" "-la" "/var"))
          """ )
     :see-also '(
          "docker/exec&"
          "docker/ps"
          "docker/run" ) }

  exec [container command & args]

  { :pre [(string? container) (string? command)] }

  (let [cmdargs* ["exec" container command]
        cmdargs* (into cmdargs* args)]
    (:out (apply docker/cmd cmdargs*))))


(defn
  ^{ :arglists '("(docker/rm container & options)")
     :doc """
          Remove a container.

          Options:

          | :force {true, false}   | Force the removal of a running container (uses SIGKILL) |
          | :link link             | Remove the specified link |
          | :volumes {true, false} | Remove anonymous volumes associated with the container |
          """
     :examples '(
          """
          (docker/rm "74789744g489")
          """ )
     :see-also '(
          "docker/prune"
          "docker/ps"
          "docker/run" )
     :spec { :options { :force   [:optional #{true, false}]
                        :link    [:optional #(string? %)]
                        :volumes [:optional #{true, false}] } } }

  rm [container & options]

  { :pre [(string? container)] }

  (let [opts     (apply hash-map options)
        _        (validate-map "option" opts (-> docker/rm meta :spec :options))
        force    (:force opts false)
        link     (:link opts nil)
        volumes  (:volumes opts false)
        cmdargs* ["rm"]
        cmdargs* (if force (conj cmdargs* "--force") cmdargs*)
        cmdargs* (if volumes (conj cmdargs* "--volumes") cmdargs*)
        cmdargs* (if (some? link) (into cmdargs* ["--link" link]) cmdargs*)
        cmdargs* (conj cmdargs* container)]
    (:out (apply docker/cmd cmdargs*))))


(defn
  ^{ :arglists '("(docker/prune)")
     :doc """
          Remove all stopped containers.
          """
     :examples '(
          """
          (docker/prune)
          """ )
     :see-also '(
          "docker/rm"
          "docker/ps"
          "docker/run" ) }

  prune []

  (let [cmdargs* ["prune" "--force"]]       ;; Do not prompt for confirmation
    (:out (apply docker/cmd cmdargs*))))


(defn
  ^{ :arglists '("(docker/cp src-path dst-path & options)")
     :doc """
          Copy files/folders between a container and the local filesystem

          Options:

          | [![width: 25%]]            | [![width: 75%]] |
          | :archive {true, false}     | Archive mode (copy all uid/gid information) |
          | :follow-link {true, false} | Always follow symbol link in SRC_PATH |
          | :quiet {true, false}       | Suppress progress output during copy. Progress output is automatically suppressed if no terminal is attached |
          """
     :examples '(
          """
          ;; Copy file from host to docker container
          (docker/cp "data.txt" "74789744g489:/data.txt")
          """,
          """
          ;; Copy file from docker container to host
          (docker/cp "74789744g489:/data.txt" "data.txt")
          """,
          """
          ;; Copy a folder from host to docker container
          (docker/cp "Desktop/images" "74789744g489:/root/img_files/car_photos/images")
          """,
          """
          ;; Copy a folder from docker container to host
          (docker/cp "74789744g489:/root/img_files/car_photos/images Desktop/images")
          """ )
     :see-also '(
          "docker/diff"
          "docker/ps"
          "docker/run" )
     :spec { :options { :archive     [:optional #{true, false}]
                        :follow-link [:optional #{true, false}]
                        :quiet       [:optional #{true, false}] } } }

  cp [src-path dst-path & options]

  { :pre [(string? src-path) (string? dst-path)] }

  (let [opts        (apply hash-map options)
        _           (validate-map "option" opts (-> docker/cp meta :spec :options))
        archive     (:archive opts false)
        follow-link (:follow-link  opts false)
        quiet       (:quiet opts false)
        cmdargs*    ["cp"]
        cmdargs*    (if archive (conj cmdargs* "--archive") cmdargs*)
        cmdargs*    (if follow-link (conj cmdargs* "--follow-link") cmdargs*)
        cmdargs*    (if quiet (conj cmdargs* "--quiet") cmdargs*)
        cmdargs*    (into cmdargs* [src-path dst-path])]
    (:out (apply docker/cmd cmdargs*))))


(defn
  ^{ :arglists '("(docker/diff container & options)")
     :doc """
          Inspect changes to files or directories on a container's filesystem.

          Options:

          | :format {:string, :json} | Returns the output either as a string or as JSON data |
          """
     :examples '(
          """
          (println (docker/diff "74789744g489"))
          """,
          """
          (docker/diff "74789744g4892" :format :json)
          """ )
     :see-also '(
          "docker/cp"
          "docker/ps"
          "docker/run" )
     :spec { :options { :format [:optional #{:string, :json}] } } }

  diff [container & options]

  { :pre [(string? container)] }

  (let [opts     (apply hash-map options)
        _        (validate-map "option" opts (-> docker/diff meta :spec :options))
        format   (:format opts :string)
        actions  {"A" :added, "C" :changed, "D" :deleted}
        cmdargs* ["diff" container]]
    (let [output (:out (apply docker/cmd cmdargs*))]
      (if (= format :json)
        (->> (str/split-lines output)
             (map #(str/split-at % 1))
             (map #(vector (get actions (first %)) (str/trim (second %)))))
        output))))


(defn
  ^{ :arglists '("(docker/pause container)")
     :doc """
          Pause all processes within a container
          """
     :examples '(
          """
          (docker/pause "74789744g489")
          """ )
     :see-also '(
          "docker/unpause"
          "docker/ps"
          "docker/run" ) }

  pause [container]

  { :pre [(string? container)] }

  (let [cmdargs* ["pause" container]]
    (:out (apply docker/cmd cmdargs*))))


(defn
  ^{ :arglists '("(docker/unpause container)")
     :doc """
          Unpause all processes within a container
          """
     :examples '(
          """
          (docker/unpause "74789744g489")
          """ )
     :see-also '(
          "docker/pause"
          "docker/ps"
          "docker/run" ) }

  unpause [container]

  { :pre [(string? container)] }

  (let [cmdargs* ["unpause" container]]
    (:out (apply docker/cmd cmdargs*))))


(defn
  ^{ :arglists '("(docker/wait & containers)")
     :doc """
          Block until one or more containers stop, then return their exit codes
          """
     :examples '(
          """
          (docker/wait "74789744g4892" "2341428e53535")
          """ )
     :see-also '(
          "docker/ps"
          "docker/rm"
          "docker/run" ) }

  wait [&containers]

  { :pre [(seq? containers) (every? string? containers)] }

  (let [cmdargs* (into ["wait"] containers)]
    (:out (apply docker/cmd cmdargs*))))


(defn
  ^{ :arglists '("(docker/logs container & options)")
     :doc """
          Get the logs of a container

          Options:

          | :tail n | Number of lines to show from the end of the logs |
          | :since ts | Show logs since timestamp or relative (e.g. "42m" for 42 minutes) |
          | :until ts | Show logs until timestamp or relative (e.g. "42m" for 42 minutes) |
          | :timestamps {true, false} | Show timestamps |
          | :follow {true, false} | Follow log output |
          | :details {true, false} | Show extra details provided to logs |
          | :stream {:out, :err, :out+err} | Return the :out and/or :err stream from the logs. Defaults to :out |
          """
     :examples '(
          """
          (docker/logs "74789744g489")
          """,
          """
          (docker/logs "74789744g489" :tail 100 :timestamps true :stream :out+err)
          """,
          """
          (docker/logs "74789744g489" :since "60m" :until "30m")
          """)
     :see-also '(
          "docker/pause"
          "docker/ps"
          "docker/run" )
     :spec { :options { :tail       [:optional #(and (long? %) (pos? %))]
                        :since      [:optional #(time/local-date-time? %)]
                        :until      [:optional #(time/local-date-time? %)]
                        :follow     [:optional #{true, false}]
                        :details    [:optional #{true, false}]
                        :timestamps [:optional #{true, false}]
                        :stream     [:optional #{:out, :err, :out+err}] } } }

  logs [container & options]

  { :pre [(string? container)] }

  (let [opts       (apply hash-map options)
        _          (validate-map "option" opts (-> docker/logs meta :spec :options))
        tail       (:tail opts nil)
        since      (:since opts nil)
        until      (:until opts nil)
        follow     (:follow opts false)
        details    (:details opts false)
        timestamps (:timestamps opts false)
        stream     (:stream opts :stdout)
        cmdargs*   ["logs"]
        cmdargs*   (if (some? tail)  (into cmdargs* ["--tail" tail]) cmdargs*)
        cmdargs*   (if (some? since) (into cmdargs* ["--since" (format-ts since)]) cmdargs*)
        cmdargs*   (if (some? until) (into cmdargs* ["--until" (format-ts until)]) cmdargs*)
        cmdargs*   (if follow (conj cmdargs* "--follow") cmdargs*)
        cmdargs*   (if details (conj cmdargs* "--details") cmdargs*)
        cmdargs*   (if timestamps (conj cmdargs* "--timestamps") cmdargs*)
        cmdargs*   (into cmdargs* [container])]
    (let [result (apply docker/cmd cmdargs*)]
      (case stream
        :out      (:out result)
        :err      (:err result)
        :out+err  (let [out+err (str (:out result) "\n" (:err result))]
                    (if timestamps 
                      (->> (str/split-lines out+err)
                           (filter #(some? (str/trim-to-nil %)))
                           (sort)
                           (str/join "\n"))
                      out+err))
        (:out result)))))



;; -----------------------------------------------------------------------------
;; Volume functions
;;   - docker/volume-list
;;   - docker/volume-inspect
;;   - docker/volume-create
;;   - docker/volume-prune
;;   - docker/volume-rm
;;   - docker/volume-exists?
;; -----------------------------------------------------------------------------

(defn
  ^{ :arglists '("(docker/volume-list & options)")
     :doc """
          List all the volumes known to Docker.

          Options:

          | :quiet {true, false} | Only display volume names |
          | :format {:table, :json} | Returns the output either as a ascii table or as JSON data |
          """
     :examples '(
          """
          (docker/volume-list)
          """ )
     :see-also '(
          "docker/volume-create"
          "docker/volume-inspect"
          "docker/volume-rm"
          "docker/volume-prune"
          "docker/volume-exists?"
          "docker/images"
          "docker/run" )
     :spec { :options { :quiet  [:optional #{true, false}]
                        :format [:optional #{:table, :json}] } } }

  volume-list [& options]

  (let [opts     (apply hash-map options)
        _        (validate-map "option" opts (-> docker/volume-list meta :spec :options))
        quiet    (:quiet opts false)
        format   (:format opts :json)
        cmdargs* ["volume" "ls"]
        cmdargs* (if quiet (conj cmdargs* "--quiet") cmdargs*)
        cmdargs* (into cmdargs* ["--format" (name format)])]
    (->> (apply docker/cmd cmdargs*)
         (:out)
         (parse-output format))))


(defn
  ^{ :arglists '("(docker/volume-create vname & options)")
     :doc """
          Create a volume.
          """
     :examples '(
          """
          (docker/volume-create "hello")
          """ )
     :see-also '(
          "docker/volume-list"
          "docker/volume-inspect"
          "docker/volume-rm"
          "docker/volume-prune"
          "docker/volume-exists?" )
     :spec { :options { :driver [:optional #(string? %)]
                        :label  [:optional #(string? %)]
                        :opts   [:optional #(and (vector? %) (every? string? %))]
                        :type   [:optional #{:mount :block}] } } }

  volume-create [vname & options]

  { :pre [(string? vname)] }

  (let [options  (apply hash-map options)
        _        (validate-map "option" options (-> docker/volume-inspect meta :spec :options))
        driver   (:driver options nil)
        label    (:label options nil)
        opts     (:opts options [])
        type     (:type options nil)
        cmdargs* ["volume" "create"]
        cmdargs* (if (some? driver) (conj cmdargs* "--driver" driver) cmdargs*)
        cmdargs* (if (some? label) (conj cmdargs* "--label" label) cmdargs*)
        cmdargs* (if (some? type) (conj cmdargs* "--type" (name type)) cmdargs*)
        cmdargs* (if (and (vector? opts) (not-empty? opts))
                  (into cmdargs* (interleave (lazy-seq (constantly "--opt")) opts))
                  cmdargs*)
        cmdargs* (into cmdargs* [vname])]
    (->> (apply docker/cmd cmdargs*)
         (:out))))


(defn
  ^{ :arglists '("(docker/volume-inspect vname & options)")
     :doc """
          Inspects a volume.

          Options:

          | :format {:string :json} | Returns the output either as a ascii or as JSON data |
           """
     :examples '(
          """
          (docker/volume-inspect "hello")
          """ )
     :see-also '(
          "docker/volume-list"
          "docker/volume-create"
          "docker/volume-inspect"
          "docker/volume-prune"
          "docker/volume-exists?")
     :spec { :options { :format [:optional #{:string :json}] } } }

  volume-inspect [vname & options]

  { :pre [(string? vname)] }

  (let [opts     (apply hash-map options)
        _        (validate-map "option" opts (-> docker/volume-inspect meta :spec :options))
        format   (:format opts nil)
        cmdargs* ["volume" "inspect"]
        cmdargs* (if (= format :json) (into cmdargs* ["--format" (name format)]) cmdargs*)
        cmdargs* (into cmdargs* [vname])]
    (->> (apply docker/cmd cmdargs*)
         (:out)
         ((fn [j] (first (first (parse-output format j))))))))


(defn
  ^{ :arglists '("(docker/volume-rm name)")
     :doc """
          Remove a volume.
          """
     :examples '(
          """
          (docker/volume-remove "hello")
          """ )
     :see-also '(
          "docker/volume-list"
          "docker/volume-create"
          "docker/volume-inspect"
          "docker/volume-prune"
          "docker/volume-exists?" ) }

  volume-rm [name]

  { :pre [(string? name)] }

  (->> (apply docker/cmd ["volume" "rm" name])
       (:out)))


(defn
  ^{ :arglists '("(docker/volume-prune )")
     :doc """
          Remove all unused local volumes. Unused local volumes are those which 
          are not referenced by any containers. Removes both named and anonymous 
          volumes!
          """
     :examples '(
          """
          (docker/volume-prune)
          """ )
     :see-also '(
          "docker/volume-list"
          "docker/volume-create"
          "docker/volume-inspect"
          "docker/volume-rm"
          "docker/volume-exists?" ) }

  volume-prune []

  (->> (apply docker/cmd ["volume" "prune" "--force" "--all"])
       (:out)))


(defn
  ^{ :arglists '("(docker/volume-exists? name)")
     :doc """
          Returns true if the volume with the specified name exists.
          """
     :examples '(
          """
          (docker/volume-exists?  "hello")
          """ )
     :see-also '(
          "docker/volume-list" ) }

  volume-exists? [name]

  { :pre [(string? name)] }

  (->> (docker/volume-list :format :json)
       (filter #(== name (get % "Name")))
       (count)
       (pos?)))




;; -----------------------------------------------------------------------------
;; Generic docker command
;; -----------------------------------------------------------------------------

(defn
  ^{ :arglists '("(docker/cmd & args)")
     :doc """
          Runs any Docker command.
          """
     :examples '(
          """
          (println (docker/cmd "ps" "--all"))
          """,
          """
          ;; a single string argument works as well
          (println (docker/cmd "ps --all"))
          """,
          """
          ;; run a command with JSON output and parse the JSON output into
          ;; Venice data
          ;; use `apply` to apply a vector of arguments
          (-<> (apply docker/cmd ["ps" "--all" "--format" "json"])
               (:out <>)
               (str/split-lines <>)
               (str/join "," <>)
               (str "[" <> "]")
               (json/read-str <>))
          """ ) }

   cmd [& args]

   (let [cmd* (apply str docker/binary " " (interpose " " args))]
     (case docker/*debug*
       :off          ((docker/os-exec) cmd* :throw-ex true)
       :on           (do
                       (println "DEBUG (no exec):"  cmd*)
                       ((docker/os-exec) cmd* :throw-ex true))
       :on-no-exec   (do (println "DEBUG:"  cmd*) nil)
       :else)))



;; -----------------------------------------------------------------------------
;; Utility functions
;;   - docker/images-query-by-repo
;;   - docker/image-ready?
;;   - docker/container-find-by-name
;;   - docker/container-exists-with-name?
;;   - docker/container-running-with-name?
;;   - docker/container-start-by-name
;;   - docker/container-stop-by-name
;;   - docker/container-remove-by-name
;;   - docker/container-status-by-name
;;   - docker/container-exec-by-name
;;   - docker/container-logs
;;   - docker/container-purge-by-name
;;   - docker/container-image-info-by-name
;; -----------------------------------------------------------------------------


(defn
  ^{ :arglists '("(docker/images-query-by-repo repo)")
     :doc """
          Returns all pulled local images for a given repo.
          """
     :examples '(
          """
          (docker/images-query-by-repo "arangodb/arangodb")
          """,
          """
          ;; return a list of ids for "arangodb/arangodb" images
          (->> (docker/images-query-by-repoo "arangodb/arangodb")
               (map #(get % "ID")))
          """ )
     :see-also '(
          "docker/images",
          "docker/image-ready?" ) }

  images-query-by-repo [repo]

  { :pre [(string? repo)] }

  (->> (docker/images :format :json)
       (filter #(== repo (get % "Repository")))))


(defn
  ^{ :arglists '("(docker/image-ready? repo tag)")
     :doc """
          Returns true if the image exists locally (is pulled) else false.
          """
     :examples '(
          """
          (docker/image-ready? "arangodb/arangodb" "3.10.10")
          """ )
     :see-also '(
          "docker/images"
          "docker/images-query-by-repo" ) }

  image-ready? [repo tag]

  { :pre [(string? repo) (string? tag)] }

  (->> (docker/images-query-by-repo repo)
       (filter #(== tag (get % "Tag")))
       (count)
       (pos?)))


(defn
  ^{ :arglists '("(docker/container-find-by-name name)")
     :doc """
          Find all containers with a specified name
          """
     :examples '(
          """
          (docker/container-find-by-name "myapp")
          """ )
     :see-also '(
          "docker/run"
          "docker/container-exists-with-name?" ) }

  container-find-by-name [name]

  { :pre [(string? name)] }

  (->> (docker/ps :all true :format :json)
       (filter #(== name (get % "Names")))))


(defn
  ^{ :arglists '("(docker/container-exists-with-name? name)")
     :doc """
          Returns true if there is container with the specified name else false.
          """
     :examples '(
          """
          (docker/container-exists-with-name? "myapp")
          """ )
     :see-also '(
          "docker/run"
          "docker/container-find-by-name" ) }

  container-exists-with-name? [name]

  { :pre [(string? name)] }

  (pos? (count (container-find-by-name name))))


(defn
  ^{ :arglists '("(docker/container-running-with-name? name)")
     :doc """
          Checks if there is container with the specified name in 'running'
          state.

          Returns true if running else false.
          """
     :examples '(
          """
          (docker/container-running-with-name? "myapp")
          """ )
     :see-also '(
          "docker/run"
          "docker/container-start-by-name"
          "docker/container-stop-by-name" ) }

  container-running-with-name? [name]

  { :pre [(string? name)] }

  (->> (docker/ps :all true :format :json)
       (filter #(== name (get % "Names")))
       (filter #(== "running" (get % "State")))
       (count)
       (pos?)))


(defn
  ^{ :arglists '("(docker/container-start-by-name name)")
     :doc """
          Starts a container with the specified name.
          """
     :examples '(
          """
          (docker/container-start-by-name "myapp")
          """ )
     :see-also '(
          "docker/run"
          "docker/container-running-with-name?"
          "docker/container-stop-by-name"
          "docker/container-remove-by-name"
          "docker/container-status-by-name"
          "docker/container-logs" ) }

  container-start-by-name [name]

  { :pre [(string? name)] }

  (let [container (first (container-find-by-name name))
        id        (get container "ID")
        status    (get container "State")]
    (assert (some? container)
            "There is no container with the name \"~{name}\" available!")
    (assert (not= status "running")
            "There is already a container running with the name \"~{name}\"!")

    (start id)))


(defn
  ^{ :arglists '(
          "(docker/container-stop-by-name name)"
          "(docker/container-stop-by-name name time)")
     :doc """
          Stops a container with the specified name.
          """
     :examples '(
          """
          (docker/container-stop-by-name "myapp")
          """ )
     :see-also '(
          "docker/run"
          "docker/container-running-with-name?"
          "docker/container-stop-by-name"
          "docker/container-remove-by-name"
          "docker/container-status-by-name"
          "docker/container-logs" ) }

  container-stop-by-name

  ([name]
    (let [container (first (container-find-by-name name))
          id        (get container "ID")
          status    (get container "State")]
      (assert (= status "running")
              "There is no container running with the name \"~{name}\"!")
      (stop id)))

  ([name time]
    (let [container (first (container-find-by-name name))
          id        (get container "ID")
          status    (get container "State")]
      (assert (= status "running")
              "There is no container running with the name \"~{name}\"!")
      (stop id :time time))))


(defn
  ^{ :arglists '("(docker/container-remove-by-name name)")
     :doc """
          Removes a container with the specified name.
          """
     :examples '(
          """
          (docker/container-remove-by-name "myapp")
          """ )
     :see-also '(
          "docker/run"
          "docker/container-find-by-name"
          "docker/container-exists-with-name?"
          "docker/container-status-by-name" ) }

  container-remove-by-name [name]

  (let [container (first (container-find-by-name name))
        id        (get container "ID")]
    (rm id)))


(defn
  ^{ :arglists '("(docker/container-purge-by-name name)")
     :doc """
          Removes a container and its image.
          """
     :examples '(
          """
          (docker/container-purge-by-name "myapp")
          """ )
     :see-also '(
          "docker/run"
          "docker/container-find-by-name"
          "docker/container-exists-with-name?"
          "docker/container-status-by-name" ) }

  container-purge-by-name [name]

  (let [container       (first (container-find-by-name name))
        container-id    (get container "ID")
        image           (get container "Image")]
    (rm container-id)
    (rmi image :force true)))


(defn
  ^{ :arglists '("(docker/container-image-info-by-name name)")
     :doc """
          Returns the image info for a container given by its name.

          Returns a map (e.g.):
          ```
          { :image  "arangodb/arangodb:3.10.10"
            :repo   "arangodb/arangodb"
            :tag    "3.10.10" }
          ```
          """
     :examples '(
          """
          (docker/container-image-info-by-name "myapp")
          """ )
     :see-also '(
          "docker/run"
          "docker/container-find-by-name"
          "docker/container-exists-with-name?"
          "docker/container-status-by-name" ) }

  container-image-info-by-name [name]

  (let [container       (first (container-find-by-name name))
        container-id    (get container "ID")
        image           (get container "Image") ]
    { :image  image
      :repo   (first (str/split image ":"))
      :tag    (second (str/split image ":")) }))


(defn
  ^{ :arglists '("(docker/container-status-by-name name)")
     :doc """
          Returns the status of container with the specified name.
          """
     :examples '(
          """
          (docker/container-status-by-name  "myapp")
          """ )
     :see-also '(
       "docker/run"
       "docker/container-find-by-name"
       "docker/container-exists-with-name?"
       "docker/container-remove-by-name" ) }

  container-status-by-name [name]

  { :pre [(string? name)] }

  (-<> (container-find-by-name name)
       (first <>)
       (get <> "State" "unavailable")))


(defn
  ^{ :arglists '("(docker/container-exec-by-name& name command)")
     :doc """
          Execute a command in the running container with the specified name
          (always in detached mode).

          Returns always an empty string because the command is run in detached
          mode. To get the commands captured stdout text use 
          `docker/container-exec-by-name` instead.

          Throws a `ShellException` if the command fails. The ShellException
          carries the exit code, stdout, and stderr text.
          """
     :examples '(
          """
          (docker/container-exec-by-name&  "myapp" "touch /tmp/execWorks")
          """ )
     :see-also '(
          "docker/container-exec-by-name"
          "docker/run"
          "docker/container-running-with-name?"
          "docker/container-exec-by-name"
          "docker/container-logs" ) }

  container-exec-by-name& [name command & args]

  { :pre [(string? name) (string? command)] }

  (let [container (first (container-find-by-name name))
        id        (get container "ID")
        status    (get container "State")]
    (assert (= status "running")
            "There is no container running with the name \"~{name}\"!")
    (apply docker/exec& id command args)))


(defn
  ^{ :arglists '("(docker/container-exec-by-name name command)")
     :doc """
          Execute a command in the running container with the specified name
          (always in non detached mode).
 
          Returns the captured stdout text if the command succeeds.

          Throws `ShellException` if the command fails. The exception carries
          the exit code and the captured stderr text.
          """
     :examples '(
          """
          (docker/container-exec-by-name  "myapp" "touch /tmp/execWorks")
          """ )
     :see-also '(
          "docker/container-exec-by-name&"
          "docker/run"
          "docker/container-running-with-name?"
          "docker/container-exec-by-name"
          "docker/container-logs" ) }

  container-exec-by-name [name command & args]

  { :pre [(string? name) (string? command)] }

  (let [container (first (container-find-by-name name))
        id        (get container "ID")
        status    (get container "State")]
    (assert (= status "running")
            "There is no container running with the name \"~{name}\"!")
    (apply docker/exec id command args)))


(defn
  ^{ :arglists '(
          "(docker/container-logs name & options)")
     :doc """
          Returns the container logs.

          Options:

          | :tail n   | Number of lines to show from the end of the logs |
          | :since ts | Show logs since timestamp or relative (e.g. "42m" for 42 minutes) |
          | :until ts | Show logs until timestamp or relative (e.g. "42m" for 42 minutes) |
          | :follow {true, false}  | Follow log output |
          | :details {true, false} | Show extra details provided to logs |
          """
     :examples '(
          """
          (docker/container-logs "myapp")
          """,
          """
          (docker/container-logs "myapp" :since "2m")
          """ )
     :see-also '(
          "docker/run"
          "docker/logs"
          "docker/container-running-with-name?" ) }

  container-logs [name & options]

  (let [container (first (container-find-by-name name))
        id        (get container "ID")]
    (apply docker/logs id options)))



;; -----------------------------------------------------------------------------
;; Private helpers
;; -----------------------------------------------------------------------------


(defn- parse-output [format output]
  (if (= format :json)
    (jsonl/read-str output)
    output))


(defn- validate-map [name map_ spec]
  (assert (map? map_) "Argument 'map_' must be a map!")
  (assert (map? spec) "Argument 'spec' must be a map!")
  (let [spec-keys    (keys spec)
        map-keys     (keys map_)
        invalid-keys (difference (into #{} map-keys) (into #{} spec-keys))]
    (when (not-empty? invalid-keys)
       (throw (ex :VncException
                  "Invalid option ~(pr-str (first (into [] invalid-keys)))")))
    (loop [ks spec-keys]
      (if (empty? ks)
        true
        (let [k         (first ks)
              mandatory (== :mandatory (first (get spec k)))
              key-spec  (second (get spec k))
              v         (get map_ k)]
          (when (and mandatory (nil? v))
            (throw (ex :VncException
                       "Missing value for mandatory ~{name} \"~(pr-str k)\"")))
          (if (some? v)
            (cond
              (set? key-spec) (when-not (contains? key-spec v)
                                (let [v_ (str/truncate (pr-str v) 50 "...")
                                      k_ (pr-str k)]
                                  (throw (ex :VncException
                                             (str "Invalid value \"~{v_}\" "
                                                  "for ~{name} \"~{k_}\"")))))
              (fn? key-spec) (key-spec v)))
          (recur (rest ks)))))))


(defn- format-ts [ts]
  (case (type ts)
    :core/string               ts
    :java.time.LocalDateTime   (time/format ts "yyyy-MM-dd'T'HH:mm:ss'Z'")
    :java.time.ZonedDateTime   (time/format ts "yyyy-MM-dd'T'HH:mm:ss'Z'")))


(defn- os-exec []
  (case (os-type)
    :mac-osx (partial sh "/bin/sh" "-c")
    :linux   (partial sh "/bin/sh" "-c")
    :windows (partial sh "cmd" "/C")))




© 2015 - 2024 Weber Informatics LLC | Privacy Policy