Scripting

by Chris Herborth

Section 1 What Is Scripting
Section 2 Application Scripting Languages for BeOS
Section 3 Setting Up a Script
Section 4 Shell Scripting 101
Section 5 Really Advanced Shell Use
Section 6 BeOS Application Scripting
Section 7 Making Your Scripts Run from the Tracker
In this section:

BeOS Application Scripting

   How It Works
      Commands
      Properties
      Specifiers
      Scripting Suites

   Application Scripting Tools
      Installing hey

   Working with hey
      Using hey
      The hey Verbs
      Sending Any Message
      Hey Property Specifiers
      Instance Names
      Values
      Property Names

   Some Examples
      Hiding the Unhidable
      Where Am I?
      Using builtin
      Name That Picture
      Email Settings

   Where Now?


BeOS Application Scripting

You've probably been wondering when I was going to finally get around to the exciting BeOS-specific application scripting stuff. Well, I wanted to make sure you had a good foundation in "normal" shell scripts before we went off into BeOS application scripting, because you'll need everything you've just learned to make the most of it!

Don't let that scare you, though. You won't need to do any shell programming to do application scripting, but knowing some shell will let you do much more complex things.

With BeOS application scripting, you can do all sorts of things: open and close images to make your own slideshow, perform a series of complex transformations on a sample in an audio editing program, make two programs interact with each other, or convert the first letter of every paragraph in a document into a fancy drop-capital using a random font. You're limited only by your own creativity (oh, and your documentation!).

When doing application scripting, it's important to understand the distinction between objects and instances. An object is a kind of thing; these are the nouns we'll be scripting, like "windows" or "applications." An instance is a particular object; if "window" is an object, the StyledEdit window would be an instance of a "window." This will come up often, particularly if you're talking to programmers or tech support people about something (especially in an operating system that uses lots of object-oriented programming, like BeOS).

How It Works

As we discussed at the start of this chapter, BeOS application scripting works by sending and receiving normal system messages. Every BeOS application works with application scripting because BeOS itself handles the generic scripting messages.

The application scripting instructions and examples in this chapter are fairly generic, but some specific applications support more advanced options. Check your favorite application's documentation to see if it supports more complex application-specific scripting commands.

Application scripting works with commands, properties, and specifiers. Commands are the actions you want to perform (like getting something, setting something, or opening a document). Properties are the things the commands act on (like window titles or documents). Specifiers are used to find the specific property to work with. If you're familiar with AppleScript (or English), you might think of these as verbs, nouns, and adjectives.

Figure 2
In the scripting command get title of window 0 of StyledEdit, the command is get, the property is title, and the specifier is window 0 of StyledEdit. Similarly, delete window 0 of StyledEdit would close StyledEdit's first window.

We'll look at specific scripting commands shortly, when we talk about the hey command-line utility (see Application Scripting Tools, this chapter).

Commands There are only six standard BeOS application scripting commands; the names given here are the "official" names for these commands, and not something you'd actually send to a running application:

  • count properties: Counts the number of instances of a property. For example, if NetPositive had several windows open, you could ask it to count them. In this case, the "property" would be "window."
  • create property: Creates a new instance of a property. This is the command that would open a new document.
  • delete property: Deletes an instance of a property. Use this to close documents or windows.
  • execute property: Executes an instance of a property. If an application let you create a macro, this would let you run the macro.
  • get property: Gets the value of an instance of a property. You can "get" the title of a window, for example.
  • set property: Sets the value of an instance of a property. You can also "set" the title of a window.

All application scripts will be built using these six commands, although some specific applications may add custom commands to better support whatever it is that the app does.

Remember, these are the "official" names for these commands; scripting utilities like hey and scripting support in programming languages like Python will call them by different names. For example, the following table showshow the official names correspond to hey's commands, and what programmers see when writing C++ code:

Official Command hey's Command C++ Command
create property create B_CREATE_PROPERTY
delete property delete B_DELETE_PROPERTY
execute property B_EXECUTE_PROPERTY
get property get B_GET_PROPERTY
set property set B_SET_PROPERTY

Yes, hey doesn't support the execute property command. That's not a problem, though--you can do a lot of useful things without it!

