2

I am to program a simple shell in C for my project that can implement environment variables. I looked up on how to use the getenv, setenv, putenv. So far so good i've tried to use the getenv to show the shell variables...well... with some succes . But I have a feeling that my reasoning is flawed. I have a char** argv which contains parsed input from the user input. I now check if argv starts with the command "echo" and then if any of the following inputs starts with a $ sign or not. Here's my code:

int executeVariables(char** arguments){
    int i = 0;
    if(strcmp(arguments[i], "echo") == 0){
        char *variable;
        for(i = 1; arguments[i] != NULL; i++){
            char *str = arguments[i];
            if( *(str + 0) == '$'){
                variable = getenv(str + 1);
            }else{
                variable = getenv(str);
            }
            if(!variable){
                //puts("not a variable");
                printf("%s ", arguments[i]);
            }else{
                //puts("a variable");
                printf("%s ", variable);
            }
        }
        printf("\n");
        exit(0);
    }

    return 1;

}

I think that normal linux shell finds the $ sign, it expands the variable before invoking the echo command. My shell isn't following this principle, it's expanding variables inside the echo command itself. Any idea as to how I can implement this? Thanks.

EDIT:

A problem I have is: echo $HOME and echo HOME gives me the same result which is wrong.

EDIT:

After various tests everything works well. But to really test it i'll need to create a local variable then echo this value. I tried it using putenv function but it doesn't create the local variable.

i = 0;
char** temp = malloc(sizeof (*temp));
if(strstr(userInput, "=") != NULL){
    //puts("we got equals");
    puts(userInput);
    if(putenv(userInput) == 0){
       printf("doing putenv(%s)\n", userInput);
        exit(0);
    }
    else{
        puts("couldnt putenv");
       exit(1);
    }
}

userInput: char *userInput is the input gotten from the command line using fgets()

mkab
  • 933
  • 4
  • 16
  • 31
  • What issues are you having? It looks like your code will achieve your desired effect, no? – Dan Fego Nov 30 '11 at 15:34
  • @DanFego Not exactly: `echo $HOME` and `echo HOME` gives me the same result which is wrong. – mkab Nov 30 '11 at 15:41
  • Does putenv() fail, or return 0? – Dan Fego Nov 30 '11 at 16:54
  • For the record, this whole question should probably have been split off, but since we're still here, I'll edit my answer. :P – Dan Fego Nov 30 '11 at 16:59
  • Oh wait, shouldn't putenv() be taking userInput, not *userInput? – Dan Fego Nov 30 '11 at 17:01
  • Thanks Dan :). Since you where here. I didn't want to take the whole trouble of creating another question =P. Yeah sorry putenv() takes userInput. Edited it. But that's not the problem. – mkab Nov 30 '11 at 17:05
  • I just noticed that your putenv program exits. putenv() will put the variable in the environment of that process, but it will be gone once it exits. You need to pass the value somehow to your parent (shell) process, and have that do a putenv(). That way, it will be set for the shell and all of its future children. – Dan Fego Nov 30 '11 at 17:16
  • Basically my shell gets the userInput. I now parse this input into an `**argv`, forks() a process and execute `**argv` in the child process using `execvp`. So if I have a userInput like `MYVAR=myvalue`, execvp would surely fail and then I call the putenv() function. So do you mean I should call the putenv in the parent process? – mkab Nov 30 '11 at 17:35
  • Yes. :) If you can check for non-command (i.e. environment setting or invalid syntax) before forking, that would work. Then if it's an environment-set, like you're looking for, just set it in that process. – Dan Fego Nov 30 '11 at 17:47
  • @dan: So if I understand you well, I should, for example, create a function which checks for environment setting or invalid syntax, then if it's an environment-set, I fork a process and call putenv in it? I'm not sure I get it well. – mkab Nov 30 '11 at 19:51

2 Answers2

4

You're specifically asking the code to do getenv() for the string, even if $ isn't found. That's why it will lookup $HOME or HOME. Just remove the else case for not finding the dollar-sign, and make sure to initialize variable to NULL at its declaration, and put it inside the loop.

Something like so:

// First, perform replacements
int executeVariables(char** arguments){
    int i = 0;

    for(i = 0; arguments[i] != NULL; i++){
        char *str = arguments[i];

        if(*str == '$'){
            // make sure the result isn't NULL, though I'm not sure a real shell does
            char *tmp = getenv(str + 1);
            if (tmp != NULL) {
                arguments[i] = getenv(str + 1); // save off the argument
            }
        }
    }

    // Then actually execute the function. This would be like bash's echo builtin
    if (strcmp(arguments[0], "echo") == 0) {
        int i;
        for (i = 1; arguments[i] != NULL; i++) {
            printf("%s ", arguments[i]);
        }
        printf("\n");
    }

    // Other functions could go here

    return 1;
}

Edit: As far as your methodology goes, why do you specifically check for echo? Why not make it generic, and check all the arguments, including the first one? You actually probably want to substitute all the potential environment variables, so if you had MYECHO=echo somewhere, the following would work. To make it more generic, you'd have this function, which would then execute the stuff based on the expanded variables. You could make it all nice and have separate functions, but to fit it all in here, I've updated the code above accordingly, though I haven't tested it. ;)

