3

I have some logic like:

if [[ -n "$SSH_CLIENT" ]]
then
    sflag="-s $(echo "$SSH_CLIENT" | awk '{ print $1}')"
else
    sflag=''
fi

iptables -A MY_RULE "$sflag" -p tcp -m tcp --dport 9999 -m conntrack -j ACCEPT

In other words, I want to mimic only passing the -s flag to iptables if SSH_CLIENT is set. What actually happens is that the empty string is inadvertently passed.

I'm interested in whether it is possible, in the interest of not repeating two quite long iptables calls, to expand the flag name and value. E.g. the command above should expand to

  • iptables -A MY_RULE -s 10.10.10.10 -p tcp -m tcp ..., or
  • iptables -A MY_RULE -p tcp -m tcp ...

The problem is that in the second case, the expansion actually becomes:

iptables -A MY_RULE '' -p tcp -m tcp

and there is an extra empty string that is treated as a positional argument. How can I achieve this correctly?

Brad Solomon
  • 38,521
  • 31
  • 149
  • 235
  • Just remove your else block and leave `sflag` undefined instead of an empty string. – jordanm Feb 25 '20 at 16:56
  • @jordanm, explain how that would make a difference (assuming `-u` is not set). – Toby Speight Feb 25 '20 at 16:58
  • I have tried your code, it does not behave as you say. The final string is correct, in my example, with your exact code. What OS and shell are you using? – Francesco Gasparetto Feb 25 '20 at 16:58
  • @franzisk, eh? The code certainly does behave as the OP describes. How are you testing? `echo ...command...` does *not* emit output that's identical to how `...command...` would be run (it doesn't distinguish between `echo "foo bar"` and `echo "foo" "bar"`, for example, even though `"foo" "bar"` and `"foo bar"` are entirely different commands). – Charles Duffy Feb 25 '20 at 17:10
  • @CharlesDuffy are you saying that an assignment like sflag='' can result in any way to the value '' ? How would my echo transform the value in that funny way? – Francesco Gasparetto Feb 25 '20 at 17:14
  • 1
    @franzisk, the `''` isn't part of the *value* at all, but when you evaluate `"$emptyvar"`, it remains `""`, which is exactly equivalent to `''` and will show up that way in `xtrace` logs from the shell (and show up as an empty C string in the argv of external programs called with it on the command line). – Charles Duffy Feb 25 '20 at 17:15
  • So are you still convinced that the final command is iptables -A MY_RULE '' -p tcp -m tcp ? So how do you test string values if echo is not reliable? – Francesco Gasparetto Feb 25 '20 at 17:19
  • 1
    @franzisk, `log() { printf '%q ' "$@"; printf '\n'; }; log iptables -A MY_RULE "$sflag" -p tcp -m tcp --dport 9999 -m conntrack -j ACCEPT` will do the trick. (Obvs., you can define that function just once and use it as many times as you want; or just use `set -x` to let the shell do its own logging). – Charles Duffy Feb 25 '20 at 17:20
  • You are right. Thanks and sorry! – Francesco Gasparetto Feb 25 '20 at 17:22
  • 1
    @franzisk, ...or, to give you another way you can test this yourself: `python -c 'import sys; print(repr(sys.argv))' iptables -A MY_RULE "$sflag" -p tcp -m tcp --dport 9999 -m conntrack -j ACCEPT` -- see the `''` in the output after `'MY_RULE'` and before `'-p'`. – Charles Duffy Feb 25 '20 at 17:23

2 Answers2

6

All POSIX shells: Using ${var+ ...expansion...}

Using ${var+ ...words...} lets you have an arbitrary number of words only if a variable is set:

iptables -A MY_RULE \
  ${SSH_CLIENT+ -s "${SSH_CLIENT%% *}"} \
  -p tcp -m tcp --dport 9999 -m conntrack -j ACCEPT

Here, if-and-only-if SSH_CLIENT is set, we add -s followed by everything in SSH_CLIENT up to the first space.


Bash (and other extended shells): Using Arrays

The more general approach is to use an array whenever you want to represent multiple strings as a single value:

ssh_client_args=( )
[[ $SSH_CLIENT ]] && ssh_client_args+=( -s "${SSH_CLIENT%% *}" )
iptables -A MY_RULE "${ssh_client_args[@]}" -p tcp -m tcp --dport 9999 -m conntrack -j ACCEPT

The syntax "${var%% *} is a parameter expansion which expands to var with the longest possible suffix starting with a space trimmed; thus, leaving the first word. This is much faster than running an external program like awk. See also BashFAQ #100 describing general best practices for native-bash string manipulation.

Charles Duffy
  • 280,126
  • 43
  • 390
  • 441
1

This is one of those occasions where you probably don't want to quote the variable expansion:

iptables -A MY_RULE $sflag -p tcp -m tcp ...

Alternatively, and more robustly, we can use ${var+...} expansion:

 iptables -A MY_RULE ${SSH_CLIENT:+-s "${SSH_CLIENT%% *}"} -p tcp -m tcp ...

Read this as "if $SSH_CLIENT is set (and not null) then expand and substitute -s "${SSH_CLIENT%% *}", else nothing".

Note that we don't quote the $.+ expansion, but we do quote the individual arguments within it where needed (obviously -s doesn't need quotes, although you're free to add them if you want).

I removed the external awk command, as $.%% expansion is the simpler and more efficient way to truncate a string.

Toby Speight
  • 27,591
  • 48
  • 66
  • 103