Properties A property is a scriptable feature of an object. Properties are given unique names within that object. For example, a window has properties named Frame (a rectangle representing the size and position of the window), Title (the text in the window's title tab), and one or more View properties (the contents of the window).

Figure 3
The NetPositive window has several properties: the Frame, a Title, and a bunch of View properties.

Some properties are other objects; for example a window's View is actually a view object, which has its own set of properties.

An object can have more than one instance of a property. If an application has several windows open, it will have more than one Window property, one for each window. Asking for the window in an application could be ambiguous, but asking for the first window, or a window named Funky would work. It's not always enough to identify something with just a property; sometimes you need a specifier to help narrow it down.

Specifiers Specifiers let you target a specific instance of a property and come in two parts:

  • The name of the property, such as Window
  • Something to identify a specific instance of a property, like its name

If you were writing a BeOS program in C++ to do your scripting, you'd have a wide range of possible identifiers, but when you're doing scripting from the shell using a command-line scripting tool like hey, you're limited to things like names or numbers. We'll talk more about this a little later.

You can "stack" several specifiers together to help locate a specific object; for example, if you want to get the frame of the second view in a NetPositive window named "Welcome to Be, Inc." you'd type a hey command like this:

hey NetPositive get Frame of View 2 of Window "Welcome to Be, Inc."

And you would see a result like the one shown in Figure 8.4.

Figure 4
The results of asking hey for the Frame of View 2 of Window "Welcome to Be, Inc."

The specifiers go from the object you're after up through all of the objects that contain it. In this case, the frame is part of the view, which is part of the window. This is pretty natural for English speakers because it's almost exactly how we would say it if we were describing it to someone else!

Scripting Suites Being able to find the scripting abilities supported by an object lets you work with almost any kind of object, even if you're never heard of it before. Every object's scripting abilities are organized into one or more scripting "suites." A suite is defined as a standard set of supported specifiers and their properties.

For example, every button in an application is the same kind of thing (a button with some text in it) and they all behave the same way, causing something to happen when you click on them. The arrows in a scrollbar don't look like buttons, but they act like them by scrolling your document when clicked. If buttons and scrollbar arrows could handle the same kinds of scripting commands (which they can), this common set of commands would be the "button" suite (buttons are actually part of a larger "control" suite--a set of common scripting commands that work with every GUI control in all BeOS applications).

You can find the suites (and the specific properties) supported by BeOS objects (like windows and buttons) by looking in the BeBook, the programming documentation for BeOS that gets installed automatically on every system. At the end of every object's description is a section on scripting support, listing the supported suites. Take a look at the BWindow description in the Interface Kit, for example. Unfortunately, these documents are intended for programmers, not users trying to do some scripting, so you'll have to experiment.

Objects can support more than one suite of commands; a given object will respond properly when you send it any command from any of those suites. For example, menus support the "menu" suite and the "view" suite. This lets them work with any command from both the "menu" and "view" suites.

Suites have MIME-style names like suite/vnd.Be-control (for user-interface controls like buttons and checkboxes) and at the time of this writing the only suites available were those defined by Be. In the future, there will be suites of scripting commands defined by third parties (and by Be) for graphics applications, for example, which will allow all graphics applications to work with similar scripting commands.

Scripting suite names look like MIME filetypes (like "suite/vnd.Be-control"), but they aren't. Suite names are completely internal to BeOS, and the fact that they look like MIME types is just a geeky detail.

Don't be fooled!

Another way of finding an object's supported suites is to use hey's getsuites command:

$ hey NetPositive getsuites

You'll learn more about hey and getsuites later in this chapter.

Application Scripting Tools

This is all pretty abstract, and we can't start looking at real examples until we've discussed the tools that let you take advantage of BeOS's application scripting from the shell. We're going to do our scripting from the shell using the hey command-line utility because it lets us take advantage of everything we learned earlier in this chapter, and because most of the other scripting languages (such as Perl) can't currently take part in BeOS message passing.

Unfortunately, there aren't very many tools available yet. Application scripting is pretty new on BeOS, and a lot of developers are still trying to figure things out. Luckily, BeOS Masters Award winner Attila Mezei has given us an excellent scripting tool in the form of hey, which is available on his Web pages (http://w3.datanet.hu/~amezei/).

Remember, even though we're talking about the shell and hey here, BeOS applications can be remote-controlled by any programming language, utility, or application that can send messages. By the time you read this, that will include Python and may include Perl--if so, you won't need hey, but the principles we talk about here will still apply.

Installing hey After you've downloaded hey (make sure you get the latest version--this section assumes you're using hey version 1.1.1 or later), and unpacked the archive, you'll be confronted with a problem. There isn't an executable in the archive!

hey is distributed as source code, with documentation and two project files (one each for PowerPC and x86 systems). This is good for developers, who love to have the code for everything, but not so good for users who just want to get something done. It's not very hard to build your very own copy of hey, though.

  1. Double-click on the appropriate project file in the hey folder--hey.PPC.proj if you're using a PowerPC-based system or hey.Intel.proj- if you're using an x86-based system.
  2. The BeIDE will pop up on your screen after a few seconds and open the project.
  3. Select Make from the Project menu or hit Alt+M (or Command+M if you're on a Mac keyboard). The IDE will build a fresh new hey executable for you.
  4. Close the BeIDE window and move your new hey executable (with its pile of blocks icon) into home/config/bin on your boot disk. You're done!

Working with hey

Just as writing a useful shell script involves stringing together a bunch of smaller shell commands, writing a script to control a GUI application involves stringing together a bunch of scripting commands.

In both cases, the individual commands only move you a little closer to your goal. It's when you stick them together that they get you were you want to go.

You can duplicate all of the examples in this section using a hey-like set of commands in Python courtesy of an add-on called heymodule. Be sure to check the Languages section of BeWare for Python add-ons that support application scripting!

Using hey hey supports the standard BeOS scripting commands shown in the Commands section above, plus a couple of other useful verbs that are listed just below.

The syntax of a hey command is:

hey application verb [ specifier [ of specifier ... ] ] [ to value ]

The syntax shorthand I use above is the standard way of showing a command's options; it describes how to use a command. Arguments in square brackets are optional, so one or more specifiers are allowed, and so is the "to" clause at the end. You'll need that "to" clause when you want to set a property.

The application can be specified as an application's name (what you see in the Deskbar, like "Tracker" or "ShowImage") or as an application signature like application/x-vnd.Be-TRAK or application/x-vnd.Be-ShowImage. (Most application signatures are in the form application/x-vnd.company.programname; if they're not, the developer receives a visit from the MIME Police!). Details on application signatures (or "app_sigs") can be found in Chapter 5, Files and the Tracker.

You can find the signature of an application by dropping it onto the File Types preference window. You can read all about the File Types application in Chapter 10, Prefs and Customization!

The hey Verbs The verb can be any one of those shown in Table 5. The last three (quit, save, and load) aren't standard scripting commands, but they are standard messages that all applications know how to handle. Although they're not, strictly speaking, part of application scripting, they're definitely useful.

Table 5 The verbs hey uses to control applications

hey Verb What It Does Comments
get Gets an instance of a property. The same as the get property command we talked about earlier.
set Sets an instance of a property. Same as set property.
count Counts the instances of a property. Same as count property.
create Creates a new instance of a property. Same as create property.
delete Deletes an existing instance of a property. Same as delete property.
getsuites Gets the supported suites for a property. This isn't a standard scripting command by itself, but shorthand for several count and get commands to get a list of supported suites
quit Quits the application. Not a standard scripting command, but useful.
save Saves a document. Another shorthand command built with standard scripting commands.
load Loads a document. More shorthand (like using create).

