Shell re-direct of output streams - positional dependancy observed

If I enter the command

snap list junk

the error message reported is

error: no matching snaps installed

I expect the following to correctly capture any such message in a specified file:

snap list junk 2>&1 >msg_file.txt

That does not work. I’ve tried wrapping the snap command within braces, which I’ve had to do for some cases, but that does not work for this scenario.

Any suggestions?

Test script
#!/bin/sh

tmp="/tmp/$(basename "${0}" ".sh")__$$.tmp" ; rm -f "${tmp}"

#for packageName in junk software-boutique
for packageName in junk
do
	skip=0

	export packageName
	rm -f "${tmp}"
	#{ snap list "${packageName}" 2>&1 } >"${tmp}"
	#testor=$( snap list "${packageName}" 2>&1 )
	snap list "${packageName}" 2>&1 >"${tmp}"

	#testor=$( echo "${testor}" | grep 'error: snap' )
	#testor=$( grep 'error: snap' "${tmp}" | grep 'not found' )
	#testor=$( grep 'error: snap' "${tmp}" )
	testor=$( grep 'error: no matching snaps installed' "${tmp}" )

echo "testor = '$testor'"

	if [ -n "${testor}" ]
	then
		echo "\n No SNAP package by name of '${packageName}' ..."
	else
		if [ -n "${packageName}" ]
		then
			rm -f "${tmp}"
			snap connections "${packageName}" >"${tmp}"

			if [ -s "${tmp}" ]
			then
				echo "\n Found connections related to package '${packageName}':\n"
				cat "${tmp}"
				echo "\n Skip or Continue ? [S/c] => \c" ; read dowhat
				if [ -z "${dowhat}" ] ; then  dowhat="A" ; fi
				case "${dowhat}" in
					[Ss] ) echo "\t Purge of '${packageName}' deferred ...\n" ; skip=1 ;;
					* ) echo " Ignoring status of connections ..." ;;
				esac
			else
				echo " Found no connections related to package.  Proceeding with purge ...\n"
			fi

			if [ ${skip} -eq 0 ]
			then
				echo "to purge"
				#snap remove --purge "${packageName}"
				echo ""
				#snap list "${packagName}"
			fi
		fi
	fi

	echo "\n Hit return to continue with next package ..." ; read k
done


My default environment is UbuntuMATE 22.04.5 LTS. The problem was detected and identified there, for both /bin/sh and /bin/bash.

I loaded a test environment for Xubuntu 26.04 LTS. The offending behaviour persists, again for both /bin/sh and /bin/bash.

The issue of the inability by both /bin/bash and /bin/sh being unable to redirect all messages (namely both stdout and stderr to a specified file) is for me a bug. That tells me that SNAP is directing messages to a non-standard “device”, contrary to the spirit of the Linux environment.

I believe that the bug must reside with SNAP because the behaviour exists for both shells.

Can anyone confirm whether they agree that it is a bug?

P.S. I tried to locate an “Issues” tab for the snapd package on GitHub, but that doesn’t exist. :frowning:

3 Likes

The behavior you’re seeing is not a bug in snapd — it’s a shell redirection ordering issue that catches a lot of people off guard. Redirection operators are evaluated strictly left to right, and that ordering matters more than most documentation makes clear.

When you write:

snap list junk 2>&1 >msg_file.txt

Here’s what the shell actually does, step by step:

  1. It evaluates 2>&1 first. At that exact moment, stdout (fd 1) is still pointing to the terminal, so stderr (fd 2) is made to point to the terminal as well.
  2. It then evaluates >msg_file.txt, which redirects stdout to the file — but stderr was already bound to the terminal in the previous step and is unaffected.

The result is that stdout goes to your file and stderr still goes to the terminal, which is the opposite of what you intended.

The correct form swaps the order:

snap list junk >msg_file.txt 2>&1

Now the shell redirects stdout to the file first, and when it hits 2>&1, stderr gets pointed at fd 1 — which is already the file. Both streams end up in the file.

In your script, the fix is straightforward:

snap list "${packageName}" >"${tmp}" 2>&1

If you want to stick with bash specifically (rather than /bin/sh), there is also a shorthand that avoids the ordering issue entirely:

snap list "${packageName}" &>"${tmp}"

The &> operator redirects both stdout and stderr to the file atomically, so order is not a concern. Note that &> is a bash extension and is not available in POSIX sh, so if your shebang stays as #!/bin/sh you should use the explicit >file 2>&1 form.

As for why the behavior is identical on both /bin/sh and /bin/bash — that’s exactly what you’d expect, since both shells evaluate redirections left to right per the POSIX specification. It would actually be a bug if they behaved differently.

