GithubHelp home page GithubHelp logo

mbland / go-script-bash Goto Github PK

View Code? Open in Web Editor NEW
93.0 12.0 16.0 1.05 MB

Framework for writing modular, discoverable, testable Bash scripts

License: ISC License

Shell 100.00%
bash testing unit-testing modular test-coverage coverage scripting portable

go-script-bash's Introduction

The ./go script: a unified development environment interface

Source: https://github.com/mbland/go-script-bash

Latest release License Continuous integration status for Linux and macOS Continuous integration status for Windows Coverage Status

A ./go script aims to abstract away many of the steps needed to develop (and sometimes deploy) a software project. It is a replacement for READMEs and other documents that may become out-of-date, and when maintained properly, should provide a cohesive and discoverable interface for common project tasks.

For a five-minute overview of the framework, see Mike Bland's go-script-bash lightning talk at Surge 2016.

Table of contents

Introduction

What's a ./go script?

The ./go script idea came from Pete Hodgson's blog posts In Praise of the ./go Script: Part I and Part II. To paraphrase Pete's original idea, rather than dump project setup, development, testing, and installation/deployment commands into a README that tends to get stale, or rely on oral tradition to transmit project maintenance knowledge, automate these tasks by encapsulating them all inside a single script in the root directory of your project source tree, conventionally named "go". Then the interface to these tasks becomes something like ./go setup, ./go test, and ./go deploy. Not only would this script save time for people already familiar with the project, but it smooths the learning curve, prevents common mistakes, and lowers friction for new contributors. This is as desirable a state for Open Source projects as it is for internal ones.

Is this related to the Go programming language?

No. The ./go script convention in general and this framework in particular are completely unrelated to the Go programming language. In fact, the actual ./go script can be named anything. However, the go command from the Go language distribution encapsulates many common project functions in a similar fashion.

Why write a framework?

Of course, the danger is that this ./go script may become as unwieldy as the README it's intended to replace, depending on the project's complexity. Even if it's heavily used and kept up-to-date, maintenance may become an intensive, frightening chore, especially if not covered by automated tests. Knowing what the script does, why it does it, and how to run it may become more and more challenging—resulting in the same friction, confusion, and fear the script was trying to avoid.

The ./go script framework makes it easy to provide a uniform and easy-to-use project maintenance interface that fits your project perfectly regardless of the mix of tools and languages, then it gets out of the way as fast as possible. The hope is that by making the right thing the easy thing, scripts using the framework will evolve and stay healthy along with the rest of your project sources, which makes everyone working with the code less frustrated and more productive all-around.

This framework accomplishes this by:

  • encouraging modular, composable ./go commands implemented as individual scripts—in the language of your choice!
  • providing a set of builtin utility commands and shell command aliases—see ./go help builtins and ./go help aliases
  • supporting automatic tab-completion of commands and arguments through a lightweight API—see ./go help env and ./go help complete
  • implementing a quick, flexible, robust, and convenient documentation system—document your script in the header, and help shows up automatically as ./go help my-command! See ./go help help.

Plus, its own tests serve as a model for testing command scripts of all shapes and sizes.

The inspiration for this model (and initial implementation hints) came from Sam Stephenson's rbenv Ruby version manager.

Why Bash?

It's the ultimate backstage pass! It's the default shell for most mainstream UNIX-based operating systems, easily installed on other UNIX-based operating systems, and is readily available even on Windows.

Will this work on Windows?

Yes. It is an explicit goal to make it as easy to use the framework on Windows as possible. Since Git for Windows in particular ships with Bash as part of its environment, and Bash is available within Windows 10 as part of the Windows Subsystem for Linux (Ubuntu on Windows), it's more likely than not that Bash is already available on a Windows developer's system. It's also available from the MSYS2 and Cygwin environments.

Why not use tool X instead?

Of course there are many common tools that may be used for managing project tasks. For example: Make, Rake, npm, Gulp, Grunt, Bazel, and the Go programming language's go tool. There are certainly more powerful scripting languages: Perl, Python, Ruby, and even Node.js is a possibility. There are even more powerful shells, such as the Z-Shell and the fish shell.

The ./go script framework isn't intended to replace all those other tools and languages, but to make it easier to use each of them for what they're good for. It makes it easier to write good, testable, maintainable, and extensible shell scripts so you don't have to push any of those other tools beyond their natural limits.

Bash scripting is really good for automating a lot of traditional command line tasks, and it can be pretty awkward to achieve the same effect using other tools—especially if your project uses a mix of languages, where using a tool common to one language environment to automate tasks in another can get weird. (Which is part of the reason why there are so many build tools tailored to different languages in the first place, to say nothing of the different languages themselves.)

If you want to incorporate different scripting languages or shells into your project maintenance, this framework makes it easy to do so. However, by starting with Bash, you can implement a ./go init command to check that these other languages or shells are installed and either install them automatically or prompt the user on how to do so. Since Bash is (almost certainly) already present, users can run your ./go script right away and get the setup or hints that they need, rather than wading through system requirements and documentation before being able to do anything.

Even if ./go init tells the user "go to this website and install this other thing", that's still an immediate, tactile experience that triggers a reward response and invites further exploration. (Think of Zork and the first "open mailbox" command.)

