#!/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))) (when (!= "0" (if (exec "id -u") ($it 0) "")) (let ((SUDO (if (exec "command -v sudo") ($it 0) "/usr/bin/sudo")) (ED (format "env EDITOR=%s" (or (env "EDITOR") "nano"))) ) (wait-pid (process (join (flat (list SUDO ED (main-args))) " "))) (exit 0))) (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*#" 'EDITOR (or (env "EDITOR") "nano") 'PROC (if (exec (format "command -v %s" EDITOR)) ($it 0) "/bin/nano") ) (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 the ;; 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" ;; * ( | )* (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" (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) (wait-pid (process (format "%s +%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) (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 "mv %s %s" TXT FILE)) )) (define (key-command-select I FILE) ; PATH (letn ((BLOCK (find-block (- (int I) 1) FILE)) (TMP "/tmp/enitool/tmp.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/tmp.conf")) (when (= (3 BLOCK) '("")) (exec (format "ed -s %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/tmp.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)) ))) (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))) (change-dir "/etc/network") (make-dir "/tmp/enitool") (while PATH (if (apply iselect (PATH 0)) (command-dispatch ($it 0) (PATH 0 1)) (pop PATH))) (exit 0)