6

this works as expected in bash:

> t="ls -l"
> $t #== ls -l
> "$t" #== "ls -l"
ls -l: command not found

But in zsh I got this:

> t="ls -l"
> $t #== "ls -l"
ls -l: command not found

How can I force the shell to parse the variable value again like bash does?

estani
  • 983

3 Answers3

15

If you want a variable that expands to more than one argument use an array:

var=(ls -l)
$var

But to store code, the most obvious storage type is a function:

myfunction() ls -l

Or:

myfunction() ls -l "$@"

for that function to take extra arguments to be passed to ls.

The fact that bash like most other Bourne-like shells splits unquoted variables upon expansion is IMO a bug. See the kind of problems it leads to. But if you want that behaviour, you can set the shwordsplit option. You could also add the globsubst option to restore another bug found in bash and other Bourne-like shells where variable expansion is also subject to globbing (aka pathname expansion). Or do the full shebang with emulate sh or emulate ksh (but lose a few more zsh features).

Without having to go there, you can also tell zsh to explicitly split a variable:

var='ls -l'
$=var # split on $IFS like the $var of bash/sh
${(s[ ])var} # split on spaces only regardless of the value of $IFS
var='*.txt'
echo $~var # do pathname expansion like the $var of bash/sh
var='ls -ld -- *.txt'
$=~var # do both word splitting and filename generation
  • I don't consider word splitting it a bug at all, since bash is built on that concept (that's why IFS is there and in that form). But it doesn't matter, I just wanted to understand why zsh makes no difference between quoted and unquoted. Thanks for the thorough explanation! – estani Jul 07 '19 at 12:00
  • Reading your post for a second time I realize you mentioned "$@" which is funny, cause that's there because of word splitting. Without code splitting theres no difference between $*, "$*",$@, "$@",$1, "$1", right? Everything is just one large parameter (but perhaps zsh behaves differently under some circumstances like function calls... not sure... – estani Jul 07 '19 at 12:07
  • 1
    @estani, zsh makes a difference between quoted and unquoted. For a scalar variable, the difference is when the variable is empty in which case $var expands to no argument at all while "$var" expands to one empty argument. For arrays, "$var" is like "${var[*]}" in ksh/bash (elements joined with the first character of $IFS), while $var is all the non-empty elements as separate arguments (same as ${var[*]} after a set -o noglob; IFS= in ksh/bash). For all the elements (including empty ones) as separate arguments you still need "${var[@]}" like in ksh/bash... – Stéphane Chazelas Jul 07 '19 at 13:06
  • ... though you can shorten it to "$var[@]" or can also use the @ parameter expansion flag ("${(@)var}"). – Stéphane Chazelas Jul 07 '19 at 13:07
  • 2
    $*/"$*"/$@/"$@"/$1`` are short forms of $argv[*], "$argv[*]", $argv[@], "$argv[@]", $argv[1] so are all different. The main difference with ksh/bash is again that word splitting and globbing are not done on top of that when unquoted (which for arrays makes even less sense). – Stéphane Chazelas Jul 07 '19 at 13:10
4

This item is covered in zsh faq.

Long story short. There are a few ways to fix the behavior:

  1. Set setopt shwordsplit:
setopt shwordsplit
t="ls -l"
$t
  1. Use eval (which better to avoid due to possible security issues):
t="ls -l"
eval $t
rush
  • 27,403
1

If the variable is already defined as a string, you can use

${=t}

The = flag tells zsh to use word splitting while expanding the variable.

Thayne
  • 235