94

Is there a way to execute a command in a different directory without having to cd to it? I know that I could simply cd in and cd out, but I'm just interested in the possibilities of forgoing the extra steps :)

Naftuli Kay
  • 39,676
  • It's rather trivial to make a script that does this: echo "#!/bin/bash; cd $1; exec $2" > /usr/local/bin/execindirectory; chmod +x /usr/local/bin/execindirectory. Might need to put a little more effort if you want it to actually support option "tags" such as -d and stuff. – LawrenceC May 26 '11 at 02:26
  • 2
    As user-unknown states in an answer, all the examples you've given here and in comments are better addressed by other solutions, so it's unclear if there is actually a problem that needs solving here. Can you come up with a better example? – Caleb May 26 '11 at 09:43

11 Answers11

102

I don't know if this counts, but you can make a subshell:

$ (cd /var/log && cp -- *.log ~/Desktop)

The directory is only changed for that subshell, so you avoid the work of needing to cd - afterwards.

Michael Mrozek
  • 93,103
  • 40
  • 240
  • 233
31

Not to undermine the value of answers given by other people, but I believe what you want is this:

(cd /path/to && ./executable [ARGS])

Note the parens to invoke cd in a sub-shell.

alex
  • 7,223
18

Some programs have options with which you can tell them to chdir(2) themselves (e.g. GNU tar’s -C/--directory).

Outside of such programs though, something will have to chdir. You could write and use some sort of compiled “binary” program instead of having the shell do it, but it probably would not yield much benefit.

In a comment in another answer, you gave an example:

execindirectory -d /var/log "cp *.log ~/Desktop"

Since the *.log pattern is expanded by the shell itself (not cp), something will have to chdir to the directory before having a shell evaluate your command.

If you are just interesting in avoiding having to “cd back”, then you can use a subshell to isolate the effect of the cd from your working shell instance.

(cd /path/to/dir && some command)

You can package this up in a shell function. (I dropped the -d option from your example usage since there is little point to this command if the directory is actually optional.)

runindir() { (cd -- "$1" && shift && eval " $@"); }

runindir /var/log 'cp -- *.log ~/Desktop' # your example runindir /var/log cp -- *.log ~/Desktop # eval takes multiple args # which it concatenates # with spaces prior to # evaluation

runindir /var/log cp -- \*.log ~/Desktop   # it is not okay to expand tilde first
                                           # as the expansion would then be
                                           # interpreted as shell code and
                                           # would break if your $HOME contained
                                           # characters special to the shell.
Chris Johnsen
  • 20,101
11

The env program from GNU coreutils-8.28 (released 2017-09-01) and newer has --chdir:

env: add --chdir option

This is useful when chaining with other commands that run commands in a different context, while avoiding using the shell to cd, and thus having to consider shell quoting the chained command.

$ env --help | grep chdir
  -C, --chdir=DIR      change working directory to DIR

So on a modern system (RHEL 8+, Ubuntu LTS 20.04+) you can just:

env --chdir=/tmp pwd

This is especially useful if you need to use sudo and don't want to mess up quoting the arguments.

FILENAME="Some file maybe with spaces.txt"
FILEDIR="/some/directory maybe with spaces"
sudo env --chdir "$FILEDIR" \
  zip "/tmp/$FILENAME.zip" "$FILENAME"
Tometzky
  • 211
  • Nice find, though not portable. For example, busybox env doesn't support the chdir option, so it won't work on Alpine linux. – ʇsәɹoɈ Mar 31 '23 at 01:42
  • For machines that don't have GNU env (e.g. macOS), a portable one-liner that would act similarly is sh -c 'cd -- "$1" || exit 1; shift; "$@"' -- DIR CMD [...] (where DIR is the desired working-directory and CMD [...] is the command, with any optional arguments) – Josh Bode Mar 30 '24 at 07:51
6

Here is something that should let you cd back where you were (using Bash), since not forgetting to do so seems to be the purpose of the question:

# Save where you are and cd to other dir
pushd /path/to/dir/that/needs/to/be/current/dir

run-your-command

# Get back where you were at the beginning.
popd

(EDIT: slightly shorter version, thanks to @Random832)

Bruno
  • 1,133
