2 # dsc -- damn small configuration manager
3 # Copyright (C) 2025 Sergey Matveev <stargrave@stargrave.org>
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 <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
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
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"
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}
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}}]
61 proc walk {root typ} {
63 set root [string trimright $root /]
64 foreach s [readents $root $typ] {
66 lappend rv {*}[walk $root/$s $typ]
78 proc find-opt-schema {opt} {
81 set opt [string trimright $opt /]
82 foreach e [split $opt /] {
83 if {[file exists $Schema/$pth/$e]} {
87 if {[file exists $Schema/$pth/*]} {
91 puts stderr "can not find $opt in schema"
94 return [string range $pth 2 end]
99 return [file exists "$Schema/[find-opt-schema $opt]/bin"]
102 proc run-checker {opt v} {
104 set fh [open |[list "$Schema/[find-opt-schema $opt]/check" $opt 2>@1 << $v] r]
106 if {[catch {close $fh}]} {
107 puts -nonewline stderr $v
114 proc assure-exists {opt} {
116 if {![file exists $Stash/$opt]} {
117 puts stderr "not found"
122 proc txtar-fn {line} {
124 [string length $line] > 6 &&
125 [string range $line 0 2] == "-- " &&
126 [string range $line end-2 end] == " --"
128 return [string range $line 3 end-3]
133 proc assure-all-list-params-exist {opt {offset 0}} {
135 set pth [find-opt-schema $opt]
136 set idx [lindex [lsearch -all [file split $pth] "*"] end-$offset]
138 set pth [file join {*}[lrange [file split $opt] 0 $idx]]
139 if {![file exists $Stash/$pth]} {
140 puts stderr "$pth does not exist"
146 set opt [lindex $argv 1]
147 switch [lindex $argv 0] {
150 set prefix [lindex $argv 1]
151 if {$argc > 1 && [lindex $argv 1] == "-v"} {
153 set prefix [lindex $argv 2]
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]
159 if {[string range $name 0 [string length $prefix]-1] != $prefix} {
163 set v [fileread $opt/title]
164 puts -nonewline "$name\t$v"
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 {
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
188 file delete -force $Stash/$opt
191 assure-all-list-params-exist $opt
192 if {[llength $argv] > 2} {
193 set v [lindex $argv 2]
195 file delete $Stash/$opt
199 file mkdir "$Stash/[file dirname $opt]"
201 set fh [open $Stash/$opt w]
202 fconfigure $fh -translation binary
203 fconfigure stdin -translation binary
207 set v [run-checker $opt $v]
208 set fh [open $Stash/$opt w]
209 puts -nonewline $fh $v
217 puts [find-opt-schema $opt]
220 if {[file tail $opt] == "*"} {
221 set opt [file dirname $opt]
222 if {![file exists $Stash/$opt]} {
225 foreach dir [readents $Stash/$opt directory] {
230 if {[file exists $Stash/$opt]} {
232 set fh [open $Stash/$opt r]
233 fconfigure $fh -translation binary
234 fconfigure stdout -translation binary
238 puts -nonewline [fileread $Stash/$opt]
242 puts -nonewline [run-checker $opt ""]
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}]} {
253 if {[file exists $Schema/$pth/*]} {
255 foreach dir [readents $Stash/$opt directory] {
256 if {[catch {exec | [list dsc apply $opt/$dir] >@stdout 2>@stderr}]} {
262 foreach ent [readents $Schema/$opt directory] {
263 if {[catch {exec | [list dsc apply $opt/$ent] >@stdout 2>@stderr}]} {
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]
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]
281 set fh [open |[list diff -u -L dirs -L dirs $dirsSaved $dirsStash] r]
282 puts -nonewline [read $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"} {
295 if {[string range $line 0 $prefixSavedLen-1] == $prefixSaved} {
296 puts "--- [string range $line $prefixSavedLen end]"
299 if {[string range $line 0 $prefixStashLen-1] == $prefixStash} {
300 puts "+++ [string range $line $prefixStashLen end]"
308 catch {file delete -force $Stash/$opt}
309 exec | [list cp -a $Saved/$opt $Stash/$opt]
312 file delete -force $Saved.bak
313 set tmp $Saved.[expr {int(rand() * 1000000)}]
314 exec | [list cp -a $Stash $tmp]
316 file rename $Saved $Saved.bak
317 file rename $tmp $Saved
318 file delete -force $Saved.bak
321 set dirs [walk $Saved/$opt directory]
323 set dirs [list $Saved/$opt {*}$dirs]
327 puts [string range $fn [string length $Saved]+1 end]
330 foreach fn [walk $dir file] {
331 set sfn [string range $fn [string length $Saved]+1 end]
333 puts "-- $sfn:base64 --"
334 set fh [open "|base64 $fn" r]
340 while {[gets $fh line] >= 0} {
341 if {[txtar-fn $line] != ""} {
342 set line "-- $line --"
352 fconfigure stdin -translation binary
353 while {[gets stdin line] >= 0} {
354 set fn [txtar-fn $line]
359 if {$fn == ".dirs"} {
360 while {[gets stdin line] >= 0} {
361 set fn [txtar-fn $line]
363 file mkdir $Stash/$fn
371 if {[string range $fn [expr {[string length $fn]-7}] end] == ":base64"} {
373 set fn [string range $fn 0 [expr {[string length $fn]-7-1}]]
376 file mkdir [file dirname $Stash/$fn]
378 set fh [open |[list base64 -d > $Stash/$fn] w]
380 set fh [open $Stash/$fn w]
381 fconfigure $fh -translation binary
386 while {[gets stdin line] >= 0} {
387 set fn [txtar-fn $line]
390 } elseif {[string range $fn 0 2] == "-- "} {
401 if {[file exists $Stash/$opt]} {
402 puts [file normalize $Stash/$opt]