use argument containing spaces in bash - bash

I know about escaping, quoting and stuff, but still have a problem.
I you have a script containing "cd $1", and call it with an argument containing spaces, cd will always return an error message - it stops at the first space and can't find the directory. I tried protecting the arguments in every way :
ls -l
+-rwx... script
+drwx... dir with spaces/
cat script
+cd $1
script dir with spaces
+cd: dir: no such file or directory
script "dir with spaces"
+cd: dir: no such file or directory
script dir\ with\ spaces
+cd: dir\: no such file or directory
but none will work.
I feel like I'm missing the obvious, thanks for enlightening me.

You need to quote the expansion of "$1" to prevent it from being word split as well as quoting the string passed to the script to prevent it from being word-split.
So
$ cat script.sh
cd -- "$1"
$ ./script.sh "dir with spaces"
Edit: As gniourf_gniourf correctly pointed out using -- as the first argument to cd prevents problems should paths ever start with -.

Use double quotes on the variable
cd "$1"

Related

Have bash interpret double quotes stored in variable containing file path

I would like to capture a directory that contains spaces in a bash variable and pass this to the ls command without surrounding in double quotes the variable deference. Following are two examples that illustrate the problem. Example 1 works but it involves typing double quotes. Example 2 does not work, but I wish it did because then I could avoid typing the double quotes.
Example 1, with quotes surrounding variable, as in the solution to How to add path with space in Bash variable, which does not solve the problem:
[user#machine]$ myfolder=/home/username/myfolder\ with\ spaces/
[user#machine]$ ls "$myfolder"
file1.txt file2.txt file3.txt
Example 2, with quotes part of variable, which also does not solve the problem. According to my understanding, in this example, the first quote character sent to the ls command before the error is thrown:
[user#machine]$ myfolder=\"/home/username/myfolder\ with\ spaces/\"
[user#machine]$ ls $myfolder
ls: cannot access '"/home/username/myfolder': No such file or directory
In example 2, the error message indicates that the first double quote was sent to the ls command, but I want these quotes to be interpreted by bash, not ls. Is there a way I can change the myfolder variable so that the second line behaves exactly as the following:
[user#machine]$ ls "/home/username/myfolder with spaces/"
The goal is to craft the myfolder variable in such a way that (1) it does not need to be surrounded by any characters and (2) the ls command will list the contents of the existing directory that it represents.
The motivation is to have an efficient shorthand to pass long directory paths containing spaces to executables on the command line with as few characters as possible - so without double quotes if that is possible.
Assuming some 'extra' characters prior to the ls command is acceptable:
$ mkdir /tmp/'myfolder with spaces'
$ touch /tmp/'myfolder with spaces'/myfile.txt
$ myfolder='/tmp/myfolder with spaces'
$ myfolder=${myfolder// /?} # replace spaces with literal '?'
$ typeset -p myfolder
declare -- myfolder="/tmp/myfolder?with?spaces"
$ set -xv
$ ls $myfolder
+ ls '/tmp/myfolder with spaces'
myfile.txt
Here's a fiddle
Granted, the ? is going to match on any single character but how likely is it that you'll have multiple directories/files with similar names where the only difference is a space vs a non-space?

What is the difference between tild and eval to find user home

What is the difference between these two commands to find the user home-
$(eval echo ~<username>)
echo ~/
are there any scenario when both return different results?
Tilde expansion is done by the shell before executing the command. So in both cases, the home directory becomes the argument to echo.
eval re-evaluates its arguments. So if the home directory contains any characters that have special meaning to the shell, these will be interpreted. For instance, if the user's home directory were /home/$foo,
echo ~username
would display the pathname with the literal $foo in it, but
eval echo ~username
would replace $foo with the value of the foo variable.
Next, putting $() around a command means that the output of the command is substituted into the command line, and then the command line is executed. So
$(echo ~username)
$(eval echo ~username)
will both try to execute the home directory as a command, which will get an error because directories aren't executable programs. But if you meant that this is being used as an argument, e.g.
cd $(echo ~username)
vs
cd ~username
There should be little difference. The only difference would be if the home directory pathname contains whitespace or wildcard characters, because these are processed after $() substitution. This problem can be avoided by quoting:
cd "$(echo ~username)"

bash equivalent to tcsh modifiers for extracting path components

In tcsh I can extract second path element from the end of path by following way
cd /some/long/directory/structure/path/
set x=`pwd`
echo ${x:h:h:t}
directory
How can I do the same in bash?
I mean , does bash also have this kind of modifiers?
The csh-style modifiers can be used with history expansion (unsurprisingly, because history expansion was borrowed from csh).
$ cd /some/long/directory/structure/path/
$ echo !!:1:h:h:t
echo directory
directory
!!:1 selects word 1 (counting from zero) of the previous command, so the argument to cd.
(echo directory appears on standard error because the shell defaults to displaying the result of history expansion before actually executing the resulting command.)
In a non-interactive bash script, history expansion commands as in #chepner's answer won't normally be available. However, you do have parameter expansions like:
$ cd /some/long//directory///structure/path/
$ set x=$(pwd)
$ echo $x
/some/long/directory/structure/path
$ y=${y%/*/*} # each /* is equivalent to one :h
$ y=${y##*/} # equivalent to :t
$ echo $y
directory
cd /some/long/path/somewhere
x=$PWD
basename "$(dirname "$x")"
> path
dirname gets the absolute path of the parent folder of the argument. basename gets the name of the argument.
Edit: remembered the much better way than I was doing before.

Terminal program: unable to find the directory/file though they are in the correct pathway

What else could be going wrong? Sorry I'm pretty new to programming so I'm not sure if this is the proper way to frame my question.
Here is the code from the terminal file:
echo "Patcher Coded by _Retro_"
PLACE=`dirname $0`
ROM=`ls ${PLACE}/Rom/*.nds | head -n 1`
PATCH=`ls ${PLACE}/Patch/*.* | head -n 1`
NAME=${ROM%.[^.]*}
$PLACE/xdelta3 -dfs $ROM $PATCH $NAME-patched.nds
Your script says this:
PLACE=`dirname $0`
First, the shell performs parameter expansion. That means (in this case) it expands $0. The variable $0 expands to the path used by the shell to execute your script, so that line expands to this:
PLACE=`dirname /Users/ShakeyBanks/Desktop/Perfect Heart CE./DS_Rom_Patcher/Rom_Patcher`
Note that there are no backslashes in the expansion! The backslashes were consumed by your interactive shell before starting the script.
Then the shell performs command substitution: it executes the command enclosed in `...`. The shell splits the command on spaces, so the command contains four words. The first word is the program to run, and the remaining three words are arguments to that command:
dirname
/Users/ShakeyBanks/Desktop/Perfect
Heart
CE./DS_Rom_Patcher/Rom_Patcher
The problem here is that the dirname program only wants one argument, but you're passing it three. It detects this and fails with an error:
usage: dirname path
To fix this, quote the $0 with double-quotes, like this:
PLACE=`dirname "$0"`
You also need to quote all subsequent uses of $PLACE, ${PLACE}, $ROM, $PATCH, and $NAME with double-quotes, because they will have the same problem.
OR, rename your directory to not contain spaces.

simple command execution inside while loop - bourne shell scripting

I have a file called inp.txt which lists 3 directory names
#!/bin/sh
while read dirname
do
echo $dirname
"ls -l" $dirname
done < inp.txt
When I run the above, I get this error:
line 5: ls -l: command not found
If I do just "ls" instead of "ls -l", it works fine. What am I missing here?
Get rid of the quotes.
while read dirname
do
echo "$dirname"
ls -l "$dirname"
done < inp.txt
When you have quotes you're saying, "treat this as a single word." The shell looks for an executable named ls -l rather than passing the argument -l to the command ls.
Nitpicker's note: If you want to use quotes properly, add them around "$dirname".
Otherwise, if you had a directory named "Music Files" with a space in the name, without quotes your script would treat that as two directory names and print something like:
ls: Music: No such file or directory
ls: Files: No such file or directory
The shell interprets space as argument separaters. By putting the quotes around something, you force shell to see it as one argument. When executing, the shell interprets the first argument as the command to execute. In this case you're telling it to execute the command named "ls -l" with no arguments, instead of "ls" with argument "-l".
You should remove the quotes around ls -l.

Resources