You can also verify this quickly without snap at all:

doesnotexist 2>&1 >test.txt   # error still printed to terminal
doesnotexist >test.txt 2>&1   # error captured in test.txt
cat test.txt

Hope that clears it up.

8 Likes

What I don’t understand is why the following scenario was not properly redirected to the file:

{ 
      snap list "${packageName}" 2>&1
} >"${tmp}"

:frowning:

3 Likes

This one tripped me up for a while too, so it’s worth going through carefully.

At first glance, { cmd 2>&1 } >"${tmp}" looks reasonable. You’re grouping the command,
merging stderr into stdout inside the group, and redirecting the whole thing to a file.
Should work, right? It doesn’t — but the reason isn’t what you might expect.

Actually, looking back at your original script, the brace form was commented out:

#{ snap list "${packageName}" 2>&1 } >"${tmp}"   ← never ran
snap list "${packageName}" 2>&1 >"${tmp}"         ← this is what actually ran

So the brace version was never tested. That matters, because { cmd } >file actually
does set up the outer redirect before anything inside runs. The shell opens the file
and points fd 1 at it first, then executes the contents of the group with that fd already
in place.

The problem with { snap list "${packageName}" 2>&1 } >"${tmp}" is subtler — and it’s
still a redirect ordering issue, just one level deeper. Here’s what happens:

  1. The outer >"${tmp}" is set up first — fd 1 for the group now points at the file. ✓
  2. The shell enters the group and evaluates 2>&1 — fd 2 gets pointed at wherever fd 1
    is right now.

At step 2, fd 1 is already the file — so 2>&1 correctly sends stderr to the file
as well. Which means this form should actually work.

The real culprit in your original script was simply the uncommented line:

snap list "${packageName}" 2>&1 >"${tmp}"

No braces, wrong order — 2>&1 evaluated when fd 1 is still the terminal, then fd 1 gets
redirected to the file. Stderr stays on the terminal, stdout goes to the file.

The fix is the same regardless:

# simplest and clearest
snap list "${packageName}" >"${tmp}" 2>&1

# brace group also works if both redirects are on the closing brace, in order
{
    snap list "${packageName}"
} >"${tmp}" 2>&1

# bash-only shorthand — sidesteps ordering entirely
snap list "${packageName}" &>"${tmp}"

The &> operator is worth understanding separately. It’s not just > and 2>&1 stitched
together — it’s a single atomic operation that opens the file and assigns both fd 1 and
fd 2 to it simultaneously, so ordering is never a factor. The downside is that it’s a bash
extension and won’t work under #!/bin/sh.

The thing to keep in mind: 2>&1 duplicates stderr to wherever fd 1 is pointing at the
moment that line is evaluated
— not where it will end up later. Get that sequencing right
and everything else follows.

6 Likes

Thank you, Marcel, for both detailed explanations. It is much appreciated. I just don’t know why, 30 years of using those interchangeably is no longer the case !!! I find that very baffling.

But, I’m a realist and will be much more meticulous going forward.

Again, thank you.

2 Likes

You’re welcome, Eric — glad it helped.

On the “30 years” point: nothing actually changed. This left-to-right evaluation rule has
been in the Bourne shell since the late 1970s and is unchanged in POSIX to this day. The
reason it likely never bit you before is straightforward.

When you’re working interactively at a terminal, both fd 1 (stdout) and fd 2 (stderr) start
out pointing at the same place — your terminal device. So cmd 2>&1 >file in an
interactive session would have stdout go to the file, and stderr stay on the terminal —
but since stderr was already going to the terminal, you’d never have noticed anything was
wrong. Both streams appeared to behave the same either way.

The difference only becomes visible when it matters where stderr actually ends up — like
when you’re trying to capture it in a file for later inspection, which is exactly what your
script was trying to do. That’s when the ordering stops being invisible and starts causing
real problems.

So it’s not that it stopped working. It’s that you finally hit a case where the distinction
was visible.

4 Likes

The issue is the order of the redirections. 2>&1 inside the block connects stderr to the current stdout before the outer redirect happens.

Use:


{
    snap list "${packageName}"
} >"${tmp}" 2>&1

Now both stdout and stderr from the command group will go into the file. Bash applies redirections left-to-right, which is why the placement matters.

1 Like

I was in a similar situation few days back and ended up using tee. For the current issue, I also had to use 2>&1 as below,

snap list "${snap_name}" 2>&1 | tee "${tmp}"

I found out by fluke that you were missing ; in your snap line. So it will be,

{
    snap list "${snap_name}" 2>&1;
} > "${tmp}"
2 Likes

This topic was automatically closed 18 hours after the last reply. New replies are no longer allowed.