bash script to change directory and execute command with arguments - bash

I am trying to do the following task:
write a shell script called changedir which
takes a directory name, a command name and (optionally) some additional arguments.
The script will then change into the directory indicated, and
executes the command indicated with the arguments provided.
Here an example:
$ sh changedir /etc ls -al
This should change into the /etc directory and run the command ls -al.
So far I have:
#!/bin/sh
directory=$1; shift
command=$1; shift
args=$1; shift
cd $directory
$command
If I run the above like sh changedir /etc ls it changes and lists the directory. But if I add arguments to the ls it does not work. What do I need to do to correct it?

You seemed to be ignoring the remainder of the arguments to your command.
If I understand correctly you need to do something like this:
#!/bin/sh
cd "$1" # change to directory specified by arg 1
shift # drop arg 1
cmd="$1" # grab command from next argument
shift # drop next argument
"$cmd" "$#" # expand remaining arguments, retaining original word separations
A simpler and safer variant would be:
#!/bin/sh
cd "$1" && shift && "$#"

Since there can probably be more than a single argument to a command, i would recommend using quotation marks. Something like this:
sh changedir.sh /etc "ls -lsah"
Your code would be much more readable if you ommited the 'shift':
directory=$1;
command=$2;
cd $directory
$command
or simply
cd DIRECTORY_HERE; COMMAND_WITH_ARGS_HERE

Related

Questions about bash

