2511142e9a9ef3804548e74d9ba6305de42363f3
[rrq/newlisp-ftw.git] / enitool.lsp
1 #!/usr/bin/newlisp
2 #
3 # Helper tool to browse and edit the ifupdown configuration
4 # uses iselect, ed, nano and sudo
5 #
6 # Extra iselect commands:
7 # # = toggle commenting of configuration block
8 # d = delete empty line
9 # e = edit the whole file
10 #
11 # right-arrow or return = follow to sourced file, or edit the current block
12 # left-arrow or q = go up or exit
13
14 ;(signal 2 (fn (x) (exit)))
15
16 (constant
17  ;; all "block starters", including blank lines
18  'ENI-KEY '( "iface" "mapping" "auto" "allow-\\w*" "rename"
19              "source" "source-directory" "$")
20  ;; regex to identify block starters
21  'ENI-HEAD (format "^\\s*#?\\s*(%s)" (join ENI-KEY "|"))
22  'ENI-COMMENT "^\\s*#"
23  'PROC (if (exec (format "command -v %s" (or (env "EDITOR") "nano"))) ($it 0)
24          "/bin/nano")
25  'SUDO (if (exec "command -v sudo") ($it 0) "")
26  )
27
28 (define (is-eni-key PAT S)
29   (when (regex PAT S 0) true))
30
31 (define (is-eni-comment S)
32   (is-eni-key ENI-COMMENT S)
33   )
34
35 (define (istrue? A B)
36   (list A B) (= A B true))
37
38 (define (eni-starters) ; DATA
39   (flat (ref-all ENI-HEAD DATA is-eni-key)))
40
41 ;; Pull out the block headed by the B line. If this head is a blank
42 ;; line, then the block includes preceeding comment and the blank line
43 ;; only. Otherwise it includes preceeding comment, head line and fthe
44 ;; following mix of non-head lines and comment lines (i.e. up to next
45 ;; head line). FROM is the line after the prior block, and it is moved
46 ;; to end of this block.
47 (define (sub-divide-DATA B (E (length DATA))) ; DATA FROM
48   (let ((SLICE (fn (F B E) (append (list F B E) (slice DATA F (- E F))))))
49     (SLICE FROM B (set 'FROM (if (empty? (DATA B)) (+ B 1) E)))))
50
51 ;; Read the "interfaces file" and split up into its "blocks"
52 ;; <block> <commentline>* <headline> ( <otherline> | <commentline> )*
53 (define (read-eni NAME)
54   (letn ((DATA (parse (read-file NAME) "\n"))
55          (BEG (eni-starters))
56          (FROM 0))
57     (setf LASTENI (map sub-divide-DATA BEG (1 BEG)))
58     ;;(map println LASTENI)
59     ;;(read-line)
60     LASTENI
61     ))
62
63 (define (add-selector X)
64   (cons (format "<s:%d>%s" (X 0) (X 1)) (2 X)))
65
66 ;; Find the definition block spanning line I in file FILE
67 (define (find-block I FILE)
68   ;;(println (list 'find-block I FILE))
69   (exists (fn (B) (< I (B 2))) (read-eni FILE)))
70
71 ############################################################
72 ### Interactive actions
73
74 ## PATH holds interactive state as a stack of [pos file]
75 (setf PATH '(( 0 "/etc/network/interfaces" )) )
76
77 ; Edit a file
78 (define (edit-file I FILE)
79   (wait-pid (process (format "%s %s +%d %s" SUDO PROC (int I) FILE))))
80
81 (define (ensure-newline TXT)
82   (if (empty? TXT) "" (ends-with TXT "\n") TXT (string TXT "\n")))
83
84 (define (update-file B E TXT FILE)
85   ;;(println (list 'update-file B E TXT FILE))
86   (let ((DATA (parse (read-file FILE) "\n")))
87     (write-file TXT (string (join (0 B DATA) "\n" true)
88                             (ensure-newline (read-file TXT))
89                             (join (E DATA) "\n")))
90     (exec (format "%s mv %s %s" SUDO TXT FILE))
91     ))
92
93 (define (key-command-select I FILE) ; PATH
94   (letn ((BLOCK (find-block (- (int I) 1) FILE))
95          (TMP "/tmp/enitool/tmp.conf")
96          (HEAD (BLOCK (- (BLOCK 1) (BLOCK 0) -3)))
97          (TAG (or (and (regex "^#?(\\w*) (.*)" HEAD 0) $1) "#"))
98          (VALUE $2))
99     (case TAG
100       ("source" (push (list 0 VALUE) PATH))
101       (true (write-file TMP (join (3 BLOCK) "\n" true))
102             (let ((F (file-info TMP 6)))
103               (edit-file 1 TMP)
104               (when (!= F (file-info TMP 6))
105                 (update-file (BLOCK 0) (BLOCK 2) TMP FILE)))
106             ))
107     ))
108
109 (define (delete-block-maybe I FILE)
110   (let ((BLOCK (find-block (- (int I) 1) FILE))
111         (TMP "/tmp/enitool/tmp.conf"))
112     (when (= (3 BLOCK) '(""))
113       (exec (format "%s ed %s" SUDO FILE)
114             (format "%dd\nw\n" (+ 1 (BLOCK 0)))))))
115
116 (define (toggle-commenting I FILE)
117   (let ((BLOCK (find-block (- (int I) 1) FILE))
118         (TMP "/tmp/enitool/tmp.conf"))
119     (letn ((H (- (BLOCK 1) (BLOCK 0)))
120            (TXT (3 BLOCK))
121            (toggle (if (starts-with (TXT H) "#")
122                        (fn (X) (if (starts-with X "#") (1 X) X))
123                      (fn (X) (string "#" X)))))
124       (write-file TMP (ensure-newline
125                        (string (join (0 H TXT) "\n" true)
126                                (join (map toggle (H TXT)) "\n"))))
127       (update-file (BLOCK 0) (BLOCK 2) TMP FILE)
128       )))
129
130 (define (command-dispatch CMD FILE)
131   (when (regex "([^:]+):([^:]+):(\\S*)\\s*(.*)" CMD 0)
132     (setf (PATH 0 0) (int $1))
133     (cond
134      ((member $2 '("KEY_RIGHT" "RETURN")) (key-command-select $1 FILE))
135      ((= $2 "d") (delete-block-maybe $1 FILE))
136      ((= $2 "#") (toggle-commenting $1 FILE))
137      ((= $2 "e") (edit-file $1 FILE SUDO))
138      )))
139
140 (define (iselect POS FILE)
141   (exec (format "iselect -n '%s' -t '%s' -a -P -K '-k#' -kd -ke -p %d < %s"
142                 "enitool" FILE (int POS) FILE)))
143
144 (change-dir "/etc/network")
145 (wait-pid (process (format "%s mkdir -m 777 -p /tmp/enitool" SUDO)))
146
147 (while PATH
148   (let ((SEL (apply iselect (PATH 0))) (FILE (PATH 0 1)))
149     (if SEL (command-dispatch (SEL 0) FILE)
150       (pop PATH))
151     ))
152
153 (exit 0)