Passing values to a specific command in a chained command using an alias [duplicate] - linux

I used to use CShell (csh), which lets you make an alias that takes a parameter. The notation was something like
alias junk="mv \\!* ~/.Trash"
In Bash, this does not seem to work. Given that Bash has a multitude of useful features, I would assume that this one has been implemented but I am wondering how.

Bash alias does not directly accept parameters. You will have to create a function.
alias does not accept parameters but a function can be called just like an alias. For example:
myfunction() {
#do things with parameters like $1 such as
mv "$1" "$1.bak"
cp "$2" "$1"
}
myfunction old.conf new.conf #calls `myfunction`
By the way, Bash functions defined in your .bashrc and other files are available as commands within your shell. So for instance you can call the earlier function like this
$ myfunction original.conf my.conf

Refining the answer above, you can get 1-line syntax like you can for aliases, which is more convenient for ad-hoc definitions in a shell or .bashrc files:
bash$ myfunction() { mv "$1" "$1.bak" && cp -i "$2" "$1"; }
bash$ myfunction original.conf my.conf
Don't forget the semi-colon before the closing right-bracket. Similarly, for the actual question:
csh% alias junk="mv \\!* ~/.Trash"
bash$ junk() { mv "$#" ~/.Trash/; }
Or:
bash$ junk() { for item in "$#" ; do echo "Trashing: $item" ; mv "$item" ~/.Trash/; done; }

The question is simply asked wrong. You don't make an alias that takes parameters because alias just adds a second name for something that already exists. The functionality the OP wants is the function command to create a new function. You do not need to alias the function as the function already has a name.
I think you want something like this :
function trash() { mv "$#" ~/.Trash; }
That's it! You can use parameters $1, $2, $3, etc, or just stuff them all with $#

TL;DR: Do this instead
Its far easier and more readable to use a function than an alias to put arguments in the middle of a command.
$ wrap_args() { echo "before $# after"; }
$ wrap_args 1 2 3
before 1 2 3 after
If you read on, you'll learn things that you don't need to know about shell argument processing. Knowledge is dangerous. Just get the outcome you want, before the dark side forever controls your destiny.
Clarification
bash aliases do accept arguments, but only at the end:
$ alias speak=echo
$ speak hello world
hello world
Putting arguments into the middle of command via alias is indeed possible but it gets ugly.
Don't try this at home, kiddies!
If you like circumventing limitations and doing what others say is impossible, here's the recipe. Just don't blame me if your hair gets frazzled and your face ends up covered in soot mad-scientist-style.
The workaround is to pass the arguments that alias accepts only at the end to a wrapper that will insert them in the middle and then execute your command.
Solution 1
If you're really against using a function per se, you can use:
$ alias wrap_args='f(){ echo before "$#" after; unset -f f; }; f'
$ wrap_args x y z
before x y z after
You can replace $# with $1 if you only want the first argument.
Explanation 1
This creates a temporary function f, which is passed the arguments (note that f is called at the very end). The unset -f removes the function definition as the alias is executed so it doesn't hang around afterwards.
Solution 2
You can also use a subshell:
$ alias wrap_args='sh -c '\''echo before "$#" after'\'' _'
Explanation 2
The alias builds a command like:
sh -c 'echo before "$#" after' _
Comments:
The placeholder _ is required, but it could be anything. It gets set to sh's $0, and is required so that the first of the user-given arguments don't get consumed. Demonstration:
sh -c 'echo Consumed: "$0" Printing: "$#"' alcohol drunken babble
Consumed: alcohol Printing: drunken babble
The single-quotes inside single-quotes are required. Here's an example of it not working with double quotes:
$ sh -c "echo Consumed: $0 Printing: $#" alcohol drunken babble
Consumed: -bash Printing:
Here the values of the interactive shell's $0 and $# are replaced into the double quoted before it is passed to sh. Here's proof:
echo "Consumed: $0 Printing: $#"
Consumed: -bash Printing:
The single quotes ensure that these variables are not interpreted by interactive shell, and are passed literally to sh -c.
You could use double-quotes and \$#, but best practice is to quote your arguments (as they may contain spaces), and \"\$#\" looks even uglier, but may help you win an obfuscation contest where frazzled hair is a prerequisite for entry.

All you have to do is make a function inside an alias:
$ alias mkcd='_mkcd(){ mkdir "$1"; cd "$1";}; _mkcd'
^ * ^ ^ ^ ^ ^
You must put double quotes around "$1" because single quotes will not work. This is because clashing the quotes at the places marked with arrows confuses the system. Also, a space at the place marked with a star is needed for the function.

