How can I use a shell command parameter as an argument? [duplicate] - linux

I often find myself using mv to rename a file. E.g.
mv app/models/keywords_builder.rb app/models/keywords_generator.rb
Doing so I need to write (ok, tab complete) the path for the second parameter. In this example it isn't too bad but sometimes the path is deeply nested and it seems like quite a bit of extra typing.
Is there a more efficient way to do this?

And another way: brace expansion.
mv app/models/keywords_{builder,generator}.rb
In general,
before{FIRST,SECOND}after
expands to
beforeFIRSTafter beforeSECONDafter
So it's also useful for other renames, e.g.
mv somefile{,.bak}
expands to
mv somefile somefile.bak
It works in bash and zsh.
More examples:
Eric Bergen > Bash Brace Expansion
Bash Brace Expansion | Linux Journal

You can use history expansion like this:
mv app/modules/keywords_builder.rb !#^:h/keywords_generator.rb
! introduces history expansion.
# refers to the command currently being typed
^ means the first argument
:h is a modifier to get the "head", i.e. the directory without the file part
It's supported in bash and zsh.
Docs:
bash history expansion
zsh history expansion

One way is to type the first file name and a space, then press Ctrl+w to delete it. Then press Ctrl+y twice to get two copies of the file name. Then edit the second copy.
For example,
mv app/models/keywords_builder.rb <Ctrl+W><Ctrl+Y><Ctrl+Y><edit as needed>

or cd apps/models && mv keywords_builder.rb keywords_generator.rb && cd -

Combined answers of Mikel and geekosaur with additonal use of ":p"
use brace expansion to avoid first argument repetition:
mv -iv {,old_}readme.txt # 'readme.txt' -> 'old_readme.txt'
mv -iv file{,.backup} # 'file' -> 'file.backup'
use history expansion to avoid first argument repetition:
mv -iv "system file" !#$.backup # 'system file' -> 'system file.backup'
the filename can be printed using the "p" designator for further edition:
mv -iv "file with a long name" !#$:p
then press "↑" to edit the command

Related

How to remove a file called * (asterisk) without using quotations?

