DEV Community

Cover image for Your bash scripts are rubbish, use another language
Tai Kedzierski
Tai Kedzierski

Posted on • Updated on

Your bash scripts are rubbish, use another language

(Headline photo from nixcraft's post to which I was reacting below)

TLDR:

  • Shell scripting is real programming
  • If you want to write shell script learn it properly
  • Use programming best practices like you would with other languages
  • Source-control it
  • It's weirdly idiosyncratic and needs extra attention
  • If the above doesn't suit you, you'll be better served by a less idiosyncratic language

So the below was a rant I posted in response to some pushback - someone suggested using Python instead of bash, and a few people complained about how it's overkill, how there are two versions of Python you need to get right, or you have to get it onto the machines in the first place, or suchlike.

I love shell scripting, and I still use it a lot, but I'm no fool. There are so many issues with it that blindly defending it for all use cases is foolhardy.

Not least because in company settings, most other people you will work with haven't the slightest idea what they're doing with shell scripts. They already get brownie points for thinking of putting the scripts in source control, kind a like getting grades for writing your name on your test.

SHELL SCRIPTING IS REAL PROGRAMMING. It should be source controlled, code reviewed and written to clean, maintainable standards. Because that code is meant for production.

So this is what I retorted:


If it's a personal machine, install Python - or other language of choice.

If it's the enterprise machines, have a policy to ensure Python - or other language of choice.

Shell is good and great, I'm a command line junkie myself, and I still turn to shell scripts for a lot of my work, but a shell script that's for more than wrapping a long pipe or two quickly becomes madness unless you actually put in the time and effort to learn the language PROPERLY. (The article feature pic is an example of utterly shitty code and their problem is not the filename spaces YET, but their RUBBISH handling of variables, and the lack of any effort to write cleanly)

I am an extensive bash scripter, and became so historically only for the reasons listed around here, which I once saw as valid reasons. They can all be worked around - and the overhead of solving "language X not on our machine farm" is often better return on effort than the years of unmaintainable brittle shell scripts you've been writing dozens of and never maintaining, or your colleagues cannot fathom and won't touch for love nor money. Unfoathomable shell scripts that run the backbones of deployments, builds, farm management and more - the backbones of many a company.

I've seen bash scripts by seasoned developers, and those are also utter trash. Your code may be cleaner than the average, but that's an extremely low bar.

Shell scripting is good and powerful in its own right, it is true, and I have advocated that people give it a proper try and actually learn it, but the sad reality is that nobody does. If it's any "proper" language, they learn the ins and outs gladly, by peer expectation or inherited bias; but shell, even if you learn it properly, someone else will come f/ck up your clean code because they can't be arsed.

I am endlessly pushing for developers and admins to actually learn to use bash/shell properly, make use of functions, encapsulate steps of logic, write clean code. "It's just a shell script," "that's overkill," "it's fine like this," "I don't want to sink time into learning this, it's not a real language anyway."

The other truth is that, in the sysadmins space, most can't even write clean Python/Ruby/Perl/PHP/JavaScript/chosen-lang either, and given the number of gotchas and things shell lets you get away with until you hit a catastrophic bug (of the coder's carelessness, the misunderstood shell behaviour is documented) (Steam bug anyone?), they'd be better off in a safer environment than shell scripting.

