English Amiga Board


Go Back   English Amiga Board > Coders > Coders. System > Coders. Scripting

 
 
Thread Tools
Old 08 July 2015, 16:21   #41
BigFan
Registered User
 
BigFan's Avatar
 
Join Date: Feb 2014
Location: Germany
Posts: 261
Reserved special characters

Time for more basic stuff

In a previous example we addressed DirOpus and send a command named "Verify" to
open a requester. If you quickly browse the list of functions ARexx supplies, you
will find a function with exact same name.
How does ARexx know?
There is a tiny variation in our code that has a big effect on how ARexx interpretes
what we wrote.

A command is interpreted if it has no adjacent special characters like "." or "("
Moving open parenthesis next to it, turns the command into a function.
Set a period close to it and it will be identified as a stem.
Wrap it in ' ' and ARexx handles a string.
ARexx will check its internal table of symbols before interpreting a symbol as command.

ARexx will complain about any syntactic error that does not accidentily fits a
correct expression, command, function or statement.

Reserved special chars

. "dot" used for stems
, "comma" glues multiple lines into one statement if you need more space to code
() "parentheses" identifies priorization
( "open parenthesis" plus symbol = function
; "semicolon" seperates statements
: "colon" identifies a label

Some commands require a counterpart to be interpreted correctly:

Do ... End
Select ... End
If ... Then
When ... Then


What happens if code contains a symbol of same name as function or command?

In most cases this will cause an error message that is quite irritating. Or as in
example below, nothing happens at all.
Think of the following code:

Code:
/**/
string = 'This is a string with a number 42'
num = Words(string)  /* counting words , num contains now value 8 */
If Datatype(Word(string, num)) = NUM ,  /* check if last word is of numeric type*/
Then Say "We have a number!"
The string "We hav..." doesn't show up.
"If" conditiona checks against 8 not NUM. Remember, we deal with upper case mostly.
Our symbol num is NUM.

If you insist on the usage of num, then you have to wrap NUM in single quotes.
Code:
If Datatype(Word(string, num)) = 'NUM'
Now ARexx differentiates.

Same with commands. If a symbol exists that shares same name with a command used
by a host, wrap the command in quotes.

Your own functions may have same name as a command, use quotes for the command.

See upcoming chapter about function and procedure.

Last edited by BigFan; 08 July 2015 at 17:23.
BigFan is offline  
Old 08 July 2015, 16:33   #42
BigFan
Registered User
 
BigFan's Avatar
 
Join Date: Feb 2014
Location: Germany
Posts: 261
Modularity and functions

Modularity

Do you remember me writing about file endings? .rexx in particular?

ARexx searches for files with .rexx first in current directory then in logical
drive REXX:. If the file ending is omitted, the file name matches both variants
e.g. file name is "wordcount.rexx", searching for "wordcount" matches as well as
"wordcount.rexx".
How is this related to modularity?
Imagine you wrote lots of small scripts for different purposes like :
word filter, word count, backup strategy, ports comunication, helper process.
All your scripts are named <filename>.rexx and reside in REXX:. Now you can use
them as command/function in your newly written code by typing their filename
without file ending. Lets assume a script called welcome.rexx is in REXX:
Code:
/**/
...
your code
welcome
...
To make the most of it your code has to be aware of arguments and parameters
passed to your external script as well as returning values from scripts.

ARexx features following methods to interact with external code:

Parsing arguments, using command "Arg" or "Parse" and check their existence with
"Arg()"

Returning values with command "Return"

Copy to Clipboard. The clipboard is available globally for all rexx programs.

Rexxsupport.library provides methods to open a rexx message port . It allows you
to send commands to a rexx program using "Address <portname> <command>". You have
to run this second program first to be up before the caller.
Use "WaitforPort" inside caller to avoid timing problems.

This lib offers the functions "AllocMem()/FreeMem()". They look similar to Getspace()/
Freespace, but memory allocated this way is public, while Getspace() takes from REXX
internal memory pools, which is private. Data stored in allocated memory can be shared
with other programs and is not freed automatically on exit. Be careful to not waste mem.

Modularity is not restricted to external code. The larger the script grows in
complexity, the higher the risk of failure and the less the readability. In short
your code becomes weird. This is called "spaghetti code".
A simple and recommended programming style is the use of own funtions.

Functions

To create your own function place a label. A label is a symbol with a colon.
Set command "Procedure" after your label. This can be placed anywhere in your
function but only once.
End function with "Return" e.g.

Code:
/**/
...
CheckForPort: Procedure 
... 
your code here
...
Return msg
To run code from own functions simply call them using command "Call" when needed.
The interpreter then stops execution at current line, jumps to the label, executes
the code and returns back to the next statement right after the call.

In very small scripts this would create some overhead, but in scripts with repetive
code sections this will shrink the code and structures it for ease of maintenance.

Functions have some advantages:

Protection of symbols
Your symbols outside your function are save from being overwritten. This feature
is on by default. It can be turned on or off for each function block, e.g.
Code:
/**/
...
CheckForPort: Procedure Expose i j
Now the variables i and j used by the caller will be overwritten with values
from your function. Expose has a left to right read order. Stems are translated
only as leftmost argument or as sole argument (msg.a becomes msg.21 if a = 21).
With " Expose A msg.A ", A becomes 21 while msg.A remains msg.A.
Compounds can be exposed
all at once using their stem (CUBE. addresses CUBE.X, CUBE.X.Y, CUBE.X.Y.Z etc).

Argument parsing
upto 15 arguments can be passed, parsed and assigned to variables for functions.
Commands use 1 Argument passed to them as a string. Parsing of a string is not
restricted by the means of length or number of substrings.

Recursion
a function can call itself. this results in very dense code. think of factorials.

Last edited by BigFan; 12 July 2015 at 10:04.
BigFan is offline  
Old 09 July 2015, 18:14   #43
BigFan
Registered User
 
BigFan's Avatar
 
Join Date: Feb 2014
Location: Germany
Posts: 261
Creating functions and commands

Funtions using Date()

ARexx offers 2 functions to check for current date and time, and they are named
Date() and Time() to no surprise.

Date() supports lots of ways to format the output string. DAYS, WEEKS, MONTHS in
this year, or in this CENTURY. NORMAL is like '10 Jul 2015'.
Use EUROPEAN or US format or SORTED as 20150709. INTERNAL is the days since
01.01.1978. Date() accepts another date as new base.
Syntax: Date(option,[date],[I|S]) /* 'I'nternal is default */

Example:

Code:
/**/

Say Date('u') /* tell us in US format */

dat0 = Date('e',Date('i')-120) /* Tell the date 3 month ago */
Say dat0  

/*Or in half a year */  

dat0 = Date('e',Date('i')+180)  /* or around 6 month past today*/

Exit
==>

07/09/15 <-- US today
11/03/15 <-- EU - 120 days ago
05/01/16 <-- EU in 180 days

These short forms use slashes. If we are going to update a version string with a new
date, as a revision bumper f.e., we have to convert this. The function in question
is Translate(). This function takes a template string (our date), a string with replacers
('.') and a string with what to be substituted ('/').

Code:
dat0 = Translate(dat0,'.','/')
==> 07.09.15

Now we need it bracketed. Simply call Insert() for surgery, with dat0 as injection,
a short string of closed parentheses as victim and the optional position to set marks
for the syringe.

Code:
dat0 = Insert(dat0,'()',1)
==> (07.09.15)

Voilà!

Let's make it a versatile function that can be used in future projects.

Use INTERNAL format for this, e.g.:

Code:
/* */
Options Results

Call ConvDate Date('i')
version.date = RESULT
Say version.date
EXIT

/* Date converter */
/* Accepts internal date format and converts it to comply
 * with version string requirements
 */
ConvDate: Procedure
Arg dat0

If dat0="" Then Return 0
If Verify(dat0,'0123456789') > 0 Then Return 0 /* check for alien characters */
dat0 = Date('e',dat0)                          /* internal to european */          
dat0 = Insert(Translate(dat0,'.','/'),'()',1)  /* beautify it */

Return dat0
To make this a command, strip off the procedure stuff and save it as "convdate.rexx"
in "REXX:"

Code:
/* Date converter */
/* Accepts internal date format and converts it to comply
 * with version string requirements
 */
Arg dat0

If dat0="" Then Return 0
If Verify(dat0,'0123456789') > 0 Then Return 0
dat0 = Date('e',dat0)
dat0 = Insert(Translate(dat0,'.','/'),'()',1)

Return dat0
Now place your new command 'convdate' in your code like this

Code:
/**/
Options RESULTS

ConvDate Date('i')
version.date = RESULT
Say version.date
EXIT

Last edited by BigFan; 09 July 2015 at 19:54.
BigFan is offline  
Old 09 July 2015, 19:34   #44
BigFan
Registered User
 
BigFan's Avatar
 
Join Date: Feb 2014
Location: Germany
Posts: 261
Translate is as powerful as confusing. In our exemple we had just one character
to be replaced with another. That is simple. The routine runs through our table
and picks from output what is defined in input according to the position.
But Translate is able to work out longer strings and to fill the gap if the output
string is shorter than input string.
The syntax goes :

Translate(template, substitute, indicator, filler)

The indicator uses the template to create a position table.

This table indicates what character has to be taken as substitute.

If there are no more characters in substitute than use the filler for the gaps.

Feel clever?
What is the output from the code snippet below ?
Code:
/**/
Say Translate('123 456 789 80#','it gdC uyo ani','80 971 645 238#','?')
Have fun

Last edited by BigFan; 11 July 2015 at 15:17.
BigFan is offline  
Old 11 July 2015, 14:48   #45
BigFan
Registered User
 
BigFan's Avatar
 
Join Date: Feb 2014
Location: Germany
Posts: 261
Parsing arguments

Parsing

One of the best features of ARexx is its parser. It is not restricted to command
line or function arguments. In fact you may parse anything from one of its input
channels. The entire argument is treated as being one long string. If the template
has more than one target, the parser looks for substrings seperated by white space.
If templatecontains strings, they get temporarily deleted from argument string.
The parser then continues with the next substring.
Values in [] are optional.

Syntax
Parse [upper] source template [,template] [,more templates] [.]

upper - is optional and translates the input to upper case

source - is one of
  • ARG - read command or function arguments
  • VAR name - use variable "name" as input (must be initialized before)
  • Pull - use 'STDIN' as input (console window mostly)

template - is a list of symbols (uninitialized variables) to assign values to.
A template consists of markers (a position) and targets (the variable to extract).
',' - seperates multiple templates when pulling (use without quotes)
'.' - is a signal dot that marks the end of line to stop parsing and tokenization.
The last value will be taken from input but assigned nowhere (use without quotes)

Examples:

Argument was "file"
Arg type
=>
type = FILE
Arg is similar to Parse Upper Arg

Argument was "5 6"
Arg i
=>
i="5 6"
i is a string now, can't be used for math

Arg i j
=>
i="5"
j=" 6"
i and j are strings of numeric type, useable in arithmetic operations now
Arg i j k
=>
i="5"
j="6"
k=""
(k is now an existing but uninitialized symbol, a LITERAL, use Symbol() to check)

Argument was "drinks food each $1"
Arg d f 'each' '$'p
=>
d="DRINKS"
f="EACH $1"
p=
Arg is shorthand Parse UPPER Arg. Either use Parse Arg or upper case in template

Argument was "Soda £1.50 Coffee £1.50 all prizes incl vat"
Parse Arg i j k l
=>
i=Soda
j=£1.5
k=Coffee
l=£1.5 all prizes incl vat
Parse Arg i j k l .
=>
i=Soda
j=£1.5
k=Coffee
l=£1.5
the remaining substring is not assigned to anything
Parse Arg i "£"j k "£"l .
=>
i=Soda
j=1.5
k=Coffee
l=1.5
currency symbol has been taken from argument string while parsing

Argument was "Soda £1.50 Coffee £1.50"

Parse Arg i "£"j k "£"l
=>
i=Soda
j=1.5
k=Coffee
l=" 1.5"
<== pay attention, the last gets it all including the white space

TIPP: Whenever possible, close the template with a dot to avoid unsolisticated
strings or white spaces.



Using markers

Markers are evaluated left to right in ascending order. If the next marker has
lower value than the previous, the parser rewinds to that position.
An operator like + or - steps from current position in right(+) or left()- direction.

Argument was "Me and Susan had a bad day!"

Parse Arg 1 me 8 su 1 we +17 "bad " -2 day
=>
me="Me and
su=Susan had a bad day!"
we="Me and Susan had a"
day="a day!"
The substring "bad " is hidden from input so -2 jumps to the position of "a" before
"bad" not "d" from "bad".

Please notice, the input string is not altered in any way. It is still the same.
The parser is doing modifications temporarily. You may parse it again with different
markers and targets.

Multiple templates
Multiple templates can be used when pulling from a console. Each template requires
a new input. Use comma to seperate templates.

Parse Pull last first, street, town
This requires the user to send 3 times his input using return key.
Assume input was
"Clause Santa"
"there is no street or road"
"why the heck should i tell you"
=>
first="Santa"
last="Clause"
street="there is no street or road"
town="why the heck should i tell you"

If we'd put commas in quotes, the parser would have tried to read all in once,
leaving street and town uninitialized.

Arguments can be parsed in functions by the user manually.
To do it yourself see following example

Code:
/* Summarize with variable argument length */
Say Sum( 5, 4, 3, 2)
Exit

Sum:procedure
sum=0
Do i=1 to Arg()
  sum = sum + Arg(i)
End
Return sum
This does not work with arguments passed to a program from outside (cli). A program
is considered being a command, not a function. Arguments are passed to commands
as one string. In our small example, Arg() will report 1 and Arg(1) is "5, 4, 3, 2"
if this is fetched from command line.
Invoking with :> rx sum 5 4 3 2

Example of erroneous code
Code:
/* Summarize with variable argument length */
sum=0
Do i=1 to Arg()           /* Arg() is 1*/
  sum = sum + Arg(i)      /* arithmetic error, sum is a number, 
                             Arg(1) is a string */
End
Return sum

Last edited by BigFan; 12 July 2015 at 10:15.
BigFan is offline  
Old 12 July 2015, 09:56   #46
BigFan
Registered User
 
BigFan's Avatar
 
Join Date: Feb 2014
Location: Germany
Posts: 261
Code obfuscation

Code obfuscation

This isn't useful at all, but fun anyway
Imagine i gave you a code job and it seems too difficult to you. You then simply
read the provided example without trying yourself first. I'm fine with that.
But i like to see a solution you did yourself so i hide the code slightly.
This is highly hypothetical, of course
The next example is very short. Would be more impressive on large code blocks.

Exercise:
Code a script that calculates the factorial of a given integer value.
Use recursion to do that. Factorial is multiplication of all integers ranging
from 1 to a given value. E.g. !5 is 1*2*3*4*5 is 120.
Recursion means, a function is calling itself to iterate.

Possible solution with obfuscation

Code:
/*
 * Factorial
 * using recursion
 *
;)f(cf = s;f grA;stluser snoitpo
;n grA esraP;erudecorP :cF;s nruter
)1-n(cF*n nruteR eslE;1 nruteR nehT 1=n fI
*/
Arg f .
  If f<1 Then Do
  Say "Error!"
  Say "Usage: rx factorial Val/N (>=1)"
  Exit
End

Options RESULTS
Pragma(D,"T:")

Open(t,"temp.rexx",w)
Writeln(t,'/* */')
Do L=5 to 7
  Writeln(t,reverse(sourceline(L)))
End
Close(t)
f = trunc(f)
temp f
r = RESULT
Say "Fakulty of "f" = "r

Address command 'delete >nil: t:temp.rexx'
Exit
In the script above the recursive function is hidden in the comments.
Peeking at the code doesn't help until you run it and analyze the output.
Or find another way to reverse the code (watching it in a mirror won't do it )
The minimum value is 1, the maximum is 171. Higher values exceed the limits of
numerical representation. The code is fast enough on standard A1200.
I have not checked stock A500.

Some remarks to the functions used
Pragma() can be used to query or set the active directory.
Options to Pragma(option,value) are
'D'-irectory, string with new directory as value or without to query current
'P'-riority, changes program runtime priority
'I'-D, the task id of your active rexx code. can be used for unique names
'S'-tack, alters the stack for the program running. Queries without a value.
'W'-orkbench, switch WB requester on/off (file not found, insert disk etc.)
*-redirects console iostreams (i never used this)

Reverse() is just doing to a string what the name says.

Sourceline(), without value can tell the number of lines in your script, a numeric
value makes it read the named line.
Because the code is hidden in the comments section, we had to put a comment on
top of the newly created or the resulting script won't be accepted as a valid
arexx script/command.

Trunc() is used to truncate the digits, the value becomes integer.

The code could be scrambled or encrypted using more advanced routines. To make it
more confusing start with hex values.

Instead of obfuscating the code, a useful script is to dense the code. Delete
comments that are not needed. Remove empty lines. Concatenate statements with
semicolons all in one block. The file uses less space on disk then. Tools to do
this were once called ARexx-Compilers, though they offered no compiling but a
little bit of compression.

Last edited by BigFan; 14 July 2015 at 13:14. Reason: typo
BigFan is offline  
Old 12 July 2015, 10:00   #47
BigFan
Registered User
 
BigFan's Avatar
 
Join Date: Feb 2014
Location: Germany
Posts: 261
Parsing with variable length

Parsing with variable length

It is fairly easy to parse any arguments if they have some sort of identifiers like
white space, special or reserved characters (tab, + sign), or are comma seperated.
But what it neither start nor length are fixed?
Assumption is made, the format has a position code that tells us where to start
reading to what length. Everything behind varies from record to record. We could
code our own parser but we don't have to.

Do parse the following

"016014jdklfjadaAdamski-Killercyvyvmmm-"
"010031***MelanieThornton-Wonderful Dream5555555"
"013030trpuwqMinistry-Everyday Is Halloween07052002"
"007030Ministry-Jesus Built My Hotrodbadrip"

The trick is, the parser sports variable position markers. This is done by reading
first the position and length from defined format, then use this variables again
on the record. Remember, you are allowed to parse arguments, pulls and vars
as often as you like.

First position index

016014

First 3 decimals represent the start, following 3 the length.
VALUE is the option to tell the Parser what to parse (record in this case).
With is a delimiter for expression with no other meaning. (VALUE expression WITH)

Example for parsing
Code:
record=Arg(1) /*"016014jdklfjadaAdamski-Killercyvyvmmm-"*/

Parse VALUE record WITH 1 start +3 len +3 =start mp3.entry +len
When parsing the targets get filled like:

start = 16
len = 14
mp3entry = Adamski-Killer

The equal sign qualifies a variable to be a fixed position marker, while +len
is a variant. The parser jumps to position stored in "start", reads the next
"len" chars and assign it to mp3entry

Again:
Our starting position is 1
Read next 3 chars and store in "start"
Read next 3 chars and store in "len"
Interprete "start" as position index (jumps to char 16, "A" of Adamski)
Read next "len" chars and store in mp3.entry (position is "r" of Killer)

Last step is to parse mp3entry to get artist and title seperately.
Parse VAR mp3.entry artist '-' title

Code:
/**/
record="016014jdklfjadaAdamski-Killercyvyvmmm-"

Parse VALUE record WITH 1 start +3 len +3 =start mp3.entry +len
parse var mp3.entry artist '-' title
say start
say len
say mp3entry
say artist
say title
Lets do a loop

Code:
/**/
record.1="016014jdklfjadaAdamski-Killercyvyvmmm-"
record.2="016014jdklfjadaAdamski-Killercyvyvmmm-"
record.3="010031***MelanieThornton-Wonderful Dream5555555"
record.4="013030trpuwqMinistry-Everyday Is Halloween07052002"
record.5="007030Ministry-Jesus Built My Hotrodbadrip"
Do i=1 to 5
Parse VALUE record.i WITH 1 start +3 len +3 =start mp3.entry.i +len
parse var mp3.entry.i artist.i '-' title.i

say mp3.entry.i
say artist.i
say title.i
end
BigFan is offline  
Old 12 July 2015, 15:11   #48
BigFan
Registered User
 
BigFan's Avatar
 
Join Date: Feb 2014
Location: Germany
Posts: 261
Back to start

All this started with scripts for DirOpus. Time to use our newly gained knowledge.

Problem:
When interacting with DOPUS, many commands deliver the selected or unselected
contant as a single string. Depending on how many files and directories are
retrieved, this string becomes very, very long. And to make it worse, it uses blanks
for seperating by default. Pathes and names containing blanks cause trouble when
reading the result string. They'll be taken as seperate files e.g. "Check o mat"
is split into 3 file names. We need a seperator to avoid misinterpretations.
But what character is uniqe and not allowed?

The mentioned scripts in the starting post use "*" (asterisk) as a seperator.
This is problematic, the "*" is a valid char in file names. It is not recommended
to use such characters for DOS names, but it is not forbidden.

Following characters are not allowed in file names: ":" colon and "/" slash.

The list of characters allowed includes: ";.,-_#'*+-°^~$" + " " (blank).
The only secure way to identify them as a real seperator and not part of the file
name is to use a combination (e.g. ;-) ), but again, someone might have named his
files like "MyBirthdayPics;-).ilbm"

A single unallowed character might not be unique, f.e. the colon might appear in
pathes or device names, but "::" will probably never happen.
I suggest "::" as delimiter. This is unallowed in files and pathes. Any path
ending with ":" then has 3 colons like "Ram Disk:::". Very easy to identify
when you read about parsing before.

Let's go:

Code:
/**/
Options Results

Address DOPUS.1

GetSelectedAll "::"
selection = result
say selection

Do s=1 until selection=""
Parse Var selection file.s "::" selection
end

Do i=1 to s
say file.i
end
==>
4.Ram Disk:> rx sel
Clipboards::ENV::T::check o mat::check o mat.info::say*.not::sel::

Clipboards
ENV
T
check o mat
check o mat.info
say*.not
sel
4.Ram Disk:>

The result has no path or devices names. A single ":" will do.
Using the parsers abilities is simple and easy to read.

Code can be reused and is royalty free. No magic, no fog.

And if you have read my examples carefully, you came across my question if a
Do While ~Eof() loop can't be done better. In a previous example i deleted last
element read to avoid redundant lines in output.
Compare to example above, the loop introduces a new option "until".
Loops are to be examined in next chapter.
BigFan is offline  
Old 12 July 2015, 15:57   #49
Korodny
Zone Friend
 
Join Date: Sep 2001
Location: Germany
Posts: 812
Quote:
Originally Posted by BigFan View Post
When interacting with DOPUS, many commands deliver the selected or unselected
contant as a single string. Depending on how many files and directories are
retrieved, this string becomes very, very long.
If a script has to iterate through all the selected files in a Directory Opus 4 lister, I usually used a different approach:

Code:
status 9 <active_lister_number>           /* # of selected files */
no_of_files = result

do no_of_files
   getnextselected <active_lister_number>
   current_file = result

    /* do something with the file here */

   selectfile '"' current_file '" 0 1'    /* 0 = deselect, 1 = immediately update display */
end
It is slightly slower, but behaves like the internal DOpus functions do - i.e. it works on the first selected file, then deselects it, then works on the next file. If you abort, all the unprocessed files are still selected.
Korodny is offline  
Old 12 July 2015, 17:32   #50
daxb
Registered User
 
Join Date: Oct 2009
Location: Germany
Posts: 3,303
You are using outdated DOpus version. DOpus5 example:

Code:
IF sel = '' THEN f = 'FILES'      /* All files */
ELSE f = 'SELFILES'         /* Selected files */
'LISTER QUERY ACTIVE' ; activehandler = result  /* Get handle of ACTIVE lister */
'LISTER QUERY' activehandler f 'STEM' filelist  /* Store (selected) files of ACTIVE lister into stem variable */
For DOpus4 you may can use '0A'x as seperator if DOpus4 can handle it.
daxb is offline  
Old 13 July 2015, 14:43   #51
BigFan
Registered User
 
BigFan's Avatar
 
Join Date: Feb 2014
Location: Germany
Posts: 261
@korodny
sure, getnextselected does it, all written in the examples given in this thread. The chapter is about parsing, i was trying to give and example for that. Maybe i stretched the DirOpus thingy a bit too often.
I'm sick of doin version stuff , but it is so easy
Except for the creators of that masterpiece below.
GetSelectedAll '*', was used in this script and please have a sharp eye on how the result string is split up
Code:
/*
 *  $VER: DOpus4-Version 1.2 (27 Jun 2015) by Wizardry and Steamworks
 *
 *      © 2015 Wizardry and Steamworks
 *
 *  PROGRAMNAME:
 *      DOpus4-Version
 *
 *  FUNCTION:
 *      Shows the version of file(s) in a DOpus4.
 *
 *  USAGE:
 *      ARexx command DOpus4-Version.rexx (from DOpus)
 *
 *  $HISTORY:
 *
 *  27 Jun 2015 : 1.2 : cleanups and fixes
 *  25 Jun 2015 : 1.1 : remove port, copy file, use random, support spaces (thanks @eab:daxb)
 *  17 Jun 2015 : 1.0 : initial release
 *
 */

/*------- Configuration Variables (change these to suit your setup) --------*/
VCMD = "C:Version"              /* Where to find the CBM Version command.   */
/*--------------------------------------------------------------------------*/

Opus = Address()    /* Get the DOpus address. */
Options RESULTS     /* Request results. */
Address value Opus  /* Use the DOpus address. */

Busy On             /* Set the busy pointer. */

/* Get all the selected items and check if something is actually selected. */
GetSelectedAll '*'  /* Get all the selected items. */
Items = RESULT

/* Normalize, heh... */
Items = Translate(Translate(Items, '/', ' '), ' ', '*')

n = Words(Items)    /* Get the number of selected items. */
If n <= 0 Then      /* Check if no items were selected. */
    Do              /* If no items were selected, */
        Busy Off    /* turn off the busy pointer. */
        Exit        /* and terminate. */
    End

/* Set the buttons for continue and abort. */
Status 26                   /* Get the value of the Ok button. */
OldOkay = RESULT
Status 26 set 'Continue'    /* Set the value of the button to continue. */
Status 27                   /* Get the value of the Cancel button. */
OldCancel = RESULT
Status 27 set 'Abort'       /* Set the value of the button to abort. */

/* Loop through the selected items and display their name and version. */
Do i = 1 To n                                   /* For all selected entries... */
    Name = Translate(Word(Items, i), ' ', '/')  /* Get the name of the entry and translate back. */
    ScrollToShow Name                           /* Scroll to the entry. */
    Status 13 i + 1                             /* Get the path to the entry. */
    Path = RESULT
    /* Generate temporary files. */
    FTMP = wasRandomHexString(Time(SECONDS), Length(Name))
    Address COMMAND "Copy >NIL:" '"'Path||Name'"' "T:"FTMP
    VTMP = wasRandomHexString(Time(SECONDS), Length(Name))
    Address COMMAND VCMD "T:"FTMP " > " "T:"VTMP
    Address COMMAND "Delete >NIL:" "T:"FTMP 'QUIET FORCE'
    /* Attempt to open the temporary file for reading. */
    If Open('output', "T:"VTMP, 'READ') ~= 1 Then
        Do                                      /* If the temporary file could not be opened, */
            Call wasDOpus4DeselectEntry(Name)   /* deselect the entry, */
            Iterate i                           /* and continue. */
        End
    Tmp = ReadLN('output')                      /* Get the output from the temporary file. */
    Close('output')                             /* Close the file. */
    Version = Word(Tmp, 2)                      /* Split the name into a name and a library version. */
    /* The assumption is that version strings must contain digits. */
    If wasStringContainsDigits(Version) ~= 1 Then   /* Check whether the version string contains digits. */
        Do                                          /* If the version string does not contain digits, */
            Call wasDOpus4DeselectEntry(Name)       /* deselect the entry, */
            Iterate i                               /* and continue. */
        End
    Select                                          /* Switch on i */
        When i = n Then Notify Name " " Version    /* When only one item remains selected just notify. */
        Otherwise                                   /* Otherwise, present a chooser whether to continue or abort. */
            Do
                Request Name " " Version                    /* Show the version. */
                CarryOn = RESULT                            /* Get continue or abort. */
                If CarryOn ~= 1 Then                        /* Check which button was pressed. */
                    Do                                      /* If the abort button was pressed then */
                        Call wasDOpus4DeselectEntry(Name)   /* deselect the entry, */
                        Leave                               /* and abort. */
                    End
            End
    End
    Address COMMAND "Delete >NIL:" "T:"VTMP 'QUIET FORCE'   /* Delete the temporary file. */
    Call wasDOpus4DeselectEntry(Name)                       /* Deselect the entry. */
End

/* Restore the continue and abort buttons. */
Status 26 set OldOkay
Status 27 set OldCancel

Busy Off    /* Turn off the busy pointer. */
Exit        /* Terminate. */

/*************************************************************************/
/*    Copyright (C) 2015 Wizardry and Steamworks - License: GNU GPLv3    */
/*************************************************************************/
wasDOpus4DeselectEntry: procedure   /* Deselect a selected entry. */
    Parse ARG Item

    GetAll '*'
    AllItems = RESULT
    AllItems = Translate(Translate(AllItems, '/', ' '), ' ', '*')

    n = Words(AllItems)

    Do i = 1 To n
        If Item = Translate(Word(AllItems, i), ' ', '/') Then
            Do
                SelectEntry i - 1 ||' '|| 0 ||' '|| 1
                Return
            End
    End

Return

/*************************************************************************/
/*    Copyright (C) 2015 Wizardry and Steamworks - License: GNU GPLv3    */
/*************************************************************************/
wasStringContainsDigits: procedure /* True if string contains digits. */
    Parse ARG String
    Do i = 0 To 9
        If Pos(i, String) ~= 0 Then
            Return 1
    End
Return 0

/*************************************************************************/
/*    Copyright (C) 2015 Wizardry and Steamworks - License: GNU GPLv3    */
/*************************************************************************/
wasRandomHexString: procedure /* Generates a random hexadecimal string.  */
    Parse ARG Seed,Size
    If Size = 0 Then Return ''
    Random = Random(0, 15, Size + Seed)
Return D2X(Random)||wasRandomHexString(Random, Size - 1)
Addendum:
DOpus commands GetFiles and GetDirs can't be replaced by GetNextSelected. You have to parse or going a long way .

Last edited by BigFan; 13 July 2015 at 19:07. Reason: addendum
BigFan is offline  
Old 13 July 2015, 14:53   #52
BigFan
Registered User
 
BigFan's Avatar
 
Join Date: Feb 2014
Location: Germany
Posts: 261
Quote:
Originally Posted by daxb View Post
You are using outdated DOpus version. DOpus5 example:
For DOpus4 you may can use '0A'x as seperator if DOpus4 can handle it.
I stick to DO4 too, because DO5 (.9 somethin', released for free years ago) still has bugs, that drives me away from it.
The command params are sent as string. Hex values don't match and the result string does not have any markers then. All as one string with no blanks.
Code:
5.Ram Disk:> rx dops
ClipboardsENVTdopsupdate2.rexxuser-startup
Addendum:
Obviously, using hex values as string wouldn't help either

Last edited by BigFan; 13 July 2015 at 19:01. Reason: addedum
BigFan is offline  
Old 13 July 2015, 15:03   #53
BigFan
Registered User
 
BigFan's Avatar
 
Join Date: Feb 2014
Location: Germany
Posts: 261
Loopings

Loops

A flow control to repeate same code is called a "loop".
ARexx supplies the Do command with some more or less usefull options. But those
option make a big difference to its default behavior.

Standard loop is like "For ... Next, While ... wend" in other languages.

Code:
/**/
Do i=1 To 10
Say "looping nr "i
End
Every loop starts with a positive value greater than zero. This is important,
because zero indicates nothing to do and aborts the loop.
Floating point values are allowed.
To do a reverse count, start with high values and set a negative offset.

Code:
/**/
Do i=10 To 2 By -2
Say "looping nr "i
End
"i" can be any variable name. Of course those fixed values may be replaced by variables.

Code:
/**/
F=10.5;L=0.0;S=-1.5
Do i=F To L By S
Say "looping nr "i
End
"i" is compared to the value after "To". If it not matches, the loop continues.

The block is processed till "End". Now the "i" is increased by 1 (this is default)
Again "i" is compared to the value after "To". If it not matches, the loop continues.
And so on.

  • Do i=1 For 10 --> equal to first example, 10 turns maximum, i gets increased
by 1 each turn

  • Do i=1 to n for 10 --> similar, but the loop breaks at "n" if n is less than 10.
10 is max loop run now(even if n n is greater), i gets increased by 1 each turn

  • Do i=2 to 20 by 2 --> loop runs 10 times, i is increased by the value given (2)
each turn

  • Do i=1 to 10 while (a<=2.5) --> the loop checks each run for "a" and continues
if "while (expression)" is true, i gets increased by 1 each turn

  • Do While ~Eof() --> runs as long as Eof() is not true, no counter used

  • Do i=1 --> this runs forever, i gets increased by 1 each turn

  • Do i=4 by 4 --> this runs forever, i gets increased by 4 each turn

  • Do Forever --> same as above but no counter is used

  • Do i=2 to 10 by 2 until (expression)
  • Do i=1 until (expression)
  • Do until (expression)

"Until" is very "similar" to while. It checks if expression is true and aborts the
loop if not. BUT!!! "Until" checks before the counter is increased, "while" checks
after the counter is increased. This is very important.

Code:
 /**/

f = 0
Do i=1 while (f~=7)
f = f + 1
end
say "f="f "i="i

f=0
Do i=1 until (f=7)
f = f + 1
end
say "f="f "i="i
See, "i" gets increased to 8 using "while", but only to 7 using "until".

Have a look at our example with file reading. Condition was (While Not EndofFile)
We read line by line. The Do block continues as long as the "while" expression
is true, that is Eof() is not true.
Rebuilding the code with "until" requires to check for Eof() being true.

Code:
     If Open(infile,filename) Then Do
       Do i=2 While ~Eof(infile)
         line.i = Readln(infile)
       End
       i = i - 2   
     End
     Else Do
       Echo "Cannot find "filename"!"
       Exit 20
     End
Correct the expression and replace while with until.

Code:
     If Open(infile,filename) Then Do
       Do i=2 Until Eof(infile)
         line.i = Readln(infile)
       End
       i = i - 1
     End
     Else Do
       Echo "Cannot find "filename"!"
       Exit 20
     End
"Until" and "while" are mutually exclusive.

The combination of Eof() and Readln() still results the same. We have to delete
any unwanted additional lines or the output gets filled with NULLstrings .

Example

Code:
/* line counter */

options results

if open(fil0,'s:user-startup') then do w=1 while ~eof(fil0)
  line=readln(fil0)
end

seek(fil0,0,b) /* rewinds file index to offset 0 from begin = start of file */

do u=1 until eof(fil0)
  line=readln(fil0)
end

close(fil0)

say "using while lines= "w
say "using until lines= "u
My user-startup has 25 lines, verify the output:

5.Ram Disk:> rx line
using while lines= 27
using until lines= 26
5.Ram Disk:>

What's the cause?
Readln() reads all characters till $0a is found .
(hex '0a' = decimal '10', signals "Line feed")
It stores the string without the trailing '0a'. The last position in
a text file is a '0a' normally, so we do not read beyond file bounderies at
the last line. Eof() is not true.
A new attempt is made to read a line. Now we exceed file length.
Because Eof() is true now, the loop breaks. That is one line beyond boundary.
"until" fails before increasing the counter, "while" after increase.
We get one or two additional lines reported.
Readln() NEVER fails. If nothing is read, a NULL string is generated. We can't
use this as an indicator, because a text may contain empty lines.
BigFan is offline  
Old 13 July 2015, 18:54   #54
BigFan
Registered User
 
BigFan's Avatar
 
Join Date: Feb 2014
Location: Germany
Posts: 261
Adding libraries

Libraries

ARexx is a bit limited regarding file system operations or user interaction, and
it lacks a graphical user interface.

One way to add more functions to ARexx is to use additional libraries. Of course,
those libraries have to support ARexx. ARexx comes with 2 libraries, the
rexxsyslib.library and rexxsupport.library. There are third party libraries
available on aminet.

We start with the rexxsupport.library as this offers to us a second way to have
more functionality. It enables us to open our own ports and listen silently while
staying in the background.

To open a library use Addlib(). We have to make sure that the requested library is
successfully opened. In other case, script will fail . Check loaded libs with Show()

Code:
/**/

Options Results

If Show(l,'rexxsupport.library')=0 Then Do
  If Addlib('rexxsupport.library',0,-30,0)=1 Then Exit
End

... Code to run ...
Show() returns 1 if named lib has been found. The code jumps to End where our
code continues.
If library is not loaded, Addlib() tries to open it.

Syntax Addlib('libname', priority, entry point, version)

Priority is a value ranging from -100 to 100. Do not use high priorities, it slows
down all other running tasks. Zero should be fine in most cases.

Entry point is an address offset to query the library if the requested command
is actually available. -30 is often correct, study the library docs if that value
differs from default.

Version is a major version number that represents the minimum version of the lib
to be open. If version is set to 39, any version lower than that causes an error.
Use it when necessary or set zero to ignore version.

An open library can be closed. Use Remlib(libname) for this purpose.

Addlib() opens ports also. Simply call Addlib(name,priority). Entry point and version
are not supported in this mode.

Rexxsupport.library grants access to the following functions:

Allocmem(bytes[,attrib]) - Allocates a block of memory in bytes, default attribute is 'Public'
Closeport(portname) - Closes a message port
Freemem(address, bytes) - Releases a block of Allocated memory
Getarg(message, slot) - Extracts command, function name or string from message pkt
Openport(portname) - Creates a public message port
Reply(message, rc) - Returns a msg pkt to the sender with a value
Showdir(directory[,'A'll|'F'ile|'D'ir][,pad]) - Returns contents of directory as strings of names
Showlist(option,name,pad) - works the same way as Show() but has more options
Statef(filename) - Returns a string containing file information like size and protection bits
Waitpkt(portname) - Waits for a msg pkt from port

Options for Showlist()
'A'ssigns and assigned devices
'D'evice drivers
'H'andlers
'I'nterrupts
'L'ibraries
'M'emory list items
'P'orts
'R'esources
'S'emaphores
'T'asks (ready)
'V'olume names
'W'aiting tasks

Last edited by BigFan; 14 July 2015 at 12:38. Reason: corrected library call and return code check
BigFan is offline  
Old 13 July 2015, 19:00   #55
BigFan
Registered User
 
BigFan's Avatar
 
Join Date: Feb 2014
Location: Germany
Posts: 261
Open your Rexx Port

If you get this far reading here, then you should be able to figure out how to use
Showdir() or Showlist(). No big deal. I save time not to explain basic stuff.

The most intrigueing functions are to control your own rexx port. This is a complex
thing and is best explained with an example code.

Example

Code:
/* */

/* */
Options Results

If ADDLIB('rexxsupport.library',0,-30,0)=0 Then Do

  port=catch
  Openport(port)

  do forever
    waitpkt(port)
    msg=getpkt(port)
    arg=upper(getarg(msg))
    if arg=quit then break
    if arg=version then do
      reply(msg,0)
      Address command 'requestchoice Title "Version" Body "Catcher V1.0" OK'
    end
  end
end
Else Do
 Say "rexxsupport.library not found"
 Exit
End
Reply(msg,1)
Closeport(catch)
Exit
We start with a check if the library can be opened successfully.
If not, find exit.

I named the port catch, no quotation marks, we deal with upper case !!
After successfully open a port, the code enters an infinite loop.
Waitpkt() is systemfriendly and sets our rexx program to sleep, waiting for the first
loves first kiss to awaken the beautiful princess (resting in the highest tower
guarded by a dragon). eerrrmm. oops

If our program receives a message we fetch it with Getpkt()
We pick the content (if any) with Getarg() and make it upper case (or we have to
find a way to deal with case-sensetivity).
We check for two commands, QUIT and VERSION. QUIT forces the code to exit. Version
prints a silly version string in a requester.

How to control?

First, run the example from workbench (add an icon, set rx as command)
or use shell command "run" to detach from console.
Now the program stays in background with the default RX output window.

Open a shell, if not already.

Check if our catcher is waiting : rx "say show(p)"
CATCH should be listed as available port

rx "address catch version"
The silly version string shows up in rexx window

rx "address catch quit"
Commands the program to terminate.

All this can be done from inside any other rexx program.

If you want to communicate (CATCH should send results to your programm, f.e.) you
have to add the code for port creation to your newly written program.

The example above lacks error handling. Never forget that almost everything could fail.
Better be save than freezed

Bug fix:
first version checked for success on open library and exits instead of continue

Last edited by BigFan; 14 July 2015 at 13:22. Reason: title added, silly bug found
BigFan is offline  
Old 14 July 2015, 16:21   #56
BigFan
Registered User
 
BigFan's Avatar
 
Join Date: Feb 2014
Location: Germany
Posts: 261
Running from Workbench

Running REXX programs from Workbench.

The shell is a useful interface during development of rexx programs. Results show
up in current or additional consoles. You have all of the control over the rexx
programs running. But after testing your code thoroughly, you may like to see it
working in the background, but on every start from wb the rx output window pops
up. A rexx script doesn't need special protection bits. All you need is an icon.

  • Icon type must be "project".
  • Set "Default Tool" to "rx".
  • Add a new tooltype.
  • Enter "Console=NIL:"


The script starts silently from wb. It is still accessable and listens for any
message to come.
Rexx offers a few tools to share data with your program or to stop them.
See sektion "Signal on ..." and prepare your script for proper error handling.

Shell Commands:

HI - send a "Halt Interrupt" to all active rexx programs, forcing them to
exit immediately.

RXSET - send data to rexx clipboard. RXC test="testclip" copies "testclip" to
clipboard entry named 'test'. From within a rexx program access it via
"dat = Getclip(test)". RXSET without argument lists the clipboard entries in
shell window.

RXC
- closes the REXX resident process after the last rexx program exits.
Meanwhile, no new rexx programs can be started, except RexxMast is restarted.
Not recommended, but sometimes helpful, if a program calls other script in an
infinite loop, otherwise memory is getting low rapidly.

WaitforPort - 10 seconds delay. Result code shows an error if named port is not
listed. E.g.: "WaitforPort CATCH" after launching "rx catch" from shell or
"WaitforPort GOLDED.1" if the editor needs some time to get loaded
(slow harddrive, f.e.).
BigFan is offline  
Old 16 July 2015, 14:22   #57
BigFan
Registered User
 
BigFan's Avatar
 
Join Date: Feb 2014
Location: Germany
Posts: 261
File examination

With our newly gained knowledge we could examinate files.
A simple method is to seek end of file with Seek().

Of course we have to open a file before we set the file cursor to any new position
in the file. That is what Seek() does, it moves a file cursor or reports its
current position. Seek() can be used in 3 ways

Syntax
Seek(file, offset, origin) - offset moves the cursor n bytes from origin.
Origin is 'B'egin, 'C'urrent or 'E'nd

Seek(file, 0, E) jumps to end of file
Seek(file, 0, B) rewinds to start
Seek(file, 4, B) to read the 5th byte !!
Seek(file, 8, C) step another 8 bytes from now (pos 12 or 13th byte in this ex.)
Seek(file, 0, C) tells the current index

Notice that the offset starts with 0, not 1. The nth byte ist at pos n-1.

In our next example we use the rexxreqtools.library (available on aminet) to
have a file requester. DOS command 'requestfile' will do, if you don't want use
rexxtools.

Example
Code:
/**/
If Exists('libs:rexxreqtools.library') Then
  Addlib('rexxreqtools.library',0,-30,0)
Else Exit

  files=rtfilerequest()

  If Open(infile,files) Then Do
     size = Seek(infile,0,e)
     Say "File size =" size "bytes"
     Say "         or" Trunc(size/1024) "kbyte"
     Close(infile)
  End
Exit

Last edited by BigFan; 17 July 2015 at 14:14. Reason: Better file checking
BigFan is offline  
Old 16 July 2015, 14:23   #58
BigFan
Registered User
 
BigFan's Avatar
 
Join Date: Feb 2014
Location: Germany
Posts: 261
Seek() is very limited. Statef() from rexxsupport.library gives more information to us.

Syntax
Statef(file | dir) - returns a string containing file information
string looks like "type size block protection days minutes ticks comment"
where type is either dir or file.

Example
Code:
/**/
If Exists('libs:rexxsupport.library') Then
  Addlib('rexxsupport.library',0,-30,0)
Else Exit
If Exists('libs:rexxreqtools.library') Then
  Addlib('rexxreqtools.library',0,-30,0)
Else Exit 

  files=rtfilerequest()

  Parse Value Statef(files) With type size blocks .

       If type=file then do
         Say "File size =" size "bytes"
         Say "         or" trunc(size/1024) "kbyte"
         Say "Size in Blocks" blocks
       End
       Else Say "This is a directory."
Exit
rtfilerequest takes 6 optional parameters, but we do not care for now.
If user selects nothing, return value is empty.
Then we parse what Statef() returns.

Exercise:
Try to parse the date and translate it into a readable form.

Last edited by BigFan; 17 July 2015 at 14:12. Reason: formerly written code wasn't fail save
BigFan is offline  
Old 16 July 2015, 14:27   #59
BigFan
Registered User
 
BigFan's Avatar
 
Join Date: Feb 2014
Location: Germany
Posts: 261
Ascii to hex

In a previous example i told you about the problem using Readln in conjuction
with EoF(). The second function to read from a file is Readch(). This function
has less to no problems with file length. We simply tell Readch() the exact numbers
of characters to read. Readch() returns a string of same length or less if file
boundaries crossed. Stepping only one character gives opportunity to check its
value (0 means end of file), makes it easier to check for Eof() than using Readln().
Readch() does not stop on any special character (e.g. $0a , aka line feed).

Exercise:
Write a little hex file reader. I suggest to make use of the following functions:
C2x()
Copies()
Min()
Open()
Readch()
Statef()
Substr()
Translate()
Xrange()

Try first before continue.

Example:
Code:
/*
* $VER: HexReader 1.1 (15.07.15)
* written by BigFan
*/
If Exists('libs:rexxsupport.library') Then
  Addlib('rexxsupport.library',0,-30,0)
Else Exit
If Exists('libs:rexxreqtools.library') Then
  Addlib('rexxreqtools.library',0,-30,0)
Else Exit 

    files=rtfilerequest()
    Parse Value Statef(files) With type size .

If size='SIZE' Then Exit  /* no file size, no fun */

 /* preparing hex code table */

as2 = Xrange('00'x,'1f'x)
as3 = Copies(".",32)


  /* variable init */
chars = ""
size = Min(size, 65535)   /* string is restricted to 64kB */

Open(infile,files)

  /* read and concatenate */
Do i=1 To size
 chars = chars||Readch(infile)
End

  /* apply hex and ascii table  */

Do i = 1 To size By 16
  hex = C2x(Substr(chars,i,16))
  asc = Substr(chars,i,16)
 tasc = Translate(asc,as3,as2)
 comb = hex||" "||tasc
 Writeln(stdout,comb)
End
Close(infile)
Exit
First we ask for a file. If nothing is returned then quit.
Two strings are created, 1st with forbidden characters, 2nd with the replacer

We face a new problem here: String length must not exceed 65536 bytes !!
The code above does check for it but doesn't deal well with it.

After opening the file we concatenate all chars read into one string.

This new string is processed in chunks of 16 byte, because hex values have 2 digits
we need 32 + 1 + 16 = 49 chars to show it.

We read a substring of 16 bytes and convert it to hex values.
Same substring again for ascii.
Now, Translate() cleans the ascii part from unwanted chars. All values lower than
32 are substituted by a period.

Hex string + blank + ascii string are concatenated and written to stdout.

This code could be enhanced.
Code is sluggish, speed up.
Find a way to process files bigger then 64kB (multiple strings).
Add file information.
Better input handling.
Circumvent the 64kB restriction (using allocated memory).
Scrolling. (uh, no, that is difficult, you have to create a list view and do
your own drawing routines(add graphics.library, poke addresses))
Send data to a file reader or editor.
Build a MUI around it with MUIRexx.
Add editing features(not serious, ARexx is not good at this, better use another
language, ARexx is for interprocess communication).

Last edited by BigFan; 17 July 2015 at 14:09. Reason: formerly written code wasn't fail save
BigFan is offline  
Old 16 July 2015, 15:41   #60
BigFan
Registered User
 
BigFan's Avatar
 
Join Date: Feb 2014
Location: Germany
Posts: 261
Exercise:

Code is sluggish, speed up.

Solution
The loop that slows all down is reading all characters one by one.
This is a bad idea. Larger files will take a long time to load. As said,
Readch() reads a number of characters if we supply size as argument.
Delete the loop and replace with chars=readch(infile,size).
Much better.
BigFan is offline  
 


Currently Active Users Viewing This Thread: 1 (0 members and 1 guests)
 
Thread Tools

Similar Threads
Thread Thread Starter Forum Replies Last Post
AmigaDOS scripting resources Photon Coders. System 26 19 March 2018 14:51
Very Basic Scripting. Confused. marduk_kurios Coders. System 5 06 February 2014 11:13
UAE Scripting Layer FrodeSolheim support.FS-UAE 15 26 January 2014 15:56
C= 64 BASIC as a Scripting Language Charlie Retrogaming General Discussion 2 17 November 2008 14:23

Posting Rules
You may not post new threads
You may not post replies
You may not post attachments
You may not edit your posts

BB code is On
Smilies are On
[IMG] code is On
HTML code is Off

Forum Jump


All times are GMT +2. The time now is 23:23.

Top

Powered by vBulletin® Version 3.8.11
Copyright ©2000 - 2024, vBulletin Solutions Inc.
Page generated in 0.15948 seconds with 14 queries