98 comments

[ 2.7 ms ] story [ 157 ms ] thread
(comment deleted)
FYI the author also offers an absurdly good commandline shellscript linter: https://github.com/koalaman/shellcheck

It's a bit like `checkbashisms` but a lot more expansive and with formal classification of the warnings.

shellcheck is awesome. There is even a Sublime plugin SublimLinter-Shellcheck.

Also, every rule in shellcheck has a corresponding github wiki page which describes the issues and common fixes. For example SC2002 (https://github.com/koalaman/shellcheck/wiki/SC2002)

I used it (the linter + its wiki) last week to work on a bunch of related scripts, it was really helpful and even taught me some things about my bad habits.
Woa, it's written in Haskell. Awesome!

EDIT: "and requires 2GB of RAM to compile." whaaat?

Haha I assumed the second observation was a result of the first? Still, this is why one uses a reasonable distro. When I "apt-get install shellcheck", a version that's just about a year old (0.3.7) gets installed. Let package maintainers worry about compiling Haskell.
I was about to comment that year-old software is a downside, but given that it's for (ba|)sh maybe not so much; bash changes but glacially, and sh has barely moved in 20 years. Stability for the win :)
I don't know where the RAM requirement come from. I've been compiling shellcheck on VPS's with less than a gig RAM for a year and never hit a memory issue.
I'm the author of the article and ShellCheck. The RAM requirement comes from my own experience, where I had to add a swap file to my VPS because 1.7GB RAM wasn't enough.

I'm very surprised you've been getting by with less than a gig. What's your arch and GHC version, if you don't mind?

Older GHC versions appeared to use significantly less RAM, and I imagine 32bit pointers would save a lot.

Wow it's great to talk to you. Thanks for your great tool!

Your guess is right, I do my builds in Debian 8 with ghc and cabal from the repository. I do cabal update before compilation but I don't think this upgrades the ghc so the details should be like:

arch: i686

ghc: 7.6.3

I also use these arguments for cabal install but no idea if they affect anything:

    --disable-documentation --enable-split-objs --ghc-options='-static -optc-static -optl-static -optl-pthread'
while not as portable, using double brackets - i.e.

  [[ -z $var ]]
can be a bit "safer" in regards to quoted variables. Works in bash and KSH.

Edit to add: http://stackoverflow.com/questions/669452/is-preferable-over...

Why isn't it being added to POSIX? What does it take to get something added to POSIX?
A time machine?
The POSIX standard is under active development.
POSIX is under active development. The issue I'm concerned about is this text quoted from the 'printf' man-page.

> %q ARGUMENT is printed in a format that can be reused as shell

> input, escaping non-printable characters with the proposed POSIX

> $'' syntax.

Don't use that. If anything, use properly quoted variables:

  [ -z "$var" ]
This has literally no possibility of failing in any weird way, no matter which Bourne or Korn shell descendant you use.
in nearly all cases, you should always do "$var" instead of $var, this should be mandated on sh scripts.

you can even go further to do "${bar}", this is even safer I feel.

Yes, it's "${bar}" that should go into style guides and linters (for consistency).
It seems kind of petty I guess but that seems kind of annoying to write and even less importantly looks pretty blah.
"${bar}" is irritating to write. It only makes sense when you need to append a word character ([a-zA-Z0-9_]) to the content of $bar, like in "${bar}qq".
"${bar}" makes my syntax highlighting light up in a satisfying way, and automatic quote/brace completion makes it peachy.
Have something​ against [[ ]] ?
Of course I have. [[ ]] is shell-specific, which for scripts almost always means bash-specific, and bash has a history of subtly changing semantics of such constructs. I've seen scripts suddenly stop working for no apparent reason, only to discover later that bash upgrade changed how [[ == ]] treated unanchored patterns (or something of similar caliber). And now go figure where the bug is.

Most of the time, whatever is in [[ ]] can be easily emulated with POSIX/SUS-compatible tools and most of the time it doesn't matter that the tool is not a shell built-in.

[[ ]] works in bash, zsh, and (iirc) ksh. Saying it's "bash-specific" isn't accurate.
Depends on what you mean by "works". Yes, all three shells recognize [[ ]] construct, as it was specifically added to SUS (or POSIX?) for implementation-specific things, but how they interpret it is up to the specific shell.

Let's take people's favourite =~ operator. In bash, according to documentation, it uses ERE syntax. In zsh, it is ERE or PCRE, depending on an option being set. This is a subtle but important difference in how [[ ]] works. And then again, your environment can change the behaviour of a script you're running, which is (a) unexpected and (b) undesirable, and the change in the behaviour can range from "doesn't find thing it should" to "crashes" to "goes awry in an unexpected way".

It fails with "set -u" enabled :-)

    [ -z "${var-}" ]
