added
authorRalph Ronnquist <ralph.ronnquist@gmail.com>
Fri, 10 Dec 2021 02:42:20 +0000 (13:42 +1100)
committerRalph Ronnquist <ralph.ronnquist@gmail.com>
Fri, 10 Dec 2021 02:42:20 +0000 (13:42 +1100)
enitool.lsp [new file with mode: 0755]

diff --git a/enitool.lsp b/enitool.lsp
new file mode 100755 (executable)
index 0000000..0efd797
--- /dev/null
@@ -0,0 +1,149 @@
+#!/usr/bin/newlisp
+#
+# Helper tool to browse and edit the ifupdown configuration
+# uses iselect, ed, nano and sudo
+#
+# Extra iselect commands:
+# # = toggle commenting of configuration block
+# d = delete empty line
+# e = edit the whole file
+#
+# right-arrow or return = follow to sourced file, or edit the current block
+# left-arrow or q = go up or exit
+
+;(signal 2 (fn (x) (exit)))
+
+(constant
+ ;; all "block starters", including blank lines
+ 'ENI-KEY '( "iface" "mapping" "auto" "allow-\\w*" "rename"
+             "source" "source-directory" "$")
+ ;; regex to identify block starters
+ 'ENI-HEAD (format "^\\s*#?\\s*(%s)" (join ENI-KEY "|"))
+ 'ENI-COMMENT "^\\s*#"
+ )
+
+(define (is-eni-key PAT S)
+  (when (regex PAT S 0) true))
+
+(define (is-eni-comment S)
+  (is-eni-key ENI-COMMENT S)
+  )
+
+(define (istrue? A B)
+  (list A B) (= A B true))
+
+(define (eni-starters) ; DATA
+  (flat (ref-all ENI-HEAD DATA is-eni-key)))
+
+;; Pull out the block headed by the B line. If this head is a blank
+;; line, then the block includes preceeding comment and the blank line
+;; only. Otherwise it includes preceeding comment, head line and fthe
+;; following mix of non-head lines and comment lines (i.e. up to next
+;; head line). FROM is the line after the prior block, and it is moved
+;; to end of this block.
+(define (sub-divide-DATA B (E (length DATA))) ; DATA FROM
+  (let ((SLICE (fn (F B E) (append (list F B E) (slice DATA F (- E F))))))
+    (SLICE FROM B (set 'FROM (if (empty? (DATA B)) (+ B 1) E)))))
+
+;; Read the "interfaces file" and split up into its "blocks"
+;; <block> <commentline>* <headline> ( <otherline> | <commentline> )*
+(define (read-eni NAME)
+  (letn ((DATA (parse (read-file NAME) "\n"))
+         (BEG (eni-starters))
+         (FROM 0))
+    (setf LASTENI (map sub-divide-DATA BEG (1 BEG)))
+    ;;(map println LASTENI)
+    ;;(read-line)
+    LASTENI
+    ))
+
+(define (add-selector X)
+  (cons (format "<s:%d>%s" (X 0) (X 1)) (2 X)))
+
+;; Find the definition block spanning line I in file FILE
+(define (find-block I FILE)
+  ;;(println (list 'find-block I FILE))
+  (exists (fn (B) (< I (B 2))) (read-eni FILE)))
+
+############################################################
+### Interactive actions
+
+## PATH holds interactive state as a stack of [pos file]
+(setf PATH '(( 0 "/etc/network/interfaces" )) )
+
+; Edit a file
+(define (edit-file I FILE SUDO)
+  (let ((PROC (if SUDO "/usr/bin/sudo /bin/nano" "/bin/nano")))
+    (wait-pid (process (format "%s -F -J 75 -I -l +%d %s" PROC (int I) FILE)))
+    ))
+
+(define (ensure-newline TXT)
+  (if (empty? TXT) "" (ends-with TXT "\n") TXT (string TXT "\n")))
+
+(define (update-file B E TXT FILE)
+  ;;(println (list 'update-file B E TXT FILE))
+  (let ((DATA (parse (read-file FILE) "\n")))
+    (write-file TXT (string (join (0 B DATA) "\n" true)
+                            (ensure-newline (read-file TXT))
+                            (join (E DATA) "\n")))
+    (exec (format "sudo mv %s %s" TXT FILE))
+    ))
+
+(define (key-command-select I FILE) ; PATH
+  (letn ((BLOCK (find-block (- (int I) 1) FILE))
+         (TMP "/tmp/enitool.conf")
+         (HEAD (BLOCK (- (BLOCK 1) (BLOCK 0) -3)))
+         (TAG (or (and (regex "^#?(\\w*) (.*)" HEAD 0) $1) "#"))
+         (VALUE $2))
+    (case TAG
+      ("source" (push (list 0 VALUE) PATH))
+      (true (write-file TMP (join (3 BLOCK) "\n" true))
+            (let ((F (file-info TMP 6)))
+              (edit-file 1 TMP)
+              (when (!= F (file-info TMP 6))
+                (update-file (BLOCK 0) (BLOCK 2) TMP FILE)))
+            ))
+    ))
+
+(define (delete-block-maybe I FILE)
+  (let ((BLOCK (find-block (- (int I) 1) FILE))
+        (TMP "/tmp/enitool.conf"))
+    (when (= (3 BLOCK) '(""))
+      (exec (format "sudo ed %s" FILE)
+            (format "%dd\nw\n" (+ 1 (BLOCK 0)))))))
+
+(define (toggle-commenting I FILE)
+  (let ((BLOCK (find-block (- (int I) 1) FILE))
+        (TMP "/tmp/enitool.conf"))
+    (letn ((H (- (BLOCK 1) (BLOCK 0)))
+           (TXT (3 BLOCK))
+           (toggle (if (starts-with (TXT H) "#")
+                       (fn (X) (if (starts-with X "#") (1 X) X))
+                     (fn (X) (string "#" X)))))
+      (write-file TMP (ensure-newline
+                       (string (join (0 H TXT) "\n" true)
+                               (join (map toggle (H TXT)) "\n"))))
+      (update-file (BLOCK 0) (BLOCK 2) TMP FILE)
+      )))
+
+(define (command-dispatch CMD FILE)
+  (when (regex "([^:]+):([^:]+):(\\S*)\\s*(.*)" CMD 0)
+    (setf (PATH 0 0) (int $1))
+    (cond
+     ((member $2 '("KEY_RIGHT" "RETURN")) (key-command-select $1 FILE))
+     ((= $2 "d") (delete-block-maybe $1 FILE))
+     ((= $2 "#") (toggle-commenting $1 FILE))
+     ((= $2 "e") (edit-file $1 FILE "sudo"))
+     )))
+
+(define (iselect POS FILE)
+  (exec (format "iselect -n '%s' -t '%s' -a -P -K '-k#' -kd -ke -p %d < %s"
+                "enitool" FILE (int POS) FILE)))
+
+(while PATH
+  (let ((SEL (apply iselect (PATH 0))) (FILE (PATH 0 1)))
+    (if SEL (command-dispatch (SEL 0) FILE)
+      (pop PATH))
+    ))
+
+(exit 0)