Applications can respond to a huge number of different messages, including the standard scripting messages that we're talking about here. Some of these messages tell the app to do useful things, like quit (because the user told it to or because the system is being shut down) or save the current document. These "other" messages are usually only available to programmers working in C++, but hey makes them available to you from the shell.

Sending Any Message The verb in your hey statement can also be any four characters enclosed in single quotes, such as '_ABR'. This lets you send any message, which can be handy if an application supports a useful but nonstandard command. BeOS messages have a four-character ID so applications can figure out what kind of messages they are; '_ABR' is the ID for "show me the About box," and every GUI application knows how to respond to this message. If you want to send a specific message like this, you must include the single quotes, or hey might thing it's one of the verbs it knows about.

BeOS flings around a lot of messages totally unrelated to scripting, like the "show me the About box" message. You can find out about these by looking in the BeBook and the Application Kit's AppDefs.h header (located in /boot/develop/headers/be/app). Most of these messages require extra information to do anything useful, which means they might cause problems if you send them using hey. An application expecting to find the right kind of data in the message could become confused and crash. So be careful!

Hey Property Specifiers The property specifiers are a little more complex. Property specifiers let you talk to and send messages to a specific object. They give you a way to let hey know what you're talking to. Specifiers come in several flavors:

name
name [index]
name [-reverse_index]
name "instance name"

