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_HASHERS -- "sha2-512 sha512" by default.
46 Newline-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_HASHERS)]} {
62 foreach pair [split $env(DSC_HASHERS) "\n"] {
63 set idx [string first " " $pair]
68 [string range $pair 0 [expr {$idx-1}]] \
69 [string range $pair [expr {$idx+1}] end]
72 set Hashers [list sha2-512 sha512]
75 proc readents {root typ} {
76 set ents [glob -directory $root -tails -nocomplain -- *]
77 set ents [lmap d $ents {if {[file type $root/$d] == $typ} {set d} {continue}}]
81 proc walk {root typ} {
83 set root [string trimright $root /]
84 foreach s [readents $root $typ] {
86 lappend rv {*}[walk $root/$s $typ]
98 proc find-opt-schema {opt} {
101 set opt [string trimright $opt /]
102 foreach e [split $opt /] {
103 if {[file exists $Schema/$pth/$e]} {
107 if {[file exists $Schema/$pth/*]} {
111 puts stderr "can not find $opt in schema"
114 return [string range $pth 2 end]
119 return [file exists "$Schema/[find-opt-schema $opt]/bin"]
122 proc run-checker {opt v} {
124 set fh [open [list "|$Schema/[find-opt-schema $opt]/check" $opt 2>@1 << $v]]
126 if {[catch {close $fh}]} {
127 puts -nonewline stderr $v
134 proc assure-exists {opt} {
136 if {![file exists $Stash/$opt]} {
137 puts stderr "not found"
142 proc txtar-fn {line} {
144 [string length $line] > 6 &&
145 [string range $line 0 2] == "-- " &&
146 [string range $line end-2 end] == " --"
148 return [string range $line 3 end-3]
153 proc path-sanitize {fn} {
154 if {[string index $fn 0] == "/"} {
155 puts stderr "absolute paths are forbidden"
158 foreach e [file split $fn] {
160 puts stderr "relative paths are forbidden"
166 proc assure-all-list-params-exist {opt {offset 0}} {
168 set pth [find-opt-schema $opt]
169 set idx [lindex [lsearch -all [file split $pth] "*"] end-$offset]
171 set pth [file join {*}[lrange [file split $opt] 0 $idx]]
172 if {![file exists $Stash/$pth]} {
173 puts stderr "$pth does not exist"
179 set opt [lindex $argv 1]
180 switch [lindex $argv 0] {
183 set prefix [lindex $argv 1]
184 if {$argc > 1 && [lindex $argv 1] == "-v"} {
186 set prefix [lindex $argv 2]
188 foreach opt [walk $Schema directory] {
189 if {![file exists $opt/title]} {continue}
190 set name [string range $opt [expr {[string length $Schema] + 1}] end]
192 if {[string range $name 0 [string length $prefix]-1] != $prefix} {
196 set v [fileread $opt/title]
197 puts -nonewline "$name\t$v"
199 if {[file exists $opt/descr]} {
200 set lines [split [fileread $opt/descr] "\n"]
201 set lines [lrange $lines 0 end-1]
202 foreach line $lines {
210 assure-all-list-params-exist $opt 1
211 set dir [file dirname $opt]
212 set tail [run-checker $opt [file tail $opt]]
213 set tail [string trimright $tail "\n"]
214 file mkdir $Stash/$dir/$tail
221 file delete -force $Stash/$opt
224 assure-all-list-params-exist $opt
225 if {[llength $argv] > 2} {
226 set v [lindex $argv 2]
228 file delete $Stash/$opt
232 file mkdir "$Stash/[file dirname $opt]"
234 set fh [open $Stash/$opt w]
235 fconfigure $fh -translation binary
236 fconfigure stdin -translation binary
240 set v [run-checker $opt $v]
241 set fh [open $Stash/$opt w]
242 puts -nonewline $fh $v
250 puts [find-opt-schema $opt]
253 if {[file tail $opt] == "*"} {
254 set opt [file dirname $opt]
255 if {![file exists $Stash/$opt]} {
258 foreach dir [readents $Stash/$opt directory] {
263 if {[file exists $Stash/$opt]} {
265 set fh [open $Stash/$opt]
266 fconfigure $fh -translation binary
267 fconfigure stdout -translation binary
271 puts -nonewline [fileread $Stash/$opt]
275 puts -nonewline [run-checker $opt ""]
278 set pth [find-opt-schema $opt]
279 if {[file exists $Schema/$pth/apply]} {
280 puts stderr "applying $opt..."
281 if {[catch {exec {*}[list "$Schema/$pth/apply" $opt] >@stdout 2>@stderr}]} {
286 if {[file exists $Schema/$pth/*]} {
288 foreach dir [readents $Stash/$opt directory] {
289 if {[catch {exec {*}[list dsc apply $opt/$dir] >@stdout 2>@stderr}]} {
295 foreach ent [readents $Schema/$opt directory] {
296 if {[catch {exec {*}[list dsc apply $opt/$ent] >@stdout 2>@stderr}]} {
302 set dirsSaved [file tempfile dirsSaved.XXXXXX]
303 set fh [open $dirsSaved w]
304 foreach fn [walk $Saved/$opt directory] {
305 puts $fh [string range $fn [string length $Saved]+1 end]
308 set dirsStash [file tempfile dirsStash.XXXXXX]
309 set fh [open $dirsStash w]
310 foreach fn [walk $Stash/$opt directory] {
311 puts $fh [string range $fn [string length $Stash]+1 end]
314 set fh [open [list |diff -u -L dirs -L dirs $dirsSaved $dirsStash]]
315 puts -nonewline [read $fh]
317 file delete $dirsSaved
318 file delete $dirsStash
319 set fh [open [list |diff -urN $Saved/$opt $Stash/$opt]]
320 set prefixSaved "--- $Saved/"
321 set prefixSavedLen [string length $prefixSaved]
322 set prefixStash "+++ $Stash/"
323 set prefixStashLen [string length $prefixStash]
324 while {[gets $fh line] >= 0} {
325 if {[string range $line 0 3] == "diff"} {
328 if {[string range $line 0 $prefixSavedLen-1] == $prefixSaved} {
329 puts "--- [string range $line $prefixSavedLen end]"
332 if {[string range $line 0 $prefixStashLen-1] == $prefixStash} {
333 puts "+++ [string range $line $prefixStashLen end]"
341 catch {file delete -force $Stash/$opt}
342 catch {exec {*}[list cp -a $Saved/$opt $Stash/$opt]}
345 file delete -force $Saved.bak
346 set tmp $Saved.[expr {int(rand() * 1000000)}]
347 exec {*}[list cp -a $Stash $tmp]
349 file rename $Saved $Saved.bak
350 file rename $tmp $Saved
351 file delete -force $Saved.bak
355 foreach {name cmd} $Hashers {
357 set fh [open [list |$cmd >@$w] w]
358 fconfigure $fh -translation binary
359 lappend hsh $name $fh $r $w
361 fconfigure stdin -translation binary
362 while {![eof stdin]} {
363 set buf [read stdin $CopyBufLen]
364 foreach {_ $fh _ _} $hsh {
365 puts -nonewline $fh $buf
368 foreach {name fh r w} $hsh {
373 puts "$name [lindex $v 0]"
377 set dirs [walk $Saved/$opt directory]
379 set dirs [list $Saved/$opt {*}$dirs]
383 puts [string range $fn [string length $Saved]+1 end]
386 foreach fn [walk $dir file] {
387 set sfn [string range $fn [string length $Saved]+1 end]
389 puts "-- $sfn:base64 --"
390 set fh [open "|base64 $fn"]
396 while {[gets $fh line] >= 0} {
397 if {[txtar-fn $line] != ""} {
398 set line "-- $line --"
408 set exporter [open [list |$argv0 export-raw $opt]]
409 fconfigure $exporter -translation binary
411 set hasher [open [list |$argv0 csum >@$w] w]
412 fconfigure $hasher -translation binary
413 fconfigure stdout -translation binary
414 while {![eof $exporter]} {
415 set buf [read $exporter $CopyBufLen]
416 puts -nonewline stdout $buf
417 puts -nonewline $hasher $buf
422 puts -nonewline [read $r]
425 fconfigure stdin -translation binary
426 while {[gets stdin line] >= 0} {
427 set fn [txtar-fn $line]
432 if {$fn == ".dirs"} {
433 while {[gets stdin line] >= 0} {
434 set fn [txtar-fn $line]
437 file mkdir $Stash/$line
446 if {[string range $fn [expr {[string length $fn]-7}] end] == ":base64"} {
448 set fn [string range $fn 0 [expr {[string length $fn]-7-1}]]
451 file mkdir [file dirname $Stash/$fn]
453 set fh [open [list |base64 -d > $Stash/$fn] w]
455 set fh [open $Stash/$fn w]
456 fconfigure $fh -translation binary
461 while {[gets stdin line] >= 0} {
462 set fn [txtar-fn $line]
465 } elseif {[string range $fn 0 2] == "-- "} {
467 } elseif {$fn == ".csum"} {
479 set hasher [open [list |$argv0 csum >@$w] w]
480 fconfigure $hasher -translation binary
481 fconfigure stdin -translation binary
482 while {[gets stdin line] >= 0} {
483 if {$line == "-- .csum --"} {
495 puts stderr "integrity failure"
496 puts -nonewline stderr $got
498 puts -nonewline stderr $exp
502 exec $argv0 import-check <$opt
503 exec $argv0 import-pipe <$opt
506 if {[file exists $Stash/$opt]} {
507 puts [file normalize $Stash/$opt]
511 puts stderr "unknown cmd"