works with +u or -u
My rule is that shell scripts with growing complexity must be converted to a language with clearer and more bulletproof treatment of data, like Python. This also happens any time I want to support several command line arguments.

At one point in history, options were more limited. Now, it is just not worth the time to read code that may or may not have obscure errors in its very expressions that you must understand before you can even begin to examine the real purpose of the program!

You still need to know how to write safe shell programs even if they're one-liners.

And once you do, shell is really, really useful and convenient.

For example, Linux distributions are full of shell scripts that would be frankly awful to write in any other language that I know of... like this, to take a random example:

https://github.com/NixOS/nixpkgs/blob/master/pkgs/applicatio...

Most app developers aren't writing the scripts in Linux distros though.
Shouldn't they be quoted?
(comment deleted)
Eh. Looks OK to me, even with Python probably not being the tersest shell language: https://gist.github.com/anonymous/4203b7c6ffc9cb9ecf34d17cb8...
Hmm, I admit that's not too bad.

I still disagree with the ideology that any non-trivial script needs to be rewritten in Python. The shell is the standard way of interacting with a *nix system, and for me that's the major benefit of using shell for "system"-ish tasks. I already know how to make a symlink, a pipeline, a chmod, etc, so I don't need to learn a new syntax for that.

The shell script probably also behaves strange if any of $out ${docker-runc} $version, $man, etc contains a space, while the python-script most likely wont.
Nix-generated paths don't contain spaces, so it's unnecessary to quote those variables. Of course, as a developer you have to understand this.

The semantics of shell string interpolation are generally not that hard to learn, compared to learning all the nuances of programming in general that can make your program incorrect, so I find it a bit strange how it's considered to make shell completely useless for scripting.

Shell scripting has been working pretty well since, like 1975! It's not that bad!

> Nix-generated paths don't contain spaces, so it's unnecessary to quote those variables

Which works as long as attacker (or sleepy admin) doesn't happen to create suitably "bad" path

> Shell scripting has been working pretty well since, like 1975! It's not that bad!

Considering the amount of borked system because spaces/uninitialized variables -stories I've heard over years, I'm not so sure about how well it has been working...

Attackers who have commit access to your repository can do a lot of bad things quite easily.
Someone should try to automate this process, or at least make a complete bash -> python guide.

This is much nicer than I expected, but the functions are scattered around a bit.

Namespaces are a honking great idea, let's do more of those!

Alternatively there's always the option of using something like sh[0] and just shelling out to the existing utilities, if you don't mind the portability loss.

[0] https://amoffat.github.io/sh/

There are several strings in your gist that have the f misplaced, e.g.

    'f{out}/libexec/docker/docker-proxy'
instead of

    f'{out}/libexec/docker/docker-proxy'
Not sure it is materially important to the argument, but I thought it was interesting
> Not sure it is materially important to the argument

Not really, I've also forgotten to convert a few dashes to underscores in f-strings and probably made a few other mistakes along the way. It doesn't detract from the main point: it's a rough estimate of how such a converted script would look.

Though it makes me wonder if PyLint already has a lint for strings which start with F and have f-string placeholders.