Edit: That being said, werewindle's comment about doing this earlier does apply -- replace the arguments, like here, and then use that updated arguments array to have a separate function do whatever it needs to do. :)

> $MYVAR totally awesome $HOME
totally awesome /home/user

Edit: As for the putenv() situation, you'll want something structured like the following. This way, it will set it for the shell, and any other processes you run in the shell.

void do_args_env(char *args[])
{
    // do putenv, etc.
}

// inside main loop in shell process
while (1) { // just an example
    if (check_args_syntax(args) != 0) {
        // error
    }

    do_args_env(args);

    // fork and do other stuff
}

(Hopefully final) Edit: As an explanation, processes generally don't (perhaps can't?) affect the environment of processes above them in their hierarchy; only the other way around. So if you putenv() in a child, that child's siblings (i.e. other processes forked from its parent) won't get the environment change.

Glad I could be of help!

Dan Fego
  • 13,644
  • 6
  • 48
  • 59
  • Thanks for the help. It solved the problem. I don't know why I even kept the `else` condition. Anyway do you think my reasoning is right? I mean i'm not following the normal shell behaviour. – mkab Nov 30 '11 at 16:00
  • Exaclty. That's my problem. How do I go about doing that? I want to make it generic. Will removing the first `if` condition and then searching each arguments contains a dollar sign and then substituing the `argv` by the right value, then using `execvp(*argv, argv)` do? – mkab Nov 30 '11 at 16:07
  • Updated the code, but werewindle's comment below points you in the right direction. Have your arguments array, perform replacements as its own action, then do the rest. – Dan Fego Nov 30 '11 at 16:24
  • Thanks. let me try it. I'll update my question with the new code. – mkab Nov 30 '11 at 16:32
  • After various tests everything works well. But to really test it i'll need to create a local variable. I tried it using `putenv` function but it doesn't create the local variable. Please check my edited question. – mkab Nov 30 '11 at 16:48
  • By "local variable," do you mean an environment variable? You can use export before calling your shell program, and that should work, like "export MYVAR=myvalue" on the command-line. – Dan Fego Nov 30 '11 at 16:53
  • No. without the use of export. I mean in the linux shell, if you type `MYVAR=myvalue` then `echo $MYVAR`, you have `myvalue` as the answer. Hoe do I implement that? By the way export is shell builtin isn't it? – mkab Nov 30 '11 at 16:57
  • So if I understand you well, I should, for example, create a function which checks for environment setting or invalid syntax, then if it's an environment-set, I fork a process and call putenv in it? I'm not sure I get it well. – mkab Nov 30 '11 at 20:11
  • Yeaaaaaaaaaaaaaahhh!! Finally got it working! The real trick was to do it outside the child process(i.e before forking) as your pseudo code suggests. Guess I have to read a lot more on the `fork()` function. Thanks a lot...you have been of great help! Learnt a lot! :D. – mkab Nov 30 '11 at 21:04
1

Yes, normal shells expand variables during parsing command line. So you need substitute variables in function, that produces array that "contains parsed input from the user input". In this case code for echo (and other commands) will be much shorter.

werewindle
  • 3,009
  • 17
  • 27
  • Sorry I didn't completely understand your second sentence. – mkab Nov 30 '11 at 15:46
  • When you parsing user input (like "echo my home at $HOME") you must substitute $HOME with actual variable value. So during calling `executeVariables`, in `arguments` will be actual data to be processed (i.e. "echo","my","home","at", "/root"). All other shells works in this way. – werewindle Nov 30 '11 at 15:56
  • Sorry, I thought, you asked how to better substitute environment variables. To fix your code, just replace `variable = getenv(str);` with `variable = NULL`. – werewindle Nov 30 '11 at 16:00
  • Ok thanks. Solved that. And if i want to better substitute environment variables? :) – mkab Nov 30 '11 at 16:05
  • 2
    Then you should substitute them earlier :) . Or you quickly find out, that you have the same code that substitutes environment variables during processing any shell command. Also, don't forget, that there are other syntax for variables, like ${MYVAR}. I suppose to create special function `subs_env(char *str)`, that will replace all possible variables with their values in `str` in early steps of parsing user input. – werewindle Nov 30 '11 at 16:13
  • Yeah that's a nice idea. Doing it earlier would be better. Let me try it. I'll update my question with the new code. – mkab Nov 30 '11 at 16:31
  • After various tests everything works well. But to really test it i'll need to create a local variable and `echo` it. I tried it using `putenv` function but it doesn't create the local variable. Please check my edited question – mkab Nov 30 '11 at 16:53
  • The actual substitutions made by shells are more complex: http://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_06 – ninjalj Nov 30 '11 at 20:20
  • @ninjalj: Nice link you got there. Thanks – mkab Nov 30 '11 at 21:06
  • @mkab: that's the current POSIX standard http://pubs.opengroup.org/onlinepubs/9699919799/ . – ninjalj Nov 30 '11 at 21:28