Working with octal permissions in bash - bash

How do I get the masked (user) permissions from that permissions octal value?
Output should look like:
testfile1:
Permissions: 644
Masked Permissions: 600
testfile2:
Permissions: 750
Masked Permissions: 700
All I have now is:
for file in $#
do
value=$(stat --printf '%a' $file)
echo "permissions: $value"
maskedvalue= # ???
echo "permissions, masked: $maskedvalue"
done

Given that it is bash, you can do:
printf "%.4o\n" $(( $(stat -f '0%Lp' "$file") & ~$(umask) ))
at least, on Mac OS X, where stat -f '%Lp' means 'print the user, group, other permissions in octal'.
If you're on Linux, you'll need to translate that to the GNU stat syntax, which looks like it probably is:
printf "%.4o\n" $(( $(stat --printf '%a' "$file") & ~$(umask) ))
The spaces after $(( and before )) are not strictly needed in the sense that the shell knows what to do even if they are missing; likewise the spaces around the &. OTOH, if anyone else has to read the code, you probably want those spaces there.
Test code:
file="./mode-file"
trap "rm -f $file; exit 1" 0 1 2 3 13 15
cp /dev/null "$file"
for mode in 777 755 644 640 444 440 400
do
chmod $mode $file
ls -l $file
for umask in 002 022 027 033 037 077 177
do
umask $umask
printf "$mode & ~$umask = %.4o\n" $(( $(stat -f '0%Lp' "$file") & ~$(umask) ))
done
done
rm -f $file
trap 0
Example output:
-rwxrwxrwx 1 jleffler staff 0 Nov 11 20:57 ./mode-file
777 & ~002 = 0775
777 & ~022 = 0755
777 & ~027 = 0750
777 & ~033 = 0744
777 & ~037 = 0740
777 & ~077 = 0700
777 & ~177 = 0600
-rwxr-xr-x 1 jleffler staff 0 Nov 11 20:57 ./mode-file
755 & ~002 = 0755
755 & ~022 = 0755
755 & ~027 = 0750
755 & ~033 = 0744
755 & ~037 = 0740
755 & ~077 = 0700
755 & ~177 = 0600
-rw-r--r-- 1 jleffler staff 0 Nov 11 20:57 ./mode-file
644 & ~002 = 0644
644 & ~022 = 0644
644 & ~027 = 0640
644 & ~033 = 0644
644 & ~037 = 0640
644 & ~077 = 0600
644 & ~177 = 0600
-rw-r----- 1 jleffler staff 0 Nov 11 20:57 ./mode-file
640 & ~002 = 0640
640 & ~022 = 0640
640 & ~027 = 0640
640 & ~033 = 0640
640 & ~037 = 0640
640 & ~077 = 0600
640 & ~177 = 0600
-r--r--r-- 1 jleffler staff 0 Nov 11 20:57 ./mode-file
444 & ~002 = 0444
444 & ~022 = 0444
444 & ~027 = 0440
444 & ~033 = 0444
444 & ~037 = 0440
444 & ~077 = 0400
444 & ~177 = 0400
-r--r----- 1 jleffler staff 0 Nov 11 20:57 ./mode-file
440 & ~002 = 0440
440 & ~022 = 0440
440 & ~027 = 0440
440 & ~033 = 0440
440 & ~037 = 0440
440 & ~077 = 0400
440 & ~177 = 0400
-r-------- 1 jleffler staff 0 Nov 11 20:57 ./mode-file
400 & ~002 = 0400
400 & ~022 = 0400
400 & ~027 = 0400
400 & ~033 = 0400
400 & ~037 = 0400
400 & ~077 = 0400
400 & ~177 = 0400
NB: Tested on Mac OS X 10.9 (Mavericks). If you are using a GNU/Linux platform, you must change the stat command to use the notation that works on your machine.

Related

SHELL: execution using variable

