You have two broad approaches.
You can use multiple effects, making multiple draw calls per model and switching effects between each. This is straightforward and would probably handle your simple multi-texturing and ambient shader combinations, and others like it, fairly easily. The trick is that you have to adjust the pipeline's blend mode between each shader (and possibly also modify the alpha outputs in the shaders) in order not to simply stamp over a previous draw's results. Because blend modes are not very configurable, this becomes constricting very fast.
You can use a single effect, with everything you want it to do combined into a single shader. This is basically what you were assuming was the answer, but you can do it in an achievable fashion. The trick is to split up the effect early, before it is an actual shader.
The first approach is cumbersome and I won't speak on it further. The second approach is more common these days, and there are a variety of ways to implement it.
The super-shader (or "ubershader") approach was popular for a while. This method basically involves you, the programmer, writing a single massive shader that "does everything," but with particular bits of the shader's functionality wrapped in preprocessor macros or some other kind of guard. For example:
color = input.color;
color = ComputeDiffuseLighting(color, normal, light_position);
color = (0.5 * color) + (0.5 * ComputeTextureColor(texture_coordinates))
Then you can create multiple actual shaders by recompiling this super-shader with various pieces turned on or off (you can either brute-force all permutations or you can intelligently preprocess all your materials to determine which combinations would be required). This way you always have a broad overview, as a programmer, of all the possible ways in which your different shader features would interact, but you also don't have to manually re-create tons of variations on very similar shaders. Here's another paper on the subject.
These days the hot trend is visual shader graphs and editors, such as ShaderForge for Unity.
The basic premise behind this approach (which is my favorite, with some caveats), is similar to the super-shader approach in that you are ultimately one final shader out of pieces. Instead, however, you the programmer author each small shader fragment independently, usually in some intermediate language or data structure of your own choosing (so you can understand the input and output variables of each fragment, and so on). Then you create a tool that allows an artist (or yourself) to string together these isolated fragments however you choose, and you post-process that graph (usually via a topological sort) into a final effect file you can feed your graphics API.
McGuire's Abstract Shade Trees paper is a good read on one way to build a shader tree system.
An interesting point about the shader tree approach is that you can choose the granularity of the nodes in your system. In some systems, nodes are almost always simple operations like "add" or "interpolate" or similar. In others, they can be very high level such as "bump map" or "cel shading." Or you can choose an in-between approach. The technique is particularly well suited to data-driven iteration, which is really nice, but it also requires a much higher up-front investment of time and technology before you can really utilize it, especially compared to the super-shader approach which can be done with a few crude regular expressions or text processing code fragments even if you don't have an actual C-style preprocessor available to you.