Search code examples
argumentscharacterzshzshrc

zshell eats :l in function immediately after argument


The Z shell seems to be eating part of my command after expanding arguments inside a function, but only in very specific cases?

Specifically this is happening if the argument is followed immediately by semicolon-l (:l). For example, I have this function in my .zshrc (which I would like to use to connect to notebooks on remote servers):

function notebook() {
    echo 1: localhost:8888:localhost:8888 aaa
    echo 2: localhost:$2:localhost:$2 $1
    echo 3: localhost:$2:ocalhost:$2 $1
    echo 4: localhost:$2localhost:$2 $1
    ssh -N -f -L localhost:$2:localhost:$2 $1
}

The thing I want to run is of course the last line, but I echo 4 other examples in hopes they can help pinpoint the issue: (1) is a hard coded version of the line I want to run - identical, except there are no arguments, (2) is exactly what I want to run, with arguments, (3) is 2 except instead of :localhost I have :ocalhost, and (4) is 2 except instead of :localhost I have localhost.

When I run notebook aaa 8888 from my terminal, I get the following output:

1: localhost:8888:localhost:8888 aaa
2: localhost:8888ocalhost:8888 aaa
3: localhost:8888:ocalhost:8888 aaa
4: localhost:8888localhost:8888 aaa
Bad local forwarding specification 'localhost:8888ocalhost:8888'

i.e.: (1), (3), and (4) all come out exactly as described in their echos, but (2) comes out with the :l gone. The same issue seems to happen when the ssh itself is called, causing my computer to complain about bad forwarding.

Why is this happening and how can I fix it? I don't even know what error to search for to describe this!

Thank you!


Solution

  • TL;DR: Add curly braces to the variable expansions: ${2}.

    Variable expansions like $1 and $var are actually shortcuts for the canonical forms: ${1} and ${var}. The shortcuts are very common since they work in a lot of situations, but sometimes there is ambiguity (e.g. when there are trailing characters after the variable name), and the shell interprets the variable without the curly braces differently. Some examples:

    > var=ABCD
    > print -l "$var" "$varX" "${var}" "${var}X"
    ABCD     # "$var"  - value of var
             # "$varX"  - nothing in variable varX
    ABCD     # "${var}"  - also the value of var
    ABCDX    # "${var}X"  - contents of $var, and literal X
    
    > set PARM1 PARM2  # assigns $1 and $2
    > print -l "$2" "$2y" "${2}y"
    PARM2    # "$2"
    PARM2y   # "$2y" - finds $2; 2y is not a valid variable name
    PARM2y   # "${2}y"
    
    > print -l "$var:l" "${var:l}" "${var}:l" 
    abcd     # "$var:l"
    abcd     # "${var:l}"
    ABCD:l   # "${var}:l"
    
    > print -l "$2:l" "$2:loc" "${2}:loc"
    parm2    # "$2:l"
    parm2oc  # "$2:loc"
    PARM2:loc # "${2}:loc"
    

    The last two sets demonstrate what you're running into. In zsh, a : after a variable name indicates that what follows is an expansion modifier that changes the behavior of the expression. In particular, the :l modifier changes all of the characters in the result to lowercase. That's not visible in your example since the variable only has numbers.

    This should work for your function:

    ssh -N -f -L "localhost:${2}:localhost:${2}" "${1}"