800

I need to write a script that starts my program with different arguments. I start my program with:

./MyProgram.exe Data/data1.txt [Logs/data1_Log.txt].

Here is the pseudocode for what I want to do:

for each filename in /Data do
  for int i = 0, i = 3, i++
    ./MyProgram.exe Data/filename.txt Logs/filename_Log{i}.txt
  end for
end for

How can I create the second argument from the first one, so it looks like dataABCD_Log1.txt and start my program?

Benjamin Loison
  • 3,782
  • 4
  • 16
  • 33
Dobrobobr
  • 8,216
  • 4
  • 17
  • 18
  • Possible duplicate of: https://stackoverflow.com/questions/1732861/linux-iterate-over-files-in-directory – Léa Gris Dec 19 '21 at 11:22
  • 3
    @LéaGris The proposed duplicate seems less stellar, especially as one of the answers there still advocates looping over `ls` output. These seem different enough that I have not nominated that as a duplicate of this, either. – tripleee Dec 20 '21 at 07:42

6 Answers6

1082

A couple of notes first: when you use Data/data1.txt as an argument, should it really be /Data/data1.txt (with a leading slash)? Also, should the outer loop scan only for .txt files, or all files in /Data? Here's an answer, assuming /Data/data1.txt and .txt files only:

#!/bin/bash
for filename in /Data/*.txt; do
    for ((i=0; i<=3; i++)); do
        ./MyProgram.exe "$filename" "Logs/$(basename "$filename" .txt)_Log$i.txt"
    done
done

Notes:

  • /Data/*.txt expands to the paths of the text files in /Data (including the /Data/ part)
  • $( ... ) runs a shell command and inserts its output at that point in the command line
  • basename somepath .txt outputs the base part of somepath, with .txt removed from the end (e.g. /Data/file.txt -> file)

If you needed to run MyProgram with Data/file.txt instead of /Data/file.txt, use "${filename#/}" to remove the leading slash. On the other hand, if it's really Data not /Data you want to scan, just use for filename in Data/*.txt.

Jack G
  • 4,553
  • 2
  • 41
  • 50
Gordon Davisson
  • 118,432
  • 16
  • 123
  • 151
  • 1
    `basename -s` does not work. I was able to run the script by changing it to `basename "$filename" .txt`, now it works at intended. P.S. I'm using cygwin so it can be the case. – Dobrobobr Dec 27 '13 at 09:23
  • 1
    Apparently `basename -s` is a nonstandard extension -- I'll edit my answer to use the standard syntax. – Gordon Davisson Dec 27 '13 at 16:23
  • 40
    If no files are found/match the wildcard I'm finding the for loops execute block is still entered once with filename = "/Data/*.txt". How can I avoid this? – Oliver Pearmain Feb 03 '15 at 15:22
  • 40
    @OliverPearmain Either use `shopt -s nullglob` before the loop (and `shopt -u nullglob` after to avoid problems later on), or add `if [[ ! -e "$filename ]]; then continue; fi` at the beginning of the loop, so it'll skip nonexistent files. – Gordon Davisson Feb 03 '15 at 19:01
  • 2
    Perfect `shopt -s nullglob` works great. Thanks for the info and prompt response. – Oliver Pearmain Feb 04 '15 at 09:23
  • 16
    This doesn't work when there are files which contain whitespace in their name. – Isa Hassen Jan 24 '16 at 20:29
  • 8
    @Isa It should work with whitespace, as long as all of the double-quotes are in place. Leave any of the double-quotes out, and you'll have problems with whitespace. – Gordon Davisson Jan 25 '16 at 00:01
  • Please note, that it by default does *not* match hidden files. To match hidden files as well, try `/Data/* /Data/.[!.]*`, see https://unix.stackexchange.com/questions/186214/how-to-match-with-hidden-files-inside-a-directory – John Mar 01 '23 at 12:24
  • Is it possible to replace the path on line 2 (`/Data/*.txt` by a string `$THEPATH` defined somewhere before in the script (`THEPATH="/Data/*.txt"`)? – ecjb Apr 15 '23 at 09:24
  • 1
    @ecjb Yes, but to get the wildcard to expand to a list of filenames, you'd have to leave the variable reference unquoted (`for filename in $THEPATH; do`, rather than `for filename in "$THEPATH"; do`), which can cause trouble if there are spaces in filenames in the path. If possible, it's better to keep the wildcards separate from the variable part so you can quote only the variable part (e.g. `THEPATH="/Data; for filename in "$THEPATH"/*.txt; do`). Other workarounds are possible, depending on the specific situation. – Gordon Davisson Apr 15 '23 at 17:05
476

Whenever you iterate over files by globbing, it's good practice to avoid the corner case where the glob does not match (which makes the loop variable expand to the (un-matching) glob pattern string itself).

For example:

for filename in Data/*.txt; do
    [ -e "$filename" ] || continue
    # ... rest of the loop body
done

Reference: Bash Pitfalls

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Cong Ma
  • 10,692
  • 3
  • 31
  • 47
  • 11
    This is still a timely warning. I thought I had created my script incorrectly, but I had my file extension lower case instead of upper case, it found no files, and returned the glob pattern. ugh. – RufusVS Sep 14 '17 at 21:13
  • 10
    Since the Bash tag is used: that's what [`shopt nullglob`](https://www.gnu.org/software/bash/manual/bashref.html#The-Shopt-Builtin) is for! (or `shopt failglob` can be used too, depending on the behavior you want). – gniourf_gniourf Dec 18 '17 at 17:46
  • Besides, this also checks for files deleted during processing, before the loop reaches them. – loxaxs Jan 01 '18 at 22:26
  • 2
    Also handles the case where you have a directory called `dir.txt` – vidstige May 03 '18 at 23:41
  • 1
    Don't be sorry; the whole purpose of Stack Overflow is to collect and curate canonical questions rather than have new users reask the same old questions. – tripleee Dec 20 '21 at 07:40
108
for file in Data/*.txt
do
    for ((i = 0; i < 3; i++))
    do
        name=${file##*/}
        base=${name%.txt}
        ./MyProgram.exe "$file" Logs/"${base}_Log$i.txt"
    done
