2 # dsc -- damn small configuration manager
3 # Copyright (C) 2025-2026 Sergey Matveev <stargrave@stargrave.org>
4 # 2025-2026 Vladimir Bobrov
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.
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.
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/>.
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-check <file.txtar -- verify configuration's checksum
35 dsc import-pipe <file.txtar -- import configuration
36 dsc import file.txtar -- verify and import configuration
37 dsc path opt -- get full path to option's value (if set)
38 dsc apply prefix -- apply configuration on given prefix
39 dsc csum <data -- compute data's checksum
41 Environmental variables:
42 $DSC_SCHEMA -- path to the schema definition
43 $DSC_STASH -- path to stashed/unsaved state
44 $DSC_SAVED -- path to committed/saved state
45 $DSC_HASHES -- sha2-512:sha512 by default
46 Comma-separated list of hash-name:cmd pairs
48 There are two kinds of options:
49 * array/list ones, which are identified with /*/ in "list"'s
50 output. "add", "del", "get opt/*" commands apply
51 * ordinary scalar ones, which can be "set", "get opt"
56 set CopyBufLen [expr {128 * 1024}]
57 if {[info exists env(DSC_SCHEMA)]} {set Schema $env(DSC_SCHEMA)} {set Schema schema}
58 if {[info exists env(DSC_STASH)]} {set Stash $env(DSC_STASH)} {set Stash stash}
59 if {[info exists env(DSC_SAVED)]} {set Saved $env(DSC_SAVED)} {set Saved saved}
60 if {[info exists env(DSC_HASHES)]} {
62 foreach pair [split $env(DSC_HASHES) ,] {
63 set cols [split $pair :]
64 if {[llength $cols] != 2} {
65 error "bad DSC_HASHES"
67 lappend Hashes [lindex $cols 0] [lindex $cols 1]
70 set Hashes [list sha2-512 sha512]
73 proc readents {root typ} {
74 set ents [glob -directory $root -tails -nocomplain -- *]
75 set ents [lmap d $ents {if {[file type $root/$d] == $typ} {set d} {continue}}]
79 proc walk {root typ} {
81 set root [string trimright $root /]
82 foreach s [readents $root $typ] {
84 lappend rv {*}[walk $root/$s $typ]
96 proc find-opt-schema {opt} {
99 set opt [string trimright $opt /]
100 foreach e [split $opt /] {
101 if {[file exists $Schema/$pth/$e]} {
105 if {[file exists $Schema/$pth/*]} {
109 puts stderr "can not find $opt in schema"
112 return [string range $pth 2 end]
117 return [file exists "$Schema/[find-opt-schema $opt]/bin"]
120 proc run-checker {opt v} {
122 set fh [open [list "|$Schema/[find-opt-schema $opt]/check" $opt 2>@1 << $v]]
124 if {[catch {close $fh}]} {
125 puts -nonewline stderr $v
132 proc assure-exists {opt} {
134 if {![file exists $Stash/$opt]} {
135 puts stderr "not found"
140 proc txtar-fn {line} {
142 [string length $line] > 6 &&
143 [string range $line 0 2] == "-- " &&
144 [string range $line end-2 end] == " --"
146 return [string range $line 3 end-3]
151 proc path-sanitize {fn} {
152 if {[string index $fn 0] == "/"} {
153 puts stderr "absolute paths are forbidden"
156 foreach e [file split $fn] {
158 puts stderr "relative paths are forbidden"
164 proc assure-all-list-params-exist {opt {offset 0}} {
166 set pth [find-opt-schema $opt]
167 set idx [lindex [lsearch -all [file split $pth] "*"] end-$offset]
169 set pth [file join {*}[lrange [file split $opt] 0 $idx]]
170 if {![file exists $Stash/$pth]} {
171 puts stderr "$pth does not exist"
177 set opt [lindex $argv 1]
178 switch [lindex $argv 0] {
181 set prefix [lindex $argv 1]
182 if {$argc > 1 && [lindex $argv 1] == "-v"} {
184 set prefix [lindex $argv 2]
186 foreach opt [walk $Schema directory] {
187 if {![file exists $opt/title]} {continue}
188 set name [string range $opt [expr {[string length $Schema] + 1}] end]
190 if {[string range $name 0 [string length $prefix]-1] != $prefix} {
194 set v [fileread $opt/title]
195 puts -nonewline "$name\t$v"
197 if {[file exists $opt/descr]} {
198 set lines [split [fileread $opt/descr] "\n"]
199 set lines [lrange $lines 0 end-1]
200 foreach line $lines {
208 assure-all-list-params-exist $opt 1
209 set dir [file dirname $opt]
210 set tail [run-checker $opt [file tail $opt]]
211 set tail [string trimright $tail "\n"]
212 file mkdir $Stash/$dir/$tail
219 file delete -force $Stash/$opt
222 assure-all-list-params-exist $opt
223 if {[llength $argv] > 2} {
224 set v [lindex $argv 2]
226 file delete $Stash/$opt
230 file mkdir "$Stash/[file dirname $opt]"
232 set fh [open $Stash/$opt w]
233 fconfigure $fh -translation binary
234 fconfigure stdin -translation binary
238 set v [run-checker $opt $v]
239 set fh [open $Stash/$opt w]
240 puts -nonewline $fh $v
248 puts [find-opt-schema $opt]
251 if {[file tail $opt] == "*"} {
252 set opt [file dirname $opt]
253 if {![file exists $Stash/$opt]} {
256 foreach dir [readents $Stash/$opt directory] {
261 if {[file exists $Stash/$opt]} {
263 set fh [open $Stash/$opt]
264 fconfigure $fh -translation binary
265 fconfigure stdout -translation binary
269 puts -nonewline [fileread $Stash/$opt]
273 puts -nonewline [run-checker $opt ""]
276 set pth [find-opt-schema $opt]
277 if {[file exists $Schema/$pth/apply]} {
278 puts stderr "applying $opt..."
279 if {[catch {exec {*}[list "$Schema/$pth/apply" $opt] >@stdout 2>@stderr}]} {
284 if {[file exists $Schema/$pth/*]} {
286 foreach dir [readents $Stash/$opt directory] {
287 if {[catch {exec {*}[list dsc apply $opt/$dir] >@stdout 2>@stderr}]} {
293 foreach ent [readents $Schema/$opt directory] {
294 if {[catch {exec {*}[list dsc apply $opt/$ent] >@stdout 2>@stderr}]} {
300 set dirsSaved [file tempfile dirsSaved.XXXXXX]
301 set fh [open $dirsSaved w]
302 foreach fn [walk $Saved/$opt directory] {
303 puts $fh [string range $fn [string length $Saved]+1 end]
306 set dirsStash [file tempfile dirsStash.XXXXXX]
307 set fh [open $dirsStash w]
308 foreach fn [walk $Stash/$opt directory] {
309 puts $fh [string range $fn [string length $Stash]+1 end]
312 set fh [open [list |diff -u -L dirs -L dirs $dirsSaved $dirsStash]]
313 puts -nonewline [read $fh]
315 file delete $dirsSaved
316 file delete $dirsStash
317 set fh [open [list |diff -urN $Saved/$opt $Stash/$opt]]
318 set prefixSaved "--- $Saved/"
319 set prefixSavedLen [string length $prefixSaved]
320 set prefixStash "+++ $Stash/"
321 set prefixStashLen [string length $prefixStash]
322 while {[gets $fh line] >= 0} {
323 if {[string range $line 0 3] == "diff"} {
326 if {[string range $line 0 $prefixSavedLen-1] == $prefixSaved} {
327 puts "--- [string range $line $prefixSavedLen end]"
330 if {[string range $line 0 $prefixStashLen-1] == $prefixStash} {
331 puts "+++ [string range $line $prefixStashLen end]"
339 catch {file delete -force $Stash/$opt}
340 catch {exec {*}[list cp -a $Saved/$opt $Stash/$opt]}
343 file delete -force $Saved.bak
344 set tmp $Saved.[expr {int(rand() * 1000000)}]
345 exec {*}[list cp -a $Stash $tmp]
347 file rename $Saved $Saved.bak
348 file rename $tmp $Saved
349 file delete -force $Saved.bak
353 foreach {name cmd} $Hashes {
355 set fh [open [list |$cmd >@$w] w]
356 fconfigure $fh -translation binary
357 lappend hsh $name $fh $r $w
359 fconfigure stdin -translation binary
360 while {![eof stdin]} {
361 set buf [read stdin $CopyBufLen]
362 foreach {_ $fh _ _} $hsh {
363 puts -nonewline $fh $buf
366 foreach {name fh r w} $hsh {
371 puts "$name [lindex $v 0]"
375 set dirs [walk $Saved/$opt directory]
377 set dirs [list $Saved/$opt {*}$dirs]
381 puts [string range $fn [string length $Saved]+1 end]
384 foreach fn [walk $dir file] {
385 set sfn [string range $fn [string length $Saved]+1 end]
387 puts "-- $sfn:base64 --"
388 set fh [open "|base64 $fn"]
394 while {[gets $fh line] >= 0} {
395 if {[txtar-fn $line] != ""} {
396 set line "-- $line --"
406 set exporter [open [list |$argv0 export-raw $opt]]
407 fconfigure $exporter -translation binary
409 set hasher [open [list |$argv0 csum >@$w] w]
410 fconfigure $hasher -translation binary
411 fconfigure stdout -translation binary
412 while {![eof $exporter]} {
413 set buf [read $exporter $CopyBufLen]
414 puts -nonewline stdout $buf
415 puts -nonewline $hasher $buf
420 puts -nonewline [read $r]
423 fconfigure stdin -translation binary
424 while {[gets stdin line] >= 0} {
425 set fn [txtar-fn $line]
430 if {$fn == ".dirs"} {
431 while {[gets stdin line] >= 0} {
432 set fn [txtar-fn $line]
435 file mkdir $Stash/$line
444 if {[string range $fn [expr {[string length $fn]-7}] end] == ":base64"} {
446 set fn [string range $fn 0 [expr {[string length $fn]-7-1}]]
449 file mkdir [file dirname $Stash/$fn]
451 set fh [open [list |base64 -d > $Stash/$fn] w]
453 set fh [open $Stash/$fn w]
454 fconfigure $fh -translation binary
459 while {[gets stdin line] >= 0} {
460 set fn [txtar-fn $line]
463 } elseif {[string range $fn 0 2] == "-- "} {
465 } elseif {$fn == ".csum"} {
477 set hasher [open [list |$argv0 csum >@$w] w]
478 fconfigure $hasher -translation binary
479 fconfigure stdin -translation binary
480 while {[gets stdin line] >= 0} {
481 if {$line == "-- .csum --"} {
493 puts stderr "integrity failure"
494 puts -nonewline stderr $got
496 puts -nonewline stderr $exp
500 exec $argv0 import-check <$opt
501 exec $argv0 import-pipe <$opt
504 if {[file exists $Stash/$opt]} {
505 puts [file normalize $Stash/$opt]
509 puts stderr "unknown cmd"