(In these cases, the square brackets and quotation marks are required--they're not part of the shell command description.)

Just specifying a property name is the easiest; for example:

$ hey application/x-vnd.Be-TRAK get Name

will return "Tracker" on my system.

The next specifier (name [index]) lets you specify an integer to find a specific instance. Open a couple of Tracker windows and try a few indexed commands like this:

$ hey Tracker get Title of Window [0]
$ hey Tracker get Title of Window [1]

You'll get a reply something like this:

BMessage(B_REPLY):
"result" (B_STRING_TYPE) : "Tracker Status"

for the windows that are open. You'll notice that there are two hidden Tracker windows around all the time, Tracker Status and Desktop. These are used by the Tracker to display the "Copying files" status window and the Desktop background; don't mess with them unless you don't mind crashing. Windows are given an index based on the order in which they're opened, and the indexes only count windows that are currently open. If you specify an index that isn't valid, you'll get an "index out of range" error message.

For example, say you've got a few Tracker windows open, and you've just seen that the last window is named "beos" (because it's open on the /boot/beos directory). You could confirm this by typing:

$ hey Tracker get Title of Window [3]

Using a negative number will count from the last item instead of the first. You could get the title of the last window using:

$ hey Tracker get Title of Window [-1]

Instance Names Another easy specifier uses the instance name (name "instance name"). This lets you refer to instances of objects by name, which is great if you already know the name. A silly example using this technique is to open StyledEdit and type this:

$ hey StyledEdit get Title of Window "Untitled 1"

You'll see that the window named "Untitled 1" has a title of...well, I'm sure you can guess. You can use this if, for example, you've used a script to open a document (StyledEdit's window title will be the filename), and you want to do something with that document. By referring to the window by name, you can be sure you're working with the document you just opened.

Values Values are the things you'll be using in a set command, and they can usually be specified as strings (put them in quotes) and numbers. Sometimes you need to specify Boolean values ("true" or "false"), specific kinds of numbers, points, rectangles, colors, or files.

You'll have to consult the documentation for the application you're trying to script to see if its set property commands require a specific kind of value or if you can just use a plain old string or number.

To use a specific kind of value, or one of the special values (such as a color), you use this syntax:

kind(value)

Values in all applications can be defined or organized by type. Table 6 lists the various types of values a given application may support. The documentation for the application you're scripting will tell you if you need a specific kind of value for a particular object and what values are allowed.

Table 6 Types of Values

Type of Value Description
bool A Boolean value can be either true or false. For example, to set something to true, you'd use a value of bool(true).
BPoint The x and y coordinates of a point on the screen, in a window, etc. BPoints take two values: BPoint(x,y), where x and y are numbers.
BRect A rectangle specified as the coordinates of the top-left corner and the bottom-right corner. Four numbers are needed here BRect(left,top,right,bottom).
double A double-precision floating-point number; double-precision floats let programs do more precise calculations, though they take up more memory.
file The path to a file, directory, or symbolic link. For example, to set something to /boot/some/file, you'd use file(/boot/some/file).
float A floating-point number, such as 1.5.
int8 An 8-bit number; the documentation that came with your application will let you know if it needs a specific kind of number (like int8, int16, or int32). Most of the time, you can just use the number you want.
int16 A 16-bit number. A 16-bit number can be twice as large as an 8-bit number.
int32 A 32-bit number. A 32-bit number can be twice as large as a 16-bit number.
rgb_color A color specified as red, green, blue, and alpha values from 0 to 255: rgb_color(red,green,blue,alpha).

Let's try some examples of finding and using values in simple hey statements. For instance, when you start StyledEdit by double-clicking on its icon in the apps folder, it puts a blank window in the top-left corner of your screen. Let's find out what the Frame rectangle of that window is:

$ hey StyledEdit get Frame of Window [0]

You should get back something like this:

BMessage(B_REPLY):
"result" (B_RECT_TYPE) : BRect(7.0, 26.0, 507.0, 426.0)

which gives you the value of the position of that window--its size and shape, as you can see in Figure 8.5.

Figure 5
The BRect of an empty StyledEdit window

We can use scripting and hey's set verb to change the value and move the window down and to the right:

$ hey StyledEdit set Frame of Window [0] to "BRect(107,76,607,476)"

Figure 6
Changing the StyledEdit window's Frame

This example returns a message that looks like something might've gone wrong:

BMessage(B_REPLY):
"error" (B_INT32_TYPE) : 0 (0x00000000)

The hey command always prints out the reply message it gets after sending your message. This reply always has one extra bit of information tagging along, the "error" value. In this case, the "error" is set to the value 0 (which we get to see as a normal number and as a hexadecimal number). As you'll remember from our earlier discussion about the exit status, 0 means "no problem!," so all is well.

If you get an "error" other than 0, and hey doesn't translate it into English for you, take a look in the Support Kit's Errors.h header (found in /boot/develop/headers/be/support), which might help. Every standard error is defined there, and the comments help explain what they mean.

You'll note that I added 100 to the top and bottom values, and 50 to the left and right values; this is to keep the StyledEdit window the same size and shape as it was originally. By changing just one of these values (or both in another proportion), you can stretch or shrink the window.

You can also change the entire size and shape of a window by altering its Frame in a script. This command:

Figure 7
Changing the StyledEdit window's Frame again; this time we change its shape, too.

$ hey StyledEdit set Frame of Window [0] to "BRect(307,76,607,476)"

lets you specify each side of the Frame rectangle of a window.

These scripting commands will work with any BeOS application that shows up in the Deskbar. Experiment for yourself to verify this!

Property Names Property names in the BeBook,. but unfortunately, they can be hard to find (they're hidden in with all of the programming documentation, in the scripting support section of most objects), and not everyone can survive wading through a long description of some code they're never going to use.

Different kinds of objects support different sets of properties (and you already know these sets are called suites). You can get a list of these properties, and some information about their contents, using hey's getsuites option:

hey NetPositive getsuites of View [0] of Window [0]

Something horrible like this will appear in your Terminal:

property   commands types specifiers

Menu
PCRT 6 2 3
(extra_data: 0x1)
(CSTR,data), (LONG,what)
Menu PDEL 6 2 3
(extra_data: 0x1)
Menu 6 2 3
MenuItem PEXE PDEL 6 2 3
(extra_data: 0x3)
MenuItem PCRT 6 2 3
(extra_data: 0x3)
(CSTR,data), (LONG,what)
MenuItem 6 2 3
(extra_data: 0x2)
MenuItem PCNT LONG 1
(extra_data: 0x4)
Enabled PGET PSET BOOL 1
(extra_data: 0x5)
Label PGET PSET CSTR 1
(extra_data: 0x6)
Mark
PGET PSET BOOL 1
(extra_data: 0x7)
property
commands types specifiers

Frame
PGET PSET RECT 1
Hidden
PGET PSET BOOL 1
View PCNT LONG 1
View 2 3 6
property commands types specifiers

Suites PGET 1
(CSTR,suites)
(SCTD,messages)
Messenger PGET MSNG 1
InternalName PGET CSTR 1

BMessage(B_REPLY):
"suites" (B_STRING_TYPE) : "suite/vnd.Be-menu"
"suites" (B_STRING_TYPE) : "suite/vnd.Be-view"
"suites" (B_STRING_TYPE) : "suite/vnd.Be-handler"
"messages" (B_PROPERTY_INFO_TYPE) : see the printout above
"messages" (B_PROPERTY_INFO_TYPE) : see the printout above
"messages" (B_PROPERTY_INFO_TYPE) : see the printout above
"error" (B_INT32_TYPE) : 0 (0x00000000)

This output is in two parts: the table at the top, and the BMessage chunk at the bottom. Within the table, the property column lists the names of the properties in View [0] of Window [0] (like Menu and Frame). The commands column shows a short version of the scripting commands that each property can understand:

Short Fform hey Command
PCNT count
PCRT create
PDEL delete
PEXE None (This is the execute property command that hey doesn't support.)
PGET get
PSET set

Lastly, the specifiers column tells you how you can use that property. The important specifiers are these three:

Specifier Description
1 A "direct" specifier; you can use this directly in the hey commands. For example, hey NetPositive get Label of View [0] of Window [0] will return that window's View's label.
2 An "index" specifier; you can use a number to choose a specific instance of this property. For example, you could get Menu [0] in this view.
6 A "name" specifier; you can choose a specific instance of this property by using its name. We've already seen this when we sent commands to a specific StyledEdit window with hey StyledEdit get Frame of Window "Unknown 1".

The rest of the output is the reply message that hey always prints when it's done. In this case, there's a lot more there than just the error value (remember, 0 means "everything is OK"). The various "suites" entries are what we're interested in; they indicate what scripting suites this object understands. In this case, the View knows about the suite/vnd.Be-menu, suite/vnd.Be-view, and suite/vnd.Be-handler suites of scripting messages. If you guessed that the BeBook's documentation about BHandler (in the Application Kit) and BMenu and BView (both in the Interface Kit) would have information about these suites, you'd be right. Each of these has a scripting support section, giving the names of the properties and the message you use to access those properties. The message is always one of the standard C++ scripting commands (shown earlier in the Commands section), such as B_GET_PROPERTY.

Looking back at the getsuites output for NetPositive, we see that there's a Label property in the first view of the first window. Just to refresh your memory a little:

$ hey NetPositive getsuites of View [0] of Window [0]
property commands types specifiers

...
PCRT
Label PGET PSET CSTR 1
(extra_data: 0x6)
...

Let's find out what that label says:

$ hey NetPositive get Label of View [0] of Window [0]
BMessage(B_REPLY):
"message" (B_STRING_TYPE) : "this menu doesn't have a label"
"error" (B_INT32_TYPE) : -2147475448 (0x80002008)

Ah-ha! View [0] must be a menu (since it's telling us "this menu doesn't have a label"). We might think that we could get the menu name because the Menu property in the getsuites listing has a 2 in the specifiers column, so it understands indexes, making us conclude that Menu [0] will be the first, and probably only, menu inside View[0]. Lets try it:

$ hey NetPositive get Menu [0] of View [0] of Window [0]
Didn't understand the specifier(s) (error 0x80002008)

That didn't work; hey didn't know what to do. Let's see what the menu knows about. Looking in the BeBook's Interface Kit section under BMenu's "Scripting Support" heading, we see that menus know about several properties, as shown in Table 7.

Table 7: Properties in the suite/vnd.Be-menu suite
Property Description
Enabled Specifies if the menu or menu item is enabled or disabled.
Label The text label in the menu or menu item.
Mark Specifies if the menu or menu item has a checkmark next to it.
Menu If the menu has another menu inside of it (hierarchical menus, for example), it will also have a Menu property. You'd refer to this submenu with something like Menu [0] of Menu [0] of View [0] of Window [0]. Look carefully if you think this is what we just tried; there's an extra Menu [0] of... in there, which means we're looking at the menu inside the menu. Yes, that's confusing; BeOS menu bars are menus, and the things that pop down when you click on them are also menus.
MenuItem The items inside the menu.

So, we should be able to get the label for the menu:

$ hey NetPositive get Label of Menu [0] of View [0] of Window [0]
BMessage(B_REPLY):
"result" (B_STRING_TYPE) : "File"

Looking in the NetPositive window, we can see that we've found the File menu. This isn't too useful, right? I mean, we can see the File menu just by looking at the window!

But menus have an Enabled property, right? What if we set that to false?

$ hey NetPositive set Enabled of Menu [0] of View [0] of Window [0] \
to "bool(false)"
BMessage(B_REPLY):

If you type this all on one line, you can leave off the "\" character. That's here in this example to let the shell know that I haven't finished typing this long command yet, and that it shouldn't try to run the command when I press the Return key.

We've just disabled NetPositive's File menu! Don't worry, you can turn it back on by setting the Enabled property to true. hey's return message seems to be totally empty; in this case, no news is good news, and setting a menu's Enabled property doesn't send us back a reply message.

Menus also have a MenuItem property; we can use this to get the items inside a menu. MenuItems have text labels, so let's assume they have a Label property just like Menu does:

$ hey NetPositive get Label of MenuItem [0] of Menu [0] of View [0] \
of Window [0]
BMessage(B_REPLY):
"result" (B_STRING_TYPE) : "New"

A quick poke into the File menu will tell you that New really is the first item. Success!

These specifiers are getting pretty huge, though; let's try moving them into a shell script where we can work in the comfort of our favorite text editor.

#! /bin/sh
#
# List the contents of NetPositive's File menu using application
# scripting.

# We'll stick part of this huge specifier into the target_menu variable
# to save typing.
#
# This will also make it easier to aim our script at another window;
# just change the index right here.

target_menu="Menu [0] of View [0] of Window [0]"

# Similarly, we'll store the application name in the app variable,
# to make it easier to aim this script at another application.

app="NetPositive"

# Start at the first item, which is numbered 0.

index=0

# Do this loop as long as the hey command's exit status indicates
# that all is well. When we get past the last MenuItem, hey will
# return an exit status that means "an error occurred", and then
# we'll stop looping.

while hey $app get Label of MenuItem [$index] of $target_menu ; do
# Go on to the next MenuItem.
let index=index+1
done

When you run this script a whole bunch of reply messages will be printed in your Terminal:

BMessage(B_REPLY):
"result" (B_STRING_TYPE) : "New"

BMessage(B_REPLY):
"result" (B_STRING_TYPE) : "Open Location..."

BMessage(B_REPLY):
"result" (B_STRING_TYPE) : "Open File..."

BMessage(B_REPLY):
"result" (B_STRING_TYPE) : ""

BMessage(B_REPLY):
"result" (B_STRING_TYPE) : "Save As..."

BMessage(B_REPLY):
"result" (B_STRING_TYPE) : "Close"

BMessage(B_REPLY):
"result" (B_STRING_TYPE) : ""

BMessage(B_REPLY):
"result" (B_STRING_TYPE) : "Page Setup..."

BMessage(B_REPLY):
"result" (B_STRING_TYPE) : "Print..."

BMessage(B_REPLY):
"result" (B_STRING_TYPE) : ""

BMessage(B_REPLY):
"result" (B_STRING_TYPE) : "About NetPositive"

BMessage(B_REPLY):
"result" (B_STRING_TYPE) : ""

BMessage(B_REPLY):
"result" (B_STRING_TYPE) : "Quit"

menu item index out of range (error 0x80000003)

Compare this to the contents of the File menu; we've got all of the menu items (with blank ones for the separators), but what's that error message at the end? After we've gotten to the end of the menu, we go through the while loop again, and this time, hey fails and kicks us out of the loop because we've asked NetPositive to tell us about a menu item that doesn't exist.

This is a little ugly, though, because there's way too much information coming out of the hey command.Let's move the hey command out into a shell function and use the sed command to get just the result.

This is one of the reasons why a "real" programming language with scripting support is high on everyone's Christmas list. Using hey from the shell to do non-trivial things can be quite a challenge; doing this from a programming language would give you back the string you asked for, without all of this extra stuff.

For example, using the Python heymodule, instead of having to deal with this reply message, you'd just get back the label of the menu item as a string, ready to use for your own evil purposes.

#! /bin/sh
# This script will give us a nice, clean list of
# NetPositive's scriptable menu items.

# Create a new function for this script, to collect the menu item's
# name and display it in a nicer fashion than hey's output.

show_result() {

# Make sure the function has the right number of arguments.
# $# returns the number of arguments, and we want to
# have two of them; otherwise you're not calling this
# function properly.

if [ $# -ne 2 ] ; then

# Sending back anything other than 0 means we
# had a problem. In this case, not enough
# arguments to do anything useful.

return 1
fi

# Store hey's output in the "info" variable.
#
# Note that NetPositive is hard-coded in here; we could
# replace it with a variable to make this easier to change.

info=$(hey NetPositive get Label of MenuItem [$1] of $2)

# If hey's exit status (in the $? variable) isn't 0, then
# something bad happened.

if [ $? -ne 0 ] ; then

# Print an error message, then return with an
# exit status of 0.

echo "Error getting label: $info"
return 2
fi

# Now use sed to strip off everything between the start of the
# line and the :, which happens to be the last thing before the
# data we really want.
#
# Note that sed is using a regular expression to strip off
# everything before the : character (".*" matches everything)
# by replacing it with nothing.

info=$(echo $info | sed -e "s/.* : //")

# $1 below is a positional parameter -- it refers to
# the position of the original argument to the function.
#
# This prints something like: menu File: "New"

echo menu $1: $info

# Return 0 as our exit status, meaning "all is well".

return 0
}

# The next line creates a variable to be used as "shorthand" later.

target_menu="Menu [0] of View [0] of Window [0]"

# Initialize the index variable at zero. Later, we'll increment it
# by 1 for every pass through the loop, so we can get data on each of
# the menu items sequentially.

index=0

# Now call the show_result function as long as its exit status
# indicates that all is well.

while show_result $index "$target_menu" ; do

# Move on to the next item.

let index=index+1
done

When you run this script, you'll see something like this:

menu 0: "New"
menu 1: "Open Location..."
menu 2: "Open File..."
menu 3: ""
menu 4: "Save As..."
menu 5: "Close"
menu 6: ""
menu 7: "Page Setup..."
menu 8: "Print..."
menu 9: ""
menu 10: "About NetPositive"
menu 11: ""
menu 12: "Quit"
Error getting label: menu item index out of range (error 0x80000003)

This is much nicer output. Changing this to use a function makes the script a little more complex, but we want to make sure we've got the right number of arguments at the start of the show_result function, and then we need to check hey's exit status to make sure it worked. Putting all of those commands inside the function actually makes life easier for us. After show_result calls hey to collect its output, we have to send its output through sed to strip off everything we're not interested in; this uses the little trick of replacing the matched string with nothing. The while loop at the end then calls show_result instead of hey, passing it the current index and the target menu as arguments.

Some Examples

Now that we've fooled around with hey a little and found out how to determine what in our applications is scriptable (by poking around with hey's getsuites command), let's try doing something a little more useful, and actually use hey to automate the GUI.

Hiding the Unhidable The PoorMan Web server has an annoying shortcoming: You can't hide its window automatically.

If you're starting PoorMan in your UserBootscript (so it comes up automatically every time you boot into BeOS), you'll end up either moving the PoorMan status window out of the way every time, or double-clicking on its title bar to hide it. Wouldn't it be better if you could hide it automatically when it starts?

You can do this by adding a couple of lines to your UserBootscript. Try adding these just after the line that starts PoorMan:

#! /bin/sh
...
# Start PoorMan

/boot/beos/apps/PoorMan &

# Give PoorMan a chance to get started, then hide its window:

sleep 2
hey PoorMan set Minimize of Window [0] to "bool(true)"

You might be able to sleep for one second (or no time at all), but that will depend on how fast your system can launch PoorMan. Now after PoorMan starts, its window will be minimized, and you won't have to look at it.

Where Am I? In Chapter 6, you learned that you could change the shell prompt from its default ($) to other things, including your current directory:

export PS1='$PWD> '

For some people (I'm not naming names; you know who you are!) this isn't enough, especially if they've got several Terminal windows open. You can still forget which directory you're in by ignoring the prompt.

It might help if the title of the Terminal window changed from "Terminal 1" to "Terminal: /boot/your/current/directory" but how could you make this work?

As mentioned in Chapter 6, the cd command is used for moving around the filesystem. If we want to update the Terminal window every time we change directories, we'll have to make up our own version of this command to change the directory and then tell the Terminal to update its window title. Let's stick these shell functions in our /boot/home/.profile:

mycd() {
# Change to the directory specified on the command line.

cd "$@"

# Use hey to set the title of the window to reflect
# the current directory. We direct all of hey's output
# to /dev/null because we don't want to see it every time
# we change directories.

hey Terminal set Title of Window [0] to "Terminal: $PWD" \
> /dev/null
}

After you reload the .profile (by typing /boot/home/.profile) you'll be able to use mycd to move around the filesystem and update the Terminal's title to show your current directory. We send hey's output to the bit bucket because we don't want to see it. Give it a try and see what happens!

Using builtin This isn't very convenient, though; you've got to remember to use a different command every time you want to move around and you've got to type more characters every time. Wouldn't it be easier if we could somehow name this cd? The shell has a command named builtin that tells it to use the built-in version of a command; this is exactly what we need! Change mycd to look like this:

cd() {

# Change to the directory specified on the command line
# using the original built-in "cd" command.

builtin cd "$@"

# Use hey to set the title of the window to reflect
# the current directory. We direct all of hey's output
# to /dev/null because we don't want to see it every time
# we change directories.

hey Terminal set Title of Window [0] to "Terminal: $PWD" \
> /dev/null
}

In other words, the shell has a command called cd, and we're defining our own function called cd. By using builtin, we can have our cake and eat it too. After you reload the .profile again, cd will update the Terminal's title bar to include the current directory.

If you're a fan of the pushd and popd commands (see your favorite bash manual for details), you can easily update these scripts to work with those commands as well, just by adding a couple of extra functions as variants of the cd function.

If you've got more than one Terminal window open, you'll notice a slight problem. The title bar of the first Terminal window is being updated, even if you're working in another window! To see this, hit Alt+N (or Command+N) in the Terminal; this fires up a new Terminal window. It'll load the .profile and use the new cd function. Now try using cd to change directories; as if by magic, the title bar of the first Terminal window tells you where the second Terminal's current directory is.

Unfortunately, there's no way to fix this, other than to limit yourself to only one Terminal window. The shell has no way of finding out which window it's running in. This isn't such a good thing (programmers and power users love having lots of Terminal windows open; I've usually got three or four going with different things happening in each one), so I've asked Be to add a feature to let the shell figure out what Terminal window it's running in.

In R4, the Terminal learned a new trick: It can use the same sequence of magic characters that the X Window System's xterm terminal uses to set its title. This little trick lets us get around the problem of not knowing what Terminal we're using.

By using this sequence of magic characters, all of your Terminals can have titles that follow you through the filesystem! Try adding this version of cd to your .profile:

cd() {

# If the cd command succeeds, set the Terminal title.

if builtin cd "$@" ; then
echo -e "\E]0;$PWD\a"
fi
}

The text inside between the ; and the \ (in this case, the contents of the PWD variable) will be displayed in the current Terminal's title bar.

Name That Picture The ShowImage application that comes with BeOS will display any image on your system, as long as you've got an appropriate Translator installed.When you use ShowImage to open an image, it uses the filename as the title for the window.

After starting ShowImage, you'll get a small, blank window with a menu. Open a Terminal and find one of the images on your hard drive. Let's get ShowImage to load the BeOS logo that gets installed in /boot/home/SampleMedia/images:

hey ShowImage load "file(/boot/home/SampleMedia/images/Be Logo 1)"

After a second, ShowImage will pop up a window titled "Be Logo 1" with a very nice rendered 3D Be, Inc. logo inside. This isn't nearly enough information for me, though. I've got a program called file installed from the GeekGadgets repository. file is a command-line tool that uses a set of rules to give you back information about a file's type. Usually this includes a little more information than the MIME type that you can get in the Tracker or the FileTypes application. Running file on this image, I can see what it is:

$ file "/boot/home/SampleMedia/images/Be Logo 1"
Be Logo 1: TIFF image data, big-endian

The file command works with most common types of files and can sometimes tell you useful stuff (with GIF files, for example, it'll tell you how big the image is).

Let's stick this extra info into our ShowImage window's title:

hey ShowImage set Title of Window "Be Logo 1" to \
"$(file '/boot/home/SampleMedia/images/Be Logo 1')"

We can write a little script to load a bunch of images and update their titles to show the extra file information, too:

#! /bin/sh

# Loop through the command-line arguments.

for name in "$@" ; do

# Tell ShowImage to load the current file. Note that we
# assume that ShowImage is already running.

echo Trying to load "$name"...
hey ShowImage load "file($name)"

# Give ShowImage a chance to load the file. You can probably
# tune this down a bit; it'll depend on the amount of memory
# you've got, what other applications are running, etc.

sleep 5


# Now tell ShowImage to change the title of this image's
# window. We store the output from "file" in the "info"
# variable just to keep things clean.

echo Trying to update title for "$name"...
info=$(file "$name")
hey ShowImage set Title of Window "$name" to "$info"

# Give ShowImage a chance to update the title. If you have
# a fast system, you can probably lower this to 1, or even
# remove it completely.

sleep 2
done

Run this with a few images and you'll have a screen full of pictures with more information than just their file names.

Email Settings BeOS comes with a useful email server and client. Unfortunately, the basic mail client, BeMail, can only cope with one user at a time. If you've got several email accounts, you'll be running the E-mail preferences application all the time, switching back and forth between your accounts to check for new mail (for information about setting up your email, see Chapter 4, Get Online Fast).

This is exactly the sort of thing that application scripting can make easier. We should write a script that changes the POP username, POP password, POP host, and SMTP host (if you've got multiple email servers), as well as the "Real name" and "Reply to" settings (especially if the accounts are for multiple people). Once we've done that, we should immediately check for new email.

The E-mail preferences application only has one window, so we'll be dealing with "something of Window [0]" here. After some poking around with hey, it looks like the interesting views are inside "View 0 of Window 0" (see Table 8).

Table 8 Finding all the interesting views in the E-mail preferences app
Specifier Label
Label of View [0] of View [0] of Window [0] POP username
Label of View [1] of View [0] of Window [0] POP password
Label of View [2] of View [0] of Window [0] POP host
Label of View [3] of View [0] of Window [0] SMTP host
Label of View [10] of View [0] of Window [0] Real name
Label of View [11] of View [0] of Window [0] Reply to
Label of View [16] of View [0] of Window [0] Check Now

You can easily modify the script we used to list the names of the items in NetPositive's File menu to give you a list of the views in the E-mail preferences panel. Try it! You'll need to change:
  • The show_result function's call to hey (you want to get the "Label of View [$1]" and you want to talk to E-mail)
  • The target_menu to "View [0] of Window[0]"

It's probably a good idea to turn on the email status window while you're doing this, to make sure everything is working properly. You can check the "Show status window" checkbox and hit the Save button, or you can type this:

hey E-mail set Value of View [13] of View [0] of Window [0] to 1
hey E-mail set Value of View [18] of View [0] of Window [0] to 1

After a second or two the email status window will appear on your Desktop. Be sure to turn on the Log checkbox if it's not already on! With the Log turned on, you'll be able to see what's happening when your system tries to download your email.

Now that we've found the views we want to script, we need to think about what our script is going to do. A script that can take one command-line argument (for the name of the email account), set everything up, and then check for new email should be pretty useful. We're going to need to:

  1. Check for the right number of arguments (the email account)
  2. Get settings for all of these things based on the email account
  3. Start the E-mail preferences application if it's not already running
  4. Set the fields in the E-mail preferences application
  5. Simulate a click on the Check Now button

You already know how to do all of this, believe it or not! Let's look at the script (which I've called check_mail):

#! /bin/sh
#
# Script to set up the E-mail preferences application for various
# different email accounts and check for new mail.

# Check for the right number of arguments; if there isn't one argument
# we exit with an exit status of 1 to indicate that something went
# wrong.

if [ $# -ne 1 ] ; then
echo "usage: $0 account"
exit 1
fi

# Now use a case statement to decide what settings to use. From the
# look of things, my cats have been using email again, and they've
# edited this script to make it easier...
#
# NOTE: some.net.com is a fictional ISP; you'll have to customize
# this case statement to include your accounts and settings!

# Attempt to match the first (and only) argument against the
# names of the accounts we know about.

case "$1" in
poe)
# Settings for Poe's email account.
pop_user_name="poe"
pop_password="claws"
pop_host="pop.some.net.com"
smtp_host="smtp.some.net.com"
real_name="Poe (Lord of the Carpet)"
reply_to="poe@cats.net.com"
;;

byron)
# Settings for Byron's email account.
pop_user_name="byron"
pop_password="canteloupe"
pop_host="pop.some.net.com"
smtp_host="smtp.some.net.com"
real_name="Byron (Baddest cat in the land)"
reply_to="byron@cats.net.com"
;;

meaghan)
# Settings for Meaghan's email account.
pop_user_name="meaghan"
pop_password="furball"
pop_host="pop.some.net.com"
smtp_host="smtp.some.net.com"
real_name="Meaghan the Cutie"
reply_to="meghan@cats.net.com"
;;

chris)
# Settings for Chris's email account.
pop_user_name="chrish"
pop_password="funkburg3r"
pop_host="pop.some.othernet.com"
smtp_host="pop.some.othernet.com"
real_name="Chris Herborth"
reply_to="chrish@qnx.com"
;;

lynette)
# Settings for Lynette's email account.
pop_user_name="lynette"
pop_password="semprini"
pop_host="pop.some.net.com"
smtp_host="smtp.some.net.com"
real_name="Lynette Woodward-Herborth"
reply_to="lynette@some.net.com"
;;

*)