done

The name=${file##*/} substitution (shell parameter expansion) removes the leading pathname up to the last /.

The base=${name%.txt} substitution removes the trailing .txt. It's a bit trickier if the extensions can vary.

Jonathan Leffler
  • 730,956
  • 141
  • 904
  • 1,278
  • 4
    I believe there's an error in your code. The one line should be `base=${name%.txt}`, instead of `base=${base%.txt}`. – kck May 15 '14 at 01:16
  • 5
    @CaseyKlimkowsky: Yes; when the code and the comments disagree, at least one of them is wrong. In this case, I think it is only the one — the code; often, it is actually both that are wrong. Thanks for pointing that out; I've fixed it. – Jonathan Leffler May 15 '14 at 01:21
  • 1
    Rather than `name=${file##*/}` you can use `name=\`basename $file\`` – Tk421 Oct 18 '20 at 23:08
  • 1
    @Tk421 — Sorta. You should use `$(…)` instead of back ticks `\`…\``. And using `basename` runs a process whereas the `${file##*/}` notation is run by the shell without an external process. Functionally, the two are equivalent except perhaps in extreme edge cases. But the shell variable expansion should be more efficient ff, and possibly even measurably more efficient. – Jonathan Leffler Apr 15 '22 at 19:00
  • correct substitution: `base=${file%%$name}` – olegtaranenko Jun 10 '23 at 00:45
35

You can use find's null-separated output option with read to iterate over directory structures safely.

#!/bin/bash
find . -type f -print0 | while IFS= read -r -d $'\0' file;
  do echo "$file" ;
done

So for your case,

#!/bin/bash
find . -maxdepth 1 -type f  -print0 | while IFS= read -r -d $'\0' file; do
  for ((i=0; i<=3; i++)); do
    ./MyProgram.exe "$file" 'Logs/'"`basename "$file"`""$i"'.txt'
  done
done

Additionally,

#!/bin/bash
while IFS= read -r -d $'\0' file; do
  for ((i=0; i<=3; i++)); do
    ./MyProgram.exe "$file" 'Logs/'"`basename "$file"`""$i"'.txt'
  done
done < <(find . -maxdepth 1 -type f  -print0)

will run the while loop in the current scope of the script (process) and allows the output of find to be used in setting variables, if needed.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Tegra Detra
  • 24,551
  • 17
  • 53
  • 78
  • 2
    `$'\0'` is a weird way of writing `''`. You're missing `IFS=` and the `-r` switch to `read`: your read statement should be: `IFS= read -rd '' file`. – gniourf_gniourf Feb 08 '19 at 11:31
  • I figure some would need search `$'\0'` and spread some stack points around. Going to make the edits you pointed out. What are the ill effects of not having `IFS=` trying `echo -e "ok \nok\0" | while read -d '' line; do echo -e "$line"; done` there seem not be any. Also -r I see is often default, but could not find an example for what it prevents happening. – Tegra Detra Feb 09 '19 at 22:56
  • 7
    `IFS=` is needed in case a filename ends with a space: try is with `touch 'Prospero '` (note the trailing space). Also you need the `-r` switch in case a file name has a backslash: try it with `touch 'Prospero\n'`. – gniourf_gniourf Feb 09 '19 at 23:02
  • 4
    Underrated answer. Try the most upvoted ones on a directory containing more than 100k files. Good luck if you're on a low-end machine. – confetti Jun 14 '20 at 08:10
9

Run a command on each file:

Do something (echo) with all .txt files,

for f in *.txt; do echo ${f}; done;
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
  • 1
    Do you care to read what other said? 1. your answer doesn't have double-quote expansions! 2. what will happen when no .txt files are present? – user894319twitter Aug 10 '22 at 07:58
-4

It looks like you're trying to execute a Windows file (.exe). Surely you ought to be using PowerShell. Anyway, on a Linux Bash shell a simple one-liner will suffice.

[/home/$] for filename in /Data/*.txt; do for i in {0..3}; do ./MyProgam.exe  Data/filenameLogs/$filename_log$i.txt; done done

Or in a Bash script:

#!/bin/bash

for filename in /Data/*.txt;
   do
     for i in {0..3};
       do ./MyProgam.exe Data/filename.txt Logs/$filename_log$i.txt;
     done
 done
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
user3183111
  • 194
  • 1
  • 7