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