--- /dev/null
+#!/usr/bin/env tclsh
+# dsc -- damn small configuration manager
+# Copyright (C) 2025 Sergey Matveev <stargrave@stargrave.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, version 3 of the License.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+if {$argc == 0} {
+ puts -nonewline stderr {Usage:
+ dsc list [-v] [prefix] -- list all available options
+ dsc add opt -- add /*/ section
+ dsc del opt -- delete /*/ section
+ dsc del "" -- delete the whole configuration, useful for import
+ dsc has opt -- check if specified /*/ option exists
+ dsc set opt value -- set option's value
+ dsc set opt "" -- unset value, make it default one
+ dsc get opt -- get option's value, maybe default one
+ dsc get opt/* -- list /*/ sections
+ dsc diff [prefix] -- show the difference between committed and current
+ dsc revert opt -- revert opt's configuration
+ dsc commit -- commit (save) configuration
+ dsc export >file.txtar -- export whole configuration
+ dsc import <file.txtar -- import it
+
+Environmental variables:
+ $DSC_SCHEMA -- path to the schema definition
+ $DSC_CURRENT -- path to current/unsaved configuration state
+ $DSC_COMMITTED -- path to committed/saved state
+
+There are two kinds of options:
+ * array/list ones, which are identified with /*/ in "list"'s
+ output. "add", "del", "get opt/*" commands apply
+ * ordinary scalar ones, which can be "set", "get opt"
+}
+ exit 1
+}
+
+if {[catch {set Schema $env(DSC_SCHEMA)}]} {set Schema schema}
+if {[catch {set Curr $env(DSC_CURRENT)}]} {set Curr current}
+if {[catch {set Comm $env(DSC_COMMITTED)}]} {set Comm committed}
+
+proc walk {root typ} {
+ set rv [list]
+ set dirs [glob -directory $root -types $typ -tails -nocomplain -- *]
+ foreach s [lsort $dirs] {
+ lappend rv $root/$s
+ lappend rv {*}[walk $root/$s $typ]
+ }
+ return $rv
+}
+
+proc fileread {fn} {
+ set fh [open $fn]
+ set v [read $fh]
+ close $fh
+ return $v
+}
+
+proc find-checker {opt} {
+ global Schema
+ set path .
+ set opt [string trimright $opt /]
+ set err "can not find checker for $opt"
+ foreach e [split $opt /] {
+ if {[file exists $Schema/$path/$e]} {
+ set path $path/$e
+ continue
+ }
+ if {[file exists $Schema/$path/*]} {
+ set path $path/*
+ continue
+ }
+ puts stderr $err
+ exit 1
+ }
+ set path [string range $path 2 end]
+ if {! [file exists $Schema/$path/check]} {
+ puts stderr $err
+ exit 1
+ }
+ return $path
+}
+
+proc run-checker {opt v} {
+ global Schema
+ set fh [open "| $Schema/[find-checker $opt]/check $opt" r+]
+ puts $fh $v
+ close $fh w
+ set v [read $fh]
+ if {[catch {close $fh}]} {
+ puts -nonewline stderr $v
+ exit 1
+ }
+ return $v
+}
+
+proc assure-exists {opt} {
+ global Curr
+ if {! [file exists $Curr/$opt]} {
+ puts stderr "not found"
+ exit 1
+ }
+}
+
+set opt [lindex $argv 1]
+switch [lindex $argv 0] {
+ list {
+ set verbose n
+ set prefix [lindex $argv 1]
+ if {$argc > 1 && [lindex $argv 1] == "-v"} {
+ set verbose y
+ set prefix [lindex $argv 2]
+ }
+ foreach opt [walk $Schema d] {
+ if {! [file exists $opt/title]} {continue}
+ set name [string range $opt [expr {[string length $Schema] + 1}] end]
+ if {$prefix != ""} {
+ if {[string range $name 0 [string length $prefix]-1] != $prefix} {
+ continue
+ }
+ }
+ set v [fileread $opt/title]
+ puts -nonewline "$name\t$v"
+ if {$verbose} {
+ if {[file exists $opt/descr]} {
+ set lines [split [fileread $opt/descr] "\n"]
+ set lines [lrange $lines 0 end-1]
+ foreach line $lines {
+ puts "\t$line"
+ }
+ }
+ }
+ }
+ }
+ add {
+ set dir [file dirname $opt]
+ set tail [run-checker $opt [file tail $opt]]
+ set tail [string trimright $tail "\n"]
+ file mkdir $Curr/$dir/$tail
+ puts $dir/$tail
+ }
+ del {
+ if {$opt != ""} {
+ assure-exists $opt
+ }
+ file delete -force $Curr/$opt
+ }
+ set {
+ if {[llength $argv] == 2} {
+ set v [read -nonewline stdin]
+ } else {
+ set v [lindex $argv 2]
+ }
+ if {$v == ""} {
+ file delete $Curr/$opt
+ exit
+ }
+ set v [run-checker $opt $v]
+ file mkdir "$Curr/[file dirname $opt]"
+ set fh [open $Curr/$opt w]
+ puts -nonewline $fh $v
+ close $fh
+ }
+ has {
+ assure-exists $opt
+ }
+ get-checker {
+ puts [find-checker $opt]
+ }
+ get {
+ if {[file tail $opt] == "*"} {
+ set opt [file dirname $opt]
+ assure-exists $opt
+ set dirs [glob -directory $Curr/$opt -types d -tails -nocomplain -- *]
+ foreach dir [lsort $dirs] {
+ puts $dir
+ }
+ exit
+ }
+ if {[file exists $Curr/$opt]} {
+ puts -nonewline [fileread $Curr/$opt]
+ exit
+ }
+ puts -nonewline [run-checker $opt ""]
+ }
+ diff {
+ set fh [file tempfile dirsComm]
+ foreach fn [walk $Comm/$opt d] {
+ puts $fh [string range $fn [string length $Comm]+1 end]
+ }
+ close $fh
+ set fh [file tempfile dirsCurr]
+ foreach fn [walk $Curr/$opt d] {
+ puts $fh [string range $fn [string length $Curr]+1 end]
+ }
+ close $fh
+ set fh [open "| diff -u -L dirs/committed -L dirs/current
+ $dirsComm $dirsCurr" r]
+ puts -nonewline [read $fh]
+ catch {close $fh}
+ file delete $dirsComm
+ file delete $dirsCurr
+ set fh [open "| diff -urN $Comm/$opt $Curr/$opt" r]
+ puts -nonewline [read $fh]
+ catch {close $fh}
+ }
+ revert {
+ catch {file delete -force $Curr/$opt}
+ catch {file copy $Comm/$opt $Curr/$opt}
+ }
+ commit {
+ file delete -force $Comm.bak
+ set tmp $Comm.[expr {int(rand() * 1000000)}]
+ file copy $Curr $tmp
+ file rename $Comm $Comm.bak
+ file rename $tmp $Comm
+ file delete -force $Comm.bak
+ }
+ export {
+ set dirs [walk $Comm d]
+ puts "-- .dirs --"
+ foreach fn $dirs {
+ puts [string range $fn [string length $Comm]+1 end]
+ }
+ foreach dir $dirs {
+ foreach fn [walk $dir f] {
+ puts "-- [string range $fn [string length $Comm]+1 end] --"
+ puts -nonewline [fileread $fn]
+ }
+ }
+ }
+ import {
+ set fn ""
+ set lines [list]
+ proc filewrite {fn v} {
+ global Curr
+ if {[llength $v] == 0} {
+ return
+ }
+ if {$fn == ".dirs"} {
+ foreach dir $v {
+ file mkdir $Curr/$dir
+ }
+ return
+ }
+ file mkdir [file dirname $Curr/$fn]
+ set fh [open $Curr/$fn w]
+ puts $fh [join $v "\n"]
+ close $fh
+ }
+ while {[gets stdin line] >= 0} {
+ if {
+ [string length $line] > 6 &&
+ [string range $line 0 2] == "-- " &&
+ [string range $line end-2 end] == " --"
+ } {
+ if {$fn != ""} {
+ filewrite $fn $lines
+ set lines [list]
+ }
+ set fn [string range $line 3 end-3]
+ } else {
+ lappend lines $line
+ }
+ }
+ if {$fn != ""} {
+ filewrite $fn $lines
+ }
+ }
+}