0

I just started learning Bash scripting and wrote a little something:

#!bin/bash
for str in stra strb strc
do
find . -name "*${str}*" | sort | cut -c3- > "${str}.list"
done

As you can see, I'm trying to create three files ("stra.list", "strb.list" and "strc.list") which would list the names of the files containing "stra", "strb", or "strc" respectively in the current directory. The cut -c3- hack is just for getting rid of the path name ./ at the beginning of find results.

But all my script does right now is creating three empty files...

So when I run

for str in stra strb strc;
do
echo "find .-name "*${str}*" | sort | cut -c3- > "${str}.list"";
done

I only see

find .-name *stra* | sort | cut -c3- > stra.list
find .-name *strb* | sort | cut -c3- > strb.list
find .-name *strc* | sort | cut -c3- > strc.list

So how can I retain the quotes around the expressions containing the variables after the expansion? I tried putting an extra set of quotes as well as using eval, but neither seems to work.


Update: What I'm asking is how I can write my find command in such a way that Bash would successfully produce the three lists with the target file names, instead of for whatever reason just creating three blank lists. Sorry about the confusion.

ladiesman
  • 39
  • 6
  • 1
    ...so, the quotes disappearing is just a problem with your `echo`; it doesn't impact the actual `find` command at all. "Don't use echo for debugging" is probably the first lesson to learn here. – Charles Duffy Jul 15 '15 at 17:36
  • 1
    To be complete -- the reasons the quotes disappear is that they're used up in syntactic parsing, and that's the behavior you want to happen. If you run `cat "filename with spaces"`, the quotes aren't part of the filename, so you don't want them passed to `cat` (presumably no file with literal quotes in its name exists); instead, they're an instruction to the shell about how to parse the command (to keep `filename with spaces` as one argument to `cat` instead of splitting it into three, which would tell `cat` to read three files: `filename`, `with`, and `spaces`). – Charles Duffy Jul 15 '15 at 17:48

2 Answers2

1

If your goal is to generate valid shell commands, then simply trying to preserve quotes is doing it wrong (in the sense that it's actually insecure; maliciously generated variable contents can perform shell injection attacks). Instead, tell the shell itself to generate valid quoting for the content you want to preserve; printf %q will do this in bash.

This particular variant requires a new enough bash to have printf -v.

#!/bin/bash
for str in stra strb strc; do
  printf -v start '%q ' find . -name "*${str}*"
  printf -v end '%q ' "${str}.list"
  printf '%s\n' "$start | sort | cut -c3- >$end"
done

By contrast, if you simply want to fix the bugs in your original script, assuming you have GNU find:

#!/bin/bash
for str in stra strb strc; do
  find . -maxdepth 1 -name "*${str}*" -printf '%P\n' | sort > "${str}.list"
done

The find action -printf '%P\n' prints the name without the starting directory, meaning no ./ is present to need to be stripped.


Since you say you're only looking for files in the current directory, by the way, this whole mess is overkill. You don't need find for the job at all.

for str in stra strb strc; do
  files=( *"$str"* )
  [[ -e $files ]] && printf '%s\n' "${files[@]}" >"$str.list"
done

Note that output from this command can be misleading if any of your filenames contain literal newlines. (For this reason, storing filenames in newline-delimited files is a bad idea to start with).

Charles Duffy
  • 280,126
  • 43
  • 390
  • 441
  • Sorry for the confusion...actually my purpose is to **simply write a functional shell script**, in my case actually produce the three (not empty!) lists; the `echo` thing was just my effort to debug and see what went wrong. – ladiesman Jul 15 '15 at 17:32
  • Don't use `echo` -- its output is often actively misleading. (See the POSIX spec for echo at http://pubs.opengroup.org/onlinepubs/009604599/utilities/echo.html, including RATIONALE and APPLICATION USAGE sections, to see just how misleading). `set -x` is a better tool for debugging. – Charles Duffy Jul 15 '15 at 17:33
  • 1
    The advice at http://unix.stackexchange.com/questions/155551/how-to-debug-a-bash-script is a good place to start. – Charles Duffy Jul 15 '15 at 17:35
  • Thanks @Charles, this has been very helpful! Would you care to explain a bit about what your `[[ -e $files ]] && printf '%s\n' "${files[@]}" >"$str.list"` is actually doing for noobs like me? – ladiesman Jul 15 '15 at 17:40
  • 1
    `[[ -e $files ]]` checks whether the first entry in the array `files` exists; it's a hack that works to check whether any files were found because globs that match no files expand to either themselves (by default) or an empty result (if the shell option nullglob is set). `printf '%s\n' "${files[@]}"` expands the array `files` and passes it as a list of arguments to `printf`. `printf` applies its subsequent arguments to the format string given as its first argument. – Charles Duffy Jul 15 '15 at 17:41
  • If you want to know more about bash arrays in general, see BashFAQ #5: http://mywiki.wooledge.org/BashFAQ/005 – Charles Duffy Jul 15 '15 at 17:42
0

There are two ways:

  1. Enclose your echo output with single quotes instead of double quotes:

    echo 'find .-name "*${str}*" | sort | cut -c3- > "${str}.list"';  
    
  2. or put a \ (backslash) in front of your inner quotes:

    echo "find .-name \"*${str}*\" | sort | cut -c3- > \"${str}.list\"";
    
Klaus
  • 538
  • 8
  • 26
  • This answers the literal question, but it's dangerous from a security perspective if the shell commands generated by this code are going to be run. (It's also useless from a debugging perspective, since the generated commands aren't guaranteed to parse identically to what the OP is trying to actually run). – Charles Duffy Jul 15 '15 at 17:28