Once I did some fun project and I'm still using it. It's showing some animation while copy files via cp command coz cp don't show anything and it's kind of frustrating. So I've made this alias for cp:
alias cp="~/SCR/spinner cp"
And this is the spinner script
#!/bin/bash
#Set timer
T=$(date +%s)
#Add some color
. ~/SCR/color
#Animation sprites
sprite=( "(* ) ( *)" " (* )( *) " " ( *)(* ) " "( *) (* )" "(* ) ( *)" )
#Print empty line and hide cursor
printf "\n${COF}"
#Exit function
function bye { printf "${CON}"; [ -e /proc/$pid ] && kill -9 $pid; exit; }; trap bye INT
#Run our command and get its pid
"$#" & pid=$!
#Waiting animation
i=0; while [ -e /proc/$pid ]; do sleep 0.1
printf "\r${GRN}Please wait... ${YLW}${sprite[$i]}${DEF}"
((i++)); [[ $i = ${#sprite[#]} ]] && i=0
done
#Print time and exit
T=$(($(date +%s)-$T))
printf "\n\nTime taken: $(date -u -d #${T} +'%T')\n"
bye
It looks like this
Cycled animation)
Here is the link to a color script mentioned above.
And new animation cycle)
So the answer to the OP's question is to use intermediate script that could shuffle args as you wish.

An alternative solution is to use marker, a tool I've created recently that allows you to "bookmark" command templates and easily place cursor at command place-holders:
I found that most of time, I'm using shell functions so I don't have to write frequently used commands again and again in the command-line. The issue of using functions for this use case, is adding new terms to my command vocabulary and having to remember what functions parameters refer to in the real-command. Marker goal is to eliminate that mental burden.

Syntax:
alias shortName="your custom command here"
Example:
alias tlogs='_t_logs() { tail -f ../path/$1/to/project/logs.txt ;}; _t_logs'

Bash alias absolutely does accept parameters. I just added an alias to create a new react app which accepts the app name as a parameter. Here's my process:
Open the bash_profile for editing in nano
nano /.bash_profile
Add your aliases, one per line:
alias gita='git add .'
alias gitc='git commit -m "$#"'
alias gitpom='git push origin master'
alias creact='npx create-react-app "$#"'
note: the "$#" accepts parameters passed in like "creact my-new-app"
Save and exit nano editor
ctrl+o to to write (hit enter); ctrl+x to exit
Tell terminal to use the new aliases in .bash_profile
source /.bash_profile
That's it! You can now use your new aliases

Here's are three examples of functions I have in my ~/.bashrc, that are essentially aliases that accept a parameter:
#Utility required by all below functions.
#https://stackoverflow.com/questions/369758/how-to-trim-whitespace-from-bash-variable#comment21953456_3232433
alias trim="sed -e 's/^[[:space:]]*//g' -e 's/[[:space:]]*\$//g'"
.
:<<COMMENT
Alias function for recursive deletion, with are-you-sure prompt.
Example:
srf /home/myusername/django_files/rest_tutorial/rest_venv/
Parameter is required, and must be at least one non-whitespace character.
Short description: Stored in SRF_DESC
With the following setting, this is *not* added to the history:
export HISTIGNORE="*rm -r*:srf *"
- https://superuser.com/questions/232885/can-you-share-wisdom-on-using-histignore-in-bash
See:
- y/n prompt: https://stackoverflow.com/a/3232082/2736496
- Alias w/param: https://stackoverflow.com/a/7131683/2736496
COMMENT
#SRF_DESC: For "aliaf" command (with an 'f'). Must end with a newline.
SRF_DESC="srf [path]: Recursive deletion, with y/n prompt\n"
srf() {
#Exit if no parameter is provided (if it's the empty string)
param=$(echo "$1" | trim)
echo "$param"
if [ -z "$param" ] #http://tldp.org/LDP/abs/html/comparison-ops.html
then
echo "Required parameter missing. Cancelled"; return
fi
#Actual line-breaks required in order to expand the variable.
#- https://stackoverflow.com/a/4296147/2736496
read -r -p "About to
sudo rm -rf \"$param\"
Are you sure? [y/N] " response
response=${response,,} # tolower
if [[ $response =~ ^(yes|y)$ ]]
then
sudo rm -rf "$param"
else
echo "Cancelled."
fi
}
.
:<<COMMENT
Delete item from history based on its line number. No prompt.
Short description: Stored in HX_DESC
Examples
hx 112
hx 3
See:
- https://unix.stackexchange.com/questions/57924/how-to-delete-commands-in-history-matching-a-given-string
COMMENT
#HX_DESC: For "aliaf" command (with an 'f'). Must end with a newline.
HX_DESC="hx [linenum]: Delete history item at line number\n"
hx() {
history -d "$1"
}
.
:<<COMMENT
Deletes all lines from the history that match a search string, with a
prompt. The history file is then reloaded into memory.
Short description: Stored in HXF_DESC
Examples
hxf "rm -rf"
hxf ^source
Parameter is required, and must be at least one non-whitespace character.
With the following setting, this is *not* added to the history:
export HISTIGNORE="*hxf *"
- https://superuser.com/questions/232885/can-you-share-wisdom-on-using-histignore-in-bash
See:
- https://unix.stackexchange.com/questions/57924/how-to-delete-commands-in-history-matching-a-given-string
COMMENT
#HXF_DESC: For "aliaf" command (with an 'f'). Must end with a newline.
HXF_DESC="hxf [searchterm]: Delete all history items matching search term, with y/n prompt\n"
hxf() {
#Exit if no parameter is provided (if it's the empty string)
param=$(echo "$1" | trim)
echo "$param"
if [ -z "$param" ] #http://tldp.org/LDP/abs/html/comparison-ops.html
then
echo "Required parameter missing. Cancelled"; return
fi
read -r -p "About to delete all items from history that match \"$param\". Are you sure? [y/N] " response
response=${response,,} # tolower
if [[ $response =~ ^(yes|y)$ ]]
then
#Delete all matched items from the file, and duplicate it to a temp
#location.
grep -v "$param" "$HISTFILE" > /tmp/history
#Clear all items in the current sessions history (in memory). This
#empties out $HISTFILE.
history -c
#Overwrite the actual history file with the temp one.
mv /tmp/history "$HISTFILE"
#Now reload it.
history -r "$HISTFILE" #Alternative: exec bash
else
echo "Cancelled."
fi
}
References:
Trimming whitespace from strings: How to trim whitespace from a Bash variable?
Actual line breaks: https://stackoverflow.com/a/4296147/2736496
Alias w/param: https://stackoverflow.com/a/7131683/2736496 (another answer in this question)
HISTIGNORE: https://superuser.com/questions/232885/can-you-share-wisdom-on-using-histignore-in-bash
Y/N prompt: https://stackoverflow.com/a/3232082/2736496
Delete all matching items from history: https://unix.stackexchange.com/questions/57924/how-to-delete-commands-in-history-matching-a-given-string
Is string null/empty: http://tldp.org/LDP/abs/html/comparison-ops.html

Respectfully to all those saying you can't insert a parameter in the middle of an alias I just tested it and found that it did work.
alias mycommand = "python3 "$1" script.py --folderoutput RESULTS/"
when I then ran mycommand foobar it worked exactly as if I had typed the command out longhand.

NB: In case the idea isn't obvious, it is a bad idea to use aliases for anything but aliases, the first one being the 'function in an alias' and the second one being the 'hard to read redirect/source'. Also, there are flaws (which i thought would be obvious, but just in case you are confused: I do not mean them to actually be used... anywhere!)
I've answered this before, and it has always been like this in the past:
alias foo='__foo() { unset -f $0; echo "arg1 for foo=$1"; }; __foo()'
which is fine and good, unless you are avoiding the use of functions all together. in which case you can take advantage of bash's vast ability to redirect text:
alias bar='cat <<< '\''echo arg1 for bar=$1'\'' | source /dev/stdin'
They are both about the same length give or take a few characters.
The real kicker is the time difference, the top being the 'function method' and the bottom being the 'redirect-source' method. To prove this theory, the timing speaks for itself:
arg1 for foo=FOOVALUE
real 0m0.011s user 0m0.004s sys 0m0.008s # <--time spent in foo
real 0m0.000s user 0m0.000s sys 0m0.000s # <--time spent in bar
arg1 for bar=BARVALUE
ubuntu#localhost /usr/bin# time foo FOOVALUE; time bar BARVALUE
arg1 for foo=FOOVALUE
real 0m0.010s user 0m0.004s sys 0m0.004s
real 0m0.000s user 0m0.000s sys 0m0.000s
arg1 for bar=BARVALUE
ubuntu#localhost /usr/bin# time foo FOOVALUE; time bar BARVALUE
arg1 for foo=FOOVALUE
real 0m0.011s user 0m0.000s sys 0m0.012s
real 0m0.000s user 0m0.000s sys 0m0.000s
arg1 for bar=BARVALUE
ubuntu#localhost /usr/bin# time foo FOOVALUE; time bar BARVALUE
arg1 for foo=FOOVALUE
real 0m0.012s user 0m0.004s sys 0m0.004s
real 0m0.000s user 0m0.000s sys 0m0.000s
arg1 for bar=BARVALUE
ubuntu#localhost /usr/bin# time foo FOOVALUE; time bar BARVALUE
arg1 for foo=FOOVALUE
real 0m0.010s user 0m0.008s sys 0m0.004s
real 0m0.000s user 0m0.000s sys 0m0.000s
arg1 for bar=BARVALUE
This is the bottom part of about 200 results, done at random intervals. It seems that function creation/destruction takes more time than redirection. Hopefully this will help future visitors to this question (didn't want to keep it to myself).

If you're looking for a generic way to apply all params to a function, not just one or two or some other hardcoded amount, you can do that this way:
#!/usr/bin/env bash
# you would want to `source` this file, maybe in your .bash_profile?
function runjar_fn(){
java -jar myjar.jar "$#";
}
alias runjar=runjar_fn;
So in the example above, i pass all parameters from when i run runjar to the alias.
For example, if i did runjar hi there it would end up actually running java -jar myjar.jar hi there. If i did runjar one two three it would run java -jar myjar.jar one two three.
I like this $# - based solution because it works with any number of params.

There are legitimate technical reasons to want a generalized solution to the problem of bash alias not having a mechanism to take a reposition arbitrary arguments. One reason is if the command you wish to execute would be adversely affected by the changes to the environment that result from executing a function. In all other cases, functions should be used.
What recently compelled me to attempt a solution to this is that I wanted to create some abbreviated commands for printing the definitions of variables and functions. So I wrote some functions for that purpose. However, there are certain variables which are (or may be) changed by a function call itself. Among them are:
FUNCNAME
BASH_SOURCE
BASH_LINENO
BASH_ARGC
BASH_ARGV
The basic command I had been using (in a function) to print variable defns. in the form output by the set command was:
sv () { set | grep --color=never -- "^$1=.*"; }
E.g.:
> V=voodoo
sv V
V=voodoo
Problem: This won't print the definitions of the variables mentioned above as they are in the current context, e.g., if in an interactive shell prompt (or not in any function calls), FUNCNAME isn't defined. But my function tells me the wrong information:
> sv FUNCNAME
FUNCNAME=([0]="sv")
One solution I came up with has been mentioned by others in other posts on this topic. For this specific command to print variable defns., and which requires only one argument, I did this:
alias asv='(grep -- "^$(cat -)=.*" <(set)) <<<'
Which gives the correct output (none), and result status (false):
> asv FUNCNAME
> echo $?
1
However, I still felt compelled to find a solution that works for arbitrary numbers of arguments.
A General Solution To Passing Arbitrary Arguments To A Bash Aliased Command:
# (I put this code in a file "alias-arg.sh"):
# cmd [arg1 ...] – an experimental command that optionally takes args,
# which are printed as "cmd(arg1 ...)"
#
# Also sets global variable "CMD_DONE" to "true".
#
cmd () { echo "cmd($#)"; declare -g CMD_DONE=true; }
# Now set up an alias "ac2" that passes to cmd two arguments placed
# after the alias, but passes them to cmd with their order reversed:
#
# ac2 cmd_arg2 cmd_arg1 – calls "cmd" as: "cmd cmd_arg1 cmd_arg2"
#
alias ac2='
# Set up cmd to be execed after f() finishes:
#
trap '\''cmd "${CMD_ARGV[1]}" "${CMD_ARGV[0]}"'\'' SIGUSR1;
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
# (^This is the actually execed command^)
#
# f [arg0 arg1 ...] – acquires args and sets up trap to run cmd:
f () {
declare -ag CMD_ARGV=("$#"); # array to give args to cmd
kill -SIGUSR1 $$; # this causes cmd to be run
trap SIGUSR1; # unset the trap for SIGUSR1
unset CMD_ARGV; # clean up env...
unset f; # incl. this function!
};
f' # Finally, exec f, which will receive the args following "ac2".
E.g.:
> . alias-arg.sh
> ac2 one two
cmd(two one)
>
> # Check to see that command run via trap affects this environment:
> asv CMD_DONE
CMD_DONE=true
A nice thing about this solution is that all the special tricks used to handle positional parameters (arguments) to commands will work when composing the trapped command. The only difference is that array syntax must be used.
E.g.,
If you want "$#", use "${CMD_ARGV[#]}".
If you want "$#", use "${#CMD_ARGV[#]}".
Etc.

I will just post my (hopefully, okay) solution
(for future readers, & most vitally; editors).
So - please edit & improve/remove anything in this post.
In the terminal:
$ alias <name_of_your_alias>_$argname="<command> $argname"
and to use it (notice the space after '_':
$<name_of_your_alias>_ $argname
for example, a alias to cat a file called hello.txt:
(alias name is CAT_FILE_)
and the $f (is the $argname, which is a file in this example)
$ alias CAT_FILE_$f="cat $f"
$ echo " " >> hello.txt
$ echo "hello there!" >> hello.txt
$ echo " " >> hello.txt
$ cat hello.txt
hello there!
Test (notice the space after '_'):
CAT_FILE_ hello.txt

As has already been pointed out by others, using a function should be considered best practice.
However, here is another approach, leveraging xargs:
alias junk="xargs -I "{}" -- mv "{}" "~/.Trash" <<< "
Note that this has side effects regarding redirection of streams.

Solution with subcommands:
d () {
if [ $# -eq 0 ] ; then
docker
return 0
fi
CMD=$1
shift
case $CMD in
p)
docker ps --all $#
;;
r)
docker run --interactive --tty $#
;;
rma)
docker container prune
docker image prune --filter "dangling=true"
;;
*)
docker $CMD $#
;;
esac
return $?
}
Using:
$ d r my_image ...
Called:
docker run --interactive --tty my_image ...

