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 get opt -- get option's value, maybe default one
27 dsc get opt/* -- list /*/ sections
28 dsc diff [prefix] -- show the difference between saved and stash
29 dsc revert opt -- revert opt's configuration
30 dsc commit -- commit (save) configuration
31 dsc export >file.txtar -- export whole configuration
32 dsc import <file.txtar -- import it
34 Environmental variables:
35 $DSC_SCHEMA -- path to the schema definition
36 $DSC_STASH -- path to stashed/unsaved state
37 $DSC_SAVED -- path to committed/saved state
39 There are two kinds of options:
40 * array/list ones, which are identified with /*/ in "list"'s
41 output. "add", "del", "get opt/*" commands apply
42 * ordinary scalar ones, which can be "set", "get opt"
47 if {[catch {set Schema $env(DSC_SCHEMA)}]} {set Schema schema}
48 if {[catch {set Stash $env(DSC_STASH)}]} {set Stash stash}
49 if {[catch {set Saved $env(DSC_SAVED)}]} {set Saved saved}
51 proc walk {root typ} {
53 set dirs [glob -directory $root -types $typ -tails -nocomplain -- *]
54 foreach s [lsort $dirs] {
56 lappend rv {*}[walk $root/$s $typ]
68 proc find-checker {opt} {
71 set opt [string trimright $opt /]
72 set err "can not find checker for $opt"
73 foreach e [split $opt /] {
74 if {[file exists $Schema/$path/$e]} {
78 if {[file exists $Schema/$path/*]} {
85 set path [string range $path 2 end]
86 if {! [file exists $Schema/$path/check]} {
93 proc run-checker {opt v} {
95 set fh [open |[list "$Schema/[find-checker $opt]/check" $opt 2>@1] r+]
99 if {[catch {close $fh}]} {
100 puts -nonewline stderr $v
106 proc assure-exists {opt} {
108 if {! [file exists $Stash/$opt]} {
109 puts stderr "not found"
114 set opt [lindex $argv 1]
115 switch [lindex $argv 0] {
118 set prefix [lindex $argv 1]
119 if {$argc > 1 && [lindex $argv 1] == "-v"} {
121 set prefix [lindex $argv 2]
123 foreach opt [walk $Schema d] {
124 if {! [file exists $opt/title]} {continue}
125 set name [string range $opt [expr {[string length $Schema] + 1}] end]
127 if {[string range $name 0 [string length $prefix]-1] != $prefix} {
131 set v [fileread $opt/title]
132 puts -nonewline "$name\t$v"
134 if {[file exists $opt/descr]} {
135 set lines [split [fileread $opt/descr] "\n"]
136 set lines [lrange $lines 0 end-1]
137 foreach line $lines {
145 set dir [file dirname $opt]
146 set tail [run-checker $opt [file tail $opt]]
147 set tail [string trimright $tail "\n"]
148 file mkdir $Stash/$dir/$tail
155 file delete -force $Stash/$opt
158 set v [lindex $argv 2]
160 file delete $Stash/$opt
163 set v [run-checker $opt $v]
164 file mkdir "$Stash/[file dirname $opt]"
165 set fh [open $Stash/$opt w]
166 puts -nonewline $fh $v
173 puts [find-checker $opt]
176 if {[file tail $opt] == "*"} {
177 set opt [file dirname $opt]
178 if {! [file exists $Stash/$opt]} {
181 set dirs [glob -directory $Stash/$opt -types d -tails -nocomplain -- *]
182 foreach dir [lsort $dirs] {
187 if {[file exists $Stash/$opt]} {
188 puts -nonewline [fileread $Stash/$opt]
191 puts -nonewline [run-checker $opt ""]
194 set fh [file tempfile dirsSaved]
195 foreach fn [walk $Saved/$opt d] {
196 puts $fh [string range $fn [string length $Saved]+1 end]
199 set fh [file tempfile dirsStash]
200 foreach fn [walk $Stash/$opt d] {
201 puts $fh [string range $fn [string length $Stash]+1 end]
204 set fh [open |[list diff -u -L dirs -L dirs $dirsSaved $dirsStash] r]
205 puts -nonewline [read $fh]
207 file delete $dirsSaved
208 file delete $dirsStash
209 set fh [open |[list diff -urN $Saved/$opt $Stash/$opt] r]
210 set prefixSaved "--- $Saved/"
211 set prefixSavedLen [string length $prefixSaved]
212 set prefixStash "+++ $Stash/"
213 set prefixStashLen [string length $prefixStash]
214 while {[gets $fh line] >= 0} {
215 if {[string range $line 0 3] == "diff"} {
218 if {[string range $line 0 $prefixSavedLen-1] == $prefixSaved} {
219 puts "--- [string range $line $prefixSavedLen end]"
222 if {[string range $line 0 $prefixStashLen-1] == $prefixStash} {
223 puts "+++ [string range $line $prefixStashLen end]"
231 catch {file delete -force $Stash/$opt}
232 catch {file copy $Saved/$opt $Stash/$opt}
235 file delete -force $Saved.bak
236 set tmp $Saved.[expr {int(rand() * 1000000)}]
237 file copy $Stash $tmp
238 file rename $Saved $Saved.bak
239 file rename $tmp $Saved
240 file delete -force $Saved.bak
243 set dirs [walk $Saved d]
246 puts [string range $fn [string length $Saved]+1 end]
249 foreach fn [walk $dir f] {
250 puts "-- [string range $fn [string length $Saved]+1 end] --"
251 puts -nonewline [fileread $fn]
258 proc filewrite {fn v} {
260 if {[llength $v] == 0} {
263 if {$fn == ".dirs"} {
265 file mkdir $Stash/$dir
269 file mkdir [file dirname $Stash/$fn]
270 set fh [open $Stash/$fn w]
271 puts $fh [join $v "\n"]
274 while {[gets stdin line] >= 0} {
276 [string length $line] > 6 &&
277 [string range $line 0 2] == "-- " &&
278 [string range $line end-2 end] == " --"
284 set fn [string range $line 3 end-3]