Daniel
Daniel

Reputation: 12026

eval printf works from command line but not in script

When I run the following command in a terminal it works, but not from a script:

eval $(printf "ssh foo -f -N "; \
       for port in $(cat ~/bar.json | grep '_port' | grep -o '[0-9]\+'); do \
           printf "-L $port:127.0.0.1:$port ";\
       done)

The error I get tells me that printf usage is wrong, as if the -L argument within quotes would've been an argument to printf itself. I was wondering why that is the case. Am I missing something obvious?

__

Context (in case my issue is an XY problem): I want to start and connect to a jupyter kernel running on a remote computer. To do so I wrote a small script that

  1. sends a command per ssh for the remote to start the kernel
  2. copies via scp a configuration file that I can use to connect to the kernel from my local computer
  3. reads the configuration file and opens appropriate ssh tunnels between local and remote

For those not familiar with jupyter, a configuration file (bar.json) looks more or less like the following:

{
  "shell_port": 35932,
  "iopub_port": 37145,
  "stdin_port": 42704,
  "control_port": 39329,
  "hb_port": 39253,
  "ip": "127.0.0.1",
  "key": "4cd3e12f-321bcb113c204eca3a0723d9",
  "transport": "tcp",
  "signature_scheme": "hmac-sha256",
  "kernel_name": ""
}

And so, in my command above, the printf statement creates an ssh command with all the 5 -L port forwarding for my local computer to connect to the remote, and eval should run that command. Here's the full script:

#!/usr/bin/env bash

# Tell remote to start a jupyter kernel.
ssh foo -t 'python -m ipykernel_launcher -f ~/bar.json' &
# Wait a bit for the remote kernel to launch and write conf. file
sleep 5
# Copy the conf. file from remote to local.
scp foo:~/bar.json ~/bar.json
# Parse the conf. file and open ssh tunnels.
eval $(printf "ssh foo -f -N "; \
       for port in $(cat ~/bar.json | grep '_port' | grep -o '[0-9]\+'); do \
           printf "-L $port:127.0.0.1:$port ";\
       done)

Finally, jupyter console --existing ~/foo.json connects to remote.

Upvotes: 1

Views: 309

Answers (1)

John Kugelman
John Kugelman

Reputation: 361909

As @that other guy says, bash's printf builtin barfs on printf "-L ...". It thinks you're passing it a -L option. You can fix it by adding --:

printf -- "-L $port:127.0.0.1:$port "

Let's make that:

printf -- '-L %s:127.0.0.1:%s ' "$port" "$port"

But since we're here, we can do a lot better. First, let's not process JSON with basic shell tools. We don't want to rely on it being formatting a certain way. We can use jq, a lightweight and flexible command-line JSON processor.

$ jq -r 'to_entries | map(select(.key | test(".*_port"))) | .[].value' bar.json
35932
37145
42704
39329
39253

Here we use to_entries to convert each field to a key-value pair. Then we select entries where the .key matches the regex .*_port. Finally we extract the corresponding .values.

We can get rid of eval by constructing the ssh command in an array. It's always good to avoid eval when possible.

#!/bin/bash

readarray -t ports < <(jq -r 'to_entries | map(select(.key | test(".*_port"))) | .[].value' bar.json)

ssh=(ssh foo -f -N)
for port in "${ports[@]}"; do ssh+=(-L "$port:127.0.0.1:$port"); done
"${ssh[@]}"

Upvotes: 2

Related Questions