Shell scripting perennial issues that are not the fault of the coder:

  • will gladly let you get away by default with undefined variables (unless you explicitly set -ue)
  • comparison vs assignment is the only place where space matters (a=b and a = b are COMPLETELY different statements, wtf)
  • you cannot return arrays from functions, only a stream of text (the power and the Achille's Heel) (this one issue compounds many of the others, by preventing workaround functions from being written)
  • arrays cannot be passed down to functions as distinct items alongside other arguments (you can use references as a way around, but how many bash scripters know those?) (easier to use global variables right? yuck)
  • variables are global by default. unless you make your iteration counter local, you stand to see some weeeeird bugs
  • string splitting is done around an inherent part of the string, not as a a function operating on it (do we all know about IFS, does everybody know how to use it? didn't think so)
  • Is it really the shell you thought it you were running? Ever deployed bash scripts only to find that the only interpreter on the machine is sh? Or the environment forces you into sh by default? Or that in fact you're not running bash but ash? Or maybe the system default is dash. Anyhow, you have to write everything now in plain sh and lose any improvements that bash ever brought that make the task more bearable.
  • Inconsistent environments for common commands are rife. Your script uses the "mail" function? GNU or BSD, which options to use? You use netcat? Which variant, which options? You use tar, grep, rsync? You using GNU, BSD or Busybox implementations? (these variations happen endlessly when mixing Ubuntu, CentOS and Alpine deployments, and that's just the surface)
  • (I scoff at any pushback of "ensuring the right version of Python on the company systems")
  • attempting anything remotely event-driven yields a nasty pile of workarounds (I've tried, with muted success) Granted, this is a space which shell is definitely not designed for, but that's to say how far I tried to do everything in shell at one point. It's possible, but it's damn hard work where another language would have been better.

Most fundamentally, the view of shell as "not a proper language" hampers any impetus at large to learn it correctly and extensively, and understand its own idiosyncracies. At least with one of the other languages, developers have an inherited mindset that their skill in that language needs continual improvement, and will work towards this.

I still write tons of bash scripting. I love it. But recommending other people use another language is much more sane. Personally, I chose Python too. But in the end, I wouldn't recommend it unless you are going to do your darndest to learn. It. Properly.

Oldest comments (68)

Collapse
 
doomhammerng profile image
Piotr Gaczkowski

I saw once a Go library designed to mimic a UNIX shell and some of UNIX utilities. The idea was to use Go for what would traditionally be shell scripts.

The main two strengths of a UNIX shell are effortless pipes (try that with Python!) and external command execution as a first-class citizen.

Collapse
 
taikedz profile image
Tai Kedzierski

Yeah, doing pipes is a nightmare in anything other than shells, and the reason why I still love using shell scripts - chaining tools.

But there's a lot of logic that can often be sub-moduled out to other languages to make a cohesive whole. Some systems people don't like the idea of a program not being self-contained in a single file and you end up with 2000+ lines of sub-optimal code at best, more often than not downright horrendous though... take a peek at the install scripts of some of your favorite software to see what I mean...

Collapse
 
xtofl profile image
xtofl

Most modern languages allow function composition (pipes between functions). And they have extensive, stable repositories of libraries.

This boils down to shipping the right packages with the distribution - something Linux distro's already have, of course.

Collapse
 
bloodgain profile image
Cliff • Edited

Python's implementation support for pipes is reasonable and not that hard to use, but it's painful compared to actually using pipes directly in bash/tcsh/etc. Even using FIFOs on the command line feels more natural than the way you have to compose them in Python. The Unix way of composing operations is probably the best implementation of functional/stream programming ever.

And I am a Python lover, so don't think I'm knocking Python!

Thread Thread
 
xtofl profile image
xtofl

Absolutely right if you're going in and out of Python to create a pipeline of full fledged processes (I suppose you refer to the subprocess module and alike).

What I meant is: you can stay in the environment that supports 'tacit/point-free programming', and make sure you have everything you need:

# 'function' composition is a dash
# functions are processes
tac logs.txt | grep "http://" | xargs wget
Enter fullscreen mode Exit fullscreen mode

Could be just as easy:

# function composition not built-in
# functions are native
compose( read_file("logs.txt"), filter_lines("http://"), wget )
Enter fullscreen mode Exit fullscreen mode

provided you have these functions lying around somewhere.

Granted: Python has these FP concepts built-in, but not as nicely as the unix way. There are better languages for that: Haskell, F#, erlang...

-- my haskell is rusty - but function composition is a dot:
-- functions are native
pipeline = read_file . (filter_lines "http://") . web_get
pipeline "logs.txt"
Enter fullscreen mode Exit fullscreen mode

Interestingly enough, someone already thought up a Haskell shell: Turtle

Thread Thread
 
bloodgain profile image
Cliff

Yes, I was talking about composing processes like you would at the shell.

I see what you're saying about point-free programming, though. I still think the Unix style is the cleanest, most natural implementation of point-free programming, and I think the fact that it is a genuine stream of processing is a big point in its camp. However, if your Haskell example is accurate, I like it. The examples the Wikipedia article give seem less intuitive and a lot more LISP-y.

I think most programmers would probably find the use of compose in Python a lot less intuitive than nested generator functions, and it's certainly an inelegant implementation of point-free programming. I also wonder if it can eliminate some of the advantages of the generators? It probably doesn't based on the sample implementation, but I'd have to think carefully about if applying partial like that would have unintended consequences, at least in some cases.

Thread Thread
 
xtofl profile image
xtofl • Edited

I think so, too. You can make a nice 'fluent' DSL out of it, though.


class Chain:
        def __init__(self, *fns):
                self.fns = fns
        def __or__(self, fn):
                return Chain(*self.fns, fn)
        def __call__(self, arg):
                return reduce(lambda ret, f: f(ret), self.fns, arg)

Chain() | read_file | create_filter("https://") | web_get

def double(x):
        return 2*x

def fromstr(s):
        return int(s)

def inc(x):
        return x+1

def repeat(n):
        return lambda s: s * n

c = Chain() | fromstr | inc | double | str | repeat(3)

assert c("1") == "444"
assert c("20") == "424242"
Enter fullscreen mode Exit fullscreen mode
Thread Thread
 
taikedz profile image
Tai Kedzierski

..... ingenious.

I'm still trying to brain this, its possibilities and its limitations but... wow.

A bit of commentary would be very welcome :-)

Thread Thread
 
xtofl profile image
xtofl

I'll expand it in a full fledged post :) Or 'leave it as an exercise'?

Thread Thread
 
bloodgain profile image
Cliff

I either love this or hate it, I can't decide. Bravo, sir!

Thread Thread
 
xtofl profile image
xtofl

Somehow, I have hit the 'publish' button on dev.to/xtofl/i-want-my-bash-pipe-34i2.

Thread Thread
 
bloodgain profile image
Cliff

I've only skimmed the article so far, but it looks like a good one. I like the title! 😁

Thread Thread
 
taikedz profile image
Tai Kedzierski • Edited

@xtofl , @Cliff , I have finally gotten round to this, I think you will be gleefully dismayed.

dev.to/taikedz/shellpipe-shellpipe...

Collapse
 
louislow profile image
Louis Low • Edited

Bash is real programming. I wrote tons of modular complex logics and functions with just Bash running on top of many critical backbone servers. Bash is a beautiful language to me. Once come to the Linux environment on Server or IoT platforms, Bash is a mandatory language. If I need my program doing floating-point maths. I'll use C or C++ to create a tiny efficient engine with added APIs for the Bash script to sit on top of it. You know... Bash loves piggyback anything. It's cute.

Collapse
 
taikedz profile image
Tai Kedzierski

Yep it's real programming alright. That said, I've seen many cases (and been guilty of a few) where the logic would have been better moved to other languages, in their own succinct modules, and used bash to tie the pieces together. It all depends on the use case of course :-)

Collapse
 
pazyp profile image
Andrew Pazikas

As an Oracle Engineer for a large coporation 95% of the automation I write I use ksh, no python3 in RHEL 7 where I work and old legacy hosts dont even have bash so ksh works everywhere. So much of the automation my team writes is all ksh for a whole host of tasks and while that last 5% is a bit of Python I can't see it dynamic changing anytime soon as SHELL scripting is so imbedded into the way the team and the business functions.

Collapse
 
taikedz profile image
Tai Kedzierski

Once a technology is adopted it's hard to unstick it... That being said in your case it sounds like everybody gets the training required to write clean ksh? So long as they've learnt ksh fully and not "just the easy bits," then my argument remains the same: so long as there is impetus and requirement to learn the language properly, there's no (or much less of a) problem!

Collapse
 
taikedz profile image
Tai Kedzierski

I'd say that's pretty much 99% correct. It is a language with a specific use case in mind, that of chaining tools. The mistake often made is trying to, as you say, venture into incompatible areas, where problems quickly pile up.

POSIX compatibility is a bit of a gripe of mine - it's there to ensure cross-compatibility, so that's definitely a bonus, but those specifications are so restrictive that they cripple a number of capabilities various shells have come up with to "resolve" the issues of POSIX.

One of them is array iteration - decently well fixed in bash, and probably workable in most other shells, but quite horrific to handle in POSIX sh. With properly setup server management practices, it should be possible to mandate any software required, so bash would be my choice to mandate, but other teams prefer other shells, and that's fine too.

Mind that I'm not saying that bash and shell scripting is bad - on the contrary I love it. I'm mostly saying that people generally don't bother to learn to use it well and for its use-case, and so we end up with a default quality of scripting across the board that is horrific - and that level of quality is passed on because that's what expected. And so my message became a distorted one: "for pity's sake.. learn it properly... stop adding to the problem..." ;-)

Collapse
 
eljayadobe profile image
Eljay-Adobe

I use bash day in and day out. It's my favorite shell. I've been using it for a long time.

But... whenever I need to do something for production, I reach for Python 3.x. I use git for source control. I have my code peer reviewed.

I've converted some other peoples bash scripts into Python. I've converted some other peoples Perl scripts into Python. I've converted some other peoples JavaScript on Node.js into Python.

Because I love Python? (Well, I am fond of it, true.) No, because the other code was hard to understand and hard to maintain and hadn't been code reviewed and wasn't abiding by the approved scripting engines. (Node.js is actually approved, but the other points hold.)

The "hard" part isn't a shortcoming of the language (after all, these aren't PHP), it's a shortcoming of the programmer making a tasty pot of spaghetti code -- and spaghetti code can be written in any language.

