Why does this one-line function work in zsh but not bash, and how can I fix it for bash? - linux

I figured out the answer as I was researching how to phrase the question, but it may be useful for somebody. I use zsh and both my .zshrc and my .bashrc have source $HOME/.alias in them. Here is the old, problematic ~/.alias
rl() { grep -v "$1" "$2" > /tmp/rl.txt && mv /tmp/rl.txt "$2" }
...
It worked fine in zsh, but in bash I got an unexpected end of file error, and rl was undefined.

The fixed ~/.alias that works in both zsh and bash is
rl() { grep -v "$1" "$2" > /tmp/rl.txt && mv /tmp/rl.txt "$2"; }
...
Bash just requires a semicolon after the last command in a one-line function declaration.

You can use sed in the function to avoid making a copy of the file and then moving it over the original file.
rl() { sed -i -e "/$1/d" "$2"; }
The -i flag edits file file in-place, and makes a backup if you supply an extension. With no extension it just edits in-place.

Related

How do I write a one-liner cd command for the following case?

I have 2 directories
testing_dir
testing_dir_win
So I need to cd to testing_dir. But here is the case
the directories can be
testing_dir or testing_dir-2.1.0
testing_dir_win or testing_dir_win-1.3.0
and my script should only take testing_dir or testing_dir-2.1.0 (based on which is available)
I have the long way of writing it:
str=`ls folder_name|grep ^testing_dir`
arr=(${str//" "/ })
ret=""
for i in "${arr[#]}"
do
if [[ $i != *"testing_dir_win"* ]] ; then
ret=$i
fi
done
but is there a one-liner for this problem? something like cd testing_dir[\-]?(This doesn't work by the way).
If your script contains
shopt -s extglob
you can use:
cd testing_dir?(-[[:digit:]]*) || exit
...if you have a guarantee that only one match will exist.
Without that guarantee, you can directly set
arr=( testing_dir?(-[[:digit:]]*) )
cd "${arr[0]}" || exit
use command with grep filters:
cd `ls | grep -w testing_dir`
this command will match the testing_dir directory without worrying for version.
P.S in case of many versions it will go inside the earliest version so add "head -1, tail -1" according to your usecase

'command not found' when passing variable into BASH function, am I quoting incorrectly?

So I have a command that basically adds a line to a file, but only if that line isn't already in the file. It uses grep to check the file and if not there then appends to the file.
The purpose of this is because I want to import all my aliases into BASH from an installation script that is likely to be executed more than once (and I don't want to fill ~/.bashrc with duplicate lines of the same alias).
The command works fine by itself, but when I try to abstract it away into a function to reuse elsewhere, I get the error: command not found.
So far I've looked at grep and pattern matches (thinking maybe & or ~ was throwing it off), parameter expansion OR command expansion and quoting.
I feel it's the latter i.e. I'm not quoting the alias string or file string correctly and it's trying to execute it as a command instead of a string.
I've been pulling my hair out for a while on this one, would somebody please be able to point me in the right direction?
Any help appreciated!
# Command (which works)
grep -qxF 'alias gs="clear && git status"' ~/.bashrc || echo 'alias gs="clear && git status"' >> ~/.bashrc
# Wrap command in function so I can reuse and pass in different parameters
function append_unique_line {
grep -qxF $1 $2 || echo $1 >> $2
}
# Doesn't work.. append_unique_line: command not found
append_unique_line 'alias gs="clear && git status"' ~/.bashrc
Try
function append_unique_line() {
grep -qxF "$1" "$2" || echo "$1" >> "$2"
}
append_unique_line 'alias gs="clear && git status"' ~/.bashrc
Always wrap your variable in " for expansion.

Shell remove pattern from filename

I have a lot of files named like activity_unpublish_39x39.png, abc_29x29.png and etc.
I want to convert the name to activity_unpublish.png (remove _39x39)
and abc.png (remove _29x29).
Could anyone tell me how I can achieve that?
It would be better working on Mac OS X.
The following small shell script should work on Linux and also on Mac OS. Note that it's working in the current folder, further you have to change pat and suf to your needs (here suf="\.png" and pat="_[0-9]+x[0-9]+$suf" to work with your given example).
It uses sed with -E which is undocumented in the manpage. It's the option to go in Mac OS which is known as -r in Linux. In Linux it is also existent but as said not documented:
#!/bin/sh
suf="\.png"
pat="_[0-9]+x[0-9]+$suf"
for f in *; do
if [[ $f =~ $pat ]]; then
newName=$(echo "$f" | sed -E "s/$pat/$suf/g")
mv "$f" "$newName"
fi
done
I got the answer from my kind colleague.
Use this shell script.
#!/bin/sh
for file in *_[0-9]*x[0-9]*.png
do
mv $file $(echo $file | sed 's/_[0-9]*x[0-9]*//')
done

sed throws bad flag in substitute command: 'l' in Mac [duplicate]

I've successfully used the following sed command to search/replace text in Linux:
sed -i 's/old_link/new_link/g' *
However, when I try it on my Mac OS X, I get:
"command c expects \ followed by text"
I thought my Mac runs a normal BASH shell. What's up?
EDIT:
According to #High Performance, this is due to Mac sed being of a different (BSD) flavor, so my question would therefore be how do I replicate this command in BSD sed?
EDIT:
Here is an actual example that causes this:
sed -i 's/hello/gbye/g' *
If you use the -i option you need to provide an extension for your backups.
If you have:
File1.txt
File2.cfg
The command (note the lack of space between -i and '' and the -e to make it work on new versions of Mac and on GNU):
sed -i'.original' -e 's/old_link/new_link/g' *
Create 2 backup files like:
File1.txt.original
File2.cfg.original
There is no portable way to avoid making backup files because it is impossible to find a mix of sed commands that works on all cases:
sed -i -e ... - does not work on OS X as it creates -e backups
sed -i'' -e ... - does not work on OS X 10.6 but works on 10.9+
sed -i '' -e ... - not working on GNU
Note Given that there isn't a sed command working on all platforms, you can try to use another command to achieve the same result.
E.g., perl -i -pe's/old_link/new_link/g' *
I believe on OS X when you use -i an extension for the backup files is required. Try:
sed -i .bak 's/hello/gbye/g' *
Using GNU sed the extension is optional.
This works with both GNU and BSD versions of sed:
sed -i'' -e 's/old_link/new_link/g' *
or with backup:
sed -i'.bak' -e 's/old_link/new_link/g' *
Note missing space after -i option! (Necessary for GNU sed)
Had the same problem in Mac and solved it with brew:
brew install gnu-sed
and use as
gsed SED_COMMAND
you can set as well set sed as alias to gsed (if you want):
alias sed=gsed
Or, you can install the GNU version of sed in your Mac, called gsed, and use it using the standard Linux syntax.
For that, install gsed using ports (if you don't have it, get it at http://www.macports.org/) by running sudo port install gsed. Then, you can run sed -i 's/old_link/new_link/g' *
Your Mac does indeed run a BASH shell, but this is more a question of which implementation of sed you are dealing with. On a Mac sed comes from BSD and is subtly different from the sed you might find on a typical Linux box. I suggest you man sed.
Insead of calling sed with sed, I do ./bin/sed
And this is the wrapper script in my ~/project/bin/sed
#!/bin/bash
if [[ "$OSTYPE" == "darwin"* ]]; then
exec "gsed" "$#"
else
exec "sed" "$#"
fi
Don't forget to chmod 755 the wrapper script.
Sinetris' answer is right, but I use this with find command to be more specific about what files I want to change. In general this should work (tested on osx /bin/bash):
find . -name "*.smth" -exec sed -i '' 's/text1/text2/g' {} \;
In general when using sed without find in complex projects is less efficient.
I've created a function to handle sed difference between MacOS (tested on MacOS 10.12) and other OS:
OS=`uname`
# $(replace_in_file pattern file)
function replace_in_file() {
if [ "$OS" = 'Darwin' ]; then
# for MacOS
sed -i '' -e "$1" "$2"
else
# for Linux and Windows
sed -i'' -e "$1" "$2"
fi
}
Usage:
$(replace_in_file 's,MASTER_HOST.*,MASTER_HOST='"$MASTER_IP"',' "./mysql/.env")
Where:
, is a delimeter
's,MASTER_HOST.*,MASTER_HOST='"$MASTER_IP"',' is pattern
"./mysql/.env" is path to file
As the other answers indicate, there is not a way to use sed portably across OS X and Linux without making backup files. So, I instead used this Ruby one-liner to do so:
ruby -pi -e "sub(/ $/, '')" ./config/locales/*.yml
In my case, I needed to call it from a rake task (i.e., inside a Ruby script), so I used this additional level of quoting:
sh %q{ruby -pi -e "sub(/ $/, '')" ./config/locales/*.yml}
Here's how to apply environment variables to template file (no backup need).
1. Create template with {{FOO}} for later replace.
echo "Hello {{FOO}}" > foo.conf.tmpl
2. Replace {{FOO}} with FOO variable and output to new foo.conf file
FOO="world" && sed -e "s/{{FOO}}/$FOO/g" foo.conf.tmpl > foo.conf
Working both macOS 10.12.4 and Ubuntu 14.04.5
Here is an option in bash scripts:
#!/bin/bash
GO_OS=${GO_OS:-"linux"}
function detect_os {
# Detect the OS name
case "$(uname -s)" in
Darwin)
host_os=darwin
;;
Linux)
host_os=linux
;;
*)
echo "Unsupported host OS. Must be Linux or Mac OS X." >&2
exit 1
;;
esac
GO_OS="${host_os}"
}
detect_os
if [ "${GO_OS}" == "darwin" ]; then
sed -i '' -e ...
else
sed -i -e ...
fi
sed -ie 's/old_link/new_link/g' *
Works on both BSD & Linux with gnu sed

zsh script [process complete] not returning back to shell

I wrote a zsh function to help me do some grepping at my job.
function rgrep (){
if [ -n "$1" ] && [ -n "$2" ]
then
exec grep -rnw $1 -r $2
elif [ -n "$1" ]
then
exec grep -rnw $1 -r "./"
else
echo "please enter one or two args"
fi
}
Works great, however, grep finishes executing I don't get thrown back into the shell. it just hangs at [process complete] any ideas?
I have the function in my .zshrc
In addition to getting rid of the unnecessary exec, you can remove the if statement as well.
function rgrep (){
grep -rwn "${1:?please enter one or two args}" -r "${2:-./}"
}
If $1 is not set (or null valued), an error will be raised and the given message displayed. If $2 is not set, a default value of ./ will be used in its place.
Do not use exec as it replace the existing shell.
exec [-cl] [-a name] [command [arguments]]
If command is supplied, it replaces the shell without creating a new process. If the -l option is supplied, the shell places a dash at the beginning of the zeroth argument passed to command. This is what the login program does. The -c option causes command to be executed with an empty environment. If -a is supplied, the shell passes name as the zeroth argument to command. If no command is specified, redirections may be used to affect the current shell environment. If there are no redirection errors, the return status is zero; otherwise the return status is non-zero.
Try this instead:
rgrep ()
{
if [ -n "$1" ] && [ -n "$2" ]
then
grep -rnw "$1" -r "$2"
elif [ -n "$1" ]
then
grep -rnw "$1" -r "./"
else
echo "please enter one or two args"
fi
}
As a completely different approach, I like to build command shortcuts like this as minimal shell scripts, rather than functions (or aliases):
% echo 'grep -rwn "$#"' >rgrep
% chmod +x rgrep
% ./rgrep
Usage: grep [OPTION]... PATTERN [FILE]...
Try `grep --help' for more information.
%
(This relies on a traditional behavior of Unix: executable text files without #! lines are considered shell scripts and are executed by /bin/sh. If that doesn't work on your system, or you need to run specifically under zsh, use an appropriate #! line.)
One of the main benefits of this approach is that shell scripts in a directory in your PATH are full citizens of the environment, not local to the current shell like functions and aliases. This means they can be used in situations where only executable files are viable commands, such as xargs, sudo, or remote invocation via ssh.
This doesn't provide the ability to give default arguments (or not easily, anyway), but IMAO the benefits outweigh the drawbacks. (And in the specific case of defaulting grep to search PWD recursively, the real solution is to install ack.)

Resources