>> need to know how to write safe shell programs

>> once you do, shell is really, really useful and convenient

Part of the issue here is that a large number of people who think they know how to write safe shell scripts do not in fact have the ability to do so.

There are so many gotchas to learn that it's all too easy to believe oneself competent, while being unaware of just how short one's knowledge falls. It's just too easy to write a script in the shell of your choice that works - until it encounters an edge case scenario that wasn't planned for, at which point the results can be catastrophic. This is true of any programming language, but the tasks for which people write shell scripts (such as massive system-wide filesystem manipulation) makes the consequences of a single-line bug very real.

> This is true of any programming language

Glad you mentioned that, because that was going to be my reply after reading the first half of your post.

I'm not sure a person who is fairly well-versed in shell scripting is going to do a worse job than trying to use python, perl, lua, etc. if he doesn't also hsve a high level of experience and compentence in those languages.

It has been literally 10 years since I touched Python. I use shell scripting every day. I know which one I'm going to be better at using to produce reliable code that does what I want it to do.

> It has been literally 10 years since I touched Python

That's fine. Python 2.6 is still in wide use.

Writing bug-free shell programs is hard because it doesn't provide safe defaults. By now I've gotten reasonably decent at shell scripts due to trial and error, and shellcheck... But if I'm doing anything fancy, I just opt for a node script using shelljs and any other third-party deps to cover my needs. This also has the benefit of typically working cross-platform. Although that's not as big of a concern for me, for some people it makes a big difference.

For my day-to-day shell, I use fish. IMO, it has much better defaults than bash and zsh, and it's fast. I still have to use bash sometimes, but it's becoming increasingly rare.

Your linked example doesn't really seem particularly tricky to me. I'd feel comfortable writing it in something like node or ruby, and I believe the result would probably be easier to consume for certain people.

There are a lot of unsafe defaults in JavaScript too, most prominently async error propagation. Basically all languages have some tricky fatal flaws, even Haskell (lazy I/O can make very strange bugs, etc).

I just don't think we should reject shell scripts entirely because some care is required to write them correctly. Shell is way too valuable.

You make a fair point. Although in more recent versions of JS the whole async error propagation isn't as big of an issue thanks to async functions and promises. I guess the biggest reason to use JS is that since I'm already using it every day, it's much easier to continue using a language I'm highly familiar with. Whenever I have to write a shell script I always have to pull up references and read through a couple Stack Overflow posts.

I don't reject shell scripts, though. I agree that in some cases it can lead to much simpler code. The power and composability with pipes and immediate substitutions is incredibly powerful. I've had a few cases where I replaced far more complicated node scripts with simpler shell scripts.

What I'd truly love is a reference guide that showed how to correctly do certain things with shell scripts. Here's a real-world example I struggled with: Writing end-to-end tests with selenium and a test runner. I wanted a script to start up a selenium server and wait for it to be ready. Then it should start the test runner. If the test runner exited successfully, kill the selenium server and browser, and exit with a 0. If the test runner or selenium server crashed or failed somehow, clean up and exit with a 1. Even after much trial and error, I still ended up with occasional Chrome copies staying open after failures.

Ah, for me one of the biggest reasons to use shell is that I use it every day to just do stuff in the terminal, so it's great to use that knowledge in scripts, too. But yeah, some of the syntax is quite obscure.

That kind of reference guide would be great. I haven't checked out any of the books on shell programming, but there should be a decent audience for a website with just good patterns for shell scripts, like you describe.