I implemented the following command to create a file called * (asterisk):
echo > '*'
Now I'm supposed to remove this file without using any quotations.
I know how to remove this by using quotations, but not sure how without using quotations.
I tried the following commands which I was sure that they won't work because of command line expansion:
rm ./*
rm /*
If someone can help me with this, I would greatly appreciate it.
I think you're supposed to work this out yourself :-)
The simplest solution not involving quoting is to use the pattern [*]. Bracket expressions in a shell work much like character classes in regular expressions so that will match a file whose name is the single character *. Thus, you can delete your file with
rm [*]
Note that you cannot use that pattern to create a file named * because the shell substitutes words containing patterns with the name(s) of the files which match the pattern; if no such file exists, then the pattern is not matched and no substitution is performed. So if there is no file named *, then touch [*] will create a file named [*].
You could use history expansion. If the rm command directly follows the echo command, you can use !$:
echo > '*'
rm !$
!$ is shorthand for !!:$: repeat the last word ($) of the last command (!!).
If there are commands between the echo and the rm command, you can find the history number using fc -l:
$ echo > '*'
$ cmd1
$ cmd2
$ cmd3
$ fc -l
[...]
27628 echo > '*'
27629 cmd1
27630 cmd2
27631 cmd3
$ rm !27628:$
!27628 expands to the command with that number in the history, and $ is again the last word of that command.
If you have to run this in a script, you can't really look up the command number and insert it, but you can count the number of commands between the echo and the rm and use a relative event designator:
echo > '*'
cmd1
rm !-2:$
where !-2 refers to the command two lines back. Notice that history expansion is by default disabled in non-interactive shells; use
shopt -o history
to enable it.
You could use rm -i * if the number of files is not too big. This will ask for confirmation for every single file. Confirm deletion only for the file * and reject it for all others.

Expanding the path stored in a variable [duplicate]

Say I have a folder called Foo located in /home/user/ (my /home/user also being represented by ~).
I want to have a variable
a="~/Foo" and then do
cd $a
I get
-bash: cd: ~/Foo: No such file or directory
However if I just do cd ~/Foo it works fine. Any clue on how to get this to work?
You can do (without quotes during variable assignment):
a=~/Foo
cd "$a"
But in this case the variable $a will not store ~/Foo but the expanded form /home/user/Foo. Or you could use eval:
a="~/Foo"
eval cd "$a"
You can use $HOME instead of the tilde (the tilde is expanded by the shell to the contents of $HOME).
Example:
dir="$HOME/Foo";
cd "$dir";
Although this question is merely asking for a workaround, this is listed as the duplicate of many questions that are asking why this happens, so I think it's worth giving an explanation. According to https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_06:
The order of word expansion shall be as follows:
Tilde expansion, parameter expansion, command substitution, and arithmetic expansion shall be performed, beginning to end.
When the shell evaluates the string cd $a, it first performs tilde expansion (which is a no-op, since $a does not contain a tilde), then it expands $a to the string ~/Foo, which is the string that is finally passed as the argument to cd.
A much more robust solution would be to use something like sed or even better, bash parameter expansion:
somedir="~/Foo/test~/ing";
cd "${somedir/#\~/$HOME}"
or if you must use sed,
cd $(echo "$somedir" | sed "s#^~#$HOME#")
If you use double quotes the ~ will be kept as that character in $a.
cd $a will not expand the ~ since variable values are not expanded by the shell.
The solution is:
eval "cd $a"

Why the Linux command CP behave differently in CLI and in script?

I want to copy a bunch of Verilog/systemverilog sources, so I use CP with wildcard expression:
cp <some_dir>/*.{v,sv,svh} .
It works. But when I put it to a script with exactly the same line, the CP command fails with the log:
cp: cannot stat `../../mytest/spiTest/*.{v,sv,svh}': No such file or directory
How is that happening?
PS: I use bash as the shell.
And here is my script:
#!/bin/bash
rdir=../../mytest/spiTest
f1="$rdir/bench.lst"
f2="$rdir/cphex" #the script to copy rom data
f3="$rdir/make*" #makefile scripts
f4="$rdir/*.hex" #rom files
f5="$rdir/*.{v,sv,svh}" #testbench files
echo 'Copying files...'
cp $f1 $f2 $f3 $f4 .
cp $f5 .
I do changed the first line to
#!/bin/bash -vx
and run this script again, and I get:
#!/bin/bash -vx
rdir=../../mytest/spiTest
+ rdir=../../mytest/spiTest
f1="$rdir/bench.lst"
+ f1=../../mytest/spiTest/bench.lst
f2="$rdir/cphex" #the script to copy rom data
+ f2=../../mytest/spiTest/cphex
f3="$rdir/make*" #makefile scripts
+ f3='../../mytest/spiTest/make*'
f4="$rdir/*.hex" #rom files
+ f4='../../mytest/spiTest/*.hex'
f5="$rdir/*.{v,sv,svh}" #testbench files
+ f5='../../mytest/spiTest/*.{v,sv,svh}'
echo 'Copying files...'
+ echo 'Copying files...'
Copying files...
cp $f1 $f2 $f3 $f4 .
+ cp ../../mytest/spiTest/bench.lst ../../mytest/spiTest/cphex ../../mytest/spiTest/makefile ../../mytest/spiTest/makefile.defines ../../mytest/spiTest/rom.hex ../../mytest/spiTest/rom_if.hex .
cp $f5 .
+ cp '../../mytest/spiTest/*.{v,sv,svh}' .
cp: cannot stat `../../mytest/spiTest/*.{v,sv,svh}': No such file or directory
Check the first line of the script. It probably reads:
#!/bin/sh
which switches the shell from BASH to Bourne Shell. Use
#!/bin/bash
instead.
[EDIT] You're running into problems with expansion. BASH has a certain order in which it expands patterns and variables. That means:
f5="$rdir/*.{v,sv,svh}" #testbench files
is quoted, so no file name expansion happens at this time. Only the variable $rdir is expanded. When
cp $f5 .
is executed, BASH first looks for file names to expand and there are none. Then it expands variables (f5) and then calls cp with two arguments: ../../mytest/spiTest/*.{v,sv,svh} and .. Since cp expects the shell to have performed the file name expansion already, you get an error.
To fix this, you have to use arrays:
f5=($rdir/*.{v,sv,svh})
This replaces the variable and then expands the file names and puts everything into the array f5. You can then call cp with this array while preserving whitespaces:
cp "${f5[#]}" .
Every single character here is important. [#] tells BASH to expand the whole array here. The quotes say: Preserve whitespace. {} is necessary to tell BASH that [#] is part of the variable "name" to expand.
Here's the problem: the order of substitutions. Bash performs brace expansion before variable expansion. In the line cp $f5 ., bash will do:
brace expansion: n/a
this is the key point: the variable contains a brace expression, but the shell does not see it now when it needs to.
tilde expansion: n/a
parameter expansion: yes -- cp ../../mytest/spiTest/*.{v,sv,svh} .
command substitution: n/a
arithmetic expansion: n/a
process substitution: n/a
word splitting: n/a
filename expansion: yes, bash looks for files in that directory ending with the string
.{v,sv,svh}. It finds none, nullglob is not set, thus the pattern is not removed from the command
quote removal: n/a
Now the command is executed and fails with the error you see.
https://www.gnu.org/software/bash/manual/bashref.html#Shell-Expansions
Solutions:
use Aaron's idea of an array
(not recommended) force a 2nd round of expansions: eval cp $f5 .
The line
f5="$rdir/*.{v,sv,svh}" #testbench files
is probably wrong. First, avoid comments at end of line, they should be (at least for readability) in a separate line. Then, avoid using globbing in variable assignment. So remove that line, and code later (that is, replace the old cp $f5 . line with)
cp "$rdir"/*.{v,sv,svh} .
BTW, I would test that "$rdir" is indeed a directory with
if [ ! -d "$rdir" ] ; then
echo invalid directory $rdir > /dev/stderr
exit 1
fi
You should read the Advanced Bash Scripting Guide

Linux bash shell scripts - spaces in file names

It has been a long time since I did much bash script writing.
This is a bash script to copy and rename files by deleting all before the first period delimiter:
#!/bin/bash
mkdir fullname
mv *.audio fullname
cd fullname
for x in * ;
do
cp $x ../`echo $x | cut -d "." -f 2-`
done
cd ..
ls
It works well for file names with no embedded spaces but not for those with spaces.
How can I change the code to fix this simple Linux bash script? Any suggestions for improving the code for other reasons would also be welcome.
Example filenames, some with embedded spaces and some not (from link)
http://www.homenetvideo.com/demo/index.php?/Radio%20%28VLC%29
Ambient.A6.SOMA Space Station.audio
Blues.B9.Blues Radio U.K.audio
Classical.K3.Radio Stephansdom - Vienna.audio
College.CI.KDVS U of California, Davis.audio
Country.Q1.K-FROG.audio
Easy.G4.WNYU.audio
Eclectic.M2.XPN.audio
Electronica.E2.Rinse.audio
Folk.F1.Radionomy.audio
Hiphop.H1.NPR.audio
Indie.I4.WAUG.audio
Jazz.J6.KCSM.audio
Latin.L3.Mega.audio
Misc.X7.Gaydio.audio
News.N9.KQED.audio
Oldies.O1.Lonestar.audio
OldTime.Y1.Roswell.audio
Progressive.P1.Aural Moon.audio
Rock.R8.WXRT.audio
Scanner.Z3.Montreal.audio
Soul.S1.181.FM.audio
Talk.T2.TWiT.audio
World.W3.Persian.audio
http://lh5.googleusercontent.com/-QjLEiAtT4cw/U98_UFcWvvI/AAAAAAAABv8/gyPhbg8s7Bw/w681-h373-no/homenet-radio.png
Whenever you deal with file names that might have spaces in them, you must reference them as "$x" rather than just $x. That's what's causing your cp command to fail.
Your echo command is also problematic. Although echo does the right thing for simple spaces - it echoes a file named A B C as A B C - it will still fail if you have more than one consecutive space in the name, or whitespace that isn't a simple space character.
Instead of passing the file names to external programs for processing, which always requires getting them through the whitespace-hostile command line, you should use bash built-in functions for string manipulations wherever possible, e.g. ${x%%foo}, ${x#bar} and similar functions. The man page describes them under "Parameter expansion".
Here's my suggestion:
#!/bin/bash
shopt -s nullglob
mkdir fullname
mv *.audio fullname
(
cd fullname || exit
for x in *; do
cp "$x" "../${x#*.}"
done
)
ls
nullglob prevents * from presenting itself if no file matches it. Just optional.
() summons a subshell and saves you from changing back to another directory.
|| exit terminates the subshell if cd fails to change directory.
${x#*.} removes the <first>. from $x and expands it.

Partial File Rename with different file types

Sorry if this is very simple compared to usual questions but I am just starting out. I have some files all with the same start name but of different file types, e.g:
1234.x
1234.y
1234.z
1234_V2.x
1234_V2.y
1234_V2.z
I want to rename the first part of these whilst keeping any ending and file type, e.g:
4321.x
4321.y
4321.z
4321_V2.x etc
I have tried using
mv 1234* 4321*
and
rename 1234* 4321*
But no luck! I have also been through all the other SO articles and although I could use a loop, most depend on the file type being the same.
Thanks in advance
You can use bash substitution:
for file in 1234*
do mv "$file" "4321${file#1234}"
done
OR, replace the do mv with the following
do mv "$file" "${file/1234/4321}"
See more in man bash under EXPANSION section, sub-section Parameter Expansion
Assuming your filenames for 1234 and 4321 i.e constant for all files, you can try this
for fn in `find . -name 1234*`
do
newf=`echo $fn | sed s/1234/4321/`
mv $fn $newfn
done
You can use a shell script, but it's kind of ugly because it will fork a lot, and thus, if you have a lot of files to rename, it will take time.
for f in 1234*; do echo mv $f $(echo $f | sed -e 's/1234/4321/'); done
Otherwize, rename is a good way to do it:
rename 's/1234/4321/' 1234*
Rename expects a regular expression as first parameter, see online documentation
See if it works:
rename "s/1234/4321/" 1234*
command means substitute(because of s) occurances of "1234" with "4321" in files that has name of pattern 1234*
You can also look at here. It is slightly more complicated than your case.

Resources