]> Cypherpunks repositories - dsc.git/blob - dsc
2c3a03cf4de6fbfbf4ea61dcbf601fd422d7edd7ae0dc3bf05cd0e51df355c4d
[dsc.git] / dsc
1 #!/usr/bin/env tclsh
2 # dsc -- damn small configuration manager
3 # Copyright (C) 2025 Sergey Matveev <stargrave@stargrave.org>
4 #
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.
8 #
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.
13 #
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/>.
16
17 if {$argc == 0} {
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
33
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
38
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"
43 }
44     exit 1
45 }
46
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}
50
51 proc walk {root typ} {
52     set rv [list]
53     set dirs [glob -directory $root -types $typ -tails -nocomplain -- *]
54     foreach s [lsort $dirs] {
55         lappend rv $root/$s
56         lappend rv {*}[walk $root/$s $typ]
57     }
58     return $rv
59 }
60
61 proc fileread {fn} {
62     set fh [open $fn]
63     set v [read $fh]
64     close $fh
65     return $v
66 }
67
68 proc find-checker {opt} {
69     global Schema
70     set path .
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]} {
75             set path $path/$e
76             continue
77         }
78         if {[file exists $Schema/$path/*]} {
79             set path $path/*
80             continue
81         }
82         puts stderr $err
83         exit 1
84     }
85     set path [string range $path 2 end]
86     if {! [file exists $Schema/$path/check]} {
87         puts stderr $err
88         exit 1
89     }
90     return $path
91 }
92
93 proc run-checker {opt v} {
94     global Schema
95     set fh [open |[list "$Schema/[find-checker $opt]/check" $opt 2>@1] r+]
96     puts $fh $v
97     close $fh w
98     set v [read $fh]
99     if {[catch {close $fh}]} {
100         puts -nonewline stderr $v
101         exit 1
102     }
103     return $v
104 }
105
106 proc assure-exists {opt} {
107     global Stash
108     if {! [file exists $Stash/$opt]} {
109         puts stderr "not found"
110         exit 1
111     }
112 }
113
114 set opt [lindex $argv 1]
115 switch [lindex $argv 0] {
116     list {
117         set verbose n
118         set prefix [lindex $argv 1]
119         if {$argc > 1 && [lindex $argv 1] == "-v"} {
120             set verbose y
121             set prefix [lindex $argv 2]
122         }
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]
126             if {$prefix != ""} {
127                 if {[string range $name 0 [string length $prefix]-1] != $prefix} {
128                     continue
129                 }
130             }
131             set v [fileread $opt/title]
132             puts -nonewline "$name\t$v"
133             if {$verbose} {
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 {
138                         puts "\t$line"
139                     }
140                 }
141             }
142         }
143     }
144     add {
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
149         puts $dir/$tail
150     }
151     del {
152         if {$opt != ""} {
153             assure-exists $opt
154         }
155         file delete -force $Stash/$opt
156     }
157     set {
158         set v [lindex $argv 2]
159         if {$v == ""} {
160             file delete $Stash/$opt
161             exit
162         }
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
167         close $fh
168     }
169     has {
170         assure-exists $opt
171     }
172     get-checker {
173         puts [find-checker $opt]
174     }
175     get {
176         if {[file tail $opt] == "*"} {
177             set opt [file dirname $opt]
178             if {! [file exists $Stash/$opt]} {
179                 exit
180             }
181             set dirs [glob -directory $Stash/$opt -types d -tails -nocomplain -- *]
182             foreach dir [lsort $dirs] {
183                 puts $dir
184             }
185             exit
186         }
187         if {[file exists $Stash/$opt]} {
188             puts -nonewline [fileread $Stash/$opt]
189             exit
190         }
191         puts -nonewline [run-checker $opt ""]
192     }
193     diff {
194         set fh [file tempfile dirsSaved]
195         foreach fn [walk $Saved/$opt d] {
196             puts $fh [string range $fn [string length $Saved]+1 end]
197         }
198         close $fh
199         set fh [file tempfile dirsStash]
200         foreach fn [walk $Stash/$opt d] {
201             puts $fh [string range $fn [string length $Stash]+1 end]
202         }
203         close $fh
204         set fh [open |[list diff -u -L dirs -L dirs $dirsSaved $dirsStash] r]
205         puts -nonewline [read $fh]
206         catch {close $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"} {
216                 continue
217             }
218             if {[string range $line 0 $prefixSavedLen-1] == $prefixSaved} {
219                 puts "--- [string range $line $prefixSavedLen end]"
220                 continue
221             }
222             if {[string range $line 0 $prefixStashLen-1] == $prefixStash} {
223                 puts "+++ [string range $line $prefixStashLen end]"
224                 continue
225             }
226             puts $line
227         }
228         catch {close $fh}
229     }
230     revert {
231         catch {file delete -force $Stash/$opt}
232         catch {file copy $Saved/$opt $Stash/$opt}
233     }
234     commit {
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
241     }
242     export {
243         set dirs [walk $Saved d]
244         puts "-- .dirs --"
245         foreach fn $dirs {
246             puts [string range $fn [string length $Saved]+1 end]
247         }
248         foreach dir $dirs {
249             foreach fn [walk $dir f] {
250                 puts "-- [string range $fn [string length $Saved]+1 end] --"
251                 puts -nonewline [fileread $fn]
252             }
253         }
254     }
255     import {
256         set fn ""
257         set lines [list]
258         proc filewrite {fn v} {
259             global Stash
260             if {[llength $v] == 0} {
261                 return
262             }
263             if {$fn == ".dirs"} {
264                 foreach dir $v {
265                     file mkdir $Stash/$dir
266                 }
267                 return
268             }
269             file mkdir [file dirname $Stash/$fn]
270             set fh [open $Stash/$fn w]
271             puts $fh [join $v "\n"]
272             close $fh
273         }
274         while {[gets stdin line] >= 0} {
275             if {
276                 [string length $line] > 6 &&
277                 [string range $line 0 2] == "-- " &&
278                 [string range $line end-2 end] == " --"
279             } {
280                 if {$fn != ""} {
281                     filewrite $fn $lines
282                     set lines [list]
283                 }
284                 set fn [string range $line 3 end-3]
285             } else {
286                 lappend lines $line
287             }
288         }
289         if {$fn != ""} {
290             filewrite $fn $lines
291         }
292     }
293 }