xargs Behavior : Semantics or Bug? - shell

Wanting to push a number of directories onto the stack, I ran:
echo ~/{Desktop,Downloads,Movies} | xargs pushd
and encountered xargs: pushd: No such file or directory
Brace expansion is not the cause of the mismatch between what I have in mind and what happens because echo ~/Desktop | xargs pusdh results in the same error.
As a point of comparison, echo ~/Desktop | xargs cd changes directory as one would expect.
What's going on here?

It's semantics, the equivalent statement should be:
pushd $(echo ~/{Desktop,Downloads,Movies})
After my experiment, the behavior of builtin command is like
#!/bin/sh
function pushd()
{
accept input from $1, $2, $3.....
# Builtin will not read from stdin! So you can't use pipe.
}
The builtin command should be viewed as shell function.
[Edit]
The command 'pushd' in zsh is implemented together with 'cd', it only accept one argument.
So you can't push a number of directories in single statement.
source is there

Are you sure xargs cd does what you expect? I'd be surprised! xargs will call a binary, but pushd is not - run type pushd if you want confirmation. cd and pushd don't make much sense as external binaries.
You'll need to capture the directories in a variable, and call pushd in a for loop in the shell process itself, rather than from xargs, which is a child process of the shell, and hence any directory state modified by xargs or its children won't pass up to the parent shell.

Related

changing directories by using cd

I executed the following command :
cd /mnt/c/Users/Daniel/Documents/Assg/ | cat file.txt
my question is why doesn't it change directory?. The output file.txt is displayed but the directory is not changed. I understand that if we execute the same command in the following order, it won't work because cd changes directory in a child process, so the net result is the same.
cat file.txt | cd /mnt/c/Users/Daniel/Documents/Assg/
Try just cd /mnt/c/Users/Daniel/Documents/Assg/
As was already stated, the following:
cd /mnt/c/Users/Daniel/Documents/Assg/
should do the trick, but I'd like to go a bit more into why the command you presented doesn't work as expected. In Bash (and other shells), you can have multiple "subshells" running under a parent shell. each of these subshells has its own working directory. When you run commands in a pipeline, as you have done, a subshell is created. The working directory of the subshell was changed, but that didn't have any effect on the shell you were working in.
It depends on the shell you use
When you run two commands in a pipeline, typically one or both of the commands is run in a separate child process.
In older shells this would be both, in later shells this can be either
the first or the last.
At one point, the ksh93 team decided to make the last command in the pipeline the parent. This would prevent race conditions, and if the command was a builtin, it allows it to run inside the current shell
process and preserve the results of the pipeline.
Nevertheless, cd is a command that does not consume or produce any input or output (except for diagnostics on stderr), and using it in a pipeline
by itself is just silly. A better, because more predictable, command line would be:
cd /mnt/c/Users/Daniel/Documents/Assg/ && cat file.txt
This will assure that cat only runs if cd succeeds, and will then
show the contents of file.txt from the given directory.
You have different options.
Perform cat after trying to change dir
cd /mnt/c/Users/Daniel/Documents/Assg/ ; cat file.txt
Perform cat only when change dir worked
cd /mnt/c/Users/Daniel/Documents/Assg/ && cat file.txt
Perform cat in the other directory, but return to the current dir when finished.
(cd /mnt/c/Users/Daniel/Documents/Assg/ && cat file.txt)
# or
cat /mnt/c/Users/Daniel/Documents/Assg/file.txt
EDIT:
Your question: "why doesnt cd /mnt/c/Users/Daniel/Documents/Assg/ | cat file.txt, change directory?." can be answered two ways.
The technical explanation is given by #Henk (The pipe introduces a subshell, and environ settings in a subshell get lost when the shell exits).
The functional explanation is that you used the wrong syntax for what you are trying to accomplish.

csh doesn't recognize command with command line options beginning with --

I have an rsync command in my csh script like this:
#! /bin/csh -f
set source_dir = "blahDir/blahBlahDir"
set dest_dir = "foo/anotherFoo"
rsync -av --exclude=*.csv ${source_dir} ${dest_dir}
When I run this I get the following error:
rsync: No match.
If I remove the --exclude option it works. I wrote the equivalent script in bash and that works as expected
#/bin/bash -f
source_dir="blahDir/blahBlahDir"
dest_dir="foo/anotherFoo"
rsync -av --exclude=*.csv ${source_dir} ${dest_dir}
The problem is that this has to be done in csh only. Any ideas on how I can get his to work?
It's because csh is trying to expand --exclude=*.csv into a filename, and complaining because it cannot find a file matching that pattern.
You can get around this by enclosing the option in quotes:
rsynv -rv '--exclude=*.csv' ...
or escaping the asterisk:
rsynv -rv --exclude=\*.csv ...
This is a consequence of the way csh and bash differ in their default treatment of arguments with wildcards that don't match a file. csh will complain while bash will simply leave it alone.
You may think bash has chosen the better way but that's not necessarily so, as shown in the following transcript where you have a file matching the argument:
pax> touch -- '--file=xyzzy.csv' ; ls -- *.csv
--file=xyzzy.csv
pax> echo --file=*.csv
--file=xyzzy.csv
You can see there that the bash shell expands the argument rather than giving it to the program as is. Both sides have their pros and cons.

How to set a Directory as an Argument in Bash