Firstly, I'm wondering how to input information from the terminal into a variable in the script file. For example, lets say I wanted to do ./name.sh dave in the terminal instead of using read -p to ask for the name in the script. Secondly, I'm wondering how to go about creating a new directory and then copying files into that directory. I know how to use the mkdir command, but not how to copy files to that new directory.
Sorry if my wording is a bit bad I wasn't sure how else to ask the questions (this is my first day messing with bash.)
When you run:
./name.sh dave
the string dave will be the first positional argument in the script. You can access it with $1. To create a directory named dave and copy files into it, you might do:
#!/bin/bash
dir=${1:?}
mkdir "$dir" || exit
cp * "$dir"
A few things are a bit cryptic, and perhaps you might prefer:
#!/bin/sh
if test -z "$1"; then
echo "Parameter missing" >&2;
exit 1
fi
mkdir "$1" && cp * "$1"
Basically, you access the parameters via $1, $2, etc. The ${1:?} syntax is a shortcut that assigns the variable dir, but aborts the script if $1 is unset or empty. (eg, if you call the script without an argument.)
The rest seems pretty self-explanatory.
Suppose you wanted to specify the files to copy, so that ./name.sh dave would create a directory named dave and copy all files in the current directory to it (as above), but if you pass more arguments it would copy only those files. In that case, you might do something like:
#!/bin/bash
dir=${1:?}
shift # Discard the first argument, shift remaining down
mkdir "$dir" || exit
case $# in
0) cp * "$dir";;
*) cp "$#" "$dir";;
esac
Here, "$#" is the list of each argument, individually quoted. (eg, if you call the script with an argument that has spaces, it will properly pass that argument to cp. Compare that with cp $# $dir or cp "$*" $dir.) If you're just starting with shell scripts, I would advise you always be careful about quotes.

Is it possible to CD into a file?

I find a list of files that I need to cd to (obviously to the parent directory).
If I do cd ./src/components/10-atoms/fieldset/package.json I get the error cd: not a directory:, which makes sense.
But isn't there a way to allow for that? Because manipulating the path-string is pretty cumbersome and to me that would make total sense to have an option for that, since cd is a directory function and it would be cool that if the path would not end up in a file, it would recursively jump higher and find the "first dir" from the given path.
So cd ./src/components/10-atoms/fieldset/package.json would put me into ./src/components/10-atoms/fieldset/ without going on my nerves, telling me that I have chosen a file rather than a dir.
You could write a shell function to do it.
cd() {
local args=() arg
for arg in "$#"; do
if [[ $arg != -* && -e $arg && ! -d $arg ]]; then
args+=("$(dirname "$arg")")
else
args+=("$arg")
fi
done
builtin cd ${args[0]+"${args[#]}"}
}
Put it in your ~/.bashrc if you want it to be the default behavior. It won't be inherited by shell scripts or other programs so they won't be affected.
It modifies cd's arguments, replacing any file names with the parent directory. Options with a leading dash are left alone. command cd calls the underlying cd builtin so we don't get trapped in a recursive loop.
(What is this unholy beast: ${args[0]+"${args[#]}"}? It's like "${args[#]}", which expands the array of arguments, but it avoids triggering a bash bug with empty arrays on the off chance that your bash version is 4.0-4.3 and you have set -u enabled.)
This function should do what you need:
cdd() { test -d "$1" && cd "$1" || cd $(dirname "$1") ; }
If its first argument "$1" is a directory, just cd into it,
otherwise cd into the directory containing it.
This function should be improved to take into account special files such as devices or symbolic links.
You can if you enter a bit longer line (or create dedicated shell script)
cd $(dirname ./src/components/10-atoms/fieldset/package.json)
If you add it in script it can be :
cd $(dirname $1)
but you need to execute it on this way:
. script_name ./src/components/10-atoms/fieldset/package.json
You can put this function in your ~/.bashrc:
function ccd() {
TP=$1 # destination you're trying to reach
while [ ! -d $TP ]; do # if $TP is not a directory:
TP=$(dirname $TP) # remove the last part from the path
done # you finally got a directory
cd $TP # and jump into it
}
Usage: ccd /etc/postfix/strangedir/anotherdir/file.txt will get you to /etc/postfix.

Execute command with backquote in bash shell script

I write up a little shell script in bash that allows me to execute commands in sub-directories. Here is the script
bat.sh:
#!/bin/sh
for d in */; do
echo "Executing \"$#\" in $d"
cd $d
`$#`
cd ..
done
With my following directory structures
/home/user
--a/
----x.txt
----y.txt
--b/
----u.txt
----v.txt
I expect the following command to list out the content of directories a and b when it is executed in the home directory
bat.sh ls
The result is
Executing "ls" in a/
/home/user/bin/bat.sh: line 6: x.txt: command not found
Executing "ls" in b/
/home/user/bin/bat.sh: line 6: u.txt: command not found
Any idea on what is going wrong here?
You don't want the back quotes; you want double quotes.
#!/bin/sh
for d in */
do
echo "Executing \"$*\" in $d"
(cd "$d" && "$#")
done
You are trying to execute the output of the command you pass, whereas you simply want to execute the command.
The use of an explicit subshell (the ( … ) notation) may avoid some problems with symlinks that jump to other directories. It is, in my (perhaps archaic) view, a safer way to switch directories for the purposes of executing commands.

In shell script, how to change current directory safely with variable?

The following shell script changes current the directory to the desktop.
v=~/Desktop/
cd $v
pwd # desktop
The following script changes the current directory to home directory instead of generating error.
cd $undefined_variable
pwd # home directory
echo $? # 0
I'm afraid that the script will remove important files if I misspelled a variable for new current directory.
Generally, how do you safely change current directory with variable in shell script?
Use:
cd ${variable:?}
if $variable is not defined or empty then bash will throw an error and exit. It's like the set -u option but not global through the file.
You can set -u to make bash exit with an error each time you expand an undefined variable.
You could use the test -d condition (checks whether the specified variable is a directory), i.e.
if [[ -d $undefined_variable ]]
then
cd $undefined_variable
echo "This will not be printed if $undefined_variable is not defined"
fi
See also here for further test options...
The Bourne Shells have a construct to substitute a value for undefined variables, ${varname-subtitution}. You can use this to have a safe fallback directory in case the variable is undefined:
cd "${undefined-/tmp/backupdir}"
If there is a variable named undefined, its value is substituted, otherwise /tmp/backupdir is substituted.
Note that I also put the variable expansion in double quotes. This is used to prevent word splitting on strings containing spaces (very common for Windows directories). This way it works even for directories with spaces.
For the gory details on all the shell substitution constructs (there are seven more for POSIX shells), read your shell manual's Parameter Substitution section.
You have to write a wrapper (this work in bash):
cd() {
if [ $# -ne 1 ] ;then
echo "cd need exactly 1 argument" >&2
return 2
fi
builtin cd "$1"
}
yes, that's shell
if you type cd without parameter it will jump to home dir.
You can can check the variable of null or empty before you cd command.
check like (cd only be called if targetDir is not empty):
test -z "$targetDir" || cd $targetDir
check like (cd only be called if targetDir really exist):
test -d "$targetDir" && cd $targetDir
Note: Thanks for -1, should read the last sentence too. So I added the real answer.

bash use second argument of previous command

how I can use the second argument of previous command in a new command ?
example, with
$ mkdir test
I make a directory, how I can use the name of directory for change to this ?
$ mkdir test && cd use_var
$_ is the last (right-most) argument of the previous command.
mkdir gash && cd "$_"
(I don't create files or directories called test, that's the name of a shell built-in and can cause confusions)
With history expansion, you can refer to arbitrary words in the current command line
mkdir dir1 && cd "!#:1"
# 0 1 2 3 4
!# refers to the line typed so far, and :1 refers to word number one (with mkdir starting at 0).
If you use this in a script (i.e., a non-interactive shell), you need to turn history expansion on with set -H and set -o history.
Pressing Esc + . places the last argument of previous command on the current place of cursor. Tested in bash shell and ksh shell.
I use functions for this. Type this in your shell:
mkcd() { mkdir "$1" ; cd "$1" ; }
Now you have a new command mkcd.
If you need this repeatedly, put the line into the file ~/.bash_aliases (if you use bash; other shells use different names).

Resources