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