Regsub not manipulating string from URL - tcl

I am getting a strange TCL error when using this iRule, the error is:
<HTTP_REQUEST> - ERR_ARG (line 2) invoked from within "HTTP::uri
[regsub "/3dnotification" [HTTP::uri] ""] "
This is an F5 irule code.
This what I have tried:
when HTTP_REQUEST
{
if { not ( [string tolower [HTTP::uri]] starts_with "/socket.io" )} then {
HTTP::uri [regsub "/3dnotification" [HTTP::uri] ""]
# need to strip trailing slash on URI otherwise results in 404 for resources...
HTTP::uri [regsub "\/$" [HTTP::uri] ""]
} elseif { [string tolower [HTTP::header Upgrade]] contains "websocket" } {
ONECONNECT::reuse disable
set oc_reuse_disable 1
}
HTTP::header replace "X-Forwarded-ContextPath" "/"
}
when SERVER_CONNECTED {
if { [info exists oc_reuse_disable] } {
# Optional; unnecessary if the HTTP profile is disabled (goes into passthrough mode).
ONECONNECT::detach disable
unset oc_reuse_disable
}
}

Since the URI is either a full URI or the protocol-less part (I can't quite tell which from what you say; I'll assume either is possible), removing a leading part or trailing part is going to be a little bit tricky. What you need to do is to first split the URI into its component parts, apply the transformation to the path part, and then reassemble. The key to the splitting and reassembly is the uri package in Tcllib.
package require uri
# Split the URI and pick out the path from the parts
set uriParts [uri::split [HTTP::uri]]
set path [dict get $uriParts path]
# Do the transforms
set path [regsub "/3dnotification" $path ""]
set path [string trimright $path "/"]; # A different way to remove trailing slashes
# Reassemble and write back
dict set uriParts path $path
HTTP::uri [uri::join {*}$uriParts]
I'm assuming that you'd put the package require (or whatever else you need in order to get the code present) at the top of the script, and the rest inside the right when clause(s).
So that you can see what URI splitting actually does, here's your example URI split (in an interactive tclsh session):
% set uri "http://www.example.com:8080/main/index.jsp?user=test&login=check"
http://www.example.com:8080/main/index.jsp?user=test&login=check
% uri::split $uri
fragment {} port 8080 path main/index.jsp scheme http host www.example.com query user=test&login=check pbare 0 pwd {} user {}
As you can see, the path part is just main/index.jsp which is enormously easier to work with than the whole URI.

Related

Is it possible to capture the error output of glob?

