├── .clasp.json ├── .editorconfig ├── .gitignore ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── src ├── Code.ts ├── appsscript.json └── gas-terminal │ ├── 0 │ └── SheetBase.ts │ ├── CommandDefinition.ts │ ├── CommandsSheet.ts │ ├── CommonUtils.ts │ ├── FileUtils.ts │ ├── LogUtils.ts │ ├── LongRun.ts │ ├── NumberUtils.ts │ ├── SettingsConst.ts │ ├── SettingsSheet.ts │ ├── StringUtils.ts │ ├── TerminalController.ts │ └── TerminalSheet.ts └── tsconfig.json /.clasp.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptId":"1d1Xg6DaKAQKBDxk6AkdE7To4xjOnWemOwxsEAxMsFqDiUi8klh9xVvDe", 3 | "rootDir": "./src" 4 | } 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /node_modules/ 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 Inclu Cat 2 | Released under the MIT license 3 | https://opensource.org/licenses/mit-license.php 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GAS-Terminal 2 | A spreadsheet that can easily execute functions (called *commands*) created with Google Apps Script. It also supports **long-running** scripts. 3 | 4 | The original spreadsheet is [here](https://docs.google.com/spreadsheets/d/1Xh0-dhsBwWjOrEbtoctB91ffzWyM3MuaiCaNloEuvrY). 5 | 6 | Related article is [here](https://inclucat.wordpress.com/2021/07/07/gas-terminal-environment-for-smart-execution-of-google-apps-script/). 7 | 8 | And an article about long-running support is [here](https://inclucat.wordpress.com/2021/07/20/an-easy-way-to-deal-with-google-apps-scripts-6-minute-limit/). 9 | 10 | ![image](https://user-images.githubusercontent.com/82203087/124220302-a0f51580-db38-11eb-9d61-05f337308221.png) 11 | 12 | 13 | ## What is this? 14 | This spreadsheet will help you in the following ways. 15 | * it makes your functions manageable and easy to understand. 16 | * it allows you to modify execution parameters and to run the functions easily. 17 | * it allows you to check the execution result log easily. 18 | 19 | ## How to use 20 | ### 1. Copy the original spreadsheet (only once). 21 | Open [this spreadsheat](https://docs.google.com/spreadsheets/d/1Xh0-dhsBwWjOrEbtoctB91ffzWyM3MuaiCaNloEuvrY), click [File]-[Make a copy], and save it to your Google Drive with a name of your choice. (You must be signed in with a Google account.) 22 | 23 | ![image](https://user-images.githubusercontent.com/82203087/124213256-38ec0280-db2b-11eb-8733-f60eb0cf9676.png) 24 | 25 | ### 2. Open the Script editor 26 | Open the new spreadsheet, click [Extensions]-[Apps Script]. 27 | 28 | ![image](https://user-images.githubusercontent.com/82203087/151940229-ce5d7f61-0096-4613-bffc-ce1dcc44baac.png) 29 | 30 | ### 3. Write your code. 31 | In the Script editor, you can write any function you need. You can write it in any source file you like (or in a new file, of course), but do not edit the group of files starting with `gas-terminal`. 32 | 33 | ![image](https://user-images.githubusercontent.com/82203087/124213916-5c637d00-db2c-11eb-9777-7780bd712df4.png) 34 | 35 | **Tips** 36 | * To output the log to the result area, use the LogUtils class. 37 | * A function can have up to five arguments. (All parameters are of type string) 38 | * If you want to implement a function that runs for a long time, use the LongRun class. (See the bottom of this README) 39 | 40 | ### 4. Write the definition of the command 41 | In the Commands sheet, write the command definition that corresponds to the function you wrote. 42 | 43 | ![image](https://user-images.githubusercontent.com/82203087/125890198-700d87f4-183d-4238-99a9-c0bc8ec372d0.png) 44 | 45 | ### 5. Execute the command 46 | In the Terminal sheet, select the command, input parameters, then click the Execute button. 47 | 48 | ![image](https://user-images.githubusercontent.com/82203087/124215772-adc13b80-db2f-11eb-8e8b-5b8349e53cbb.png) 49 | 50 | 51 | ### 6. Authorize the script to run (only once) 52 | The first time you run it, you will see a dialog asking for permission to run the script. Follow the steps below to allow the script to run. 53 | * Click `Continue` 54 | ![image](https://user-images.githubusercontent.com/82203087/124216139-7901b400-db30-11eb-8779-64ee5d08b5e5.png) 55 | 56 | * Select your account (Perhaps it will be a different procedure for signing in.) 57 | ![image](https://user-images.githubusercontent.com/82203087/124216263-ced65c00-db30-11eb-9ca8-b76ed62b9d9a.png) 58 | 59 | * Click `Advanced`. 60 | ![image](https://user-images.githubusercontent.com/82203087/124218572-72297000-db35-11eb-8415-7fea148d679a.png) 61 | 62 | * Click `Go to GAS-Terminal (unsafe)`. 63 | ![image](https://user-images.githubusercontent.com/82203087/124218687-b4eb4800-db35-11eb-8e64-64ffe67d2911.png) 64 | 65 | * Click `Allow`. 66 | ![image](https://user-images.githubusercontent.com/82203087/124218833-fe3b9780-db35-11eb-8aa7-21949c756da3.png) 67 | 68 | * Then click Execute button again. 69 | 70 | **Don't worry about the word "unsafe". It's a dialog that all of us face when running our personal scripts.👍** 71 | 72 | 73 | ### 7. Click `Yes` and check the result (as shown at the top of this page). 74 | ![image](https://user-images.githubusercontent.com/82203087/124218975-4c509b00-db36-11eb-9e84-7b4b5f6e425e.png) 75 | 76 | 77 | ## If you want to modify this tool by using clasp and TypeScript 78 | 1. Follow the instructions above to copy the original spreadsheet. 79 | 2. Open the Script editor, click `Project Settings`, copy the `Script ID`. (We'll use it later) 80 | 3. If you have not installed `clasp` yet, install `clasp`, following [the official page](https://github.com/google/clasp). 81 | 4. Do the command `clasp login`, and login with your account. (If you have not done yet) 82 | 5. Clone this repository, and open the cloned project with your IDE. 83 | 6. Open `.clasp.json`, and replace the `scriptId` with your `Script ID` copied above. 84 | 7. You can make any changes you like to the cloned code. 85 | 8. Then do the command `clasp push`, and it will replace your copied spreadsheet's Apps Script with your code. 86 | 87 | ## Support for long-running scripts 88 | Google Apps Script has the 6 minute execution time limit. This problem is very tricky. 89 | You can solve this problem by using the GAS-Terminal's class `LongRun`. 90 | Please see the sample function named `LongRunTest` in [`Code.ts`](https://github.com/inclu-cat/GAS-Terminal/blob/main/src/Code.ts). You also can run the sample command by select the command named `Long-Running Test (Sample)` and can execute it. 91 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "GAS-Terminal", 3 | "lockfileVersion": 2, 4 | "requires": true, 5 | "packages": { 6 | "": { 7 | "dependencies": { 8 | "@types/google-apps-script": "^1.0.33" 9 | } 10 | }, 11 | "node_modules/@types/google-apps-script": { 12 | "version": "1.0.33", 13 | "resolved": "https://registry.npmjs.org/@types/google-apps-script/-/google-apps-script-1.0.33.tgz", 14 | "integrity": "sha512-9UxTHvxHwAOINtbKWAz0gn7WL41m3YwGQNpTrYERyqwQw+cBnKi4Dl3f5AtsA9o6XyhsIvrIiOvDR8O6A8SVjA==" 15 | } 16 | }, 17 | "dependencies": { 18 | "@types/google-apps-script": { 19 | "version": "1.0.33", 20 | "resolved": "https://registry.npmjs.org/@types/google-apps-script/-/google-apps-script-1.0.33.tgz", 21 | "integrity": "sha512-9UxTHvxHwAOINtbKWAz0gn7WL41m3YwGQNpTrYERyqwQw+cBnKi4Dl3f5AtsA9o6XyhsIvrIiOvDR8O6A8SVjA==" 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@types/google-apps-script": "^1.0.33" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/Code.ts: -------------------------------------------------------------------------------- 1 | import {LogUtils} from "./gas-terminal/LogUtils"; 2 | import {LongRun} from "./gas-terminal/LongRun"; 3 | 4 | function HelloWorld(name: string) { 5 | LogUtils.i('Hello ' + name + ' !!'); 6 | } 7 | 8 | function LongRunTest( 9 | // there must be no arguments, because the parameters must be retrieved from LongRun class. 10 | /* times: number, funcExecutionSeconds: number, maxExecutionSeconds: number, triggerDelayMinutes: number */ 11 | ) { 12 | let longRun = LongRun.instance; 13 | 14 | // funcName must equal this function's name. 15 | const funcName = 'LongRunTest'; 16 | 17 | // you can get the parameters from LongRun class. 18 | // the order of the array is the same as the order of the command definition. 19 | const params = longRun.getParameters(funcName); 20 | const times = parseInt(params[0]); 21 | const funcExecutionSeconds = parseInt(params[1]); 22 | const maxExecutionSeconds = parseInt(params[2]); 23 | const triggerDelayMinutes = parseInt(params[3]); 24 | 25 | // you can set the long-running configurations. of course you can use the default values. 26 | longRun.setMaxExecutionSeconds(maxExecutionSeconds); // default is 240 seconds 27 | longRun.setTriggerDelayMinutes(triggerDelayMinutes); // default is 1 minute 28 | 29 | // you should get the index to resume(zero for the first time) 30 | let startIndex = longRun.startOrResume(funcName); 31 | if( startIndex === 0 ){ 32 | LogUtils.i('--- LongRunTest command started. ---'); 33 | } 34 | 35 | try { 36 | // Execute the iterative process. 37 | for (let i = startIndex; i < times; i++) { 38 | LogUtils.i('Processing: ' + i); 39 | 40 | // Each time before executing a process, you need to check if it should be stopped or not. 41 | if (longRun.checkShouldSuspend(funcName, i)) { 42 | // if checkShouldSuspend() returns true, the next trigger has been set 43 | // and you should get out of the loop. 44 | LogUtils.i('*** The process has been suspended. ***'); 45 | break; 46 | } 47 | 48 | // *** code your main process here! *** 49 | Utilities.sleep(funcExecutionSeconds * 1000); // demonstrate the process 50 | 51 | LogUtils.i('Processing Done!: ' + i); 52 | } 53 | } 54 | catch (e) { 55 | LogUtils.ex(e); 56 | } 57 | finally { 58 | // you must always call end() to reset the long-running variables if there is no next trigger. 59 | const finished = longRun.end(funcName); 60 | if( finished ){ 61 | LogUtils.i('--- LongRunTest command finished. ---'); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/appsscript.json: -------------------------------------------------------------------------------- 1 | { 2 | "timeZone": "Asia/Tokyo", 3 | "dependencies": { 4 | }, 5 | "exceptionLogging": "STACKDRIVER", 6 | "runtimeVersion": "V8" 7 | } -------------------------------------------------------------------------------- /src/gas-terminal/0/SheetBase.ts: -------------------------------------------------------------------------------- 1 | import Sheet = GoogleAppsScript.Spreadsheet.Sheet; 2 | import Range = GoogleAppsScript.Spreadsheet.Range; 3 | import File = GoogleAppsScript.Drive.File; 4 | import Spreadsheet = GoogleAppsScript.Spreadsheet.Spreadsheet; 5 | import {FileUtils} from "../FileUtils"; 6 | /** 7 | * Base Class for handling Google Sheets 8 | */ 9 | export abstract class SheetBase{ 10 | /** Sheet object */ 11 | protected _sheet:Sheet; 12 | 13 | /** 14 | * Constructor 15 | * @param sheet 16 | */ 17 | protected constructor(sheet:Sheet){ 18 | this._sheet = sheet; 19 | } 20 | /** 21 | * Returns the sheet's file id. 22 | * @returns {string} 23 | */ 24 | public getFileId():string{ 25 | return this._sheet.getParent().getId(); 26 | } 27 | /** 28 | * Returns the sheet file. 29 | * @returns {string} 30 | */ 31 | public getFile():File{ 32 | return FileUtils.getFileById(this.getFileId()); 33 | } 34 | /** 35 | * Returns the sheet object. 36 | * @returns {Sheet} 37 | */ 38 | public getSheet():Sheet{ 39 | return this._sheet; 40 | } 41 | 42 | /** 43 | * Set the sheet object. 44 | * @param sheet 45 | */ 46 | public setSheet(sheet:Sheet):void{ 47 | this._sheet = sheet; 48 | } 49 | 50 | /** 51 | * Returns spreadsheet object. 52 | * @returns {Spreadsheet} 53 | */ 54 | public getSpreadSheet():Spreadsheet{ 55 | return this._sheet.getParent(); 56 | } 57 | 58 | /** 59 | * Checks if the cell is of a particular type. 60 | * If not, throws exception. 61 | * @param values 62 | * @param rowIndex 63 | * @param colIndex 64 | * @param correctType 65 | */ 66 | protected checkCellTypeForArray(values:any[][], rowIndex:number, colIndex:number, correctType:string):void{ 67 | let cellType = typeof values[rowIndex][colIndex]; 68 | if( cellType !== correctType ){ 69 | throw new Error("Invalid type[row:" + (rowIndex+1) + ", col:" + (colIndex+1) + "]" + 70 | "[expected type:" + correctType + "][real type:" + cellType + "]"); 71 | } 72 | } 73 | /** 74 | * Checks if the cell is of a particular type. 75 | * If not, throws exception. 76 | * @param value 77 | * @param cellName 78 | * @param correctType 79 | */ 80 | protected checkCellTypeForCell(value:any, cellName:string, correctType:string):void{ 81 | let cellType = typeof value; 82 | if( cellType !== correctType ){ 83 | throw new Error("Invalid type[cell:" + cellName + "]" + 84 | "[expected type:" + correctType + "][real type:" + cellType + "]"); 85 | } 86 | } 87 | 88 | /** 89 | * Returns the number of rows in the data range. 90 | */ 91 | protected getDataRangeRowCount():number{ 92 | let dataRange:Range = this._sheet.getDataRange(); 93 | return dataRange.getLastRow(); 94 | } 95 | 96 | /** 97 | * Returns the specified range of the data range as Range. 98 | * @param rowIndexStart 99 | * @param colIndexStart 100 | * @param rowCount the number of rows to retrieve (-1: to get the all data) 101 | * @param colCount 102 | * @returns {Range} 103 | */ 104 | protected getTableRange(rowIndexStart:number, colIndexStart:number, rowCount:number, colCount:number):Range{ 105 | let ret:Range = null; 106 | if( rowCount == -1 ){ 107 | let dataRange:Range = this._sheet.getDataRange(); 108 | let dataRowCount:number = dataRange.getLastRow() - rowIndexStart; 109 | if( dataRowCount > 0 ){ 110 | ret = this._sheet.getRange(rowIndexStart+1, colIndexStart+1, dataRowCount, colCount); 111 | } 112 | } 113 | else if( rowCount > 0 ){ 114 | let sheetRowCount:number = this._sheet.getMaxRows(); 115 | if( rowCount > sheetRowCount ){ 116 | rowCount = sheetRowCount; 117 | } 118 | ret = this._sheet.getRange(rowIndexStart+1, colIndexStart+1, rowCount, colCount); 119 | } 120 | return ret; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/gas-terminal/CommandDefinition.ts: -------------------------------------------------------------------------------- 1 | import {StringUtils} from "./StringUtils"; 2 | 3 | /** 4 | * The class that represents a command definition 5 | */ 6 | export class CommandDefinition { 7 | /** Command name */ 8 | private _commandName:string; 9 | /** Function name */ 10 | private _funcName:string; 11 | /** Description */ 12 | private _description:string; 13 | /** Is long-running */ 14 | private _isLongRun:boolean; 15 | /** Parameters */ 16 | private _params:string[] = []; 17 | 18 | /** 19 | * Constructor 20 | * @param commandName 21 | * @param funcName 22 | * @param description 23 | * @param isLongRun 24 | * @param params 25 | */ 26 | constructor(commandName:string, funcName:string, description:string, isLongRun: boolean, params:string[]) { 27 | this._commandName = commandName; 28 | this._funcName = funcName; 29 | this._description = description; 30 | this._isLongRun = isLongRun; 31 | this._params = params; 32 | } 33 | 34 | /** 35 | * Returns if this definition is valid. 36 | */ 37 | public isValid():boolean{ 38 | return !StringUtils.isEmpty(this._commandName) && 39 | !StringUtils.isEmpty(this._funcName); 40 | } 41 | 42 | get commandName():string { 43 | return this._commandName; 44 | } 45 | 46 | set commandName(value:string) { 47 | this._commandName = value; 48 | } 49 | 50 | get funcName():string { 51 | return this._funcName; 52 | } 53 | 54 | set funcName(value:string) { 55 | this._funcName = value; 56 | } 57 | 58 | get description():string { 59 | return this._description; 60 | } 61 | 62 | set description(value:string) { 63 | this._description = value; 64 | } 65 | 66 | 67 | get isLongRun(): boolean { 68 | return this._isLongRun; 69 | } 70 | 71 | set isLongRun(value: boolean) { 72 | this._isLongRun = value; 73 | } 74 | 75 | get params():string[] { 76 | return this._params; 77 | } 78 | 79 | set params(value:string[]) { 80 | this._params = value; 81 | } 82 | 83 | getParamCount(): number { 84 | return this._params.length; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/gas-terminal/CommandsSheet.ts: -------------------------------------------------------------------------------- 1 | import Range = GoogleAppsScript.Spreadsheet.Range; 2 | import {CommandDefinition} from "./CommandDefinition"; 3 | import {SheetBase} from "./0/SheetBase"; 4 | import {StringUtils} from "./StringUtils"; 5 | /** 6 | * The class that represents the command definition sheet. 7 | */ 8 | export class CommandsSheet extends SheetBase{ 9 | // Constants of the command definition sheet 10 | private static _SHEET_NAME:string = 'Commands'; 11 | private static _ROW_INDEX_START:number = 1; 12 | private static _COL_COUNT:number = 9; 13 | private static _COL_INDEX_NAME:number = 0; 14 | private static _COL_INDEX_FUNC:number = 1; 15 | private static _COL_INDEX_DESCRIPTION:number = 2; 16 | private static _COL_INDEX_IS_LONG_RUN:number = 3; 17 | private static _COL_INDEX_PARAM1:number = 4; 18 | private static _COL_INDEX_PARAM2:number = 5; 19 | private static _COL_INDEX_PARAM3:number = 6; 20 | private static _COL_INDEX_PARAM4:number = 7; 21 | private static _COL_INDEX_PARAM5:number = 8; 22 | /** Singleton instance */ 23 | private static _instance:CommandsSheet; 24 | /** Data */ 25 | private _commands:CommandDefinition[] = []; 26 | 27 | /** 28 | * Private constructor 29 | * @param sheet 30 | */ 31 | private constructor(sheet:GoogleAppsScript.Spreadsheet.Sheet) { 32 | super(sheet); 33 | } 34 | /** 35 | * Returns singleton instance. 36 | */ 37 | public static get instance():CommandsSheet { 38 | if (!this._instance) { 39 | let sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(this._SHEET_NAME); 40 | this._instance = new CommandsSheet(sheet); 41 | this._instance.load(); 42 | } 43 | return this._instance; 44 | } 45 | 46 | /** 47 | * Loads the sheet's data 48 | */ 49 | public load():void{ 50 | this._commands = []; 51 | let dataRange:Range = this.getTableRange( 52 | CommandsSheet._ROW_INDEX_START,0,-1, CommandsSheet._COL_COUNT); 53 | if( dataRange != null ) { 54 | let values:any[][] = dataRange.getValues(); 55 | for (let i = 0; i < dataRange.getNumRows(); i++) { 56 | let row:CommandDefinition = this.readRow(values[i]); 57 | if (row.isValid()) { 58 | this._commands.push(row); 59 | } 60 | } 61 | } 62 | } 63 | 64 | /** 65 | * Creates a command definition object from a single line of data. 66 | * @param row 67 | * @returns CommandDefinition 68 | */ 69 | protected readRow(row:any[]): CommandDefinition{ 70 | // makes parameter array 71 | let params:string[] = []; 72 | let colIndexes:number[] = [ 73 | CommandsSheet._COL_INDEX_PARAM1, 74 | CommandsSheet._COL_INDEX_PARAM2, 75 | CommandsSheet._COL_INDEX_PARAM3, 76 | CommandsSheet._COL_INDEX_PARAM4, 77 | CommandsSheet._COL_INDEX_PARAM5 78 | ]; 79 | for( let i = 0; i < colIndexes.length; i++ ){ 80 | let param:string = String(row[colIndexes[i]]); 81 | if( StringUtils.isEmpty(param) ){ 82 | break; 83 | } 84 | params.push(param); 85 | } 86 | // make command definition object 87 | return new CommandDefinition( 88 | String(row[CommandsSheet._COL_INDEX_NAME]), 89 | String(row[CommandsSheet._COL_INDEX_FUNC]), 90 | String(row[CommandsSheet._COL_INDEX_DESCRIPTION]), 91 | String(row[CommandsSheet._COL_INDEX_IS_LONG_RUN]).toUpperCase() == 'TRUE', 92 | params 93 | ); 94 | } 95 | 96 | /** 97 | * Returns the specified command definition object. 98 | * @param commandName 99 | * @returns {CommandDefinition} 100 | */ 101 | public findCommand(commandName:string):CommandDefinition{ 102 | let find:CommandDefinition = null; 103 | for( let i = 0; i < this._commands.length; i++ ){ 104 | let command:CommandDefinition = this._commands[i]; 105 | if( command.commandName == commandName ){ 106 | find = command; 107 | } 108 | } 109 | return find; 110 | } 111 | 112 | get commands():CommandDefinition[] { 113 | return this._commands; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/gas-terminal/CommonUtils.ts: -------------------------------------------------------------------------------- 1 | 2 | import {NumberUtils} from "./NumberUtils"; 3 | /** 4 | * Common utilities 5 | */ 6 | export class CommonUtils{ 7 | 8 | /** 9 | * Determines if the value is null or undefined or NaN. 10 | * @param value 11 | */ 12 | public static isEmptyObject(value:any):boolean{ 13 | return value == null || // this checks both null and undefined 14 | NumberUtils.isNaN(value); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/gas-terminal/FileUtils.ts: -------------------------------------------------------------------------------- 1 | import Folder = GoogleAppsScript.Drive.Folder; 2 | import File = GoogleAppsScript.Drive.File; 3 | 4 | /** 5 | * Utilities for file. 6 | */ 7 | export class FileUtils{ 8 | /** 9 | * Retrieves a file based on its file ID. 10 | * @param id 11 | */ 12 | public static getFileById(id:string):File{ 13 | try{ 14 | return DriveApp.getFileById(id); 15 | } 16 | catch (e){ 17 | return null; 18 | } 19 | } 20 | /** 21 | * Retrieves a folder based on its file ID. 22 | * @param id 23 | */ 24 | public static getFolderById(id:string):Folder{ 25 | try{ 26 | return DriveApp.getFolderById(id); 27 | } 28 | catch (e){ 29 | return null; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/gas-terminal/LogUtils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Logger interface 3 | */ 4 | import {TerminalSheet} from "./TerminalSheet"; 5 | 6 | export interface LogInterface{ 7 | /** 8 | * Outputs a log. 9 | */ 10 | outputLog(message:string):void; 11 | } 12 | /** 13 | * Utilities for logging. 14 | */ 15 | export class LogUtils{ 16 | /** 17 | * Outputs a information log. 18 | */ 19 | public static i(message:any){ 20 | message = "[INF] " + message; 21 | console.info(message); 22 | this.logCommon(message, 0); 23 | } 24 | /** 25 | * Outputs a warning log. 26 | */ 27 | public static w(message:any){ 28 | message = "[WARN] " + message; 29 | console.warn(message); 30 | this.logCommon(message, 1); 31 | } 32 | /** 33 | * Outputs a error log. 34 | */ 35 | public static e(message:any){ 36 | message = "[ERR] " + message; 37 | console.error(message); 38 | this.logCommon(message, 2); 39 | } 40 | 41 | /** 42 | * Outputs a exception log. 43 | */ 44 | public static ex(e:any){ 45 | let message:string = 'FATAL EXCEPTION: ' + e.fileName + ': ' + e.lineNumber + '\n' + e.name + ': ' + e.message + '\n' + e.stack; 46 | console.error(message); 47 | this.logCommon(message, 2); 48 | } 49 | /** 50 | * Outputs a exception log (as warning). 51 | */ 52 | public static exWarn(e:any){ 53 | let message:string = 'FATAL EXCEPTION: ' + e.fileName + ': ' + e.lineNumber + '\n' + e.name + ': ' + e.message + '\n' + e.stack; 54 | console.warn(message); 55 | this.logCommon(message, 1); 56 | } 57 | 58 | /** 59 | * Outputs a log. ( common processing ) 60 | * @param message 61 | * @param type 0:info 1:warn 2:error 62 | */ 63 | private static logCommon(message:string, type:number){ 64 | // Script log 65 | Logger.log(message); 66 | 67 | // If TerminalSheet is exists, output log to it. 68 | if( TerminalSheet.instance ){ 69 | TerminalSheet.instance.outputLog(message); 70 | } 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /src/gas-terminal/LongRun.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Long-Running Support 3 | */ 4 | import Properties = GoogleAppsScript.Properties.Properties; 5 | 6 | export class LongRun { 7 | // singleton instance 8 | private static _instance:LongRun; 9 | 10 | // constants 11 | static PREFIX_RUNNING:string = "running_"; 12 | static PREFIX_TRIGGER_KEY:string = "trigger_"; 13 | static PREFIX_START_POS:string = "start_"; 14 | static PREFIX_OPTION:string = "option_"; 15 | static RUNNING_MAX_SECONDS:number = 4*60; 16 | static RUNNING_DELAY_MINUTES:number = 1; 17 | static EXECUTE_LONGRUN_FUNCNAME:string = "_executeLongRun"; 18 | 19 | /** 20 | * Private constructor 21 | * @private 22 | */ 23 | private constructor() { 24 | } 25 | 26 | /** 27 | * Returns singleton instance. 28 | */ 29 | public static get instance():LongRun { 30 | if (!this._instance) { 31 | this._instance = new LongRun(); 32 | } 33 | return this._instance; 34 | } 35 | 36 | /** start time map */ 37 | startTimeMap:{} = {}; 38 | 39 | /** 40 | * Returns if function is running now. 41 | * @param funcName 42 | */ 43 | isRunning(funcName:string):boolean{ 44 | // get spreadsheet properties 45 | let properties:Properties = PropertiesService.getScriptProperties(); 46 | let running:string = properties.getProperty(LongRun.PREFIX_RUNNING+funcName); 47 | return !(running == null || running === ''); 48 | } 49 | 50 | /** 51 | * Sets the function is running 52 | * @param funcName 53 | * @param running 54 | */ 55 | setRunning(funcName:string, running: boolean): void { 56 | let properties: Properties = PropertiesService.getScriptProperties(); 57 | const key = LongRun.PREFIX_RUNNING + funcName; 58 | if(running) { 59 | properties.setProperty(key, "running"); 60 | } 61 | else{ 62 | properties.deleteProperty(key); 63 | } 64 | } 65 | 66 | /** 67 | * Sets max execution seconds 68 | * @param seconds 69 | */ 70 | setMaxExecutionSeconds(seconds:number){ 71 | LongRun.RUNNING_MAX_SECONDS = seconds; 72 | } 73 | /** 74 | * Sets the trigger's delay minutes 75 | * @param minutes 76 | */ 77 | setTriggerDelayMinutes(minutes:number){ 78 | LongRun.RUNNING_DELAY_MINUTES = minutes; 79 | } 80 | 81 | /** 82 | * Returns the function parameters 83 | * @param funcName 84 | */ 85 | getParameters(funcName: string): string[]{ 86 | let properties:Properties = PropertiesService.getScriptProperties(); 87 | let parameters = properties.getProperty(LongRun.PREFIX_OPTION+funcName); 88 | if( parameters != null ){ 89 | return parameters.split(','); 90 | } 91 | else{ 92 | return []; 93 | } 94 | } 95 | /** 96 | * Sets the function parameters. 97 | * @param funcName 98 | * @param parameters 99 | */ 100 | setParameters(funcName: string, parameters: string[]):void{ 101 | let properties:Properties = PropertiesService.getScriptProperties(); 102 | if( parameters != null ) { 103 | properties.setProperty(LongRun.PREFIX_OPTION + funcName, parameters.join(',')); 104 | } 105 | else{ 106 | properties.deleteProperty(LongRun.PREFIX_OPTION + funcName); 107 | } 108 | } 109 | 110 | /** 111 | * Starts or Resume Long-Run process. 112 | * @returns start index ( 0 for the first time ) 113 | */ 114 | startOrResume(funcName:string):number{ 115 | // save start time 116 | this.startTimeMap[funcName] = new Date().getTime(); 117 | 118 | // get properties of spreadsheet 119 | let properties:Properties = PropertiesService.getScriptProperties(); 120 | 121 | // set running-flag 122 | this.setRunning(funcName, true); 123 | 124 | // if the trigger exists, delete it. 125 | this.deleteTrigger(LongRun.PREFIX_TRIGGER_KEY+funcName); 126 | 127 | // get start index 128 | let startPos:number = parseInt(properties.getProperty(LongRun.PREFIX_START_POS+funcName)); 129 | if( !startPos ){ 130 | return 0; 131 | } 132 | else{ 133 | return startPos; 134 | } 135 | } 136 | 137 | /** 138 | * Determines whether the process should be suspended. 139 | * If it should be suspended, the next trigger will be registered. 140 | * @param funcName 141 | * @param nextIndex - start position when resuming 142 | * @return true - it should be suspended 143 | */ 144 | checkShouldSuspend(funcName:string, nextIndex:number): boolean{ 145 | let startTime = this.startTimeMap[funcName]; 146 | let diff = (new Date().getTime() - startTime) / 1000; 147 | // If it's past the specified time, suspend the process 148 | if(diff >= LongRun.RUNNING_MAX_SECONDS){ 149 | 150 | // register the next trigger and set running-flag off 151 | this.registerNextTrigger(funcName, nextIndex); 152 | 153 | return true; 154 | } 155 | else{ 156 | return false; 157 | } 158 | } 159 | 160 | /** 161 | * Resets Long-Running variables 162 | * @param funcName 163 | */ 164 | reset(funcName:string):void{ 165 | // delete trigger 166 | this.deleteTrigger(LongRun.PREFIX_TRIGGER_KEY+funcName); 167 | // delete spreadsheet properties 168 | let properties:Properties = PropertiesService.getScriptProperties(); 169 | properties.deleteProperty(LongRun.PREFIX_START_POS+funcName); 170 | properties.deleteProperty(LongRun.PREFIX_OPTION+funcName); 171 | properties.deleteProperty(LongRun.PREFIX_RUNNING+funcName); 172 | properties.deleteProperty(LongRun.PREFIX_TRIGGER_KEY+funcName); 173 | } 174 | 175 | /** 176 | * Resets Long-Running variables if there is no next trigger. 177 | * Returns whether the command has finished or not. 178 | * @param funcName 179 | */ 180 | end(funcName:string):boolean { 181 | let ret: boolean = false; 182 | if( !this.existsNextTrigger(funcName) ){ 183 | this.reset(funcName); 184 | ret = true; 185 | } 186 | return ret; 187 | } 188 | 189 | /** 190 | * Returns if there is next trigger. 191 | * @param funcName 192 | */ 193 | existsNextTrigger(funcName:string):boolean { 194 | let triggerId = PropertiesService.getScriptProperties().getProperty(LongRun.PREFIX_TRIGGER_KEY+funcName); 195 | return triggerId != null; 196 | } 197 | 198 | /** 199 | * register the next trigger and set running-flag off 200 | * @param funcName 201 | * @param nextIndex - start position when resuming 202 | */ 203 | registerNextTrigger(funcName:string, nextIndex:number):void{ 204 | // get spreadsheet properties 205 | let properties:Properties = PropertiesService.getScriptProperties(); 206 | properties.setProperty(LongRun.PREFIX_START_POS+funcName, String(nextIndex)); // save next start position 207 | this.setTrigger(LongRun.PREFIX_TRIGGER_KEY+funcName, funcName); // set trigger 208 | 209 | // turn off running-flag 210 | properties.deleteProperty(LongRun.PREFIX_RUNNING+funcName); 211 | } 212 | 213 | /** 214 | * Deletes the trigger 215 | * @param triggerKey 216 | */ 217 | private deleteTrigger(triggerKey:string):void { 218 | let triggerId = PropertiesService.getScriptProperties().getProperty(triggerKey); 219 | 220 | if(!triggerId) return; 221 | 222 | ScriptApp.getProjectTriggers().filter(function(trigger){ 223 | return trigger.getUniqueId() == triggerId; 224 | }) 225 | .forEach(function(trigger) { 226 | ScriptApp.deleteTrigger(trigger); 227 | }); 228 | PropertiesService.getScriptProperties().deleteProperty(triggerKey); 229 | } 230 | 231 | /** 232 | * Sets a trigger 233 | * @param triggerKey 234 | * @param funcName 235 | */ 236 | private setTrigger(triggerKey, funcName){ 237 | this.deleteTrigger(triggerKey); // delete if exists. 238 | let dt:Date = new Date(); 239 | dt.setMinutes(dt.getMinutes() + LongRun.RUNNING_DELAY_MINUTES); // will execute after the specified time 240 | let triggerId = ScriptApp.newTrigger(funcName).timeBased().at(dt).create().getUniqueId(); 241 | // save the trigger id to delete the trigger later. 242 | PropertiesService.getScriptProperties().setProperty(triggerKey, triggerId); 243 | } 244 | 245 | } 246 | 247 | /** 248 | * A function allows you to easily execute long-run task using the LongRun class. 249 | * 250 | * @param mainFuncName - Name of the function to be executed each time. 251 | * @param loopCount - Number of times to execute the main function. 252 | * @param params - Parameters passed to each function (string[]). (optional) 253 | * @param initializerName - Name of the first function to be executed on first or restart. (optional) 254 | * @param finalizerName - Name of the function to be called on interruption or when all processing is complete. (optional) 255 | * 256 | * The definition of each function to be passed should be as follows. 257 | * Main function: function [function name](index: number, params: string[]) {...} 258 | * Initializer: function [function name](startIndex: number, params: string[]) {...} 259 | * Finalizer: function [function name](isFinished: boolean, params: string[]) {...} 260 | * 261 | * Note that it is not possible to use executeLongRun() to execute different long-time processes simultaneously. 262 | */ 263 | export function executeLongRun( mainFuncName: string, 264 | loopCount: number, 265 | params: string[] = null, 266 | initializerName: string = null, 267 | finalizerName: string = null ) { 268 | const longRunParams: string[] = []; 269 | longRunParams.push(mainFuncName); 270 | longRunParams.push(String(loopCount)); 271 | longRunParams.push(initializerName === null ? '' : initializerName); 272 | longRunParams.push(finalizerName === null ? '' : finalizerName); 273 | if ( params != null && params.length > 0 ) { 274 | longRunParams.push(params.join(',')); 275 | } 276 | 277 | LongRun.instance.setParameters(LongRun.EXECUTE_LONGRUN_FUNCNAME, longRunParams); 278 | _executeLongRun(); 279 | } 280 | 281 | /** 282 | * The main body of executeLongRun 283 | */ 284 | function _executeLongRun(){ 285 | let longRun = LongRun.instance; 286 | 287 | // get parameters 288 | const longRunParams = longRun.getParameters(LongRun.EXECUTE_LONGRUN_FUNCNAME); 289 | const mainFuncName = longRunParams[0]; 290 | const loopCount = parseInt(longRunParams[1]); 291 | const initializerName = longRunParams[2]; 292 | const finalizerName = longRunParams[3]; 293 | const params: string[] = []; 294 | for ( let i = 4; i < longRunParams.length; i++ ){ 295 | params.push('"' + longRunParams[i] + '"'); 296 | } 297 | const paramsLiteral = '[' + params.join(',') + ']'; 298 | 299 | let startIndex = longRun.startOrResume(LongRun.EXECUTE_LONGRUN_FUNCNAME); 300 | try { 301 | // *** call initializer *** 302 | if ( initializerName != null && initializerName.length > 0 ){ 303 | eval(initializerName + '(' + startIndex + ',' + paramsLiteral + ')'); 304 | } 305 | // execute the iterative process. 306 | for (let i = startIndex; i < loopCount; i++) { 307 | // Each time before executing a process, you need to check if it should be stopped or not. 308 | if (longRun.checkShouldSuspend(LongRun.EXECUTE_LONGRUN_FUNCNAME, i)) { 309 | // if checkShouldSuspend() returns true, the next trigger has been set 310 | // and you should get out of the loop. 311 | console.log('*** The process has been suspended. ***'); 312 | break; 313 | } 314 | // *** call main process *** 315 | eval(mainFuncName + '(' + i + ',' + paramsLiteral + ')'); 316 | } 317 | } 318 | catch (e) { 319 | console.log(e.message); 320 | } 321 | finally { 322 | // you must always call end() to reset the long-running variables if there is no next trigger. 323 | const finished = longRun.end(LongRun.EXECUTE_LONGRUN_FUNCNAME); 324 | // *** call finalizer *** 325 | if ( finalizerName != null && finalizerName.length > 0 ){ 326 | eval(finalizerName + '(' + finished + ',' + paramsLiteral + ')'); 327 | } 328 | } 329 | } 330 | -------------------------------------------------------------------------------- /src/gas-terminal/NumberUtils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Utilities for numbers. 3 | */ 4 | export class NumberUtils{ 5 | 6 | /** 7 | * Determines whether value is finite. 8 | * @param value 9 | * @returns {boolean} 10 | */ 11 | public static isFinite(value):boolean { 12 | return typeof value === "number" && isFinite(value); 13 | } 14 | /** 15 | * Determines whether value is Nan and is type of Number. 16 | * @param value 17 | * @returns {boolean} 18 | */ 19 | public static isNaN(value):boolean { 20 | return typeof value === "number" && isNaN(value); 21 | } 22 | 23 | /** 24 | * Determines whether value is type of Number. 25 | * @param value 26 | */ 27 | public static isNumberType(value):boolean{ 28 | return typeof value === "number"; 29 | } 30 | 31 | /** 32 | * Returns the max value in the array. 33 | * @param nums 34 | */ 35 | public static getMax(nums:number[]){ 36 | return Math.max.apply(null, nums); 37 | } 38 | /** 39 | * Returns the min value in the array. 40 | * @param nums 41 | */ 42 | public static getMin(nums:number[]){ 43 | return Math.min.apply(null, nums); 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/gas-terminal/SettingsConst.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Constant of settings 3 | */ 4 | export class SettingsConst{ 5 | // Settings Sheet 6 | public static SHEET_NAME:string = "Settings"; 7 | public static START_ROW_INDEX:number = 1; 8 | public static COL_INDEX_SECTION:number = 0; 9 | public static COL_INDEX_KEY:number = 1; 10 | public static COL_INDEX_VALUE:number = 2; 11 | 12 | // Settings of Terminal 13 | public static SECTION_TERMINAL:string = "SECTION_TERMINAL"; 14 | public static KEY_TERMINAL_SHEET_NAME:string = "KEY_TERMINAL_SHEET_NAME"; 15 | public static KEY_TERMINAL_CELL_COMMAND:string = "KEY_TERMINAL_CELL_COMMAND"; 16 | public static KEY_TERMINAL_CELL_DESCRIPTION:string = "KEY_TERMINAL_CELL_DESCRIPTION"; 17 | public static KEY_TERMINAL_CELL_PARAM1:string = "KEY_TERMINAL_CELL_PARAM1"; 18 | public static KEY_TERMINAL_CELL_PARAM2:string = "KEY_TERMINAL_CELL_PARAM2"; 19 | public static KEY_TERMINAL_CELL_PARAM3:string = "KEY_TERMINAL_CELL_PARAM3"; 20 | public static KEY_TERMINAL_CELL_PARAM4:string = "KEY_TERMINAL_CELL_PARAM4"; 21 | public static KEY_TERMINAL_CELL_PARAM5:string = "KEY_TERMINAL_CELL_PARAM5"; 22 | public static KEY_TERMINAL_CELL_RESULTS:string = "KEY_TERMINAL_CELL_RESULTS"; 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/gas-terminal/SettingsSheet.ts: -------------------------------------------------------------------------------- 1 | 2 | import {SheetBase} from "./0/SheetBase"; 3 | import {SettingsConst} from "./SettingsConst"; 4 | import Range = GoogleAppsScript.Spreadsheet.Range; 5 | import {StringUtils} from "./StringUtils"; 6 | /** 7 | * The class that represents the settings sheet. 8 | */ 9 | export class SettingsSheet extends SheetBase{ 10 | private _settingMap:{} = {}; 11 | private static _instance:SettingsSheet; 12 | 13 | /** 14 | * Private constructor 15 | * @param sheet Spreadsheet.Sheet 16 | * @private 17 | */ 18 | private constructor(sheet:GoogleAppsScript.Spreadsheet.Sheet) { 19 | super(sheet); 20 | } 21 | 22 | /** 23 | * Returns singleton instance. 24 | */ 25 | public static get instance():SettingsSheet { 26 | if (!this._instance) { 27 | let sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(SettingsConst.SHEET_NAME); 28 | if( sheet == null ){ 29 | throw new Error("Sheet[" + SettingsConst.SHEET_NAME + "] not found"); 30 | } 31 | this._instance = new SettingsSheet(sheet); 32 | this._instance.load(); 33 | } 34 | return this._instance; 35 | } 36 | 37 | /** 38 | * Loads the sheet's data. 39 | */ 40 | public load():void{ 41 | this._settingMap = {}; 42 | 43 | let dataRange:Range = this._sheet.getDataRange(); 44 | let values:any[][] = dataRange.getValues(); 45 | for(let i = SettingsConst.START_ROW_INDEX; i < dataRange.getNumRows(); i++ ){ 46 | 47 | let row:[string,string,string] = this.readRow(values[i]); 48 | let section:string = row[0]; 49 | let key:string = row[1]; 50 | let value:string = row[2]; 51 | if( StringUtils.isEmpty(section) || StringUtils.isEmpty(key) ){ 52 | continue; 53 | } 54 | if( !(section in this._settingMap) ) { 55 | this._settingMap[section] = {}; 56 | } 57 | this._settingMap[section][key] = value; 58 | } 59 | } 60 | 61 | /** 62 | * Creates a settings information object from a single line of data. 63 | * @param row Spreadsheet's row 64 | * @protected 65 | */ 66 | protected readRow(row:any[]):[string,string,string]{ 67 | if( row.length <= SettingsConst.COL_INDEX_SECTION || 68 | row.length <= SettingsConst.COL_INDEX_KEY || 69 | row.length <= SettingsConst.COL_INDEX_VALUE ){ 70 | return [null,null,null]; 71 | } 72 | 73 | return [ 74 | row[SettingsConst.COL_INDEX_SECTION], 75 | row[SettingsConst.COL_INDEX_KEY], 76 | row[SettingsConst.COL_INDEX_VALUE] 77 | ]; 78 | } 79 | 80 | /** 81 | * Returns the value of a given key. 82 | * @param section 83 | * @param key 84 | */ 85 | public getValue(section:string, key:string):string{ 86 | if( !(section in this._settingMap) ) { 87 | throw new Error("Setting Not Found [" + section + ":" + key + "]"); 88 | } 89 | else{ 90 | if( !(key in this._settingMap[section]) ) { 91 | throw new Error("Setting Not Found [" + section + ":" + key + "]"); 92 | } 93 | return this._settingMap[section][key]; 94 | } 95 | } 96 | /** 97 | * Returns the value of a given key as Integer. 98 | * @param section 99 | * @param key 100 | */ 101 | public getValueAsInt(section:string, key:string):number{ 102 | return parseInt(this.getValue(section,key)); 103 | } 104 | /** 105 | * Returns the value of a given key as Float. 106 | * @param section 107 | * @param key 108 | */ 109 | public getValueAsFloat(section:string, key:string):number{ 110 | return parseFloat(this.getValue(section,key)); 111 | } 112 | 113 | /** 114 | * For debug 115 | */ 116 | public dbgGetAllSetting():{}{ 117 | return this._settingMap; 118 | } 119 | 120 | } 121 | -------------------------------------------------------------------------------- /src/gas-terminal/StringUtils.ts: -------------------------------------------------------------------------------- 1 | import {NumberUtils} from "./NumberUtils"; 2 | import {CommonUtils} from "./CommonUtils"; 3 | 4 | /** Date Format Object */ 5 | let dateFormat = { 6 | _fmt : { 7 | "yyyy": function(date) { return date.getFullYear() + ''; }, 8 | "MM": function(date) { return ('0' + (date.getMonth() + 1)).slice(-2); }, 9 | "dd": function(date) { return ('0' + date.getDate()).slice(-2); }, 10 | "hh": function(date) { return ('0' + date.getHours()).slice(-2); }, 11 | "mm": function(date) { return ('0' + date.getMinutes()).slice(-2); }, 12 | "ss": function(date) { return ('0' + date.getSeconds()).slice(-2); } 13 | }, 14 | _priority : ["yyyy", "MM", "dd", "hh", "mm", "ss"], 15 | format: function(date, format){ 16 | return this._priority.reduce((res, fmt) => res.replace(fmt, this._fmt[fmt](date)), format) 17 | } 18 | }; 19 | 20 | /** 21 | * Utilities for Strings 22 | */ 23 | export class StringUtils{ 24 | /** 25 | * Returns whether the string is empty. 26 | */ 27 | public static isEmpty(str:string):boolean{ 28 | return str === "" || CommonUtils.isEmptyObject(str); 29 | } 30 | 31 | 32 | /** 33 | * Formats the date. 34 | * @param date 35 | * @param format 36 | * @returns {string} 37 | */ 38 | public static dateFormat(date:Date, format:string):string{ 39 | if( !this.isValidDate(date) ){ 40 | return null; // 日付ではない 41 | } 42 | else { 43 | return dateFormat.format(date, format); 44 | } 45 | } 46 | /** 47 | * Formats the date. ( by using string ) 48 | * @param dateStr 49 | * @param format 50 | * @returns {string} 51 | */ 52 | public static dateFormatByString(dateStr:string, format:string):string{ 53 | if(dateStr == null){ 54 | return null; 55 | } 56 | return this.dateFormat(new Date(dateStr), format); 57 | } 58 | /** 59 | * Returns whether the date is valid. 60 | * @param date 61 | */ 62 | public static isValidDate(date:Date):boolean{ 63 | return ! NumberUtils.isNaN(date.getTime()); 64 | } 65 | /** 66 | * Returns whether the date is valid.( by using string ) 67 | * @param dateStr 68 | */ 69 | public static isValidDateString(dateStr:string):boolean{ 70 | if(dateStr == null){ 71 | return false; 72 | } 73 | let date = new Date(dateStr); 74 | return this.isValidDate(date); 75 | } 76 | 77 | /** 78 | * Returns a zero padding string. 79 | * @param size number of zero 80 | * @param value integer 81 | */ 82 | public static zeroPadding(size:number, value:number):string{ 83 | let padding:string = Array(size+1).join("0"); 84 | return (padding + value).slice(size*-1); 85 | } 86 | 87 | /** 88 | * Returns whether the string consists only number characters. 89 | * @param val 90 | * @returns {boolean} 91 | */ 92 | public static isNumberOnly(val:string):boolean{ 93 | let regex = new RegExp("/^[0-9]+$/"); 94 | return regex.test(val); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/gas-terminal/TerminalController.ts: -------------------------------------------------------------------------------- 1 | import {SettingsSheet} from "./SettingsSheet"; 2 | import {SettingsConst} from "./SettingsConst"; 3 | import Sheet = GoogleAppsScript.Spreadsheet.Sheet; 4 | import {TerminalSheet} from "./TerminalSheet"; 5 | import {StringUtils} from "./StringUtils"; 6 | import {CommandDefinition} from "./CommandDefinition"; 7 | import {CommandsSheet} from "./CommandsSheet"; 8 | import {LogUtils} from "./LogUtils"; 9 | import {LongRun} from "./LongRun"; 10 | 11 | /** 12 | * Runs when a user opens a spreadsheet. 13 | */ 14 | function onOpen(e) { 15 | // Clear all 16 | TerminalSheet.instance.clearAll(true); 17 | } 18 | 19 | /** 20 | * Runs when a user changes a value in a spreadsheet. 21 | */ 22 | function onEdit(e){ 23 | let settingsSheet:SettingsSheet = SettingsSheet.instance; 24 | let terminalSheetName:string = settingsSheet.getValue(SettingsConst.SECTION_TERMINAL, SettingsConst.KEY_TERMINAL_SHEET_NAME); 25 | let commandNameCell:string = settingsSheet.getValue(SettingsConst.SECTION_TERMINAL, SettingsConst.KEY_TERMINAL_CELL_COMMAND); 26 | 27 | const sheet:Sheet = e.source.getActiveSheet(); 28 | const range = e.source.getActiveRange(); 29 | // Checks if command name changed 30 | if( sheet.getName() == terminalSheetName && range.getA1Notation() == commandNameCell ){ 31 | new TerminalController().onChangeCommandName(); 32 | } 33 | } 34 | /** 35 | * Runs when a user clicks the Execute button. 36 | */ 37 | function onExecuteButtonClick():void{ 38 | new TerminalController().onExecuteButtonClick(); 39 | } 40 | 41 | /** 42 | * GAS Terminal Controller 43 | */ 44 | export class TerminalController { 45 | /** 46 | * Handles command name changed. 47 | */ 48 | public onChangeCommandName():void{ 49 | try { 50 | // Clear all except command name 51 | TerminalSheet.instance.clearAll(false); 52 | // Get command name 53 | let commandName:string = TerminalSheet.instance.getCommandName(); 54 | if( StringUtils.isEmpty(commandName) ){ 55 | return; 56 | } 57 | // Get command define 58 | let commandDef:CommandDefinition = CommandsSheet.instance.findCommand(commandName); 59 | // Update view 60 | TerminalSheet.instance.setDescription(commandDef.description); 61 | TerminalSheet.instance.setParamsView(commandDef.params); 62 | 63 | } 64 | catch (e) { 65 | LogUtils.ex(e); 66 | } 67 | } 68 | 69 | /** 70 | * Handles clicking the Execute button. 71 | */ 72 | public onExecuteButtonClick():void{ 73 | try { 74 | // get the command name, 75 | let commandName:string = TerminalSheet.instance.getCommandName(); 76 | if( StringUtils.isEmpty(commandName) ){ 77 | Browser.msgBox("You should select command first") 78 | return; 79 | } 80 | // confirm with the user. 81 | let ret:string = Browser.msgBox("Are you sure to execute ["+commandName+"]?", Browser.Buttons.YES_NO); 82 | if( ret != "yes" ){ 83 | return; 84 | } 85 | 86 | // get the command define. 87 | let commandDef:CommandDefinition = CommandsSheet.instance.findCommand(commandName); 88 | 89 | // check if the command has already run. 90 | if( LongRun.instance.isRunning(commandDef.funcName) ){ 91 | let ret:string = Browser.msgBox('[' + commandDef.commandName + '] is already running.\\n' + 92 | 'Do you want to ignore it and run?', Browser.Buttons.YES_NO); 93 | if( ret != "yes" ){ 94 | return; 95 | } 96 | } 97 | 98 | // long-run commands 99 | if( commandDef.isLongRun ) { 100 | this.executeLongRunCommand(commandDef); 101 | } 102 | // the ordinarily commands 103 | else { 104 | this.executeNormalCommand(commandDef); 105 | } 106 | } 107 | catch (e) { 108 | LogUtils.ex(e); 109 | } 110 | } 111 | 112 | /** 113 | * Executes normal command 114 | * @param commandDef 115 | * @private 116 | */ 117 | private executeNormalCommand(commandDef:CommandDefinition): void{ 118 | let params:string[] = TerminalSheet.instance.getParams(commandDef.getParamCount()); 119 | let paramsStr:string = ""; 120 | 121 | // clear the log. 122 | TerminalSheet.instance.clearLog(); 123 | 124 | // make the parameter string. 125 | if (params.length > 0) { 126 | paramsStr = '"' + params.join('","') + '"'; 127 | } 128 | 129 | // make the function call string. 130 | let callStr:string = commandDef.funcName + "(" + paramsStr + ")"; 131 | 132 | try { 133 | // set running-flg on 134 | LongRun.instance.setRunning(commandDef.funcName, true); 135 | // execute 136 | eval(callStr); 137 | } 138 | finally { 139 | // set running-flg off 140 | LongRun.instance.setRunning(commandDef.funcName, false); 141 | } 142 | } 143 | 144 | /** 145 | * Executes long-running command 146 | * @param commandDef 147 | * @private 148 | */ 149 | private executeLongRunCommand(commandDef:CommandDefinition): void{ 150 | let params:string[] = TerminalSheet.instance.getParams(commandDef.getParamCount()); 151 | 152 | // check if the command has already run. 153 | if( LongRun.instance.existsNextTrigger(commandDef.funcName) ){ 154 | let ret:string = Browser.msgBox('[' + commandDef.commandName + '] already has next trigger.\\n' + 155 | 'Do you want to ignore it and run?', Browser.Buttons.YES_NO); 156 | if( ret != "yes" ){ 157 | return; 158 | } 159 | } 160 | 161 | // clear the log. 162 | TerminalSheet.instance.clearLog(); 163 | 164 | // clear the variables 165 | LongRun.instance.reset(commandDef.funcName); 166 | // store the parameters and paramsStr should be empty. 167 | LongRun.instance.setParameters(commandDef.funcName, params); 168 | 169 | // make the function call string. 170 | let callStr:string = commandDef.funcName + "()"; 171 | 172 | // execute 173 | eval(callStr); 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/gas-terminal/TerminalSheet.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The class that represents the Terminal sheet. 3 | */ 4 | import {SettingsSheet} from "./SettingsSheet"; 5 | import {SettingsConst} from "./SettingsConst"; 6 | import {SheetBase} from "./0/SheetBase"; 7 | import Range = GoogleAppsScript.Spreadsheet.Range; 8 | import {StringUtils} from "./StringUtils"; 9 | import {LogUtils} from "./LogUtils"; 10 | 11 | export class TerminalSheet extends SheetBase { 12 | protected _CELL_COMMAND:string; 13 | protected _CELL_DESCRIPTION:string; 14 | protected _CELL_PARAM:string[] = []; 15 | protected _CELL_RESULTS:string; 16 | 17 | private static _instance:TerminalSheet; 18 | 19 | /** 20 | * Private constructor 21 | * @param sheet 22 | */ 23 | private constructor(sheet:GoogleAppsScript.Spreadsheet.Sheet) { 24 | super(sheet); 25 | 26 | let settingsSheet:SettingsSheet = SettingsSheet.instance; 27 | this._CELL_COMMAND = settingsSheet.getValue(SettingsConst.SECTION_TERMINAL, SettingsConst.KEY_TERMINAL_CELL_COMMAND); 28 | this._CELL_DESCRIPTION = settingsSheet.getValue(SettingsConst.SECTION_TERMINAL, SettingsConst.KEY_TERMINAL_CELL_DESCRIPTION); 29 | this._CELL_PARAM.push(settingsSheet.getValue(SettingsConst.SECTION_TERMINAL, SettingsConst.KEY_TERMINAL_CELL_PARAM1)); 30 | this._CELL_PARAM.push(settingsSheet.getValue(SettingsConst.SECTION_TERMINAL, SettingsConst.KEY_TERMINAL_CELL_PARAM2)); 31 | this._CELL_PARAM.push(settingsSheet.getValue(SettingsConst.SECTION_TERMINAL, SettingsConst.KEY_TERMINAL_CELL_PARAM3)); 32 | this._CELL_PARAM.push(settingsSheet.getValue(SettingsConst.SECTION_TERMINAL, SettingsConst.KEY_TERMINAL_CELL_PARAM4)); 33 | this._CELL_PARAM.push(settingsSheet.getValue(SettingsConst.SECTION_TERMINAL, SettingsConst.KEY_TERMINAL_CELL_PARAM5)); 34 | this._CELL_RESULTS = settingsSheet.getValue(SettingsConst.SECTION_TERMINAL, SettingsConst.KEY_TERMINAL_CELL_RESULTS); 35 | } 36 | 37 | /** 38 | * Returns singleton instance. 39 | */ 40 | public static get instance():TerminalSheet { 41 | if (!this._instance) { 42 | let settingsSheet:SettingsSheet = SettingsSheet.instance; 43 | let sheetName:string = settingsSheet.getValue(SettingsConst.SECTION_TERMINAL, SettingsConst.KEY_TERMINAL_SHEET_NAME); 44 | let sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName); 45 | this._instance = new TerminalSheet(sheet); 46 | } 47 | return this._instance; 48 | } 49 | /** 50 | * Loads the sheet's data. 51 | */ 52 | public load():void{ 53 | throw Error("not supported"); 54 | } 55 | 56 | /** 57 | * Returns the name of the current command. 58 | * @returns {String} 59 | */ 60 | public getCommandName():string{ 61 | return String(this._sheet.getRange(this._CELL_COMMAND).getValue()); 62 | } 63 | /** 64 | * Returns the specified parameter. 65 | * @param num 66 | */ 67 | public getParam(num:number):string{ 68 | return String(this._sheet.getRange(this._CELL_PARAM[num]).getValue()); 69 | } 70 | 71 | /** 72 | * Returns the parameters. 73 | */ 74 | public getParams(paramCount: number):string[]{ 75 | let ret:string[] = []; 76 | for( let i = 0; i < paramCount && i < this._CELL_PARAM.length; i++ ){ 77 | let range:Range = this._sheet.getRange(this._CELL_PARAM[i]); 78 | let value:string = String(range.getValue()); 79 | ret.push(StringUtils.isEmpty(value)?"":value); 80 | } 81 | return ret; 82 | } 83 | /** 84 | * Sets the appearance of the parameter fields. 85 | */ 86 | public setParamsView(paramDefs:string[]):void{ 87 | for( let i = 0; i < this._CELL_PARAM.length; i++ ){ 88 | let range:Range = this._sheet.getRange(this._CELL_PARAM[i]); 89 | range.setValue(""); 90 | if( paramDefs.length > i ){ 91 | if( !StringUtils.isEmpty(paramDefs[i]) ){ 92 | range.setBackground("black"); 93 | range.setNote(paramDefs[i]); 94 | } 95 | else{ 96 | range.setBackground("gray"); 97 | range.setNote(""); 98 | } 99 | } 100 | } 101 | } 102 | /** 103 | * Sets the description 104 | * @param value 105 | */ 106 | public setDescription(value:string):void{ 107 | this._sheet.getRange(this._CELL_DESCRIPTION).setValue(value); 108 | } 109 | 110 | /** 111 | * Outputs a log. 112 | * @param message 113 | */ 114 | public outputLog(message:string):void{ 115 | let logRange:Range = this._sheet.getRange(this._CELL_RESULTS); 116 | let value:string = String(logRange.getValue()); 117 | value += "\n" + message; 118 | logRange.setValue(value); 119 | } 120 | /** 121 | * Clears logs. 122 | */ 123 | public clearLog():void{ 124 | this._sheet.getRange(this._CELL_RESULTS).setValue(""); 125 | } 126 | 127 | /** 128 | * Clears all inputs. 129 | * @param clearCommandName 130 | */ 131 | public clearAll(clearCommandName:boolean):void{ 132 | if( clearCommandName ) { 133 | this._sheet.getRange(this._CELL_COMMAND).setValue(""); 134 | } 135 | this._sheet.getRange(this._CELL_DESCRIPTION).setValue(""); 136 | for( let i = 0; i < this._CELL_PARAM.length; i++ ){ 137 | let range:Range = this._sheet.getRange(this._CELL_PARAM[i]); 138 | range.setValue(""); 139 | range.setBackground("gray"); 140 | range.setNote(""); 141 | } 142 | this._sheet.getRange(this._CELL_RESULTS).setValue(""); 143 | } 144 | 145 | } 146 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["esnext"], 4 | "experimentalDecorators": true, 5 | "target": "es5" 6 | } 7 | } 8 | --------------------------------------------------------------------------------