aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSergey Poznyakoff <gray@gnu.org>2016-09-23 15:51:29 +0300
committerSergey Poznyakoff <gray@gnu.org>2016-09-23 17:26:49 +0300
commit7decb3e9b9b22d1743813ed97cd168a596bd3bea (patch)
tree847265391f697473655e100a3b8953de0aed5868
parent67e638c55b844846e5ffb6a3b60c59f420d1e467 (diff)
downloadrex-7decb3e9b9b22d1743813ed97cd168a596bd3bea.tar.gz
rex-7decb3e9b9b22d1743813ed97cd168a596bd3bea.tar.bz2
Further improvements of command-line and rc parsers
Restore priority of command-line settings over the rc * rex (version): Raise to 3.91 (optiondef): New global array. (throw_config_error) (catch_config_error): New functions. (option_set): New function. (::getopt::parse): Rewrite. Instead of taking option groups as positional arguments, accept the -group option which defines an option group. It takes two arguments: group ID and a list of group definitions. Return a dictionary where preprocessed options can be found. Calling [::getopt::run dict id] will apply the options from group ID. (::getopt::run): Take two arguments: the parse dictionary returned by ::getopt::parse, and the ID of the option group to run. All uses updated. (rex_parse_cmdline): Use three option groups: initial, which is run before parsing anything else, global, which is parsed after sourcing main rc file, and mode, which is parsed after sourcing hostgroup rc. (::config::host): Append to the hostlist variable, instead of the legacy hosts, to avoid triggering the deprecation warning. (::config::locus): New function. (::config::*): Use ::config::locus for diagnostic purposes. (::config::read): Catch and process custom rex throws. (common_config_setup): Remove. (runcmd): Process password authentication for files passed with -D * rex.man8: Update.
-rwxr-xr-xrex871
-rw-r--r--rex.man8124
2 files changed, 685 insertions, 310 deletions
diff --git a/rex b/rex
index aaee3f7..163bc7f 100755
--- a/rex
+++ b/rex
@@ -17,7 +17,7 @@ exec expect "$0" -- "$@"
# You should have received a copy of the GNU General Public License
# along with Rex. If not, see <http://www.gnu.org/licenses/>.
-set version "3.90"
+set version "3.91"
set sysconfdir "/etc/rex"
set usrconfdir "$env(HOME)/.rex"
set confpath [list $usrconfdir $sysconfdir]
@@ -40,6 +40,236 @@ if {[info exists env(EXPECT_DEBUG)]} {
set ssh_options {-oStrictHostKeyChecking=yes -oLogLevel=VERBOSE}
+# Option definitions for the option_set command.
+# Keys are formed as follows: OPTNAME,PROP , where OPTNAME is the name
+# of the option and PROP is the property name. The following option
+# properties are defined:
+# nargs
+# Allowed number of arguments.
+# If the value is an integer, then the option must take exactly that
+# number of arguments. If it is a two-element list, element 0 gives
+# minimum number of arguments and element 1 gives maximum number. An
+# empty list is equivalent to 0.
+# name
+# Name of the corresponding option in the global ::config array. If
+# that property is set, and the 'lambda' property is not defined, the
+# option value will be assigned to ::config(option,NAME), where NAME is
+# the value of that property.
+# lambda
+# If present, supplies a lambda-expression to apply(n) in order to
+# process the option. The expression is variadic, the arguments to
+# the option_set command will be passed to it.
+# If both name and lambda are present, lambda is given preference.
+# rx
+# Regular expression a validoption argument must match.
+# translate
+# A variadic lambda-expression which translates the actual arguments
+# to the form acceptable for the option.
+# type
+# A short-cut property to define some most commonly used types. So far
+# the only meaningful value is bool, which defines a boolean option.
+# Option arguments can be: on, true, yes, 1 - meaning True, and
+# off, false, no, 0 - meaning False.
+#
+array set optiondef {
+ group,nargs 1
+ group,name hostgroup
+
+ buffer-output,type bool
+
+ confirm,type bool
+
+ copy,type bool
+
+ data-file,nargs 1
+ data-file,lambda { arg {
+ if {[file exists $arg] && [file readable $arg]} {
+ lappend ::config(data) $arg
+ } else {
+ if {[info exists locus]} {
+ append locus ": "
+ } else {
+ set locus ""
+ }
+ return -code error "file $arg doesn't exist or is unreadable"
+ }
+ }}
+
+ host,lambda { args {
+ foreach a $args {
+ lappend ::config(hosts) {*}[split $arg ","]
+ }
+ } }
+ exclude-host,lambda { args {
+ foreach a $args {
+ lappend ::config(exclude_hosts) {*}[split $a ","]
+ }
+ } }
+
+ ignore-hosts,type bool
+
+ interactive,type bool
+
+ interpreter,nargs 1
+ interpreter,lambda { arg {
+ set ::config(option,copy) 1
+ set ::config(option,interpreter) $arg
+ }}
+
+ jobs,rx {^[[:digit:]]+$}
+ jobs,name jobs
+
+ no-host-header,type bool
+
+ password,nargs 1
+ password,lambda { arg { set ::config(pass) $arg }}
+
+ prefix,type bool
+
+ source,nargs 1
+ source,type bool
+
+ sudo,type bool
+ sudo,lambda { arg setsudo }
+
+ user,nargs 1
+ user,lambda { arg { set ::config(user) $arg }}
+
+ zsh-quirk,type bool
+}
+
+proc throw_config_error {locus text} {
+ return -code error -errorcode [list REX $locus] $text
+}
+
+proc catch_config_error {prog code} {
+ switch -- [catch [list uplevel $prog] result options] {
+ 0 { return $result }
+ 1 {
+ set errorcode [dict get $options -errorcode]
+ if {[lindex $errorcode 0] == "REX"} {
+ terror "[lindex $errorcode 1]: $result"
+ exit $code
+ } else {
+ return -code 1 -options $options
+ }
+ }
+ default {
+ puts "B"
+ return -code [dict get $options -code] -options $options
+ }
+ }
+}
+
+proc option_set {args} {
+ global optiondef
+ global config
+
+# puts "option_set [llength $args] $args"
+ if {[llength $args] < 2 || [llength $args] > 3} {
+ return -code error "bad # args"
+ }
+ set opt [lindex $args 0]
+ set arg [lindex $args 1]
+ if {[llength $args] == 3} {
+ set locus [lindex $args 2]
+ } else {
+ set locus $opt
+ }
+
+ if {[info exists optiondef($opt,type)]} {
+ switch -- $optiondef($opt,type) {
+ bool {
+ set optiondef($opt,nargs) { 0 1 }
+ set optiondef($opt,translate) { args
+ { if {[llength $args] == 0} {
+ return 1
+ } else {
+ switch -- [string tolower [lindex $args 0]] {
+ on -
+ true -
+ yes -
+ 1 { return 1 }
+ off -
+ false -
+ no -
+ 0 { return 0 }
+ default { return -code error }
+ }
+ }
+ }
+ }
+ if {![info exist optiondef($opt,name)]} {
+ set optiondef($opt,name) $opt
+ }
+ unset optiondef($opt,type)
+ }
+ default {
+ return -code error "bad value for optiondef($opt,type)"
+ }
+ }
+ }
+
+ if {[info exists optiondef($opt,nargs)]} {
+ # Check number of arguments
+ if {![info exists arg]} {
+ set nargs 0
+ } else {
+ set nargs [llength $arg]
+ }
+ set lim $optiondef($opt,nargs)
+ switch -- [llength $lim] {
+ 2 {
+ if {$nargs < [lindex $lim 0]} {
+ throw_config_error $locus "too few arguments (at least [lindex $lim 0] required)"
+ }
+ if {$nargs > [lindex $lim 1]} {
+ throw_config_error $locus "too many arguments (max. [lindex $lim 1]"
+ }
+ }
+ 1 {
+ if {$nargs != $lim} {
+ throw_config_error $locus "option takes exactly $lim arguments, but passed $nargs"
+ }
+ }
+ 0 {
+ if {$nargs > 0} {
+ throw_config_error $locus "option takes no arguments"
+ }
+ }
+ default {
+ return -code error "bad value for optiondef($opt,nargs)"
+ }
+ }
+ }
+
+ if {[info exists optiondef($opt,rx)]} {
+ if {![regexp -- $optiondef($opt,rx) $arg]} {
+ throw_config_error $locus "bad argument for $opt"
+ }
+ }
+
+ if {[info exists optiondef($opt,translate)]} {
+ if {[catch {apply $optiondef($opt,translate) $arg} trans]} {
+ throw_config_error $locus "bad argument for $opt"
+ } else {
+ set arg $trans
+ }
+ }
+
+ if {[info exists optiondef($opt,lambda)]} {
+ set lambda $optiondef($opt,lambda)
+ if {[catch {apply $lambda {*}$arg} result]} {
+ throw_config_error $locus $result
+ }
+ } elseif {[info exists optiondef($opt,name)]} {
+ set config(option,$optiondef($opt,name)) $arg
+ } else {
+ throw_config_error $locus "no such option $opt"
+ }
+}
+
+# FIXME: Rename to option_get?
# config_option KEY [VAR]
# Return the value of the configuration option KEY, or "" if not
# defined.
@@ -627,6 +857,7 @@ namespace eval ::getopt {
}
proc parse {args} {
+# puts "ARGS [llength $args] $args"
while {[llength $args] > 0} {
set arg [lindex $args 0]
switch -- $arg {
@@ -634,14 +865,6 @@ namespace eval ::getopt {
set progname [lindex $args 1]
set args [lreplace $args [set args 0] 1]
}
- -before {
- set before [lindex $args 1]
- set args [lreplace $args [set args 0] 1]
- }
- -postprocess {
- set postprocess [lindex $args 1]
- set args [lreplace $args [set args 0] 1]
- }
-docstring {
set docdict(docstring) [lindex $args 1]
set args [lreplace $args [set args 0] 1]
@@ -662,18 +885,33 @@ namespace eval ::getopt {
set docdict(footer) [lindex $args 1]
set args [lreplace $args [set args 0] 1]
}
+ -commit {
+ set commit 1
+ set args [lreplace $args [set args 0] 0]
+ }
+ -group {
+ lappend group_id [lindex $args 1]
+ lappend group_def [lindex $args 2]
+ set args [lreplace $args [set args 0] 2]
+ }
default { break }
}
}
-
- if {[llength $args] != 3} {
- return -code error "bad number of arguments [llength $args]"
- }
+ if {[llength $args] != 2} {
+ return -code error "bad # args"
+ }
+
upvar [lindex $args 0] argc
upvar [lindex $args 1] argv
- set defs [lindex $args 2]
+
+ set groupidx -1
+ set defidx 0
+ set defcnt 0
+ set parsedict [dict create]
+ dict set parsedict group {}
+
# Variables:
# shortopts - list of short options
set shortopts "h"
@@ -682,11 +920,29 @@ namespace eval ::getopt {
# docs - list of documentation strings; format:
# optlist argname docstr
# select - list of code
- for {set i 0} {$i < [llength $defs]} {incr i 3} {
+ while 1 {
+ if {$defidx == $defcnt} {
+ incr groupidx
+ if {$groupidx >= [llength $group_id]} {
+ break
+ }
+ set group_name [lindex $group_id $groupidx]
+ set defs [lindex $group_def $groupidx]
+ set defcnt [llength $defs]
+ if {$defcnt == 0} {
+ continue
+ }
+ dict lappend parsedict id $group_name
+# puts "DEFS $defcnt $defs"
+ set defidx 0
+ }
+
unset -nocomplain argname repr optlist
set longacc {}
- set optstr [lindex $defs $i]
+ set optstr [lindex $defs $defidx]
+ incr defidx
+
set n [string last "=" $optstr]
if {$n > 0} {
set argname [string range $optstr [expr $n + 1] end]
@@ -733,11 +989,20 @@ namespace eval ::getopt {
} else {
lappend entry {}
}
- lappend entry [lindex $defs [expr $i + 1]]
+ lappend entry [lindex $defs $defidx]
+ incr defidx
+
lappend docdict(optdoc) $entry
- lappend select $repr [lindex $defs [expr $i + 2]]
+ set cmd [lindex $defs $defidx]
+ incr defidx
+ lappend select $repr [subst -nocommand {
+ upvar 1 parsedict parser
+ set lambda {{optchar optarg} {$cmd}}
+ set item [list \$lambda \$optchar \$optarg]
+ dict lappend parser code $group_name [list \$item]
+ }]
}
-
+
if {[info exists docdict(optdoc)]} {
set docdict(optdoc) [lsort -command optlistcmp $docdict(optdoc)]
}
@@ -747,7 +1012,7 @@ namespace eval ::getopt {
# puts "shortopts=$shortopts"
# puts "longopts=$longopts"
# puts "docs=$docdict(optdoc)"
- # puts "select=$select"
+ # puts "select=$select"
# exit
if {[info exists progname]} {
@@ -758,12 +1023,8 @@ namespace eval ::getopt {
}
lappend param $argc $argv
lappend param $shortopts
-
+
getopt {*}$param {
- upvar before before
- if {[info exists before]} {
- eval $before
- }
upvar select select
switch -- $optchar {*}$select \
h parsehelp \
@@ -773,45 +1034,29 @@ namespace eval ::getopt {
}
}
+ # puts $parsedict
+
variable optind
set argv [lrange $argv $optind end]
set argc [expr $argc - $optind]
- if {[info exists postprocess]} {
- foreach optitem [eval $postprocess] {
- set opt [lindex $optitem 0]
-
- if {[info exists progname]} {
- set loc "$progname:"
- } else {
- set loc ""
- }
- if {[llength $optitem] == 3} {
- append loc "[lindex $optitem 2]:"
- }
- append loc [lindex $optitem 1]
-
- init
- set param [list -progname $loc]
- if {[info exists longopts]} {
- lappend param -longopts $longopts
- }
-
- lappend param 1 $opt
- lappend param $shortopts
+ if {[info exists commit]} {
+ foreach rl $group_id {
+ run $parsedict $rl
+ }
+ } else {
+ return $parsedict
+ }
+ }
- getopt {*}$param {
- upvar before before
- if {[info exists before]} {
- eval $before
- }
- upvar select select
- switch -- $optchar {*}$select \
- ? { exit 1 } \
- default {
- return -code error "option should have been recognized: $optchar"
- }
- }
+ proc run {d id} {
+ global config
+ if {[dict exists $d code $id]} {
+ foreach item [dict get $d code $id] {
+ # Each list element is: {lambda optchar optarg}
+ # FIXME
+ #puts "run [llength $item] $item"
+ apply {*}$item
}
}
}
@@ -1830,14 +2075,14 @@ proc runcprev {args} {
}
-re {assword:|assword for .*:} {
- debug 3 "[job id $batch $sid]: prompted for password in state [job get $batch $spawn_id state]"
+ debug 3 "[job id $batch $spawn_id]: prompted for password in state [job get $batch $spawn_id state]"
switch -- [job get $batch $spawn_id state] {
{INIT} {
if {[catch {hostpass $host} result] == 0} {
send "$result\r"
job transition batch $spawn_id PASS_SENT
} else {
- terror "[job id $batch $sid]: requires password, but none is given"
+ terror "[job id $batch $spawn_id]: requires password, but none is given"
job close batch $spawn_id
}
}
@@ -1855,9 +2100,9 @@ proc runcprev {args} {
-re {^[^\n]*\n} {
set text [regsub -all {[\r\n]} $expect_out(0,string) {}]
if {[job get $batch $spawn_id state] == "INIT"} {
- terror -nosave "[job id $batch $sid]: $text"
+ terror -nosave "[job id $batch $spawn_id]: $text"
} elseif {[regexp {^scp: (.*)$} $text x msg]} {
- terror "[job id $batch $sid]: $msg"
+ terror "[job id $batch $spawn_id]: $msg"
}
}
@@ -1932,11 +2177,19 @@ proc runcmd {hosts command} {
-re {^Authenticated to} {
set sid $expect_out(spawn_id)
- job transition batch $sid AUTHENTICATED
- while {[job exists $batch $sid earlycmd]} {
- set cmd [job lshift batch $sid earlycmd]
- debug 3 "$sid: sending early $cmd"
- send -i $sid "$cmd\r"
+ switch -- [job get $batch $sid state] {
+ {COPY_INIT} -
+ {COPY_PASS_SENT} {
+ job transition batch $sid COPY
+ }
+ {default} {
+ job transition batch $sid AUTHENTICATED
+ while {[job exists $batch $sid earlycmd]} {
+ set cmd [job lshift batch $sid earlycmd]
+ debug 3 "[job id $batch $sid]: sending early $cmd"
+ send -i $sid "$cmd\r"
+ }
+ }
}
}
-re {assword:|assword for .*:} {
@@ -1953,6 +2206,12 @@ proc runcmd {hosts command} {
job close batch $sid
}
}
+ {COPY_INIT} {
+ if {[catch {hostpass $host} result] == 0} {
+ send -i $sid "$result\r"
+ job transition batch $sid COPY_PASS_SENT
+ }
+ }
{COMMAND} {
if {$config(sudo) != ""} {
if {[catch {hostpass $host} result] == 0} {
@@ -2014,29 +2273,29 @@ proc runcmd {hosts command} {
-re $config(prompt) {
set sid $expect_out(spawn_id)
set host [job get $batch $sid host]
- debug 3 "$sid: prompt seen in state [job get $batch $sid state]"
+ debug 3 "[job id $batch $sid]: prompt seen in state [job get $batch $sid state]"
switch -- [job get $batch $sid state] {
{AUTHENTICATED} {
set cmd [job lshift batch $sid cmdlist]
- debug 3 "$sid: sending $cmd"
+ debug 3 "[job id $batch $sid]: sending $cmd"
send -i $sid "$cmd\r"
job transition batch $sid STTY
}
{STTY} {
if {[job exists $batch $sid cmdlist]} {
set cmd [job lshift batch $sid cmdlist]
- debug 3 "$sid: sending $cmd"
+ debug 3 "[job id $batch $sid]: sending $cmd"
send -i $sid "$cmd\r"
} elseif {[info exists config(data)]} {
set host [job get $batch $sid host]
set cmd $config(data)
lappend cmd "[hostuser $host]@$host:[job get $batch $sid wd]"
- debug 3 "$sid: starting scp $cmd"
- set subsid [job start batch $host scp {*}$cmd]
- job transition batch $subsid COPY
+ debug 3 "[job id $batch $sid]: starting scp $cmd"
+ set subsid [job start batch $host scp {*}$ssh_options {*}$cmd]
+ job transition batch $subsid COPY_INIT
job set batch $subsid master $sid
} else {
- debug 3 "$sid: sending $command"
+ debug 3 "[job id $batch $sid]: sending $command"
send -i $sid "$command\r"
job transition batch $sid COMMAND
}
@@ -2044,7 +2303,7 @@ proc runcmd {hosts command} {
{COMMAND} {
if {[job exists $batch $sid wd]} {
set host [job get $batch $sid host]
- debug 3 "$sid: removing $host:[job get $batch $sid wd]"
+ debug 3 "[job id $batch $sid]: removing $host:[job get $batch $sid wd]"
send -i $sid "$config(sudo)rm -f -r [job get $batch $sid wd]\r"
if {$config(sudo) != ""} {
job transition batch $sid SUDO2
@@ -2120,10 +2379,12 @@ proc runcmd {hosts command} {
debug 2 "finished $command on $hosts"
}
-proc setsudo {} {
- global config
-
- set config(sudo) "sudo "
+proc setsudo {args} {
+ if {[llength $args] == 0 || [lindex $args 0] != 0} {
+ set ::config(sudo) "sudo "
+ } else {
+ set ::config(sudo) ""
+ }
}
# The hostproc namespace provides functions for processing output
@@ -2199,46 +2460,50 @@ namespace eval ::config {
}
proc host {args} {
- variable hosts
- lappend hosts {*}$args
+ variable hostlist
+ lappend hostlist {*}$args
}
proc ifmode {args} {
switch -- $::config(mode) {*}$args
}
- proc option {args} {
- global config
- set frame [info frame -1]
- lappend opt $args
- lappend opt [dict get $frame line]
- if {[dict get $frame type] == "source"} {
- lappend opt [dict get $frame file]
+ proc locus {{dfl {}}} {
+ variable source_file
+ set locus $dfl
+ if {[info exists source_file]} {
+ for {set i 1} {$i <= [info level]} {incr i} {
+ set frame [info frame [expr - $i]]
+ # if {[dict get $frame type] == "source"} {
+ # puts "$i: [dict get $frame file]:[dict get $frame line]"
+ # }
+
+ if {[dict get $frame type] == "source" &&
+ [dict get $frame file] == $source_file} {
+ set locus "[dict get $frame file]:[dict get $frame line]"
+ break
+ }
+ }
}
- lappend config(postprocess) $opt
+ return $locus
+ }
+
+ proc option {args} {
+ option_set [lindex $args 0] [lrange $args 1 end] [locus [lindex $args 0]]
}
proc timeout {args} {
- set frame [info frame -1]
- set locus [dict get $frame line]
- if {[dict get $frame type] == "source"} {
- set locus "[dict get $frame file]:$locus"
- }
switch -- [llength $args] {
0 { return $::timeout }
- 1 { set ::timeout [lindex $args 1] }
+ 1 { set ::timeout $args }
default {
- terror "$locus: usage: timeout ?value?"
+ terror "[locus timeout]: usage: timeout ?value?"
}
}
}
proc environ {args} {
- set frame [info frame -1]
- set locus [dict get $frame line]
- if {[dict get $frame type] == "source"} {
- set locus "[dict get $frame file]:$locus"
- }
+ global env
set mode set
foreach a $args {
switch -- $mode {
@@ -2252,7 +2517,7 @@ namespace eval ::config {
set env([lindex $match 1]) [lindex $match 2]
}
default {
- terror "$locus: that doesn't look like a variable assignment: $a"
+ terror "[locus environ]: that doesn't look like a variable assignment: $a"
}
}
}
@@ -2276,12 +2541,7 @@ namespace eval ::config {
global config
if {[llength $arglist] == 0} {
- set frame [info frame -2]
- set locus [dict get $frame line]
- if {[dict get $frame type] == "source"} {
- set locus "[dict get $frame file]:$locus"
- }
- terror "$locus: usage: $name ?-clear? list"
+ terror "[locus $name]: usage: $name ?-clear? list"
return
}
@@ -2309,21 +2569,15 @@ namespace eval ::config {
# sudo on|off
proc sudo {args} {
- set frame [info frame -2]
- set locus [dict get $frame line]
- if {[dict get $frame type] == "source"} {
- set locus "[dict get $frame file]:$locus"
- }
-
- if {[llength $arglist] != 1} {
- terror "$locus: usage: sudo on|off"
+ if {[llength $args] != 1} {
+ terror "[locus sudo]: usage: sudo on|off"
return
}
switch -nocase -- [lindex $args 0] {
on setsudo
off { unset -nocomplain ::config(sudo) }
default {
- terror "$locus: usage: sudo on|off"
+ terror "[locus sudo]: usage: sudo on|off"
}
}
}
@@ -2339,17 +2593,30 @@ namespace eval ::config {
proc read {file} {
variable cfgvars
variable initialized
+ variable source_file
+
foreach var $cfgvars {
variable $var
}
+
+ set source_file $file
if {[catch {source $file} result options] == 0} {
set initialized 1
+ unset source_file
} else {
+ unset source_file
set locus "$file"
if {[dict get $options -code] == 1} {
+ set errorcode [dict get $options -errorcode]
+ if {[lindex $errorcode 0] == "REX"} {
+ # FIXME
+ terror "[lindex $errorcode 1]: $result"
+ exit 1
+ }
append locus ":" [dict get $options -errorline]
}
terror "$locus: $result"
+# return -code 1 -options $options
foreach {tok prm} [dict get $options -errorstack] {
switch -- $tok {
{CALL} {
@@ -2387,6 +2654,7 @@ namespace eval ::config {
foreach var $cfgvars {
if {[exists $var]} {
+ puts "VAR $var"
terror -nosave "warning: your rc file sets obsolete variable \"$var\""
if {[info exists update_hint($var)]} {
terror -nosave "warning: please change this to \"[apply $update_hint($var) [valueof $var]]\""
@@ -2405,6 +2673,13 @@ namespace eval ::config {
}
}
}
+
+ variable hostlist
+ if {[info exists hostlist] &&
+ ![info exists config(option,ignore-hosts)]} {
+ lappend config(hosts) {*}$hostlist
+ debug 3 "config(hosts) = $config(hosts)"
+ }
}
}
@@ -2472,9 +2747,11 @@ proc hostlist_setup {} {
}
if {![info exists config(hosts)] || [llength $config(hosts)] == 0} {
- terror "no host list"
if {![info exists config(option,hostgroup)]} {
+ terror "no host list"
listgroups
+ } else {
+ terror "no hosts defined in group $config(option,hostgroup)"
}
exit 1
}
@@ -2498,19 +2775,112 @@ proc hostlist_setup {} {
}
}
-proc common_config_setup {} {
- global config
- global confpath
- global rexdb
+proc cleanup {} {
+ global argv0
+ global errors
+ global cleanup_files
+ debug 1 "cleaning up"
+ updatedb
+
+ if {[info exist cleanup_files]} {
+ foreach fname $cleanup_files {
+ file delete $fname
+ }
+ unset cleanup_files
+ }
+
+ if {[info exist errors]} {
+ send_error "$argv0: there were [llength $errors] errors:\n"
+ foreach err $errors {
+ send_error "$argv0: $err\n"
+ }
+ exit 2
+ }
+}
+
+proc getinterpreter {name} {
+ set retval "/bin/sh"
+ if {![config_option interpreter retval]} {
+ if {[catch {open $name "r"} fd] == 0} {
+ if {[gets $fd line] >= 0} {
+ regexp {^#![[:space:]]*(.+?)[[:space:]]*$} $line dummy retval
+ }
+ close $fd
+ } else {
+ terror "can't open file $name: $fd"
+ exit 2
+ }
+ }
+ return $retval
+}
+
+proc regsub-eval {args} {
+ set optlist {}
+ while {[llength $args] > 0} {
+ set opt [lshift args]
+ switch -regexp -- $opt {
+ {^--$} {
+ set opt [lshift args]
+ break
+ }
+ {^-.+} { lappend optlist $opt }
+ default { break }
+
+ }
+ }
+ if {[llength $args] != 2} {
+ return -code error "bad # args"
+ }
+ set re $opt
+ set string [lshift args]
+ set cmd [lshift args]
+
+ subst [regsub {*}$optlist -- $re [regsub -all {[][$\\]} $string {\\&}] \
+ \[[regsub -all {&} $cmd {\\&}]\]]
+}
+
+# Return true, if cmd looks like a pipeline or shell construct.
+proc ispipeline {cmd} {
+ if {[llength $cmd] != 1} {
+ return 0
+ }
+ regexp -- {(^(if|case|for|while|time|function|select)[[:space:]])|[|&><;]} $cmd
+}
+
+proc rex_parse_cmdline {args} {
+ global argc argv argv0
+
+ set arglist {}
+ while {[llength $args] > 0} {
+ switch -- [lindex $args 0] {
+ -usage -
+ -alias -
+ -description -
+ -footer -
+ -docstring {
+ set opt [lshift args]
+ lappend arglist $opt [lshift args]
+ }
+ default { break }
+ }
+ }
+
+ set parser [catch_config_error {::getopt::parse {*}$arglist {*}$args argc argv} 1]
+
+ catch_config_error {::getopt::run $parser initial} 1
+
# Read databases
+ global rexdb
+ global confpath
+ global config
+
foreach dir $confpath {
set db "$dir/db"
if {[file exists $db]} {
readdb $db rexdb
}
}
-
array unset rexdb updated
# Read configuration
@@ -2529,6 +2899,9 @@ proc common_config_setup {} {
}
}
+ catch_config_error {::getopt::run $parser global } 1
+
+ # Read hostgroup config
if {[info exists config(option,hostgroup)]} {
unset -nocomplain cfg
foreach dir $confpath {
@@ -2564,6 +2937,7 @@ proc common_config_setup {} {
}
}
+ # Read in additional source file
if {[info exists config(option,source)]} {
set sourcebase $config(option,source)
set name [scanlibpath ${sourcebase}.tcl]
@@ -2577,83 +2951,12 @@ proc common_config_setup {} {
debug 2 "importing configuration settings"
::config::export
- if {[info exists config(postprocess)]} {
- return $config(postprocess)
- }
-}
-
-proc cleanup {} {
- global argv0
- global errors
- global cleanup_files
-
- debug 1 "cleaning up"
- updatedb
-
- if {[info exist cleanup_files]} {
- foreach fname $cleanup_files {
- file delete $fname
- }
- unset cleanup_files
- }
- if {[info exist errors]} {
- send_error "$argv0: there were [llength $errors] errors:\n"
- foreach err $errors {
- send_error "$argv0: $err\n"
- }
- exit 2
- }
+ # Process level 2 options
+ catch_config_error {::getopt::run $parser mode } 1
}
-proc getiterpreter {name} {
- set retval "/bin/sh"
- if {![config_option interpreter retval]} {
- if {[catch {open $name "r"} fd] == 0} {
- if {[gets $fd line] >= 0} {
- regexp {^#![[:space:]]*(.+?)[[:space:]]*$} $line dummy retval
- }
- close $fd
- } else {
- terror "can't open file $name: $fd"
- exit 2
- }
- }
- return $retval
-}
-
-proc regsub-eval {args} {
- set optlist {}
- while {[llength $args] > 0} {
- set opt [lshift args]
- switch -regexp -- $opt {
- {^--$} {
- set opt [lshift args]
- break
- }
- {^-.+} { lappend optlist $opt }
- default { break }
-
- }
- }
- if {[llength $args] != 2} {
- return -code error "bad # args"
- }
- set re $opt
- set string [lshift args]
- set cmd [lshift args]
- subst [regsub {*}$optlist -- $re [regsub -all {[][$\\]} $string {\\&}] \
- \[[regsub -all {&} $cmd {\\&}]\]]
-}
-
-# Return true, if cmd looks like a pipeline or shell construct.
-proc ispipeline {cmd} {
- if {[llength $cmd] != 1} {
- return 0
- }
- regexp -- {(^(if|case|for|while|time|function|select)[[:space:]])|[|&><;]} $cmd
-}
proc rex_command args {
global argv0
@@ -2669,72 +2972,66 @@ proc rex_command args {
}
}
- ::getopt::parse -usage {rex run [OPTIONS] PROGRAM [ARGS...]} \
+ rex_parse_cmdline \
+ -usage {rex run [OPTIONS] PROGRAM [ARGS...]} \
-docstring {Runs PROGRAM on the given hosts.} \
- -postprocess common_config_setup \
- -before {global config} argc argv {
+ -group global {
+ group,g=NAME
+ {select host group}
+ { option_set group $optarg }
+ } \
+ -group mode {
buffer-output,b
{buffer output from servers}
- { set config(option,buffer-output) 1 }
+ { option_set buffer-output true }
confirm,w
{prompt and wait for confirmation before each host}
- { set config(option,confirm) 1 }
+ { option_set confirm true }
copy
{copy PROGRAM to each host before executing}
- {set config(option,copy) 1}
+ { option_set copy true}
data-file,D=FILE
{copy FILE to each host before running command}
- { if {[file exists $optarg] && [file readable $optarg]} {
- lappend config(data) $optarg
- } else {
- terror "data file $optarg doesn't exist or is unreadable"
- }
- }
- group,g=NAME
- {select host group}
- { set config(option,hostgroup) $optarg }
+ { option_set data-file $optarg }
host,H=HOST
{add HOST to the host list}
- { lappend config(hosts) {*}[split $optarg ","] }
+ { lappend ::config(hosts) {*}[split $optarg ","] }
exclude-host,X=HOST
{remove HOST from the host list}
- { lappend config(exclude_hosts) {*}[split $optarg ","] }
+ { lappend ::config(exclude_hosts) {*}[split $optarg ","] }
ignore-hosts,i
{ignore the list of hosts read from the hostgroup file}
- { set config(option,ignore-hosts) 1 }
+ { option_set ignore-hosts true }
interactive,I
{interactively ask for missing usernames and passwords}
- { set config(option,interactive) 1 }
+ { option_set interactive true }
interpreter=PROGRAM
{use COMMAND as interpreter for running PROGRAM; implies --copy}
- {
- set config(option,copy) 1
- set config(option,interpreter) $optarg
- }
+ { option_set interpreter $optarg }
jobs,j=N
{run N jobs at once}
- { set config(option,jobs) $optarg }
+ { option_set jobs $optarg }
no-host-header
{don't print hostname before output from the host}
- { set config(option,no-host-header) 1 }
+ { option_set no-host-header true }
password,p=PASS
{set password (unsafe!)}
- { set config(pass) $optarg }
+ { set ::config(pass) $optarg }
prefix,P
{prefix each output line with the server name or IP}
- { set config(option,prefix) 1 }
+ { option_set prefix true }
source,s=NAME
{source .rex/NAME.tcl}
- { set config(option,source) $optarg }
+ { option_set source $optarg }
sudo
{run PROGRAM via sudo}
- setsudo
+ { option_set sudo true }
user,u=NAME
{log in as user NAME}
- { set config(user) $optarg }
+ { set ::config(user) $optarg }
zsh-quirk,Z
{try to cope with hosts running zsh}
- { set config(option,zsh-quirk) 1 }
+ { option_set zsh-quirk true }
}
# FIXME
@@ -2829,7 +3126,7 @@ proc rex_command args {
if {[config_option copy]} {
lappend config(data) $config(progname)
- set config(command) "[getiterpreter $config(progname)] [file tail $config(progname)] $config(params)"
+ set config(command) "[getinterpreter $config(progname)] [file tail $config(progname)] $config(params)"
}
debug 2 "sudo=$config(sudo), progname=$config(progname), params=$config(params), command=$config(command)"
@@ -2855,7 +3152,7 @@ proc rex_list {} {
global config
global env
- ::getopt::parse \
+ rex_parse_cmdline \
-usage {rex list [groups]} \
-alias {{rex list [OPTIONS] hosts}} \
-docstring {Lists hostgroups or hosts in a hostgroup} \
@@ -2864,20 +3161,21 @@ hostgroups along with their descriptions.
In the second form, lists hostnames obtained as a result of applying
the OPTIONS (at least one must be given).} \
- -postprocess common_config_setup \
- -before {global config} argc argv {
+ -group global {
group,g=NAME
{select host group}
- { set config(option,hostgroup) $optarg }
+ { option_set group $optarg }
+ } \
+ -group mode {
host,H=HOST
{add HOST to the host list}
- { lappend config(hosts) {*}[split $optarg ","] }
+ { lappend ::config(hosts) {*}[split $optarg ","] }
exclude-host,X=HOST
{remove HOST from the host list}
- { lappend config(exclude_hosts) {*}[split $optarg ","] }
+ { lappend ::config(exclude_hosts) {*}[split $optarg ","] }
ignore-hosts,i
{ignore the list of hosts read from the hostgroup file}
- { set config(option,ignore-hosts) 1 }
+ { option_set ignore-hosts true }
}
switch -- $argc {
@@ -2887,6 +3185,7 @@ the OPTIONS (at least one must be given).} \
hosts {
hostlist_setup
if {[info exists config(option,hostgroup)]} {
+ puts "Hosts in group $config(option,hostgroup)"
foreach host $config(hosts) {
puts "$host"
}
@@ -2915,23 +3214,21 @@ proc rex_copy_from {} {
global env
global config
- ::getopt::parse \
+ rex_parse_cmdline \
-usage {rex rcp|copy-from [OPTIONS] HOST FILE [FILE...] DEST} \
-docstring {Copies FILEs from HOST to DEST on the local machine.} \
- -postprocess common_config_setup \
- -before {global config} argc argv {
-
+ -group mode {
interactive,I
{interactively ask for missing usernames and passwords}
- { set config(option,interactive) 1 }
+ { option