Where can I run it?

The real question is: Where can't you run it?

The core framework is written 100% in Bash and it's been tested under Bash 3.2, 4.2, 4.3, and 4.4 across OS X, Ubuntu Linux, Arch Linux, Alpine Linux, FreeBSD 9.3, FreeBSD 10.3, and Windows 10 (using all the environments described in the "Will this work on Windows?" section above).

Can I use it to write standalone programs that aren't project scripts?

Actually, yes. See the Standalone mode section below.

Also see the following question...

Can I have more than one ./go script in the same project source tree?

Yes. You can share one copy of the go-bash-framework sources, and even have common code in the lib/ directory, but set each script to use its own command scripts dir.

This may be especially useful if you're writing a standalone program, in which one script provides the actual program interface, and the other provides the development-only interface.

How is it tested?

The project's own ./go test command does it all. Combined with automatic tab-completion enabled by ./go env and pattern-matching via ./go glob, the ./go test command provides a convenient means of selecting subsets of test cases while focusing on a particular piece of behavior. (See ./go help test.)

The tests are written using mbland/bats, an optimized version of Sam Stephenson's Bash Automated Testing System (BATS). Code coverage comes from Simon Kagstrom's kcov code coverage tool, which not only provides code coverage for Bash scripts (!!!) but can push the results to Coveralls!

Environment setup

To run a ./go script that uses this module, or to add it to your own project, you must have Bash version 3.2 or greater installed on your system. Run bash --version to make sure Bash is in your PATH and is a compatible version. You should see output like this:

GNU bash, version 3.2.57(1)-release (x86_64-apple-darwin15)
Copyright (C) 2007 Free Software Foundation, Inc.

If you do not see this, follow the instructions in the Installing Bash section later in this document.

Note: While Bash is required to run this framework, your individual command scripts can be in any other interpreted language installed on the host system.

How to use this framework

First you'll need a copy of this framework available in your project sources. The most expedient way to bootstrap your program is to use the go-template file as a starting point (replacing curl with wget, fetch, or whichever tool you prefer):

$ curl https://raw.githubusercontent.com/mbland/go-script-bash/master/go-template >./go
$ chmod ugo+rx ./go

