1

I’m trying to write a script – or an alias, to be more precise – which allows me to move files and follow (cd) them to their target directory. The accepted answer to this question suggests this code:

mvf() { mv "$@" && goto "$_"; }

where goto is just a safer variant of cd and $_ is the last argument passed to the last command.

My derived implementation is this:

alias mvaf="mv $@ && cd $_"

Note that I didn’t quote $@ in order to not try to move a file by the name of all arguments. I did try this variant originally, but the script failed, too. If I call the above implementation with mvaf test1 test2 .., it throws (translated): “mv: Missing file operand”

While debugging, I tried without the cd, and indeed alias mvaf="mv $@ ", which basically is just renaming mv, moves the files.

I’d like to know now why mv lacks an operand in my first implementation, and how this may be caused by the &&.

2
  • 1
    Note: From the Aliases section of the bash(1) man page. "There is no mechanism for using arguments in the replacement text. If arguments are needed, a shell function should be used (see FUNCTIONS below)."
    – Deathgrip
    Jun 15 '17 at 22:49
  • 1
    This is a very common FAQ. Perhaps the Bash manual page should more explicitly say "don't use aliases, you nitwit."
    – tripleee
    Jun 16 '17 at 4:59
5

Alias does not support operands like $@, or $1,$2 etc.

Your command

alias mvaf="mv $@ && cd $_"

equals to mv ' ' && cd $_ because $@ is not recognized by alias in the way you expect.

This can be proved easily like this:

$ alias mvaf='echo "Part 1:" $@ && echo "Part 2: " $_'
$ mvaf file66 /tmp/
Part 1:
Part 2:  Part 1: file66 /tmp/
#Part 2 includes the previous executed command (echo "Part 1:" $@) & the text sent after alias name 

$ alias mvaf='echo "Part 1:" $@;echo "Part 2: "'
$ mvaf file66 /tmp/
Part 1:
Part 2:  file66 /tmp/

On the other hand , this works but not because of $@

$ alias mvaf='echo "mv $@"'
$ mvaf file66 /tmp/
mv  file66 /tmp/

$ alias mvaf='echo "mv"'
$ mvaf file66 /tmp/
mv file66 /tmp/

As a general idea, alias is a kind of simple substitution.

Alias aa='command1;command2' , when called like aa sometext equals to command1;command2 sometext

To make this to work, you need to do it with a function. Bash discourage the use of alias and encourages the use of functions for such jobs. You can stick this function to your bash profile file, and this function can be called by name directly from your terminal as you would do with any alias:

mvcd() { mv "$1" "$2" && cd "$2"; }

Chaining mv and cd commands with && is important here, since && ensures that second command cd will be executed only if the previous command mv was successful.

Alternativelly, as has been already advised in the link of accepted answer in your question, you could do something like

mvf() { mv "$@" && goto "$_"; }
goto() { [ -d "$1" ] && cd "$1" || cd "$(dirname "$1")"; }

Be careful about bash word splitting . To make such a function to work correctly you need to insert double quotes when calling the function if the file you are going to move or the directory that file is going to be sent include space in their name.

$ mvcd() { echo "1=$1";echo "2=$2";echo "3=$3";echo "4=$4"; }

$ mvcd spaced file1 /spaced directory/
1=spaced
2=file1
3=/spaced
4=directory/

$ mvcd "spaced file1" "/spaced directory/"
1=spaced file1
2=/spaced directory/
3=
4=
2
  • Your first mvcd has mv instead of cd. But +1 for being the only one so far to say what everybody is thinking; don't use an alias.
    – tripleee
    Jun 16 '17 at 4:54
  • @tripleee Typo mistake - corrected. Thank you! Jun 16 '17 at 6:07
1

alias does not consider $@ or $_ (or $anything) special, so they are just passed to the shell verbatim.

Thus your original:

alias mvaf="mv $@ && cd $_"

means that:

mvaf f dst/

turns into:

mv $@ && cd $_ f dat/

Since $@ and $_ are unlikely to be defined, the shell interprets as this:

mv && cd f dat/

thus mv is called without an argument and throws the "Missing file operand" error.

When you redefine as:

alias mvaf="mv $@ "

and execute:

mvaf f dst/

it turns into:

mv $@ f dst/

which as before reduces to:

mv f dst/

and tricks you into thinking it has worked!

0

As @Deathgrip has written in comment,

There is no mechanism for using arguments in the replacement text. If arguments are needed, a shell function should be used (see FUNCTIONS below).

How do aliases work?

Let us think of what will happen when you type the following two lines in the command line:

$ alias word1="echo example"

$ word1 word2 word3

Upon executing the first command, bash remembers that whenever word1 is the first word of any command it should substitute it for echo example. When it sees the second command, first of all, it performs the substitution in the dumbest (simplest) way possible - it creates a command echo example word2 word3 from that input. Therefore, it echoes example word2 word3.

Now, what happens with your alias?

When it sees mfav one two it just evaluates mv $@ && cd $_ one two. Since $@ is empty (try echo $@ in console), mv fails. cd fails as well, since it receives more arguments than one.

Note: I have oversimplified writing that alias substitutes only the first word. For situation when this rule is violated, see my other answer.

0

All the other answers explain why the alias is not working, and George Vasiliou's answer gives an alternative function that does the job.

I wanted to build on that answer, however, to make it more robust. Currently, the function

mvcd() { mv "$1" "$2" && cd "$2"; }

will not work if the destination is a file. To fix that, we can use bash variable substring notation:

mvcd() { mv "$1" "$2" && [[ -d $2 ]] && cd $2 || cd ${2%/*}; }

To see what's going on here, let's write it in a little bit more readable format:

mvcd () {
    mv "$1" "$2";     # Do the move
    if [[ -d $2 ]]    # Check if arg $2 is a dir
    then
        cd $2;        # if it is, cd into it
    else
        cd ${2%/*};   # else remove the filename and cd
    fi;
}

We check if the destination is a directory[1] - and if it is, we simply cd into it. If it is not, then we substring the second argument from the beginning to the rightmost / and cd into that.


[1] I wasted an embarassingly large amount of time trying to do it without the conditional, stringing together dirname and readlink or simply always prepending the ./ to the path or other hacks, but I always had a problem - either it would cd too far back (in the case of readlink) or it wouldn't work with cding to .., or it wouldn't work with filenames or... yeah. If anyone can do it without the conditional, I'm very much open to suggestions.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.