I write below statements in my .bashrc
alias ls='ls -l'
$LS = 'ls'
I expected $LS execute ls -l in command line prompt because ls already modified to ls -l. but $LS execute ls only ls.
ls execute ls -l because alias as below:
> ls
total 28
-rw-r--r-- 1 chlee chlee 82 8월 30 22:07 '#test.py#'
drwxr-xr-x 2 chlee chlee 4096 8월 21 04:09 exer
-rw-r--r-- 1 chlee chlee 64 8월 30 21:49 test.py
-rw-r--r-- 1 chlee chlee 49 8월 30 21:47 test.py~
drwxr-xr-x 29 chlee chlee 12288 8월 21 02:51 vsdbg
$LS execute only ls as below:
> $LS
'#test.py#' exer test.py test.py~ vsdbg
How can I get the shell to take into account aliases when variable evaluation?
This should work as you expect:
#!/bin/bash
alias ls="ls -l"
LS () {
ls
}
echo trying lower
ls
echo Trying upper
LS
The function is called LS including parses aliases.
# source LS.sh
trying lower
total 4
-rwxr-xr-x 1 root root 89 Sep 19 21:50 LS.sh
Trying upper
total 4
-rwxr-xr-x 1 root root 89 Sep 19 21:50 LS.sh
Jetchisel was pointing you to this in the comments.

Delete A Specific File From Directories Based On FIND Results

I have the following find command that searches all subdirectories and lists those folders that contain a *.RAR AND a *.MKV file.
find -type d -exec sh -c '[ -f "$0"/*.rar ] && [ -f "$0"/*.mkv ]' '{}' \; -print | sort
What I want to do now is to delete the *.MKV file from those directories.
For example, the above command finds FILEA.RAR and FILEB.MKV and lists the directory as DIRECTORY_CHARLIE. I would like to be able to have the above code, also delete the FILEB.MKV file, well delete the found MKV file from each directory that had both file types.
To start, I've created "_TestDir" with the following subfolders:
#useroneserver ~/files/_TestDir $ ls -all
drwxr-xr-x 2 userone userone 50 Feb 27 19:31 Folder01
drwxr-xr-x 2 userone userone 50 Feb 27 19:32 Folder02
drwxr-xr-x 2 userone userone 50 Feb 27 19:32 Folder03
drwxr-xr-x 2 userone userone 50 Feb 27 19:33 Folder04
drwxr-xr-x 2 userone userone 50 Feb 27 19:34 Folder05
Each folder has 2 files, except Folder03, which only has one file.
userone#remoteserver ~/files/_TestDir $ ls Folder01 -all
drwxr-xr-x 2 userone userone 50 Mar 1 20:03 .
drwxr-xr-x 7 userone userone 105 Feb 27 19:30 ..
-rw-r--r-- 1 userone userone 0 Feb 27 19:31 File1.rar
-rw-r--r-- 1 userone userone 0 Feb 27 19:30 FileA.mkv
userone#remoteserver ~/files/_TestDir $ ls Folder02 -all
drwxr-xr-x 2 userone userone 50 Mar 1 20:04 .
drwxr-xr-x 7 userone userone 105 Feb 27 19:30 ..
-rw-r--r-- 1 userone userone 0 Feb 27 19:32 File2.rar
-rw-r--r-- 1 userone userone 0 Feb 27 19:31 FileB.mkv
userone#remoteserver ~/files/_TestDir $ ls Folder03 -all
drwxr-xr-x 2 userone userone 30 Mar 1 20:04 .
drwxr-xr-x 7 userone userone 105 Feb 27 19:30 ..
-rw-r--r-- 1 userone userone 0 Feb 27 19:32 FileC.mkv
userone#remoteserver ~/files/_TestDir $ ls Folder04 -all
drwxr-xr-x 2 userone userone 50 Mar 1 20:04 .
drwxr-xr-x 7 userone userone 105 Feb 27 19:30 ..
-rw-r--r-- 1 userone userone 0 Feb 27 19:33 File4.rar
-rw-r--r-- 1 userone userone 0 Feb 27 19:33 FileD.mkv
userone#remoteserver ~/files/_TestDir $ ls Folder05 -all
drwxr-xr-x 2 userone userone 50 Mar 1 20:05 .
drwxr-xr-x 7 userone userone 105 Feb 27 19:30 ..
-rw-r--r-- 1 userone userone 0 Feb 27 19:34 File5.rar
-rw-r--r-- 1 userone userone 0 Feb 27 19:33 FileE.mkv
When I run the command in the original post, I get this:
userone#remoteserver ~/files/_TestDir $ find -type d -exec sh -c '[ -f "$0"/*.rar ] && [ -f "$0"/*.mkv ]' '{}' \; -print | sort
./Folder01
./Folder02
./Folder04
./Folder05
That is what I expect to see as the results, since folders 1, 2, 4 & 5 have a file of each of the extensions that I am looking for (*.rar & *.mkv), where as folder 3 only has one *.mkv file.
I have not tried to add any delete function since I have no clue where to start.
What I would like to happen is to be able to remove/delete the following
files:
FileA.mkv from Folder01
FileB.mkv from Folder02
FileD.mkv from Folder04
FileE.mkv from Folder05
Nothing gets deleted from Folder03 since it does not have a .RAR AND a .MKV file, it only has the .MKV. Hope this helps clarify.
Thank you for your assistance.
Regards.
You want to use xargs to run rm which will delete the files.
And define the replacement string, using the -I option of xargs.
Using your directory structure, you can do:
find -type d -exec sh -c '[ -f "$0"/*.rar ] && [ -f "$0"/*.mkv ]' '{}' \; -print | sort | xargs -I % sh -c "rm -f %/File?.mkv"
I took your exact find command, and added this:
| xargs -I % sh -c "rm -f %/File?.mkv"
Explanation
when find runs, it will output
./Folder01
./Folder02
./Folder04
./Folder05
Since xargs is used with the -I % option, it will run the command, replacing % with each directory (like in a loop, one by one). You could use another character than %, but avoid wildcard characters.
The command that xargs will run is sh -c "rm -f %/File?.mkv"
It will therefore do the following commands, in succession:
sh -c "rm -f Folder01/File?.mkv"
sh -c "rm -f Folder02/File?.mkv"
sh -c "rm -f Folder04/File?.mkv"
sh -c "rm -f Folder05/File?.mkv"
Obviously, you can adjust as required.

