2

Here is the context: I'm making a small Linux command for me to automatically create the base code when I create a C program. The name of my command is ctouch: it's like the touch command but after creating the file(s) it also creates the basic structure of a C program:

#include <stdio.h>
#include <stdlib.h>

int main(){



    return 0:
}

Appending these lines works. Here is my code:

#! /bin/bash

for arg in "$@"
do
    [ ${arg: -2} == ".c" ] && touch "$arg" || touch "$arg.c"

    echo '#include <stdio.h>' > "$arg"
    echo '#include <stdlib.h>' >> "$arg"
    echo $'\nint main90{\n\n\t' >> "$arg"
    echo $'\n\treturn 0;\n\n}' >> "$arg"
done

As you see, it can create as much files as touch can take arguments. The conditional loop (the line after the do) is useful in case I forgot to specify the .c extension: but the problem is that both if and else are executed when the argument does not end in .c But it's ok when I don't "forget" to specify that it is a C file.

How can I fix that?

tripleee
  • 175,061
  • 34
  • 275
  • 318
qux
  • 55
  • 2
  • 7
  • 1
    If `$arg` doesn't contain a `.c` you create a file named `$arg.c`. Later on in your script you write to `$arg`, but we just established that `$arg` doesn't contain a `.c` so you are writing to the non-`.c` file with that redirect. I suspect this script works fine when your `$arg` contains `.c` though. – JNevill Mar 19 '19 at 15:43
  • 4
    Notice that `condition && cmd1 || cmd2` is not exactly equivalent to `if condition then cmd1; else cmd2; fi` – if `cmd1` fails, `cmd2` will be executed! (See [Bash Pitfall 22](https://mywiki.wooledge.org/BashPitfalls#cmd1_.26.26_cmd2_.7C.7C_cmd3)) – Benjamin W. Mar 19 '19 at 15:51
  • You can trade one `bash`ism for another, clearer one: `if [[ $arg == *.c ]]; then touch "$arg"; else touch "$arg.c"`. – chepner Mar 19 '19 at 16:30
  • 1
    You can also dispense with the test altogether, with a POSIX-compatible parameter expansion at that: `arg=${arg%.c}.c`. This strip `.c` if present, then puts it back whether it was there in the first place or not. – chepner Mar 19 '19 at 16:31

4 Answers4

4

It isn't taking both branches. It does the touch depending on the extension, but then the echo sequence places its output in the file without an extension if that's what you passed in.

I would refactor to use case, use a here document to print multiple lines, and skip the touch entirely; but the minimal fix is just to change the output file name if it lacks the extension.

for arg in "$@"
do
    case $arg in
      *.c) ;;
      *) arg="$arg.c";;
    esac

    cat <<'____HERE' > "$arg"
#include <stdio.h>
#include <stdlib.h>

int main(){




    return 0;
}
____HERE
done

With these changes, the code should also be portable to POSIX /bin/sh.

tripleee
  • 175,061
  • 34
  • 275
  • 318
  • probably because heredocs are easier to read and to write (and change). – umläute Mar 19 '19 at 16:30
  • Actually I used `printf` rather than a heredoc, but someone thought they could "improve" my answer. Either way, `echo` has some pesky portability problems; both `printf` and `cat` a here document avoid those elegantly. – tripleee Mar 19 '19 at 16:32
  • The `printf` answer I originally posted made tabs etc explicit; but a here document with tabs in the right places will work fine too (just not show symbolically where the tabs go), so I'll leave the edit by @JohnKugelman. – tripleee Mar 19 '19 at 16:37
1

A little alteration:

#! /bin/bash

for arg in "$@"
do
     [[ ${arg: -2} == ".c" ]] && FILE="$arg" || FILE="$arg.c"
    echo '#include <stdio.h>' > "$FILE"
    echo '#include <stdlib.h>' >> "$FILE"
    echo $'\nint main90{\n\n\t' >> "$FILE"
    echo $'\n\treturn 0;\n\n}' >> "$FILE"
done
1

A simplified solution avoids any need for a conditional. You want to create a .c file whether or not the original argument ends with .c, effectively adding it if it isn't already there. This is the same as always adding it to the result of trying to remove it. You also don't need to explicitly create the file with touch; output redirection will create the file if necessary.

This also demonstrates Yet Another Way to write the contents of your template to a file.

for arg in "$@"
do
  {
    printf '#include <stdio.h>\n'
    printf '#include <stdlib.h>\n'
    printf '\nint main90 {\n'
    printf '\n\treturn 0;\n\n}\n'  # end with a newline
  } > "${arg%.c}.c"
done

Note that your file should probably be a proper POSIX text file, ending with a newline character.

chepner
  • 497,756
  • 71
  • 530
  • 681
  • My original answer had a single `printf` so you could do away with the braces, too. (This is more elegant than the blind copy/paste of the OP's code that I had, though.) – tripleee Mar 19 '19 at 17:11
  • I toyed with that, but it gets ugly unless you use an array of lines, in which case you might as well just accept the cost of running `cat` with an here document like you used. – chepner Mar 19 '19 at 18:40
0

alternative with here document:

for arg in "$@"; do
    if [ "${arg: -2}" == ".c" ]; then
        filename="$arg"
    else
        filename="${arg}.c"
    fi

cat<<EOT >"$filename"
#include <stdio.h>
#include <stdlib.h>

int main() {


    return 0;
}
EOT

done
UtLox
  • 3,744
  • 2
  • 10
  • 13
  • Hello, thanks for your answer. Can you explain what does this cat EOF... etc, mean? Or can you give me a link that explain it? – qux Mar 19 '19 at 16:06