diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 08d3c63f..ea5eaaee 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -103,7 +103,7 @@ jobs: - name: Check scripts with Shellcheck uses: essentialkaos/shellcheck-action@v1 with: - files: scripts/* + files: scripts/bibop-dep scripts/bibop-docker scripts/bibop-entrypoint scripts/bibop-libtest-gen scripts/bibop-linked scripts/bibop-massive scripts/bibop-multi-check scripts/bibop-so-exported Hadolint: name: Hadolint diff --git a/COOKBOOK.md b/COOKBOOK.md index 6b3e3f8a..9b2d1e5d 100644 --- a/COOKBOOK.md +++ b/COOKBOOK.md @@ -18,6 +18,7 @@ * [Common](#common) * [`exit`](#exit) * [`wait`](#wait) + * [`template`](#template) * [Input/Output](#inputoutput) * [`expect`](#expect) * [`print`](#print) @@ -490,6 +491,71 @@ command "echo 'ABCD'" "Simple echo command" +##### `template` + +Creates a file from a template. If file already exists, it will be rewritten with the same UID, GID and mode. + +▲ _Note that 'template' action will not automatically backup the destination file if it already exists (use `backup` and `backup-restore` actions to preserve the original file). Also, the created file will remain after tests execution if it was created outside the working directory._ + +You can use the following methods in your templates: + +- `Var "name"` - get variable value; +- `Is "name" "value"` - compare variable value. + +Simple example: + +``` +# Sysconfig for postgresql service + +PG_ENGINE="" +PG_POSTMASTER="" +{{ if not .Is "data_dir" "" }} +PG_DATA="{{ .Var "data_dir" }}/db" +{{ else }} +PG_DATA="" +{{ end }} +PG_LOG="" +PG_UPLOG="" +PG_SOCKET_DIR="" +TIMEOUT="" +DISABLE_AUTO_NUMA="" +``` + +**Syntax:** `template [file-mode]` + +**Arguments:** + +* `source` - Path to template file (_String_) +* `dest` - Destination path (_String_) +* `file-mode` - Destination file mode (_Integer_) [Optional | 644] + +**Negative form:** No + +**Example:** + +```yang +command "-" "Create configuration file" + template app.template /etc/myapp.conf +``` + +```yang +command "-" "Create configuration file" + template app.template /etc/myapp.conf 640 +``` + +```yang +command "-" "Replace configuration file" + backup /etc/myapp.conf + template app.template /etc/myapp.conf 640 + +... + +command "-" "Restore original configuration file" + backup-restore /etc/myapp.conf +``` + + + #### Input/Output Be aware that the output store limited to 2 Mb of data for each stream (`stdout` _and_ `stderr`). So if command generates lots of output data, it better to use `expect` action to working with the output. @@ -563,6 +629,8 @@ command "echo 'ABCD'" "Simple echo command" wait-output 10.0 ``` + + ##### `output-match` Checks output with given [regular expression](https://en.wikipedia.org/wiki/Regular_expression). diff --git a/README.md b/README.md index 658e5825..b9a728f6 100644 --- a/README.md +++ b/README.md @@ -15,13 +15,13 @@
-`bibop` is a utility for testing command-line tools and daemons. Initially, this utility was created for testing packages from [ESSENTIAL KAOS Public Repository](https://pkgs.kaos.st). +`bibop` is a utility for testing command-line tools, packages and daemons. Initially, this utility was created for testing packages from [ESSENTIAL KAOS Public Repository](https://kaos.sh/kaos-repo). Information about bibop recipe syntax you can find in our [cookbook](COOKBOOK.md). ### Usage demo -[![demo](https://gh.kaos.st/bibop-600.gif)](#usage-demo) +https://github.com/essentialkaos/bibop/assets/182020/c63dc147-fa44-40df-92e2-12f530c411af ### Installation @@ -99,7 +99,8 @@ Usage: bibop {options} recipe Options --dry-run, -D Parse and validate recipe - --extra, -X Print the last lines from command output if action was failed + --extra, -X lines Number of output lines for failed action (default: 10) + --pause, -P duration Pause between commands in seconds --list-packages, -L List required packages --list-packages-flat, -L1 List required packages in one line (useful for scripts) --variables, -V List recipe variables @@ -128,6 +129,12 @@ Examples bibop app.recipe --tag init,service Run tests from app.recipe and execute commands with tags init and service + bibop app.recipe --extra + Run tests from app.recipe and print the last 10 lines from command output if action was failed + + bibop app.recipe --extra=50 + Run tests from app.recipe and print the last 50 lines from command output if action was failed + bibop app.recipe --format json 1> ~/results/app.json Run tests from app.recipe and save result in JSON format diff --git a/action/auxi.go b/action/auxi.go index 6000d3e0..cbf3ce12 100644 --- a/action/auxi.go +++ b/action/auxi.go @@ -30,7 +30,7 @@ type OutputContainer struct { // ////////////////////////////////////////////////////////////////////////////////// // // escapeCharRegex is regexp for searching escape characters -var escapeCharRegex = regexp.MustCompile(`\x1b\[[0-9\;]+m`) +var escapeCharRegex = regexp.MustCompile("[\u001B\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[a-zA-Z\\d]*)*)?\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PRZcf-ntqry=><~]))") // ////////////////////////////////////////////////////////////////////////////////// // @@ -101,11 +101,11 @@ func (c *OutputContainer) Tail(lines int) string { } if line == lines { - return strings.TrimRight(string(data[i+1:]), " \n\r") + return strings.Trim(string(data[i+1:]), " \n\r") } } - return strings.TrimRight(string(data), " \n\r") + return strings.Trim(string(data), " \n\r") } // IsEmpty returns true if container is empty @@ -156,5 +156,6 @@ func fmtValue(v string) string { // sanitizeData removes escape characters func sanitizeData(data []byte) []byte { + data = bytes.ReplaceAll(data, []byte("\r"), nil) return escapeCharRegex.ReplaceAll(data, nil) } diff --git a/action/basic.go b/action/basic.go index 289ae5dd..b599a95f 100644 --- a/action/basic.go +++ b/action/basic.go @@ -29,7 +29,7 @@ func Wait(action *recipe.Action) error { return err } - durSec = mathutil.BetweenF64(durSec, 0.01, 3600.0) + durSec = mathutil.Between(durSec, 0.01, 3600.0) time.Sleep(timeutil.SecondsToDuration(durSec)) diff --git a/action/fs.go b/action/fs.go index 382de2d0..ce116e44 100644 --- a/action/fs.go +++ b/action/fs.go @@ -604,7 +604,7 @@ func Cleanup(action *recipe.Action) error { err = os.RemoveAll(obj) if err != nil { - return fmt.Errorf("Can't remove object %q: %v", err) + return fmt.Errorf("Can't remove object %q: %v", obj, err) } } diff --git a/action/http.go b/action/http.go index 4f976c17..dc48b84c 100644 --- a/action/http.go +++ b/action/http.go @@ -297,16 +297,7 @@ func HTTPSetHeader(action *recipe.Action) error { // ////////////////////////////////////////////////////////////////////////////////// // -// isHTTPMethodSupported returns true if HTTP method is supported -func isHTTPMethodSupported(method string) bool { - switch method { - case req.GET, req.POST, req.DELETE, req.PUT, req.PATCH, req.HEAD: - return true - } - - return false -} - +// checkRequestData checks request data func checkRequestData(method, payload string) error { switch method { case req.GET, req.POST, req.DELETE, req.PUT, req.PATCH, req.HEAD: @@ -353,6 +344,6 @@ func makeHTTPRequest(action *recipe.Action, method, url, payload string) *req.Re // parseJSONQuery converts json query to slice func parseJSONQuery(q string) []string { - q = strings.Replace(q, "[", ".[", -1) + q = strings.ReplaceAll(q, "[", ".[") return strings.Split(q, ".") } diff --git a/action/io.go b/action/io.go index 3e809804..e22e350a 100644 --- a/action/io.go +++ b/action/io.go @@ -48,7 +48,7 @@ func Expect(action *recipe.Action, output *OutputContainer) error { } start := time.Now() - timeout = mathutil.BetweenF64(timeout, 0.01, 3600.0) + timeout = mathutil.Between(timeout, 0.01, 3600.0) timeoutDur := timeutil.SecondsToDuration(timeout) for range time.NewTicker(_DATA_READ_PERIOD).C { @@ -98,7 +98,7 @@ func Input(action *recipe.Action, input *os.File, output *OutputContainer) error } if !strings.HasSuffix(text, "\n") { - text = text + "\n" + text += "\n" } output.Purge() diff --git a/action/service.go b/action/service.go index 4124ed65..377fd9b8 100644 --- a/action/service.go +++ b/action/service.go @@ -109,7 +109,7 @@ func WaitService(action *recipe.Action) error { } start := time.Now() - timeout = mathutil.BetweenF64(timeout, 0.01, 3600.0) + timeout = mathutil.Between(timeout, 0.01, 3600.0) timeoutDur := timeutil.SecondsToDuration(timeout) for range time.NewTicker(time.Second / 2).C { diff --git a/action/system.go b/action/system.go index e83adc7c..f5370e7e 100644 --- a/action/system.go +++ b/action/system.go @@ -87,7 +87,7 @@ func WaitPID(action *recipe.Action) error { } start := time.Now() - timeout = mathutil.BetweenF64(timeout, 0.01, 3600.0) + timeout = mathutil.Between(timeout, 0.01, 3600.0) timeoutDur := timeutil.SecondsToDuration(timeout) for range time.NewTicker(25 * time.Millisecond).C { @@ -147,7 +147,7 @@ func WaitFS(action *recipe.Action) error { } start := time.Now() - timeout = mathutil.BetweenF64(timeout, 0.01, 3600.0) + timeout = mathutil.Between(timeout, 0.01, 3600.0) timeoutDur := timeutil.SecondsToDuration(timeout) for range time.NewTicker(25 * time.Millisecond).C { @@ -203,7 +203,7 @@ func WaitConnect(action *recipe.Action) error { } start := time.Now() - timeout = mathutil.BetweenF64(timeout, 0.01, 3600.0) + timeout = mathutil.Between(timeout, 0.01, 3600.0) timeoutDur := timeutil.SecondsToDuration(timeout) for range time.NewTicker(25 * time.Millisecond).C { @@ -264,7 +264,7 @@ func Connect(action *recipe.Action) error { timeout = 1.0 } - timeout = mathutil.BetweenF64(timeout, 0.01, 3600.0) + timeout = mathutil.Between(timeout, 0.01, 3600.0) timeoutDur := timeutil.SecondsToDuration(timeout) conn, err := net.DialTimeout(network, address, timeoutDur) diff --git a/action/template.go b/action/template.go new file mode 100644 index 00000000..ce4b4468 --- /dev/null +++ b/action/template.go @@ -0,0 +1,114 @@ +package action + +// ////////////////////////////////////////////////////////////////////////////////// // +// // +// Copyright (c) 2023 ESSENTIAL KAOS // +// Apache License, Version 2.0 // +// // +// ////////////////////////////////////////////////////////////////////////////////// // + +import ( + "fmt" + "os" + "strconv" + "text/template" + + "github.com/essentialkaos/bibop/recipe" +) + +// ////////////////////////////////////////////////////////////////////////////////// // + +// varWrapper is a recipe wrapper for accessing variables +type varWrapper struct { + r *recipe.Recipe +} + +// ////////////////////////////////////////////////////////////////////////////////// // + +// Template is action processor for "template" +func Template(action *recipe.Action) error { + mode := uint64(0644) + source, err := action.GetS(0) + + if err != nil { + return err + } + + dest, err := action.GetS(1) + + if err != nil { + return err + } + + isSafePath, err := checkPathSafety(action.Command.Recipe, source) + + if err != nil { + return err + } + + if !isSafePath { + return fmt.Errorf("Action uses unsafe path (%s)", source) + } + + isSafePath, err = checkPathSafety(action.Command.Recipe, dest) + + if err != nil { + return err + } + + if !isSafePath { + return fmt.Errorf("Action uses unsafe path (%s)", dest) + } + + if action.Has(2) { + modeStr, _ := action.GetS(1) + mode, err = strconv.ParseUint(modeStr, 8, 32) + + if err != nil { + return err + } + } + + tmplData, err := os.ReadFile(source) + + if err != nil { + fmt.Errorf("Can't read template %q: %v", source, err) + } + + tmpl, err := template.New("").Parse(string(tmplData)) + + if err != nil { + return fmt.Errorf("Can't parse template %q: %v", source, err) + } + + fd, err := os.OpenFile(dest, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, os.FileMode(mode)) + + if err != nil { + return fmt.Errorf("Can't save template data into %q: %v", dest, err) + } + + defer fd.Close() + + vw := &varWrapper{action.Command.Recipe} + err = tmpl.Execute(fd, vw) + + if err != nil { + return fmt.Errorf("Can't render template %q: %v", source, err) + } + + return nil +} + +// ////////////////////////////////////////////////////////////////////////////////// // + +// Var returns variable value +func (vw *varWrapper) Var(name string) string { + return vw.r.GetVariable(name, true) +} + +// Is compares variable value +func (vw *varWrapper) Is(name, value string) bool { + return vw.r.GetVariable(name, true) == value +} + +// ////////////////////////////////////////////////////////////////////////////////// // diff --git a/cli/barcode.go b/cli/barcode.go index 3f08d816..ce2f092f 100644 --- a/cli/barcode.go +++ b/cli/barcode.go @@ -55,7 +55,7 @@ func getPackagesInfo(pkgs []string) ([]byte, error) { // getRPMPackagesInfo returns info about installed packages from rpm func getRPMPackagesInfo(pkgs []string) ([]byte, error) { - cmd := exec.Command("rpm", "-q", "--queryformat", "%{FILEMD5S}\n") + cmd := exec.Command("rpm", "-q", "--qf", "%{pkgid}\n") cmd.Env = []string{"LC_ALL=C"} cmd.Args = append(cmd.Args, pkgs...) diff --git a/cli/cli.go b/cli/cli.go index 8c773f18..96f16c55 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -18,7 +18,6 @@ import ( "github.com/essentialkaos/ek/v12/fmtutil/panel" "github.com/essentialkaos/ek/v12/fmtutil/table" "github.com/essentialkaos/ek/v12/fsutil" - "github.com/essentialkaos/ek/v12/mathutil" "github.com/essentialkaos/ek/v12/options" "github.com/essentialkaos/ek/v12/req" "github.com/essentialkaos/ek/v12/strutil" @@ -41,7 +40,7 @@ import ( // Application info const ( APP = "bibop" - VER = "7.5.0" + VER = "8.0.0" DESC = "Utility for testing command-line tools" ) @@ -56,6 +55,7 @@ const ( OPT_BARCODE = "B:barcode" OPT_EXTRA = "X:extra" OPT_TIME = "T:time" + OPT_PAUSE = "P:pause" OPT_FORMAT = "f:format" OPT_DIR = "d:dir" OPT_PATH = "p:path" @@ -81,8 +81,9 @@ var optMap = options.Map{ OPT_LIST_PACKAGES_FLAT: {Type: options.BOOL}, OPT_VARIABLES: {Type: options.BOOL}, OPT_BARCODE: {Type: options.BOOL}, - OPT_EXTRA: {Type: options.MIXED}, + OPT_EXTRA: {Type: options.INT, Value: 10, Min: 1, Max: 256}, OPT_TIME: {Type: options.BOOL}, + OPT_PAUSE: {Type: options.FLOAT, Max: 60}, OPT_FORMAT: {}, OPT_DIR: {}, OPT_PATH: {}, @@ -261,7 +262,7 @@ func process(file string) { switch { case options.GetB(OPT_LIST_PACKAGES), options.GetB(OPT_LIST_PACKAGES_FLAT): - listPackages(r.Packages) + listPackages(r) os.Exit(0) case options.GetB(OPT_VARIABLES): listVariables(r) @@ -278,13 +279,11 @@ func process(file string) { cfg := &executor.Config{ Quiet: options.GetB(OPT_QUIET), DisableCleanup: options.GetB(OPT_NO_CLEANUP), + DebugLines: options.GetI(OPT_EXTRA), + Pause: options.GetF(OPT_PAUSE), ErrsDir: errDir, } - if options.GetB(OPT_EXTRA) { - cfg.DebugLines = mathutil.Max(10, options.GetI(OPT_EXTRA)) - } - e := executor.NewExecutor(cfg) tags := strutil.Fields(options.GetS(OPT_TAG)) @@ -320,16 +319,16 @@ func validate(e *executor.Executor, r *recipe.Recipe, tags []string) { } // listPackages shows list packages required by recipe -func listPackages(pkgs []string) { - if len(pkgs) == 0 { +func listPackages(r *recipe.Recipe) { + if len(r.Packages) == 0 { return } if options.GetB(OPT_LIST_PACKAGES_FLAT) { - fmt.Println(strings.Join(pkgs, " ")) + fmt.Println(strings.Join(r.Packages, " ")) } else { fmtc.If(!rawOutput).NewLine() - for _, pkg := range pkgs { + for _, pkg := range r.Packages { fmtc.If(!rawOutput).Printf("{s-}•{!} %s\n", pkg) fmtc.If(rawOutput).Printf("%s\n", pkg) } @@ -362,11 +361,12 @@ func getValidationConfig(tags []string) *executor.ValidationConfig { if options.GetB(OPT_DRY_RUN) { vc.IgnoreDependencies = true + vc.IgnorePackages = true vc.IgnorePrivileges = true } if options.GetB(OPT_IGNORE_PACKAGES) { - vc.IgnoreDependencies = true + vc.IgnorePackages = true } return vc @@ -430,11 +430,11 @@ func printCompletion() int { switch options.GetS(OPT_COMPLETION) { case "bash": - fmt.Printf(bash.Generate(info, "bibop", "recipe")) + fmt.Print(bash.Generate(info, "bibop", "recipe")) case "fish": - fmt.Printf(fish.Generate(info, "bibop")) + fmt.Print(fish.Generate(info, "bibop")) case "zsh": - fmt.Printf(zsh.Generate(info, optMap, "bibop", "*.recipe")) + fmt.Print(zsh.Generate(info, optMap, "bibop", "*.recipe")) default: return 1 } @@ -444,12 +444,7 @@ func printCompletion() int { // printMan prints man page func printMan() { - fmt.Println( - man.Generate( - genUsage(), - genAbout(""), - ), - ) + fmt.Println(man.Generate(genUsage(), genAbout(""))) } // genUsage generates usage info @@ -459,7 +454,8 @@ func genUsage() *usage.Info { info.AppNameColorTag = "{*}" + colorTagApp info.AddOption(OPT_DRY_RUN, "Parse and validate recipe") - info.AddOption(OPT_EXTRA, "Print the last lines from command output if action was failed", "?lines") + info.AddOption(OPT_EXTRA, "Number of output lines for failed action {s-}(default: 10){!}", "lines") + info.AddOption(OPT_PAUSE, "Pause between commands in seconds", "duration") info.AddOption(OPT_LIST_PACKAGES, "List required packages") info.AddOption(OPT_LIST_PACKAGES_FLAT, "List required packages in one line {s-}(useful for scripts){!}") info.AddOption(OPT_VARIABLES, "List recipe variables") diff --git a/cli/executor/executor.go b/cli/executor/executor.go index ffa34ff2..99d4c3c1 100644 --- a/cli/executor/executor.go +++ b/cli/executor/executor.go @@ -28,7 +28,7 @@ import ( "github.com/essentialkaos/ek/v12/timeutil" "github.com/essentialkaos/ek/v12/tmp" - "github.com/google/goterm/term" + "github.com/creack/pty" "github.com/essentialkaos/bibop/action" "github.com/essentialkaos/bibop/recipe" @@ -55,6 +55,7 @@ type Executor struct { // ExecutorConfig contains executor configuration type Config struct { ErrsDir string + Pause float64 DebugLines int Quiet bool DisableCleanup bool @@ -63,6 +64,7 @@ type Config struct { // ValidationConfig is config for validation type ValidationConfig struct { Tags []string + IgnorePackages bool IgnoreDependencies bool IgnorePrivileges bool } @@ -71,7 +73,13 @@ type ValidationConfig struct { type CommandEnv struct { cmd *exec.Cmd output *action.OutputContainer - pty *term.PTY + term *PTY +} + +// PTY contains pseudo-terminal structs +type PTY struct { + pty *os.File + tty *os.File } // ////////////////////////////////////////////////////////////////////////////////// // @@ -137,6 +145,7 @@ var handlers = map[string]action.Handler{ recipe.ACTION_LIB_EXPORTED: action.LibExported, recipe.ACTION_PYTHON2_PACKAGE: action.Python2Package, recipe.ACTION_PYTHON3_PACKAGE: action.Python3Package, + recipe.ACTION_TEMPLATE: action.Template, } var temp *tmp.Temp @@ -163,10 +172,14 @@ func (e *Executor) Validate(r *recipe.Recipe, cfg *ValidationConfig) []error { errs.Add(checkRecipePrivileges(r)) } - if !cfg.IgnoreDependencies { + if !cfg.IgnorePackages { errs.Add(checkPackages(r)) } + if !cfg.IgnoreDependencies { + errs.Add(checkDependencies(r)) + } + if !errs.HasErrors() { return nil } @@ -201,6 +214,23 @@ func (e *Executor) Run(rr render.Renderer, r *recipe.Recipe, tags []string) bool // ////////////////////////////////////////////////////////////////////////////////// // +// Close closes tty and pty +func (t *PTY) Close() { + if t == nil { + return + } + + if t.pty != nil { + t.pty.Close() + } + + if t.tty != nil { + t.pty.Close() + } +} + +// ////////////////////////////////////////////////////////////////////////////////// // + // applyRecipeOptions applies recipe options to executor func applyRecipeOptions(e *Executor, rr render.Renderer, r *recipe.Recipe) { if r.HTTPSSkipVerify { @@ -254,7 +284,9 @@ func processRecipe(e *Executor, rr render.Renderer, r *recipe.Recipe, tags []str rr.CommandDone(command, isLastCommand) } - if r.Delay > 0 { + if e.config.Pause > 0 { + time.Sleep(timeutil.SecondsToDuration(e.config.Pause)) + } else if r.Delay > 0 { time.Sleep(timeutil.SecondsToDuration(r.Delay)) } } @@ -290,7 +322,7 @@ func runCommand(e *Executor, rr render.Renderer, c *recipe.Command) bool { } if err != nil { - if !e.config.Quiet && e.config.DebugLines > 0 && cmdEnv != nil && !cmdEnv.output.IsEmpty() { + if !e.config.Quiet && cmdEnv != nil && !cmdEnv.output.IsEmpty() { fmtc.NewLine() panel.Panel( "☴ OUTPUT", "{y}", @@ -319,7 +351,7 @@ func execCommand(c *recipe.Command) (*CommandEnv, error) { return nil, err } - cmdEnv.pty, err = createPseudoTerminal(cmdEnv.cmd) + cmdEnv.term, err = createPTY(cmdEnv.cmd) if err != nil { return nil, err @@ -332,6 +364,7 @@ func execCommand(c *recipe.Command) (*CommandEnv, error) { err = cmdEnv.cmd.Start() if err != nil { + cmdEnv.term.Close() return nil, err } @@ -408,7 +441,7 @@ func runAction(a *recipe.Action, cmdEnv *CommandEnv) error { case recipe.ACTION_EXPECT: return action.Expect(a, cmdEnv.output) case recipe.ACTION_PRINT: - return action.Input(a, cmdEnv.pty.Master, cmdEnv.output) + return action.Input(a, cmdEnv.term.pty, cmdEnv.output) case recipe.ACTION_WAIT_OUTPUT: return action.WaitOutput(a, cmdEnv.output) case recipe.ACTION_OUTPUT_CONTAINS: @@ -436,25 +469,20 @@ func runAction(a *recipe.Action, cmdEnv *CommandEnv) error { return handler(a) } -// createPseudoTerminal creates pseudo-terminal -func createPseudoTerminal(cmd *exec.Cmd) (*term.PTY, error) { - pty, err := term.OpenPTY() +// createPTY creates pseudo-terminal +func createPTY(cmd *exec.Cmd) (*PTY, error) { + p, t, err := pty.Open() if err != nil { return nil, err } - termios := &term.Termios{} - termios.Raw() - termios.Set(pty.Slave) - + cmd.Stdin, cmd.Stdout, cmd.Stderr = t, t, t cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true, Setctty: true} - cmd.Stdin = pty.Slave - cmd.Stdout = pty.Slave - cmd.Stderr = pty.Slave + pty.Setsize(p, &pty.Winsize{Rows: 80, Cols: 256}) - return pty, nil + return &PTY{pty: p, tty: t}, nil } // outputIOLoop reads data from reader and writes it to output store @@ -462,14 +490,15 @@ func outputIOLoop(cmdEnv *CommandEnv) { buf := make([]byte, 8192) for { - n, _ := cmdEnv.pty.Master.Read(buf[:cap(buf)]) + n, _ := cmdEnv.term.pty.Read(buf[:cap(buf)]) if n > 0 { cmdEnv.output.Write(buf[:n]) + continue } if cmdEnv.cmd.ProcessState != nil && cmdEnv.cmd.ProcessState.Exited() { - cmdEnv.pty.Close() + cmdEnv.term.Close() return } } diff --git a/cli/executor/validators.go b/cli/executor/validators.go index a4393ad9..25f5b0ca 100644 --- a/cli/executor/validators.go +++ b/cli/executor/validators.go @@ -121,7 +121,7 @@ func checkRecipeVariables(r *recipe.Recipe) []error { return errs } -// checkPackages checks packages +// checkPackages checks if required packages are installed on the system func checkPackages(r *recipe.Recipe) []error { if len(r.Packages) == 0 { return nil @@ -137,6 +137,67 @@ func checkPackages(r *recipe.Recipe) []error { return []error{errors.New("Can't check required packages availability: Unsupported OS")} } +// checkDependencies checks if all required binaries are present on the system +func checkDependencies(r *recipe.Recipe) []error { + var errs []error + + binCache := make(map[string]bool) + + for _, c := range r.Commands { + for _, a := range c.Actions { + var binary string + + switch a.Name { + case recipe.ACTION_SERVICE_PRESENT: + binary = "systemctl" + case recipe.ACTION_SERVICE_ENABLED: + binary = "systemctl" + case recipe.ACTION_SERVICE_WORKS: + binary = "systemctl" + case recipe.ACTION_WAIT_SERVICE: + binary = "systemctl" + case recipe.ACTION_LIB_LOADED: + binary = "ldconfig" + case recipe.ACTION_LIB_CONFIG: + binary = "pkg-config" + case recipe.ACTION_LIB_LINKED: + binary = "readelf" + case recipe.ACTION_LIB_RPATH: + binary = "readelf" + case recipe.ACTION_LIB_SONAME: + binary = "readelf" + case recipe.ACTION_LIB_EXPORTED: + binary = "nm" + case recipe.ACTION_PYTHON2_PACKAGE: + binary = "python" + case recipe.ACTION_PYTHON3_PACKAGE: + binary = "python3" + } + + if !hasBinary(binCache, binary) { + errs = append(errs, fmt.Errorf( + "Line %d: Action %q requires %q binary", a.Line, a.Name, binary, + )) + } + } + } + + return errs +} + +// hasBinary checks if binary is present on the system +func hasBinary(binCache map[string]bool, binary string) bool { + isExist, ok := binCache[binary] + + if ok { + return isExist + } + + binCache[binary] = env.Which(binary) != "" + + return binCache[binary] +} + // getDynamicVars returns slice with dynamic vars func getDynamicVars(a *recipe.Action) []string { switch a.Name { diff --git a/cli/support/support.go b/cli/support/support.go index 9374af07..cfdd9493 100644 --- a/cli/support/support.go +++ b/cli/support/support.go @@ -11,6 +11,7 @@ import ( "fmt" "os" "runtime" + "runtime/debug" "strings" "github.com/essentialkaos/ek/v12/fmtc" @@ -67,6 +68,10 @@ func showApplicationInfo(app, ver, gitRev string) { runtime.GOOS, runtime.GOARCH, )) + if gitRev == "" { + gitRev = extractGitRevFromBuildInfo() + } + if gitRev != "" { if !fmtc.DisableColors && fmtc.IsTrueColorSupported() { printInfo(7, "Git SHA", gitRev+getHashColorBullet(gitRev)) @@ -107,6 +112,23 @@ func showDepsInfo(gomod []byte) { } } +// extractGitRevFromBuildInfo extracts git SHA from embedded build info +func extractGitRevFromBuildInfo() string { + info, ok := debug.ReadBuildInfo() + + if !ok { + return "" + } + + for _, s := range info.Settings { + if s.Key == "vcs.revision" && len(s.Value) > 7 { + return s.Value[:7] + } + } + + return "" +} + // getHashColorBullet return bullet with color from hash func getHashColorBullet(v string) string { if len(v) > 6 { @@ -118,7 +140,7 @@ func getHashColorBullet(v string) string { // printInfo formats and prints info record func printInfo(size int, name, value string) { - name = name + ":" + name += ":" size++ if value == "" { diff --git a/cli/support/support_linux.go b/cli/support/support_linux.go index 99f08a05..ef1c5671 100644 --- a/cli/support/support_linux.go +++ b/cli/support/support_linux.go @@ -15,6 +15,7 @@ import ( "github.com/essentialkaos/ek/v12/fmtutil" "github.com/essentialkaos/ek/v12/fsutil" "github.com/essentialkaos/ek/v12/system" + "github.com/essentialkaos/ek/v12/system/container" ) // ////////////////////////////////////////////////////////////////////////////////// // @@ -26,13 +27,14 @@ func showOSInfo() { if err == nil { fmtutil.Separator(false, "OS INFO") - printInfo(12, "Name", osInfo.Name) - printInfo(12, "Pretty Name", osInfo.PrettyName) - printInfo(12, "Version", osInfo.VersionID) + printInfo(12, "Name", osInfo.ColoredName()) + printInfo(12, "Pretty Name", osInfo.ColoredPrettyName()) + printInfo(12, "Version", osInfo.Version) printInfo(12, "ID", osInfo.ID) printInfo(12, "ID Like", osInfo.IDLike) printInfo(12, "Version ID", osInfo.VersionID) printInfo(12, "Version Code", osInfo.VersionCodename) + printInfo(12, "Platform ID", osInfo.PlatformID) printInfo(12, "CPE", osInfo.CPEName) } @@ -40,11 +42,9 @@ func showOSInfo() { if err != nil { return - } else { - if osInfo == nil { - fmtutil.Separator(false, "SYSTEM INFO") - printInfo(12, "Name", systemInfo.OS) - } + } else if osInfo == nil { + fmtutil.Separator(false, "SYSTEM INFO") + printInfo(12, "Name", systemInfo.OS) } printInfo(12, "Arch", systemInfo.Arch) @@ -52,11 +52,13 @@ func showOSInfo() { containerEngine := "No" - switch { - case fsutil.IsExist("/.dockerenv"): + switch container.GetEngine() { + case container.DOCKER: containerEngine = "Yes (Docker)" - case fsutil.IsExist("/run/.containerenv"): + case container.PODMAN: containerEngine = "Yes (Podman)" + case container.LXC: + containerEngine = "Yes (LXC)" } fmtc.NewLine() diff --git a/go.mod b/go.mod index 86436757..da5d1708 100644 --- a/go.mod +++ b/go.mod @@ -4,15 +4,15 @@ go 1.18 require ( github.com/buger/jsonparser v1.1.1 + github.com/creack/pty v1.1.20 github.com/essentialkaos/check v1.4.0 github.com/essentialkaos/depsy v1.1.0 - github.com/essentialkaos/ek/v12 v12.79.0 - github.com/google/goterm v0.0.0-20200907032337-555d40f16ae2 + github.com/essentialkaos/ek/v12 v12.83.2 ) require ( github.com/kr/pretty v0.3.1 // indirect github.com/kr/text v0.2.0 // indirect github.com/rogpeppe/go-internal v1.10.0 // indirect - golang.org/x/sys v0.12.0 // indirect + golang.org/x/sys v0.13.0 // indirect ) diff --git a/go.sum b/go.sum index e9ebc632..6920c7dc 100644 --- a/go.sum +++ b/go.sum @@ -1,14 +1,14 @@ github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.20 h1:VIPb/a2s17qNeQgDnkfZC35RScx+blkKF8GV68n80J4= +github.com/creack/pty v1.1.20/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/essentialkaos/check v1.4.0 h1:kWdFxu9odCxUqo1NNFNJmguGrDHgwi3A8daXX1nkuKk= github.com/essentialkaos/check v1.4.0/go.mod h1:LMKPZ2H+9PXe7Y2gEoKyVAwUqXVgx7KtgibfsHJPus0= github.com/essentialkaos/depsy v1.1.0 h1:U6dp687UkQwXlZU17Hg2KMxbp3nfZAoZ8duaeUFYvJI= github.com/essentialkaos/depsy v1.1.0/go.mod h1:kpiTAV17dyByVnrbNaMcZt2jRwvuXClUYOzpyJQwtG8= -github.com/essentialkaos/ek/v12 v12.79.0 h1:Dq/bCqk8/N5h/r5jJA2UHc1YoUEVYcc7xnR0DI5L9wA= -github.com/essentialkaos/ek/v12 v12.79.0/go.mod h1:S9/XSKhEAdylL3PF8GAnUeKKyd92VrDGR4YGacHfz0c= -github.com/google/goterm v0.0.0-20200907032337-555d40f16ae2 h1:CVuJwN34x4xM2aT4sIKhmeib40NeBPhRihNjQmpJsA4= -github.com/google/goterm v0.0.0-20200907032337-555d40f16ae2/go.mod h1:nOFQdrUlIlx6M6ODdSpBj1NVA+VgLC6kmw60mkw34H4= +github.com/essentialkaos/ek/v12 v12.83.2 h1:gXFwLIBAZsdi5uT/vJj9ka/rd94jLR1NF6OGxAYbgkQ= +github.com/essentialkaos/ek/v12 v12.83.2/go.mod h1:X0gkyjBCP4QiD+sV4D52aquLDLGUmHteMEL7Rsgbev0= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -17,5 +17,5 @@ github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsK github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= -golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/parser/parser.go b/parser/parser.go index 6fd04967..79b7c9a3 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -273,7 +273,7 @@ func getTokenInfo(keyword string) recipe.TokenInfo { // isUselessRecipeLine return if line doesn't contains recipe data func isUselessRecipeLine(line string) bool { // Skip empty lines - if line == "" || strings.Replace(line, " ", "", -1) == "" { + if line == "" || strings.ReplaceAll(line, " ", "") == "" { return true } diff --git a/recipe/recipe_test.go b/recipe/recipe_test.go index 1d60b7c5..ff471bfa 100644 --- a/recipe/recipe_test.go +++ b/recipe/recipe_test.go @@ -284,7 +284,7 @@ func (s *RecipeSuite) TestPythonVariables(c *C) { python3Bin = "_unknown_" c.Assert(evalPythonCode(3, "test"), Equals, "") - c.Assert(getPythonBindingSuffix(), Equals, "") + c.Assert(getPythonBindingSuffix(3), Equals, "") python3Bin = "python3" } diff --git a/recipe/runtime_variables.go b/recipe/runtime_variables.go index ec33575b..e8b9e538 100644 --- a/recipe/runtime_variables.go +++ b/recipe/runtime_variables.go @@ -8,7 +8,6 @@ package recipe // ////////////////////////////////////////////////////////////////////////////////// // import ( - "fmt" "os" "os/exec" "strconv" @@ -153,7 +152,7 @@ func getRuntimeVariable(name string, r *Recipe) string { dynVarCache[name] = getPythonSiteArch(3) case "PYTHON3_BINDING_SUFFIX": - dynVarCache[name] = getPythonBindingSuffix() + dynVarCache[name] = getPythonBindingSuffix(3) case "LIBDIR": dynVarCache[name] = getLibDir(false) @@ -195,21 +194,8 @@ func getPythonSiteArch(majorVersion int) string { } // getPythonBindingSuffix returns suffix for Python bindings -func getPythonBindingSuffix() string { - version := getPythonVersion(3) - - if version == "" { - return "" - } - - version = strutil.Exclude(version, ".") - systemInfo := getSystemInfo() - - if systemInfo == nil { - return "" - } - - return fmt.Sprintf(".cpython-%sm-%s-linux-gnu.so", version, systemInfo.Arch) +func getPythonBindingSuffix(majorVersion int) string { + return evalPythonCode(majorVersion, `import sysconfig; print(sysconfig.get_config_var("EXT_SUFFIX"))`) } // evalPythonCode evaluates Python code diff --git a/recipe/tokens.go b/recipe/tokens.go index 64f3340e..58e55db0 100644 --- a/recipe/tokens.go +++ b/recipe/tokens.go @@ -106,6 +106,8 @@ const ( ACTION_PYTHON2_PACKAGE = "python2-package" ACTION_PYTHON3_PACKAGE = "python3-package" + + ACTION_TEMPLATE = "template" ) // ////////////////////////////////////////////////////////////////////////////////// // @@ -217,4 +219,6 @@ var Tokens = []TokenInfo{ {ACTION_PYTHON2_PACKAGE, 1, 1, false, false}, {ACTION_PYTHON3_PACKAGE, 1, 1, false, false}, + + {ACTION_TEMPLATE, 2, 3, false, false}, } diff --git a/render/renderer_terminal.go b/render/renderer_terminal.go index cc8a1820..8cb508f7 100644 --- a/render/renderer_terminal.go +++ b/render/renderer_terminal.go @@ -187,7 +187,7 @@ func (rr *TerminalRenderer) Result(passes, fails, skips int) { } d := rr.formatDuration(time.Since(rr.start), true) - d = strings.Replace(d, ".", "{s-}.", -1) + "{!}" + d = strings.ReplaceAll(d, ".", "{s-}.") + "{!}" fmtc.NewLine() fmtc.Println(" {*}Duration:{!} " + d) diff --git a/render/renderer_xml.go b/render/renderer_xml.go index f363e15d..e2b22599 100644 --- a/render/renderer_xml.go +++ b/render/renderer_xml.go @@ -150,9 +150,9 @@ func (rr *XMLRenderer) Result(passes, fails, skips int) { // ////////////////////////////////////////////////////////////////////////////////// // func (rr *XMLRenderer) escapeData(data string) string { - data = strings.Replace(data, "<", "<", -1) - data = strings.Replace(data, ">", ">", -1) - data = strings.Replace(data, "&", "&", -1) + data = strings.ReplaceAll(data, "<", "<") + data = strings.ReplaceAll(data, ">", ">") + data = strings.ReplaceAll(data, "&", "&") return data } diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 00000000..315225da --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,157 @@ +### `bibop` scripts + +- `bibop-dep` — utility for installing/uninstalling recipe dependecnies +- `bibop-docker` — `bibop` docker/podman wrapper +- `bibop-libtest-gen` — utility for generating compilation tests for libraries +- `bibop-libtest-gen` — utility listing linked shared libraries +- `bibop-massive` — utility for mass package testing +- `bibop-multi-check` — utility for checking different versions of package +- `bibop-so-exported` — utility for generating exported symbols tests + +#### `bibop-dep` + +``` +Usage: bibop-dep {options} {action} + +Actions + + install, i Install packages + reinstall, r Reinstall packages + uninstall, u Uninstall packages + +Options + + --enablerepo, -ER repo Enable repository + --disablerepo, -DR repo Disable repository + --yes, -y Automatically answer yes for all questions + --no-color, -nc Disable colors in output + --help, -h Show this help message + --version, -v Show information about version + +Examples + + bibop-dep install -ER kaos-testing myapp.recipe + Install packages for myapp recipe with enabled kaos-testing repository + + bibop-dep install -ER kaos-testing,epel,cbr -y myapp.recipe + Install packages for myapp recipe with enabled repositories + + bibop-dep uninstall + Uninstall all packages installed by previous transaction +``` + +#### `bibop-libtest-gen` + +``` +Usage: bibop-libtest-gen {options} devel-package + +Options + + --list-libs, -L List all libs in package + --output, -o name Output source file (default: test.c) + --lib, -l name Lib name + --no-color, -nc Disable colors in output + --help, -h Show this help message + --version, -v Show information about version + +Examples + + bibop-libtest-gen dirac-devel-1.0.2-15.el7.x86_64.rpm + Generate test.c with all required headers for RPM package + + bibop-libtest-gen dirac-devel + Generate test.c with all required headers for installed package +``` + +#### `bibop-linked` + +``` +Usage: bibop-linked {options} binary-file + +Options + + --no-color, -nc Disable colors in output + --help, -h Show this help message + --version, -v Show information about version + +Examples + + bibop-linked /usr/bin/curl + List required shared libraries for binary file + + bibop-linked /usr/lib64/libcurl.so.4 + List required shared libraries for other library +``` + +#### `bibop-massive` + +``` +Usage: bibop-massive {options} recipe… + +Options + + --validate, -V Just validate recipes + --recheck, -R Run only failed checks + --fresh, -F Clean all caches before run + --interrupt, -X Interrupt checks after first error + --barcode, -B Print unique barcode for every test + --enablerepo, -ER repo Enable repository + --disablerepo, -DR repo Disable repository + --error-dir, -e dir Path to directory with tests errors + --log, -l file Path to log file + --no-color, -nc Disable colors in output + --help, -h Show this help message + --version, -v Show information about version + +Examples + + bibop-massive ~/tests/ + Run all tests in given directory + + bibop-massive -ER kaos-testing ~/tests/package1.recipe ~/tests/package2.recipe + Run 2 tests with enabled repository 'kaos-testing' for installing packages + + bibop-massive -ER kaos-testing,epel,cbr ~/tests/package1.recipe + Run verbose test with enabled repositories for installing packages +``` + +#### `bibop-multi-check` + +``` +Usage: bibop-multi-check {options} recipe package-list + +Options + + --enablerepo, -ER repo Enable repository + --disablerepo, -DR repo Disable repository + --error-dir, -e dir Path to directory with tests errors + --log, -l file Path to log file + --no-color, -nc Disable colors in output + --help, -h Show this help message + --version, -v Show information about version + +Examples + + bibop-multi-check app.recipe package.list + Run tests for every package in list + + bibop-multi-check -ER kaos-testing,epel,cbr ~/tests/package1.recipe app.recipe package.list + Run tests with enabled repositories for installing packages +``` + +#### `bibop-so-exported` + +``` +Usage: bibop-so-exported {options} package-name + +Options + + --no-color, -nc Disable colors in output + --help, -h Show this help message + --version, -v Show information about version + +Examples + + bibop-so-exported zlib + Create tests for exported symbols for shared libraries in package zlib +``` diff --git a/scripts/bibop-linked b/scripts/bibop-linked index 129141af..33268f9f 100755 --- a/scripts/bibop-linked +++ b/scripts/bibop-linked @@ -178,9 +178,9 @@ usage() { show "" show "Options" $BOLD show "" - show " ${CL_GREEN}--no-color, -nc${CL_NORM} ${CL_DARK}..........${CL_NORM} Disable colors in output" - show " ${CL_GREEN}--help, -h${CL_NORM} ${CL_DARK}...............${CL_NORM} Show this help message" - show " ${CL_GREEN}--version, -v${CL_NORM} ${CL_DARK}............${CL_NORM} Show information about version" + show " ${CL_GREEN}--no-color, -nc${CL_NORM} ${CL_DARK}..${CL_NORM} Disable colors in output" + show " ${CL_GREEN}--help, -h${CL_NORM} ${CL_DARK}.......${CL_NORM} Show this help message" + show " ${CL_GREEN}--version, -v${CL_NORM} ${CL_DARK}....${CL_NORM} Show information about version" show "" show "Examples" $BOLD show "" diff --git a/scripts/bibop-massive b/scripts/bibop-massive index b1d0b73c..0c89025c 100755 --- a/scripts/bibop-massive +++ b/scripts/bibop-massive @@ -3,7 +3,7 @@ ################################################################################ APP="bibop-massive" -VER="1.11.0" +VER="1.12.0" DESC="Utility for mass package testing" ################################################################################ @@ -100,6 +100,9 @@ max_recipe_len=0 # Path to package manager log file pm_log="" +# Pre install transaction ID +preinstall_transaction_id="" + # Canceled flag is_canceled="" @@ -262,12 +265,12 @@ processRecipes() { for recipe in "$@" ; do if [[ -d $recipe ]] ; then - recipe_list=$(find "$recipe" -type f -name "*.recipe" | grep -vE '\-el[7-9]') + recipe_list=$(find "$recipe" -name "*.recipe") if [[ -n "$recipe_list" ]] ; then recipes="$recipes $recipe_list" fi - elif [[ -f $recipe && $recipe = *.recipe ]]; then + elif [[ -e $recipe && $recipe = *.recipe ]]; then recipes="$recipes $recipe" else error "Can't use $recipe as a recipe source" @@ -296,9 +299,14 @@ filterRecipes() { for recipe in $recipe_list ; do if [[ -n "$dist" && -z "$validate" ]] ; then - # Check for dist specific recipe - if [[ -f "${recipe%.recipe}-el${dist}.recipe" ]] ; then - recipe="${recipe%.recipe}-el${dist}.recipe" + if [[ $recipe =~ .*-el[7-9]\.recipe$ ]] ; then + if [[ "$recipe" != "${recipe%-el?.recipe}-el${dist}.recipe" ]] ; then + continue + fi + else + if [[ -e "${recipe%.recipe}-el${dist}.recipe" ]] ; then + continue + fi fi fi @@ -728,6 +736,8 @@ checkPackagesAvailability() { installPackages() { local opts pkg_count tmp_output status problems + preinstall_transaction_id=$(getLastTransactionID) + log "Installing packages → $*" truncate -s 0 "$pm_log" @@ -770,12 +780,16 @@ installPackages() { installPackagesVerbose() { local pkg_list="$*" + preinstall_transaction_id=$(getLastTransactionID) + show "Installing ${pkg_list// /, }…\n" $BOLD # shellcheck disable=SC2046 LC_ALL=C yum -y $(getPMOpts) install "$@" - return $? + status=$? + + return $status } # Uninstall required packages @@ -789,6 +803,11 @@ uninstallPackages() { truncate -s 0 "$pm_log" + if [[ "$preinstall_transaction_id" == "$(getLastTransactionID)" ]] ; then + log "No packages were installed, nothing to uninstall" + return $STATUS_OK + fi + tmp_output=$(mktemp) yum -y history undo last &> "$tmp_output" @@ -824,6 +843,11 @@ uninstallPackages() { uninstallPackagesVerbose() { show "Uninstalling packages…\n" $BOLD + if [[ "$preinstall_transaction_id" == "$(getLastTransactionID)" ]] ; then + show "No packages were installed, nothing to uninstall. Continue…" $GREY + return $STATUS_OK + fi + yum -y history undo last return $? @@ -956,6 +980,14 @@ cleanPMCache() { fi } +# Get ID of the latest transaction in history +# +# Code: No +# Echo: ID (Number) +getLastTransactionID() { + yum history list | grep -A2 'ID ' | tail -1 | tr -s ' ' | sed 's/^ \+//' | cut -f1 -d' ' +} + # Generate list of YUM/DNF options # # Code: No diff --git a/scripts/bibop-so-exported b/scripts/bibop-so-exported index 8a6b602b..5d60ecc0 100755 --- a/scripts/bibop-so-exported +++ b/scripts/bibop-so-exported @@ -204,9 +204,9 @@ usage() { show "" show "Options" $BOLD show "" - show " ${CL_GREEN}--no-color, -nc${CL_NORM} ${CL_DARK}..........${CL_NORM} Disable colors in output" - show " ${CL_GREEN}--help, -h${CL_NORM} ${CL_DARK}...............${CL_NORM} Show this help message" - show " ${CL_GREEN}--version, -v${CL_NORM} ${CL_DARK}............${CL_NORM} Show information about version" + show " ${CL_GREEN}--no-color, -nc${CL_NORM} ${CL_DARK}..${CL_NORM} Disable colors in output" + show " ${CL_GREEN}--help, -h${CL_NORM} ${CL_DARK}.......${CL_NORM} Show this help message" + show " ${CL_GREEN}--version, -v${CL_NORM} ${CL_DARK}....${CL_NORM} Show information about version" show "" show "Examples" $BOLD show ""