# We didn't match any of the accounts we know
# about, so gripe at the user and exit with an
# exit status of 2 to indicate the kind of error.

echo "$1" is not a valid email account
exit 2
;;
esac

# Now we should start up the E-mail preferences app if it's not already
# running.
#
# We check to see if E-mail is already running by attempting to get
# its Title. We aren't actually interested in the title, so we
# send hey's output to /dev/null.
#
# The test in the if statement is to see if hey's exit status is
# not "all's well"; this will be the case if E-mail isn't running
# yet (because hey won't be able to get its window title).

if ! hey E-mail get Title of Window 0 > /dev/null ; then
echo Starting E-mail preferences...
/boot/preferences/E-mail &

# Just in case you've got a slow machine...this until
# loop will print the message and sleep for one second
# over and over until hey is able to get E-mail's
# window title (which means it's running). The test
# in the "until" will only be true when hey can get the
# title.
#
# Again, we don't want the output of hey (we just want its exit # status) so we send it to the bit bucket.


until hey E-mail get Title of Window 0 > /dev/null ; do
echo Sleeping for a second while E-mail starts...
sleep 1
done
fi

# E-mail is now up and running, so we can set our fields based on our
# account info.
#
# All of these hey commands are redirected to /dev/null because we
# don't really care about their output. The commands still do
# something (send a scripting message to E-mail), but by redirecting
# hey's output to /dev/null, we won't be interrupted by all of the
# reply messages that every hey command prints.
#
# The \ at the end of each line just tells the shell that we're
# not done with the command yet; it ties it together with the next line.
# This is just to make things fit nicely on the page; in your version
# of this script, you could just type it all on one line without
# the \ character.

