A shell parser, formatter, and interpreter with bash support; includes shfmt

sh

Go Reference

A shell parser, formatter, and interpreter. Supports POSIX Shell, Bash, and mksh. Requires Go 1.14 or later.

Quick start

To parse shell scripts, inspect them, and print them out, see the syntax examples.

For high-level operations like performing shell expansions on strings, see the shell examples.

shfmt

GO111MODULE=on go get mvdan.cc/sh/v3/cmd/shfmt

shfmt formats shell programs. See canonical.sh for a quick look at its default style. For example:

shfmt -l -w script.sh

For more information, see its manpage, which can be viewed directly as Markdown or rendered with scdoc.

Packages are available on Alpine, Arch, Docker, FreeBSD, Homebrew, MacPorts, NixOS, Scoop, Snapcraft, Void and webi.

gosh

GO111MODULE=on go get mvdan.cc/sh/v3/cmd/gosh

Proof of concept shell that uses interp. Note that it's not meant to replace a POSIX shell at the moment, and its options are intentionally minimalistic.

Fuzzing

This project makes use of go-fuzz to find crashes and hangs in both the parser and the printer. The fuzz-corpus branch contains a corpus to get you started. For example:

git checkout fuzz-corpus
./fuzz

Caveats

  • When indexing Bash associative arrays, always use quotes. The static parser will otherwise have to assume that the index is an arithmetic expression.