Collapse
 
taikedz profile image
Tai Kedzierski

spaghetti code can be written in any language.

Yes, but some languages (and their baggage) can be more conducive to spaghetti ;-)

If you look for examples of good Python, Java, JavaScript, C, Golang, etc, you can find them, and there are LOTS of people trying to demonstrate how to do it properly. Examples are everywhere.

Shell (and to a lesser extent Perl) is plentiful in the wild - and it is mostly the awful stuff that is most readily available. (this was my point about peer expectations to improve skills in some languages but not others).

If you did the conversions in a company, and everyone else can write clean Python then great :) Although perhaps getting people to write clean lang-x in the first place would have been just as productive. I speak from experience when I say trying to make people learn and write clean shell is a pain.

Collapse
 
eljayadobe profile image
Eljay-Adobe

That is a valid counterpoint, and I concur. Good code can be written in any language, except PHP of course. (I would have said PHP or Perl, but I've actually seen good code in Perl, so I know it is actually possible.)

Collapse
 
titanhero profile image
Lex

I support you, I love bashScript is my favorite code language, it is very powerful, in my begin with bash, my due to my big ego I did it my bashScripts complex, you know, to prove that I knew coding with bash, right now I always try to do my code very, very simple, for anyone can understand it easily, I like a lot the one liners and use the test [[ ]] and && for the flow control, in one line, but that do it more complex of read my code, so the best is use indent😁 and do your code of the most simplest way😁👍✌️

Collapse
 
autoferrit profile image
Shawn McElroy

In regards to having to choose between 2 versions of python, no not anymore. 2.7 is end of live and at minimum everyone should be using 3.6+. But you should be on the latest stable, currently 3.8

Collapse
 
taikedz profile image
Tai Kedzierski

Yeah, but legacy scripts are totally a thing. Some outfits don't even know you can have both installed side-by-side so you can do a progressive script migration. So they did no migration. And they mandate all Python2 even now. It's not pretty. Doing my best on my side to further educate my colleagues, but that's just my corner...

Collapse
 
autoferrit profile image
Shawn McElroy

Yea legacy is a different thing I agree. But we should all be encouraging to get people to upgrade.

Collapse
 
bhupesh profile image
Bhupesh Varshney 👾 • Edited

what are your thoughts on shellcheck ?

Collapse
 
taikedz profile image
Tai Kedzierski

Good in principle, but it's unrelated. Shellcheck will perform syntax linting.

It will not highlight bad style or practices ;-)