4

Sadly, your example:

execindirectory -d /var/log "cp *.log ~/Desktop"

doesn't need a change to the dir, because

cp /var/log/*.log ~/Desktop

would do the same. Can't you get closer to your real problem? Because we might know a better solution for that too.

A complicated way to solve your problem, which is far away from the elegance of Michaels solution, is the usage of find, which has a switch '-execdir' to be performed in the dir, where a file is found. Badly adopted to your example:

find /var/log -maxdepth 1 -type f -name "*.log" -execdir echo cp {} ~/Desktop ";"

Maybe it is useful for your real problem. -okdir instead of -execdir will ask you to confirm every invocation.

-okdir and -execdir might need gnu-find to be installed, which is typically used on Linux.

user unknown
  • 10,482
2

How about ./your/path/command.sh?

Kevin
  • 40,767
2

If I have a bash script in /home/user/ and the bash script is myscript there are a lot of solutions.

/home/user/myscript

#!/bin/bash
echo "WELCOME BASH"

Run chmod +x /home/user/myscript

  1. create an alias and execute by alias name

    • Temporary alias — run this in the terminal

      alias myscript="/home/user/myscript"

    • Permanent alias in ~/.bashrc (/home/user/.bashrc) — add/edit this to your .bashrc file

      alias myscript="/home/user/myscript"

    Now run in terminal: myscript

  2. add the directory path to $PATH and execute by name

Run in terminal:

  1. /home/user/myscript
  2. ./home/user/myscript         This works only if your current directory is /.
  3. /home/user/./myscript
  4. . ./home/user/myscript         This works only if your current directory is /.
  5. source ./home/user/myscript      This works only if your current directory is /.
  6. source /home/user/./myscript
  7. source /home/user/myscript
  8. bash /home/user/myscript
  9. bash ./home/user/myscript      This works only if your current directory is /.
  10. bash /home/user/./myscript

References to explain differences among some of the above:

I run this commands in a terminal on a Debian 10.9 and they work.

GNU bash, Version 5.0.3(1)-release (x86_64-pc-linux-gnu)

Z0OM
  • 3,149
  • (1) script is a bad example name for a script.  I suggest that you use things like foo, myscript, or perhaps something derived from your name, or the OP’s.  (2) Your answers are fundamentally the same as bdictator’s answer, which missed the point of the question; hence, so do yours.  (3) Answer 8 simply *will not work.*  Several others depend on an unstated assumption; in other words, they will not work in general.  (4) Answers 5, 9, and 13 are just silly.  … (Cont’d) – G-Man Says 'Reinstate Monica' Jul 14 '22 at 17:26
  • (Cont’d) … (5) If you’re going to post a list of similar answers, you should explain how they differ (because they most certainly are not equivalent). … … … … … … … … … … … … … … … … … Please do not respond in comments; [edit] your answer to make it clearer and more complete. – G-Man Says 'Reinstate Monica' Jul 14 '22 at 17:26
  • The question on the title is | Execute a specific command in a given directory without cd'ing to it? – Z0OM Jul 14 '22 at 17:34
  • 2
    The answer differ from bdictator, there is alias, path, source and bash – Z0OM Jul 14 '22 at 17:54
  • @G-Man Says 'Reinstate Monica' I think you can edit the title and the question if i missed the point of the question? my english is not the best. – Z0OM Jul 14 '22 at 17:56
  • I wish Stack Exchange made it easier for users to indicate that English is not their first language.  The question title is ambiguous; it becomes somewhat clearer if you read the second sentence of the question body, and then Naftuli Kay’s comment on bdictator’s answer. … … … … … … … … … … … … … … … … … … … … … … Imagine if the ls command didn’t take arguments (or you were limited to running it without arguments), so the only way to list the files in /var/log would be cd /var/log && ls.  The question is “How can I run ls *in* /var/log without needing to cd there?”  … (Cont’d) – G-Man Says 'Reinstate Monica' Jul 14 '22 at 20:37
  • (Cont’d) …  Thank you for responding to my comments (and for doing so politely).  You’ve obviously put a lot of work into this, so I have changed my downvote to an upvote. (But, really, injecting /./ in the middle of a pathname *is* silly.) – G-Man Says 'Reinstate Monica' Jul 14 '22 at 20:40
0

I'm not expert enough to know how universally it would work, but the source command lets you load/execute something in another directory. I use this to activate a virtual environment, e.g. source ./venv/bin/activate.

oCn
  • 11
  • Thanks for contributing,  but this is largely the same as bdictator’s answer, but with the added complication of the source command, which doesn’t help. – G-Man Says 'Reinstate Monica' Jul 14 '22 at 16:15
  • I initially tried exactly that (bdictator's answer), but it didn't work. It did when I included the source command, though. Is there a reason why one or the other would work with specific commands? – oCn Jul 15 '22 at 17:34
  • Well, the point is that bdictator’s answer is a wrong answer for this question (see Naftuli Kay’s comment), and so is yours.  See my comments on Blockchain Office’s answer.  So you and bdictator are both answering different questions, and using their answer on your problem is like comparing oranges and pumpkins. – G-Man Says 'Reinstate Monica' Jul 15 '22 at 17:50
  • OK, I missed the OP's comment on bdictator's answer. My response was to the way the original question was stated (I got here because I had the same question, as I understood it), which as you say turns out to be a different question. – oCn Jul 17 '22 at 11:12
0
(cd dir && cmd)

as already provided is the approach you'd want to use when cmd is an external command. In many shells, the subshell doesn't involve an extra process, as cmd is often executed in the process that was started to run the subshell, so the difference with cd dir; cmd is that the fork is done earlier.

In those shells that don't, you can always change it to (cd dir && exec cmd) to do the optimisation by yourself.

You can't use that approach though if the command you want to perform in that directory need to make permanent changes to your shell environment (such as setting some variables, change the umask, open some files...).

In:

(cd /dir && cmd && result_files=(*.out))

The result_files array would only be set in that subshell and lost when that subshell terminates.

Using:

pushd /dir && {
  cmd && result_files=(*.out)
  ret=$?
  popd
  (exit "$ret")
}

(Or solutions using cd /dir and cd - instead of pushd/popd), besides being cumbersome are potentially unreliable as the $OLDPWD could end up referring to a different directory after cmd has returned.

If you do that in an interactive shell and press Ctrl+C while cmd is running, your old directory will also not be restored.

The correct way to temporarily change to a different directory is to open() . on some file descriptor (with O_PATH where supported so you don't even need to have read access to the directory), chdir() to the new dir and then fchdir() to the saved fd. That will bring you back to where you started even if your old current directory was renamed or removed.

However, I don't know of any shell that has an interface to fchdir() let alone open(O_PATH). On Linux however you can approximate fchdir(x) with chdir("/dev/fd/x"), so in the zsh shell, and assuming you have read access to the current working directory, you could do something like:

{ 
  cd /some/dir && cmd && result=( *.out(N) )
} always {
  cd -P /dev/fd/3
} 3<.

The always block is always executed regardless of errors, break return or continue in the try block.

You could also set the close-on-exec flag to that file descriptor so it's no leaked to commands in the try block with:

zmodload zsh/system

sysopen -u3 -o cloexec . && { cd /some/dir && cmd && result=( *.out(N) ) } always { cd -P /dev/fd/3 exec 3<&- }

0

Here's a zsh shell alias and function to run a command in a directory, without using a subshell (in case the command has side effects on the shell environment that you'd like to keep):

alias direval='noglob _direval'
function _direval() {
  pushd -- $1 && {
    shift && eval " $@" 
  } always {
    popd
  }
}

Usage examples:

direval my-git-repo/scripts git ls-files
direval /usr/bin ls g*
direval /usr/bin GEXECS=$(echo g*)

The noglob in the alias means you don't have to worry about escaping glob characters; they will be evaluated later in the directory you chose. Unfortunately I couldn't figure out a way to make direval /usr/bin GEXECS=g* work, but using direval /usr/bin GEXECS=$(echo g*) is an OK workaround.

Credit for some of the pieces here that came from other answers: thanks Stéphane Chazelas for the && { ... } always { popd } idea, to Bruno for using pushd / popd, and to Chris Johnson for the idea of making a shell function to do this.

jbyler
  • 121