I am having trouble finding out how to set a directory as an argument in bash.
The directory I am trying to have as an argument is /home/rrodriguez/Documents/one.
Anywhere I try to look for an answer I see examples like dir = $1 but I cant seem to find an explanation of what this means or how to set it up so that it references my specific file location. Could anyone show me how to set up my variable for my path directory?
Adding my code for a better understanding of what im trying to do:
#!bin/bash
$1 == 'home/rrodriguez/Documents/one/'
dir = $1
touch -c $dir/*
ls -la $dir
wc$dir/*
Consider:
#!bin/bash
dir=$1
touch -c "$dir"/*
ls -la "$dir"
This script takes one argument, a directory name, and touches files in that directory and then displays a directory listing. You can run it via:
bash script.sh 'home/rrodriguez/Documents/one/'
Since home/rrodriguez/Documents/one/ is the first argument to the script, it is assigned to $1 in the script.
Notes
In shell, never put spaces on either side of the = in an assignment.
I omitted the line wc$dir/* because it wasn't clear to me what the purpose of it was.
I put double-quotes around $dir to prevent the shell from, among other things, performing word-splitting. This would matter if dir contains spaces.

pushd popd global directory stack?

I do not know if there is a valid way to do this. But, have always wanted to see if its possible.
I know that pushd, popd and dirs are useful for a number of things like copying between directories you have recently visited.
But, is there a way in which you can keep a global stack? So that if I push something (using pushd) in one terminal it gets reflected in another (maybe only the terminals in that login session).
You should be able to do this with a pair of shell functions and a temporary file.
Your temporary file would be named something like '/home/me/.directory_stack' and would simply contain a list of directories:
/home/me
/etc
/var/log
Your 'push_directory' function would simply add the current directory to the list. The 'pop_directory' function would pull the most recent off of the list and switch to that directory. Storing the stack in a file like this ensures that the information exists across all open terminals (and even across reboots).
Here are some example functions (warning: only lightly tested)
directory_stack=/home/me/.directory_stack
function push_dir() {
echo $(pwd) >> $directory_stack
cd $1
}
function pop_dir() {
[ ! -s $directory_stack ] && return
newdir=$(sed -n '$p' $directory_stack)
sed -i -e '$d' $directory_stack
cd $newdir
}
Add that to your .bashrc and they'll automatically be defined every time you log into the shell.
You'll probably want to write a few shell functions and use them in place of pushd and popd. Something like the following (untested) functions might do the job:
mypushd() { echo "$1" >> ~/.dir_stack ; cd "$1" }
mypopd() { dir=`tail -1 ~/.dir_stack` ; cd "$dir" ;
foo=`wc -l ~/.dir_stack | egrep -o '[0-9]+'` ;
((foo=$foo-1)) ;
mv ~/.dir_stack ~/.dir_stack_old ;
head -n $foo ~/.dir_stack_old > ~/.dir_stack }
You could get rid of some of the uglier bits if you write a small program that returns and removes the last line of the file.

xargs with command that open editor leaves shell in weird state

I tried to make an alias for committing several different git projects. I tried something like
cat projectPaths | \
xargs -I project git --git-dir=project/.git --work-tree=project commit -a
where projectPaths is a file containing the paths to all the projects I want to commit. This seems to work for the most part, firing up vi in sequence for each project so that I can write a commit msg for it. I do, however, get a msg:
"Vim: Warning: Input is not from a terminal"
and afterward my terminal is weird: it doesn't show the text I type and doesn't seem to output any newlines. When I enter "reset" things pretty much back to normal, but clearly I'm doing something wrong.
Is there some way to get the same behavior without messing up my shell?
Thanks!
Using the simpler example of
ls *.h | xargs vim
here are a few ways to fix the problem:
xargs -a <( ls *.h ) vim
or
vim $( ls *.h | xargs )
or
ls *.h | xargs -o vim
The first example uses the xargs -a (--arg-file) flag which tells xargs to take its input from a file rather than standard input. The file we give it in this case is a bash process substitution rather than a regular file.
Process substitution takes the output of the command contained in <( ) places it in a filedescriptor and then substitutes the filedescriptor, in this case the substituted command would be something like xargs -a /dev/fd/63 vim.
The second command uses command substitution, the commands are executed in a subshell, and their stdout data is substituted.
The third command uses the xargs --open-tty (-o) flag, which the man page describes thusly:
Reopen stdin as /dev/tty in the child process before executing the
command. This is useful if you want xargs to run an interactive
application.
If you do use it the old way and want to get your terminal to behave again you can use the reset command.
The problem is that since you're running xargs (and hence git and hence vim) in a pipeline, its stdin is taken from the output of cat projectPaths rather than the terminal; this is confusing vim. Fortunately, the solution is simple: add the -o flag to xargs, and it'll start git (and hence vim) with input from /dev/tty, instead of its own stdin.
The man page for GNU xargs shows a similar command for emacs:
xargs sh -c 'emacs "$#" < /dev/tty' emacs
(in this command, the second "emacs" is the "dummy string" that wisbucky refers to in a comment to this answer)
and says this:
Launches the minimum number of copies of Emacs needed, one after the
other, to edit the files listed on xargs' standard input. This example
achieves the same effect as BSD's -o option, but in a more flexible and
portable way.
Another thing to try is using -a instead of cat:
xargs -a projectPaths -I project git --git-dir=project/.git --work-tree=project commit -a
or some combination of the two.
If you have GNU Parallel http://www.gnu.org/software/parallel/ installed you should be able to do this:
cat projectPaths |
parallel -uj1 git --git-dir={}/.git --work-tree={} commit -a
In general this works too:
cat filelist | parallel -Xuj1 $EDITOR
in case you want to edit more than one file at a time (and you have set $EDITOR to your favorite editor).
-o for xargs (as mentioned elsewhere) only works for some versions of xargs (notably it does not work for GNU xargs).
Watch the intro video to learn more about GNU Parallel http://www.youtube.com/watch?v=OpaiGYxkSuQ
Interesting! I see the exact same behaviour on Mac as well, doing something as simple as:
ls *.h | xargs vim
Apparently, it is a problem with vim:
http://talideon.com/weblog/2007/03/xargs-vim.cfm

Resources