Trapping signals and keeping track of child PIDs is quite tricky, yeah. (It's quite tricky in any language!)

You might be interested in my "wd" program that's basically a bash wrapper for the WebDriver API, which I made because I wanted to write end-to-end tests and automation tools in shell.

https://github.com/mbrock/wd

Could you explain how writing, for example, text manipulation, would be safer in python than in shell? I understand formatting errors, shell escape failures, and the low hanging fruit, but in the end if I am just sed and awking my way around, I don't know how to do nearly the level of stuff I do in awk in python.

I also will never forget the time I spent a day doing a perl script to parse some data... took some time, came back, and did it in a one line awk... Very powerful.

I think somewhere in here is room for discussion of data, text, and the unix philosophy as everything as a file.

I said scripts with “growing complexity” should be expressed differently. I can and do use shell scripts for things that are relatively straightforward, and sed/awk-style transforms are a good example (although, even there, if too many quotes are involved you start to long for real escaping and things like triple-quotes).
A one-liner in Awk is almost certainly also a one-liner in Perl.
I didn't say I wasn't doing the perl badly.
It's an issue of comprehensibility, not any current performance metric.
Comprehensible to who? Other python programmers?

Most sysadmins do not use python routinely. They work in the shell all day every day though.

You're assuming that shell and Python which do the same thing are equivalently readable to a proficient user. As someone who frequently uses both, I would strongly disagree with that. I have inherited lengthy shell scripts which probably took as long to read as it would have taken to learn Python.

I would argue that a well written Python script is going to be more comprehensible to the average sysadmin than an equivalent shell script, even assuming the sysadmin is more conversant in shell. More portable and testable (!!) too.

or write it in go and run it in a container!
Shellcheck is a boon to write more (by at least three orders of magnitude) reliable shell code. I wish there were a 'set -o strict' that would make bash consider all shellcheck warnings into errors as well as unilaterally set -euo pipefail. That said, bash is still my go to language for many many things because at the end of the day it's insanely efficient as that koan[0] is still valid when substituting C for mostly any other language. As an anecdote I've replaced ridiculously big and slow Rakefiles and unmaintainable ad hoc ruby code with much shorter, readable and reliable shell scripts. Using the shell doesn't mean you can't drop a call to python, perl, jq or whatever, and the composability and performance can be terrific when properly wielded.

[0] http://www.catb.org/esr/writings/unix-koans/ten-thousand.htm...

I agree. Shell scripts are awesome until you reach a certain threshold of complexity, and that threshold is... low.

Error handling is particularly problematic. If you know all the tricks (things like set -e, set -o pipefail, always capture stderr, etc.), it's not too bad, but there are still so many traps that the mental overhead of catching every single edge case is exhausting.

One nasty issue with Bash is quoting, escaping and word splitting, because almost everything in Bash is about string manipulation. There are so many edge cases: $FOO is not the same as "$FOO", and sometimes you have no choice but to turn it into an array with eval FOO=($FOO) if you need mimic Bash's tokenization behaviour (e.g. respect backslashes and quoting).

(Yes, Bash has arrays! A lot of scripts get simpler when you realize this.)

These days I reach for Ruby very quickly even for small things.

> The moral of the story? Same as always: quote, quote quote. Even when things appear to work.

I would like to propose a different moral of the story, which is, as soon as your shell script requires control flow beyond executing a series of commands one after another, switch to a better programming language, such as Python.

Shell still has a huge advantage over other scripting languages- the syntax to invoke other programs is completely first class and natural. In real programming languages it's always some .os.exec() or whatever. Is python different?
Xonsh (a terrific python-based shell) addresses this with great elegance.
subprocess.check_call and subprocess.check_output are far less error-prone than the first class natural syntax of sh, where you have to remember to quote everything or have hidden bugs, likely even security vulnerabilities. And god forbid your coworker has to try to understand your sh code.

Clear syntax is better than pretty syntax.

Use the proper tool for the job, that I agree with. For most sysadmin tasks, that is shell scripts. Yes you have to be aware of the warts, as with any language or framework.

I would no more want to do system administration with Python than I would want to write a web application server in bash.

Python maybe better but there are FAR better languages out there...
Is anyone like me and just uses Perl as a Bash replacement? If I need to write more than a 2 line script, I just use Perl.
Perl is the reason my bash knowledge never grew beyond a rudimentary level...
I used to write many Perl tools for command line (I currently use Python because of team's tribal knowledge), though I make the choice between Perl/Python and shell looking not at the expected size, but at what kind of processing it is. Shell is an excellent tool when it comes to call external commands and generally working with files, and no other language makes it as easy.
Replacing bash+sed+awk is more or less why Perl was written initially (which also explains much of its syntax). I like it better than shell scripts mostly because the built-in data structures are a bit nicer. And I like Perl/PCRE regex syntax better than POSIX regex syntax.
I went the opposite way. I started out writing few liner bash scripts that just basically wrapped my perl script with proper environment vars, args, etc. and do all the heavy lifting in Perl. It also happens that my dad was a a build engineer back when they didn't really have a title for it and his Swiss Army knife was Perl. I literally learned coding beyond BASIC on HP-UX, Solaris, and Perl.

At some point, I just started doing more and more in bash and started using Perl only specifically for anything I was having trouble getting working with sed+awk.

As of today it's probably been 3+ years since I wrote anything in Perl, I'll just drop to Python if I need it now. Also my sed and awk skills have gotten better.

Bash is such a painful and error-prone language, I'm surprised anyone still writes it accept as a last resort. I've tried to mostly replace bash scripts with Python personally. It'd be excellent to see a bash meta language emerge something like what CoffeeScript or ES6 have done for JavaScript. App-level programmers shouldn't have to deal with this kind of bad "UX" regularly.
Has replacing scripts with Python worked well for you?

Context: I have the same feelings about Bash.

Well enough with libraries like Fabric and Click. Though my projects still occasionally have Makefiles with bash one-liners for commands like what one would put in package.json in a Node app. It's almost all upside. The only real catch is if you're running against an unknown system Python version, but using virtual environments solves that and is the best practice anyway even if it feels a little heavy for one file < 100 lines.

I also really like the pattern Django uses for extensible custom commands. I haven't built a system that extensive for my own apps yet but have enjoyed working with it when I am doing Django apps.

https://docs.djangoproject.com/en/1.11/howto/custom-manageme...

People continue to write in English as well, and except the errors within it :)
This problem isn't a Bash problem, nor is it a shell problem. '[' is a binary that takes a set of args terminates with a ']'. It's often just a symlink to the 'test' binary.

The other "problem" is quoting. In which case what would you expect to happen? Most people avoid spaces in file paths for this very issue. Argument passing is whitespace separated, so an application has no idea that you meant two (or more) args to be just one. Simple solution, use quotes.

If you were to shun all CLI usage in favor of another programming language, then you would be doing the exact same thing with system calls; Using the example from the article for consistency (even though it wouldn't make much sense in a more expressive language):

    var = "some value";
    exec("[", "-z", var, "]");
I was referring to replacing the logic with Python, ie:

    if var is not None:
        ...
    else:
        ...
Or

    if not var:
        ...
Or

    if var == '':
        ...
Etc. Depending on what you want to achieve.

That is a lot more readable to me than:

    [ -z $var ]
There's no disagreement there. Let's remember that `test` is older than most programming languages. I can't find an exact date, but 1979 is the earliest mention of it being in `sh v7` and turned into a UNIX System III built-in in 1981. And, that it was created to solve a problem where there is no type system, so there can be no test for NULL

The Bash-ism `[[` (Compound Conditional) is an attempt to reduce the unintuitiveness of `[` or `test`. However, you would still need to quote "$var". The biggest pain point of using an external application like `[` is that `&&` is a shell construct. As you may know, `&&` means "if the previous program succeeds, then execute this next statement. But, if you do `[ -z "$var" && ...`, then `[` will always fail because the command lacks the terminating arg `]`. To do conditionals with `[` you would have to use `-a` for "and" or `-o` for "or". You would want to do either `[ -z "$foo" ] && [ -z "$bar" ]` or `[ -z "$foo" -a "$bar" ]`.

Conditional Expressions relieve those issues as `&&` and `||` will work between the braces. So, `[[ -z "$foo" && -z "$bar" ]]` is okay.

(Some other pain points to `test` are `=` tests for string equality, not assignment; although, `==` is a synonym. For numerical equality testing, there are `-eq`, `-ne`, `-lt`, `-gt`, etc. Then, for those unfamiliar with the Unix way (and even, to some extend, to those who are), there are three cryptic sets of tests for File Type, Access Permission, and File Characteristics.)

But, then, Conditional Expressions makes a few head scratching changes. Like `<` and `>` are for sorting strings; not numbers. And, instead of leaving `-a` and `-o` as-is, they were remapped so that `-a` is synonymous with `-e` (wat?) and that `-o` is for testing shell options. (Doing a little research tell me that `[[` comes from `ksh88`, so maybe they're there for parity.)

Manpage for `test`, `[`: https://www.mankier.com/1/test

Conditional Expressions: http://www.gnu.org/software/bash/manual/html_node/Bash-Condi...

There is one, called Batsh. On mobile so can't link you, but check it out
Please. Use `[[ -z $var ]]` instead, and get this load of nonsense off your table.

And yes, `[[ ... ]]` is a bashism. It's not going work on the HP-UX box, that you do not have in your server fleet.

It won't work on dash or on BSD ash either. Do what you want, but you won't be doing it working for me because I care about portability.
Better make sure those shell scripts all run in csh/tcsh too. Never know when you'll be running on MacOS 10.1!
csh is still the default root shell on FreeBSD, which is in heavy use in millions of places around the world.

There are also a relatively large number of tcsh users in the world. This is why, even now on latest fedora, /etc/profile.d contains csh scripts.

As for sh, standards exist for a reason. Try to empathize with people who are not in your bubble.

Dash is on all my main servers, but Busybox is on the vendored stuff.

That means dash & ash.

Portability can matter.

the nice thing about standards is that you don't have to make sure the script runs in a billion different shells. You just have to make sure your code is POSIX compatible, and all of a sudden, all POSIX compatible shells will interpret it just fine! Isn't that fantastic?
csh and tcsh are not POSIX compatible.
Isn't that like arguing that one should code only in the form of polyglots that would run on any major programming language? The reality is that bash itself is both highly portable and already widely ported: in 2017 (or 2007) my bash script will run on essentially as many platforms as your POSIX shell script (including the aforementioned HP-UX: the last time I had access to one of those I definitely had access to a copy of bash; there was no reasonable JVM available!! but I definitely had bash).
Not HP-UX, but I absolutely use embedded systems (busybox) and *BSD.
> And yes, `[[ ... ]]` is a bashism. It's not going work on the HP-UX box, that you do not have in your server fleet.

I wish. I've to take care not to break things like AIX, HP-UX, Solaris 10, Openbsd 5.1. That's an open source project, but there've been actual production customers complaining when we broke things in the past...

EDIT: corrected openbsd version, machine has been updated since the last complaint

What about if $var is '== -z' :)
Confirmed. The title statement is wrong. I think the moral of _that_ story is that not even renowned experts can reliably get bash right.
Can an "empty" variable have a length greater than zero?

In Bourne once scripts I have been testing

   test ${#var} -gt 0 and

   test ${#var} -ge 1
In the past I have seen others use

   test x"" = x$var
Yes, the test to see if "x" matches is the old school trick I was taught
Yeah, in that it vaguely works in some cases.
Better lesson? Use zsh, where variable​s don't need to be quoted because they're expanded as a single shell word.
Thereby destroying compatibility with sh. At that point, why not use Python?
How is:

    [ -z $var ]
different from:

    [ $var ]
? I don't understand the distinction between a length-0 string and "the null string" in the context of /bin/sh.

I always just use [ "$var" ] and it's never failed me so far; should I be worried?

If var starts with a '-' then you'll get surprises.
I've just tried that out, it seems like it's OK as long as the argument is not exactly `-[abcdefgGhknoprsStuwxz]`, it's ok to have "-d test" or "-dtest".