But good shout out for linting tools!

Collapse
 
csgeek profile image
csgeek • Edited

I usually start with a bash script. Because it's just easy. Then I need to do something too complicated like concatenate a string or use arrays, hashmaps etc and I end up rewriting the entire thing in python.

I'm sure there are many people that are amazing at writing bash scripts but it's never been very readable. Once you get just a bit complicated languages like PERL are starting to look pretty by comparison.

Also my biggest petpeave bash sed and awk are not compatible between Linux, mac and likely other variants.

Collapse
 
taikedz profile image
Tai Kedzierski • Edited

I'm sure there are many people that are amazing at writing bash scripts

No there are only a handful :-D (I'm only half kidding)

Also my biggest petpeave bash sed and awk are not compatible between Linux, mac and likely bad variants.

My biggest gripe with shell scripting is the tooling dependency.

I used to quip "in bash, ANY language is your library!". The obverse of course is true: any tool can subsume any other in any given environment, and you won;t know til your script crashes.

macOS uses the BSD Utils by default, as do the BSDs in general ; Fedora uses GNU Coreutils, except when they use a BSD adaptation ; Ubuntu is GNU Coreutils most of the time; Alpine uses BusyBox (a great tool in and of itself, but a thorn for cross-platform shell scripting) ; ...

Collapse
 
csgeek profile image
csgeek

it's just limited. It makes it harder to write tests and follow most coding patterns.

Granted there are tools like this: github.com/sstephenson/bats but not sure if anyone uses them. Also.. Libraries!! How many times do we need to re-write the same fix to the same problem.

macOS uses the BSD Utils by default, as do the BSDs in general ; Fedora uses GNU Coreutils,
except when they use a BSD adaptation ; Ubuntu is GNU Coreutils most of the time; Alpine uses
BusyBox (a great tool in and of itself, but a thorn for cross-platform shell scripting) ; ...

So.. you're saying that it's a very repeatable and consistent ecosystem? O.o

Yeah that's part of the issue. It's easy, it's everywhere just run bash foobar.sh, except when it doesn't work and you have to write 7 versions to support all the various edge cases.

It's not as easy to write, but I'm really liking go. It's way more complicated and verbose than bash, but at the end of the day i end up with 1 file to copy around.

Thread Thread
 
taikedz profile image
Tai Kedzierski

Yeah libraires... is why I started my bash-builder project and its sibling bash-libs. Build the script and have... a single file to copy around ;-)