Copy only files including files in subfolder

I have two root folders with the same structure like that :
Folder1
SubFolder1
File1
File2
SubFolder2
File3
File4
SubFolder3
File5
File6
File7
File8
How can I copy all files in 'Folder1' including files in subfolder of it to another destination name 'Folder2'.
'Folder2' have the same structure with 'Folder1' and all subfolder are already created in 'Folder2'.
A simple
cp -a /path/to/folder1/* /path/to/folder2
will do the trick. The command will check wether a sub folder of folder1 already exists in folder2 (create it if it doesn't), copy the files contained and recursively do that for any sub folders found.
See the cp man page for details (which you can also read locally on the shell by issuing man cp).
Make use of rsync tool
Datasetup: Creating Folder1 and Folder2 with respective subdirs
:~/User> ls -laRt Folder1/
Folder1/:
-rw-r--r-- 1 test test 0 2015-09-29 09:08 f7
-rw-r--r-- 1 test test 0 2015-09-29 09:08 f8
drwxr-xr-x 2 test test 4096 2015-09-29 09:08 sub3
drwxr-xr-x 2 test test 4096 2015-09-29 09:08 sub2
drwxr-xr-x 2 test test 4096 2015-09-29 09:08 sub1
Folder1/sub3:
-rw-r--r-- 1 test test 0 2015-09-29 09:08 f5
-rw-r--r-- 1 test test 0 2015-09-29 09:08 f6
Folder1/sub2:
-rw-r--r-- 1 test test 0 2015-09-29 09:08 f3
-rw-r--r-- 1 test test 0 2015-09-29 09:08 f4
Folder1/sub1:
-rw-r--r-- 1 test test 0 2015-09-29 09:08 f1
-rw-r--r-- 1 test test 0 2015-09-29 09:08 f2
:~/User> ls -laRt Folder2
drwxr-xr-x 2 test test 4096 2015-09-29 09:10 sub1
drwxr-xr-x 2 test test 4096 2015-09-29 09:10 sub2
drwxr-xr-x 2 test test 4096 2015-09-29 09:10 sub3
copy using rsync
:~/User> rsync -avh Folder1/ Folder2/
building file list ... done
./
f7
f8
sub1/
sub1/f1
sub1/f2
sub2/
sub2/f3
sub2/f4
sub3/
sub3/f5
sub3/f6
sent 537 bytes received 220 bytes 1.51K bytes/sec
total size is 0 speedup is 0.00
Verify
:~/User> ls -laRt Folder2
Folder2:
-rw-r--r-- 1 test test 0 2015-09-29 09:08 f7
-rw-r--r-- 1 test test 0 2015-09-29 09:08 f8
drwxr-xr-x 2 test test 4096 2015-09-29 09:08 sub3
drwxr-xr-x 2 test test 4096 2015-09-29 09:08 sub2
drwxr-xr-x 2 test test 4096 2015-09-29 09:08 sub1
Folder2/sub3:
-rw-r--r-- 1 test test 0 2015-09-29 09:08 f5
-rw-r--r-- 1 test test 0 2015-09-29 09:08 f6
Folder2/sub2:
-rw-r--r-- 1 test test 0 2015-09-29 09:08 f3
-rw-r--r-- 1 test test 0 2015-09-29 09:08 f4
Folder2/sub1:
-rw-r--r-- 1 test test 0 2015-09-29 09:08 f1
-rw-r--r-- 1 test test 0 2015-09-29 09:08 f2

BASH / Command Line number of bytes

In this directory when I run
ls -l
it prints the following output:
-rw------- 1 csundl dcsugrad 0 Dec 5 13:51 file3
drwx------ 2 csundl dcsugrad 4096 Dec 5 13:51 Photos
drwx------ 2 csundl dcsugrad 4096 Dec 5 13:51 Pron
drwx------ 2 csunfi dcsugrad 4096 Dec 5 13:51 Spreadsheets
drwx------ 3 csundl dcsugrad 4096 Dec 5 15:12 Stuff
-rwx------ 1 csundl dcsugrad 149 Dec 5 15:08 untitled.sh
The bolded values are the byte sizes (?).
However when I run:
ls -l | wc -c
The total byte size comes up as 340. Why is this?
Thanks.
ls command displays size of the file
but this command:
ls -l | wc -c
counts number of characters in the output of ls command.
To count total size of files in a directory use du command:
du -hs mydir

Bash script passing arguments to tcsh interpreter

This one will require some knowledge of both bash and tcsh. I would like to put a line in a bash script that calls a command with some arguments, but using the tcsh interpreter. Suppose the command I want to call is ls (which is silly, but it demonstrates the problem). So if my_script contains:
#!/bin/bash
/bin/tcsh -c "ls"' "$*"' "${#}"
and then I call it as follows:
my_script "first file" "second file"
I get this:
ls: first file second file: No such file or directory
The problem is that tcsh is receiving only one argument, first file second file, instead of two arguments. How can I remedy this?
Despite the comments, there's no direct problem with tcsh (and believe me, I'm no fan of C shell), nor is there a problem with bash per se. The problems would be similar, in fact, if you replaced the tcsh with bash.
The problem is that what you're trying to do is actually extremely difficult to do. Let me explain...
In the bash script, you are trying to create a single string that will contain a valid command line that tcsh will interpret correctly, including preserving spaces in arguments.
Developing an answer step-by-step
Let's start with some easy stuff — arguments without spaces in them:
set -- /bin/ls /bin/sh /bin/bash # Set the arguments to bash
/bin/tcsh -c "ls -l $*"
This will work fine; it will execute the C shell and the C shell will process the string and execute:
ls -l /bin/ls /bin/sh /bin/bash
So, the problem is how to relay arguments with spaces in them to C shell reliably when the command as a whole is being specified as a single string.
You already know that this runs into problems:
mkdir "./a b c" "./d e f"
set -- "a b c" "d e f" # Two arguments with spaces
/bin/tcsh -c "ls -al $*"
On my machine, I get:
ls: a: No such file or directory
ls: b: No such file or directory
ls: c: No such file or directory
ls: d: No such file or directory
ls: e: No such file or directory
ls: f: No such file or directory
If we do the expansion manually, we can get the desired result (for this limited example) with:
mkdir "./a b c" "./d e f"
set -- "a b c" "d e f" # Two arguments with spaces
/bin/tcsh -c "ls -al 'a b c' 'd e f'"
This yields:
a b c:
total 0
drwxr-xr-x 2 jleffler staff 68 Aug 25 12:21 .
drwxr-xr-x 4 jleffler staff 136 Aug 25 12:21 ..
d e f:
total 0
drwxr-xr-x 2 jleffler staff 68 Aug 25 12:21 .
drwxr-xr-x 4 jleffler staff 136 Aug 25 12:21 ..
(I'm going to assume that the two directories 'a b c' and 'd e f' exist from here onwards without creating them each time.)
So, the objective must be to find a way to create a string that will be safe when interpreted by the C shell, automatically (not manually as shown). Because of the metasyntactic zoo that C shell has (lots of special characters), the full task will be hard, but let's get the easy stuff done first — spaces and no metacharacters.
For each argument, we want to add single quotes to the start and end, and ensure that any single quotes inside the string are protected. That is its own little party; the trick is to replace the embedded single quotes with the sequence '\'' where the first single quote ends the current single-quoted string, the backslash single-quote embeds a single quote, and the final single quote starts a new single-quoted string. And we want that added to the end of the current command string. So, this leads to:
set -- "a b c" "d e f" # Two arguments with spaces
cmd="ls -al"
for arg in "$#"
do escaped=$(sed -e "s/'/'\\''/g" -e "s/^/'/" -e "s/$/'/" <<< "$arg")
cmd="$cmd $escaped"
done
echo "$cmd"
tcsh -c "$cmd"
This yields (the ls line is from the echo, of course):
ls -al 'a b c' 'd e f'
a b c:
total 0
drwxr-xr-x 2 jleffler staff 68 Aug 25 12:21 .
drwxr-xr-x 4 jleffler staff 136 Aug 25 12:21 ..
d e f:
total 0
drwxr-xr-x 2 jleffler staff 68 Aug 25 12:21 .
drwxr-xr-x 4 jleffler staff 136 Aug 25 12:21 ..
OK, so far, so good. What about the metasyntactic zoo? Fortunately, most of the characters have no special meaning inside single quotes.
Time to add some more complex directories to the list (these will survive for the duration of the question, too). Make sure you know what names are being created; you need to understand shell quoting rather well.
As an exercise, for each directory name created during this question, write alternatives that give the same result when enclosed in single quotes, when enclosed in double quotes, and without any quotes around the whole argument.
$ mkdir '! % *' '$(pwd)' '`pwd`'
And the script is mostly unchanged — it uses the shell glob to generate the list of directory names, echoes each argument in turn, and lists the inode numbers too:
set -- *
cmd="ls -ail"
for arg in "$#"
do echo "arg: $arg"
escaped=$(sed -e "s/'/'\\''/g" -e "s/^/'/" -e "s/$/'/" <<< "$arg")
cmd="$cmd $escaped"
done
echo "cmd: $cmd"
tcsh -c "$cmd"
Hey presto:
arg: ! % *
arg: $(pwd)
arg: `pwd`
arg: a b c
arg: d e f
cmd: ls -ail '! % *' '$(pwd)' '`pwd`' 'a b c' 'd e f'
! % *:
total 0
1640119 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:34 .
1640040 drwxr-xr-x 7 jleffler staff 238 Aug 25 12:34 ..
$(pwd):
total 0
1640120 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:34 .
1640040 drwxr-xr-x 7 jleffler staff 238 Aug 25 12:34 ..
`pwd`:
total 0
1640121 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:34 .
1640040 drwxr-xr-x 7 jleffler staff 238 Aug 25 12:34 ..
a b c:
total 0
1640056 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:21 .
1640040 drwxr-xr-x 7 jleffler staff 238 Aug 25 12:34 ..
d e f:
total 0
1640057 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:21 .
1640040 drwxr-xr-x 7 jleffler staff 238 Aug 25 12:34 ..
Just what the doctor ordered! But we haven't yet been brutal enough: like Knuth says, you have to get into a really nasty mean mindset when you're testing code, so let's try:
$ mkdir "O'Reilly's Books"
$ mkdir "' \` \""
$ mkdir '${HOME}' '$PATH' 'He said, "Don'\''t Do It!"'
$ ls -l
total 0
drwxr-xr-x 2 jleffler staff 68 Aug 25 12:34 ! % *
drwxr-xr-x 2 jleffler staff 68 Aug 25 12:34 $(pwd)
drwxr-xr-x 2 jleffler staff 68 Aug 25 12:45 $PATH
drwxr-xr-x 2 jleffler staff 68 Aug 25 12:45 ${HOME}
drwxr-xr-x 2 jleffler staff 68 Aug 25 12:43 ' ` "
drwxr-xr-x 2 jleffler staff 68 Aug 25 12:45 He said, "Don't Do It!"
drwxr-xr-x 2 jleffler staff 68 Aug 25 12:43 O'Reilly's Books
drwxr-xr-x 2 jleffler staff 68 Aug 25 12:34 `pwd`
drwxr-xr-x 2 jleffler staff 68 Aug 25 12:21 a b c
drwxr-xr-x 2 jleffler staff 68 Aug 25 12:21 d e f
$
And the result is:
arg: ! % *
arg: $(pwd)
arg: $PATH
arg: ${HOME}
arg: ' ` "
arg: He said, "Don't Do It!"
arg: O'Reilly's Books
arg: `pwd`
arg: a b c
arg: d e f
cmd: ls -ail '! % *' '$(pwd)' '$PATH' '${HOME}' '''' ` "' 'He said, "Don'''t Do It!"' 'O'''Reilly'''s Books' '`pwd`' 'a b c' 'd e f'
Unmatched `.
That's not what we wanted. Part of the trouble, though, is that sequence of 4 single quotes in the line tagged 'cmd:'; it should be ''\''. So, the sed script isn't accurate enough.
set -- *
cmd="ls -ail"
for arg in "$#"
do echo "arg: $arg"
escaped=$(sed -e "s/'/'\\\\''/g" -e "s/^/'/" -e "s/$/'/" <<< "$arg")
cmd="$cmd $escaped"
done
echo "cmd: $cmd"
tcsh -c "$cmd"
And when it is run, we get:
arg: ! % *
arg: $(pwd)
arg: $PATH
arg: ${HOME}
arg: ' ` "
arg: He said, "Don't Do It!"
arg: O'Reilly's Books
arg: `pwd`
arg: a b c
arg: d e f
arg: x.sh
cmd: ls -ail '! % *' '$(pwd)' '$PATH' '${HOME}' ''\'' ` "' 'He said, "Don'\''t Do It!"' 'O'\''Reilly'\''s Books' '`pwd`' 'a b c' 'd e f' 'x.sh'
1640231 -rw-r--r-- 1 jleffler staff 223 Aug 25 12:56 x.sh
! % *:
total 0
1640119 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:34 .
1640040 drwxr-xr-x 13 jleffler staff 442 Aug 25 12:56 ..
$(pwd):
total 0
1640120 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:34 .
1640040 drwxr-xr-x 13 jleffler staff 442 Aug 25 12:56 ..
$PATH:
total 0
1640176 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:45 .
1640040 drwxr-xr-x 13 jleffler staff 442 Aug 25 12:56 ..
${HOME}:
total 0
1640175 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:45 .
1640040 drwxr-xr-x 13 jleffler staff 442 Aug 25 12:56 ..
' ` ":
total 0
1640163 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:43 .
1640040 drwxr-xr-x 13 jleffler staff 442 Aug 25 12:56 ..
He said, "Don't Do It!":
total 0
1640177 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:45 .
1640040 drwxr-xr-x 13 jleffler staff 442 Aug 25 12:56 ..
O'Reilly's Books:
total 0
1640164 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:43 .
1640040 drwxr-xr-x 13 jleffler staff 442 Aug 25 12:56 ..
`pwd`:
total 0
1640121 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:34 .
1640040 drwxr-xr-x 13 jleffler staff 442 Aug 25 12:56 ..
a b c:
total 0
1640056 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:21 .
1640040 drwxr-xr-x 13 jleffler staff 442 Aug 25 12:56 ..
d e f:
total 0
1640057 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:21 .
1640040 drwxr-xr-x 13 jleffler staff 442 Aug 25 12:56 ..
Mean enough? Getting close. What about directory names containing backslashes?
$ mkdir "a \\' \\\` \\$ b \\\" c" # Make sure you do the exercise!
$ mkdir 'a \\'\'' \\\` \\$ b \\\" c' # Make sure you do the exercise!
$ ls -li
total 8
1640119 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:34 ! % *
1640120 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:34 $(pwd)
1640176 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:45 $PATH
1640175 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:45 ${HOME}
1640163 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:43 ' ` "
1640177 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:45 He said, "Don't Do It!"
1640164 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:43 O'Reilly's Books
1640121 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:34 `pwd`
1640243 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:58 a \' \` \$ b \" c
1640259 drwxr-xr-x 2 jleffler staff 68 Aug 25 13:01 a \\' \\\` \\$ b \\\" c
1640056 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:21 a b c
1640057 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:21 d e f
1640231 -rw-r--r-- 1 jleffler staff 223 Aug 25 12:56 x.sh
$
And with the ls -ail changed to ls -dil, the output is:
$ bash x.sh
arg: ! % *
arg: $(pwd)
arg: $PATH
arg: ${HOME}
arg: ' ` "
arg: He said, "Don't Do It!"
arg: O'Reilly's Books
arg: `pwd`
arg: a \' \` \$ b \" c
arg: a \\' \\\` \\$ b \\\" c
arg: a b c
arg: d e f
arg: x.sh
cmd: ls -dil '! % *' '$(pwd)' '$PATH' '${HOME}' ''\'' ` "' 'He said, "Don'\''t Do It!"' 'O'\''Reilly'\''s Books' '`pwd`' 'a \'\'' \` \$ b \" c' 'a \\'\'' \\\` \\$ b \\\" c' 'a b c' 'd e f' 'x.sh'
1640119 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:34 ! % *
1640120 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:34 $(pwd)
1640176 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:45 $PATH
1640175 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:45 ${HOME}
1640163 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:43 ' ` "
1640177 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:45 He said, "Don't Do It!"
1640164 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:43 O'Reilly's Books
1640121 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:34 `pwd`
1640243 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:58 a \' \` \$ b \" c
1640259 drwxr-xr-x 2 jleffler staff 68 Aug 25 13:01 a \\' \\\` \\$ b \\\" c
1640056 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:21 a b c
1640057 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:21 d e f
1640271 -rw-r--r-- 1 jleffler staff 223 Aug 25 13:03 x.sh
$
Working Script
set -- *
cmd="ls -ail"
for arg in "$#"
do echo "arg: $arg"
escaped=$(sed -e "s/'/'\\\\''/g" -e "s/^/'/" -e "s/$/'/" <<< "$arg")
cmd="$cmd $escaped"
done
echo "cmd: $cmd"
tcsh -c "$cmd"
Summary
The key parts to the solution are:
Recognizing that single quotes are needed around the arguments.
Knowing how to escape single quotes.
Knowing how to escape backslashes.
Being really brutal when you do your testing!
It helps if you've done it before...
Oh futz! I forgot to test arguments containing newlines:
$ mkdir "a
> b
> c"
$ ls -li
total 8
1640119 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:34 ! % *
1640120 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:34 $(pwd)
1640176 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:45 $PATH
1640175 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:45 ${HOME}
1640163 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:43 ' ` "
1640177 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:45 He said, "Don't Do It!"
1640164 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:43 O'Reilly's Books
1640121 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:34 `pwd`
1640336 drwxr-xr-x 2 jleffler staff 68 Aug 25 13:16 a?b?c
1640243 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:58 a \' \` \$ b \" c
1640259 drwxr-xr-x 2 jleffler staff 68 Aug 25 13:01 a \\' \\\` \\$ b \\\" c
1640056 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:21 a b c
1640057 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:21 d e f
1640271 -rw-r--r-- 1 jleffler staff 223 Aug 25 13:03 x.sh
$
Well, there are some reasons why you should never try to parse the output from ls; it has generated question marks in place of the newlines (this is on Mac OS X 10.8.1 and is not GNU ls, just for those keeping score at home; other systems may behave differently).
And when the script (x.sh) is run, I get:
$ bash x.sh
arg: ! % *
arg: $(pwd)
arg: $PATH
arg: ${HOME}
arg: ' ` "
arg: He said, "Don't Do It!"
arg: O'Reilly's Books
arg: `pwd`
arg: a
b
c
arg: a \' \` \$ b \" c
arg: a \\' \\\` \\$ b \\\" c
arg: a b c
arg: d e f
arg: x.sh
cmd: ls -dil '! % *' '$(pwd)' '$PATH' '${HOME}' ''\'' ` "' 'He said, "Don'\''t Do It!"' 'O'\''Reilly'\''s Books' '`pwd`' 'a'
'b'
'c' 'a \'\'' \` \$ b \" c' 'a \\'\'' \\\` \\$ b \\\" c' 'a b c' 'd e f' 'x.sh'
ls: a: No such file or directory
1640119 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:34 ! % *
1640120 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:34 $(pwd)
1640176 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:45 $PATH
1640175 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:45 ${HOME}
1640163 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:43 ' ` "
1640177 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:45 He said, "Don't Do It!"
1640164 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:43 O'Reilly's Books
1640121 drwxr-xr-x 2 jleffler staff 68 Aug 25 12:34 `pwd`
b: Command not found.
c: Command not found.
$
There are multiple issues here. The sed script treated each line of the argument separately. That really isn't soluble using sed; or, perhaps more accurately, it isn't something I want to solve using sed. Æons ago, I wrote a C program escape to do the job that the sed script almost does.
#!/bin/bash
set -- *
escaped=$(escape "$#")
cmd="ls -dil $escaped"
echo "cmd: $cmd"
bash -c "$cmd"
tcsh -c "$cmd"
Note that I've added an invocation of bash in there. The output is:
cmd: ls -dil '! % *' '$(pwd)' '$PATH' '${HOME}' ''\'' ` "' 'He said, "Don'\''t Do It!"' 'O'\''Reilly'\''s Books' '`pwd`' 'a
b
c' 'a b c' 'd e f' x.sh
178474064 drwxr-xr-x 2 jleffler staff 68 Aug 25 13:38 ! % *
178474065 drwxr-xr-x 2 jleffler staff 68 Aug 25 13:38 $(pwd)
178474219 drwxr-xr-x 2 jleffler staff 68 Aug 25 13:38 $PATH
178474218 drwxr-xr-x 2 jleffler staff 68 Aug 25 13:38 ${HOME}
178474170 drwxr-xr-x 2 jleffler staff 68 Aug 25 13:38 ' ` "
178474220 drwxr-xr-x 2 jleffler staff 68 Aug 25 13:38 He said, "Don't Do It!"
178474131 drwxr-xr-x 2 jleffler staff 68 Aug 25 13:38 O'Reilly's Books
178474066 drwxr-xr-x 2 jleffler staff 68 Aug 25 13:38 `pwd`
178474998 drwxr-xr-x 2 jleffler staff 68 Aug 25 13:40 a?b?c
178473958 drwxr-xr-x 2 jleffler staff 68 Aug 25 13:38 a b c
178473959 drwxr-xr-x 2 jleffler staff 68 Aug 25 13:38 d e f
178475097 -rw-r--r-- 1 jleffler staff 115 Aug 25 13:41 x.sh
Unmatched '.
b: Command not found.
Unmatched '.
Wassup? Well, bash and other shells derived from the Bourne shell such as ksh, are OK with a string starting on one line and continuing over other lines, but the C shell and its derivatives are not. They demand a backslash before the newline. So, to work with tcsh, I'd have to upgrade escape to generate the output for the C shell. Not at all hard to do, but it would need doing. Presumably, that would be an option -c and for general purpose safety, the invocation would become:
escaped=$(escape -c -- "$#")
with the double-dash preventing misinterpretation of arguments in "$#" as options to escape itself. In part, this goes to show that it is hard to write scripts that deal with file names that contain characters outside the portable file name character set. Fortunately, I don't have to deal with the C shell very often; I don't plan to make that a part of escape because it is a change of interface (the current code does not have any options of its own, so I do not use the double-dash notation with escape). If I need it, it will become cescape to unconditionally support the C shell.
Your tcsh script is wrong. Beware, I don't know tcsh, but this is what I can tell from a few tests.
change it to
#!/bin/bash
/bin/tcsh -c 'ls "$1" "$2"' "$#"
You would need a "$#" instead of your "$*" on the tcsh side just like on the bash side. In bash, "$*" expands to a single word (the concatenation of all positional arguments, separated by the first IFS character (default space)), whereas "$#" expands to one word for each positional argument.
In tcsh, it seems like "$*" does the same, but I found no equivalent of "$#". In this case, you know the number of arguments and can use "$1" "$2" instead. Note that many people have cursed tcsh, so it might be very possible that there simply is no better solution...
So, to correct your assumptions, in your original script, it is not tcsh which receives only a single argument from bash, but it's ls receiving only a single one from tcsh.
To answer my own question... this is the solution:
/bin/tcsh -c "ls "'$argv:q' "${#}"

Resources