]> Cypherpunks repositories - dsc.git/blob - dsc
c89bfe4458a27befa774ec7d4e9ca7eac7aabe7471b01398ee6a129939b46cd0
[dsc.git] / dsc
1 #!/usr/bin/env tclsh
2 # dsc -- damn small configuration manager
3 # Copyright (C) 2025 Sergey Matveev <stargrave@stargrave.org>
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation, version 3 of the License.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
16
17 if {$argc == 0} {
18     puts -nonewline stderr {Usage:
19     dsc list [-v] [prefix] -- list all available options
20     dsc add opt       -- add /*/ section
21     dsc del opt       -- delete /*/ section
22     dsc del ""        -- delete the whole configuration, useful for import
23     dsc has opt       -- check if specified /*/ option exists
24     dsc set opt value -- set option's value
25     dsc set opt ""    -- unset value, make it default one
26     dsc set opt <data -- set binary option's value
27     dsc get opt       -- get option's value, maybe default one
28     dsc get opt/*     -- list /*/ sections
29     dsc diff [prefix] -- show the difference between saved and stash
30     dsc revert opt    -- revert opt's configuration
31     dsc commit        -- commit (save) configuration
32     dsc export [prefix] >file.txtar -- export (whole by default) configuration
33     dsc import          <file.txtar -- import it
34     dsc path opt      -- get full path to option's value (if set)
35
36 Environmental variables:
37     $DSC_SCHEMA -- path to the schema definition
38     $DSC_STASH  -- path to stashed/unsaved state
39     $DSC_SAVED  -- path to committed/saved state
40
41 There are two kinds of options:
42     * array/list ones, which are identified with /*/ in "list"'s
43       output. "add", "del", "get opt/*" commands apply
44     * ordinary scalar ones, which can be "set", "get opt"
45 }
46     exit 1
47 }
48
49 if {[catch {set Schema $env(DSC_SCHEMA)}]} {set Schema schema}
50 if {[catch {set Stash $env(DSC_STASH)}]} {set Stash stash}
51 if {[catch {set Saved $env(DSC_SAVED)}]} {set Saved saved}
52
53 proc walk {root typ} {
54     set rv [list]
55     set root [string trimright $root /]
56     set dirs [glob -directory $root -types $typ -tails -nocomplain -- *]
57     foreach s [lsort $dirs] {
58         lappend rv $root/$s
59         lappend rv {*}[walk $root/$s $typ]
60     }
61     return $rv
62 }
63
64 proc fileread {fn} {
65     set fh [open $fn]
66     set v [read $fh]
67     close $fh
68     return $v
69 }
70
71 proc find-opt-schema {opt} {
72     global Schema
73     set path .
74     set opt [string trimright $opt /]
75     foreach e [split $opt /] {
76         if {[file exists $Schema/$path/$e]} {
77             set path $path/$e
78             continue
79         }
80         if {[file exists $Schema/$path/*]} {
81             set path $path/*
82             continue
83         }
84         puts stderr "can not find $opt in schema"
85         exit 1
86     }
87     return [string range $path 2 end]
88 }
89
90 proc is-bin {opt} {
91     global Schema
92     return [file exists "$Schema/[find-opt-schema $opt]/bin"]
93 }
94
95 proc run-checker {opt v} {
96     global Schema
97     set fh [open |[list "$Schema/[find-opt-schema $opt]/check" $opt 2>@1] r+]
98     puts $fh $v
99     close $fh w
100     set v [read $fh]
101     if {[catch {close $fh}]} {
102         puts -nonewline stderr $v
103         exit 1
104     }
105     return $v
106 }
107
108 proc assure-exists {opt} {
109     global Stash
110     if {! [file exists $Stash/$opt]} {
111         puts stderr "not found"
112         exit 1
113     }
114 }
115
116 proc txtar-fn {line} {
117     if {
118         [string length $line] > 6 &&
119         [string range $line 0 2] == "-- " &&
120         [string range $line end-2 end] == " --"
121     } {
122         return [string range $line 3 end-3]
123     }
124     return ""
125 }
126
127 set opt [lindex $argv 1]
128 switch [lindex $argv 0] {
129     list {
130         set verbose n
131         set prefix [lindex $argv 1]
132         if {$argc > 1 && [lindex $argv 1] == "-v"} {
133             set verbose y
134             set prefix [lindex $argv 2]
135         }
136         foreach opt [walk $Schema d] {
137             if {! [file exists $opt/title]} {continue}
138             set name [string range $opt [expr {[string length $Schema] + 1}] end]
139             if {$prefix != ""} {
140                 if {[string range $name 0 [string length $prefix]-1] != $prefix} {
141                     continue
142                 }
143             }
144             set v [fileread $opt/title]
145             puts -nonewline "$name\t$v"
146             if {$verbose} {
147                 if {[file exists $opt/descr]} {
148                     set lines [split [fileread $opt/descr] "\n"]
149                     set lines [lrange $lines 0 end-1]
150                     foreach line $lines {
151                         puts "\t$line"
152                     }
153                 }
154             }
155         }
156     }
157     add {
158         set dir [file dirname $opt]
159         set tail [run-checker $opt [file tail $opt]]
160         set tail [string trimright $tail "\n"]
161         file mkdir $Stash/$dir/$tail
162         puts $dir/$tail
163     }
164     del {
165         if {$opt != ""} {
166             assure-exists $opt
167         }
168         file delete -force $Stash/$opt
169     }
170     set {
171         if {[llength $argv] > 2} {
172             set v [lindex $argv 2]
173             if {$v == ""} {
174                 file delete $Stash/$opt
175                 exit
176             }
177         }
178         file mkdir "$Stash/[file dirname $opt]"
179         if {[is-bin $opt]} {
180             set fh [open $Stash/$opt w]
181             fconfigure $fh -translation binary
182             fconfigure stdin -translation binary
183             fcopy stdin $fh
184             close $fh
185         } else {
186             set v [run-checker $opt $v]
187             set fh [open $Stash/$opt w]
188             puts -nonewline $fh $v
189             close $fh
190         }
191     }
192     has {
193         assure-exists $opt
194     }
195     get-checker {
196         puts [find-opt-schema $opt]
197     }
198     get {
199         if {[file tail $opt] == "*"} {
200             set opt [file dirname $opt]
201             if {! [file exists $Stash/$opt]} {
202                 exit
203             }
204             set dirs [glob -directory $Stash/$opt -types d -tails -nocomplain -- *]
205             foreach dir [lsort $dirs] {
206                 puts $dir
207             }
208             exit
209         }
210         if {[file exists $Stash/$opt]} {
211             if {[is-bin $opt]} {
212                 set fh [open $Stash/$opt r]
213                 fconfigure $fh -translation binary
214                 fconfigure stdout -translation binary
215                 fcopy $fh stdout
216                 close $fh
217             } else {
218                 puts -nonewline [fileread $Stash/$opt]
219             }
220             exit
221         }
222         puts -nonewline [run-checker $opt ""]
223     }
224     diff {
225         set fh [file tempfile dirsSaved]
226         foreach fn [walk $Saved/$opt d] {
227             puts $fh [string range $fn [string length $Saved]+1 end]
228         }
229         close $fh
230         set fh [file tempfile dirsStash]
231         foreach fn [walk $Stash/$opt d] {
232             puts $fh [string range $fn [string length $Stash]+1 end]
233         }
234         close $fh
235         set fh [open |[list diff -u -L dirs -L dirs $dirsSaved $dirsStash] r]
236         puts -nonewline [read $fh]
237         catch {close $fh}
238         file delete $dirsSaved
239         file delete $dirsStash
240         set fh [open |[list diff -urN $Saved/$opt $Stash/$opt] r]
241         set prefixSaved "--- $Saved/"
242         set prefixSavedLen [string length $prefixSaved]
243         set prefixStash "+++ $Stash/"
244         set prefixStashLen [string length $prefixStash]
245         while {[gets $fh line] >= 0} {
246             if {[string range $line 0 3] == "diff"} {
247                 continue
248             }
249             if {[string range $line 0 $prefixSavedLen-1] == $prefixSaved} {
250                 puts "--- [string range $line $prefixSavedLen end]"
251                 continue
252             }
253             if {[string range $line 0 $prefixStashLen-1] == $prefixStash} {
254                 puts "+++ [string range $line $prefixStashLen end]"
255                 continue
256             }
257             puts $line
258         }
259         catch {close $fh}
260     }
261     revert {
262         catch {file delete -force $Stash/$opt}
263         catch {file copy $Saved/$opt $Stash/$opt}
264     }
265     commit {
266         file delete -force $Saved.bak
267         set tmp $Saved.[expr {int(rand() * 1000000)}]
268         file copy $Stash $tmp
269         exec sync
270         file rename $Saved $Saved.bak
271         file rename $tmp $Saved
272         file delete -force $Saved.bak
273     }
274     export {
275         set dirs [walk $Saved/$opt d]
276         if {$opt != ""} {
277             set dirs [list $Saved/$opt {*}$dirs]
278         }
279         puts "-- .dirs --"
280         foreach fn $dirs {
281             puts [string range $fn [string length $Saved]+1 end]
282         }
283         foreach dir $dirs {
284             foreach fn [walk $dir f] {
285                 set sfn [string range $fn [string length $Saved]+1 end]
286                 if {[is-bin $sfn]} {
287                     puts "-- $sfn:base64 --"
288                     set fh [open "|base64 $fn" r]
289                     fcopy $fh stdout
290                     close $fh
291                 } else {
292                     puts "-- $sfn --"
293                     set fh [open $fn]
294                     while {[gets $fh line] >= 0} {
295                         if {[txtar-fn $line] != ""} {
296                             set line "-- $line --"
297                         }
298                         puts $line
299                     }
300                     close $fh
301                 }
302             }
303         }
304     }
305     import {
306         fconfigure stdin -translation binary
307         while {[gets stdin line] >= 0} {
308             set fn [txtar-fn $line]
309             if {$fn != ""} {
310                 break
311             }
312         }
313         if {$fn == ".dirs"} {
314             while {[gets stdin line] >= 0} {
315                 set fn [txtar-fn $line]
316                 if {$fn == ""} {
317                     file mkdir $Stash/$fn
318                 } else {
319                     break
320                 }
321             }
322         }
323         proc openfh {fn} {
324             set bin no
325             if {[string range $fn [expr {[string length $fn]-7}] end] == ":base64"} {
326                 set bin yes
327                 set fn [string range $fn 0 [expr {[string length $fn]-7-1}]]
328             }
329             global Stash
330             file mkdir [file dirname $Stash/$fn]
331             if {$bin} {
332                 set fh [open |[list base64 -d > $Stash/$fn] w]
333             } else {
334                 set fh [open $Stash/$fn w]
335                 fconfigure $fh -translation binary
336             }
337             return $fh
338         }
339         set fh [openfh $fn]
340         while {[gets stdin line] >= 0} {
341             set fn [txtar-fn $line]
342             if {$fn == ""} {
343                 puts $fh $line
344             } elseif {[string range $fn 0 2] == "-- "} {
345                 puts $fh $fn
346             } else {
347                 close $fh
348                 set fh [openfh $fn]
349             }
350         }
351         close $fh
352         exec sync
353     }
354     path {
355         if {[file exists $Stash/$opt]} {
356             puts [file normalize $Stash/$opt]
357         }
358     }
359 }