1

I need to use double quotes in an already double quoted string. I tried using $(...) too, and checked multiple stack posts that were somewhat related [1], [2], but none addressed the issue. These are the commands I am trying to execute -

git submodule foreach 'ssh "${instance_ipaddr}" "[ -d ${REMOTE_GIT_REPO_DIR}/${path}/.git ] || git init ${REMOTE_GIT_REPO_DIR}/${path}"'

git submodule foreach 'submodule_stash_commit=$(git rev-parse HEAD); git push -uf "ssh://${instance_ipaddr}/${REMOTE_GIT_REPO_DIR}/${path}" "${submodule_stash_commit}:refs/heads/remote-push"'

git submodule foreach 'submodule_stash_commit=$(git rev-parse HEAD); ssh "${instance_ipaddr}" "cd ${REMOTE_GIT_REPO_DIR}/${path} && git checkout ${submodule_stash_commit}"'

In these commands I want to replace the single quotes after the git submodule foreach command.

Charles Duffy
  • 280,126
  • 43
  • 390
  • 441
Aditya
  • 5,509
  • 4
  • 31
  • 51
  • 2
    Please don't mark it a duplicate unless you are very sure the answer addressed the issue. I have already linked possible answers that could be confused as duplicates. – Aditya Sep 25 '19 at 18:06
  • You should be able to use `\"` inside double-quotes? Or you can use `\ ` (backslash and space) to inject spaces without using quotes, but there are other special characters. – Gem Taylor Sep 25 '19 at 18:07
  • 2
    Why do you want to replace the single quotes? – that other guy Sep 25 '19 at 18:10
  • Also, you can put double-quotes inside single quotes. – Gem Taylor Sep 25 '19 at 18:11
  • @thatotherguy I need to replace the single quotes so that the variables are substituted. The command works for me on Mac bash, but does not run in the shell script. See https://stackoverflow.com/questions/6697753/difference-between-single-and-double-quotes-in-bash – Aditya Sep 25 '19 at 18:56
  • @GemTaylor I don't think escaping is applicable here. I want the variable to be substituted. Change single quote to double quote and then escaping the existing double quotes gives `path: unbound variable` error – Aditya Sep 25 '19 at 19:00
  • This is not the case. You can mix and match single and double quotes any way you want. See [this answer](https://stackoverflow.com/questions/13799789/expansion-of-variable-inside-single-quotes-in-a-command-in-bash) for details. – that other guy Sep 25 '19 at 19:00
  • @thatotherguy thanks for the link to the answer. I tried adding the quotes as suggested in the post, to get the following command: `git submodule foreach 'ssh '"${instance_ipaddr}"' '"[ -d ${REMOTE_GIT_REPO_DIR}/${path}/.git ] || git init ${REMOTE_GIT_REPO_DIR}/${path}"'' `. But that gives `path: unbound variable` error – Aditya Sep 25 '19 at 19:58
  • Note that I also tried to replace just the variables with double quotes around it - `git submodule foreach 'ssh '"${instance_ipaddr}"' [ -d '"${REMOTE_GIT_REPO_DIR}"'/'"${path}"'/.git ] || git init '"${REMOTE_GIT_REPO_DIR}"'/'"${path}" `. But that gives the same error – Aditya Sep 25 '19 at 20:15
  • A very good way to deal with this is to encapsulate the code you want to run into an exported function; that way you don't need to deal with escaping *at all*, but can let the shell do it for you. – Charles Duffy Sep 25 '19 at 20:28
  • 1
    @Aditya `path: unbound variable` means that the variable is expanding, but that it's not set. According to the `git` docs, this variable will be provided by `git` and not by you. Please carefully go through the string and determine which variables you provide, then only expand those. – that other guy Sep 25 '19 at 20:29
  • BTW, it's notable that the question's current/initial revision does not at all describe a specific error you encountered, as [mre] guidelines request. – Charles Duffy Sep 25 '19 at 20:33
  • The issue seems to be that $path variable is not correctly expanded in the above command. Interaction with both ssh and git submodule foreach is causing issues. Note that `git submodule foreach env` shows that $path is available – Aditya Sep 25 '19 at 22:09

1 Answers1

1

Instead of trying to figure out how to nest quotes yourself, let the shell do it for you. Consider:

# put the code we want to run remotely in a function
cmd1_remote_part() { [ -d "$1/.git" ] || git init "$1"; }

cmd1() {
  # create a single string with the remote argument(s) we want to pass w/ eval-safe quoting
  printf -v args_q '%q ' "${REMOTE_GIT_REPO_DIR}/${path}"
  # pass to the remote shell (1) the function definition; (2) a function invocation;
  # ...(3) the above argument list.
  ssh "${instance_ipaddr}" "$(declare -f cmd1_remote_part); cmd1_remote_part $args_q"
}

cmd2() {
  submodule_stash_commit=$(git rev-parse HEAD)
  git push -uf "ssh://${instance_ipaddr}/${REMOTE_GIT_REPO_DIR}/${path}" \
               "${submodule_stash_commit}:refs/heads/remote-push"
}

cmd3() {
  submodule_stash_commit=$(git rev-parse HEAD)
  # substituting paths into remote ssh commands introduces security risks absent eval-safe
  # ...quoting, as done with printf %q.
  printf -v remote_cmd_q 'cd %q && git checkout %q' \
    "${REMOTE_GIT_REPO_DIR}/${path}" "${submodule_stash_commit}"
  ssh "${instance_ipaddr}" "$remote_cmd_q"
}

git submodule foreach "$(declare -f cmd1 cmd1_remote_part); cmd1"
git submodule foreach "$(declare -f cmd2); cmd2"
git submodule foreach "$(declare -f cmd3); cmd3"

Note that the bodies of all of these functions are written exactly the way you'd write them for a local shell; declare -f then emits a function declaration that can be expanded remotely.

Charles Duffy
  • 280,126
  • 43
  • 390
  • 441
  • Thanks for the detailed answer. I was trying out cmd1, and I get `bash: -c: line 0: \`; cmd1_remote_part ~/path_added_correctly `. cmd1_remote_part does not seem to be expanded. Any suggestions to debug this? – Aditya Sep 25 '19 at 22:05
  • A good place to start is liberal use of `set -x` -- both in the outer script, and also in the inner commands, as in `set -x; git submodule foreach "set -x; $(declare -f cmd1); cmd1"`. – Charles Duffy Sep 25 '19 at 22:27
  • BTW, we don't expect `cmd1_remote_part` to be expanded (in the sense of replaced with different text); what we *do* expect is for it to be preceded by a function declaration that gives it meaning when it's run on the remote side. – Charles Duffy Sep 25 '19 at 22:32
  • (Likewise, for the SSH parts, `ssh "$instance_ipaddr" "set -x; $(declare -f cmd1_remote_part); cmd1_remote_part $args_q"` should ensure that the remote system logs the commands it's running to stderr as it invokes them). – Charles Duffy Sep 25 '19 at 22:34
  • Oooh -- just realized that you might need to change that to `git submodule foreach "$(declare -f cmd1 cmd1_remote_part); cmd1"` to ensure that both local and remote parts are defined in the child shell. Editing the answer to fit. BTW, for any variables needed by those functions that git doesn't provide, you might also add a `$(declare -p varname1 varname2 ...);` as well, so as to not need to `export` them. – Charles Duffy Sep 25 '19 at 22:36
  • I did not know we could do set -x in the inner commands as well! The $(declare -f cmd1_remote_part) is missing from cmd1. `ssh 1.1.1.1 'set -x; ; cmd1_remote_part ~/valid_path '`. – Aditya Sep 25 '19 at 22:38
  • Does changing the top-level `declare -f` command (that is, the one in the `git submodule foreach` command) to list `cmd1_remote_part` as an argument fix that? – Charles Duffy Sep 25 '19 at 22:39
  • 1
    (As this answer was first written, it didn't pass `cmd1_remote_part` from the top-level shell to the one started by `git`, so it wasn't available in the shell started by git to be passed to ssh; incidentally, if the shell started by git and the top-level one are both the same version of bash, an alternate way to fix it is by exporting the functions: `export -f cmd1 cmd1_remote_part cmd2 cmd3` will make all those functions available in bash instances started as local children to the current process, though you'll still need to use `declare -f` to pass them over ssh, or to non-bash shells). – Charles Duffy Sep 25 '19 at 22:45
  • Yes, your edit fixed that. And thanks for taking the time to actually explain it in such detail! – Aditya Sep 25 '19 at 22:48
  • I see that `${instance_ipaddr}` and `${REMOTE_GIT_REPO_DIR}` variable were not available in the ssh command. I currently solved the issue by exporting this variables before these commands. Is there a cleaner solution than that? – Aditya Sep 26 '19 at 22:33
  • 1
    Where do they come from in the first place? If they start out as regular shell variables in the parent shell, then you need to *either* export them or generate code that defines them (just as `declare -f funcname` emits a declaration for a function, `declare -p varname` emits a declaration for a regular variable, including those -- like arrays -- that can't be exported without losing data). – Charles Duffy Sep 26 '19 at 23:28