Copyright (c) Hyperion Entertainment and contributors.

Difference between revisions of "Writing a UserShell"

From AmigaOS Documentation Wiki
Jump to navigation Jump to search
Line 103: Line 103:
 
Don't forget to set pr_HomeDir for programs loaded from disk. The Lock in pr_HomeDir should be a DupLock() of the directory the program was loaded from. For programs from the resident list, leave it NULL.
 
Don't forget to set pr_HomeDir for programs loaded from disk. The Lock in pr_HomeDir should be a DupLock() of the directory the program was loaded from. For programs from the resident list, leave it NULL.
   
Please support multi-assigned C: directories. The important thing here is to not lock C:. Instead, prepend ``C:'' onto the filename you wish to Lock()/LoadSeg(). Also, if a command is loaded from C:, get its pr_HomeDir by Lock()ing the file (with C: prepended), and then using ParentDir() to get its home directory.
+
Please support multi-assigned C: directories. The important thing here is to not lock C:. Instead, prepend "C:" onto the filename you wish to Lock()/LoadSeg(). Also, if a command is loaded from C:, get its pr_HomeDir by Lock()ing the file (with C: prepended), and then using ParentDir() to get its home directory.
   
 
The Path is attached to cli_CommandDir. It is a BPTR to a NULL
 
The Path is attached to cli_CommandDir. It is a BPTR to a NULL

Revision as of 20:27, 19 March 2013

Introduction

The OS allows the user to change the system default shell, or the UserShell. Any time the user opens a shell with NewShell, executes a script, RUNs a command, or indirectly calls System() with SYS_UserShell, the OS will call the UserShell instead of the BootShell (by default the system sets up the BootShell as the UserShell).