The backbone of most of my bash scripting nowadays...

Collapse
 
gwutama profile image
Galuh Utama

I have my own rules regarding bash.

Use bash for:

  1. init scripts
  2. scripts that will barely grow in the future
  3. scripts that are less than 50 lines of code

Use python for everything else.

Collapse
 
taikedz profile image
Tai Kedzierski

Yes in principle however

init scripts

If that script grows arms and legs, you'll have wanted it to be just the glue that calls the more complex parts (in other-lang)

scripts that will barely grow in the future

I'm sure you are seasoned enough to know, this NEVER goes to plan :-D

scripts that are less than 50 lines of code

I would say, less than 20 lines, but I've written ones more than 300+ lines. I just wrote them cleanly, namespaced the heck out of the functions, and re-used code as much as possible (the reason behind my bash-builder project)

I mean yes, I do agree with your points, but life has surprises, of which Inexperienced Colleagues is but one of many...!

Collapse
 
jrbrtsn profile image
John Robertson

Bash (and sh relatives) is probably the most valuable and yet poorly understood scripting language in existence. This owes largely to confusion about the nature of subshells and ignorance of the 'source' command, as well as ignorance of the builtin regular expression parsing facility and associative arrays. I can't think of a single instance where I would prefer Python over Bash or C or C++, or ...

Collapse
 
taikedz profile image
Tai Kedzierski

Shell languages are great when used as "glue" logic, it becomes iffy when you start trying to do "business" (read: "non-glue") logic in them...

I did try to base a coding practice / workflow around sourcing, but sourcing is always relative. Function re-usability is important for good practice and reducing repetition etc, but if two re-used files re-source a same file, things can get messy... This problem was actually my primary motivation for creating bash-builder: a set of re-usable "libraries" that could be re-used in building other scripts... without worrying about where to source from, or what the end-point's scripts setup was.

I love the [[ $X =~ regex ]] operation. Use it lots. Pain when the environment is not bash but dash or ash or POSIX ... There are ways around it (install), but sometimes it's not always possible...

Well... I did once write a web server in bash... required very specific versions of netcat and grep, and one mail notification script I wrote wouldn't run the same under the Ubuntu and Fedora servers (difference in mail implementations)

Collapse
 
shaunakde profile image
Shaunak De

There have been so many times I've started writing a shell script, midway thought that the task would he so much better solved in python, but continued writing shell anyway. I do agree, for most people Python with a shell script calling the python function is going to be a better solution.

Collapse
 
smcjones profile image
Sean • Edited

