Skip to content
Browse files

cleanup & readme

  • Loading branch information...
1 parent 9ed52d5 commit 340ac70af916bb34cae9872bc50e2e0f1b0ffc29 @franks42 committed Jan 31, 2012
Showing with 64 additions and 230 deletions.
  1. +1 −1 .lein-deps-sum
  2. +41 −46 README.md
  3. +10 −6 bin/cljsh.sh
  4. +1 −3 project.clj
  5. +0 −149 src/cljsh/complete.clj
  6. +3 −7 src/cljsh/completion.clj
  7. +8 −18 src/cljsh/core.clj
View
2 .lein-deps-sum
@@ -1 +1 @@
-f98366c7b75a527389a0ea99edb6f4642249a873
+361ec9976e4b966cf5e1ee9d9ce9308e490aaacf
View
87 README.md
@@ -1,44 +1,60 @@
-#CLJSH: A lightweight Clojure Shell frontend/client that uses rlwrap and socat with lein's repl-server.
+#CLJSH: A lightweight Clojure Shell - a bash-shell client that uses socat and optionally rlwrap, to interact with a persistent repl-server.
-Cljsh is a bash shell script that interacts with Leiningen's networked repl-server. It allows the user to submit Clojure statement and Clojure script files to the persistent networked repl for evaluation. The script uses socat to make the networked repl appear local. Socat also makes this client lightweight and fast, very much like the ruby-based cake-client. The Clojure statements are send thru socat to the persistent Leiningen-repl-server, and the results are brought back thru socat to stdout.
+Cljsh is a bash shell script that interacts with a persistent Leiningen's networked repl-server. It allows the user to submit Clojure statement and Clojure script files to the persistent networked repl for evaluation. The script uses socat to make the networked repl appear local: the repl-server's stdin and stdout are transparently extended to cljsh. Socat also makes this client lightweight and fast, very much like the ruby-based cake-client or nailgun. The Clojure statements are sent thru socat to the persistent Leiningen-repl-server, and the results are brought back thru socat to stdout.
-The advantage of using socat and the networked repl is that there is no real protocol - or no protocol different from the normal repl-interaction: feed Clojure form in thru stdin, and have the results or printed side-effect presented on stdout.
+The advantage of using socat and the networked repl is that there is no real protocol - or no protocol different from the normal repl-interaction: feed forms in thru stdin to the clojure-reader, and have the results or printed side-effect returned on stdout. This cljsh approach is different from cake, nailgun, swank and nrepl, which have true client-server protocols that arguably make those apps more powerful and more complicated.
-This cljsh approach is different from cake, nailgun, swank and nrepl, which have true client-server protocols. Cljsh, however, always sends normal Clojure-statement to the repl-server, while the output is whatever the program returns from either the evaluation or from the side-effect printing. You can choose to switch the printing of the evaluation results off, such that you can completely control the output thru explicit printing to stdout.
+The repl-server is based on Leiningen's native "repl" task, which is basically refactored as a true plugin "repls", to which a number of hooks are added to turn the repl-prompt and eval-result printing on and off. By not printing the prompt and eval-result, it's easier to write clojure-scripts that rely on its side-effects like printing to stdout. "repls" is installed and run as a normal Leiningen plugin (browse clojars for the latest "lein-repls" version available):
-The result is a lightweight repl-client like cake/nailgun thru socat, which includes history and completion thru rlwrap for interactive use.
+ $ lein plugin install repls 1.?.?
+ $ lein repls
+
+cljsh's main purpose is sending clj-statements and/or clj-files to the persistent repl. That clj-code is specified as command line arguments like:
+
+ $ cat three.clj | cljsh -c "(println "one") -f two.clj - four.clj -args
+
+The sequence of positional arguments determine the evalation sequence, where stdin is indicated by "-" (default last). The first non-option should indicate a clj-file with optional args.
+(see cljsh -h) The options should reflect most clojure invocation flavors.
-It can also be used as a one-shot Clojure client that you feed clojure-scripts thru stdin and yields evaluated results thru stdout from a persistent repl.
+In addition, cljsh also offers an interactive repl mode, that is similar to the other repls out there. The difference again is that it's lightweight and allows for initialization scripts before the interactive session. In addition, it will use rlwrap with word completion. The word completion file can easily be updated to reflect the context of your session. (it still is a poor-man's completion, though, compared to "real" context sensitive completers as in emacs...). It may also work with JLine, but I have not tested that.
## Install
-Note that this cljsh depends on a very recent development version of leiningen (v 1.7.0 from Jan 9 2012)!
-So... clone leiningen's github and make sure you have the latest version in the 1.x branch, then use the lein script from inside the bin directory of that repo... until it is part of the standard lein distro...
-
-Cljsh also needs an installed version of socat and rlwrap.
-An easy way is thru ports/macports on macosx, but any other recent version will probably do:
+"cljsh" needs an installed version of "socat" and optionally "rlwrap".
+An easy way is thru ports/macports on macosx, but substitute your own brewing mechanism as you like:
-sudo port install socat
-sudo port install rlwrap
+ $ sudo port install socat
+ $ sudo port install rlwrap
-The easiest way to play with cljsh is to clone the github [cljsh project](https://github.com/franks42/cljsh) ("https://github.com/franks42/cljsh"), build it with lein, and start the lein repl in a terminal inside the cljsh repo directory:
+The "repls" plugin is installed thru the standard Leiningen mechanism:
- $ lein deps
- $ lein lein plugin install lein-repls
+ $ lein plugin install lein-repls
$ lein repls
REPL started; server listening on 0.0.0.0 port 12357
- cljsh.core=>
+ user=>
-This will give you the standard repl interaction, but we will not use this repl-interface directly, but use the "server listening on 0.0.0.0 port 12357".
+This will start the persistent repl server, and will give you a "console" with a standard repl interaction. cljsh will use that repl server listening on the indicated port.
-Then there are two scripts in the bin directory "cljsc" and "catcljsh", that you should put somewhere in your PATH.
+Lastly, you will have to download the cljsh shell script and put it somewhere on your path:
-That's all...
+ curl https://raw.github.com/franks42/lein-repls/master/bin/cljsh.sh > cljsh
+ chmod +x cljsh
+ mv cljsh /somewhere-on-your-path/cljsh
+
+Alternatively, you can clone the github repo: "https://github.com/franks42/lein-repls"
+
+That's all... you're ready to repl.
## Usage
-In a different, separate terminal session, we can play with the cljsh interface.
+Go to one of your clj-project, and start the repls server:
+
+ $ lein repls
+ REPL started; server listening on 0.0.0.0 port 12357
+ user=>
+
+In a different, separate terminal session, we will work with the cljsh repls-client. Make sure you are within the project's directory tree when you invoke cljsh, such that cljsh will automatically pickup the server's port number.
### evaluate clojure-code passed as command line argument
@@ -48,9 +64,9 @@ In a different, separate terminal session, we can play with the cljsh interface.
### to start a interactive repl-session:
- $ cljsh -ip
+ $ cljsh -r
"Welcome to your cljsh-lein-repl"
- cljsh.core=>
+ user=>
For examples of most supported features, please take a look at the [cljsh-test.sh](https://github.com/franks42/cljsh/blob/master/bin/cljsh-test.sh) ("https://github.com/franks42/cljsh/blob/master/bin/cljsh-test.sh") in the distro's bin directory. Even better, run cljsh-test.sh to see if all works well.
@@ -64,34 +80,13 @@ The only way to communicate with the networked repl-server is thru sending cloju
The cljsh client accepts clojure script code and files as command line options, and sends those to the server for evaluation.
-It writes all the clojure statements passed on the commandline to a tmp-file, and a load-file statement for that tmp-file is send to the server. In that way you have access to somewhat more meaningful debug info in the stacktrace in case of error.
-
-The clojure file passed as an argument is dealt with in the same way by sending a load-file directive to the server.
+It writes all the clojure statements passed on the commandline to a tmp-clj-file. All clj-file command line directives are added to that tmp-clj-file as load-file clj-statements. After the command line options processing, that tmp-clj-file is sent to the repl server for eval over stdin, and the results and printed output is received back on stdout.
-Any code passed to cljsh thru stdin however, is also directly piped on to socat and the repl-server as one cannot predict the end of the statements. Debugging is somewhat more challenging in that case.
+The clj-code passed in thru stdin to cljsh, is also piped-thru to the repl-server. The positional "-" option determines when the stdin-clj-code is processed with respect to the other clj-statement and clj-file.
Cljsh does also send a few more clojure statements transparently to the repl-server that are related to functionality like turning the repl-prompt on, turning the eval-result printing on, communicating the command line options passed with a clojure file, and a kill-switch to indicate the last clojure statement has been eval'ed.
See the cljsh-test.sh for more explanations about those.
-The following options must be added to the project.clj to make it work:
-
-> :repl-options [
-> ;; set the repl-prompt to print nothing by default in cljsh.core.clj
-> :prompt cljsh.core/*repl-prompt*
-> ;; do not print the eval-result by default in cljsh.core.clj
-> :print cljsh.core/*repl-result-print*
-> ]
->
-> ;; hardcode the port number such that the cljsh-client can easily find it
-> ;; (ideally, lein dynamically maintains the port number used by the repl-server in some "." file such that cljsh can pick it up)
-> :repl-port 12357
-> :repl-host "0.0.0.0"
->
-> ;; have to bring-in the cljsh.core ns in order to refer to the vars in the options.
-> :project-init (require 'cljsh.core)
-
-In the cljsh.core.clj file, the repl-prompt and the eval-result printing is maintained on a per thread basis. It feels like a bit of a hack to associate the repl-session threads explicitly with the prompt/eval-print functions... It may make more sense to maintain them in the (binding ...) context of the repl creation, but that requires changes in the leiningen code... If this effort gets serious, then that may be the right way forward.
-
## License
Copyright (C) 2011 - Frank Siebenlist
View
16 bin/cljsh.sh
@@ -8,11 +8,12 @@
# the terms of this license.
# You must not remove this notice, or any other, from this software.
+###############################################################################
# "cljsh" is a bash shell script that interacts with Leiningen's plugin "repls".
# It allows the user to submit Clojure statement and Clojure script files
# to a persistent networked repl for evaluation.
-CLJSH_VERSION="0.9"
+CLJSH_VERSION="0.99"
# util functions
@@ -75,6 +76,10 @@ CLJSH_STDIN="REDIRECTED"
if [[ -p /dev/stdin ]]; then CLJSH_STDIN="PIPE"; fi
if [[ -t 0 ]]; then CLJSH_STDIN="TERM"; fi
+###############################################################################
+# Option processing.
+
+# file that will hold clj-statements associated with cmd-line options
CLJ_CODE=`mktemp -t ${CLJ_TMP_FNAME}` || exit 1
CLJ_CODE_BEFORE=${CLJ_CODE}
CLJ_CODE_AFTER=
@@ -217,7 +222,7 @@ if [ -n "$CLJFILE" ]; then
echo '(load-file "'"${CLJFILEFP}"'")' >> ${CLJ_CODE}
fi
-# special cases
+# special cases for options
# no options, no clj-file and connected to the terminal => assume interactive repl
if [[ ${OPTIND} -eq 1 && -z "${CLJFILE}" && ${CLJSH_STDIN} == "TERM" ]]; then
@@ -226,7 +231,7 @@ if [[ ${OPTIND} -eq 1 && -z "${CLJFILE}" && ${CLJSH_STDIN} == "TERM" ]]; then
echo "${CLJ_EVAL_PRINT_CODE}" >> ${CLJ_CODE}
fi
-# welcome message for interactive repl
+# add welcome message for interactive repl
if [ ${CLJ_REPL_PROMPT:-0} = 1 ]; then
/bin/echo '(str "Welcome to your Clojure (" (clojure-version) ") lein-repls (" cljsh.core/lein-repls-version ") client!")' >> ${CLJ_CODE};
fi
@@ -246,8 +251,7 @@ fi
###############################################################################
# Load File Generation
-# write clj-code first in a file, and then load thru second file
-# we get the benefit of line# debug in stacktrace
+# load code-file thru second file => benefit of line# debug in stacktraces
CLJ_BEFORE_LOAD=`mktemp -t ${CLJ_TMP_FNAME}` || exit 1
/bin/echo '(load-file "'${CLJ_CODE_BEFORE}'")' >> $CLJ_BEFORE_LOAD
@@ -296,7 +300,7 @@ else # no REPL, nothing interactive
# user's code is responsible for closing *in* to indicate eof as we cannot deduce it to use the kill-switch
exec cat ${CLJ_BEFORE_LOAD} - | socat -t ${CLJSH_MAXTIME} - TCP4:${LEIN_REPL_HOST}:${LEIN_REPL_PORT};
- else # expect clojure code to be piped-in from stdin and kill/end the session after that
+ else # expect clojure code to be piped-in from stdin and kill&end the session after that
# because the repl keeps reading from stdin for clojure-statements, we can append the kill-switch at the end
exec cat ${CLJ_BEFORE_LOAD} - ${CLJ_AFTER_LOAD} ${CLJ_KILL_FILE} | socat -t ${CLJSH_MAXTIME} - TCP4:${LEIN_REPL_HOST}:${LEIN_REPL_PORT};
View
4 project.clj
@@ -1,7 +1,5 @@
-(defproject lein-repls "1.4.0-SNAPSHOT"
+(defproject lein-repls "1.5.0-SNAPSHOT"
:description "A leiningen plugin to start a persistent repl server for use with cljsh"
:dependencies [ [org.clojure/clojure "1.3.0"]
- [growl-clj "1.0.0-SNAPSHOT"]
- ;[swank-clojure "1.3.4"]
])
View
149 src/cljsh/complete.clj
@@ -1,149 +0,0 @@
-(ns cljsh.complete
- (:import [java.util.jar JarFile] [java.io File]))
-
-;; Code adapted from swank-clojure (http://github.com/jochu/swank-clojure)
-
-(defn namespaces
- "Returns a list of potential namespace completions for a given namespace"
- [ns]
- (map name (concat (map ns-name (all-ns)) (keys (ns-aliases ns)))))
-
-(defn ns-public-vars
- "Returns a list of potential public var name completions for a given namespace"
- [ns]
- (map name (keys (ns-publics ns))))
-
-(defn ns-vars
- "Returns a list of all potential var name completions for a given namespace"
- [ns]
- (for [[sym val] (ns-map ns) :when (var? val)]
- (name sym)))
-
-(defn ns-classes
- "Returns a list of potential class name completions for a given namespace"
- [ns]
- (map name (keys (ns-imports ns))))
-
-(def special-forms
- (map name '[def if do let quote var fn loop recur throw try monitor-enter monitor-exit dot new set!]))
-
-(defn- static? [#^java.lang.reflect.Member member]
- (java.lang.reflect.Modifier/isStatic (.getModifiers member)))
-
-(defn ns-java-methods
- "Returns a list of potential java method name completions for a given namespace"
- [ns]
- (for [class (vals (ns-imports ns)) method (.getMethods class) :when (static? method)]
- (str "." (.getName method))))
-
-(defn static-members
- "Returns a list of potential static members for a given class"
- [class]
- (for [member (concat (.getMethods class) (.getDeclaredFields class)) :when (static? member)]
- (.getName member)))
-
-(defn path-files [path]
- (cond (.endsWith path "/*")
- (for [jar (.listFiles (File. path)) :when (.endsWith (.getName jar) ".jar")
- file (path-files (.getPath jar))]
- file)
-
- (.endsWith path ".jar")
- (try (for [entry (enumeration-seq (.entries (JarFile. path)))]
- (.getName entry))
- (catch Exception e))
-
- :else
- (for [file (file-seq (File. path))]
- (.replace (.getPath file) path ""))))
-
-(def classfiles
- (for [prop ["sun.boot.class.path" "java.ext.dirs" "java.class.path"]
- path (.split (System/getProperty prop) File/pathSeparator)
- file (path-files path) :when (and (.endsWith file ".class") (not (.contains file "__")))]
- file))
-
-(defn- classname [file]
- (.. file (replace File/separator ".") (replace ".class" "")))
-
-(def top-level-classes
- (future
- (doall
- (for [file classfiles :when (re-find #"^[^\$]+\.class" file)]
- (classname file)))))
-
-(def nested-classes
- (future
- (doall
- (for [file classfiles :when (re-find #"^[^\$]+(\$[^\d]\w*)+\.class" file)]
- (classname file)))))
-
-(defn resolve-class [sym]
- (try (let [val (resolve sym)]
- (when (class? val) val))
- (catch ClassNotFoundException e)))
-
-(defmulti potential-completions
- (fn [prefix ns]
- (cond (.contains prefix "/") :scoped
- (.contains prefix ".") :class
- :else :var)))
-
-(defmethod potential-completions :scoped
- [prefix ns]
- (let [scope (symbol (first (.split prefix "/")))]
- (map #(str scope "/" %)
- (if-let [class (resolve-class scope)]
- (static-members class)
- (when-let [ns (or (find-ns scope) (scope (ns-aliases ns)))]
- (ns-public-vars ns))))))
-
-(defmethod potential-completions :class
- [prefix ns]
- (concat (namespaces ns)
- (if (.contains prefix "$")
- @nested-classes
- @top-level-classes)))
-
-(defmethod potential-completions :var
- [_ ns]
- (concat special-forms
- (namespaces ns)
- (ns-vars ns)
- (ns-classes ns)
- (ns-java-methods ns)))
-
-(defn completions
- "Return a sequence of matching completions given a prefix string and an optional current namespace."
- ([prefix] (completions prefix *ns*))
- ([prefix ns]
- (for [completion (potential-completions prefix ns) :when (.startsWith completion prefix)]
- completion)))
-
-(defn fqn-str
- ([sv]
- (cond (string? sv)
- (let [svs (.split sv "/")
- n (count svs)
- f (first svs)]
- (when-not (or (> n 2) ( = f ""))
- (if (= n 1)
- (fqn-str (symbol f))
- (fqn-str (symbol f) (symbol (second svs))))))
- (symbol? sv)
- (str sv)
- :else "else what?"
- ))
- ([s1 s2]
- "ok..."
- ))
-(defn ns+var=>str [ns]
- (let [ns (find-ns ns)] (sort (map #(str ns "/" %) (keys (ns-publics ns))))))
-
-; (if-let [v (or (and (var? sv) sv)
-; (and (symbol? sv) (resolve sv))
-; (and (string? sv) (resolve (symbol sv))))]
-; (str (:ns (meta v)) "/" (:name (meta v)))
-; (and (= (type sv) clojure.lang.Namespace) (str (ns-name sv)))))
-
-
View
10 src/cljsh/completion.clj
@@ -6,14 +6,11 @@
;; copied swank.command.completions with all dependent code from swank.*
;; to avoid direct dependencies on the swank-clojure code base.
+;; simple clojure-swank dependency in project.clj doesn't seem to work
;; too bad those function are not moved into a separate library
;; at the end of the file there are some convenient functions to create
;; the rlwrap word list.
-; (:use (swank util core commands)
-; (swank.util string clojure java class-browse)))
-
-;; swank.core
(defn maybe-ns [package]
(cond
@@ -279,6 +276,7 @@
(potential-classes ns)
(potential-dot ns))))
+
;; custom additions for cljsh and rlwarp
;; for the rlwrap word list, we want all words from ns-map
;; and all fqn's from all public keys for each ns in all-ns
@@ -298,13 +296,11 @@
(map #(cljsh.completion/potential-completions % "")
(map #(symbol (ns-name %))
(all-ns))))))))
-
+
(defn print-all-words []
(doall (map println (sort (concat
special-forms
(cljsh.completion/potential-completions nil *ns*)
(flatten (map #(cljsh.completion/potential-completions % "")
(map #(symbol (ns-name %))
(all-ns)))))))))
-
-
View
26 src/cljsh/core.clj
@@ -7,17 +7,11 @@
;; You must not remove this notice, or any other, from this software.
(ns cljsh.core
- ;(:use (swank util core commands))
- (:require [clojure.main]
- [cljsh.complete]
- [cljsh.completion]
- ;;[swank.commands.completion]
- ))
+ (:require [clojure.main]))
-(defn jjj [] (cljsh.completion/potential-ns))
;; note that we have to keep this in sync with the project.clj entry
-(def lein-repls-version "1.4.0-SNAPSHOT")
+(def lein-repls-version "1.5.0-SNAPSHOT")
(defn current-thread [] (. Thread currentThread))
@@ -79,7 +73,6 @@
(swap! *repl-result-print-map* assoc (current-thread) print-fun)
print-fun)
-
(defn repl-result-print
"returns the print-function that is mapped to the current thread"
[]
@@ -91,25 +84,22 @@
(set-repl-result-print prn)
;; by default turn printing off
(set-repl-result-print (fn [&a]))))))
-
;; this function setting is used inside of the repl(s) code
;; indirection needed because of all the delayed loading thru quoting
(def ^:dynamic *repl-result-print* (fn [a] ((repl-result-print) a)))
-;; see if we can redirect the error messages...
+;; redirect the error messages from the stacktraces...
+
;;if we do not want a repl-prompt, we infer that we do not want the error messages to stderr but to the *console-err*
;; any errors in scripts passed to cljsh will be shown on the console.
+;; still the reader's error messages are not redirected... todo.
-(defn cljsh-repl-caught [e]
+(defn cljsh-repl-caught
+ "Set in leiningen.repls/repl-options and hooks in before clojure.main/repl-caught to redirect stderr if needed. If no repl prompt, then redirect the stderr to the console's, otherwise just forward."
+ [e]
(if (= (get @*repl-thread-prompt-map* (current-thread)) repl-nil-prompt)
(binding [*err* cljsh.core/*console-err*]
(clojure.main/repl-caught e))
(clojure.main/repl-caught e)))
-
-;;
-(defn completion-words []
- (let [completions (mapcat (comp keys ns-publics) (all-ns))
- all-completions (concat completions ['if 'def])] ;; special forms
- (println (apply str (interpose \newline (sort all-completions))))))

0 comments on commit 340ac70

Please sign in to comment.
Something went wrong with that request. Please try again.