1

I had a working shell script to assign a different layout to an external USB keyboard when it is plugged in, and it no longer works. I don't know it is because I switch from sh to zsh.

usbkbd_id="$(xinput -list | grep "USB Keyboard" | awk -F'=' '{print $2}' | grep "keyboard" | cut -c 1-2)"
for ID in $usbkbd_id; do
    setxkbmap -device ${ID} -layout "es"
done

I've noticed that when I checked it with command echo "device: ${ID} layout: ${usbkbd_layout}" >> test.txt in deed it's not looping through each ID, but simply adding "device: "at the beginning of the whole first variable output...

I've gone through similar questions and answers, trying different alternatives I've seen, like ${usbkbd_id[@]}, with my beginner level scripting skills, but I couldn't solve this problem.

Actually, maybe the real problem at the root is that I always get 3 IDs for my external USB keyboard, and actually assigning the keyboard layout to only one of them is effective, and I can't determine which. So sometimes this command works, sometimes it doesn't (when the correct ID is not the last one I guess).

Sadi
  • 495
  • @KamilMaciorowski Thanks! I've quoted both "$usbkbd_id" and "${ID}" now. I still get the same output if I send the output to a txt file though. Are you sure this will work? Switching was prompted just for better looks (like oh-my-zsh), I have to confess. ;-) – Sadi Dec 06 '20 at 11:56
  • @KamilMaciorowski Oh, yes, "Zsh does not split unquoted $usbkbd_id" does not necessarily mean "Zsh does split quoted $usbkbd_id", my mistake. ;-) I'll see if I can find a solution within zsh. Or just use bash for this script? Thanks... – Sadi Dec 06 '20 at 12:12
  • 1
    BTW if you have the full name of the device you might be able to use xinput list --id-only 'actual name of USB Keyboard device' – rowboat Dec 06 '20 at 13:37
  • 1
    @rowboat Thanks a lot! Actually it seems I was also too lazy to try and single out the Device ID I need from several IDs I get from xinput -list | grep "USB Keyboard". Now `xinput list --id-only 'SEM USB Keyboard' will result in a cleaner command (excluding 2 IDs for 'SEM USB Keyboard Consumer Control' and 1 ID for 'SEM USB Keyboard System Control' which apparently I don't need). – Sadi Dec 06 '20 at 14:02

1 Answers1

3

var=value is a scalar assignment. It assigns one and only one value to $var. You might have been confused by the behaviour of sh where it's also a scalar assignment but since sh does not have list/array variables, the expansion of variables, when not quoted undergoes a special split+glob operation (cause of many bugs and security issues in general and fixed in most more modern shells, including rc, es, fish or zsh).

Here, since you want to store several values, you want to use an array/list variable, not a scalar variable.

In zsh, array variable assignment is done with the:

var=( values )

syntax.

So:

usbkbd_ids=(
  $(xinput list | grep -Po 'USB Keyboard.*id=\K\d+(?=.*keyboard)')
)
for id in $usbkbd_ids; do
    setxkbmap -device $id -layout es
done

Though here, you might as well do:

xinput list |
  grep -Po 'USB Keyboard.*id=\K\d+(?=.*keyboard)' |
  xargs -I ID setxkbmap -device ID -layout es

Note that you rarely need to pipe awk, grep and cut together as awk is a superset of both other commands. However, awk is not very good at pattern-based extractions (unless you can use GNU awk extensions). Above that -P is a non-standard grep extension. As a standard alternative, you could use sed as:

  xinput list |
    sed -n 's/^.*USB Keyboard.*id=\([0-9]*\).*keyboard.*/\1/p'

With standard awk, that could be:

xinput list |
  awk '/USB Keyboard.*id=.*keyboard/ && match($0, /id=[0-9]+/) {
         print substr($0, RSTART + 3, RLENGTH - 3)
       }'

You could also use zsh's own pattern extraction features like:

usbkbd_ids=()
for line (${(f)"$(xinput list)"})
  [[ $line =~ 'USB Keyboard.*id=([0-9]+).*keyboard' ]] &&
    usbkbd_ids+=$match[1]
  • Muchas gracias! Worked like magic! And learned a little more about shell scripting. ;-) – Sadi Dec 06 '20 at 12:49