/*++ Copyright (c) 1999 Intel Corporation Module Name: batch.c Abstract: Functions implementing batch scripting in the shell. Revision History --*/ #include "shelle.h" /* * Constants */ #define ASCII_LF ((CHAR8)0x0a) #define ASCII_CR ((CHAR8)0x0d) #define UNICODE_LF ((CHAR16)0x000a) #define UNICODE_CR ((CHAR16)0x000d) /* Can hold 64-bit hex error numbers + null char */ #define LASTERROR_BUFSIZ (17) /* * Statics * (needed to maintain state across multiple calls or for callbacks) */ STATIC UINTN NestLevel; STATIC UINTN LastError; STATIC CHAR16 LastErrorBuf[LASTERROR_BUFSIZ]; STATIC BOOLEAN Condition; STATIC BOOLEAN GotoIsActive; STATIC UINT64 GotoFilePos; STATIC BOOLEAN BatchIsActive; STATIC BOOLEAN EchoIsOn; STATIC BOOLEAN BatchAbort; STATIC SIMPLE_INPUT_INTERFACE *OrigConIn; STATIC SIMPLE_TEXT_OUTPUT_INTERFACE *OrigConOut; STATIC EFI_FILE_HANDLE CurrentBatchFile; /* * Definitions for the argument list stack * * In order to support nested scripts (script calling script calling script...) * there is an argument list stack "BatchInfoStack". BatchInfoStack is a * list of argument lists. Each argument list contains Argv[0] - Argv[n] * for the corresponding script file. The head of BatchInfoStack corresponds * to the currently active script file. * * This allows positional argument substitution to be done when each line * is read and scanned, and calls to other script files can overwrite the * shell interface's argument list. */ #define EFI_BATCH_INFO_SIGNATURE EFI_SIGNATURE_32('b','i','r','g') typedef struct { UINTN Signature; LIST_ENTRY Link; CHAR16 *ArgValue; } EFI_SHELL_BATCH_INFO; #define EFI_BATCH_INFOLIST_SIGNATURE EFI_SIGNATURE_32('b','l','s','t') typedef struct { UINTN Signature; LIST_ENTRY Link; LIST_ENTRY ArgListHead; /* Head of this argument list */ UINT64 FilePosition; /* Current file position */ } EFI_SHELL_BATCH_INFOLIST; STATIC LIST_ENTRY BatchInfoStack; /* * Prototypes */ STATIC EFI_STATUS BatchIsAscii( IN EFI_FILE_HANDLE File, OUT BOOLEAN *IsAscii ); STATIC EFI_STATUS BatchGetLine( IN EFI_FILE_HANDLE File, IN BOOLEAN Ascii, IN OUT UINT64 *FilePosition, IN OUT UINTN *BufSize, OUT CHAR16 *CommandLine ); VOID SEnvInitBatch( VOID ) /*++ Function Name: SEnvInitBatch Description: Initializes global variables used for batch file processing. --*/ { NestLevel = 0; LastError = EFI_SUCCESS; ZeroMem( LastErrorBuf, LASTERROR_BUFSIZ ); Condition = TRUE; GotoIsActive = FALSE; GotoFilePos = (UINT64)0x00; BatchIsActive = FALSE; EchoIsOn = TRUE; BatchAbort = FALSE; OrigConIn = ST->ConIn; OrigConOut = ST->ConOut; InitializeListHead( &BatchInfoStack ); SEnvInitForLoopInfo(); } BOOLEAN SEnvBatchIsActive( VOID ) /*++ Function Name: SEnvBatchIsActive Description: Returns whether any batch files are currently being processed. --*/ { /* * BUGBUG should be able to return IsListEmpty( &BatchInfoStack ); * instead of using this variable */ return BatchIsActive; } VOID SEnvSetBatchAbort( VOID ) /*++ Function Name: SEnvSetBatchAbort Description: Sets a flag to notify the main batch processing loop to exit. --*/ { BatchAbort = TRUE; return; } VOID SEnvBatchGetConsole( OUT SIMPLE_INPUT_INTERFACE **ConIn, OUT SIMPLE_TEXT_OUTPUT_INTERFACE **ConOut ) /*++ Function Name: SEnvBatchGetConsole Description: Returns the Console I/O interface pointers. --*/ { *ConIn = OrigConIn; *ConOut = OrigConOut; return; } EFI_STATUS SEnvBatchEchoCommand( IN ENV_SHELL_INTERFACE *Shell ) /*++ Function Name: SEnvBatchEchoCommand Description: Echoes the given command to stdout. --*/ { UINTN i; CHAR16 *BatchFileName; EFI_STATUS Status; /* * Echo the parsed-and-expanded command to the console */ if ( SEnvBatchIsActive() && EchoIsOn ) { BatchFileName = NULL; Status = SEnvBatchGetArg( 0, &BatchFileName ); if ( EFI_ERROR(Status) ) { goto Done; } Print( L"%E" ); for ( i=0; i ", BatchFileName ); for ( i=0; iShellInt.Argc; i++ ) { Print( L"%s ", Shell->ShellInt.Argv[i] ); } for ( i=0; iShellInt.RedirArgc; i++ ) { Print( L"%s ", Shell->ShellInt.RedirArgv[i] ); } Print( L"\n" ); } Done: /* * Switch output attribute to normal */ Print (L"%N"); return Status; } VOID SEnvBatchSetEcho( IN BOOLEAN Val ) /*++ Function Name: SEnvBatchSetEcho Description: Sets the echo flag to the specified value. --*/ { EchoIsOn = Val; return; } BOOLEAN SEnvBatchGetEcho( VOID ) /*++ Function Name: SEnvBatchGetEcho Description: Returns the echo flag. --*/ { return EchoIsOn; } EFI_STATUS SEnvBatchSetFilePos( IN UINT64 NewPos ) /*++ Function Name: SEnvBatchSetFilePos Description: Sets the current script file position to the specified value. --*/ { EFI_STATUS Status = EFI_SUCCESS; EFI_SHELL_BATCH_INFOLIST *BatchInfo = NULL; Status = CurrentBatchFile->SetPosition( CurrentBatchFile, NewPos ); if ( EFI_ERROR(Status) ) { goto Done; } if ( !IsListEmpty( &BatchInfoStack ) ) { BatchInfo = CR( BatchInfoStack.Flink, EFI_SHELL_BATCH_INFOLIST, Link, EFI_BATCH_INFOLIST_SIGNATURE ); } if ( BatchInfo ) { BatchInfo->FilePosition = NewPos; } else { Status = EFI_NOT_FOUND; goto Done; } Done: return Status; } EFI_STATUS SEnvBatchGetFilePos( UINT64 *FilePos ) /*++ Function Name: SEnvBatchGetFilePos Description: Returns the current script file position. --*/ { EFI_SHELL_BATCH_INFOLIST *BatchInfo = NULL; EFI_STATUS Status = EFI_SUCCESS; if ( !FilePos ) { Status = EFI_INVALID_PARAMETER; goto Done; } if ( !IsListEmpty( &BatchInfoStack ) ) { BatchInfo = CR( BatchInfoStack.Flink, EFI_SHELL_BATCH_INFOLIST, Link, EFI_BATCH_INFOLIST_SIGNATURE ); } if ( BatchInfo ) { *FilePos = BatchInfo->FilePosition; } else { Status = EFI_NOT_FOUND; goto Done; } Done: return Status; } VOID SEnvBatchSetCondition( IN BOOLEAN Val ) /*++ Function Name: SEnvBatchSetCondition Description: Sets the condition flag to the specified value. --*/ { Condition = Val; return; } VOID SEnvBatchSetGotoActive( VOID ) /*++ Function Name: SEnvBatchSetGotoActive Description: Sets the goto-is-active to TRUE and saves the current position of the active script file. --*/ { GotoIsActive = TRUE; SEnvBatchGetFilePos( &GotoFilePos ); return; } BOOLEAN SEnvBatchVarIsLastError( IN CHAR16 *Name ) /*++ Function Name: SEnvBatchVarIsLastError Description: Checks to see if variable's name is "lasterror". --*/ { return (StriCmp( L"lasterror", Name ) == 0); } VOID SEnvBatchSetLastError( IN UINTN NewLastError ) /*++ Function Name: SEnvBatchSetLastError Description: Sets the lasterror variable's value to the given value. --*/ { LastError = NewLastError; return; } CHAR16* SEnvBatchGetLastError( VOID ) /*++ Function Name: SEnvBatchGetLastError Description: Returns a pointer to a string representation of the error value returned by the last shell command. --*/ { ValueToHex( LastErrorBuf, (UINT64)LastError ); return LastErrorBuf; } STATIC EFI_STATUS BatchIsAscii( IN EFI_FILE_HANDLE File, OUT BOOLEAN *IsAscii ) /*++ Function Name: BatchIsAscii Description: Checks to see if the specified batch file is ASCII. --*/ { EFI_STATUS Status=EFI_SUCCESS; CHAR8 Buffer8[2]; /* UNICODE byte-order-mark is two bytes */ UINTN BufSize; /* * Read the first two bytes to check for byte order mark */ BufSize = sizeof(Buffer8); Status = File->Read( File, &BufSize, Buffer8 ); if ( EFI_ERROR(Status) ) { goto Done; } Status = File->SetPosition( File, (UINT64)0 ); if ( EFI_ERROR(Status) ) { goto Done; } /* * If we find a UNICODE byte order mark assume it is UNICODE, * otherwise assume it is ASCII. UNICODE byte order mark on * IA little endian is first byte 0xff and second byte 0xfe */ if ( (Buffer8[0] | (Buffer8[1] << 8)) == UNICODE_BYTE_ORDER_MARK ) { *IsAscii = FALSE; } else { *IsAscii = TRUE; } Done: return Status; } STATIC EFI_STATUS BatchGetLine( IN EFI_FILE_HANDLE File, IN BOOLEAN Ascii, IN OUT UINT64 *FilePosition, IN OUT UINTN *BufSize, OUT CHAR16 *CommandLine ) /*++ Function Name: BatchGetLine Description: Reads the next line from the batch file, converting it from ASCII to UNICODE if necessary. If end of file is encountered then it returns 0 in the BufSize parameter. --*/ { EFI_STATUS Status; CHAR8 Buffer8[MAX_CMDLINE]; CHAR16 Buffer16[MAX_CMDLINE]; UINTN i = 0; UINTN CmdLenInChars = 0; UINTN CmdLenInBytes = 0; UINTN CharSize = 0; /* * Check params */ if ( !CommandLine || !BufSize || !FilePosition ) { Status = EFI_INVALID_PARAMETER; goto Done; } /* * Initialize OUT param */ ZeroMem( CommandLine, MAX_CMDLINE ); /* * If beginning of UNICODE file, move past the Byte-Order-Mark (2 bytes) */ if ( !Ascii && *FilePosition == (UINT64)0 ) { *FilePosition = (UINT64)2; Status = File->SetPosition( File, *FilePosition ); if ( EFI_ERROR(Status) ) { goto Done; } } /* * (1) Read a buffer-full from the file * (2) Locate the end of the 1st line in the buffer * ASCII version and UNICODE version */ if ( Ascii ) { CharSize = sizeof(CHAR8); Status = File->Read( File, BufSize, Buffer8 ); if ( EFI_ERROR(Status) || *BufSize == 0 ) { goto Done; } for ( i=0; i<*BufSize; i++ ) { if ( Buffer8[i] == ASCII_LF ) { CmdLenInChars = i; CmdLenInBytes = CmdLenInChars; break; } } } else { /* UNICODE */ CharSize = sizeof(CHAR16); Status = File->Read( File, BufSize, Buffer16 ); if ( EFI_ERROR(Status) || *BufSize == 0 ) { goto Done; } for ( i=0; i < *BufSize/CharSize; i++ ) { if ( Buffer16[i] == UNICODE_LF ) { CmdLenInChars = i; CmdLenInBytes = CmdLenInChars * CharSize; break; } } } /* * Reset the file position to just after the command line */ *FilePosition += (UINT64)(CmdLenInBytes + CharSize); Status = File->SetPosition( File, *FilePosition ); /* * Copy, converting chars to UNICODE if necessary */ if ( Ascii ) { for ( i=0; iArgListHead ) ) { for ( Link=BatchInfo->ArgListHead.Flink; Link!=&BatchInfo->ArgListHead; Link=Link->Flink) { ArgEntry = CR( Link, EFI_SHELL_BATCH_INFO, Link, EFI_BATCH_INFO_SIGNATURE); if ( i++ == Argno ) { *Argval = ArgEntry->ArgValue; return EFI_SUCCESS; } } } *Argval = NULL; return EFI_NOT_FOUND; } EFI_STATUS SEnvExecuteScript( IN ENV_SHELL_INTERFACE *Shell, IN EFI_FILE_HANDLE File ) /*++ Function Name: SEnvExecuteScript Description: Execute the commands in the script file specified by the file parameter. Arguments: Shell: shell interface of the caller File: file handle to open script file Returns: EFI_STATUS --*/ { EFI_FILE_INFO *FileInfo; UINTN FileNameLen = 0; BOOLEAN EndOfFile = FALSE; EFI_STATUS Status = EFI_SUCCESS; UINTN BufSize = 0; UINTN FileInfoSize = 0; CHAR16 CommandLine[MAX_CMDLINE]; EFI_SHELL_BATCH_INFOLIST *BatchInfo = NULL; EFI_SHELL_BATCH_INFO *ArgEntry = NULL; UINTN i = 0; BOOLEAN Output = TRUE; ENV_SHELL_INTERFACE NewShell; UINTN GotoTargetStatus; UINTN SkippedIfCount; /* * Initialize */ BatchIsActive = TRUE; Status = EFI_SUCCESS; NestLevel++; SEnvInitTargetLabel(); /* * Check params */ if ( !File ) { Status = EFI_INVALID_PARAMETER; goto Done; } /* * Figure out if the file is ASCII or UNICODE. */ Status = BatchIsAscii( File, &Shell->StdIn.Ascii ); if ( EFI_ERROR( Status ) ) { goto Done; } /* * Get the filename from the file handle. */ /* * Allocate buffer for file info (including file name) * BUGBUG 1024 arbitrary space for filename, as elsewhere in shell */ FileInfoSize = SIZE_OF_EFI_FILE_INFO + 1024; FileInfo = AllocatePool(FileInfoSize); if (!FileInfo) { Status = EFI_OUT_OF_RESOURCES; goto Done; } /* Get file info */ Status = File->GetInfo( File, &GenericFileInfo, &FileInfoSize, FileInfo ); if ( EFI_ERROR(Status) ) { return Status; } /* * Save the handle */ CurrentBatchFile = File; /* * Initialize argument list for this script * This list is needed since nested batch files would overwrite the * argument list in Shell->ShellInt.Argv[]. Here we maintain the args in * a local list on the stack. */ BatchInfo = AllocateZeroPool( sizeof( EFI_SHELL_BATCH_INFOLIST ) ); if ( !BatchInfo ) { Status = EFI_OUT_OF_RESOURCES; goto Done; } BatchInfo->Signature = EFI_BATCH_INFOLIST_SIGNATURE; BatchInfo->FilePosition = (UINT64)0x00; InitializeListHead( &BatchInfo->ArgListHead ); for ( i=0; iShellInt.Argc; i++ ) { /* Allocate the new element of the argument list */ ArgEntry = AllocateZeroPool( sizeof( EFI_SHELL_BATCH_INFO ) ); if ( !ArgEntry ) { Status = EFI_OUT_OF_RESOURCES; goto Done; } /* Allocate space for the argument string in the arglist element */ ArgEntry->ArgValue = AllocateZeroPool(StrSize(Shell->ShellInt.Argv[i])); if ( !ArgEntry->ArgValue ) { Status = EFI_OUT_OF_RESOURCES; goto Done; } /* Copy in the argument string */ StrCpy( ArgEntry->ArgValue, Shell->ShellInt.Argv[i] ); ArgEntry->Signature = EFI_BATCH_INFO_SIGNATURE; /* Add the arglist element to the end of the list */ InsertTailList( &BatchInfo->ArgListHead, &ArgEntry->Link ); } /* Push the arglist onto the arglist stack */ InsertHeadList( &BatchInfoStack, &BatchInfo->Link ); /* * Iterate through the file, reading a line at a time and executing each * line as a shell command. Nested shell scripts will come through * this code path recursively. */ EndOfFile = FALSE; SkippedIfCount = 0; while (1) { /* * Read a command line from the file */ BufSize = MAX_CMDLINE; Status = BatchGetLine( File, Shell->StdIn.Ascii, &BatchInfo->FilePosition, &BufSize, CommandLine ); if ( EFI_ERROR( Status ) ) { goto Done; } /* * No error and no chars means EOF * If we are in the middle of a GOTO then rewind to search for the * label from the beginning of the file, otherwise we are done * with this script. */ if ( BufSize == 0 ) { if ( GotoIsActive ) { BatchInfo->FilePosition = (UINT64)(0x00); Status = File->SetPosition( File, BatchInfo->FilePosition ); if ( EFI_ERROR( Status ) ) { goto Done; } else { continue; } } else { goto Done; } } /* * Convert the command line to an arg list */ ZeroMem( &NewShell, sizeof(NewShell ) ); Status = SEnvStringToArg( CommandLine, TRUE, &NewShell.ShellInt.Argv, &NewShell.ShellInt.Argc ); if (EFI_ERROR(Status)) { goto Done; } /* * Skip comments and blank lines */ if ( NewShell.ShellInt.Argc == 0 ) { continue; } /* * If a GOTO command is active, skip everything until we find * the target label or until we determine it doesn't exist. */ if ( GotoIsActive ) { /* * Check if we have the right label or if we've searched * the whole file */ Status = SEnvCheckForGotoTarget( NewShell.ShellInt.Argv[0], GotoFilePos, BatchInfo->FilePosition, &GotoTargetStatus ); if ( EFI_ERROR( Status ) ) { goto Done; } switch ( GotoTargetStatus ) { case GOTO_TARGET_FOUND: GotoIsActive = FALSE; SEnvFreeTargetLabel(); continue; case GOTO_TARGET_NOT_FOUND: continue; case GOTO_TARGET_DOESNT_EXIST: GotoIsActive = FALSE; Status = EFI_INVALID_PARAMETER; LastError = Status; SEnvPrintLabelNotFound(); SEnvFreeTargetLabel(); continue; default: Status = EFI_INVALID_PARAMETER; SEnvFreeTargetLabel(); Print( L"Internal error: invalid GotoTargetStatus\n" ); break; } } else if ( NewShell.ShellInt.Argv[0][0] == L':' ) { /* * Skip labels when no GOTO is active */ continue; } /* * Skip everything between an 'if' whose condition was false and its * matching 'endif'. Note that 'endif' doesn't do anything if * Condition is TRUE, so we only track endif matching when it is false. */ if ( !Condition ) { if ( StriCmp( NewShell.ShellInt.Argv[0], L"if") == 0 ) { /* * Keep track of how many endifs we have to skip before we are * done with the FALSE Condition */ SkippedIfCount += 1; continue; } else if ( StriCmp( NewShell.ShellInt.Argv[0], L"endif") == 0 ) { if ( SkippedIfCount > 0 ) { SkippedIfCount -= 1; continue; } /* * When SkippedIfCount goes to zero (as here), we have the * endif that matches the if with the FALSE condition that * we are dealing with, so we want to fall through and have * the endif command reset the Condition flag. */ } else { /* * Condition FALSE, not an if or an endif, so skip */ continue; } } /* * Execute the command */ LastError = SEnvDoExecute( Shell->ShellInt.ImageHandle, CommandLine, &NewShell, TRUE ); /* * Save the current file handle */ CurrentBatchFile = File; if ( BatchAbort ) { goto Done; } } Done: /* * Clean up */ /* Decrement the count of open script files */ NestLevel--; /* Free any potential remaining GOTO target label */ SEnvFreeTargetLabel(); /* Reset the IF condition to TRUE, even if no ENDIF was found */ SEnvBatchSetCondition( TRUE ); /* Close the script file */ if ( File ) { File->Close( File ); } /* Free the file info structure used to get the file name from the handle */ if ( FileInfo ) { FreePool( FileInfo ); FileInfo = NULL; } /* Pop the argument list for this script off the stack */ if ( !IsListEmpty( &BatchInfoStack ) ) { BatchInfo = CR( BatchInfoStack.Flink, EFI_SHELL_BATCH_INFOLIST, Link, EFI_BATCH_INFOLIST_SIGNATURE ); RemoveEntryList( &BatchInfo->Link ); } /* Free the argument list for this script file */ while ( !IsListEmpty( &BatchInfo->ArgListHead ) ) { ArgEntry = CR( BatchInfo->ArgListHead.Flink, EFI_SHELL_BATCH_INFO, Link, EFI_BATCH_INFO_SIGNATURE ); if ( ArgEntry ) { RemoveEntryList( &ArgEntry->Link ); if ( ArgEntry->ArgValue ) { FreePool( ArgEntry->ArgValue ); ArgEntry->ArgValue = NULL; } FreePool( ArgEntry ); ArgEntry = NULL; } } FreePool( BatchInfo ); BatchInfo = NULL; /* * If we are returning to the interactive shell, then reset * the batch-is-active flag */ if ( IsListEmpty( &BatchInfoStack ) ) { BatchIsActive = FALSE; BatchAbort = FALSE; } return Status; }