$ echo '${array[spaced string]}' | shfmt
1:16: not a valid arithmetic operator: string
$ echo '${array[dash-string]}' | shfmt
${array[dash - string]}
  • $(( and (( ambiguity is not supported. Backtracking would complicate the parser and make streaming support via io.Reader impossible. The POSIX spec recommends to space the operands if $( ( is meant.
$ echo '$((foo); (bar))' | shfmt
1:1: reached ) without matching $(( with ))
  • Some builtins like export and let are parsed as keywords. This allows statically building their syntax tree, as opposed to keeping the arguments as a slice of words. Note that this means expansions like declare {a,b}=c are not supported.

JavaScript

A subset of the Go packages are available as an npm package called mvdan-sh. See the _js directory for more information.

Docker

To build a Docker image, checkout a specific version of the repository and run:

docker build -t my:tag -f cmd/shfmt/Dockerfile .

This creates an image that only includes shfmt. Alternatively, if you want an image that includes alpine, add --target alpine. To use the Docker image, run:

docker run --rm -v $PWD:/mnt -w /mnt my:tag <shfmt arguments>

pre-commit

It is possible to use shfmt with pre-commit and a local repo configuration like:

  - repo: local
    hooks:
      - id: shfmt
        name: shfmt
        minimum_pre_commit_version: 2.4.0
        language: golang
        additional_dependencies: [mvdan.cc/sh/v3/cmd/[email protected]]
        entry: shfmt
        args: [-w]
        types: [shell]

Related projects

The following editor integrations wrap shfmt:

Other noteworthy integrations include:

Owner
Comments
  • syntax: add mksh support

    syntax: add mksh support

    Initially suggested by @D630 in #77.

    Open questions needed to be answered before we decide whether or not to support it:

    • Where is the formal specification of its syntax? The man page (https://www.mirbsd.org/htman/i386/man1/false.htm) seems to be the only reference.
    • What language features does it have that we don't yet support via either POSIX Shell or Bash?
    • Is it compatible with the POSIX Shell syntax? i.e., do they conflict?
    • Is it compatible with the Bash syntax? i.e., do they conflict?

    And of course, whether or not this is worth the effort. I myself haven't seen any mksh scripts in the wild, whereas sh and bash are both very common.

  • cmd/shfmt: official docker images

    cmd/shfmt: official docker images

    I'd be great if you could provide a docker image on docker hub. My use case is to use it with drone.io, there are other CIs that can use docker images natively. Something like:

    FROM alpine:latest
    ENTRYPOINT ["/usr/bin/shfmt"]
    ADD https://github.com/mvdan/sh/releases/download/v1.2.0/shfmt_v1.2.0_linux_amd64 /usr/bin/shfmt
    RUN chmod +x /usr/bin/shfmt
    

    would work for me.

    And maybe the following for automated builds:

    FROM golang:alpine
    ENTRYPOINT ["shfmt"]
    COPY . /go/src/app
    WORKDIR /go/src/app
    RUN set -ex \
           && apk add --no-cache --virtual .build-deps git \
           && go-wrapper download ./... \
           && go-wrapper install ./... \
           && apk del .build-deps 
    
  • syntax: newline style around &&, || and | configurable in the printer

    syntax: newline style around &&, || and | configurable in the printer

    Right now the code:

    [ -z "$HOME" ] && foo || bar
    [ -z "$HOME" ] &&
        foo ||
        bar
    

    will be formatted:

    [ -z "$HOME" ] && foo || bar
    [ -z "$HOME" ] \
        && foo \
        || bar
    

    Can you add an option or something to control the formatting of the line breaks ?

  • syntax: configurable formatting of opening braces

    syntax: configurable formatting of opening braces

    Thanks for the great tool. I have started using this tool with the VS Code. I would like to provide one feedback w.r.t formatting functions (which would at least help couple of us).

    The opening brace of the function definition should be in next line.

    Currently it's as below...

    function fun() {
    }
    

    Suggested way...

    function fun()
    {
    }
    

    Refer examples here: https://www.shellscript.sh/functions.html

    One advantage of following the suggested format is, in the terminal editors like (vim), you can jump between functions by pressing [ and ] character twice. This is very important feature for specially scripts - as once you are done developing the script on your desktop, you would copy/run it on a system where there may not be graphical interfaces or IDEs.

  • syntax: add option to configure how many statements in a list can fit in a single line

    syntax: add option to configure how many statements in a list can fit in a single line

    Thanks for this tool. In our shell script, we have lines like

     	local cmd="docker build -t $DOCKER_REGISTRY/$IMG:$TAG ."
    	echo $cmd; $cmd
    

    We would like to not break lines like echo $cmd; $cmd into 2 separate lines. I did not see any flag to support this. Can this be supported?

  • cmd/shfmt: add a way to skip custom directories when walking directories

    cmd/shfmt: add a way to skip custom directories when walking directories

    When recursing it would be nice to be able to ignore certain directories or files (for example, node_modules).

    Either a --exclude=node_modules, or --ignore=node_modules, or a .shfmtignore-type functionality would be nice.

  • Introduce an option to disable changing white space within source lines

    Introduce an option to disable changing white space within source lines

    I'm starting to use shfmt (via Visual Studio Code editor), but one behaviour that prevents me from using it wholeheartedly is the way shfmt changes the whitespace inside source lines. I wonder if there can be an option to disable this behaviour, but it would continue to change the white spaces at the beginning or end of the line.

    The motivation is to not mess up custom alignment of code, creating layout that might be used to help the code read better.

  • interp: builtins skip the exec handler

    interp: builtins skip the exec handler

    im sure that this is definitely intentional, but this is actually undesirable in my use case im developing hilbish and use this module to run sh of course, but used to only use this as a last option fallback instead of running my custom builtins or lua code. but now im considering actually using an exec handler for the runner but realized cd wasnt behaving how i expected it to be since it's this module's cd and not mine

    so is there a way for me to change how the builtin works from outside the package? i havent found anything in docs like that

  • interp: TestRunnerTerminalExec/Pseudo flaky on Mac

    interp: TestRunnerTerminalExec/Pseudo flaky on Mac

    --- FAIL: TestRunnerTerminalExec (0.00s)
        --- FAIL: TestRunnerTerminalExec/Pseudo (0.14s)
    ##[error]        terminal_test.go:129: signal: hangup
    FAIL
    FAIL	mvdan.cc/sh/v3/interp	4.548s
    

    @theclapp perhaps you could help me reproduce this again? Do you have any ideas?

    Worst case scenario, we can temporarily skip the pty tests on Mac.

  • interp: calling a program with >> on Windows does not append

    interp: calling a program with >> on Windows does not append

    For simple echo it's working, but:

    $ ls >> out
    ls: write error: Bad file descriptor
    

    ls is from Windows Git (C:\Program Files\Git\usr\bin\ls.exe).

    Replace in interp\module.go, fix it, but it's bad implementation:

    var DefaultOpen = ModuleOpen(func(ctx context.Context, path string, flag int, perm os.FileMode) (io.ReadWriteCloser, error) {
    	if flag&os.O_APPEND != 0 {
    		flag = flag & (^os.O_APPEND)
    		f, err := os.OpenFile(path, flag, perm)
    		f.Seek(0, 2)
    		return f, err
    	}
    	return os.OpenFile(path, flag, perm)
    })
    
  • Use official Docker GitHub actions

    Use official Docker GitHub actions

    Use the following actions instead of manual Docker buildx setup:

    • https://github.com/docker/login-action
    • https://github.com/docker/setup-qemu-action
    • https://github.com/docker/build-push-action

    Notes:

    • It would be good to merge "Build and push" and "Build and push (alpine)" steps but I have no solution to propose for now;
    • there are some linting changes (using prettier).

    WIP because CI needs to be tested against upstream repo:

    • create a DOCKER_USER secret so that forks can test the CI (push);
    • I am using token and 2FA on DockerHub so the update description step could not be tested.

    Regarding the last step (Update DockerHub description), we may consider using this https://github.com/marketplace/actions/docker-hub-description.

  • syntax: remove backslash-newline-semicolon when it's unnecessary

    syntax: remove backslash-newline-semicolon when it's unnecessary

    Take this piece of shell:

    $ cat f.sh
    if
    	true \
    	; then
    	echo yes
    fi
    $ bash f.sh
    yes
    $ shfmt f.sh
    if
    	true \
    		;
    then
    	echo yes
    fi
    

    Note how it runs, but shfmt didn't improve it that much. It could do better, instead producing:

    $ cat f2.sh
    if
    	true
    then
    	echo yes
    fi
    $ bash f2.sh 
    yes
    $ shfmt f2.sh
    if
    	true
    then
    	echo yes
    fi
    
  • Joining of lines with options/arguments is inconsistent

    Joining of lines with options/arguments is inconsistent

    In my scripts, I have a few commands that follow this general pattern:

    env\
    	VAR1='1'\
    	VAR2='2'\
    	somecommand\
    	--big-flag-1='big_flag_value_1'\
    	--big-flag-2='big_flag_value_2'\
    	--big-flag-3='big_flag_value_3'\
    	-x\
    	-y\
    	-z\
    	file_with_a_very_long_name_1\
    	file_with_a_very_long_name_2\
    	file_with_a_very_long_name_3\
    	file_with_a_very_long_name_4\
    	;
    

    This is line-oriented to simplify diffing, sorting, etc. I would expect shfmt -p to either leave the lines as-is (maybe adding or removing a space before the final \), but the result is rather unexpected and weird:

    env VAR1='1' \
           VAR2='2' \
           somecommand --big-flag-1='big_flag_value_1' \
           --big-flag-2='big_flag_value_2' \
           --big-flag-3='big_flag_value_3' \
           -x -y -z file_with_a_very_long_name_1 file_with_a_very_long_name_2 file_with_a_very_long_name_3 file_with_a_very_long_name_4
    

    I get the addition of extra spaces, but then it also combines all of the single-letter flags and all non-option arguments into one unreadable line. But not the envs or the long-form flags.

    I feel like shfmt shouldn't interfere with the placement of arguments on separate lines at all, but if it should, it should also at the very least not produce these overlong lines of mixed short options and positional arguments.

  • cmd/shfmt: Files with non-shell extension excluded

    cmd/shfmt: Files with non-shell extension excluded

    Hi, I'm using shfmt -f . to list files I want to format. However, many files aren't found as they don't have a .sh/.bash extension, but are named something like mkosi.finalize and I can't change the name as it is expected by some consumer. The files have, however, a valid shebang.

    The files are excluded because of the following line:

    https://github.com/mvdan/sh/blob/279b806d8f2417af933a40a726ad8299395d9183/fileutil/file.go#L80

    Would be great to just omit it, and do the shebang check in any case.

  • syntax: allow printing redirections before all arguments

    syntax: allow printing redirections before all arguments

    Sometimes, to improve readability, I put logfile redirections at the beginning of the line:

    >>$LOGFILE echo "[label]" 1 2 3
    >>$LOGFILE some_command 4 5 6
    

    This is currently, as of shfmt v3.5.1, incorrectly reformatted as follows:

    echo >> $LOGFILE "[label]" 1 2 3
    some_command >> $LOGFILE 4 5 6
    

    Research

    Putting redirects at various places in-between arguments and at the end seems to have been supported from the very beginning of sh in 1971. Putting redirects at the beginning was not supported in UNIX V1, but emerged sometime later. I haven't found out when.

    UNIX V1 manual (1971-03-11) (and code):

    Each command is a sequence of non-blank command arguments separated by blanks. [...] Two characters [i.e., "<" or ">"] cause the immediately following string to be interpreted as a special argument to the shell itself, not passed to the command. An argument of the form <arg causes the file arg to be used as the standard input file of the given command; an argument of the form “>arg” causes file “arg” to be used as the standard output file for the given command.

    Bash documentation, section 3.6 (2022-11-13):

    The following redirection operators may precede or appear anywhere within a simple command or may follow a command. Redirections are processed in the order they appear, from left to right. [...]

    Bash documentation, section 3.7.1 (2022-11-13):

    When a simple command is executed, the shell performs the following expansions, assignments, and redirections, from left to right, in the following order: 1. The words that the parser has marked as variable assignments (those preceding the command name) and redirections are saved for later processing. [...]

  • cmd/shfmt: link to web demo or playground

    cmd/shfmt: link to web demo or playground

    shfmt is a great tool, and it would be fantastic if it was more widely adopted. Unfortunately many potential users don't have an easy way to run it: the only *nix machines they have access to are managed by others, and they don't have the expertise to set up something like a VM. To help those users out, I'm wondering if someone would be willing to host a playground like ShellCheck where users can simply paste their code and get a formatted version back in another part of the page.

  • syntax: IsIncomplete only works with Interactive

    syntax: IsIncomplete only works with Interactive

    IsIncomplete reports whether a Parser error could have been avoided with extra input bytes. For example, if an io.EOF was encountered while there was an unclosed quote or parenthesis.

    Given this godoc, it seems like it should work with all parse APIs, like Parse or Words, but it does not. For example, see https://go.dev/play/p/DxfQ8ZlBFuF.

    We should either make it work everyhwere, or document its limitation. Ideally the former.

Mdfmt - A Markdown formatter that follow the CommonMark. Like gofmt, but for Markdown

Introduction A Markdown formatter that follow the CommonMark. Like gofmt, but fo

Dec 18, 2022
Parse line as shell words

go-shellwords Parse line as shell words. Usage args, err := shellwords.Parse("./foo --bar=baz") // args should be ["./foo", "--bar=baz"] envs, args, e

Dec 23, 2022
Pryrite, interactively execute shell code blocks in a markdown file
Pryrite, interactively execute shell code blocks in a markdown file

Pryrite Pryrite is a command line tool that interactively runs executable blocks in a markdown file. One can think of pryrite as a console REPL/debugg

Dec 18, 2022
omniparser: a native Golang ETL streaming parser and transform library for CSV, JSON, XML, EDI, text, etc.
omniparser: a native Golang ETL streaming parser and transform library for CSV, JSON, XML, EDI, text, etc.

omniparser Omniparser is a native Golang ETL parser that ingests input data of various formats (CSV, txt, fixed length/width, XML, EDI/X12/EDIFACT, JS

Jan 4, 2023
A simple CSS parser and inliner in Go

douceur A simple CSS parser and inliner in Golang. Parser is vaguely inspired by CSS Syntax Module Level 3 and corresponding JS parser. Inliner only p

Dec 12, 2022
Unified diff parser and printer for Go

go-diff Diff parser and printer for Go. Installing go get -u github.com/sourcegraph/go-diff/diff Usage It doesn't actually compute a diff. It only rea

Dec 14, 2022
Quick and simple parser for PFSense XML configuration files, good for auditing firewall rules

pfcfg-parser version 0.0.1 : 13 January 2022 A quick and simple parser for PFSense XML configuration files to generate a plain text file of the main c

Jan 13, 2022
A NMEA parser library in pure Go

go-nmea This is a NMEA library for the Go programming language (Golang). Features Parse individual NMEA 0183 sentences Support for sentences with NMEA

Dec 20, 2022
TOML parser for Golang with reflection.

THIS PROJECT IS UNMAINTAINED The last commit to this repo before writing this message occurred over two years ago. While it was never my intention to

Dec 30, 2022
User agent string parser in golang

User agent parsing useragent is a library written in golang to parse user agent strings. Usage First install the library with: go get xojoc.pw/userage

Aug 2, 2021
Simple HCL (HashiCorp Configuration Language) parser for your vars.

HCL to Markdown About To write a good documentation for terraform module, quite often we just need to print all our input variables as a fancy table.

Dec 14, 2021
A markdown parser written in Go. Easy to extend, standard(CommonMark) compliant, well structured.

goldmark A Markdown parser written in Go. Easy to extend, standards-compliant, well-structured. goldmark is compliant with CommonMark 0.29. Motivation

Dec 29, 2022
A PDF renderer for the goldmark markdown parser.
A PDF renderer for the goldmark markdown parser.

goldmark-pdf goldmark-pdf is a renderer for goldmark that allows rendering to PDF. Reference See https://pkg.go.dev/github.com/stephenafamo/goldmark-p

Jan 7, 2023
Experimental parser Angular template

Experimental parser Angular template This repository only shows what a parser on the Go might look like Benchmark 100k line of template Parser ms @ang

Dec 15, 2021
A dead simple parser package for Go
A dead simple parser package for Go

A dead simple parser package for Go V2 Introduction Tutorial Tag syntax Overview Grammar syntax Capturing Capturing boolean value Streaming Lexing Sta

Dec 30, 2022
Freestyle xml parser with golang

fxml - FreeStyle XML Parser This package provides a simple parser which reads a XML document and output a tree structure, which does not need a pre-de

Jul 1, 2022
An extension to the Goldmark Markdown Parser

Goldmark-Highlight An extension to the Goldmark Markdown Parser which adds parsing / rendering capabilities for rendering highlighted text. Highlighte

May 25, 2022
A parser combinator library for Go.

Takenoco A parser combinator library for Go. Examples CSV parser Dust - toy scripting language Usage Define the parser: package csv import ( "err

Oct 30, 2022
A simple json parser built using golang

jsonparser A simple json parser built using golang Installation: go get -u githu

Dec 29, 2021