Rattlesnake FTP Command Shell Interpreter

This package contains the Rattlesnake FTP command interpreter, which reads and executes FTP command files (a.k.a. FTP command scripts).


Lexical Constraints

FTP command scripts are text files containing FTP commands (statements). Commands are delimited by newlines. Commands in turn are composed of tokens (or words), which are separated by whitespace characters (spaces and tabs).

Comments begin with a hash mark (#) and end at the end of line (the (next newline). Any word beginning with a hash mark indicates the start of a comment. Examples:

    get '#not#a#comment'        #comment#word
    put not#a#comment.txt       # comment

String literals are enclosed within double quotes (e.g., "foo") or single quotes (e.g., 'bar'). Strings containing embedded quote characters must specify the quotes using character escape sequences, which are $" for a double quote and $' for a single quote. So the string specified as "<He said, $"Hello!$">" represents the string <He said, "Hello!">. An embedded '$' is specified as $$, e.g., "$$50.00".

Token words can always be quoted so that they are not misinterpreted:

    a+long/file*name.txt            #one token
    'a+long/file*name.txt'          #ditto
    ($path/$file)                   #expression, five tokens
    '($path/$file)'                 #one token

Token words that begin with a '(', '<', '>', '&', or § are treated as expressions, and are split into multiple tokens.

    set x1 = $path/$file.$ext           #single word '$path/$file.$ext'
    set x2 = "$path/$file.$ext"         #same as x1

    set y1 = ($path/$file.$ext)         #expression
    set y2 = ( $path / $file . $ext )   #same as y1
    echo >>$path/$file.$ext $count      #

Certain contexts expect a pathname, e.g.:

    get dir/foo.txt my/bar.txt

Pathnames can always be quoted to insure that they are not treated as expressions:

    if $v = "dir/foo.txt" then
        set x = $path/$Ff     # expression
    else
        set x = '$path/$Ff'   # string

Conversely, an expression can be provided in any context where a pathname is expected by enclosing it within parentheses:

    get ('dir/' . $fname . $ext)

Vars (variables) can always be quoted to enforce their "stringiness":

    "$F"
    "${path}/$Fn"

Parentheses are required in most contexts that expect a Term expression, which can lead to some surprises. For example:

    if (:read foo.txt) ...
The foo.txt looks like a well-formed filename, but since it occurs within enclosing parentheses, it is parsed as three separate tokens (foo . txt), forming the operands and operators of a concatenation expression. The result is the string 'footext', which is probably not what was intended. This kind of situation is solved by using quotes:
    if (:read 'foo.txt') ...
Now the operand of the :read operator is the string 'foo.txt'.

Since Term expressions can be single words, strings, or variable names, parentheses are not needed. So the following examples are all legal:

    get foo.$ext
    if $flag
        put $fname
Anything more complicated that a single token, though, generally requires the presence of surrounding parentheses:
    get (foo . $ext)
    if (not $flag)
        echo done

A good rule of thumb is:

When in doubt, add extra quotes or parentheses.

Pathnames

Pathnames on MS-DOS and MS/Windows can look like:

    "c:\usr\bob\bin\foo.txt"
    'bin\$Ff'
whereas the same filenames look different in Unix:
    "/usr/bob/bin/foo.txt"
    'bin/$Ff'

The norm() function (described below) normalizes either form of the pathname into its proper native form.


Variables (Vars)

A var (or variable) is a named value that can be set by the user or the FTP interpreter. Built-in vars are predefined by the interpreter, and others vars can be defined by the user. Vars can also be used to access the values of external environment variables. All vars are internally stored as character strings, but can be interpreted within expressions as either strings or as numbers (integers) depending on the context.

Var names are words composed of alphabetic letters, digits, and/or underscores. Some examples:

    i
    name
    fName
    filename2
    file_name_1

Vars are defined with the var command:

    var file
    var count = 0
    var root = /u/home/bob
    var path = '$root/foo.txt'
    var n = $count + 1

Vars that are not explicitly initialized to a value are set to an empty string ('').

Vars are set using the set command:

    set i = 1
    set name = 'foo.txt'
    set count = ($count+1)
    set file = ($root . / . $path . / $name . '.' $ext)
    set path = "$root/$path/$name.$ext"
    set path = (sub($file, 0, 12))

Vars are replaced with their values by prefixing their name with a $ character:

    get $name $destName.txt

Var names can also be placed within braces, which may be necessary in situations where the name is followed by alphanumeric characters:

    get ${name}_orig.txt ${dest}2.txt

Var replacement occurs in all contexts, even within quoted strings:

    get $name.arc "${name}.tar"
    !tar -xvf '${name}.tar'

Var replacement is disabled by escaping the $ operator by preceding it with another $ operator:

    get foo.$$tmp       #Get the file named "foo.$tmp"

Var replacement is also disabled for a token word or string by the $- escape sequence, which turns off var replacement for the rest of the word or string:

    get $-$foo.$tmp             #Get '$foo.$tmp', var $tmp is not replaced
    put $dir/$-file$tmp.txt     #Var $dir is replaced but $tmp is not

Var names are not case-sensitive, except for the the first character. That is, case is only important for the initial character of the var name, and is ignored for the rest of the name. So the names $file, $filE, and $fILE all designate the same var, whereas $err and $Err designate two different vars.

Built-in (predefined) var names always start with a capital (upper-case) letter or underscore, such as $F, $Error, and $_t. User-defined var names always start with a lower-case letter, such as $f and $count.

The special syntax ${$foo} refers to the value of the environment variable named foo. If there is no such environment variable, the resulting value is an empty string (''). Whether or not the name is case-sensitive depends on the underlying operating system. Note that the braces are required, since $$foo causes the $ to be escaped, resulting in the value '$foo'. Note also that environment variable values are always assumed to be string (non-numeric) values.

For example, on MS-DOS and MS/Windows systems, the PATH environment variable is set to the list of directories to search for executable programs. This variable can be examined from the FTP shell:

    if (${$path} = '')
    {
        echo 'PATH is not set.'
        exit
    }
    echo 'PATH:' ${$path}

Syntax

Commands

    Stmt...                                      # Command list
        : (empty)
        : Stmt Stmt...

    Stmt
        : BlockStmt
        : AppendStmt
        : AsciiStmt
        : BinaryStmt
        : BreakStmt
        : CallStmt
        : CdupStmt
        : ChdirStmt
        : CloseStmt
        : ContinueStmt
        : DeleteStmt
        : DirStmt
        : EchoStmt
        : ExitStmt
        : ForeachStmt
        : FuncDefStmt
        : GetStmt
        : GlobStmt
        : GotoStmt
        : IfStmt
        : LabelStmt
        : LocalChdirStmt
        : MdeleteStmt
        : MdirStmt
        : MgetStmt
        : MkdirStmt
        : MlsStmt
        : MputStmt
        : NopStmt
        : OpenStmt
        : PrintStmt
        : PutStmt
        : PwdStmt
        : ReadStmt
        : RenameStmt
        : RepeatStmt
        : ReturnStmt
        : RmdirStmt
        : SetStmt
        : ShellStmt
        : ShiftStmt
        : SleepStmt
        : TimeoutStmt
        : UserStmt
        : VarStmt
        : WhileStmt
        : SpecialStmt

    AppendStmt                                   # Append (put) a file
        : app[end] [Session] Term [Term] nl

    AsciiStmt                                    # ASCII text mode
        : asc[ii] [Session] nl

    BinaryStmt                                   # Binary mode
        : bin[ary] [Session] nl

    BlockStmt                                    # Command group
        : '{' nl Stmt... '}' nl
        : '{' '}' nl

    BreakStmt                                    # Terminate looping
        : break [name] nl

    CallStmt                                     # Subroutine call
        : FuncCallExpr nl

    CdupStmt                                     # Change directory up
        : cdup [Session] nl

    ChdirStmt                                    # Change remote directory
        : cd|chdir [Session] Term nl

    CloseStmt                                    # Close connection
        : close [Session] nl
        : disc[onnect] [Session] nl

    ContinueStmt                                 # Next loop iteration
        : continue [name] nl

    DeleteStmt                                   # Delete file
        : del[ete] [Session] Term... nl

    DirStmt                                      # Directory listing
        : dir [Session] [max Term] [Term...] nl
        : ls  [Session] [max Term] [Term...] nl

    EchoStmt                                     # Print to stdout
        : echo ['-n'] ['>'|'>>' Term]
            Term [[','] Term]... nl

    ExitStmt                                     # Terminate
        : exit [Term] nl
        : quit [Term] nl
        : bye  [Term] nl

    ForeachStmt                                  # File get loop
        : foreach [Session] Term... [in Term] [max Term] nl
            Stmt

    FuncDefStmt                                  # Subroutine definition
        : func '@'name '(' [name [',' name]...] ')' nl
            Stmt

    GetStmt                                      # Get a file
        : get [Session] Term [Term] nl

    GotoStmt                                     # Goto label
        : goto name nl

    IfStmt                                       # If-then-else
        : if Term nl Stmt
            [else nl Stmt]
        : if Term nl Stmt
            [else IfStmt]

    LabelStmt                                    # Command label
        : name: nl

    LocalChdirStmt                               # Change local directory
        : lcd|lchdir [Session] Term nl

    MkdirStmt                                    # Create directory
        : mkdir [Session] Term nl

    NopStmt                                      # No-op command
        : nop nl

    OpenStmt                                     # Open connection
        : open [Session] Term [Term] nl

    PrintStmt                                    # Print to a file
        : print ['>'|'>>' Term]
            Term [[','] Term]... nl

    PutStmt                                      # Put a file
        : put [Session] Term [Term] nl

    PwdStmt                                      # Get current dir
        : pwd [Session] nl

    ReadStmt                                     # Read into a var
        : read name ['<' Term] nl

    RenameStmt                                   # Rename file/dir
        : ren[ame] [Session] Term Term nl

    RepeatStmt                                   # Repeat loop
        : repeat nl Stmt

    ReturnStmt                                   # Subroutine return
        : return [Term] nl

    RmdirStmt                                    # Remove directory
        : rmdir [Session] Term... nl

    SetStmt                                      # Variable set
        : set name = Term nl
        : set name '[' Expr ']' = Term nl

    ShellStmt                                    # Shell command
        : '!' Term... nl

    ShiftStmt                                    # Shift command parms
        : shift [Term] nl

    SleepStmt                                    # Sleep
        : sleep Term [Term] nl

    UserStmt                                     # User ID and password
        : user [Session] Term [Term] nl

    VarStmt                                      # Variable definition
        : var name ['=' Term ] nl

    WhileStmt                                    # While loop
        : while Term nl Stmt

    SpecialStmt                                  # Debugging info
        : '%stack' nl
        : '%sessions' nl
        : '%vars' nl
        : '%verbose' Term nl

    Session                                      # Session number
        : '&' Term
        : '§' Term

    nl                                           # End of line
        : newline

Commands

FTP commands are listed in alphabetical order.

Block command
BlockStmt
    : '{' nl [Stmt...] '}' nl
    : '{' '}' nl

Command block.
Executes zero or more commands as a single group. The commands comprising the command block are enclosed within braces ({ }). A command block is syntactically equivalent to a single command, so it can be used in any context where a single command is expected but more than one command is desired.

Command blocks may contain other (nested) command blocks. Each command block opens another level of scope, so any vars declared within it are accessible only to the commands or nested blocks within that block.

Example:

    if ($flag)
    {
        var fname = $Fn

        get $fname.txt
        put $fname.tmp
        ren $fname.tmp $fname.txt

        foreach $fname*.html
        {
            get $Ff
            if ($Error != 0)
            {
                del $Ff
                !mv -f $Ff $Ff.done
            }
        }
    }

append command
AppendStmt
    : app[end] [Session] Term [Term] nl

Appends a local file to a file on the remote FTP system.
The optional Session specifies which FTP session to send the file to. If this is not specified, session &1 is assumed. The Term-1 expression is evaluated and the resulting value specifies the name of the local file to send. The optional Term-2 expression specifies the name of the file to append to on the remote system. If this is not specified, the remote name is the same as the local name (Term-1).

This command is similar in operation to the put command, except that the local file contents are appended to the end of the remote file instead of replacing its contents entirely. File appends are non-destructive, meaning that the file is only read from the local system, and is not deleted after the append operation.

Example:

    append foo.txt
    append $fname.$ext
    append (repl('foo', 'bar', $F)) '$F.new'

See also: get, put.

break command
BreakStmt
    : break [name] nl

Terminate a loop.
This command stops all iterations of the currently executing loop command (i.e., a foreach, repeat, or while command). Execution then resumes at the next command following the loop.

Example:

    foreach *.txt
    {
        get $Ff
        if ($Error != 0)
            break
    }

See also: continue, foreach, repeat, while.

cd command
ChdirStmt
    : cd|chdir [Session] [Term] nl

Change the directory on the remote system.
This command changes the current directory on the remote system to the name specified by Term. The optional Session specifies which FTP session the command affects. If this is not specified, session &1 is assumed.

Example:

    cd src
    chdir &2 /users/$user/docs

See also: cdup, lcd, pwd.

cdup command
CdupStmt
    : cdup [Session] nl

Change the directory on the remote system one level up, to the parent directory of the current directory.
The optional Session specifies which FTP session the command affects. If this is not specified, session &1 is assumed.

Example:

    cdup
    chdir &2

See also: cd, lcd, pwd.

close command
CloseStmt
    : close [Session] [Term] nl
    : dis[connect] [Session] [Term] nl

Close the remote connection.
Logs off and disconnects the remote FTP session. The optional Session specifies which FTP session the command affects. If this is not specified, session &1 is assumed.

Example:

    close
    disconnect &2

See also: open, user.

continue command
ContinueStmt
    : continue [name] nl

Force the next iteration of a loop.
This command skips the rest of the commands in the body of the currently executing loop command (i.e., a foreach, repeat, or while command), and starts the next iteration of the loop.

Example:

    foreach &1 *.txt
    {
        get &1 $Ff
        if ($Error > 0)
            continue
        put &2 $Ff
    }

See also: break, foreach, repeat. while.

delete command
DeleteStmt
    : del[ete] [Session] Term... nl

Deletes (removes) one or more files on the remote FTP system.
The optional Session specifies which FTP session the command affects. If this is not specified, session &1 is assumed. Each Term expression is evaluated and the resulting value specifies the name of a remote file to delete.

Example:

    del foo.txt bar.txt
    del $fname.$ext
    delete (repl('foo', 'bar', $F))

dir command
DirStmt
    : dir|ls ['&' Term] [max Term] [Term...] nl

List the directory contents of the remote FTP system.
The optional Session specifies which FTP session the command affects. If this is not specified, session &1 is assumed. Each Term expression is evaluated and the resulting value specifies a filename pattern to list on the remote system. The optional max clause specifies the maximum number of files to list.

Example:

    dir *.txt
    ls &2 max 100

echo command
EchoStmt
    : echo ['-n'] ['>'|'>>' Term]
        Term [[','] Term]... nl

+INCOMPLETE

See also: print.

exit command
ExitStmt
    : exit|bye|quit [Term] nl

+INCOMPLETE

foreach command
ForeachStmt
    : foreach [Session] Term... [in Term-2] [max Term-3] nl
        Stmt

Loops over a list of matching filenames.
One or more Term expressions specify the filename patterns to search for on the remote system. The optional Session specifies which FTP session the command affects. If this is not specified, session &1 is assumed. The optional in Term-2 operand specifies the directory to search. If this is not specified, the current remote directory is assumed. The optional max Term-3 operand specifies the maximum number of filenames to find. If this is zero or not specified, no maximum is assumed and all matching filenames on the remote system are found.

Due to the way the FTP protocol is implemented, the in expression is not limited to being a subdirectory name, but can be any valid command option accepted by the remote dir or ls command. For example, most Unix systems can be passed an argument of '-tr', which causes the list of files to be sorted in reverse order according to their modification date.

As a special case, a Session expression that evaluates to zero (&0) indicates that the local system is to be searched for matching filenames.

Once all of the matching filenames have been found, the body of the loop (Stmt) is executed, once for each filename. Upon entering the loop body, the predefined vars $F, $Fd, etc. are set to the components of the next matching filename.

The break and continue commands can be used to modify the looping. A continue command skips the rest of the commands in the body of the loop and begins the next iteration of the loop. A break command stops the looping completely and causes execution to resume at the next command following the loop. A goto command can be used to transfer execution out of the loop body entirely.

Example:

    # Find all files on the remote system
    foreach *
        echo $F

    # Retrieve all text files from the 'docs' directory
    foreach &2 *.txt *.html in docs max 100
    {
        get &2 docs/$Ff $Ff
        del &2 docs/$Ff
    }

    # Delete all temp files in the current local directory
    foreach &0 *.tmp
        !del "$Ff"

See also: break, cd, continue, lcd, repeat, while.

get command
GetStmt
    : get [Session] Term-1 [Term-2] nl

Retrieves a file from the remote FTP system.
The optional Session specifies which FTP session the command affects. If this is not specified, session &1 is assumed. The Term-1 expression is evaluated and the resulting value specifies the name of the remote file to retrieve. The optional Term-2 expression specifies the name to give the file on the local system. If this is not specified, the local name is the same as the remote name (Term-1).

File retrievals are non-destructive, meaning that the file is only read from the remote system, and is not deleted after the get operation.

Example:

    get foo.txt
    get &2 $fname.$ext
    get (repl('foo', 'bar', $F)) '$F.new'

See also: append, cd, lcd, put.

goto command
GotoStmt
    : goto name nl

Transfer control to a label.
This command causes the specified command label to be the next command that will be executed. If the label is outside the block containing the goto command (e.g., if the goto command is within a nested block or loop command), the execution of the current block is terminated. Any vars defined within the block go out of scope and no longer exist.

Example:

    foreach *.txt
    {
        get $Ff
        if ($Error != 0)
            goto Error
        put $Ff
    }
    Error:
    echo Get failed for: $Ff, error: $Error

See also: break, continue.

if command
IfStmt
    : if Term nl Stmt-1 [else nl Stmt-2]
    : if Term nl Stmt-1 [else IfStmt-2]

Conditional execution command.
Evaluates the Term expression, and if the resulting value is true, Stmt-1 (the then command) is executed, otherwise the command following the else is executed. The else clause is optional. If the then clause comprises more than one command, it must be enclosed within block braces ({ }).

Example:

    # Simple if-then command
    if $stop
    {
        echo Terminating
        exit
    }

    # Chained if-then-else commands
    if (:read 'foo.txt')
        put foo.txt
    else if (:read 'bar.txt')
        put bar.txt
    else
        echo File not found

See also: while.

Command label
LabelStmt
    : name: nl

Command label.
The name specifies a name for the command. This is useful for naming a target command for the goto, break, and continue commands. A label name is like a variable name but is suffixed by a colon (:). A label can also be a keyword followed by a colon (e.g., quit:), but this practice is discouraged.

A label is a command all by itself, and does not need to be followed by another command. It only serves as a named location, and does not perform any action (similar to the the nop command). One or more labels may precede a non-label command, each of which provides a different name for that command.

Labels are not case sensitive, so that label:, Label:, and LABEL: all refer to the same command label name. (It is recommended that the same capitalization style be used for all labels within a command script for readability.) Labels must be unique within the block in which they appear. Label names within a nested block may hide other labels with the same name defined in outer blocks.

Example:

    Loop:
    foreach *.txt
    {
        get $Ff
        if (not $Error = 0)
            goto Error
        put $Ff
        continue Loop
    Error:
        echo Failure for: $Ff, file skipped
    }

See also: break, continue, goto, nop.

lcd command
LocalChdirStmt
    : lcd|lchdir [Session] [Term] nl

Change the directory on the local system.
This command changes the current directory on the local system to the name specified by Term. The optional Session specifies which FTP session the command affects. This means that each session has its own notion of the local directory separate from all other sessions, which affects where local files are located by the get, put, and foreach commands. If a session is not specified, session &1 is assumed. Note that unlike the cd command, session &0 may be specified (even though it has no associated remote directory).

Example:

    lcd src
    lchdir &2 /users/$user/docs

See also: cd, foreach, get, put.

nop command
NopStmt
    : nop nl

No-op command.
Does not do anything. This command is useful in contexts expecting a command but no action needs to be performed.

Example:

    if (len($fname) = 0)
        nop
    else if ($fname ~ *.tmp)
        nop
    else
        get $fname

See also: block command.

print command
PrintStmt
    : print ['>'|'>>' Term] Term [[','] Term]... nl

+INCOMPLETE

See also: echo.

put command
PutStmt
    : put [Session] Term [Term] nl

Stores a local file to the remote FTP system.
The optional Session specifies which FTP session the command affects. If this is not specified, session &1 is assumed. The Term-1 expression is evaluated and the resulting value specifies the name of the local file to send. The optional Term-2 expression specifies the name to give the file on the remote system. If this is not specified, the remote name is the same as the local name (Term-1).

File sends are non-destructive, meaning that the file is only read from the local system, and is not deleted after the put operation.

Example:

    put foo.txt
    put &2 $fname.$ext
    put (repl('foo', 'bar', $F)) '$F.new'

See also: append, cd, lcd, get.

repeat command
RepeatStmt
    : repeat nl Stmt

Looping command.
Loops repeatedly, executing the Stmt command comprising the body of the loop. Since this is effectively an infinite loop, the break command must be used to terminate the loop.

The break and continue commands can be used to modify the looping. A continue command skips the rest of the commands in the body of the loop and begins the next iteration of the loop. A break command stops the looping completely and causes execution to resume at the next command following the loop. A goto command can be used to transfer execution out of the loop body entirely.

Example:

    # Wait until file 'foo.txt' is created
    repeat
    {
        if (:exists 'foo.txt')
            break
        sleep 1000
    }

    # Retrieve a log file periodically
    repeat
    {
        get logfile.txt
        if ($Error != 0)
            break
        sleep (20*60*1000)
    }

See also: break, continue, foreach, while.

rmdir command
RmdirStmt
    : rmdir [Session] Term... nl

Deletes (removes) one or more directories on the remote FTP system.
The optional Session specifies which FTP session the command affects. If this is not specified, session &1 is assumed. Each Term expression is evaluated and the resulting value specifies the name of a remote directory to delete.

Example:

    rmdir foo bar
    rmdir $dirname
    rmdir (repl('foo', 'bar', $Fd))

set command
SetStmt
    : set name = Term nl
    : set name '[' Expr ']' = Term nl

Sets the value of a var (variable).
Evaluates the Term expression and changes the value of the var named name to be the resulting value.

The name refers to the var within the most local block scope. If a var of that name is not defined within the local block, the next outer block is searched, and then the next outermost enclosing block, and so on until a matching var name is found. If name does not designate a var defined in any accessible enclosing scope, the interpreter will issue an error when the command script is compiled. Predefined (built-in) vars are defined within the outermost block.

The Term expression can refer to any var accessible within the local scope, including the var that is being modified by the set command. All expressions result in string values, even if they involve arithmetic operations.

Array vars may be set by following the var name with a bracketed subscript expression. Array indices are not limited to numeric values, and may be any string value, including empty strings (''). In this respect, arrays are more properly called hash tables. Thus a[1], a[xyz], and a['x${user}y'] are all valid elements of array a.

Note that since array indices are strings and not numbers, the exact value of the subscript expression is used to locate an element to modify in the specified hash table. This means that a[1] and a[01] designate two different elements, because 1 and 01 are two different strings (words). To limit array indexing to only numeric values, prefix the subscript expressions with a '+' unary operator. This forces both a[+1] and a[+01] to refer to the same element, which is a['1'].

Note also that an unsubscripted var name designates a var that is separate from the array of the same name, e.g., abc is a var separate and distinct from element abc[$i] (for any value of $i).

Note also that only one-dimensional arrays are supported. So a[$i] designates a valid array element, but a[$i][$j] is a syntax error. Multi-dimensional arrays can be simulated, however, by concatenating one or more array indices together to form a single unique array index. For example, a[$i.';'.$j] joins index $i and $j together to produce a single index into array a.

Example:

    set fname = foo.txt
    set newfile = '$oldfile.new'
    set count = ($count+1)
    set out = ($path . '/' . repl('.in' '.out' $infile))
    set a[1] = /bin/file1.txt
    set key['foo'] = 'bar'
    set fname[$idx[$i+1]] = ('/bin/' . ($i+1))
    set table['$i&$j'] = (100*$i + $j)
    set path = ${$path}

See also: var, subscript operator.

+INCOMPLETE

var command
VarStmt
    : var name ['=' Term ] nl

Defines a var (variable).
Adds a new var named name to the local scope. If Term is specified, the expression is evaluated and the var is initialized with the resulting value. Otherwise, the var is initialized to an empty string (''), which is logically equal to false and numerically equal to zero.

Var definitions within a block can hide other var definitions in outer scopes (which will cause a warning to be issued by the interpreter when the command script is compiled). Predefined (built-in) vars cannot be redefined or hidden. A given var name can be defined only once within a block scope.

Once control passes out of a block, all vars defined within that block go out of scope and no longer exist.

Example:

    var fname
    var count = 1
    var newfile = '$oldfile.new'
    var out = ($path . '/' . repl('.in' '.out' $infile))
    var cwd = ${$cd}

See also: set.

while command
WhileStmt
    : while Term nl Stmt

Conditional loop command.
Loops repeatedly, executing the Stmt command comprising the body of the loop. Before each iteration, the conditional Term expression is evaluated, and if the resulting value is true, the body command is executed. The loop iterations continue until the conditional expression is false. A conditional expression of true can be specified, effectively producing an infinite loop (equivalent to a repeat command). If the loop body comprises more than one command, it must be enclosed within block braces ({ }).

The break and continue commands can be used to modify the looping. A continue command skips the rest of the commands in the body of the loop and begins the next iteration of the loop. A break command stops the looping completely and causes execution to resume at the next command following the loop. A goto command can be used to transfer execution out of the loop body entirely.

Example:

    while $keep_going
        call @getNextFile()

    while true
    {
        get $fname
        if (not :exists $fname)
            break
    }

    while ($count <= $max)
    {
        get $fname.$count.txt
        set count = ($count+1)
    }

See also: break, continue, foreach, repeat.

Dangling Else

One problem encountered with this syntax is the classic "dangling else" problem. Consider the following commands:

    if $e1
        if $e2
            stmt1
    else
        stmt2
As shown, the else command is matched up with the second if command, so the commands are actually interpreted as:
    if $e1
    {
        if $e2
            stmt1
        else
            stmt2
    }
Appropriately placed braces (command block) solve this problem:
    if $e1
    {
        if $e2
            stmt1
    }
    else
        stmt2

Expressions

    Term
        : true
        : false
        : Var
        : number
        : string
        : word
        : '(' Expr ')'

    ExprList
        : Expr
        : Expr ',' ExprList

    Expr
        : IfExpr

    IfExpr
        : OrExpr
        : OrExpr '?' IfExpr ':' IfExpr    # Conditional

    OrExpr
        : AndExpr
        : AndExpr or Expr                 # Logical or

    AndExpr
        : NotExpr
        : NotExpr and AndExpr             # Logical and

    NotExpr
        : RelExpr
        : not NotExpr                     # Logical not

    RelExpr
        : TestExpr
        : TestExpr '='  TestExpr          # Comparison
        : TestExpr '!=' TestExpr
        : TestExpr '<'  TestExpr
        : TestExpr '<=' TestExpr
        : TestExpr '>'  TestExpr
        : TestExpr '>=' TestExpr
        : TestExpr '~'  TestExpr          # Pattern match
        : TestExpr '!~' TestExpr

    TestExpr
        : StringExpr
        : ':dir'    StringExpr            # Is directory
        : ':exists' StringExpr            # Exists
        : ':file'   StringExpr            # Is file
        : ':mtime'  StringExpr            # Modification time
        : ':read'   StringExpr            # Can read
        : ':size'   StringExpr            # Size
        : ':write'  StringExpr            # Can write

    StringExpr
        : AddExpr
        : AddExpr '.' StringExpr

    AddExpr
        : MulExpr
        : MulExpr '+' AddExpr             # Add
        : MulExpr '-' AddExpr             # Subtract

    MulExpr
        : UnaryExpr
        : UnaryExpr '*' MulExpr           # Multiply
        : UnaryExpr '×' MulExpr           # Multiply
        : UnaryExpr '/' MulExpr           # Divide
        : UnaryExpr '÷' MulExpr           # Divide
        : UnaryExpr '%' MulExpr           # Modulo

    UnaryExpr
        : CallExpr
        : '+' UnaryExpr                   # Numeric
        : '-' UnaryExpr                   # Numeric negative

    CallExpr
        : BuiltinExpr
        : FuncCallExpr
        : SubscriptExpr

    SubscriptExpr
        : Term
        : Var
        : Var '[' Expr ']'

    BuiltinExpr
        : format '(' Expr ',' Expr ')'
        : index  '(' Expr ',' Expr ')'
        : len    '(' Expr ')'
        : norm   '(' Expr ')'
        : repl   '(' Expr ',' Expr ',' Expr ')'
        : rindex '(' Expr ',' Expr ')'
        : sub    '(' Expr ',' Expr [',' Expr] ')'
        : trim   '(' Expr ')'

    FuncCallExpr
        : '@'name ['(' [ExprList] ')']    # Subroutine call

    Var
        : '$'name                         # Variable
        : '${'name'}'                     # Variable
        : '${$'name'}'                    # Environment variable

Operators

Operator precedences, from lowest to highest, are:

There are also prefix operators, which introduce a term within a command but otherwise do not act like actual operators:


Expressions

Operators are listed in order from lowest precedence to highest.

conditional ?: operator
IfExpr
    : Expr-1 '?' Expr-2 ':' Expr-3

Conditional logical expression (if-then-else).
If the Expr-1 operand evaluates to true, then Expr-2 (the "then" operand) is evaluated and its result is returned, otherwise Expr-3 (the "else" operand) is evaluated and its result is returned.

Note that the '?' and ':' operators generally should be surrounded by spaces in order to allow the parser to distinguish them from pathnames and filename patterns. For example, a?b:c resembles a pattern, whereas a ? b : c is an expression and cannot be confused with a pattern or filename.

Example:

    set blk = ($n<0 ? $size : $size/2)

or operator
OrExpr
    : Expr-1 or Expr-2

Logical disjunction (or).
Returns true if either Expr-1 or Expr-2 evaluate to true. Note that if the left operand is true, the right operand is not evaluated (this is called short-circuit evaluation).

This operator returns a logical true (which is equal to 1) or false (which is equal to 0) value.

Example:

    set x = ($n < 0  or  $n > $max)
    if ($rate = 0  or  $cost/$rate <= 0)
        echo Indeterminate: $cost/$rate

and operator
AndExpr
    : Expr-1 and Expr-2

Logical conjunction (and).
Returns true if both Expr-1 and Expr-2 evaluate to true. Note that if the left operand is false, the right operand is not evaluated (this is called short-circuit evaluation).

This operator returns a logical true (which is equal to 1) or false (which is equal to 0) value.

Example:

    set x = ($n >= 0  and  $n <= $max)
    if ($rate != 0  and  $cost/$rate > 0)
        echo Result: $cost/$rate = ($cost/$rate)

not operator
NotExpr
    : not Expr

Logical negation (not).
Returns true if Expr evaluates to false, and vice versa. In other words, the result is the logical inverse of Expr.

This operator returns a logical true (which is equal to 1) or false (which is equal to 0) value.

Example:

    set x = (not $c < 0)
    if (not :mtime 'foo.txt' > $lastTime)
        echo File has not aged: 'foo.txt'

Relational operators
RelExpr
    : Expr-1 '='  Expr-2        # Equal to
    : Expr-1 '!=' Expr-2        # Not equal to
    : Expr-1 '<'  Expr-2        # Less than
    : Expr-1 '<=' Expr-2        # Less than or equal to
    : Expr-1 '>'  Expr-2        # Greater than
    : Expr-1 '>=' Expr-2        # Greater than or equal to
    : Expr-1 '~'  Expr-2        # Pattern match
    : Expr-1 '!~' Expr-2        # Pattern mismatch

Relational/comparison operators.
Compares the value of Expr-1 to the value of Expr-2. The =, !=, <, <=, >, and >= operators test for equality, inequality, less than, less than or equal to, greater than, and greater than or equal to, respectively. If both operands are numeric values, the comparison is an arithmetic operation, comparing the numeric values of the operands. Otherwise it is a string operation, comparing the character string values of the operands.

Note that the != operator can be used as an exclusive-or operator:

    if (($size < 0) != ($max < 0))
    {
        # Either $size or $max is negative, but not both
        echo satisfied conditions
    }

Pattern match operators.
The ~ operator treats the right operand as a filename pattern, and returns true if the left operand matches the pattern. The !~ operator operates similarly, but returns the opposite result (thus x !~ y is equivalent to not x ~ y).

All of the relational operators return a logical true (which is equal to 1) or false (which is equal to 0) value.

Example:

    set flag = ($fname >= 'foo000.txt')        # String compare
    set stop = (+$count = 0)                   # Numeric compare
    set flag = (len($user) <= 8)
    set flag = ($fname ~ '[0-9]*[a-z].[a-z]*[a-z0-9]')

File predicate operators
TestExpr
    : ':dir'    StringExpr             # Is directory
    : ':exists' StringExpr             # Exists
    : ':file'   StringExpr             # Is file
    : ':mtime'  StringExpr             # Modification time
    : ':read'   StringExpr             # Can read
    : ':size'   StringExpr             # Size
    : ':write'  StringExpr             # Can write

File test predicates.
Evaluates Expr and uses the resulting value as a filename, applying a test to the local file that it names. The :mtime operator returns the time that the file was last modified (in the same format as var $Time), and the :size operator returns the numeric length of the file (in bytes). The other operators return a logical true if the file meets the conditions of the predicate operator, and false otherwise. If the named file does not exist on the local system, the numeric operators (:mtime and :size) return zero and the boolean operators return false.

Example:

    # Wait for a file to be deleted
    while (:exists $fname)
        sleep 1000

    # Send a file only if it's not empty
    if (:size $fname > 0)
        put $fname

String operator
StringExpr
    : Expr-1 '.' Expr-2

The . operator concatenates (joins together) the string values of Expr-1 and Expr-2, resulting in a new string value.

Due to the lexical constraints of the command interpreter, the '.' character may appear as either an operator or as a filename character, depending on the context in which it appears, which can lead to some surprising results:

    set file = foo
    set ext =  txt
    get $file.$ext                 # => get 'foo.txt'
    get '$file.$ext'               # => get 'foo.txt'
    get ($file.$ext)               # => get 'footxt'
    if (:read $file.$ext) ...      # => if (:read 'footxt')
    if (:read '$file.$ext') ...    # => if (:read 'foo.txt')

Remember that outside parentheses or within quotes, a '.' character is just a character, but within parentheses it is an operator. When in doubt, use quotes or parentheses to make the intended meaning clear to the parser.

The concatenation operator can be used to convert a numeric value into a string value, so that the resulting value can be used in a string comparison expression:

    set n = 123
    if ($n > '0999') ...           # Numeric compare: 123 > 999 (false)
    if ($n . '' > '0999') ...      # String compare:  '123' > '0999' (true)

    echo $n                        # Prints string: 0123
    echo ($n."")                   # Prints string: 0123
    echo (+$n)                     # Prints number: 123

Example:

    set fullname = ($path . '/' . $name . '.' . $ext)

Addition operators
AddExpr
    : Expr-1 '+' Expr-2
    : Expr-1 '-' Expr-2

Addition operators.
Evaluates Expr-1 and Expr-2 and adds or subtracts the resulting numeric values together, producing a numeric result.

The + operator can be used to convert a string value into a numeric value:

    set str = 00123                # = '00123'
    set num = (+$str)              # = 123

    set xyz = +0847.50             # = '+0847.50'
    set num = (+$xyz)              # = 847 (integer)

    if ($str > 99) ...             # String compare:  '00123' > '99' (false)
    if (+$str > 99) ...            # Numeric compare: 123 > 99 (true)

Example:

    set size = ($size + $incr)

+INCOMPLETE

( ) grouping operator
Term
    : '(' Expr ')'

Parenthetical grouping.
Returns the result of Expr. Parentheses can be used to group subexpressions so that they are evaluated in the intended order instead of in the default precedence order.

Note that parentheses are required for any Term expression involving more than a single operand.

Example:

    set x = ($n >= 0  and  ($n <= $max  or  $err))

[ ] subscript operator
SubscriptExpr
    : Term
    : Var
    : Var '[' Expr ']'

Array subscript (index).
An array expression (e.g., $abc) followed by a bracketed expression specifies an array indexing operation, treating the var as an array and using the value of the expression as in index into the array. Array indices are not limited to numeric values, and may be any string value, including empty strings (''). In this respect, arrays are more properly called hash tables. Thus $a[1], $a[xyz], and $a['x${user}y'] are all valid elements of array $a.

Note that since array indices are strings and not numbers, the exact value of the subscript expression is used to locate an element in the specified hash table. This means that $a[1] and $a[01] are two different elements, because 1 and 01 are two different strings (words). To limit array indexing to only numeric values, prefix the subscript expressions with a '+' unary operator. This forces both $a[+1] and $a[+01] to refer to the same element, which is $a['1'].

Note also that an unsubscripted var name designates a var that is separate from the array of the same name, e.g., $abc is a var separate and distinct from element $abc[$i] (for any value of $i).

Note also that only one-dimensional arrays are supported. So $a[$i] designates a valid array element, but $a[$i][$j] is a syntax error. Multi-dimensional arrays can be simulated, however, by concatenating one or more array indices together to form a single unique array index. For example, $a[$i.@.$j] joins index $i and $j together to produce a single index into array $a.

Due to limitations in the current version of the command script parser, array subscripts can only occur within parenthesized expressions (terms), and cannot appear within string literals. Thus ($arr[$i+1]) is a valid term, but '$arr[$i+1]' is not; the latter is interpreted like the expression ('$arr'.'[$i+1]'). It is recommended that temporary vars be used to get around this limitation.

See also: set.

Term operand
Term
    : string
    : word
    : Var
    : number
    : true
    : false
    : '(' Expr ')'

The lowest level component of any expression is a term, which acts as a single operand within the expression.

A term is a string literal (e.g., 'Hello.txt'), a number literal (e.g., 42), a word token (e.g., file, bin/$app/foo.dat, and c:\$app\bin\foo.exe), a var name (e.g., $Ff and ${count}), or the keyword true or false.

String tokens are sequences of characters enclosed within single quotes (') or double quotes ("). They can contain almost any character, including spaces and multinational characters. For example, 'path/docs/$file.txt', "Hello, world", 'Resumé.doc', 'Años', and "schlußel.txt". Strings may also include character escape sequences, e.g., 'my$$acnt' and 'don`'t know'.

Word tokens are composed of just about any sequence of characters, including multinational characters, but not spaces or leading parentheses. For example: file, c:\bin\file.exe, $Error, and $count#xyz($n). Words may also include character escape sequences, e.g., my$$acnt and don`'t`sknow.

Number literals are actually just a special cases of word tokens, being composed of decimal digits (0 thru 9) and an optional leading sign (+ or -). For example: 0, -1, +42. Only integers are supported, so words such as 3.141592 and -12.50 are not numbers.

The true keyword evaluates to true within logical expressions and to 1 in numeric or string contexts. The false keyword evaluates to false within logical expressions and to 0 in numeric or string contexts.

A term operand may also be any expression enclosed within parentheses. Note that the presence of the opening parenthesis '(' causes the words within the parentheses to be split into individual tokens. This means that $path/$file.txt is a single word token, but ($path/$file.txt) is seven separate tokens forming an expression: ( $path / $file . txt ).


Functions

The built-in (predefined) funcs are:

format(fmt, val)
Formats val according to the fmt specification. The val is usually a var containing a date/time value, or a file or directory pathname.

index(sub, str)
Returns the position of the first occurrence of substring sub within string str, or zero if the substring is not found. The first (leftmost) character is at position 1. For example, index('ana', 'banana') returns 2.

lc(str)
Converts string str to lower case, i.e., all upper case alphabetic characters in the string are replaced with their lower case equivalents. For example, len('BanAna') returns 'banana'.

len(str)
Returns the length of string str. For example, len('banana') returns 6.

norm(str)
Treats string str as a pathname, normalizing it into a proper pathname suitable for the native runtime environment. For example, on MS/Windows systems, file('~/doc/resume.txt') might return 'c:\doc\resume.txt', while on a Unix system it might return '/u/home/bob/doc/resume.txt'.

repl(old, new, str)
Replaces the first occurrence of substring old within string str with substring new. For example, repl('ana', 'oo', 'banana') returns 'boona'.

rindex(sub, str)
Similar to index(), but returns the position of the last occurrence of substring sub within string str, or zero if the substring is not found. The first (leftmost) character is at position 1. For example, rindex('ana', 'banana') returns 4.

sub(str, pos [, len])
Returns the substring within string str starting at position pos with a length of len. If len is not specified, the rest of the string is returned. If pos is negative, the substring is extracted from the end (right side) of the string. The first (leftmost) character of str is position 1, and the last (rightmost) character is at position -1. If the string does not contain any characters at the specified positions, or if the length is less than 1, an empty string ('') is returned. For example, substr('banana', 1, 1) returns 'b', substr('banana', -2, 3) returns 'nan', and substr('banana', 9, 4) and substr('banana', 3, 0) both return an empty string ''.

trim(str)
Removes leading and trailing whitespace characters from string str. Whitespace characters include spaces, tabs, and control characters. For example, trim(' banana `t`t') returns 'banana'.

uc(str)
Converts string str to upper case, i.e., all lower case alphabetic characters in the string are replaced with their upper case equivalents. For example, len('BanAna') returns 'BANANA'.

Predefined Variables

The following is a list of predefined (built-in) vars (variables). The examples shown below assume a filename of "/path/dir/file.ext" and a date of <Fri 2006-05-02 09:14:36.075>.

$$
Plain '$' character.

$`
Plain '`' character.

$Connectsecs[session]
Connection time, in seconds. This is the number of seconds elapsed since the last successful connect or open command. Counting stops at the next disconnect command.

$Connectime[session]
Connection time, in the same format as $Time. This indicates the time of the last successful connect or open command.

$Dataport[session]
FTP data port number. This is set using the port command.

$Disconnectime
Disconnection time, in the same format as $Time. This indicates the time of the last dicconnect command.

$Error
Error code from the last command executed. This is a numeric value, where 0 indicates success and any other value indicates an error.

$Errormsg
Last FTP command response message.

$F
Current foreach filename. This is the complete pathname, including the directory, filename, and extension (e.g., '/path/dir/file.ext'). This is empty ('') if there are no more matching foreach filenames.

$Fd
Final directory component of the current foreach filename (e.g., 'dir').

$Fe
Filename extension component of the current foreach filename (e.g., '.ext'). Equivalent to sub($F, index($F, '.')).

$Ff
Name component of the current foreach filename (e.g., 'file.ext'). Equivalent to sub($F, index($F, $Fs)+1).

$Fh
Remote host (FTP server node) name of the current foreach filename (e.g., 'ftp.server.com').

$Fn
Name component of the current foreach filename, sans extension (e.g., 'file').

$Fp
Path directory components of the current foreach filename (e.g., '/path/dir').

$Fs
Filename directory separator (e.g., '/' or '\').

$Fx
Filename extension component of the current foreach filename, sans '.' (e.g., 'ext'). Equivalent to sub($F, index($F, '.')+1).

$Host[session]
Remote FTP host name. This is set using the open command.

$Password[session]
FTP user password. This is set using the user command.

$Port[session]
Remote FTP port number. This is set using the open command.

$Random
Returns a pseudo-random 18-digit numeric value, in the range 000000000000000000 to 999999999999999999.

$Randomseed
Pseudo-random number generator seed. This is 0 by default. Setting this value causes subsequent $Random values to follow a particular sequence.

$Time
Returns the current date/time as the number of milliseconds since 1970-01-01 00:00:00 Z. Use the format() func to convert the value into a calendar representation.

$Timeout[session]
FTP time-out interval, in seconds. This is set using the timeout command.

$User[session]
FTP user name. This is set using the user command.

+INCOMPLETE


Filename Patterns

Several of the commands and expressions make use of filename patterns (or just patterns for short). These are a special kind of regular expression for matching filenames. Empty patterns ("") are not allowed. Patterns consist of one or more of the following:

Pattern Description
. Matches a period (.) in a filename.
? Matches any single filename character.
* Matches zero or more filename characters.
[abc]
[a-z]
Matches a single filename character matching any of the characters within the character set inside the brackets. The form [a-z] is used to specify a character range, which matches any filename character between a and z inclusive.
[^abc]
[^a-z]
Matches a single filename character that does not match any of the characters within the brackets.
other Any ordinary printable character, which matches that exact character within a filename.

Note that some file systems support case-sensitive filenames, so alphabetic characters must match exactly, i.e., the pattern character q matches only a q within a filename and not a Q. On the other hand, other file systems support case-insensitive filenames, treating upper and lower case alphabetic characters the same, i.e., the pattern character q matches both a q and a Q within a filename. The same rules also apply to character sets and ranges.


Example Applications

Listing 1
    # Retrieve remote ZIP files periodically and extract their contents
    var cnt = 0
    user xxx yyy

    repeat
    {
        # Get a list of zipfiles sorted by date
        if ($cnt = 0)
            open ftp.host

        set cnt = 0
        foreach *.zip in '-t' max 100
        {
            # Retrieve a zipfile
            set Error = 0
            get $Ff
            if ($Error != 0)
                continue                # Skip the file
            set cnt = ($cnt + 1)

            # Extract the contents of the zipfile
            !jar -xf "$Ff"
            !del "$Ff"
        }

        # Wait if there are no zipfiles to get yet
        if ($cnt = 0)
        {
            close
            sleep (20 * 60*1000)        # 20 minutes
        }
    }
Listing 2
    # Move files from one remote system to another
    user &1 xxx1 yyy1
    user &2 xxx2 yyy2

    repeat
    {
        # Connect to the remote systems
        repeat
        {
            set Error = 0
            open &1 ftp.host1
            if ($Error = 0)
            {
                set Error = 0
                open &2 ftp.host2
                if ($Error = 0)
                    break
                close &1
            }

            # Can't connect to both systems, wait and try again
            sleep (30*1000)     # 30 sec
        }

        # Move files from one system to the other
        repeat
        {
            # Get a list of files to transfer
            foreach &1 * in '-t' max 100
            {
                # Get a file from the 1st system
                set Error = 0
                get &1 $Ff
                if ($Error != 0)
                    continue
                del &1 $Ff

                # Copy the file to the 2nd system
                set Error = 0
                put &2 $Ff $Ff.tmp
                if ($Error != 0)
                    continue
                ren &2 $Ff.tmp $Ff
                !del $Ff
            }

            # Sleep if there were no files transferred
            if ($F = '')
                break
        }        

        # Close the FTP connections and wait for a while
        close &1
        close &2
        sleep (10 * 60*1000)    # 10 min
    }