Suppose if I have the following glob command wrapped in try/trap/finally:
proc generateSubmissionFolder {cover_letter_resume submission_path} {
set submission_parent [file dirname $submission_path]
set latest_submission_folder [lindex [lsort [glob -directory $submission_parent -type d *]] end]
set latest_submission_file [lindex [glob -directory $latest_submission_folder *[file extension $cover_letter_resume]] end]
createSubmissionFolder $latest_submission_file $submission_path
}
proc createSubmissionFolder {source destination} {
puts "Creating $destination folder."
file mkdir $destination
puts "Copying $source to $destination"
file copy $source $destination
}
try {
# I gathered user input and stored them in the variables $company_name and $position.
set submission_path [file join $company_name $position $yearmonthday]
if {[file exists [file dirname $submission_path]]} {
generateSubmissionFolder $coverletterresume $submission_path
} else {
createSubmissionFolder $coverletterresume $submission_path
}
} trap {Value Empty} {errormessage} {
puts "$errormessage"
} finally {
puts "$argv0 exiting."
}
If no folder is found, I would like to provide a human readable error message, but I'm unsure of what error to trap. According to the answer to my previous question:
Tcl doesn't have a pre-defined hierarchy of exceptions.
The only work around I tried was to use the -nocomplain switch and afterward check if the latest_submission_folder was blank.
Is there a way to trap a FileNotFound or FolderNotFound error?
For trivial cases like your example, use an on error handler, not trap. Or use catch instead of try.
Example tclsh session:
% try { glob *.bar } on error {what} { puts "Ooops: $what" }
Ooops: no files matched glob pattern "*.bar"
% if {[catch { glob *.bar } result] == 1} { puts "Ooops: $result" }
Ooops: no files matched glob pattern "*.bar"
Or if you do want to use trap because you also want to handle a bunch of other possible specific errors from more complicated code, glob raises a TCL OPERATION GLOB NOMATCH on failure:
% try { glob *.bar } trap {TCL OPERATION GLOB NOMATCH} {msg} { puts "Ooops: $msg" }
Ooops: no files matched glob pattern "*.bar"
You can discover what to use with trap for any given command's particular error with something like:
% catch { glob *.bar } result errdict
1
% dict get $errdict -errorcode
TCL OPERATION GLOB NOMATCH
In this specific case, glob has an option that helps: -nocomplain. It turns off the error on no match — which was only ever really intended for interactive use — as a lot of use cases can cope with an empty returned list just fine. (It's the way it is for historical reasons, and is maintained that way so we don't break the large number of existing scripts that use it. As language warts go, it's not too terrible.)
set latest_submission_folder [glob -directory $submission_parent -type d *]
if {![llength $latest_submission_folder]} {
# Make your own handling right here. Whatever you want.
puts "I didn't find a directory inside $submission_parent"
} elseif {[llength $latest_submission_folder] > 1} {
# Many subdirectories found. Is this an error given your single-thing var name?
} else {
# Strip a layer of list; good idea in case directory name has a space in it!
set latest_submission_folder [lindex $latest_submission_folder 0]
}

TCL regsub uses RegEx match as index in associate array

I'd like to automatically convert URLs, i.e
"https://sc-uat.ct.example.com/sc/" into "https://invbeta.example.com/sc/"
"https://sc-dev.ct.example.com/sc/" into "https://invtest.example.com/sc/"
"https://sc-qa.ct.example.com/sc/" into "https://invdemo.example.com/sc/"
I've tried following code snippet in TCL
set loc "https://sc-uat.ct.example.com/sc/"
set envs(dev) "test"
set envs(uat) "beta"
set envs(qa) "demo"
puts $envs(uat)
regsub -nocase {://.+-(.+).ct.example.com} $loc {://inv[$envs(\1)].example.com} hostname
puts "new location = $hostname"
But the result is: new location = https://inv[$envs(uat)].example.com/sc/
It seems that [$envs(uat)] is NOT evaluated and substituted further with the real value. Any hints will be appreciated. Thanks in advance
But the result is: new location =
https://inv[$envs(uat)].example.com/sc/ It seems that [$envs(uat)] is eval-ed further.
You meant to say: [$envs(uat)] is not evaluated further?
This is because due to the curly braces in {://inv[$envs(\1)].example.com}, the drop-in string is taken literally, and not subjected to variable or command substitution. Besides, you don't want command and variable substitution ([$envs(\1)]), just one of them: $envs(\1) or [set envs(\1)].
To overcome this, you must treat the regsub-processed string further via subst:
set hostname [subst -nocommands -nobackslashes [regsub -nocase {://.+-(.+).ct.example.com} $loc {://inv$envs(\1).example.com}]]
Suggestions for improvement
I advise to avoid the use of subst in this context, because even when restricted, you might run into conflicts with characters special to Tcl in your hostnames (e.g., brackets in the IPv6 authority parts). Either you have to sanitize the loc string before, or, better work on string ranges like so:
if {[regexp -indices {://(.+-(.+)).ct.example.com} $loc _ replaceRange keyRange]} {
set key [string range $loc {*}$keyRange]
set sub [string cat "inv" $envs($key)]
set hostname [string replace $loc {*}$replaceRange $sub]
}

Extracting query string value

How to extract the username value from this query string (HTTP url-encoded): username=james&password=pwd in Tcl?
I can get it through Java's request.getParameter("username"); but how to get using Tcl?
The first stage is to split the query string up, and form a dictionary of it (which isn't strictly correct, but I'm guessing you don't care about the case where someone puts multiple username fields in the query string!). However, you also need to decode the encoding of the contents, and that's pretty awful:
proc QueryStringToDict {qs} {
set mapping {}
foreach item [split $qs "&"] {
if {[regexp {^([^=]+)=(.*)$} $item -> key value]} {
dict set mapping [DecodeURL $key] [DecodeURL $value]
}
}
return $mapping
}
proc DecodeURL {string} {
# This *is* tricky! The URL encoding of fields is way nastier than you thought!
set mapped [string map {+ { } \[ "\\\[" \] "\\\]" $ "\\$" \\ "\\\\"} $string]
encoding convertfrom utf-8 \
[subst [regsub -all {%([[:xdigit:]]{2})} $string {[format %c 0x\1]}]]
}
set qs "username=james&password=pwd"
set info [QueryStringToDict $qs]
puts "user name is [dict get $info username]"
In 8.7 (currently in alpha) it'll be much simpler to do that inner encoding; there won't need to be that subst call in there for example. But you haven't got that version of Tcl; nobody has (except for people who insist on being right on the bleeding edge and get themselves into trouble over it).
Assuming this is a CGI environment, where the environment will contain
REQUEST_METHOD=GET
QUERY_STRING='username=james&password=pwd'
or
REQUEST_METHOD=POST
CONTENT_LENGTH=27
# and stdin contains "username=james&password=pwd"
then use tcllib's ncgi module
$ cat > cgi.tcl
#!/usr/bin/env tclsh
package require ncgi
::ncgi::parse
array set params [::ncgi::nvlist]
parray params
$ printf "username=james&password=pwd" | env REQUEST_METHOD=POST CONTENT_LENGTH=27 ./cgi.tcl
params(password) = pwd
params(username) = james
$ env REQUEST_METHOD=GET QUERY_STRING='username=james&password=pwd' ./cgi.tcl
params(password) = pwd
params(username) = james
An alternative to Donal's suggestion, sharing the spirit, but building on battery pieces: tcllib rest package:
(1) To process the query (as part of a valid URL)
% package req rest
1.3.1
% set query [rest::parameters ?username=jo%3Dhn]; # http:// is default scheme, ? is minimum URL boilerplate
username jo%3Dhn
(2) Run a URL decoder (e.g., the one by Donal or the one from Rosetta code):
% proc urlDecode {str} {
set specialMap {"[" "%5B" "]" "%5D"}
set seqRE {%([0-9a-fA-F]{2})}
set replacement {[format "%c" [scan "\1" "%2x"]]}
set modStr [regsub -all $seqRE [string map $specialMap $str] $replacement]
return [encoding convertfrom utf-8 [subst -nobackslash -novariable $modStr]]
}
then:
% set info [lmap v $query {urlDecode $v}]
username jo=hn
% dict get $info username
jo=hn

Assigning value to a variable only if argv specified in TCL

I am new to the TCL scripting .I have a script called "Sample.tcl". In the Sample.tcl I have a variable called $name. How can I assign a value to the variable if there exist a specific argv i.e.
Sample.tcl -step xyz
Only if I specify -step then $name should be xyz.
I'm not sure what $name might be in this context (it's a really unusual name for a variable, and using variable variable names is typically a bad idea) but under the guess that you're trying to set step to xyz in this case, you can put this in your script:
apply {{} {
# For each pair of values in the arguments (after the script name)
global argv
foreach {key value} $argv {
# Safety-check: if the key starts with a hyphen...
if {[string match -* $key]} {
# ... strip the leading hyphen(s)
set varname [string trimleft $key "-"]
# ... bind that global var name to a local name
upvar 1 $varname var
# ... and set the variable to the value we've got.
set var $value
}
}
}}
It's done in an apply so that we don't pollute the global namespace with all our working variables (key, value, varname and var) and because we don't really need to make a procedure for something we're only going to do once.
This isn't a safe piece of code, not by any means, but it is a useful and flexible way to get something working.
In general, parsing command line arguments can take quite a bit of thought to get perfectly right and there's various packages to help out, but that's only really important when writing code for other people to run. When it's just for yourself, you can be a lot sloppier and get the job done in a few minutes.
Using the cmdline package from tcllib you could write:
#!/usr/bin/env tclsh
package require cmdline
set options {
{step.arg "" "Set the step value"}
}
try {
array set params [cmdline::getoptions argv $options]
} on error e {
puts stderr $e
exit 1
}
if {$params(step) ne ""} {
set name $params(step)
}
if {[info exists name]} {
puts "name = $name"
} else {
puts "name is not set"
}

how can i use input.properties files like concept in TCL script

in ANt script we access properties file as below
<property file="input.properties"/>
in perl script we access properties file as below
do "config.cfg";
same way how can i access properties file in TCL script.
Can anyone help me out pls?
thanks in advance...
Okay, if you want it as dumb as in Perl, just source the file in Tcl.
Configuration file sample (named config.tcl):
# Set "foo" variable:
set foo bar
To load this configuration file:
source config.tcl
After source-ing, you can access your variable foo in your script.
As with perl, a malicious user might put something like
exec rm -rf ~
in your "config file" and wish you all the good luck.
The equivalent of perls
$var = "test";
is in Tcl
set var "test"
So if you want it as easy as in Perl, I suggest kostix answer.
But you could also try to use dicts as config file:
This will look like
var {hello world}
other_var {Some data}
foo {bar baz}
I personally love using this, it allows even nesting:
nestedvar {
subvar {value1}
subvar2 {value2}
}
And comments: Kind of a hack, in fact has the key #
# {This is a comment}
Parsing:
set fd [open config.file]
set config [read $fd]
close $fd
dict unset config #; # Remove comments.
Access:
puts [dict get $config var]
puts [dict get $config nestedvar subvar]
But if you want really something like $var = "foo"; (which is valid Perl code but not Tcl), then you have to parse this file yourself.
An example:
proc parseConfig {file} {
set fd [open $file]
while {[gets $fd line] != -1} {
if {[regexp {^\s*\$([^\s\=]+)\s*\=\s*(.*);?$} $line -> var value]} {
# The expr parses funny stuff like 1 + 2, \001 inside strings etc.
# But this is NOT perl, so "foo" . "bar" will fail.
set ::$var [expr $value]
}
}
}
Downside: does not allow multi-line settings, will throw an error if there is an invalid value, and allows command injection (but you Perl solution does that too).
The simplest mechanism is to either make it a script or to make it the contents of an array. Here's how to do the latter while still supporting comments:
proc loadProperties {arrayName fileName} {
# Put array in context
upvar 1 $arrayName ary
# Load the file contents
set f [open $fileName]
set data [read $f]
close $f
# Magic RE substitution to remove comment lines
regsub -all -line {^\s*#.*$} $data {} data
# Flesh out the array from the (now clean) file contents
array set ary $data
}
Then you'd use it like this:
loadProperties myProps ~/myapp.props
if {[info exists myProps(debug)] && $myProps(debug)} {
parray myProps
}
With a file in your home directory (called myapp.props) like this:
# Turn on debug mode
debug true
# Set the foos and the bars
foo "abc"
bar "Harry's place downtown"
You can do a lot more complicated than that, but it gives you an easy format to get going with.
If you prefer to use an executable configuration, just do:
# Define an abstraction that we want users to use
proc setProperty {key value} {
# Store in a global associative array, but could be anything you want
set ::props($key) $value
}
source ~/myapp_config.tcl
If you want to restrict the operations to ones that won't cause (much) trouble, you need a slightly more complex approach:
interp create -safe parser
proc SetProp {key value} {
set ::props($key) $value
}
# Make a callback in the safe context to our main context property setter
interp alias parser setProperty {} SetProp
# Do the loading of the file. Note that this can't be invoked directly from
# within the safe context.
interp invokehidden parser source [file normalize ~/myapp_config.tcl]
# Get rid of the safe context; it's now surplus to requirements and contaminated
interp delete parser
Safety has pretty low overhead.