Comment by pxc
I do it this way but I indent the rest of the pipeline (like one typically would in a functional language with a pipeline operator or thread macro, or in an OOP language with method chaining via `.`):
foo --long-something \
| bar --long-other
and if the lines in subsequent commands in the pipeline start to get long, I also indent their arguments: foo --long-flag \
| bar \
--long-other-flag \
--long-option a \
--long-option b \
| baz \
--long-another-flag \
--long-flag-again \
> /path/to/some/file
I really like to use && on its own line like that. One of my favorite things about Fish is how it turns && and || from special syntax into commands, which it calls combiners, so you could write: foo \
| bar \
| baz
and echo done
I use this often for conditions whose bodies would only be one line, avoiding the nesting and indentation: test -n "$SOMETHING"
or set -x SOMETHING some-default
command $SOMETHING
In Bash, I usually use parameter substitution for this, but in other situations (other than setting default values for vars) I throw a backslash at the end of a line, indent and use && or ||, imitating the Fish style.One of my favorite patterns for readability is to use indented, long-form pipelines like this to set variables. They work fine inside subshells, but for uniformity and clarity I prefer to do
shopt -s lastpipe
foo \
| bar \
| baz \
| read SOMEVAR
I really enjoy 'maximizing' pipelines like this because it makes it possible to use long pipelines everywhere without making your program terse and mysterious, or creating unwieldy long lines.If you do this, you end up with a script is mostly 'flat' (having very little nested control flow, largely avoiding loops), has very few variable assignments, and predictably locates the variable assignments it does have at the ends of pipelines. Each pipeline is a singular train of thought requiring you to consider context and state only at the very beginning and very end, and you can typically likewise think of all the intermediate steps/commands in functional terms.
I tend to write all of my shell scripts this way, including the ones I write interactively at my prompt. One really cool thing about shell languages is that unlike in 'real' programming languages, loops are actually composable! So you can freely mix ad-hoc/imperative and pipeline-centric styles like this (example is Fish):
find -name whatever -exec basename '{}' \;
| while read -l data
set -l temp (some-series $data)
set -l another (some-command $temp)
blahdiblah --something $temp --other $another
end \
| bar \
| baz \
> some-list.txt
(I typically use 2 spaces to indent when writing Bash scripts, but Fish defaults to 4 in the prompt, which it also automatically indents for you. I'm not sure if that's configurable but I haven't attempted to change it.)I tend to follow my guideline suggested earlier and do this only close to the very beginning or very end of a pipeline if that loop actually modifies the filesystem or non-local variables, but it's really nice to have that flexibility imo. (It's especially handy if you want to embed some testing or conditional logic into the pipeline to filter results in a complex way.)
Shell script authors like yourself make me very happy. The pipe-to-read is a fun idea, I’ll use it.
One stanza I have at the beginning of every script:
This lets you trace any script just by setting the environment variable. And it’s nounset-safe.This was typed from memory on mobile so if the above is bugged, my bad :)