blog · git · desktop · images · contact & privacy · gopher
2017-07-03
Consider a very simple file:
foo 1
bar 2
baz 3
yadda 4
whatever 5
You know want to edit this file in-place, moving the first line to the end of the file. The expected result is this:
bar 2
baz 3
yadda 4
whatever 5
foo 1
How do you do this in a shell script?
My first idea was using sed
:
sed -ni '1{h;n}; $!p; ${p;x;p}' data
It goes like this:
It works, but it’s not so easy to understand. You have to know about the hold space and what not. I dropped this solution, because I didn’t want to “impose” it on my colleagues.
Alright, let’s try a very explicit approach. How about this one:
lines=$(wc -l <data)
if (( lines > 1 ))
then
new_tail=$(head -n 1 data)
all_but_first=$(tail -n +2 data)
cat >data <<EOF
$all_but_first
$new_tail
EOF
fi
That’s easy to understand, isn’t it? It also shows a flaw in my sed
command above: It fails if there is only one line in data
. Hence the
if
.
Since I still had the discussion about bad taste in mind, I
wasn’t happy with this solution, either. That if
shouldn’t be needed.
Isn’t there something nicer?
Let’s try awk
.
awk 'NR == 1 { s = $0 }; NR > 1 { print }; END { print s }' data
That’s better. You can make it shorter by removing the { print }
block, because that’s the default action.
Problem is, awk
cannot edit files in place. So, the actual call looks
like this:
tmpfile=$(mktemp)
awk '...' <data >"$tmpfile"
mv "$tmpfile" data
Ugh, that’s pretty verbose. Plus, if the script was to fail for whatever
reason at the awk
step, there’d be a dangling temporary file. To avoid
this, we would have to install a trap
…
Still not happy.
It bothered me that this task is so easy to do in vi
, so why has it to
be hard in a shell script? Wait a second. Why not use ed
?
ed -s data <<END
1m$
wq
END
That’s not bad. It has the same problem as sed
: Very unusual commands.
Of course, they are related. But there is no corner case. And it’s just
one command, really. Pretty much anyone will understand wq
.
I like that.