From ffe63730d0c412aa06da95992042fc990cc233da Mon Sep 17 00:00:00 2001 From: xushiwei Date: Fri, 16 Feb 2024 12:32:09 +0800 Subject: [PATCH] x/gsh --- gsh/README.md | 135 +++++++++++++++++++++++++++++++++++++++++++++++ gsh/classfile.go | 78 +++++++++++++++++++++++++++ 2 files changed, 213 insertions(+) create mode 100644 gsh/README.md create mode 100644 gsh/classfile.go diff --git a/gsh/README.md b/gsh/README.md new file mode 100644 index 0000000..a7d2ceb --- /dev/null +++ b/gsh/README.md @@ -0,0 +1,135 @@ +Go+ DevOps Tools +====== + +[![Language](https://img.shields.io/badge/language-Go+-blue.svg)](https://github.com/goplus/gop) +[![GitHub release](https://img.shields.io/github/v/tag/goplus/gop.svg?label=Go%2b+release)](https://github.com/goplus/gop/releases) +[![Discord](https://img.shields.io/badge/Discord-online-success.svg?logo=discord&logoColor=white)](https://discord.gg/mYjWCJDcAr) +[![GoDoc](https://pkg.go.dev/badge/github.com/qiniu/x/gsh.svg)](https://pkg.go.dev/github.com/qiniu/x/gsh) + +This is an alternative to writing shell scripts. + +Yes, now you can write `shell script` in Go+. It supports all shell commands. + +## Usage + +First, let's create a Go+ source file named `./example.gsh` and write the following code: + +```coffee +mkdir "testgsh" +``` + +Then run it: + +```sh +gop run example.gsh +``` + +It's strange to you that the file extension of Go+ source is not `.gop` but `.gsh`. It is only because Go+ register `.gsh` as a builtin [classfile](https://github.com/goplus/gop/blob/main/doc/classfile.md). + +We can change `./example.gsh` more complicated: + +```coffee +type file struct { + name string + fsize int +} + +mkdir! "testgsh" + +mkdir "testgsh2" +lastErr! + +mkdir "testgsh3" +if lastErr != nil { + panic lastErr +} + +capout => { ls } +println output.fields + +capout => { ls "-l" } +files := [file{flds[8], flds[4].int!} for e <- output.split("\n") if flds := e.fields; flds.len > 2] +println files + +rmdir "testgsh", "testgsh2", "testgsh3" +``` + +### Check last error + +If we want to ensure `mkdir` successfully, there are three ways: + +The simplest way is: + +```coffee +mkdir! "testsh" # will panic if mkdir failed +``` + +The second way is: + +```coffee +mkdir "testsh" +lastErr! +``` + +Yes, `gsh` provides `lastErr` to check last error. + +The third way is: + +```coffee +mkdir "testsh" +if lastErr != nil { + panic lastErr +} +``` + +This is the most familiar way to Go developers. + +### Capture output of commands + +And, `gsh` provides a way to capture output of commands: + +```coffee +capout => { + ... +} +``` + +Similar to `lastErr`, the captured output result is saved to `output`. + +For an example: + +```coffee +capout => { ls "-l" } +println output +``` + +Here is a possible output: + +```s +total 72 +-rw-r--r-- 1 xushiwei staff 11357 Jun 19 00:20 LICENSE +-rw-r--r-- 1 xushiwei staff 127 Jun 19 10:00 README.md +-rw-r--r-- 1 xushiwei staff 365 Jun 19 00:25 example.gsh +-rw-r--r-- 1 xushiwei staff 126 Jun 19 09:33 go.mod +-rw-r--r-- 1 xushiwei staff 165 Jun 19 09:33 go.sum +-rw-r--r-- 1 xushiwei staff 1938 Jun 19 10:00 gop_autogen.go +``` + +We can use [Go+ powerful built-in data processing capabilities](doc/docs.md#data-processing) to process captured `output`: + +```coffee +type file struct { + name string + fsize int +} + +files := [file{flds[8], flds[4].int!} for e <- output.split("\n") if flds := e.fields; flds.len > 2] +``` + +In this example, we split `output` by `"\n"`, and for each entry `e`, split it by spaces (`e.fields`) and save into `flds`. Condition `flds.len > 2` is to remove special line of output: + +```s +total 72 +``` + +At last, pick file name and size of all selected entries and save into `files`. diff --git a/gsh/classfile.go b/gsh/classfile.go new file mode 100644 index 0000000..21df06f --- /dev/null +++ b/gsh/classfile.go @@ -0,0 +1,78 @@ +/* + Copyright 2024 Qiniu Limited (qiniu.com) + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package gsh + +import ( + "bytes" + "io" + "os" + "os/exec" +) + +// App is project class of this classfile. +type App struct { + fout io.Writer + ferr io.Writer + fin io.Reader + cout string + err error +} + +func (p *App) initApp() { + p.fin = os.Stdin + p.fout = os.Stdout + p.ferr = os.Stderr +} + +// Gop_Exec executes a shell command. +func (p *App) Gop_Exec(name string, args ...string) error { + cmd := exec.Command(name, args...) + cmd.Stdin = p.fin + cmd.Stdout = p.fout + cmd.Stderr = p.ferr + p.err = cmd.Run() + return p.err +} + +// LastErr returns error of last command execution. +func (p *App) LastErr() error { + return p.err +} + +// Capout captures stdout of doSth() execution and save it to output. +func (p *App) Capout(doSth func()) (string, error) { + var out bytes.Buffer + old := p.fout + p.fout = &out + defer func() { + p.fout = old + }() + doSth() + p.cout = out.String() + return p.cout, p.err +} + +// Output returns result of last capout. +func (p *App) Output() string { + return p.cout +} + +// Gopt_App_Main is main entry of this classfile. +func Gopt_App_Main(a interface{ initApp() }) { + a.initApp() + a.(interface{ MainEntry() }).MainEntry() +}