I say, write in the language that makes you feel happiest, but write it like you're the one who is going to have to debug it/add to it in a year's time. Comment, separate variables from content, and separate concerns into separate functions (or files in the case of bash).

If someone comes to you and asks you what the code you wrote a year ago is doing, (1) you already messed up, and (2) if you can't figure it out in a matter of moments, then you messed up.

Most of us have been in this situation, so we can only strive to do better. For me, the last thing I want to do is debug a very large bash script, so I go for another language (Python, C, etc.)

Collapse
 
taikedz profile image
Tai Kedzierski

Totally!

That said, I've been reviewing code recently for various CI developers. If I show them how their Python is not clean and there is a better way they're "huh yeah I get it, I'll try to remember that for next tiem". I do the same thing with their shell and they're "yeah, my shell skills are bad haha, anyway, moving on." Whether I press the point or not is moot here -- that initial lack of interest is the core problem... and I do think that "perception" of shell languages is in part a problem... hence for these kinds of people, all-out moving towards other languages, as you recognise, is probably more conducive to clean code...

Collapse
 
ivor52 profile image
ivor52

if i'm not wrong, all the reasons you stated to not use bash fall with the coder. yes, there are language-based limitations (can't pass arrays, all variables global, etc.), but that's the point, isn't it?

so when the job is done by a good coder, is maintained, and you don't need to do something fancy like pass arrays around around, it works just fine. i know, i have a production system of thousands of IoT's all over the world that uses scripts for all of its small one-off tasks (real work is done in C and docker). they are clean, controlled and upgradeable.

python requires a moving dependency, not a good choice for distributed systems where you might not have complete control of the overall environment, imo.

richard

Collapse
 
taikedz profile image
Tai Kedzierski

all the reasons you stated to not use bash fall with the coder

Yes, exactly. And it would be SO MUCH BETTER if this situation improved!

so when the job is done by a good coder

Like I said, I've seen utter trash written by otherwise competent coders. I don't know why the discipline goes out the window as soon as they face bash. Probably if I threw Haskell at them they'd try to learn it properly. But shell? It's similar enough to C and JavaScript that they can write stuff, but sufficiently idiosyncratic that they just want to be over with it already.

python requires a moving dependency

However, shell is FULL of moving dependencies. If you're writing shell, you're probably wanting to target *nix systems - OK fine. So you have a CentOS farm, Ubuntu instances, some Dockers with Alpine, and some BSD just to keep some diversity.

Each of these (can) have in varying forms different implementations of mail, grep, rsync, sed. Some use GNU Utils, some rely on BSD Utils in certain situations, some use Busybox, some use other things... I have been deep in that hole before and cried many times "why is this ONE environment different in this ONE way making me write an ENTIRE shell submodule to cope with it???" (and every environment has that one item)

Python as a moving target between 2 and 3 is a piece of cake in comparison.

Collapse
 
sheldonhull profile image
Sheldon

This is where PowerShell shines. Cross platform, shell capable, rich objects and ecosystem. Pester test framework supports tested robust modules. If dotnet is in your environment it works great and the pipeline is powerful.
I was surprised at how much of a transition I had leaving windows behind and moving to macOS and docker based workspaces. Most of my stuff runs smoothly on all 3 systems

Collapse
 
taikedz profile image
Tai Kedzierski

I never gave PowerShell a go on anything other than Windows... I found it there to be excessively verbose for a command language, but I can see where that might in fact improve its use as a scripting language.

The availability of object output from tools is certainly a step up from parsing text streams (especially those that are designed for on-screen display, instead of automations), but then that puts the onus on the tool writer to write for that compatibility. I'd more readily have a JSON parser in lang-x and use it to extract from the outputs from other tools, and standardise other tools around JSON (seems to be the most common thing to handle these days). This could even unify web-API-based programming and shell programming nicely...

Question remains though - is most of its functionality derived from built-in functions, or calls to commands (actual executables) in the local system? Not sure, but as I understand you, you are running the same PS in Windows and non-Windows environments?

Also what's the average quality of examples out in the wild to learn from? From Python / JS / C etc there are plenty of articles, blogs, tutorials and the likes where "clean code" is insisted on, and professional environments may insist on it.

shell scripts - including PowerShell so far as I have seen - are rarely seen as targets for the same zeal of cleanliness and suffer as a result....