Here's the example:
alias gcommit='function _f() { git add -A; git commit -m "$1"; } ; _f'
Very important:
There is a space after { and before }.
There is a ; after each command in sequence. If you forget this after the last command, you will see > prompt instead!
The argument is enclosed in quotes as "$1"

To give specific answer to the Question posed about creating the alias to move the files to Trash folder instead of deleting them:
alias rm="mv "$1" -t ~/.Trash/"
Offcourse you have to create dir ~/.Trash first.
Then just give following command:
$rm <filename>
$rm <dirname>

Here is another approach using read. I am using this for brute search of a file by its name fragment, ignoring the "permission denied" messages.
alias loc0='( IFS= read -r x; find . -iname "*" -print 2>/dev/null | grep $x;) <<<'
A simple example:
$ ( IFS= read -r x; echo "1 $x 2 ";) <<< "a b"
1 a b 2
Note, that this converts the argument as a string into variable(s). One could use several parameters within quotes for this, space separated:
$ ( read -r x0 x1; echo "1 ${x0} 2 ${x1} 3 ";) <<< "a b"
1 a 2 b 3

Functions are indeed almost always the answer as already amply contributed and confirmed by this quote from the man page: "For almost every purpose, aliases are superseded by shell functions."
For completeness and because this can be useful (marginally more lightweight syntax) it could be noted that when the parameter(s) follow the alias, they can still be used (although this wouldn't address the OP's requirement). This is probably easiest to demonstrate with an example:
alias ssh_disc='ssh -O stop'
allows me to type smth like ssh_disc myhost, which gets expanded as expected as: ssh -O stop myhost
This can be useful for commands which take complex arguments (my memory isn't what it use t be anymore...)

For taking parameters, you should use functions!
However $# get interpreted when creating the alias instead of during the execution of the alias and escaping the $ doesn’t work either. How do I solve this problem?
You need to use shell function instead of an alias to get rid of this problem. You can define foo as follows:
function foo() { /path/to/command "$#" ;}
OR
foo() { /path/to/command "$#" ;}
Finally, call your foo() using the following syntax:
foo arg1 arg2 argN
Make sure you add your foo() to ~/.bash_profile or ~/.zshrc file.
In your case, this will work
function trash() { mv $# ~/.Trash; }

Both functions and aliases can use parameters as others have shown here. Additionally, I would like to point out a couple of other aspects:
1. function runs in its own scope, alias shares scope
It may be useful to know this difference in cases you need to hide or expose something. It also suggests that a function is the better choice for encapsulation.
function tfunc(){
GlobalFromFunc="Global From Func" # Function set global variable by default
local FromFunc="onetwothree from func" # Set a local variable
}
alias talias='local LocalFromAlias="Local from Alias"; GlobalFromAlias="Global From Alias" # Cant hide a variable with local here '
# Test variables set by tfunc
tfunc # call tfunc
echo $GlobalFromFunc # This is visible
echo $LocalFromFunc # This is not visible
# Test variables set by talias
# call talias
talias
echo $GlobalFromAlias # This is invisible
echo $LocalFromAlias # This variable is unset and unusable
Output:
bash-3.2$ # Test variables set by tfunc
bash-3.2$ tfunc # call tfunc
bash-3.2$ echo $GlobalFromFunc # This is visible
Global From Func
bash-3.2$ echo $LocalFromFunc # This is not visible
bash-3.2$ # Test variables set by talias
bash-3.2$ # call talias
bash-3.2$ talias
bash: local: can only be used in a function
bash-3.2$ echo $GlobalFromAlias # This is invisible
Global From Alias
bash-3.2$ echo $LocalFromAlias # This variable is unset and unusable
2. wrapper script is a better choice
It has happened to me several times that an alias or function can not be found when logging in via ssh or involving switching usernames or multi-user environment. There are tips and tricks with sourcing dot files, or this interesting one with alias: alias sd='sudo ' lets this subsequent alias alias install='sd apt-get install' work as expect (notice the extra space in sd='sudo '). However, a wrapper script works better than a function or alias in cases like this. The main advantage with a wrapper script is that it is visible/executable for under intended path (i.e. /usr/loca/bin/) where as a function/alias needs to be sourced before it is usable. For example, you put a function in a ~/.bash_profile or ~/.bashrc for bash, but later switch to another shell (i.e. zsh) then the function is not visible anymore.
So, when you are in doubt, a wrapper script is always the most reliable and portable solution.

alias junk="delay-arguments mv _ ~/.Trash"
delay-arguments script:
#!/bin/bash
# Example:
# > delay-arguments echo 1 _ 3 4 2
# 1 2 3 4
# > delay-arguments echo "| o n e" _ "| t h r e e" "| f o u r" "| t w o"
# | o n e | t w o | t h r e e | f o u r
RAW_ARGS=("$#")
ARGS=()
ARG_DELAY_MARKER="_"
SKIPPED_ARGS=0
SKIPPED_ARG_NUM=0
RAW_ARGS_COUNT="$#"
for ARG in "$#"; do
#echo $ARG
if [[ "$ARG" == "$ARG_DELAY_MARKER" ]]; then
SKIPPED_ARGS=$((SKIPPED_ARGS+1))
fi
done
for ((I=0; I<$RAW_ARGS_COUNT-$SKIPPED_ARGS; I++)); do
ARG="${RAW_ARGS[$I]}"
if [[ "$ARG" == "$ARG_DELAY_MARKER" ]]; then
MOVE_SOURCE_ARG_NUM=$(($RAW_ARGS_COUNT-$SKIPPED_ARGS+$SKIPPED_ARG_NUM))
MOVING_ARG="${RAW_ARGS[$MOVE_SOURCE_ARG_NUM]}"
if [[ "$MOVING_ARG" == "$ARG_DELAY_MARKER" ]]; then
echo "Error: Not enough arguments!"
exit 1;
fi
#echo "Moving arg: $MOVING_ARG"
ARGS+=("$MOVING_ARG")
SKIPPED_ARG_NUM=$(($SKIPPED_ARG_NUM+1))
else
ARGS+=("$ARG")
fi
done
#for ARG in "${ARGS[#]}"; do
#echo "ARGN: $ARG"
#done
#echo "RAW_ARGS_COUNT: $RAW_ARGS_COUNT"
#echo "SKIPPED_ARGS: $SKIPPED_ARGS"
#echo "${ARGS[#]}"
QUOTED_ARGS=$(printf ' %q' "${ARGS[#]}")
eval "${QUOTED_ARGS[#]}"

Related

how to redirect command output in bash shell?

I'm having a problem writing a small bash command. Basically I want to echo the wrapper command and redirect the output of the real command to a log file.
Something like this in my .bashrc doesn't work -- the output still gets to the console.
cmd="some_command >& output.log";
echo $cmd;
$cmd;
But the following works -- the output is directed into the log file.
cmd = "some_command";
echo $cmd" >& output.log";
$cmd >& output.log;
What is wrong with the first method? How to fix it?
Thanks!
Using eval works, but is bad practice for security reasons. The Right Thing, when you need to perform redirections inside code stored for reuse, is to define a function:
cmd() { some_command &> output.log; } # define it
declare -p cmd # print it
cmd # run it
If you don't need redirections, then the right thing is an array:
cmd=( something 'with spaces' 'in args' ) # define it
printf '%q ' "${cmd[#]}"; echo # print it
"${cmd[#]}" # run it
This is safer, inasmuch as array contents won't go through a full eval pass. Think about if you did cmd="something-with $filename", and filename contained $(rm -rf /). If you used eval, this would run the rm command!
To provide a more specific example, this would hose your system if run as root:
# !!! I AM DANGEROUS DO NOT RUN ME !!!
evil_filename='/tmp/foo $(rm -rf /)'
cmd="echo $evil_filename" # define it (BROKEN!)
eval "$cmd" # run it (DANGEROUS!)
On the other hand, this would be safe:
evil_filename='/tmp/foo $(rm -rf /)'
cmd=( echo "$evil_filename" ) # define it (OK!)
printf '%q ' "${cmd[#]}"; echo # print it (OK!)
"${cmd[#]}" # run it (OK!)
...and it would still be safe even if you left out some of the quotes -- it would work wrong, but still not break your system:
# I'm broken, but not in a way that damages system security
evil_filename='/tmp/foo $(rm -rf /)'
cmd=( echo $evil_filename ) # define it (BROKEN!)
${cmd[#]} # run it (BROKEN!)
And this would be safe too:
evil_filename='/tmp/foo $(rm -rf /)'
cmd() { echo "$1"; } # define it (OK!)
cmd "$evil_filename" # run it (OK!)
For a more in-depth discussion, see BashFAQ #50 (on properly storing command sequences for reuse), and BashFAQ #48 (on why eval is dangerous).
What is wrong with the first method?
When you include the redirection operators within a variable, the shell doesn't treat those as special. Instead those are considered as arguments to the program in question.
One solution is to make use of eval:
cmd="some command >& output.log";
eval $cmd;
As an aside, the following is wrong:
cmd = "some command";
You cannot have spaces around = in a variable assignment.

I keep getting a 'while syntax' error on the output of the at job in unix and I have no idea why

#!/usr/dt/bin/dtksh
while getopts w:m: option
do
case $option in
w) wflag=1
wval="$OPTARG";;
m) mflag=1
mval="$OPTARG";;
?) printf 'BAD\n' $0
exit 2;;
esac
done
if [ ! -z "$wflag" ]; then
printf "W and -w arg is $wval\n"
fi
if [ ! -z "$mflag" ]; then
printf "M and -m arg is $mval\n"
fi
shift $(($OPTIND - 1))
printf "Remaining arguments are: $* \n"
at $wval <<ENDMARKER
echo $* >> Search_List
tr " " "\n" <Search_List >Usr_List
while true; do
if [ -s Usr_List ]; then
for i in $(cat Usr_List); do
if finger -m | grep $i; then
echo '$i is online' | elm user
sed '/$i/d' <Usr_List >tmplist
mv tmplist Usr_List
fi
done
else
break
fi
done
ENDMARKER
Essentially I want to keep searching through until it is empty. Each time an element of the list is found, it is deleted. Once the list is empty quit.
There are no error messages when I first run the command, it only shows up in an email containing the output of the at job.
Thanks in advance for any advice
EDIT: The script uses getopts and takes one argument for -w and one for -m, the w value is set as the time for the at job, the m still has to be used. Any arguments after the one for m are sent to a file called Search_List, Search_List is edited and saved as Usr_List. Then in the while loop, while Usr_List is not empty, the script checks the results of finger -m against the names in Usr_List. If a name is found, it is removed from Usr_List. Once Usr_List is empty, the program should stop.
elm is a way to send an email, so elm user sends an email to user.
The error is :
while: Expression syntax
at uses /bin/sh by default.
at now <<ENDMARKER
<code here>
ENDMARKER
All of this executes under /bin/sh, which on some systems can be Bourne Shell (Solaris for example).
You need to figure out what /bin/sh is for your system, then modify things accordingly. Plus, read the gurantees about what is and what is not in your "at" environment. I think the problem lies there. You have both UNIX and linux tags. So I cannot give a lot more help than that.
You can enable logging -- the way YOU need it -- of the at code chunk:
exec 2&>1 > /tmp/somefile.log
Then write debugging messages to stdout or stderr.
Your HEREDOC is being interpolated. Try quoting the delimiter:
at $wval << 'ENDMARKER'
Although ( I haven't looked closely) it appears that you want some interpolation. But you definitely do not want it on the line in which you reference $i, so quote that $ if you do not quote the entire heredoc:
if finger -m | grep \$i; then
You need to pass the -k option to at:
...
at -k $wval <<ENDMARKER
...
at is otherwise defaulting to your login shell which is csh or one of its derivatives.
It turns out that the while command and the if command needed to be combined.
while [[ -s Usr_List ]]; do
......
done

How can I preserve quotes in printing a bash script's arguments

I am making a bash script that will print and pass complex arguments to another external program.
./script -m root#hostname,root#hostname -o -q -- 'uptime ; uname -a'
How do I print the raw arguments as such:
-m root#hostname,root#hostname -o -q -- 'uptime ; uname -a'
Using $# and $* removes the single quotes around uptime ; uname -a which could cause undesired results. My script does not need to parse each argument. I just need to print / log the argument string and pass them to another program exactly how they are given.
I know I can escape the quotes with something like "'uptime ; uname -a'" but I cannot guarantee the user will do that.
The quotes are removed before the arguments are passed to your script, so it's too late to preserve them. What you can do is preserve their effect when passing the arguments to the inner command, and reconstruct an equivalent quoted/escaped version of the arguments for printing.
For passing the arguments to the inner command "$#" -- with the double-quotes, $# preserves the original word breaks, meaning that the inner command receives exactly the same argument list that your script did.
For printing, you can use the %q format in bash's printf command to reconstruct the quoting. Note that this won't always reconstruct the original quoting, but will construct an equivalent quoted/escaped string. For example, if you passed the argument 'uptime ; uname -a' it might print uptime\ \;\ uname\ -a or "uptime ; uname -a" or any other equivalent (see #William Pursell's answer for similar examples).
Here's an example of using these:
printf "Running command:"
printf " %q" innercmd "$#" # note the space before %q -- this inserts spaces between arguments
printf "\n"
innercmd "$#"
If you have bash version 4.4 or later, you can use the #Q modifier on parameter expansions to add quoting. This tends to prefer using single-quotes (as opposed to printf %q's preference for escapes). You can combine this with $* to get a reasonable result:
echo "Running command: innercmd ${*#Q}"
innercmd "$#"
Note that $* mashes all arguments together into a single string with whitespace between them, which is normally not useful, but in this case each argument is individually quoted so the result is actually what you (probably) want. (Well, unless you changed IFS, in which case the "whitespace" between arguments will be the first character of $IFS, which may not be what you want.)
Use ${##Q} for a simple solution. To test put the lines below in a script bigQ.
#!/bin/bash
line="${##Q}"
echo $line
./bigQ 1 a "4 5" b="6 7 8"
'1' 'a' '4 5' 'b=6 7 8'
If the user invokes your command as:
./script 'foo'
the first argument given to the script is the string foo without the quotes. There is no way for your script to differentiate between that and any of the other methods by which it could get foo as an argument (eg ./script $(echo foo) or ./script foo or ./script "foo" or ./script \f\o""''""o).
If you want to print the argument list as close as possible to what the user probably entered:
#!/bin/bash
chars='[ !"#$&()*,;<>?\^`{|}]'
for arg
do
if [[ $arg == *"'"* ]]
then
arg=\""$arg"\"
elif [[ $arg == *$chars* ]]
then
arg="'$arg'"
fi
allargs+=("$arg") # ${allargs[#]} is to be used only for printing
done
printf '%s\n' "${allargs[*]}"
It's not perfect. An argument like ''\''"' is more difficult to accommodate than is justified.
As someone else already mentioned, when you access the arguments inside of your script, it's too late to know which arguments were quote when it was called. However, you can re-quote the arguments that contain spaces or other special characters that would need to be quoted to be passed as parameters.
Here is a Bash implementation based on Python's shlex.quote(s) that does just that:
function quote() {
declare -a params
for param; do
if [[ -z "${param}" || "${param}" =~ [^A-Za-z0-9_#%+=:,./-] ]]; then
params+=("'${param//\'/\'\"\'\"\'}'")
else
params+=("${param}")
fi
done
echo "${params[*]}"
}
Your example slightly changed to show empty arguments:
$ quote -m root#hostname,root#hostname -o -q -- 'uptime ; uname -a' ''
-m root#hostname,root#hostname -o -q -- 'uptime ; uname -a' ''
In my case, I have tried to call the bash like script --argument="--arg-inner=1 --arg-inner2".
Unfortunately any solution upper don't help in my case.
Definitive solution was
#!/bin/bash
# Fix given array argument quotation
function quote() {
local QUOTED_ARRAY=()
for ARGUMENT; do
case ${ARGUMENT} in
--*=*)
QUOTED_ARRAY+=( "${ARGUMENT%%=*}=$(printf "%q" "${ARGUMENT#*=}")" )
shift
;;
*)
QUOTED_ARRAY+=( "$(printf " %q" "${ARGUMENT}")" )
;;
esac
done
echo ${QUOTED_ARRAY[#]}
}
ARGUMENTS="$(quote "${#}")"
echo "${ARGUMENTS}"
The result in the case of MacOS is --argument=--arg-inner=1\ --arg-inner2 which is logically the same.
Just separate each argument using quotes, and the nul character:
#! /bin/bash
sender () {
printf '"%s"\0' "$#"
}
receiver () {
readarray -d '' args < <(function "$#")
}
receiver "$#"
As commented by Charles Duffy.

Parameter list with double quotes does not pass through properly in Bash

I have a Bash script that calls another Bash script. The called script does some modification and checking on a few things, shifts, and then passes the rest of the caller's command line through.
In the called script, I have verified that I have everything managed and ready to call. Here's some debug-style code I've put in:
echo $SVN $command $# > /tmp/shimcmd
bash /tmp/shimcmd
$SVN $command $#
Now, in /tmp/shimcmd you'll see:
svn commit --username=myuser --password=mypass --non-interactive --trust-server-cert -m "Auto Update autocommit Wed Apr 11 17:33:37 CDT 2012"
That is, the built command, all on one line, perfectly fine, including a -m "my string with spaces" portion.
It's perfect. And the "bash /tmp/shimcmd" execution of it works perfectly as well.
But of course I don't want this silly tmp file and such (only used it to debug). The problem is that calling the command directly, instead of via the shim file:
$SVN $command $#
results in the svn command itself NOT receiving the quoted string with spaces--it garbles the '-m "my string with spaces"' parameter and shanks the command as if it was passed as '-m my string with spaces'.
I have tried all manner of crazy escape methods to no avail. Can't believe it's dogging me this badly. Again, by echoing the very same thing ($SVN $command $#) to a file and then executing that file, it's FINE. But calling directly garbles the quoted string. That element alone shanks.
Any ideas?
Dan
Did you try:
eval "$SVN $command $#"
?
Here's a way to demonstrate the problem:
$ args='-m "foo bar"'
$ printf '<%s> ' $args
<-m> <"foo> <bar">
And here's a way to avoid it:
$ args=( -m "foo bar" )
$ printf '<%s> ' "${args[#]}"
<-m> <foo bar>
In this latter case, args is an array, not a quoted string.
Note, by the way, that it has to be "$#", not $#, to get this behavior (in which string-splitting is avoided in favor of respecting the array entries' boundaries).
this
echo -n -e $SVN \"$command\" > /tmp/shimcmd
for x in "$#"
do
a=$a" "\"$x\"
done
echo -e " " $a >> /tmp/shimcmd
bash /tmp/shimcmd
or simply
$SVN "$command" "$#"

How can I store a command in a variable in a shell script?

I would like to store a command to use at a later time in a variable (not the output of the command, but the command itself).
I have a simple script as follows:
command="ls";
echo "Command: $command"; #Output is: Command: ls
b=`$command`;
echo $b; #Output is: public_html REV test... (command worked successfully)
However, when I try something a bit more complicated, it fails. For example, if I make
command="ls | grep -c '^'";
The output is:
Command: ls | grep -c '^'
ls: cannot access |: No such file or directory
ls: cannot access grep: No such file or directory
ls: cannot access '^': No such file or directory
How could I store such a command (with pipes/multiple commands) in a variable for later use?
Use eval:
x="ls | wc"
eval "$x"
y=$(eval "$x")
echo "$y"
Do not use eval! It has a major risk of introducing arbitrary code execution.
BashFAQ-50 - I'm trying to put a command in a variable, but the complex cases always fail.
Put it in an array and expand all the words with double-quotes "${arr[#]}" to not let the IFS split the words due to Word Splitting.
cmdArgs=()
cmdArgs=('date' '+%H:%M:%S')
and see the contents of the array inside. The declare -p allows you see the contents of the array inside with each command parameter in separate indices. If one such argument contains spaces, quoting inside while adding to the array will prevent it from getting split due to Word-Splitting.
declare -p cmdArgs
declare -a cmdArgs='([0]="date" [1]="+%H:%M:%S")'
and execute the commands as
"${cmdArgs[#]}"
23:15:18
(or) altogether use a bash function to run the command,
cmd() {
date '+%H:%M:%S'
}
and call the function as just
cmd
POSIX sh has no arrays, so the closest you can come is to build up a list of elements in the positional parameters. Here's a POSIX sh way to run a mail program
# POSIX sh
# Usage: sendto subject address [address ...]
sendto() {
subject=$1
shift
first=1
for addr; do
if [ "$first" = 1 ]; then set --; first=0; fi
set -- "$#" --recipient="$addr"
done
if [ "$first" = 1 ]; then
echo "usage: sendto subject address [address ...]"
return 1
fi
MailTool --subject="$subject" "$#"
}
Note that this approach can only handle simple commands with no redirections. It can't handle redirections, pipelines, for/while loops, if statements, etc
Another common use case is when running curl with multiple header fields and payload. You can always define args like below and invoke curl on the expanded array content
curlArgs=('-H' "keyheader: value" '-H' "2ndkeyheader: 2ndvalue")
curl "${curlArgs[#]}"
Another example,
payload='{}'
hostURL='http://google.com'
authToken='someToken'
authHeader='Authorization:Bearer "'"$authToken"'"'
now that variables are defined, use an array to store your command args
curlCMD=(-X POST "$hostURL" --data "$payload" -H "Content-Type:application/json" -H "$authHeader")
and now do a proper quoted expansion
curl "${curlCMD[#]}"
var=$(echo "asdf")
echo $var
# => asdf
Using this method, the command is immediately evaluated and its return value is stored.
stored_date=$(date)
echo $stored_date
# => Thu Jan 15 10:57:16 EST 2015
# (wait a few seconds)
echo $stored_date
# => Thu Jan 15 10:57:16 EST 2015
The same with backtick
stored_date=`date`
echo $stored_date
# => Thu Jan 15 11:02:19 EST 2015
# (wait a few seconds)
echo $stored_date
# => Thu Jan 15 11:02:19 EST 2015
Using eval in the $(...) will not make it evaluated later:
stored_date=$(eval "date")
echo $stored_date
# => Thu Jan 15 11:05:30 EST 2015
# (wait a few seconds)
echo $stored_date
# => Thu Jan 15 11:05:30 EST 2015
Using eval, it is evaluated when eval is used:
stored_date="date" # < storing the command itself
echo $(eval "$stored_date")
# => Thu Jan 15 11:07:05 EST 2015
# (wait a few seconds)
echo $(eval "$stored_date")
# => Thu Jan 15 11:07:16 EST 2015
# ^^ Time changed
In the above example, if you need to run a command with arguments, put them in the string you are storing:
stored_date="date -u"
# ...
For Bash scripts this is rarely relevant, but one last note. Be careful with eval. Eval only strings you control, never strings coming from an untrusted user or built from untrusted user input.
For bash, store your command like this:
command="ls | grep -c '^'"
Run your command like this:
echo $command | bash
Not sure why so many answers make it complicated!
use alias [command] 'string to execute'
example:
alias dir='ls -l'
./dir
[pretty list of files]
I tried various different methods:
printexec() {
printf -- "\033[1;37m$\033[0m"
printf -- " %q" "$#"
printf -- "\n"
eval -- "$#"
eval -- "$*"
"$#"
"$*"
}
Output:
$ printexec echo -e "foo\n" bar
$ echo -e foo\\n bar
foon bar
foon bar
foo
bar
bash: echo -e foo\n bar: command not found
As you can see, only the third one, "$#" gave the correct result.
I faced this problem with the following command:
awk '{printf "%s[%s]\n", $1, $3}' "input.txt"
I need to build this command dynamically:
The target file name input.txt is dynamic and may contain space.
The awk script inside {} braces printf "%s[%s]\n", $1, $3 is dynamic.
Challenge:
Avoid extensive quote escaping logic if there are many " inside the awk script.
Avoid parameter expansion for every $ field variable.
The solutions bellow with eval command and associative arrays do not work. Due to bash variable expansions and quoting.
Solution:
Build bash variable dynamically, avoid bash expansions, use printf template.
# dynamic variables, values change at runtime.
input="input file 1.txt"
awk_script='printf "%s[%s]\n" ,$1 ,$3'
# static command template, preventing double-quote escapes and avoid variable expansions.
awk_command=$(printf "awk '{%s}' \"%s\"\n" "$awk_script" "$input")
echo "awk_command=$awk_command"
awk_command=awk '{printf "%s[%s]\n" ,$1 ,$3}' "input file 1.txt"
Executing variable command:
bash -c "$awk_command"
Alternative that also works
bash << $awk_command
As you don't specify any scripting language, I would recommand tcl, the Tool Command Language for this kind of purpose.
Then in the first line, add the appropriate shebang:
#!/usr/local/bin/tclsh
with appropriate location you can retrieve with which tclsh.
In tcl scripts, you can call operating system commands with exec.
#!/bin/bash
#Note: this script works only when u use Bash. So, don't remove the first line.
TUNECOUNT=$(ifconfig |grep -c -o tune0) #Some command with "Grep".
echo $TUNECOUNT #This will return 0
#if you don't have tune0 interface.
#Or count of installed tune0 interfaces.
First of all, there are functions for this. But if you prefer variables then your task can be done like this:
$ cmd=ls
$ $cmd # works
file file2 test
$ cmd='ls | grep file'
$ $cmd # not works
ls: cannot access '|': No such file or directory
ls: cannot access 'grep': No such file or directory
file
$ bash -c $cmd # works
file file2 test
$ bash -c "$cmd" # also works
file
file2
$ bash <<< $cmd
file
file2
$ bash <<< "$cmd"
file
file2
Or via a temporary file
$ tmp=$(mktemp)
$ echo "$cmd" > "$tmp"
$ chmod +x "$tmp"
$ "$tmp"
file
file2
$ rm "$tmp"
Be careful registering an order with the: X=$(Command)
This one is still executed. Even before being called. To check and confirm this, you can do:
echo test;
X=$(for ((c=0; c<=5; c++)); do
sleep 2;
done);
echo note the 5 seconds elapsed
It is not necessary to store commands in variables even as you need to use it later. Just execute it as per normal. If you store in variables, you would need some kind of eval statement or invoke some unnecessary shell process to "execute your variable".

Resources