bashubuntuxdotool

xdotool commands from an array in a bash script


I have a bash script which needs to run several xdotool commands in a sequence. I cannot get the type commands with strings to run properly.

It works fine if I type them out manually:

#!/bin/bash

xdotool type 'git status'
xdotool key KP_Enter

But I now need to do some other bits such as sleeping in-between the commands, so it is more sustainable to move to an array of commands and a for loop:

#!/bin/bash

declare -a COMMANDS=(
    "type 'git status'"
    "key KP_Enter"
    )

for COMMAND in "${COMMANDS[@]}"; do
    xdotool "$COMMAND"
    #Do some other stuff...
done

I have tried every combination of single quotes, double quotes and back ticks I can think of but it always either eats the space in 'git status':

$ 'gitstatus'
Command 'gitstatus' not found, did you mean:
  command 'mgitstatus' from deb mgitstatus (2.2+dfsg-2)
Try: sudo apt install <deb name>

Or xdotool doesn't register the commands:

xdotool: Unknown command: type 'git status'
Run 'xdotool help' if you want a command list
xdotool: Unknown command: key KP_Enter
Run 'xdotool help' if you want a command list

Solution

  • Understanding The Problem

    When you expand a scalar variable in a quoted context, the exact contents of that variable become part of exactly one string. That means that quotes inside the variable are just data, instead of acting as shell syntax -- so using JSON for clarity, you're effectively running ["xdotool", "type 'git status'"], when you instead want to run ["xdotool", "type", "git status"].

    Leaving out the syntactic double quotes isn't better: that makes your code run ["xdotool", "type", "'git", "status'"], splitting on spaces but not treating the quotes as special but just passing them as data within the individual arguments.

    To make sure the quotes are treated as syntax and tell the shell which words to coalesce together, you need to do something different.


    Approach A: One Array Per Command

    This doesn't require any non-bash tool, but it does require your data structure to be modified -- notice how here we're defining a separate array for each command to run, and then iterating over variables with the prefix all those arrays share.

    #!/usr/bin/env bash
    
    case $BASH_VERSION in
      [1-3].*|4.[012].*)
        echo "ERROR: Bash 4.3+ needed" >&2
        exit 1;;
    esac
    
    xdotool_command_01=( type 'git status' )
    xdotool_command_02=( key KP_Enter )
    # ...can go through xdotool_command_99 here; make it 3 digits if you need more
    
    for _cmdvar_name in "${!xdotool_command_@}"; do
      declare -n _cmdvar="$_cmdvar_name" || exit
      xdotool "${_cmdvar[@]}"
      unset -n _cmdvar
    done
    

    Approach B: Interpret String To Array Before Running

    I'm using xargs here; we have existing Q&A that provides alternatives, including Python. In pretty much every case (that isn't abjectly insecure as with eval), however, this requires calling an external tool rather than using only bash builtins.

    declare -a commands=(
      "type 'git status'"
      "key KP_Enter"
    )
    
    for cmd in "${commands[@]}"; do
      xargs xdotool <<<"$cmd"
    done
    

    Insecure Approach C: Using eval

    Mind, if you trust all your commands not to do anything malicious if parsed as shell syntax, there's an easier answer here:

    declare -a commands=(
      "type 'git status'"
      "key KP_Enter"
    )
    
    for cmd in "${commands[@]}"; do
      eval "xdotool $cmd"
    done
    

    eval combines all its arguments into one string (which we're mooting for clarity by making everything one string explicitly), then parses that string as syntax (so quotes that would otherwise be literal data instead have their syntactic meaning).

    Do not ever do this when your string has had untrusted data (filenames, command-line arguments, or anything else you-as-the-developer haven't personally vetted to be safe) substituted in.