6

I'm observing differences between zsh and bash when using read on macOS.

With bash this script

echo "characters" | while IFS= read -d '' -n 1 a; do printf %s "$a-"; done

Produces

c-h-a-r-a-c-t-e-r-s-

Whereas on zsh I have no output and no errors Is it possible to use read to iterate character by character?

3 Answers3

10

The options for the read command are significantly different between bash and zsh. In this case, the problem is that -n has completely different meanings between the two: in bash, it says how many characters to read, while in zsh it modifies the -p or -l options (which have to do with completion functions, and are irrelevant here).

In zsh, you use -k to specify the number of characters to read, but it also defaults to reading from the terminal rather than stdin, so you have to also add -u 0 to tell it to read from stdin.

Long story short: in zsh, use read -n '' -k 1 -u 0 somevar to read a single character from stdin.

BTW, there are lots more differences between read in bash vs zsh (and vs other shells). The POSIX standard only specifies the -r option; everything else is a nonstandard addition, and any similarity between different shells' extensions should be considered a happy accident.

0

In the interest of having a reproduction of what the original poster was interested in with minimal changes, here is something that does mostly the same, along with output:

➜  ~ echo "characters" | while IFS= read -u 0 -k 1 a; do printf %s "$a-"; done

Results in:

c-h-a-r-a-c-t-e-r-s-
-%

Note the extra characters at the line. I haven't looked into it in detail, but it's outputting the newline that it finds at the end, followed by a - and then I suspect the % has something to do with the end of stream.

Magnus
  • 101
0

I am perfectly aware that the original question asks: "Is it possible to use read to iterate character by character?" The question also uses a while loop.

But if the real emphasis is on iterating character by character, then there are two much better ways to do it (in zsh).

str='characters'

Iterate by index

for i in {1..$#str}; do echo $str[i]; done

Iterate by character

for char in ${(s[])str}; do echo $char; done

If you just desire to get the c-h-a-r-a-c-t-e-r-s result, just do

echo ${(j[-])${(s[])str}}

That is: first split the input string on the empty pattern, then join again with a dash.