echo Setting up email preferences for account: "$1"

# To save typing we'll use $container as shorthand:

container="View [0] of Window [0]"

hey E-mail set Value of View [0] of $container to "$pop_user_name" \
> /dev/null
hey E-mail set Value of View [1] of $container to "$pop_password" \
> /dev/null
hey E-mail set Value of View [2] of $container to "$pop_host" \
> /dev/null
hey E-mail set Value of View [3] of $container to "$smtp_host" \
> /dev/null
hey E-mail set Value of View [10] of $container to "$real_name" \
> /dev/null
hey E-mail set Value of View [11] of $container to "$reply_to" \
> /dev/null

# Now simulate a click on the Check Now button by setting the
# button's value to 1:

echo Checking for new mail...
hey E-mail set Value of View [16] of $target to 1 > /dev/null

# That's all folks! We leave with the exit status of the last
# hey command.

exit

The Python heymodule package found on BeWare includes a Python-based version of this script. It's much easier to read, and runs a lot faster!

After you run this, you can hit the Revert button and close the E-mail preferences window to restore your original settings.

Another approach to this problem would be to find where the E-mail preferences are stored and keep one preferences file for each account. Then you could swap the preferences, fire up the E-mail preferences app, and hit Check Now to get that account's email.

I could be a total freak, but I like the scripting solution better; it doesn't cause permanent changes to anything, and I don't really have to do much. I could even set up icons on my Desktop that called check_mail for one of the accounts; then I could just double-click on the icon and find out if one of my cats has any extra email lying around.

Where Now?

Now that you've learned the basics of application scripting under BeOS, the best thing for you to do is fire up a few applications and experiment. As scripting takes off, you'll be able to do more and more useful things with applications via remote control, such as asking an FTP client to automatically download all new or updated files since the last time you were online.

Until then, though, you can manipulate controls in running applications, move their windows around, hide or show windows, and do a host of other things using what you've learned in this section. Be sure to read the documentation that came with your favorite applications to see what interesting scripting commands they support, and keep an eye on BeWare's Languages section for updates to scripting languages like Python and Perl that will let you write your scripts in something other than the shell.

Don't be afraid to experiment! Until programmers document their software better, you'll need to experiment to find the views for the controls you want to script. Luckily, this is one of those things that will improve over time. If a program includes some interesting scripting abilities, be sure to thank the developer!


<< previous
^ top ^
next >>

Readers-Only Access
Scripting
Games
Emulation
Hardware and Peripherals
The Kits
The Future
About BeOS | Online Chapters | Interviews | Updates

Please direct technical questions about this site to webmaster@peachpit.com.

Copyright © 1999 Peachpit Press and the respective authors.