Creating UserShells is not easy, and requires doing a fairly large number of things for no apparent reason (the reasons are there, they're just not obvious to the outsider). This section will attempt to give you the information you need in order to create a usable, system-friendly UserShell.

Initialization

The entity that starts the shell calls the shell code in C style (RTS to exit). This entity also sends a startup packet to your process port. You must retrieve this packet before doing any DOS I/O (much like WBMessages). You can use WaitPkt() for this. The entity will take care of attaching a CommandLineInterface structure to your process, which will be freed on exit from the UserShell by the system.

In your process structure, check the SegArray pointed to by pr_Seglist (note that it's a BPTR). If SegArray[4] is NULL, you must move the value from SegArray[3] to SegArray[4], and NULL out SegArray[3]. This is because SegArray[3] will be used to store the seglist pointer for each program you run.

The startup packet contains some information that tells the UserShell what kind of shell to be. At present, the two sets of sources can launch the UserShell:

  1. The Run command, Execute(), or System()
  2. The NewShell or NewCLI resident commands

The size of the stack that the system gives the UserShell depends on how the user started the shell. If it was started from Execute() or System(), the stack is 3200 bytes. If the UserShell was started from Run, NewShell, or NewCLI, the stack is 4000.

The type of shell required is specified by the combination of the packet's dp_Res1 and dp_Res2 fields. Here's a piece of code for turning them into a value from 0 to 3:

init_type = (parm_pkt->dp_Res1 == 0 ? 0:2)|(parm_pkt->dp_Res2 == 0 ? 0:1);

Currently, only types 0 and 2 are implemented. For 1 and 3 you should exit with an error (returning the packet). Type 0 is for Run, Execute() and System(), type 2 is for NewShell and NewCLI. After setting up your SegArray as above, for type 0 call CliInitRun(pkt), and for type 2 call CliInitNewcli(pkt). These both return a value we'll call ``fn. Keep fn around, it has useful state information that you'll need later. Note that these CliInitXxxx functions don't follow the normal DOS convention of Dn for arguments (they use A0 for pkt!).

The CliInitXxxx functions do many magic things to get all the streams and structures properly set up, etc. You shouldn't need to know anything about this or what the values in the packet are, other than dp_Res1 and dp_Res2 (see below for more information on these functions).

Definitions for the values of fn:
Bit 31 Set to indicate flags are valid
Bit 3 Set to indicate an asynchronous System() call
Bit 2 Set if this is a System() call
Bit 1 Set if user provided input stream
Bit 0 Set if RUN provided output stream

If fn bit 31 is 0 (fn >= 0), then you must check IoErr() to find out what to do. If IoErr() is a pointer to your process, there has been an error in the initialization of the CLI structure and processing the packet. In this case you should clean up and exit. You don't have to return the packet because the CliInitXxxx functions take care of this for you if there is an error. If IoErr() isn't a pointer to your process, then if this is a NewCLI or NewShell command (init_type of 2), reply the packet immediately.

If the init_type is 0, you have to look at fn to determine when to send back the startup packet. If the shell was called from an asynchronous System() function ((fn & 0x8000000C) == 0x8000000C), return the packet immediately. If this is a synchronous System() call ((fn & 0x8000000C) == 0x80000004) or the fn flags are valid but this is not a System() call ((fn & 0x8000000C) == 0x80000000) (Execute() does this), you return the packet just before exiting from your shell (see the Cleanup section below). If the fn flags are invalid (bit 31 == 0), but there is something other than your task pointer in IoErr(), then this shell was called by the Run command. Here you can either return the packet immediately, or return it after having loaded the first command (or failed to find/load it). This delay in reply helps avoid the disk thrashing caused by two commands loading at the same instant.

When you do a ReplyPkt(), use ReplyPkt(pkt, pkt->dp_Res1, pkt->dp_Res2) to avoid losing error codes set up by CliInitXxxx.

Initialize pr_HomeDir to NULL, set up any local shell variables, etc.

We're all set up now, so you can now enter your main loop and start taking commands.

A Word About the Shell's I/O Handles

There are three pairs of I/O handles in a shell process. The shell's Process structure contains the pr_CIS (current input stream) and pr_COS (current output stream) file handles. That Process's CommandLineInterface structure contains the other two pairs of I/O handles: cli_StandardInput/cli_StandardOutput and cli_CurrentInput/cli_CurrentOutput. Each has different uses within a normal shell.

Routines that operate on Input() or Output(), such as ReadArgs() or ReadItem(), use the pr_CIS and pr_COS I/O handles (which they acquire by calling the dos.library routines Input() and Output(), not by directly looking at the Process structure). Shell-launched application programs the run on the shell's process also use these I/O handles as their normal input and output channels. This is where functions like scanf() and printf() get and send their input and output. The shell changes these file handles (using SelectInput()/SelectOutput()) according to the shell defaults and according to any I/O redirection.

The cli_StandardInput and cli_StandardOutput I/O handles are the default input and output channels for the shell. They usually refer to the user's console window and will not change while the shell is running. The shell should use these values as the default values for pr_CIS and pr_COS (via SelectInput() and SelectOutput()) when it runs a command from a command line.

The cli_CurrentInput handle is the current source of command lines. This normally is the same as cli_StandardInput. The cli_CurrentInput handle will differ from cli_StandardInput when the shell is executing a script or when handling an Execute() or System() call. In these cases, it points to a file handle from which the shell is reading commands. This handle refers to one of three files: the script file you called with the execute command, a temporary file created by the execute command, or a pseudo file created by Execute() or System().

When a shell runs the execute command, If cli_CurrentInput differs from cli_StandardInput, The execute command will close cli_CurrentInput and replace it with a new one, so don't cache the value of cli_CurrentInput as it will be invalid. In this case, cli_CurrentInput must not be the same as pr_CIS when you call RunCommand() if the executable could possible be the execute commands (or anything else that tries to close cli_CurrentInput).

The cli_CurrentOutput file handle is currently unused by the system. It's initialized to the same as cli_StandardOutput.

The Main Shell Loop

Note
Some things in this section assume your UserShell will act similarly to the Boot Shell. If not, modify to see fit, but pay close attention to things external programs will notice (such as the setup of the process and CLI structures). In particular, the article assumes that you handle scripts by redirecting cli_CurrentInput to a file with the script in it, as the execute command does. Note that the execute command will attempt to do this if you run it--be careful.

Before reading a command line, you need to SelectInput() on the I/O handle in the current cli_CurrentInput, and SelectOutput() on cli_StandardOutput. This makes sure the shell is reading from its command line source and writing to the the default output handle.

If this shell is executing a script, you should check if the user hit the break key for scripts (Ctrl-D is what the BootShell uses). If you do detect a script break, you can print an error message to the current output stream by calling PrintFault(304, "<your shell name>"). 304 is the error number (ERROR_BREAK) and the string gets prepended to the error message (which is currently " :***Break"). This allows the OS to print the error message using the standard error message which can be internationalized in future versions of the OS.

Next, determine if you should print a prompt. The nasty statement below sets up the Interactive flag for you, by setting it if the following are true:

This shell is not a background shell input has not been redirected to an Execute/script file this is not a System() call

You don't have to handle it precisely this way, but this works (Note: 0x80000004 is a test for whether this is a System() call, see the ``fn bit definitions above).

#define SYSTEM     ((((LONG)fn) & 0x80000004) == 0x80000004)
#define NOTSCRIPT  (clip->cli_CurrentInput == clip->cli_StandardInput)
 
clip->cli_Interactive = (!clip->cli_Background && NOTSCRIPT && !SYSTEM) ? DOSTRUE : FALSE;

The BootShell prints a prompt if cli_Interactive is DOSTRUE.

Do all your mucking with the input line, alias and local variable expansion, etc.

Finding a Program

There are several possible places a shell can look for commands passed to it. The resident list is an important place to look as it contains many commands that the user finds important enough to keep loaded into memory at all times. Some shells have commands built directly into them. Of course, if the shell cannot find a command in the resident list or in its collection of internal commands, the shell has to scan the path looking for the command. If a shell supports the script bit, when it finds a command on disk with the script bit set, it should read commands from the script file.

Here's how you deal with commands on the resident list: After finding the command (under Forbid()), if the Segment structure's seg_UC is >= 0, increment it; if less than 0, don't modify it. If seg_UC is less than CMD_DISABLED, the corresponding resident command is currently disabled and you should not execute it. The same is true if seg_UC is equal to CMD_SYSTEM. After incrementing seg_UC, you can Permit(). After using a resident command, decrement the seg_UC count if it's greater than 0 (under Forbid() again).

When identifying scripts, I advise that you use something unique to identify your scripts, and pass all other scripts to the Boot Shell via System() for execution. A good method is to include within the first 256 characters or so, the string "#!<your shell name, ala csh>!#". BootShells could, for example, start with "; #!c:execute!#. The idea is the string inside the #!...!# should be the interpreter to run on the script. If none is specified, give it to the BootShell. If you want, you could extend this to include handling of the sequence for all interpreters. The programs should be invoked as "<interpreter> <filename> <args>" as if the user had typed that.

Don't forget to set pr_HomeDir for programs loaded from disk. The Lock in pr_HomeDir should be a DupLock() of the directory the program was loaded from. For programs from the resident list, leave it NULL.

Please support multi-assigned C: directories. The important thing here is to not lock C:. Instead, prepend "C:" onto the filename you wish to Lock()/LoadSeg(). Also, if a command is loaded from C:, get its pr_HomeDir by Lock()ing the file (with C: prepended), and then using ParentDir() to get its home directory.

The Path is attached to cli_CommandDir. It is a BPTR to a NULL terminated, singly-linked list (connected via BPTRs) of directory Locks:

struct pathBPTRlistentry {
    BPTR   pathBPTRlistentry *next;
    struct Lock              directorylock
}

Please don't modify the list; use the Path command instead. This will make it far easier for us to improve this in the future.

Make sure you clear the SIGBREAK_CTRL_x signals before starting a program. In order to prevent the user from hitting a break key somewhere between where you check for the break and where you clear the signals (thus losing the break signal), you may wish check for a break and clear the signals at the same time. The safest way is to use:

uint32 oldsigs = SetSignal(0, SIGBREAK_CTRL_C |
                              SIGBREAK_CTRL_D |
                              SIGBREAK_CTRL_E |
                              SIGBREAK_CTRL_F);

Then you can check oldsigs for any signals that you care about.

Running a Program

Cleanup

Installing the New User Shell

Example