You may rename this file whatever you wish (i.e. it doesn't have to be named ./go), update its documentation and variables to fit your project, and check it into your project repository. See the go-template comments for details.

If you'd prefer to download a copy of the framework and check it into your sources, versioned archives are available from the go-script-bash Releases page. The archives for the current release are:

You can also add this repository to your project as a Git submodule:

$ git submodule add https://github.com/mbland/go-script-bash <target-dir>
$ git commit -m 'Add go-script-bash framework'
$ git submodule update --init

where <target-dir> is any point inside your project directory structure that you prefer.

If you're not using go-template, create a bash script in the root directory of your project to act as the main ./go script. This script need not be named go, but it must contain the following lines, with @go "$@" as the last line of the script:

. "${0%/*}/go-core.bash" "scripts"
@go "$@"

where:

  • ${0%/*} produces the path to the project's root directory based on the path to the ./go script
  • ${0%/*}/go-core.bash produces the path to the framework's go-core.bash file within your project's copy of the framework (adjusted to reflect where your copy of go-script-bash actually resides)
  • scripts is the path to the directory holding your project's command scripts relative to the project root (it can be any name you like)

Directory structure

The ./go script changes to the project root directory before executing any commands. That means every command script you write will also run within the project root directory, so every relative file and directory path will be interpreted as relative to the project root.

Your project structure may look something like this:

project-root/
  go - main ./go script
  lib/ - publicly-exported modules (if the project is a go-bash-script plugin)
  scripts/ (or bin/) - project (or plugin) ./go command scripts
    lib/ - project-specific Bash library modules (see "Modules" section)
    plugins/ - (optional) third-party command scripts (see `./go help plugins`)
      .../
        bin/ - plugin ./go command scripts
        lib/ - publicly-exported Bash library modules (see "Modules" section)
    go-script-bash/
      go-core.bash - top-level functions
      lib/ - publicly-exported Bash library modules (see "Modules" section)
      libexec/ - builtin ./go command scripts

This structure implies that the first line of your ./go script will be:

. "${0%/*}/scripts/go-script-bash/go-core.bash" "scripts"

Variables and plugin scoping

The following variables are set by the framework based on the above example (note there are many other variables set in go-core.bash and elsewhere; see ./go help vars):

  • _GO_ROOTDIR: /absolute/path/to/project-root
  • _GO_CORE_DIR: /absolute/path/to/project-root/scripts/go-script-bash
  • _GO_SCRIPTS_DIR: $_GO_ROOTDIR/scripts
  • _GO_PLUGINS_DIR: /absolute/path/to/project-root/plugins

For plugins, _GO_ROOTDIR and _GO_SCRIPTS_DIR will be scoped to the root directory of the plugin installation; the other variables will remain the same. See ./go help plugins for more details.

Command scripts

Each command script for your project residing in the scripts directory must adhere to the following conditions:

  • No filename extensions.
  • It must be executable, with a #! (a.k.a. "she-bang") line. The interpreter name will be parsed from this line, whether it is an absolute path (#!/bin/bash) or is of the form: #!/usr/bin/env bash.
  • If scripts/parent is a command script, subcommand scripts must reside within a directory named: scripts/parent.d.

Scripts can use any interpreted language available on the host system; they need not be written in Bash. Bash scripts will be sourced (i.e. imported into the same process running the ./go script itself). Other languages will use the PATH environment variable to discover the interpreter for the script.

See ./go help commands for details on the algorithm used to discover command scripts for execution.

Command summaries and help text

The builtin ./go help command will parse command script summaries and help text from the header comment block of each script. Run ./go help help to learn more about the formatting rules.

Tab completion

By evaluating the value of ./go env - within your shell, all builtin commands and aliases provide automatic tab completion of file, directory, and other arguments. If an implementation isn't available for your shell (within lib/internal/env/), it's very easy to add one. Feel free to open an issue or, better yet, send a pull request!

To learn the API for adding tab completion to your own command scripts, run ./go help complete. You can also learn by reading the scripts for the builtin commands.

Standalone mode

If you wish to use the framework to write a standalone program, rather than a project-specific development script, set _GO_STANDALONE in your top-level script to prevent alias commands, builtin commands, and plugin commands from showing up in help output or from being offered as tab completions. (help will still appear as a top-level tab completion.) All of these commands will still be available, but users won't be presented with them directly.

_GO_STANDALONE also prevents the script from setting PWD to _GO_ROOTDIR, enabling the script to process relative file path arguments anywhere in the file system. Note that then you'll have to add _GO_ROOTDIR manually to any _GO_ROOTDIR-relative paths in your own scripts.

Including common code

There are a number of possible methods available for sharing code between command scripts. Some possibilities are:

  • The generally preferred method is to use . $_GO_USE_MODULES to source optional library modules; see the Modules section.
  • Include common code and constants in the top-level ./go script, after sourcing go-core.bash and before calling @go.
  • Source a file in the same directory that isn't executable.
  • Source a file in a child directory that may not have a name of the form: parent.d.
  • Source files from a dedicated directory relative to $_GO_ROOTDIR, e.g.:
    . "path/to/lib/common.sh"
  • Subcommand scripts can source the parent command via:
    . "${BASH_SOURCE[0]%.d/*}"

Command script API

Any script in any language can invoke other command scripts by running ./go <command> [args..]. In Bash, however, you can also invoke the @go function directly as @go <command> [args...].

The @go, @go.printf, and @go.print_stack_trace functions are available to command scripts written in Bash, as Bash command scripts are sourced rather than run using another language interpreter.

A number of global variables defined and documented in go-core.bash, all starting with the prefix _GO_, are exported as environment variables and available to scripts in all languages (along with the global COLUMNS environment variable). Run ./go vars to see them all along with their values, and run ./go help vars for more details.

Plugins

You can add third-party plugin command scripts to the plugins subdirectory of your scripts directory. Run ./go help plugins for more information.

Modules

You can import optional Bash library code from the core framework, third-party plugins, or your own project's scripts directory by sourcing the _GO_USE_MODULES script. For example, to import the core logging utilities:

. "$_GO_USE_MODULES" 'log'

Run ./go help modules and ./go modules --help for more information.

Logging

The core library log module provides functions for standard logging facilities. For example:

@go.log INFO Hello, World!
@go.log ERROR Goodbye, World!

For more information, run ./go modules --help log.

Bats test assertions and helpers

The assertions and helpers from the test suite have been extracted into the lib/bats libraries. While these are not modules you can import with _GO_USE_MODULES, they are completely independent of the rest of the core framework and you may source them in your own Bats tests. (Whether or not these will ever become a separate library remains an open question.)

Variables, helper functions, and assertions for testing features based on the core framework are available in the lib/testing directory. The lib/bats-main library makes it easy to write a ./go test command script with the same interface and features as the core framework's ./go test command.

Read the comments from each file for more information.

kcov-ubuntu module for test coverage on Linux

The kcov-ubuntu module provides the run_kcov function that will download and compile kcov, then run kcov with the original ./go command line arguments to collect test coverage. Only available on Ubuntu Linux for now, hence the name. Run ./go modules --help kcov-ubuntu for more information and see scripts/test for an example of how it may be used.

Feedback and contributions

Feel free to comment on or file a new GitHub issue or otherwise ping @mbland with any questions or comments you may have, especially if the current documentation hasn't addressed your needs.

If you'd care to contribute to this project, be it code fixes, documentation updates, or new features, please read the CONTRIBUTING file.

Installing Bash

If you're using a flavor of UNIX (e.g. Linux, OS X), you likely already have a suitable version of Bash already installed and available. If not, use your system's package manager to install it.

On Windows, the Git for Windows, MSYS2 and Cygwin distributions all ship with a version of Bash. On Windows 10, you can also use the Windows Subsystem for Linux.

Updating your PATH environment variable

Once you've installed bash, your PATH environment variable must include its installation directory. On UNIX, you can add it in the appropriate initialization file for your shell; look up your shell documentation for details.

On Windows, in most cases, you'll use the terminal program that ships with Git for Windows, MSYS2, or Cygwin, or you'll invoke the Windows System for Linux environment by entering bash in a built-in Command Prompt window. These terminals automatically set PATH so that Bash is available.

However, if you want to use the Git, MSYS2, or Cygwin bash from the built-in Command Prompt window, open the Start menu and navigate to Windows System > Control Panel > System and Security > System > Advanced system settings. Click the Environment Variables... button, select PATH, and add the directory containing your bash installation. The likely paths for each environment are:

  • Git: C:\Program Files\Git\usr\bin\
  • MSYS2: C:\msys64\usr\bin\
  • Cygwin: C:\cygwin64\bin\

To use one of these paths temporarily within a Command Prompt window, you can run the following:

C:\path\to\my\go-script-bash> set PATH=C:\Program Files\Git\usr\bin\;%PATH%

# To verify:
C:\path\to\my\go-script-bash> echo %PATH%
C:\path\to\my\go-script-bash> where bash

# To run the tests:
C:\path\to\my\go-script-bash> bash ./go test

It should not be necessary to set Bash as your default shell. On Windows, however, you may wish to execute the bash command to run it as your shell before executing the ./go script or any other Bash scripts, to avoid having to run it as bash ./go every time.

Recommended utilities

Most of the framework as-is does not require any other external tools. However, in order for the automatic command help and output formatting to work, you'll need the following utilities installed:

  • tput (ncurses) on Linux, OS X, UNIX
  • mode.com should be present on Windows

To use the get file builtin, either curl, wget, or fetch must be installed on your system. get git-repo requires git, naturally.

Open Source License

This software is made available as Open Source software under the ISC License. For the text of the license, see the LICENSE file.

Prior work

This is a Bash-based alternative to the 18F/go_script Ruby implementation.

go-script-bash's People

Contributors

chuckkarish avatar juansaavedrauy avatar mbland avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

go-script-bash's Issues

Create command to generate new commands, modules

Should be easy to run ./go new-command foo to generate the following and make it executable, and to open EDITOR if defined:

#! /usr/bin/env bash
#
# Description of foo

_foo() {
  :
}

_foo "$@"

There should probably be something similar for modules as well.

Add `subcommand` module with `@go.subcommand_select` function

The first implementation of demo-core (#57) will provide a list of available subcommands whenever it's actually executed, rather than one of its subcommands. When the argument list is empty, it's not an error; when it isn't empty, a subcommand wasn't found, hence it is an error.

The next iteration will include a select statement-based interface that will prompt the user to select a subcommand whenever the standard input (file descriptor zero) is a terminal. Since this behavior seems like it could be generally useful, I'll implement it as a module function.

Replace `IFS=$'\n'` joining in tests with `test_join'

It will basically be identical to the updated @go.join, with a stripped-down variable name check:

test_join() {
  if [[ ! "$2" =~ ^[[:alpha:]_][[:alnum:]_]*$ ]]; then
    printf '"%s" is not a valid variable identifier.\n" "$2" >&2
    return 1
  fi

  local IFS="${1:-$'\n'}"
  printf -v "$2" "${*:3}"
}

Note that this ensures the delimiter won't get added to the end, and then need to get trimmed off.

Also, for writing files where it's needed:

  printf '%s\n' "${lines[@]}" >path/to/file

JSON parser/emitter module

Thinking about #35 and researching options, I'm thinking it might not be too difficult to write a pure Bash JSON parser/emitter module. It would probably borrow much of the interface from JSON.sh, but avoid using grep, pipes, etc. (Unlike JSON.sh, it'll be Bash 3.2+ specific.)

Once the module is in place, emitting JSON logs (and testing the behavior of such) should prove cleaner and easier.

Enhanced `log` setup capability

Per @JohnOmernik, it would be nice to provide the user with a more comprehensive configuration API. We should knock some ideas around, but as a first draft, I'm thinking of a sourced bash file (logging.conf per John's suggestion) something like:

_GO_LOG_DATEFMT='%Y-%m-%d %H:%M:%S'
_GO_LOG_OPTIONS_INFO=(
  "fmt:default /dev/stdout"
  "date:true /var/log/app/info.log"
  "/var/log/app/info.json"
)
_GO_LOG_OPTIONS_FATAL=(
  "fmt:\e[1m\e[32m /dev/stderr"
  "fmt:same /var/log/app/fatal.log"
  "/var/log/app/fatal.json"
)
_GO_LOG_OPTIONS_CUSTOM=(
  "fmt:json /var/log/app/custom.log"
)

And so forth. I'm gravitating towards Bash vs. JSON or other formats because it minimizes the need for external tooling and I have an idea of how to implement this. For example:

  • The option:value parameters are first because they won't contain spaces; I can split the string, loop over the values to parse the options, then join the remaining strings back into a file path (to handle paths with spaces).
  • Options like fmt:same will inherit the format of the preceding entry.
  • File names ending in .json will automatically get converted to JSON format, but other file names can specify fmt:json as well.
  • One timestamp format to rule them all. If it's defined, all log messages will receive them, unless date:false is specified.
  • JSON format always gets a timestamp.
  • Builtin levels will remain as they are unless overridden by a _GO_LOG_OPTIONS_* setting.

This is completely up for discussion. I'm open to being convinced to use things like YAML and JSON, too, but tend to favor Bash-only solutions to minimize dependencies.

Tab completion changes directory

If you use tab completion, your current directory is changed to the rootdir for the go script.

For example:

cd /tmp
git clone [email protected]:mbland/go-script-bash.git
cd go-script-bash
eval "$(./go env -)"
cd /tmp
go he<tab>
pwd

The result is that the current working directory is /tmp/go-script-bash.

I would expect that running the go script would not change my current working directory.

There are tests for the directory being changed when completion is in use, so I imagine there is a good reason for it. Will you please explain?

Better error checking in module loading

When modules paths are checked in go-script-bash/lib/internal/use basically the script tries to source them and if it fails, it just ends up on unknown module

if ! . "$_GO_CORE_DIR/lib/$__go_module_name" 2>/dev/null &&
! . "$_GO_SCRIPTS_DIR/plugins/${__go_module_name////lib/}" 2>/dev/null &&
! . "$_GO_SCRIPTS_DIR/lib/$__go_module_name" 2>/dev/null; then
@go.printf "ERROR: Unknown module: $__go_module_name" >&2
exit 1
fi

However, this doesn't differentiate between the module not existing, and the module having an error. I.e. if the module file exists, but has a bash syntax error, it will fail with the same "Unknown module" error that it fails with if the module file doesn't exist.

Ideally, we'd have two messages, one for issue with the module file not existing, another stating the file doesn't exist, however there was an error in loading it.

Add `assert_lines_match`, `assert_file_equals`, `assert_file_matches`

As I'm starting to focus on emitting JSON from the log module, I'm starting to realize I need more flexible assertions to examine file output, particularly if those files contain timestamps.

Specifically, in the log/timestamp test cases I figured out that setting _GO_LOG_TIMESTAMP_FORMAT='%M:%S' and validating output against [0-5][0-9]:[0-5][0-9] should be enough to validate timestamp behavior without breaking at any particular time or any particular locale. With the new logging tests, I potentially need to validate that and other patterns against multiple lines in a file, and spelling each one out manually with assert_line_matches may prove tedious.

Also, I realized some of the behavior in assert_log_file_equals from tests/log/helpers.bash is already ripe for extraction.

Hence I've bitten the bullet and begun to build out these assertions, and even added a fail_if assertion negator in the process. I'll likely address both #48 and #51 in the upcoming pull request that adds these new assertions.

Emit `__GO_LOG_COMMAND_DEPTH` in `@go.log_command` output

Thinking about the upcoming JSON support and how to handle @go.log_command output, I'm thinking of emitting each line as something like:

{"d":_GO_LOG_COMMAND_DEPTH,"o":"line of output"}

It may also be useful to add the depth to the other @go.log calls as well.

Add `@go.test_printf` helper

As it's often helpful to inspect the internal state of various variables while running Bats tests, it'd be helpful to have a function like the following:

@go.test_printf() {
  if [[ -n "$TEST_DEBUG" ]]; then
    @go.printf "$@" >&2
  fi
}

Then the command line may be prefixed with TEST_DEBUG='true', or inside individual test cases you can specify TEST_DEBUG='true' run ....

Theoretically @go.log DEBUG may cover this, but I'm expecting usage of @go.test_printf to entail temporary injections that shouldn't get checked into the master branch of a project.

Consider more stack traces, improve stack trace API

It may be worth looking throughout the code for places it would make sense to add a @go.print_stack_trace call, to make it easier for the user to diagnose issues detected by the framework.

Also, a lot of the calls now are in a context of the form:

if [[ "$BAD_STUFF_HAPPENED" ]]; then
  echo "Some helpful context" >&2
  @go.print_stack_trace 1 >&2
  exit 1
fi

I'd like to update the interface with more optional parameters, maybe even keywords. For example, the following would produce the same effect:

if [[ "$BAD_STUFF_HAPPENED" ]]; then
  @go.print_stack_trace "Some helpful context" exit:1 >&2
fi

There would also be an optional skip: parameter, replacing the current first positional parameter and defaulting to skip:1 (to place the caller of the function calling @go.print_stack_trace at the top of the stack). This would be a breaking API change, but again, since the current adoption of this function is so contained, I'm looking for only a minor version bump.

More ideas welcome!

Return `@go.log_timestamp` result by parent variable

Currently @go.log_timestamp returns its result by printing it to standard output. While this is the way one would typically return single values from shell functions, it's not ideal in this framework, since practically every other function uses a __go_* variable defined by the parent to return the value. This is to make the framework as fast as possible by creating as few subshells as possible, as those subshells add up quick in this context—especially on Windows, where fork() isn't directly supported, and requires an expensive workaround. (This last note needs to go in the go-script-bash coding/testing guide I need to write per #29.)

Move framework test helpers to public module

Some of the functions used to test the framework itself may be useful to others writing programs based on the framework. lib/bats already contains reusable modules that depend on Bats, but not go-script-bash; I'm thinking maybe of moving tests/environment.bash to lib/test-environment.bash for these helpers.

Document plugin protocol

The plugin protocol mimics that of npm's node_modules. This needs to be documented in the README at least, possibly in the plugins builtin, and possibly as a proper website manual.

Create a document explaining Bash version workarounds

Inspired by the need for @go.join and @go.split per #62, #81, and commit 99ab780. All these deep technical reasons should not get lost to the sands of commit log history, or excessively clutter the code comments in multiple places, if there's an effective practice focused on maintenance of a centralized artifact.

Improve plugin protocol

Now that I'm trying to write a separate plugin, I'm hitting some rough edges. Basically, I'd like plugins to potentially be standalone programs, but that collides with some presumptions the core framework makes regarding command and module lookup paths. The solution here may overlap with #118.

Use `printf -v` to replace `eval`, return variables

It just occurred to me that some eval statements that assign to variables can be replaced with printf -v, and that some generic library functions (in lib/*) can return their results by a similar mechanism rather than relying upon required variable declarations. Should be relatively quick and painless (in fact, I've already eliminated eval instances locally), but want to track it nonetheless.

Replace `IFS` splitting and joining

Per http://mywiki.wooledge.org/Arguments, it may be better practice to replace IFS-based value splitting with something more like:

IFS=, read -ra value_array <<< "$value"

Joining items using ${value_array[*]} might still require the save-and-restore pattern in some form. I'm also considering encapsulating those operations in @go.split and @go.join functions, like with other languages.

Optional `strftime`-style timestamps in `log` module

Per @JohnOmernik, there should be the option to precede log messages with strftime-style timestamps.

Starting with some 4.x version of Bash, the %(datefmt)T format specifier provides exactly this behavior; Bash 3.2, as ships with macOS, does not. So there'd need to be logic to use the builtin format if available, resort to the date program if not, and produce a warning if neither happens to be available.

Include timing info in `@go.log_command` output

Should be relatively straightforward to add by replacing the default TIMEFORMAT from:

$'\nreal\t%3lR\nuser\t%3lU\nsys%3lS'

with something like this in @go.log_command_invoke:

TIMEFORMAT='real %3lR user %3lU sys %3lS'

to produce output something like this:

$time { sleep 1; echo 'Hello, World!'; }
Hello, World!
real 0m1.005s user 0m0.001s sys 0m0.002s

Then the timing info can be parsed in a similar fashion to the exit status.

Push more file descriptors onto log levels

Right now, the only way to add file descriptors to a log level is to call @go.log_add_output_file. Should add this behavior to @go.log_add_or_update_log_level as well.

Emit `@go.log RUN` messages to standard output when running under `@go.log_command`

This is almost a theoretical concern, as @go.log RUN emits messages to standard output by default. Still, it may prove wise to ensure that even when the user has replaced the default @go.log RUN file descriptor with something other than standard output or error, that commands running under @go.log_command always print RUN messages to standard output. Otherwise they would not, as [email protected]_command_should_skip_file_descriptor would return true for all RUN file descriptors.

In fact, this should be nearly trivial by adding this to @go.log just before the for loop:

  if [[ "$__GO_LOG_COMMAND_DEPTH" != '0' && "$log_level" == 'RUN' ]]; then
    __go_log_level_file_descriptors=('1')
  fi

Improve escape code stripping in `@go.log`

Right now it only strips escape codes of the form \\e\[[0-9]{1,3}m. It won't handle codes like \e[30;47m at all.

I'm thinking something like this (which might live in the format module):

  local format_pattern='\\e\[[0-9]+(;[0-9])*m'
  while [[ "$value" =~ $format_pattern ]]; do
    value="${value//${BASH_REMATCH[0]}}"
  done

printf escaping issue in @go.log

I am trying to log a json output from an API with @go.log

The value of the variable I am printing is

{"timestamp":1482524700778,"timeofday":"2016-12-23 08:25:00.778 GMT+0000","status":"OK","total":0,"data":[],"messages":["Successfully created volume: 'zeta.shared.dockerregv2'"]}

The error I am getting is:

/home/zetaadm/zetago/scripts/go-script-bash/lib/log: line 172: printf: `m': invalid format character

Add assertion tests for success case to ensure proper return handling

Per the instructions in lib/bats/assertions, it's imperative that every Bats assertion function using that module call return_from_bats_assertion immediately before returning. However, while the need to call return_from_bats_assertion in the failure case is documented thoroughly in the comments for that function, the need to call it in the success case isn't, and the ramifications of not calling it in the success case aren't tested, either.

Specifically, if an assertion doesn't call return_from_bats_assertion in the success case, the assertion's entry isn't cleaned from the Bats stack traces and set -o functrace doesn't get called. When that assertion is followed by another assertion that fails, the failure output may contain a reference to the earlier, passing assertion. When assertions are composed from existing assertions that also fail to call return_from_bats_assertion in the success case, the problem compounds.

Right now, assert_lines_equal is missing this success-case call to return_from_bats_assertion. It's the only assertion in the file missing the call, but an additional two checks need to be added to the expect_success test helper to ensure all assertions satisfy this condition.

`@go.log_command` should send all command output to log files

Right now @go.log_command just logs the command itself, not its output. It should be updated to something like this:

while read -r line; do
  line="${line%$'\r'}"  # Windows
  line="${line//%/%%}" # printf
  line="${line//\\/\\\\}" # Backslashes
  for fd in "$([email protected]_level_file_desciptors "$level_index"); do
    printf "${line}\n"
  done
done < $("${args[@]}" 2>&1)
exit_status="$?"

Not sure how close the above is to what will actually work, especially with regards to exit_status. (Maybe $("$args[@]" 2>&1; exit_status="$?") would be the trick?)

Write testing guide

Though I'm hoping that the files in lib/bats and the existing tests are as clear as possible, it probably wouldn't hurt to develop and introductory tutorial.

Send log messages to multiple output files

Per @JohnOmernik, it may prove convenient to print log messages both the console and a file.

A proof-of-concept of the approach is already implemented in the file module as @go.fds_printf. That function won't be directly applicable; the console will need to include format codes, and the file output should have them stripped by default. Also per #35, it may be desirable to convert the output to JSON for some output files.

Add `@go.test_filter`

Along with #82, implement @go.test_filter thus (already tried it and it works!):

@go.test_filter() {
  if [[ -n "$TEST_FILTER" && ! "$BATS_TEST_DESCRIPTION" =~ $TEST_FILTER ]]; then 
    skip 
  fi
}

Call this from the setup() of every test file, and then use it thus:

$ TEST_FILTER='strip' ./go test format
 - format: does nothing for empty argv (skipped)
 - format: pads argv items (skipped)
 - format: zip empty items (skipped)
 - format: zip matching items (skipped)
 ✓ format: strip formatting codes from empty string
 ✓ format: strip formatting codes from string with no codes
 ✓ format: strip formatting codes from string with one code
 ✓ format: strip formatting codes from string with multiple codes

8 tests, 0 failures, 4 skipped

real    0m0.691s
user    0m0.329s
sys     0m0.343s

Between this and @go.test_printf, could make iterating over specific test cases much easier, with minimal temporary hand-hacks to isolate specific conditions and inspect program state.

Way to configure behaivior of @go.log FATAL

I am liking the new changes. One thing I noticed was now when I use @go.log FATAL it produces my error message as well as a stack trace.

From a user point of view, if I am trapping an error (thus producing the @go.log FATAL) what I provide to them should be sufficient... perhaps there are other cases where I wish to provide them a stack trace, but I've found most of the place I've used @go.log FATAL my message is sufficient.

For example:

If I run a script and the script expects a certain argument -c=/path/to/conf

I try to get the conf file, and if it doesn't exist

if [ ! -f "CONF_FILE" ]; then
@go.log FATAL "Please provide a valid path to configuration file with -c="
fi

The stack trace is not needed here, and if anything confuses the user. (Think "Java Errors")

Thus, I am guessing you have a way to enable or disable stack traces globally, however, maybe we should have two "FATAL"s, one for trapped errors like mine, and another when we need more debugging?

DEBUGFATAL? (lots of characters?) DBFTL? DBFATAL? FEMFATAL? (Ok that last one is pushing it).

Am I making sense or rambling?

Improve documentation of variables, call patterns

After integrating #44, some readonly variables that may be initialized with user-defined values must be set before calling . "$_GO_USE_MODULES". There needs to be better documentation of this pattern and of the setup of specific modules (e.g. log).

Implement `./go plugins install`

Now that the plugin protocol is complete per the merging of #136 and closing of #120, there should be a new ./go plugins install subcommand to automatically and recursively install plugins similarly to npm install. It will likely read from .config/go-script-bash/plugins per #134.

Add `validation` module

This one is very small, basically moving some of the logic in the new file module to another dedicated module.

I'd considered adding it to go-core.bash, but decided it might be best to give it its own module, since go-core.bash should be as lean as possible and not all programs or modules are likely to require the behavior. (That's why I added the module capability, and probably why modules as a concept were invented in the first place!)

Consider refactoring Bats to avoid pipelines, subshells

As mentioned in #76, fork() isn't native to Windows and requires a expensive hack to emulate. While the go-script-bash framework itself avoids subshells and pipelines pretty thoroughly and itself runs fairly quickly everywhere, Bats is written in more traditional Bash that doesn't shy away from either. This makes the current suite of tests on Windows take on the order of 30min, when they take on the order of five or less on Linux and macOS systems. This is consistent across Git for Windows, Cygwin, MSYS2 (which is the basis for Git for Windows), and even the Windows Subsystem for Linux now included in Windows 10.

Also, knowing from experience that folks often won't write tests if they're slow to run—especially if it's too slow to experiment effectively and achieve the flow state necessary for deep learning—the inherent slowness of the Bats framework may turn off devs on Windows in particular. Since I'm hoping go-script-bash will prove not only useful for writing ./go scripts and other Bash apps, but also for encouraging thorough and effective Bats testing as well (with the lib/bats library helping out), the slowness may become a significant obstacle to this goal, especially for Windows users.

And there's a huge opportunity here, given the prevalence of Git for Windows and the Windows Subsystem for Linux! See also:

Consequently, I'm considering forking sstephenson/bats and seeing if there are any significant performance wins to be had without substantially complicating the code. On top of being a potentially huge positive factor for Windows, it will likely reap benefits for the other platforms as well.

Can `set -o functrace` be conditional in `return_from_bats_assertion`?

After thinking more about return_from_bats_assertion per #48 and #50, I'm wondering if calling set -o functrace can be called in the unset 'BATS_{CURRENT,PREVIOUS}_STACK_TRACE conditions (at least the CURRENT condition), obviating the need for an assertion function to call set +o functrace after every other assertion it calls.

.config/go-script-bash directory for projects

Thinking of patterning a .config/go-script-bash directory convention along the lines of the XDG_CONFIG_HOME spec. This way there could be a mechanism to import .config/go-script-bash/* to get the top-level config for the project (some possibly standard, but can pull in user-defined config like this, too).

This could also support a recursive mechanism to import and install plugins by following every .config/go-script-bash/plugins file in the _GO_PLUGINS_DIR tree. Only plugins defined in the top-level .config/go-script-bash/plugins file would then be available to the top-level command script, to support an npm-like node_modules plugin directory structure where by common plugins are shared, but without cluttering up the main command namespace.

More generally, there could be a mechanism to determine which commands to expose as part of the public vs. private interface, but that's for a future issue.

Consider including a `trap` module

After working a bit with traps as part of @go.log_command and in tests/assertions, it occurs to me it might be helpful to produce a trap module that helps command scripts set traps that invoke any previously defined traps.

For example, any command scripts written in Bash executed via @go.log_command are sourced into the [email protected]_command_invoke environment. If that command script needs to set an EXIT trap, it also needs to take care to invoke the EXIT trap defined by [email protected]_command_invoke as well.

I'm thinking something along the lines of:

@go.parse_existing_trap_command {
  local signal="$1"
  local trap_command="$(trap -p $signal)"
  trap_command="${trap_command#*\'}"
  __go_existing_trap="${trap_command%\'*}"
}

@go.add_trap_command() {
  local commands="$1"
  local signal="$2"
  local __go_existing_trap
  @go.parse_existing_trap_command "$signal"
  trap "$commands; $__go_existing_trap" "$signal"
}

@go.remove_trap_command() {
  local command_to_remove="$1"
  local signal="$2"
  @go.replace_trap_command "$command_to_remove" '' "$signal"
}

@go.replace_trap_command() {
  local existing_command="$1; "
  local new_command="$2; "
  local signal="$3"

  local __go_existing_trap
  @go.parse_existing_trap "$signal"

  if [[ ! "$__go_existing_trap" =~ $existing_command ]]; then
    @go.printf 'Existing trap commands for %s not found: %s\n' \
      "$signal" "${existing_command%; }" >&2
    @go.print_stack_trace >&2
    exit 1
  fi
  trap "${__go_existing_trap/$existing_command/$new_command}" "$signal"
}

Alternatively, it may be desirable to update [email protected]_command_script to invoke the command using a new $BASH process if __GO_LOG_COMMAND_DEPTH -ne '0'. Or perhaps both?

Either way, the module may be generally useful all the same.

Optional JSON output format from `log` module

Per @JohnOmernik, this will enable more robust log processing by powerful tools.

I'm pretty this option should be at least optional per log level. I'm also considering making it optional per output file, so that a regular log message can be written to the console or a file, and a JSON representation of the same message can get sent to another file.

Might be worth adding jq to the process:

Plugin delegate scripts

There could be a generic mechanism for an application to delegate to a specific plugin, such that it adopts the help text of a plugin script or module automatically.

Basically, a script can be written right now as:

#! /bin/bash
#
# Delegates to plugins/foo/bin/bar

. "$_GO_SCRIPTS_DIR/plugins/foo/bin/bar" "$@"

And we can eventually parse # Delegates to plugins/foo/bin/bar to include that help text instead. Eventually we could even remove the need to include any implementation, possibly making it optional.

Add `demo-core` builtin

Got the idea to start writing a demo program for @go.log, which got me thinking how to generalize it. Already most of the way through the implementation.

Add `split_bats_output_into_lines`

This would preserve the blank lines from output, which are eliminated from lines by default. The implementation would be:

split_bats_output_into_lines() {
  local line
  lines=()

  while IFS= read -r line; do
    lines+=("${line%$'\r'}")
  done <<<"$output"
}

Add tests for plugin paths and subcommand scripts

Per #131, I realized there'd been a bug from #130 whereby [email protected]_plugin_command_script assumed the _GO_PLUGINS_DIR of the command script should be the immediate parent. #130 fixed this such that _GO_PLUGINS_DIR is set to the nearest /bin parent dir, but I still need to add a few test cases for subcommand scripts to make sure this condition holds.

Handle `%` characters in `@go.log` messages

The following causes @go.log to emit mbland/go-script-bash/lib/log: line 194: printf: Y': invalid format characterand stop printing variables after_GO_LOG_TIMESTAMP_FORMAT`:

_GO_LOG_TIMESTAMP_FORMAT='%Y-%m-%d %H:%M:%S'
. "$_GO_USE_MODULES" 'log'
@go.log INFO "$(@go vars)"

The following works as expected:

_GO_LOG_TIMESTAMP_FORMAT='%Y-%m-%d %H:%M:%S'
. "$_GO_USE_MODULES" 'log'
vars="$(@go vars)"
@go.log INFO "${vars//%/%%}"

However, @go.log should escape % characters on behalf of the caller so that the first case works as expected.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.