2 # dsc -- damn small configuration manager
3 # Copyright (C) 2025 Sergey Matveev <stargrave@stargrave.org>
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.
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.
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/>.
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)
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
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"
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}
53 proc walk {root typ} {
55 set root [string trimright $root /]
56 set dirs [glob -directory $root -types $typ -tails -nocomplain -- *]
57 foreach s [lsort $dirs] {
59 lappend rv {*}[walk $root/$s $typ]
71 proc find-opt-schema {opt} {
74 set opt [string trimright $opt /]
75 foreach e [split $opt /] {
76 if {[file exists $Schema/$path/$e]} {
80 if {[file exists $Schema/$path/*]} {
84 puts stderr "can not find $opt in schema"
87 return [string range $path 2 end]
92 return [file exists "$Schema/[find-opt-schema $opt]/bin"]
95 proc run-checker {opt v} {
97 set fh [open |[list "$Schema/[find-opt-schema $opt]/check" $opt 2>@1] r+]
101 if {[catch {close $fh}]} {
102 puts -nonewline stderr $v
108 proc assure-exists {opt} {
110 if {! [file exists $Stash/$opt]} {
111 puts stderr "not found"
116 proc txtar-fn {line} {
118 [string length $line] > 6 &&
119 [string range $line 0 2] == "-- " &&
120 [string range $line end-2 end] == " --"
122 return [string range $line 3 end-3]
127 set opt [lindex $argv 1]
128 switch [lindex $argv 0] {
131 set prefix [lindex $argv 1]
132 if {$argc > 1 && [lindex $argv 1] == "-v"} {
134 set prefix [lindex $argv 2]
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]
140 if {[string range $name 0 [string length $prefix]-1] != $prefix} {
144 set v [fileread $opt/title]
145 puts -nonewline "$name\t$v"
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 {
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
168 file delete -force $Stash/$opt
171 if {[llength $argv] > 2} {
172 set v [lindex $argv 2]
174 file delete $Stash/$opt
178 file mkdir "$Stash/[file dirname $opt]"
180 set fh [open $Stash/$opt w]
181 fconfigure $fh -translation binary
182 fconfigure stdin -translation binary
186 set v [run-checker $opt $v]
187 set fh [open $Stash/$opt w]
188 puts -nonewline $fh $v
196 puts [find-opt-schema $opt]
199 if {[file tail $opt] == "*"} {
200 set opt [file dirname $opt]
201 if {! [file exists $Stash/$opt]} {
204 set dirs [glob -directory $Stash/$opt -types d -tails -nocomplain -- *]
205 foreach dir [lsort $dirs] {
210 if {[file exists $Stash/$opt]} {
212 set fh [open $Stash/$opt r]
213 fconfigure $fh -translation binary
214 fconfigure stdout -translation binary
218 puts -nonewline [fileread $Stash/$opt]
222 puts -nonewline [run-checker $opt ""]
225 set fh [file tempfile dirsSaved]
226 foreach fn [walk $Saved/$opt d] {
227 puts $fh [string range $fn [string length $Saved]+1 end]
230 set fh [file tempfile dirsStash]
231 foreach fn [walk $Stash/$opt d] {
232 puts $fh [string range $fn [string length $Stash]+1 end]
235 set fh [open |[list diff -u -L dirs -L dirs $dirsSaved $dirsStash] r]
236 puts -nonewline [read $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"} {
249 if {[string range $line 0 $prefixSavedLen-1] == $prefixSaved} {
250 puts "--- [string range $line $prefixSavedLen end]"
253 if {[string range $line 0 $prefixStashLen-1] == $prefixStash} {
254 puts "+++ [string range $line $prefixStashLen end]"
262 catch {file delete -force $Stash/$opt}
263 catch {file copy $Saved/$opt $Stash/$opt}
266 file delete -force $Saved.bak
267 set tmp $Saved.[expr {int(rand() * 1000000)}]
268 file copy $Stash $tmp
270 file rename $Saved $Saved.bak
271 file rename $tmp $Saved
272 file delete -force $Saved.bak
275 set dirs [walk $Saved/$opt d]
277 set dirs [list $Saved/$opt {*}$dirs]
281 puts [string range $fn [string length $Saved]+1 end]
284 foreach fn [walk $dir f] {
285 set sfn [string range $fn [string length $Saved]+1 end]
287 puts "-- $sfn:base64 --"
288 set fh [open "|base64 $fn" r]
294 while {[gets $fh line] >= 0} {
295 if {[txtar-fn $line] != ""} {
296 set line "-- $line --"
306 fconfigure stdin -translation binary
307 while {[gets stdin line] >= 0} {
308 set fn [txtar-fn $line]
313 if {$fn == ".dirs"} {
314 while {[gets stdin line] >= 0} {
315 set fn [txtar-fn $line]
317 file mkdir $Stash/$fn
325 if {[string range $fn [expr {[string length $fn]-7}] end] == ":base64"} {
327 set fn [string range $fn 0 [expr {[string length $fn]-7-1}]]
330 file mkdir [file dirname $Stash/$fn]
332 set fh [open |[list base64 -d > $Stash/$fn] w]
334 set fh [open $Stash/$fn w]
335 fconfigure $fh -translation binary
340 while {[gets stdin line] >= 0} {
341 set fn [txtar-fn $line]
344 } elseif {[string range $fn 0 2] == "-- "} {
355 if {[file exists $Stash/$opt]} {
356 puts [file normalize $Stash/$opt]